modules/SdnDiag.Health/SdnDiag.Health.psm1
# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. Using module .\SdnDiag.Health.Helper.psm1 Import-Module $PSScriptRoot\SdnDiag.Health.Helper.psm1 Import-Module $PSScriptRoot\..\SdnDiag.Common\SdnDiag.Common.psm1 Import-Module $PSScriptRoot\..\SdnDiag.NetworkController.FC\SdnDiag.NetworkController.FC.psm1 Import-Module $PSScriptRoot\..\SdnDiag.NetworkController.SF\SdnDiag.NetworkController.SF.psm1 Import-Module $PSScriptRoot\..\SdnDiag.Utilities\SdnDiag.Utilities.psm1 # create local variable to store configuration data $configurationData = Import-PowerShellDataFile -Path "$PSScriptRoot\SdnDiag.Health.Config.psd1" New-Variable -Name 'SdnDiagnostics_Health' -Scope 'Script' -Force -Value @{ Cache = @{} Config = $configurationData } ##### FUNCTIONS AUTO-POPULATED BELOW THIS LINE DURING BUILD ##### 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 Test-EncapOverhead { <# .SYNOPSIS Retrieves the VMSwitch across servers in the dataplane to confirm that the network interfaces support EncapOverhead or JumboPackets and that the settings are configured as expected #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) [int]$encapOverheadExpectedValue = 160 [int]$jumboPacketExpectedValue = 1674 # this is default 1514 MTU + 160 encap overhead $sdnHealthObject = [SdnHealth]::new() $array = @() try { "Validating the network interfaces across the SDN dataplane support Encap Overhead or Jumbo Packets" | Trace-Output $encapOverheadResults = Invoke-PSRemoteCommand -ComputerName $SdnEnvironmentObject.ComputerName -Credential $Credential -Scriptblock {Get-SdnNetAdapterEncapOverheadConfig} if($null -eq $encapOverheadResults){ $sdnHealthObject.Result = 'FAIL' } else { foreach($object in ($encapOverheadResults | Group-Object -Property PSComputerName)){ foreach($interface in $object.Group){ "[{0}] {1}" -f $object.Name, ($interface | Out-String -Width 4096) | Trace-Output -Level:Verbose if($interface.EncapOverheadEnabled -eq $false -or $interface.EncapOverheadValue -lt $encapOverheadExpectedValue){ "EncapOverhead settings for {0} on {1} are disabled or not configured correctly" -f $interface.NetworkInterface, $object.Name | Trace-Output -Level:Verbose $encapDisabled = $true } if($interface.JumboPacketEnabled -eq $false -or $interface.JumboPacketValue -lt $jumboPacketExpectedValue){ "JumboPacket settings for {0} on {1} are disabled or not configured correctly" -f $interface.NetworkInterface, $object.Name | Trace-Output -Level:Verbose $jumboPacketDisabled = $true } # if both encapoverhead and jumbo packets are not set, this is indication the physical network cannot support VXLAN encapsulation # and as such, environment would experience intermittent packet loss if ($encapDisabled -and $jumboPacketDisabled) { $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation += "Ensure EncapOverhead and JumboPacket for interface {0} on {1} are enabled and configured correctly." -f $interface.NetworkInterface, $object.Name "EncapOverhead and JumboPacket for interface {0} on {1} are disabled or not configured correctly." -f $interface.NetworkInterface, $object.Name | Trace-Output -Level:Error } $array += $interface } } $sdnHealthObject.Properties = $array } return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-HostRootStoreNonRootCert { <# .SYNOPSIS Validate the Cert in Host's Root CA Store to detect if any Non Root Cert exist #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() try { "Validating Certificates under Root CA Store" | Trace-Output $scriptBlock = { $nonRootCerts = @() $rootCerts = Get-ChildItem Cert:LocalMachine\Root foreach ($rootCert in $rootCerts) { if ($rootCert.Subject -ne $rootCert.Issuer) { $certInfo = [PSCustomObject]@{ Thumbprint = $rootCert.Thumbprint Subject = $rootCert.Subject Issuer = $rootCert.Issuer } $nonRootCerts += $certInfo } } return $nonRootCerts } foreach($node in $SdnEnvironmentObject.ComputerName){ $nonRootCerts = Invoke-PSRemoteCommand -ComputerName $node -Credential $Credential -ScriptBlock $scriptBlock -PassThru # If any node have Non Root Certs in Trusted Root Store. Issue detected. if($nonRootCerts.Count -gt 0){ $sdnHealthObject.Result = 'FAIL' $object = [PSCustomObject]@{ ComputerName = $node NonRootCerts = $nonRootCerts } foreach($nonRootCert in $nonRootCerts) { $sdnHealthObject.Remediation += "Remove Certificate Thumbprint:{0} Subject:{1} from Host:{2}" -f $nonRootCert.Thumbprint, $nonRootCert.Subject, $node } $array += $object } } $sdnHealthObject.Properties = $array return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-MuxBgpConnectionState { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() $netConnectionExistsScriptBlock = { param([Parameter(Position = 0)][String]$arg0) $tcpConnection = Get-NetTCPConnection -RemotePort 179 -RemoteAddress $arg0 -ErrorAction SilentlyContinue | Where-Object { $_.State -eq "Established" } if ($tcpConnection) { return $true } } try { "Validating the BGP connectivity between LoadBalancerMuxes and Top of Rack (ToR) Switches." | Trace-Output $loadBalancerMux = Get-SdnLoadBalancerMux -NcUri $SdnEnvironmentObject.NcUrl.AbsoluteUri -Credential $NcRestCredential # if no load balancer muxes configured within the environment, return back the health object to caller if ($null -ieq $loadBalancerMux) { return $sdnHealthObject } # enumerate through the load balancer muxes in the environment and validate the BGP connection state foreach ($mux in $loadBalancerMux) { $virtualServer = Get-SdnResource -NcUri $SdnEnvironmentObject.NcUrl.AbsoluteUri -ResourceRef $mux.properties.virtualServer.resourceRef -Credential $NcRestCredential [string]$virtualServerConnection = $virtualServer.properties.connections[0].managementAddresses $peerRouters = $mux.properties.routerConfiguration.peerRouterConfigurations.routerIPAddress foreach ($router in $peerRouters) { $connectionExists = Invoke-PSRemoteCommand -ComputerName $virtualServerConnection -Credential $Credential -ScriptBlock $netConnectionExistsScriptBlock -ArgumentList $router if (-NOT $connectionExists) { "{0} is not connected to {1}" -f $virtualServerConnection, $router | Trace-Output -Level:Error $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation += "Fix BGP Peering between $($virtualServerConnection) and $($router)." # create a custom object to store the load balancer mux and the router that it is not connected to # this will be added to the array $object = [PSCustomObject]@{ LoadBalancerMux = $virtualServerConnection TopOfRackSwitch = $router } $array += $object } else { "{0} is connected to {1}" -f $virtualServerConnection, $router | Trace-Output -Level:Verbose } } } # if the array is not empty, add it to the health object if ($array) { $sdnHealthObject.Properties = $array } return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-NcHostAgentConnectionToApiService { <# .SYNOPSIS Validates the TCP connection between Server and primary replica of Api service within Network Controller. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() $netConnectionExistsScriptBlock = { $tcpConnection = Get-NetTCPConnection -RemotePort 6640 -ErrorAction SilentlyContinue | Where-Object { $_.State -eq "Established" } if ($tcpConnection) { return $true } } try { "Validating connectivity between Server and primary replica of API service within Network Controller" | Trace-Output $servers = Get-SdnServer -NcUri $SdnEnvironmentObject.NcUrl.AbsoluteUri -Credential $NcRestCredential # if no load balancer muxes configured within the environment, return back the health object to caller if ($null -ieq $servers) { return $sdnHealthObject } # get the current primary replica of Network Controller # if we cannot return the primary replica, then something is critically wrong with Network Controller # in which case we should mark this test as failed and return back to the caller with guidance to fix the SlbManagerService $primaryReplicaNode = Get-SdnServiceFabricReplica -NetworkController $SdnEnvironmentObject.EnvironmentInfo.NetworkController[0] -ServiceTypeName 'ApiService' -Credential $Credential -Primary if ($null -ieq $primaryReplicaNode) { "Unable to return primary replica of ApiService" | Trace-Output -Level:Error $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation = "Fix the primary replica of ApiService within Network Controller." return $sdnHealthObject } # enumerate through the servers in the environment and validate the TCP connection state # we expect the NCHostAgent to have an active connection to ApiService within Network Controller via port 6640, which informs # Network Controller that the host is operational and ready to receive policy configuration updates foreach ($server in $servers) { [System.Array]$connectionAddress = Get-SdnServer -NcUri $SdnEnvironmentObject.NcUrl.AbsoluteUri -ResourceId $server.resourceId -ManagementAddressOnly -Credential $NcRestCredential $connectionExists = Invoke-PSRemoteCommand -ComputerName $connectionAddress[0] -Credential $Credential -ScriptBlock $netConnectionExistsScriptBlock if (-NOT $connectionExists) { "{0} is not connected to ApiService of Network Controller" -f $server.resourceRef | Trace-Output -Level:Error $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation += "Ensure NCHostAgent service is started. Investigate and fix TCP connectivity or x509 authentication between $($primaryReplicaNode.ReplicaAddress) and $($server.resourceRef)." $object = [PSCustomObject]@{ Server = $server.resourceRef ApiPrimaryReplica = $primaryReplicaNode.ReplicaAddress } $array += $object } else { "{0} is connected to {1}" -f $server.resourceRef, $primaryReplicaNode.ReplicaAddress | Trace-Output -Level:Verbose } } $sdnHealthObject.Properties = $array return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-NcUrlNameResolution { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() try { "Validate that the Network Controller NB API URL resolves to the correct IP address" | Trace-Output $ncApiReplicaPrimary = Get-SdnServiceFabricReplica -NetworkController $SdnEnvironmentObject.ComputerName[0] -Credential $Credential -ServiceTypeName 'ApiService' -Primary if ($null -eq $ncApiReplicaPrimary) { "Unable to find the primary replica for the ApiService" | Trace-Output -Level:Warning return $sdnHealthObject } $networkController = Get-SdnNetworkController -NetworkController $SdnEnvironmentObject.ComputerName[0] -Credential $Credential if ($null -eq $networkController) { "Unable to retrieve results from Get-SdnNetworkController" | Trace-Output -Level:Warning return $sdnHealthObject } # depending on the configuration returned, will determine if we need to use the RestIPAddress or RestName $nbApiName = $networkController.ServerCertificate.Subject.Split('=')[1].Trim() if ($networkController.RestIPAddress) { $expectedIPAddress = $($networkController.RestIPAddress).Split('/')[0].Trim() # we expect to be in IP/CIDR format "Network Controller is configured with static RestIPAddress: {0}" -f $expectedIPAddress | Trace-Output -Level:Verbose } else { "Network Controller is configured with RestName" | Trace-Output -Level:Verbose $ncNodeName = $ncApiReplicaPrimary.ReplicaAddress.Split(':')[0].Trim() $isIpAddress = [System.Net.IPAddress]::TryParse($ncNodeName, [ref]$null) if ($isIpAddress) { $expectedIPAddress = $ncNodeName.ToString() } else { $dnsResultNetworkControllerNode = Resolve-DnsName -Name $ncNodeName -NoHostsFile -ErrorAction SilentlyContinue if ($null -ieq $dnsResultNetworkControllerNode) { "Unable to resolve IP address for {0}" -f $ncNodeName | Trace-Output -Level:Warning return $sdnHealthObject } else { $expectedIPAddress = $dnsResultNetworkControllerNode.IPAddress "ApiService replica primary is hosted on {0} with an IP address of {1}" -f $ncApiReplicaPrimary.ReplicaAddress, $expectedIPAddress | Trace-Output -Level:Verbose } } } # in this scenario, the certificate is using an IP address as the subject, so we will need to compare the IP address to the expected IP address # if they match, we will return a success $isIpAddress = [System.Net.IPAddress]::TryParse($nbApiName, [ref]$null) if ($isIpAddress -and ($nbApiName -ieq $expectedIPAddress)) { return $sdnHealthObject } # perform some DNS resolution to ensure that the NB API URL resolves to the correct IP address $dnsResult = Resolve-DnsName -Name $nbApiName -NoHostsFile -ErrorAction SilentlyContinue if ($null -ieq $dnsResult) { $sdnHealthObject.Result = 'FAIL' "Unable to resolve DNS name for {0}" -f $nbApiName | Trace-Output -Level:Warning return $sdnHealthObject } elseif ($dnsResult[0].IPAddress -ine $expectedIPAddress) { $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation = 'Ensure that the DNS name for the Network Controller NB API URL resolves to the correct IP address.' "DNS name for {0} resolves to {1} instead of {2}" -f $nbApiName, $dnsResult[0].IPAddress, $expectedIPAddress | Trace-Output -Level:Warning return $sdnHealthObject } return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-NetworkControllerCertCredential { <# .SYNOPSIS Query the NC Cert credential used to connect to SDN Servers, ensure cert exist. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $arrayList = [System.Collections.ArrayList]::new() try { "Validate cert credential resource of SDN Servers. Ensure certificate exists on each of the Network Controller " | Trace-Output # enumerate each server's conection->credential object into the array $servers = Get-SdnServer -NcUri $SdnEnvironmentObject.NcUrl.AbsoluteUri -Credential $NcRestCredential $serverCredentialRefs = [System.Collections.Hashtable]::new() foreach ($server in $servers) { # find the first connection with credential type of X509Certificate $serverConnection = $server.properties.connections | Where-Object {$_.credentialType -ieq "X509Certificate" -or $_.credentialType -ieq "X509CertificateSubjectName"} | Select-Object -First 1; if ($null -ne $serverConnection) { $credRef = $serverConnection.credential[0].resourceRef "Adding credential {0} for server {1} for validation" -f $credRef, $serverConnection.managementAddresses[0] | Trace-Output -Level:Verbose if ($null -ne $credRef) { if (-NOT $serverCredentialRefs.ContainsKey($credRef)) { $serverList = [System.Collections.ArrayList]::new() $serverCredentialRefs.Add($credRef, $serverList) } [void]$serverCredentialRefs[$credRef].Add($server) } } } # iterate the credential object to validate certificate on each NC foreach ($credRef in $serverCredentialRefs.Keys) { $credObj = Get-SdnResource -NcUri $SdnEnvironmentObject.NcUrl.AbsoluteUri -Credential $NcRestCredential -ResourceRef $credRef if ($null -ne $credObj) { $thumbPrint = $credObj.properties.value $scriptBlock = { param([Parameter(Position = 0)][String]$param1) if (-NOT (Test-Path -Path Cert:\LocalMachine\My\$param1)) { return $false } else { return $true } } # invoke command on each NC seperately so to record which NC missing certificate foreach ($nc in $SdnEnvironmentObject.ComputerName) { "Validating certificate [{0}] on NC {1}" -f $thumbPrint, $nc | Trace-Output -Level:Verbose $result = Invoke-PSRemoteCommand -ComputerName $nc -Credential $Credential -ScriptBlock $scriptBlock -ArgumentList $thumbPrint if ($result -ne $true) { # if any NC missing certificate, it indicate issue detected $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation += "Install certificate [$thumbPrint] on Network Controller [$nc]" $object = [PSCustomObject]@{ NetworkController = $nc CertificateMissing = $thumbPrint AffectedServers = $serverCredentialRefs[$credRef] } [void]$arrayList.Add($object) } } } } $sdnHealthObject.Properties = $arrayList return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-NetworkInterfaceAPIDuplicateMacAddress { <# .SYNOPSIS Validate there are no adapters within the Network Controller Network Interfaces API that are duplicate. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() try { "Validate no duplicate MAC addresses for network interfaces in Network Controller" | Trace-Output $networkInterfaces = Get-SdnResource -NcUri $SdnEnvironmentObject.NcUrl.AbsoluteUri -Resource:NetworkInterfaces -Credential $NcRestCredential if($null -eq $networkInterfaces){ # if there are no network interfaces, then there is nothing to validate # pass back the health object to the caller return $sdnHealthObject } $duplicateObjects = $networkInterfaces.properties | Group-Object -Property privateMacAddress | Where-Object {$_.Count -ge 2} if($duplicateObjects){ $sdnHealthObject.Result = 'FAIL' # since there can be multiple grouped objects, we need to enumerate each duplicate group foreach($obj in $duplicateObjects){ $sdnHealthObject.Remediation += "Remove the duplicate MAC addresses for $($obj.Name) within Network Controller Network Interfaces" $duplicateInterfaces = $networkInterfaces | Where-Object {$_.properties.privateMacAddress -eq $obj.Name} $array += $duplicateInterfaces "Located {0} virtual machines associated with MAC address {1}:`r`n`n{2}`r`n" -f $obj.Count, $obj.Name, ` ($duplicateInterfaces ` | Select-Object @{n="ResourceRef";e={"`t$($_.resourceRef)"}} ` | Select-Object -ExpandProperty ResourceRef ` | Out-String ` ) | Trace-Output -Level:Error } } $sdnHealthObject.Properties = $array return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-ProviderNetwork { <# .SYNOPSIS Performs ICMP tests across the computers defined to confirm that jumbo packets are able to successfully traverse between the provider addresses on each host #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() try { "Validating Provider Address network has connectivity across the SDN dataplane" | Trace-Output $providerAddresses = (Get-SdnProviderAddress -ComputerName $SdnEnvironmentObject.ComputerName -Credential $Credential).ProviderAddress if ($null -eq $providerAddresses){ "No provider addresses were found on the hosts." | Trace-Output } else { $connectivityResults = Invoke-PSRemoteCommand -ComputerName $SdnEnvironmentObject.ComputerName -Credential $Credential -Scriptblock { param([Parameter(Position = 0)][String[]]$param1) Test-SdnProviderAddressConnectivity -ProviderAddress $param1 } -ArgumentList $providerAddresses foreach($computer in $connectivityResults | Group-Object PSComputerName){ foreach($destinationAddress in $computer.Group){ $jumboPacketResult = $destinationAddress | Where-Object {$_.BufferSize -gt 1472} $standardPacketResult = $destinationAddress | Where-Object {$_.BufferSize -le 1472} if($destinationAddress.Status -ine 'Success'){ $sdnHealthObject.Result = 'FAIL' # 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 = "Ensure ICMP enabled on {0} and {1}. If issue persists, investigate physical network." -f $destinationAddress[0].DestinationAddress, $destinationAddress[0].SourceAddress $sdnHealthObject.Remediation += $remediationMsg "Cannot ping {0} from {1} ({2})." ` -f $destinationAddress[0].DestinationAddress, $computer.Name, $destinationAddress[0].SourceAddress | Trace-Output -Level:Error } # 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 {0} and {1} configured to support VXLAN or NVGRE encapsulated packets with minimum MTU of 1660." ` -f $destinationAddress[0].DestinationAddress, $destinationAddress[0].SourceAddress $sdnHealthObject.Remediation += $remediationMsg "Cannot send jumbo packets to {0} from {1} ({2})." ` -f $destinationAddress[0].DestinationAddress, $computer.Name, $destinationAddress[0].SourceAddress | Trace-Output -Level:Error } } else { "Successfully sent jumbo packet to {0} from {1} ({2})" ` -f $destinationAddress[0].DestinationAddress, $computer.Name, $destinationAddress[0].SourceAddress | Trace-Output } $array += $destinationAddress } } } $sdnHealthObject.Properties = $array return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-ResourceConfigurationState { <# .SYNOPSIS Validate that the configurationState of the resources. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() try { "Validating configuration state of {0}" -f $SdnEnvironmentObject.Role.ResourceName | Trace-Output $sdnResources = Get-SdnResource -NcUri $SdnEnvironmentObject.NcUrl.AbsoluteUri -Resource $SdnEnvironmentObject.Role.ResourceName -Credential $NcRestCredential 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 ($sdnHealthObject.Result -ne 'FAIL') { $sdnHealthObject.Result = 'WARNING' } $traceLevel = 'Warning' } 'Failure' { $sdnHealthObject.Result = 'FAIL' $traceLevel = 'Error' } 'InProgress' { # if we already have a failure, we will not change the result to warning if ($sdnHealthObject.Result -ne 'FAIL') { $sdnHealthObject.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 ($sdnHealthObject.Result -ne 'FAIL') { $sdnHealthObject.Result = 'WARNING' } $traceLevel = 'Warning' } } default { $traceLevel = 'Verbose' } } 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 $sdnHealthObject.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 $sdnHealthObject.Remediation += $remediationString } } } } # print the overall configuration state to screen, with each of the messages that were captured # as part of the detailedinfo property $msg = "{0} is reporting configurationState status {1}:`n`t- {2}" ` -f $object.resourceRef, $object.properties.configurationState.Status, ($errorMessages -join "`n`t- ") $msg | Trace-Output -Level $traceLevel.ToString() $details = [PSCustomObject]@{ resourceRef = $object.resourceRef configurationState = $object.properties.configurationState } $array += $details } $sdnHealthObject.Properties = $array return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-ResourceProvisioningState { <# .SYNOPSIS Validate that the provisioningState of the resources. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() try { "Validating provisioning state of {0}" -f $SdnEnvironmentObject.Role.ResourceName | Trace-Output $sdnResources = Get-SdnResource -NcUri $SdnEnvironmentObject.NcUrl.AbsoluteUri -Resource $SdnEnvironmentObject.Role.ResourceName -Credential $NcRestCredential 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' { $sdnHealthObject.Result = 'FAIL' $msg | Trace-Output -Level:Error $sdnHealthObject.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 ($sdnHealthObject.Result -ne 'FAIL') { $sdnHealthObject.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 $sdnHealthObject.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 } $sdnHealthObject.Properties = $array return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-ScheduledTaskEnabled { <# .SYNOPSIS Ensures the scheduled task responsible for etl compression is enabled and running #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() $scriptBlock = { $object = [PSCustomObject]@{ TaskName = 'SDN Diagnostics Task' State = $null } try { # check to see if logging is enabled on the registry key # if it is not, return the object with the state set to 'Logging Disabled' $isLoggingEnabled = Get-ItemPropertyValue -Path "HKLM:\Software\Microsoft\NetworkController\Sdn\Diagnostics\Parameters" -Name 'IsLoggingEnabled' if (-NOT $isLoggingEnabled ) { $object.State = 'Logging Disabled' return $object } $result = Get-ScheduledTask -TaskName 'SDN Diagnostics Task' -ErrorAction Stop if ($result) { $object.State = $result.State.ToString() return $object } } catch { # if the scheduled task does not exist, return the object with the state set to 'Not Found' $object.State = 'Not Found' return $object } } try { $scheduledTaskReady = Invoke-PSRemoteCommand -ComputerName $SdnEnvironmentObject.ComputerName -Credential $Credential -ScriptBlock $scriptBlock -AsJob -PassThru foreach ($result in $scheduledTaskReady) { switch ($result.State) { 'Logging Disabled' { "SDN Diagnostics Task is not available on {0} because logging is disabled." -f $result.PSComputerName | Trace-Output -Level:Verbose } 'Not Found' { "Unable to locate SDN Diagnostics Task on {0}." -f $result.PSComputerName | Trace-Output -Level:Error $sdnHealthObject.Result = 'FAIL' } 'Disabled' { "SDN Diagnostics Task is disabled on {0}." -f $result.PSComputerName | Trace-Output -Level:Error $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation += "Use 'Repair-SdnDiagnosticsScheduledTask' to enable the 'SDN Diagnostics Task' scheduled task on $($result.PSComputerName)." } default { "SDN Diagnostics Task is {0} on {1}." -f $result.State, $result.PSComputerName | Trace-Output -Level:Verbose } } $array += [PSCustomObject]@{ State = $result.State Computer = $result.PSComputerName } } $sdnHealthObject.Properties = $array return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-ServerHostId { <# .SYNOPSIS Queries the NCHostAgent HostID registry key value across the hypervisor hosts to ensure the HostID matches known InstanceID results from NC Servers API. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() try { "Validating Server HostID registry matches known InstanceIDs from Network Controller Servers API." | Trace-Output $scriptBlock = { $result = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NcHostAgent\Parameters' -Name 'HostId' -ErrorAction SilentlyContinue return $result.HostID } $servers = Get-SdnResource -NcUri $SdnEnvironmentObject.NcUrl.AbsoluteUri -Resource $SdnEnvironmentObject.Role.ResourceName -Credential $NcRestCredential $hostId = Invoke-PSRemoteCommand -ComputerName $SdnEnvironmentObject.ComputerName -Credential $Credential -ScriptBlock $scriptBlock -AsJob -PassThru foreach($id in $hostId){ if($id -inotin $servers.instanceId){ "{0}'s HostID {1} does not match known instanceID results in Network Controller Server REST API" -f $id.PSComputerName, $id | Trace-Output -Level:Error $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation += "Update the HostId registry key on $($id.PSComputerName) to match the InstanceId of the Server resource in Network Controller" $object = [PSCustomObject]@{ HostID = $id Computer = $id.PSComputerName } $array += $object } else { "{0}'s HostID {1} matches known InstanceID in Network Controller Server REST API" -f $id.PSComputerName, $id | Trace-Output -Level:Verbose } } $sdnHealthObject.Properties = $array return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-ServiceFabricApplicationHealth { <# .SYNOPSIS Validate the health of the Network Controller application within Service Fabric. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() try { "Validating the Service Fabric Application Health for Network Controller" | Trace-Output $ncNodes = Get-SdnServiceFabricNode -NetworkController $SdnEnvironmentObject.ComputerName[0] -Credential $credential if($null -eq $ncNodes){ throw New-Object System.NullReferenceException("Unable to retrieve service fabric nodes") } $applicationHealth = Get-SdnServiceFabricApplicationHealth -NetworkController $SdnEnvironmentObject.ComputerName[0] -Credential $Credential if ($applicationHealth.AggregatedHealthState -ine 'Ok') { $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation += "Examine the Service Fabric Application Health for Network Controller to determine why the health is not OK." } return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-ServiceFabricClusterHealth { <# .SYNOPSIS Validate the health of the Network Controller cluster within Service Fabric. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() try { "Validating the Service Fabric Cluster Health for Network Controller" | Trace-Output $ncNodes = Get-SdnServiceFabricNode -NetworkController $SdnEnvironmentObject.ComputerName[0] -Credential $credential if($null -eq $ncNodes){ throw New-Object System.NullReferenceException("Unable to retrieve service fabric nodes") } $clusterHealth = Get-SdnServiceFabricClusterHealth -NetworkController $SdnEnvironmentObject.ComputerName[0] -Credential $Credential if ($clusterHealth.AggregatedHealthState -ine 'Ok') { $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation += "Examine the Service Fabric Cluster Health for Network Controller to determine why the health is not OK." } return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-ServiceFabricNodeStatus { <# .SYNOPSIS Validate the health of the Network Controller nodes within Service Fabric. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() try { "Validating the Service Fabric Nodes for Network Controller" | Trace-Output $ncNodes = Get-SdnServiceFabricNode -NetworkController $SdnEnvironmentObject.ComputerName[0] -Credential $credential if($null -eq $ncNodes){ throw New-Object System.NullReferenceException("Unable to retrieve service fabric nodes") } foreach ($node in $ncNodes) { if ($node.NodeStatus -ine 'Up') { $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation = 'Examine the Service Fabric Nodes for Network Controller to determine why the node is not Up.' } } return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-ServiceFabricPartitionDatabaseSize { <# .SYNOPSIS Validate the Service Fabric partition size for each of the services running on Network Controller. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() try { "Validate the size of the Service Fabric Partition Databases for Network Controller services" | Trace-Output $ncNodes = Get-SdnServiceFabricNode -NetworkController $SdnEnvironmentObject.ComputerName[0] -Credential $credential if($null -eq $ncNodes){ throw New-Object System.NullReferenceException("Unable to retrieve service fabric nodes") } foreach($node in $ncNodes){ $ncApp = Invoke-SdnServiceFabricCommand -NetworkController $SdnEnvironmentObject.ComputerName[0] -Credential $Credential -ScriptBlock { param([Parameter(Position = 0)][String]$param1) # The 3>$null 4>$null sends unwanted verbose and debug streams into the bit bucket $null = Connect-ServiceFabricCluster -TimeoutSec 15 3>$null 4>$null Get-ServiceFabricDeployedApplication -ApplicationName 'fabric:/NetworkController' -NodeName $param1 } -ArgumentList @($node.NodeName.ToString()) $ncAppWorkDir = $ncApp.WorkDirectory if($null -eq $ncAppWorkDir){ throw New-Object System.NullReferenceException("Unable to retrieve working directory path") } # Only stateful service have the database file $ncServices = Get-SdnServiceFabricService -NetworkController $SdnEnvironmentObject.ComputerName[0] -Credential $Credential | Where-Object {$_.ServiceKind -eq "Stateful"} foreach ($ncService in $ncServices){ $replica = Get-SdnServiceFabricReplica -NetworkController $SdnEnvironmentObject.ComputerName[0] -ServiceName $ncService.ServiceName -Credential $Credential | Where-Object {$_.NodeName -eq $node.NodeName} $imosStorePath = Join-Path -Path $ncAppWorkDir -ChildPath "P_$($replica.PartitionId)\R_$($replica.ReplicaId)\ImosStore" $imosStoreFile = Invoke-PSRemoteCommand -ComputerName $node.NodeName -Credential $Credential -ScriptBlock { param([Parameter(Position = 0)][String]$param1) if (Test-Path -Path $param1) { return (Get-Item -Path $param1) } else { return $null } } -ArgumentList @($imosStorePath) if($null -ne $imosStoreFile){ $formatedByteSize = Format-ByteSize -Bytes $imosStoreFile.Length $imosInfo = [PSCustomObject]@{ Node = $node.NodeName Service = $ncService.ServiceName ImosSize = $formatedByteSize.GB } # if the imos database file exceeds 4GB, want to indicate failure as it should not grow to be larger than this size # need to perform InvariantCulture to ensure that the decimal separator is a period if([float]::Parse($formatedByteSize.GB, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture) -gt 4){ "[{0}] Service {1} is reporting {2} GB in size" -f $node.NodeName, $ncService.ServiceName, $formatedByteSize.GB | Trace-Output -Level:Warning $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation = "Engage Microsoft CSS for further support" } else { "[{0}] Service {1} is reporting {2} GB in size" -f $node.NodeName, $ncService.ServiceName, $formatedByteSize.GB | Trace-Output -Level:Verbose } $array += $imosInfo } else { "No ImosStore file for service {0} found on node {1} from {2}" -f $ncService.ServiceName, $node.NodeName, $imosStorePath | Trace-Output -Level:Warning } } } $sdnHealthObject.Properties = $array return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-ServiceState { <# .SYNOPSIS Confirms that critical services for gateway are running #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() $serviceStateResults = @() try { [string[]]$services = $SdnEnvironmentObject.Role.Properties.Services.Keys if ([string]::IsNullOrEmpty($services)) { return $sdnHealthObject } "Validating {0} service state for {1}" -f ($services -join ', '), ($SdnEnvironmentObject.ComputerName -join ', ') | Trace-Output $scriptBlock = { param([Parameter(Position = 0)][String]$param1) $result = Get-Service -Name $param1 -ErrorAction SilentlyContinue return $result } foreach ($service in $services) { $serviceStateResults += Invoke-PSRemoteCommand -ComputerName $SdnEnvironmentObject.ComputerName -Credential $Credential -Scriptblock $scriptBlock -ArgumentList $service } foreach($result in $serviceStateResults){ $array += $result if($result.Status -ine 'Running'){ $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation += "Start $($result.Name) service on $($result.PSComputerName)" "{0} is {1} on {2}" -f $result.Name, $result.Status, $result.PSComputerName | Trace-Output -Level:Error } else { "{0} is {1} on {2}" -f $result.Name, $result.Status, $result.PSComputerName | Trace-Output -Level:Verbose } } $sdnHealthObject.Properties = $array return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-SlbManagerConnectionToMux { <# .SYNOPSIS Validates the TCP connection between LoadBalancerMuxes and primary replica of SlbManager service within Network Controller. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() $netConnectionExistsScriptBlock = { $tcpConnection = Get-NetTCPConnection -LocalPort 8560 -ErrorAction SilentlyContinue | Where-Object { $_.State -eq "Established" } if ($tcpConnection) { return $true } } try { "Validating connectivity between LoadBalancerMuxes and primary replica of SlbManager service within Network Controller" | Trace-Output $loadBalancerMux = Get-SdnLoadBalancerMux -NcUri $SdnEnvironmentObject.NcUrl.AbsoluteUri -Credential $NcRestCredential # if no load balancer muxes configured within the environment, return back the health object to caller if ($null -ieq $loadBalancerMux) { return $sdnHealthObject } # get the current primary replica of Network Controller # if we cannot return the primary replica, then something is critically wrong with Network Controller # in which case we should mark this test as failed and return back to the caller with guidance to fix the SlbManagerService $primaryReplicaNode = Get-SdnServiceFabricReplica -NetworkController $SdnEnvironmentObject.EnvironmentInfo.NetworkController[0] -ServiceTypeName 'SlbManagerService' -Credential $NcRestCredential -Primary if ($null -ieq $primaryReplicaNode) { "Unable to return primary replica of SlbManagerService" | Trace-Output -Level:Error $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation = "Fix the primary replica of SlbManagerService within Network Controller." return $sdnHealthObject } # enumerate through the load balancer muxes in the environment and validate the TCP connection state # we expect the primary replica for SlbManager within Network Controller to have an active connection for DIP:VIP programming to the Muxes foreach ($mux in $loadBalancerMux) { $virtualServer = Get-SdnResource -NcUri $SdnEnvironmentObject.NcUrl.AbsoluteUri -ResourceRef $mux.properties.virtualServer.resourceRef -Credential $NcRestCredential $virtualServerConnection = $virtualServer.properties.connections[0].managementAddresses $connectionExists = Invoke-PSRemoteCommand -ComputerName $virtualServerConnection -Credential $Credential -ScriptBlock $netConnectionExistsScriptBlock if (-NOT $connectionExists) { "{0} is not connected to SlbManager of Network Controller" -f $mux.resourceRef | Trace-Output -Level:Error $sdnHealthObject.Result = 'FAIL' $sdnHealthObject.Remediation += "Investigate and fix TCP connectivity or x509 authentication between $($primaryReplicaNode.ReplicaAddress) and $($mux.resourceRef)." $object = [PSCustomObject]@{ LoadBalancerMux = $mux.resourceRef SlbManagerPrimaryReplica = $primaryReplicaNode.ReplicaAddress } $array += $object } else { "{0} is connected to {1}" -f $mux.resourceRef, $primaryReplicaNode.ReplicaAddress | Trace-Output -Level:Verbose } } $sdnHealthObject.Properties = $array return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-VfpDuplicatePort { <# .SYNOPSIS Validate there are no ports within VFP layer that may have duplicate MAC addresses. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() try { "Validate no duplicate MAC addresses for ports within Virtual Filtering Platform (VFP)" | Trace-Output $vfpPorts = Get-SdnVfpVmSwitchPort -ComputerName $SdnEnvironmentObject.ComputerName -Credential $Credential $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){ $array += $duplicateObjects $sdnHealthObject.Result = 'FAIL' # since there can be multiple grouped objects, we need to enumerate each duplicate group foreach($obj in $duplicateObjects){ $sdnHealthObject.Remediation += "Remove the duplicate MAC addresses for $($obj.Name) within VFP" "Located {0} VFP ports associated with {1}:`r`n`n{2}`r`n" -f $obj.Count, $obj.Name, ` ($obj.Group ` | Select-Object @{n="Portname";e={"`t$($_.Portname)"}} ` | Select-Object -ExpandProperty Portname ` | Out-String ` ) | Trace-Output -Level:Error } } $sdnHealthObject.Properties = $array return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Test-VMNetAdapterDuplicateMacAddress { <# .SYNOPSIS Validate there are no adapters within hyper-v dataplane that may have duplicate MAC addresses. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SdnFabricEnvObject]$SdnEnvironmentObject, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) $sdnHealthObject = [SdnHealth]::new() $array = @() try { "Validate no duplicate MAC addresses for network adapters within Hyper-V" | Trace-Output $vmNetAdapters = Get-SdnVMNetworkAdapter -ComputerName $SdnEnvironmentObject.ComputerName -AsJob -PassThru -Timeout 900 -Credential $Credential $duplicateObjects = $vmNetAdapters | Group-Object -Property MacAddress | Where-Object {$_.Count -ge 2} if($duplicateObjects){ $array += $duplicateObjects $sdnHealthObject.Result = 'FAIL' # since there can be multiple grouped objects, we need to enumerate each duplicate group foreach($obj in $duplicateObjects){ $sdnHealthObject.Remediation += "Remove the duplicate MAC addresses for $($obj.Name) within Hyper-V" "Located {0} virtual machines associated with MAC address {1}:`r`n`n{2}`r`n" -f $obj.Count, $obj.Name, ` ($obj.Group ` | Select-Object @{n="VMName";e={"`t$($_.VMName)"}} ` | Select-Object -ExpandProperty VMName ` | Out-String ` ) | Trace-Output -Level:Error } } $sdnHealthObject.Properties = $array return $sdnHealthObject } catch { $_ | Trace-Exception $_ | Write-Error } } function Write-HealthValidationInfo { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String]$Role, [Parameter(Mandatory = $true)] [String]$Name, [Parameter(Mandatory = $false)] [String[]]$Remediation ) $details = Get-HealthData -Property 'HealthValidations' -Id $Name $outputString = "[$Role] $Name" $outputString += "`r`n`r`n" $outputString += "--------------------------`r`n" $outputString += "Description:`t$($details.Description)`r`n" $outputString += "Impact:`t`t$($details.Impact)`r`n" if (-NOT [string]::IsNullOrEmpty($Remediation)) { $outputString += "Remediation:`r`n`t -`t$($Remediation -join "`r`n`t -`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 += "`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 NcRestCredential Specifies a user account that has permission to access the northbound NC API interface. 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 ) if ($Global:SdnDiagnostics.EnvironmentInfo.ClusterConfigType -ine 'ServiceFabric') { throw New-Object System.NotSupportedException("This function is only supported on Service Fabric clusters.") } $script:SdnDiagnostics_Health.Cache = $null $aggregateHealthReport = @() if (Test-ComputerNameIsLocal -ComputerName $NetworkController) { Confirm-IsNetworkController } try { $environmentInfo = Get-SdnInfrastructureInfo -NetworkController $NetworkController -Credential $Credential -NcRestCredential $NcRestCredential if($null -eq $environmentInfo){ throw New-Object System.NullReferenceException("Unable to retrieve environment details") } # 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 = [SdnFabricHealthReport]@{ Role = $object.ToString() } $sdnFabricDetails = [SdnFabricEnvObject]@{ 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 = @{ SdnEnvironmentObject = $sdnFabricDetails NcRestCredential = $NcRestCredential } $computerCredParams = @{ SdnEnvironmentObject = $sdnFabricDetails Credential = $Credential } $computerCredAndRestApiParams = @{ SdnEnvironmentObject = $sdnFabricDetails NcRestCredential = $NcRestCredential Credential = $Credential } # 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 # perform the health validations for the appropriate roles that were specified directly # or determined via which ComputerNames were defined switch ($object) { 'Gateway' { $roleHealthReport.HealthValidation += @( Test-ResourceProvisioningState @restApiParams Test-ResourceConfigurationState @restApiParams Test-ServiceState @computerCredParams Test-ScheduledTaskEnabled @computerCredParams ) } 'LoadBalancerMux' { $roleHealthReport.HealthValidation += @( Test-ResourceProvisioningState @restApiParams Test-ResourceConfigurationState @restApiParams Test-ServiceState @computerCredParams Test-ScheduledTaskEnabled @computerCredParams Test-MuxBgpConnectionState @computerCredAndRestApiParams Test-SlbManagerConnectionToMux @computerCredAndRestApiParams ) } 'NetworkController' { $roleHealthReport.HealthValidation += @( Test-NcUrlNameResolution @computerCredAndRestApiParams Test-ServiceState @computerCredParams Test-ServiceFabricPartitionDatabaseSize @computerCredParams Test-ServiceFabricClusterHealth @computerCredParams Test-ServiceFabricApplicationHealth @computerCredParams Test-ServiceFabricNodeStatus @computerCredParams Test-NetworkInterfaceAPIDuplicateMacAddress @restApiParams Test-ScheduledTaskEnabled @computerCredParams Test-NetworkControllerCertCredential @computerCredAndRestApiParams ) } 'Server' { $roleHealthReport.HealthValidation += @( Test-ResourceProvisioningState @restApiParams Test-ResourceConfigurationState @restApiParams Test-EncapOverhead @computerCredParams Test-ProviderNetwork @computerCredParams Test-ServiceState @computerCredParams Test-ServerHostId @computerCredAndRestApiParams Test-VfpDuplicatePort @computerCredParams Test-VMNetAdapterDuplicateMacAddress @computerCredParams Test-HostRootStoreNonRootCert @computerCredParams Test-ScheduledTaskEnabled @computerCredParams Test-NcHostAgentConnectionToApiService @computerCredAndRestApiParams ) } } # 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 ($healthStatus in $roleHealthReport.HealthValidation) { if ($healthStatus.Result -eq 'Warning') { $roleHealthReport.Result = $healthStatus.Result } elseif ($healthStatus.Result -eq 'FAIL') { $roleHealthReport.Result = $healthStatus.Result break } } # add the individual role health report to the aggregate report $aggregateHealthReport += $roleHealthReport } 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') { $role = $_.Role # enumerate all the individual role tests performed so we can determine if any completed that are not PASS $_.HealthValidation | ForEach-Object { 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 -Role $([string]$role) -Name $_.Name -Remediation $remediationList } } } } # save the aggregate health report to cache so we can use it for further analysis $script:SdnDiagnostics_Health.Cache = $aggregateHealthReport "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 } } catch { $_ | Trace-Exception $_ | 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-ServiceState' #> [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 } # SIG # Begin signature block # MIInwAYJKoZIhvcNAQcCoIInsTCCJ60CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDo3gxEwNQLgvyn # jokEFsEMp+9hFKfPunexRxs1hx39lKCCDXYwggX0MIID3KADAgECAhMzAAADrzBA # DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA # hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG # 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN # xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL # go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB # tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd # mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ # 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY # 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp # XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn # TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT # e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG # OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O # PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk # ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx # HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt # CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # 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 # /Xmfwb1tbWrJUnMTDXpQzTGCGaAwghmcAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEINQzX3ZC1y69pKSREgRpns1L # LRPAgYwLTnXiKikQ0pnGMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAUvn3FA4LgMs9Px/DVrtUVjPVimoc29e0PkQjOBWb5ub9DX5mfnosW9GP # fxHfgMFH8BqypQOfWIaoa6TRTZjtPm/2MPuWa91VLAw+rXHP2+3ewIBmh9HeTt8Q # 9MqI20g5yJobU3VQkLcEHCvsXeBmJKoDtzidOvBj9YjdcPLoR6CG6bTp59GN7HZi # B+6kontedpk+PUHCSrzddmUP/xLZfA+JqV4vusZ9FKsDHXzGsNnDZle495EKCZP4 # 5PSlDRVPSzdvqKvI7dhEaHblkTav6K+Bx6RQLXqNmXReUT8v51bpVYHBhkDNogat # ag96Sj0UkR8pyo56U+42LfqKi7bRlaGCFyowghcmBgorBgEEAYI3AwMBMYIXFjCC # FxIGCSqGSIb3DQEHAqCCFwMwghb/AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFXBgsq # hkiG9w0BCRABBKCCAUYEggFCMIIBPgIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCDZhndJbWQ+qBjT+XTEHMHZLUkxiHY22bPWaELGx7VD5wIGZurT/vux # GBEyMDI0MDkyNDE5MjE1My4xWjAEgAIB9KCB2KSB1TCB0jELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxh # bmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjox # NzlFLTRCQjAtODI0NjElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy # dmljZaCCEXswggcnMIIFD6ADAgECAhMzAAAB4NT8HxMVH35dAAEAAAHgMA0GCSqG # SIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTIzMTAx # MjE5MDcxOVoXDTI1MDExMDE5MDcxOVowgdIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI # EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv # ZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh # dGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046MTc5RS00QkIw # LTgyNDYxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsh5zzocW70QE5xo2/+n7gYYd2 # S86LQMQIHS2mf85ERVHXkis8jbd7aqKzCuxg73F3SrPqiDFG73p5R/sOd7LD2uq2 # D++tGbhawAa37Hq39JBWsjV1c8E+42qyThI5xTAafsovrsENk5ybsXM3HhuRQx6y # COrBehfO/ZT+snWNAQWZGfbd/Xv7LzUYngOYFJ7/2HDP2yDGP0GJnfRdAfnmxWIv # jx+AJF2oTZBYCvOTiGkawxr4Z8Tmv+cxi+zooou/iff0B5HSRpX50X20N0FzP+f7 # pgTihuCaBWNZ4meUVR+T09Prgo8HKoU2571LXyvjfsgdm/axGb6dk7+GcGMxHfQP # VbGDLmYgkm2hTJO+y8FW5JaZ8OGh1iVyZBGJib8UW3E4RPBUMjqFZErinOTlmdvl # jP4+dKG5QNLQlOdwGrr1DmUaEAYfPZxyvpuaTlyl3WDCfnHri2BfIecv3Fy0DDpq # iyc+ZezC6hsFNMx1fjBDvC9RaNsxBEOIi+AV/GJJyl6JxxkGnEgmi2aLdpMiVUbB # UsZ9D5T7x1adGHbAjM3XosPYwGeyvbNVsbGRhAayv6G4qV+rsYxKclAPZm1T5Y5W # 90eDFiNBNsSPzTOheAHPAnmsd2Fi0/mlgmXqoiDC8cslmYPotSmPGRMzHjUyghCO # cBdcMaq+k9fzEKPvLQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFHBeFz9unVfvErrK # ANV10Nkw0pnSMB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1Ud # HwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3Js # L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggr # BgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNv # bS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIw # MTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgw # DgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQDAE84OkfwNJXTuzKhs # Q9VSY4uclQNYR29B3NGI7b+1pMUPIsH35bpV+VLOuLQ9/tzU9SZKYVs2gFn9sCnQ # MN+UcbUBtYjjdxxGdF9t53XuCoP1n28eaxB5GfW8yp0f9jeQNevsP9aW8Cc3X0XJ # yU93C8msK/5GIzFnetzj9Bpau9LmuFlBPz6OaVO60EW1hKEKM2NuIQKjnMLkXJug # m9CQXkzgnkQZ7RCoIynqeKUWkLe2/b7rE/e1niXH2laLJpj7bGbGsIJ6SI2wWueb # R37pNLw5GbWyF41OJq+XZ7PXZ2pwXQUtj2Nzd4SHwjxDrM6rsBy5H5BWf/W8cPP3 # kSZXbaLpB6NemnxPwKj/7JphiYeWUdKZoFukHF/uta3YuZAyU8whWqDMmM1EtEhG # 8qw2f6dijrigGDZ4JY4jpZZXLdLiVc9moH3Mxo47CotgEtVml7zoYGTZhsONkhQd # ampaGvCmrsfUNhxyxPIHnv+a4Dp8fc0m31VHOyHETaHauke7/kc/j+lyrToMgqlv # /q4T5qf5+xatgRk0ZHMv/4Zkt9qeqsoJa9iuDqCQyV8RbOpcHPA/OqpVHho1MqO4 # VcuVb8gPstJhpxALgPObbDnFD5c8FhebL/geX89+Tlt1+EqZOUojbpZyxUTzOVwr # Eh6r3GwvEd6sI9sNXrz4WcQ7jTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkA # AAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX # YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg # Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl # IEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVow # fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl # ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd # TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX # 9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1q # UoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8d # q6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byN # pOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2k # rnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4d # Pf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgS # Uei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8 # QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6Cm # gyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzF # ER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQID # AQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQU # KqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1 # GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0 # dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0 # bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMA # QTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbL # j+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1p # Y3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0w # Ni0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIz # LmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwU # tj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN # 3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU # 5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5 # KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGy # qVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB6 # 2FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltE # AY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFp # AUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcd # FYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRb # atGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQd # VTNYs6FwZvKhggLXMIICQAIBATCCAQChgdikgdUwgdIxCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5k # IE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046MTc5 # RS00QkIwLTgyNDYxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZp # Y2WiIwoBATAHBgUrDgMCGgMVAG3z0dXwV+h8WH8j8fM2MyVOXyEMoIGDMIGApH4w # fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl # ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd # TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEFBQACBQDq # nTsgMCIYDzIwMjQwOTI0MjEyMDMyWhgPMjAyNDA5MjUyMTIwMzJaMHcwPQYKKwYB # BAGEWQoEATEvMC0wCgIFAOqdOyACAQAwCgIBAAICIAECAf8wBwIBAAICEWEwCgIF # AOqejKACAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQAC # AwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQBW85SkSJrz6ffav4Ew # c3x3xDb+tJeMxL91NuYRDQGanji+beLNS+SsNjOCdJnfPO/M2M9wMrJaa+82NceC # L9CW4nnXBhoK1oVkI64vVx4K60eLSc/x5Zp4T+YmBROFWVFaMwim+DyLfGKDhk8g # yCGMLUzZA8+8/hmjFiB8fCIWbjGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYTAlVT # MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1l # LVN0YW1wIFBDQSAyMDEwAhMzAAAB4NT8HxMVH35dAAEAAAHgMA0GCWCGSAFlAwQC # AQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkE # MSIEIB4bEKUInAqnZIGDGyc2sPpIPBtOJL2VRx8qy3XyPdyMMIH6BgsqhkiG9w0B # CRACLzGB6jCB5zCB5DCBvQQg4+5Sv/I55W04z73O+wwgkm+E2QKWPZyZucIbCv9p # CsEwgZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ # MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u # MSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAeDU # /B8TFR9+XQABAAAB4DAiBCD9ItgaRqUGDwwpMfByTAyXFF/wSRscu+xEmzBn9p4/ # ATANBgkqhkiG9w0BAQsFAASCAgAqCyquSsE8KEzm9rjUgx1tAAeKx95hXRQDl7j5 # wYFEIG3zVsQNDr9NaGnQHtLrcFhpbWRW1ru5NDpousvzElY6WsTlABMJhqfpRZVz # chVR0LVi1VwzBS1wTYjkKwNmsjUyuQXdHll1mbcFjmWlAEiJlSd9Oy4XW4LZ0yCe # fZg8sdJbO72CylL4smBhDpY0SYM45l3J82A9nDhTimoyaUFcwq2pCXqjLjJrHOTU # A6FMIASuHm34OMS8rl5xVXjyGEG1E+Jpw0iJHEdhzGGfPOQp1aUTR2t2J2Sw7GM8 # N94hzhCGGl02BzvVp67B/QFMOyjeeL1fNhpgw1aUPP21v8UnwMyrom9kvhOmIrqE # LuT+9hOa1cP6MtUX0HZiJD8QmJaarIP0grwcA2B2L6wFxhcz1FnlpH5XYroju1ZG # HwG9f4/oQXmmufZFy80NdyZznStkezxxGQOrh7nj7z3gUoXiVOwfWkQgybvykGIy # gty334+/EsFimTglL9Bi+jrJNDwIESAtJfQpBkvmXBV4Gt/iry8aFGxerJe4kDrR # JKTo19fQvRmVZnn4tUYoxp5vca+8qWzUDnI5ZmA2z6kQ7f3mLTDcLTgksXZ6GNg8 # 6n8iOZdWoZx5iT60xeolsYVXfRDq41CIbxjq609A7YW4nr0GXi92ZauZBpXeyI8O # 24QhIg== # SIG # End signature block |