CQDTools.psm1
class CQDBuildingFile { [string]$Network [string]$NetworkName [int]$NetworkRange [string]$BuildingName [string]$OwnershipType [string]$BuildingType [string]$BuildingOfficeType [string]$City [string]$ZipCode [string]$Country [string]$State [string]$Region [int]$InsideCorp [int]$ExpressRoute } function Write-VerboseLog{ <# .SYNOPSIS Writes messages to Verbose output and Verbose log file .DESCRIPTION This fuction will direct verbose output to the console if the -Verbose paramerter is specified. It will also write output to the Verboselog. #> param( [String]$Message ) $VerboseMessage = ('{0} Line:{1} {2}' -f (Get-Date), $MyInvocation.ScriptLineNumber, $Message) #OLD $VerboseMessage = "$(Get-Date):$($Invocation.MyCommand) $($Message)" Write-Verbose -Message $VerboseMessage Add-Content -Path $VerboseLogFileName -Value $VerboseMessage } function Write-ScriptError{ param( [Parameter(Mandatory=$true,HelpMessage='Provide message to include in the log')] [string] $Message, [Parameter(Mandatory=$false,HelpMessage='Error object to report')] [Object]$ErrorObject, [bool] $Terminating = $false ) Write-VerboseLog -Message $Message Write-VerboseLog -Message ('Error occurred: {0}' -f $ErrorObject.Exception.Message) Write-VerboseLog -Message ('STACKTRACE:{0}' -f $ErrorObject.ScriptStackTrace) if ($Terminating){Write-Error -Message $Message -ErrorAction Stop} } filter ArrayToHash { begin { $hash = @{} } process { $hash[$_] = 'exists' } end { return $hash } } function DottedToBinary { <# .SYNOPSIS Converts a dotted IPv4 address into binary #> param ( # The dotted IPv4 address to convert, e.g. 192.168.1.1 [Parameter(Mandatory=$true)] [string]$DottedIP ) $DottedIP.split(".") | ForEach-Object {$BinaryIP += $([convert]::ToString($_,2).padleft(8,"0"))} Return [string]$BinaryIP } function Compare-IPSubnets { <# .SYNOPSIS Compares two IPv4 subnets and tells if the subnets are identical or if one is a subnet to another. #> [CmdletBinding()] param ( # this functions expects two dottend IPAddresses (e.g. 192.168.1.1) and the subnet mask bits (e.g. 24) [Parameter(Mandatory=$true)] [string]$FirstIPAddressDotted, [Parameter(Mandatory=$true)] [int]$FirstSubnetMaskBits, [Parameter(Mandatory=$false)] [string]$SecondIPAddressDotted, [Parameter(Mandatory=$true)] [int]$SecondSubnetMaskBits ) $Result = New-Object System.Object $Result | Add-Member -MemberType NoteProperty -Name CompareStatus -TypeName string -value $null $Result | Add-Member -MemberType NoteProperty -Name FirstIPAddressDotted -TypeName string -value $FirstIPAddressDotted $Result | Add-Member -MemberType NoteProperty -Name SecondIPAddressDotted -TypeName string -value $SecondIPAddressDotted $Result | Add-Member -MemberType NoteProperty -Name CompareDetails -TypeName string -value $null $Result | Add-Member -MemberType NoteProperty -Name IsSubnetSupernet -TypeName Bool -value $null $Result | Add-Member -MemberType NoteProperty -Name Match -TypeName bool -value $false # check for identical subnets, can shorten the process in this case if(($FirstIPAddressDotted -eq $SecondIPAddressDotted) -and ($FirstSubnetMaskBits -eq $SecondSubnetMaskBits) ) { $message = "Subnet and Mask are identical: " $message += "$FirstIPAddressDotted/$FirstSubnetMaskBits and $SecondIPAddressDotted/$SecondSubnetMaskBits" Write-Debug $message $Result.IsSubnetSupernet = $false $Result.CompareDetails = $message $Result.CompareStatus = $strIdenticalSubnets $Result.match = $true return $Result } # Identify smaller subnet and larger supernet if($SecondSubnetMaskBits -gt $FirstSubnetMaskBits) { $SupernetDotted = $FirstIPAddressDotted $SupernetMaskBits = $FirstSubnetMaskBits $SubnetDotted = $SecondIPAddressDotted $SubnetMaskBits = $SecondSubnetMaskBits } else { $SupernetDotted = $SecondIPAddressDotted $SupernetMaskBits = $SecondSubnetMaskBits $SubnetMaskBits = $FirstSubnetMaskBits $SubnetDotted = $FirstIPAddressDotted } # Convert dotted network to binary and identify the actual network ID $SubnetBinary = DottedToBinary($SubnetDotted) $SubnetBinaryNetworkdID = $SubnetBinary.Substring(0,$SubnetMaskBits).PadRight(32,"0") $SupernetBinary = DottedToBinary($SupernetDotted) $SupernetBinaryNetworkID = $supernetBinary.Substring(0,$SupernetMaskBits).PadRight(32,"0") # compare if supernet network ID is the same as the subnet network ID if we apply the supernet mask to the subnet ID if($SubnetBinaryNetworkdID.Substring(0,$SupernetMaskBits) -eq $SupernetBinaryNetworkID.substring(0,$SupernetMaskBits)) { if($SupernetMaskBits -eq $SubnetMaskBits) { $message = "Subnet and Mask are identical: $SubnetDotted/$SubnetMaskBits is same Subnet as $SupernetDotted/$SupernetMaskBits" Write-VerboseLog -Message $message $Result.IsSubnetSupernet = $false $Result.CompareDetails = $message $Result.CompareStatus = $strIdenticalSubnets $Result.match = $true } else { $message = "Overlapping Subnet/Supernet found: $SubnetDotted/$SubnetMaskBits is a subnet to $SupernetDotted/$SupernetMaskBits" Write-VerboseLog -Message $message $Result.IsSubnetSupernet = $true $Result.CompareDetails = $message $Result.CompareStatus = $strOverlapFound $Result.match = $true } return $Result } # no overlap found, returning empty Result set return $Result } function Get-CQDSubnetsFromAD { $SubnetMappingFile = @() # get all Sites from currenct AD Forest try { Write-VerboseLog -Message 'Connecting to current AD forest' $CurrentForest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() $CurrentForestName = $CurrentForest.Name $Sites = $CurrentForest.Sites $ConfigurationPartitionDN = $CurrentForest.Schema.name.Substring(10) Write-VerboseLog -Message "Connected to $CurrentForestName" } catch { # This is a fatal error. Cannot continue without connectivity to AD. Write-ScriptError -Message 'Could not connect to Active Directory. Please make sure that this computer is member of an Active Directory domain and you are logged on to the domain.' -ErrorObject $_ -Terminating $true } Write-VerboseLog -Message ('Processing {0} sites from {1}' -f $sites.count, $CurrentForestName) # Looping through all sites foreach ($Site in $Sites) { Write-VerboseLog -Message ('Processing site: {0}' -f $site.name) Write-VerboseLog -Message ('Found {0} subnets' -f $site.subnets.count) # Processing all subnets within the same site foreach ($subnet in $site.Subnets) { Write-VerboseLog -Message ('Processing subnet: {0}' -f $subnet.name) #split Subnet into Address and mask $SplitSubnet = $subnet.name.split("/") if ($SplitSubnet.count -ne 2) { # testing if split was successufl Write-VerboseLog -Message ('Cannot split {0} into Subnet ID and mask, skipping..."' -f $Subnet.name) continue } # is this a valid IPv4 address? CQD doesn't support IPv6 if(!($SplitSubnet[0] -match $IPv4Regex)) { # not a IPv4 address, continue with next subnet for same site Write-VerboseLog -Message ('Skipping subnet, not a valid IPv4 address: {0}' -f $SplitSubnet) continue } switch ($BuildingNameSource) { SiteName { $BuildingName = ([string]$Subnet.Site) } SubnetDescription { $ADSIPath = ('LDAP://CN={0},CN=Subnets,CN=Sites,{1}' -f ([string]$Subnet).replace('/','\/'), $ConfigurationPartitionDN ) Write-VerboseLog -Message "Retrieving Site Description from Description of Subnet: $ADSIPath" $objSubnet = [ADSI]$ADSIPath [string]$BuildingName = $objSubnet.description if([string]::IsNullOrEmpty($BuildingName)) { Write-VerboseLog -Message ('No description found for subnet {0}, using Sitename {1} instead' -f [string]$Subnet, [string]$subnet.Site) $BuildingName = [string]$Subnet.Site } } SiteDescription { $ADSIPath = ('LDAP://CN={0},CN=Sites,{1}' -f [string]$Subnet.Site, $ConfigurationPartitionDN ) Write-VerboseLog -Message "Retrieving Site Description from Description of Site: $ADSIPath" $objSites = [ADSI]$ADSIPath [string]$BuildingName = $objSites.description if([string]::IsNullOrEmpty($BuildingName)) { Write-VerboseLog -Message ('No description found for site {0}, using Sitename {1} instead' -f [string]$Subnet.site, [string]$subnet.Site) $BuildingName = [string]$Subnet.Site } } } # create and populate our object $Row = New-Object System.Object # replacing any comma character ',' in the sting with an underscore '_' so that the comma doesn't mess up with the CSV export # AD doesn't allow any special characters in the site name, no special handling required there. $Row | Add-Member -MemberType NoteProperty -Name Network -Value ([string]$SplitSubnet[0]) $Row | Add-Member -MemberType NoteProperty -Name NetworkName -Value $NetworkName.replace(',','_') $Row | Add-Member -MemberType NoteProperty -Name NetworkRange -Value ([int]$SplitSubnet[1]) $Row | Add-Member -MemberType NoteProperty -Name BuildingName -Value $BuildingName.replace(',','_') $Row | Add-Member -MemberType NoteProperty -Name OwnershipType -Value $OwnershipType.replace(',','_') $Row | Add-Member -MemberType NoteProperty -Name BuildingType -Value $BuildingType.replace(',','_') $Row | Add-Member -MemberType NoteProperty -Name BuildingOfficeType -Value $BuildingOfficeType.replace(',','_') $Row | Add-Member -MemberType NoteProperty -Name City -Value $City.replace(',','_') $Row | Add-Member -MemberType NoteProperty -Name ZipCode -Value $ZipCode.replace(',','_') $Row | Add-Member -MemberType NoteProperty -Name Country -Value $Country.replace(',','_') $Row | Add-Member -MemberType NoteProperty -Name State -Value $State.replace(',','_') $Row | Add-Member -MemberType NoteProperty -Name Region -Value $Region.replace(',','_') $Row | Add-Member -MemberType NoteProperty -Name InsideCorp -Value '1' $Row | Add-Member -MemberType NoteProperty -Name ExpressRoute -Value ([int]$ExpressRoute) # Add Results to mapping file $SubnetMappingFile += $Row } } return $SubnetMappingFile } function Get-v4ScopeFromMsftDHCPServer { [CmdletBinding()] param ( # The dotted IPv4 address to convert, e.g. 192.168.1.1 [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [string]$MSftDHCPServer ) Write-VerboseLog -Message ('Function Invocation: {0}' -f ($MyInvocation.BoundParameters | out-string)) Write-VerboseLog -Message "Processing $MSftDHCPServer" try{$Scopes = (get-DHCPServerv4Scope -computername $MSftDHCPServer -ErrorAction stop)} catch { Write-Warning "Couldn't retrieve data from $MSftDHCPServer" } $Scopes = $Scopes | Select-Object Name, ScopeId, SubnetMask, State, @{N='Server';E={$MSftDHCPServer}} return $Scopes } function Get-BuildingMappingFromMsftDHCP { <# .SYNOPSIS Creats a CQD compatible building file from Microsoft DHCP servers. .DESCRIPTION Creats a CQD compatible building file from Microsoft DHCP servers. Retrieves IPv4 DHCP scopes from Msft DHCP servers and turns them into CQD building information. The result is stored in an output file for validation and upload to CQD. The script can be run locally on a DHCP Server, or used to retrieve data from a specific server, or it can iterate through all authorized DHCP servers in AD. Requires Remote Server Administration Tools for DHCP to be installed on the local machine. This script requires the DHCPServer PowerShell module and read permissions to the DHCP service. The Scope name is used as the building name. The results file is already in a CQD compatible format. After manual validation for accuracy it can be uploaded into CQD without any further reformatting. .PARAMETER EnabledOnly Include only enabled (active) IPv4 DHCP scopes. .PARAMETER IterateThroughAllServers Find all authorized DHCP servers in Active Directory, retrieve IPv4 DHCP scope information from all servers found. Cannot be used together with the DHCPServer parameter. .PARAMETER DHCPServer Used to specify a specific DHCP server to retrieve IPv4 DHCP scopes from. Cannot be used together with the IterateThroughAllServers parameter. .PARAMETER OutputFileName Specifies the name of the outputfile containing the subnet mapping data. .EXAMPLE Get-BuildingMappingFromMsftDHCP Gets IPv4 DHCP scopes from a DHCP server, assumes that this script is run on the DHCP server itself. .EXAMPLE Get-BuildingMappingFromMsftDHCP -IterateThroughAllServers -EnabledOnly Gets IPv4 DHCP scopes from all authorized DHCP servers in AD. Includes enalbed IPv4 DHCP scopes only. .NOTES © 2018 Microsoft Corporation.  All rights reserved.  This document is provided "as-is." Information and views expressed in this document, including URL and other Internet Web site references, may change without notice. This document does not provide you with any legal rights to any intellectual property in any Microsoft product. Skype for Business and Microsoft Teams customers and partners may copy, use and share these materials for planning, deployment and operation of Skype for Business and Microsoft Teams. This sript requires permissions to read data from DHCP, i.e. DHCP Users or higher. #> [CmdletBinding()] param ( [Parameter(Mandatory=$false)] [string]$OutputFileName='.\BuildingFile.csv', [Parameter(Mandatory=$false)] [switch]$EnabledOnly, [Parameter(Mandatory=$false)] [switch]$AllADAuthorizedDHCPServers, [Parameter(Mandatory=$false)] [string]$DHCPServer='localhost', [Parameter(Mandatory=$false)] [string] $VerboseLogFileName = 'VerboseOutput.Log' ) Write-VerboseLog -Message ('Function Invocation: {0}' -f ($MyInvocation.BoundParameters | out-string)) Write-VerboseLog -Message ('Module Version: {0}' -f (get-module CQDTools | out-string )) # You should only have AllADAuthorizedDHCPServer or provided a list of servers, not both. if($AllADAuthorizedDHCPServers -and ($DHCPServer -ne 'localhost')) { Write-ScriptError -Message 'Invalid parameter combination. You cannot set both AllADAuthorizedDHCPServers and provide a DHCP Server list. Please select only one of the two parameters' -Terminating $true } if (!(Get-Module DhcpServer -ListAvailable)) { # module not found Write-ScriptError 'Cannot find required module DhcpServer, please install DHCP RSAT components and try again.' -Terminating $true } $DHCPScopes =@() # AllADAuthorizedDHCPServers switch is set, we should iterate through all servers if($AllADAuthorizedDHCPServers) { Write-Host Write-Host 'Getting DHCP Servers from AD' Write-Host Write-VerboseLog -Message 'Getting DHCP Servers from AD' $DHCPServer = '' $DHCPServer = (get-DHCPServerInDC).DnsName } # Do we have any DHCP servers to process? if($DHCPServer.Length -lt 1) { Write-ScriptError -Message 'No DHCP servers found, nothing to do here.' -Terminating $true } # Get Scopes from MSFT DHCP Servers server list Write-Host ('Retrieving IPv4 DHCP Scopes from {0} DHCP servers.' -f $DHCPServer.count) Write-VerboseLog -Message ('Serverlist: {0}' -f ($DHCPServer | Out-String) ) $DHCPScopes += $DHCPServer | Get-v4ScopeFromMsftDHCPServer Write-VerboseLog -Message ('Found {0} scopes' -f $DHCPScopes.count) if($DHCPScopes.count -lt 1) { Write-ScriptError -Message 'No DHCP scopes found, please check for previous errors. Possible reasons: Not running locally on the DHCP server and no server name or -AllADAuthorizedDHCPServers parameter provided, or no permissions to read, or no connectivity to DHCP servers.' -Terminating $true } # remove non-enabled DHCP Scopes if needed if($EnabledOnly) { $DHCPScopes = $DHCPScopes | Where-Object {$_.State -eq "Active"} Write-VerboseLog -Message ('Removed inactive scopes, new scope count: {0}' -f $DHCPScopes.count) } Write-VerboseLog -Message 'Formatting data for CQD' $SubnetMappingFile =@() $SubnetMappingFile = Convert-ToCQDBuildingFormat -InputObject $DHCPScopes -Network 'ScopeID' -NetworkRange 'SubnetMask' -BuildingName 'Name' Write-Host ('Writing file to path: {0}' -f $OutputFileName) Write-VerboseLog -Message ('Writing file to path: {0}' -f $OutputFileName) Save-CQDBuildingFile -CQDBuildingFile $SubnetMappingFile -ExportFilePath $OutputFileName # Check for duplicates and overlap Write-Verbose -Message ('Calling Get-BuildingMappingFromADSites to check for dupliactes in file {0}' -f $OutputFileName ) Get-BuildingMappingFromADSites -InputFileName $OutputFileName } function Convert-ToCQDBuildingFormat { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [array]$InputObject, [Parameter(Mandatory=$true)] [string]$Network, [Parameter(Mandatory=$false)] [string]$NetworkName, [Parameter(Mandatory=$true)] [string]$NetworkRange, [Parameter(Mandatory=$true)] [string]$BuildingName, [Parameter(Mandatory=$false)] [string]$OwnershipType, [Parameter(Mandatory=$false)] [string]$BuildingType, [Parameter(Mandatory=$false)] [string]$BuildingOfficeType, [Parameter(Mandatory=$false)] [string]$City, [Parameter(Mandatory=$false)] [string]$ZipCode, [Parameter(Mandatory=$false)] [string]$Country, [Parameter(Mandatory=$false)] [string]$State, [Parameter(Mandatory=$false)] [string]$Region, [Parameter(Mandatory=$false)] [string]$InsideCorp=1, [Parameter(Mandatory=$false)] [string]$ExpressRoute=0 ) $CQDBuildingFile = @() foreach($subnet in $InputObject) { $CQDSubnetMapping = new-object CQDBuildingFile $CQDSubnetMapping.Network = $subnet.$Network $CQDSubnetMapping.NetworkName = $subnet.$NetworkName if(([string]$subnet.$NetworkRange).contains(".")) { #Network range is dotted, need to convert into bitmask $NetmaskBinary = DottedToBinary($subnet.$NetworkRange) $NetmaskBinary = $NetmaskBinary.trim("0") $NetmaskBits = $NetmaskBinary.Length $CQDSubnetMapping.NetworkRange = $NetmaskBits } else { #Network Range is a bitmask already, nothing to convert $CQDSubnetMapping.NetworkRange = $subnet.$NetworkRange } $CQDSubnetMapping.BuildingName = $subnet.$BuildingName $CQDSubnetMapping.OwnershipType = $subnet.$OwnershipType $CQDSubnetMapping.BuildingType = $subnet.$BuildingType $CQDSubnetMapping.BuildingOfficeType = $subnet.$BuildingOfficeType $CQDSubnetMapping.City = $subnet.$City $CQDSubnetMapping.ZipCode = $subnet.$ZipCode $CQDSubnetMapping.Country = $subnet.$Country $CQDSubnetMapping.State = $subnet.$State $CQDSubnetMapping.Region = $subnet.$Region if($InsideCorp -eq 1 -or $InsideCorp -eq 0) { $CQDSubnetMapping.InsideCorp = $InsideCorp } else { $CQDSubnetMapping.InsideCorp = $subnet.$InsideCorp } if($ExpressRoute -eq 1 -or $InsideCorp -eq 0) { $CQDSubnetMapping.ExpressRoute = $ExpressRoute } else { $CQDSubnetMapping.ExpressRoute = $subnet.$ExpressRoute } $CQDBuildingFile += $CQDSubnetMapping } return $CQDBuildingFile } function Get-BuildingMappingFromADSites { <# .SYNOPSIS Retrieves AD Site and Subnet information and creates a subnet mapping file for Call Quality Dashboard (CQD). Performs Check for overlap and duplicates. .DESCRIPTION Get-BuildingMappingFromADSites iterates through all AD Sites and Subnets. All valid IPv4 addresses are exported into a CSV-style format, all other formats like IPv6 as skipped as CQD doesn't support them today. You can use the optional parameters to specify additional information for the mapping file, including City, Country, Region and all other data fields that CQD supports. The script requires the computer to be domain joined so that it can leverage an AD context to retrieve data. Computers joined to Azure Active Directory aren't supported. The script also performs check for overlap (aka supernetting) and duplicate subnet information. This check can be run without connection to AD if an input file is specified through -InputFileName parameter. .EXAMPLE Get-BuildingMappingFromADSites Read AD site and subnet information and create building mapping file. .EXAMPLE Get-BuildingMappingFromADSites -BuildingNameSource SubnetDescription Read AD site and subnet information and create building mapping file, uses site description as a name for the building. .EXAMPLE Get-BuildingMappingFromADSites -OutputFileName 'MyFile.csv' -BuildingOfficeType 'CompanyOwned' Read AD site and subnet information, use OutputFileName MyFile.csv and assign BuildingOfficeType = CompanyOwned .EXAMPLE Get-BuildingMappingFromADSites -ExpressRoute Read AD site and subnet information, mark all sites to be connected via ExpressRoute .Example Get-BuildingMappingFromADSites -InputFileName MyCQDFile.csv Reads existing CQD building file and performs check for overlapping or duplicate subnets. Accepts both tab and comma delimited file (.tsv and .csv). .Link https://docs.microsoft.com/en-us/SkypeForBusiness/using-call-quality-in-your-organization/turning-on-and-using-call-quality-dashboard#upload-building-information .NOTES © 2018 Microsoft Corporation.  All rights reserved.  This document is provided "as-is." Information and views expressed in this document, including URL and other Internet Web site references, may change without notice. This document does not provide you with any legal rights to any intellectual property in any Microsoft product. Skype for Business and Microsoft Teams customers and partners may copy, use and share these materials for planning, deployment and operation of Skype for Business and Microsoft Teams. You need to run this script from a Domain joined machine to read data from Active Directory. #> [cmdletbinding()] param( [Parameter(Mandatory=$false)] [string]$OutputFileName='.\BuildingFile.csv', [Parameter(Mandatory = $false)] [ValidateSet('SiteName','SubnetDescription','SiteDescription')] [string] $BuildingNameSource = 'SiteName', [Parameter(Mandatory=$false)] [string]$NetworkName='', [Parameter(Mandatory=$false)] [string]$OwnershipType='', [Parameter(Mandatory=$false)] [string]$BuildingType='', [Parameter(Mandatory=$false)] [string]$BuildingOfficeType='', [Parameter(Mandatory=$false)] [string]$City='', [Parameter(Mandatory=$false)] [string]$ZipCode='', [Parameter(Mandatory=$false)] [string]$Country='', [Parameter(Mandatory=$false)] [string]$State='', [Parameter(Mandatory=$false)] [string]$Region='', [Parameter(Mandatory=$false)] [Switch]$ExpressRoute, [Parameter(Mandatory=$false)] [string]$InputFileName, [Parameter(Mandatory=$false)] [string] $VerboseLogFileName = 'VerboseOutput.Log' ) Write-VerboseLog -Message ('Function Invocation: {0}' -f ($MyInvocation.BoundParameters | out-string)) Write-VerboseLog -Message ('Module Version: {0}' -f (get-module CQDTools | out-string )) #region define variables and prepare for execution # a few strings used that'll be used in the issues report New-Variable -name strIdenticalSubnets -Value 'IdenticalSubnetsFound' -Option ReadOnly New-Variable -Name strOverlapFound -Value 'OverlappingSubnetsFound' -Option ReadOnly # RegEx to detect a single IPv4 Octet New-Variable -Name Octet -Value '(?:0?0?[0-9]|0?[1-9][0-9]|1[0-9]{2}|2[0-5][0-5]|2[0-4][0-9])' -Option ReadOnly # and we need four of them for a valid IPv4 address, combine them into a RegEx [regex] $IPv4Regex = "^(?:$Octet\.){3}$Octet$" # Cannot process Switch parameter during export, have to convert it into bool. if($ExpressRoute) { [bool]$ExpressRoute = $true } else { [bool]$ExpressRoute = $false } # This file will collect overlap and duplicats, if there are any. $IssuesFileName = 'Issues.csv' # Define colum header if we need to read from a CQD input file as this file doesn't contain any headers $CQDHeader = "Network", "NetworkName" , "NetworkRange" , "BuildingName", "OwnershipType", "BuildingType", "BuildingOfficeType", "City", "ZipCode", "Country", "State", "Region", "InsideCorp", "ExpressRoute" New-Variable -Name CQDDelimiter -Value `t # Create the empty array as a container for the mapping file $SubnetMappingFile = @() #endregion #region main script # check if an input file is given, don't need to fetch data from AD in this case if([string]::IsNullOrEmpty($InputFileName)) { # no input file given, fetch data from AD # Test if output file already exist, let's not override any existing file if(Test-Path($OutputFileName)) { $ExistingFile = get-item $OutputFileName Write-ScriptError -message ('File {0} already exists, please remove existing file.' -f $ExistingFile.FullName) -Terminating $true } Write-Host Write-Host "Reading subnet data from AD" Write-Host $SubnetMappingFile = Get-CQDSubnetsFromAD if($SubnetMappingFile.Length -lt 1) { # No subnets in AD found Write-ScriptError ('No IPv4 Subnets in Forest {0} found.' -f [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Name) -Terminating $true } } else { # file found, lets import Write-VerboseLog -Message "Reading input file $InputFileName" # Check if file is comma or tab separated $FirstLine = get-content -Path $InputFileName if(!$FirstLine.Contains($CQDDelimiter)) { # doesn't contain tab, assuming comma as separator $CQDDelimiter = ',' } $SubnetMappingFile = Import-Csv -Path $InputFileName -Delimiter $CQDDelimiter -Header $CQDHeader -ErrorAction Stop } # retrieved all subnets Write-Host ('Found {0} IPv4 subnets' -f $SubnetMappingFile.Length) # loop through all records, compare subnet[i] with all subnet[n>i] and store Results # report on Results (if Resultset not empty, call out issues found) $FirstSiteIndex=0 $SiteCount = $SubnetMappingFile.Count write-host "Checking $SiteCount subnets for overlap and duplicates. This may take a while for a large amount of subnets." Write-Host $Results = @() foreach ($FirstSite in $SubnetMappingFile) { Write-Progress -Activity "Checking $SiteCount Subnets for overlap and duplicates" -Status ('Processing network {0}' -f $Firstsite.Network) -PercentComplete (($FirstSiteIndex+1)/$SiteCount*100) Write-VerboseLog -Message "Processing FirstSite: $FirstSiteIndex of $SiteCount" if ($FirstSiteIndex -lt $SiteCount-1) { foreach ($SecondSiteIndex in ($FirstSiteIndex+1)..($SiteCount-1)) { $SecondSite = $SubnetMappingFile[$SecondSiteIndex] write-debug "Comparing FirstSite: $FirstSiteIndex with SecondSite: $SecondSiteIndex" Write-debug ('First site: {0} second site: {1}' -f $FirstSite.Network, $SecondSite.Network) $Result = New-Object System.Object $Result = Compare-IPSubnets -FirstIPAddressDotted $FirstSite.Network -FirstSubnetMaskBits $FirstSite.NetworkRange -SecondIPAddressDotted $SecondSite.Network -SecondSubnetMaskBits $SecondSite.NetworkRange if($Result.match) { # found match, needs to be fixed $Results += $Result } } } $FirstSiteIndex++ write-debug "Next first site" } # Are there any issues? if($Results.Length -gt 0 ) { Write-Warning ('{0} Overlapping or duplicate subnets found. Please check output in {1} and resolve issues before uploading to CQD. You may use the -InputFile paramater to check the adjusted file.' -f $Results.Length, $IssuesFileName) Write-VerboseLog -Message ('{0} Overlapping or duplicate subnets found. Please check output in {1} and resolve issues before uploading to CQD. You may use the -InputFile paramater to check the adjusted file.' -f $Results.Length, $IssuesFileName) Write-Host $Results | Export-csv -NoTypeInformation -Path $IssuesFileName } else { Write-Host "No overlapping or duplicate subnets found." -ForegroundColor Green Write-VerboseLog "No overlapping or duplicate subnets found." Write-Host } # Export the file only if no input file was given. if([string]::IsNullOrEmpty($InputFileName)) { Write-VerboseLog -Message ('Writing {0} rows of CQD building file to: {1}' -f $SubnetMappingFile.count, $OutputFileName) Save-CQDBuildingFile -CQDBuildingFile $SubnetMappingFile -ExportFilePath $OutputFileName } Write-Host Write-Host 'Please validate the accuracy, adjust and complete as needed.' -ForegroundColor Green Write-Host 'Upload the file to CQD at https://cqd.lync.com to complete the process.' Write-Host #endregion } function Save-CQDBuildingFile { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [array]$CQDBuildingFile, [Parameter(Mandatory=$true)] [string]$ExportFilePath ) # Convert into CSV format $CQDBuildingFile = $CQDBuildingFile | convertto-csv -NoTypeInformation -Delimiter ',' # CQD doesn't expect quotes in the text, so we have to remove them $CQDBuildingFile = $CQDBuildingFile.Replace('"','') Write-Host ('Saving building mapping file: {0}' -f $ExportFilePath ) -ForegroundColor Green # CDQ also doesn't expect column headings, so we remove them and save the remaining content to disk $CQDBuildingFile[1..$CQDBuildingFile.Length] | Set-Content -Path $ExportFilePath -ErrorAction Stop } function Find-MissingBuildingsInCQD { <# .SYNOPSIS Helps you with CQD building mapping maintenance. Finds unmapped subnets in CQD that show the same public IP as seen for mapped subnets, reports found subnets. .DESCRIPTION Gets stream count for all subnets from CQD including public IP (second reflexive local IP) and second building. Checks all unmapped subnets (subnets with no building information) if the public IP is also seen for a mapped subnet. It is very likely that those unmapped subnets actually belong the the same building(s). Found subnets, streamcount and matching buildings are reported. The Results of this script can be used to enhance and further complete the CQD building mapping file. .PARAMETER ResultsFilePath Path and filename for the Results. Default: .\PotentialMissedSubnets.csv .PARAMETER Days Number of days for which data is returned from CQD. Default: 28 days. .PARAMETER DisplayResults If specified, all Results are shown after the script completes, in addition to saving all Results in the Results file. .EXAMPLE Find-MissingBuildingsInCQD Connects to CQD, reads data for the past 28 days (plus 2 day offset to account for CQD data delay), analyses Results and reports out any subnets that seem to be missing from the CQD building file. .Link https://docs.microsoft.com/en-us/SkypeForBusiness/using-call-quality-in-your-organization/turning-on-and-using-call-quality-dashboard#upload-building-information .Notes © 2018 Microsoft Corporation.  All rights reserved.  This document is provided "as-is." Information and views expressed in this document, including URL and other Internet Web site references, may change without notice. This document does not provide you with any legal rights to any intellectual property in any Microsoft product. Skype for Business and Microsoft Teams customers and partners may copy, use and share these materials for planning, deployment and operation of Skype for Business and Microsoft Teams. Will prompt for credentials to read data from CQD. Provide a user with CQD access, a member of any of these O365 admin roles work: Report Reader, Skype Admin, Tenant Admin. #> [cmdletbinding()] param( [Parameter(Mandatory=$false)] [string]$ResultsFilePath='.\PotentialMissedSubnets.csv', [Parameter(Mandatory=$false)] [int16]$Days=28, [Parameter(Mandatory=$false)] [switch]$DisplayResults, [Parameter(Mandatory=$false)] [string] $VerboseLogFileName = 'VerboseOutput.Log' ) Write-VerboseLog -Message ('Function Invocation: {0}' -f ($MyInvocation.BoundParameters | out-string)) Write-VerboseLog -Message ('Module Version: {0}' -f (get-module CQDTools | out-string )) #region define variables and prepare for execution #check for CQDPowerShell module to read data from CQD if (!(Get-Module CQDPowershell -ListAvailable)) { # module not found Write-Warning 'Cannot find required module CQDPowerShell, trying to install in current user context...' try { Install-Module CQDPowerShell -ErrorAction Stop -Scope CurrentUser } catch { Write-Error 'Could not install PowerShell module CQDPowerShell. This module is required for this action. Please install module manually from PowerShell Gallery or run Install-Module CQDPowerShell.' break } } $Dimensions = @() $Dimensions += 'SecondTenantDataBuilding.Second Building Name' $Dimensions += 'AllStreams.Second Subnet' $Dimensions += 'AllStreams.Second Reflexive Local IP' New-Variable -Name Measures -Value 'Measures.Total Stream Count' New-Variable -Name GroupProperty -Value 'Second Building Name' New-Variable -Name OffsetDays -Value 2 New-Variable -Name StartDate -Value (Get-Date).AddDays(-$Offsetdays-$days) New-Variable -Name EndDate -Value (Get-Date).AddDays(-$Offsetdays) New-Variable -Name I $MappedSecondReflexiveIPs = @() $SecondReflexiveIPsArray = @() $SecondReflexiveIPsHash = @{} $CQDUnmanagedRows =@() $Results = @() # Cannot process Switch parameter during export, have to convert it into bool. if($DisplayResults) { [bool]$DisplayResults = $true } else { [bool]$DisplayResults = $false } #endregion #region main script Write-Host "Establishing connection to CQD. If prompted, please enter credentials with permissions to access CQD - a member of the Report Reader, Skype admin, or Tenant admin role." Write-Host "Reading Data from CQD might take a while, please be patient." # Getting data from CQD # clean-up data to remove rows with 2nd reflexive IP set to '0.0.0.' or with Second Subnet being empty, set to <null>, or containing ':' try{ $CQDData = get-CQDData -Dimensions $Dimensions -Measures $Measures -Transport 'UDP' -StartDate $StartDate -EndDate $EndDate -OutputType DataTable -ErrorAction Stop | Where-Object {` $_.'Second Subnet' -notlike '' ` -and $_.'Second Reflexive Local IP' -notlike '0.0.0.' ` -and $_.'Second Reflexive Local IP' -notlike '' ` -and $_.'Second Reflexive Local IP' -notlike '<null>' ` -and $_.'Second Reflexive Local IP' -notlike '*:*' ` } } catch { Write-ScriptError -Message 'Unable to connect to CQD' -ErrorObject $_ -Terminating $true } Write-VerboseLog -Message ('Retrieved {0} rows from CQD' -f $CQDData.Count) # Group by Second Building Name Write-VerboseLog -Message 'Group by Second Building Name' $MappedElements = $CQDData | Group-Object -Property $GroupProperty -AsHashTable Write-VerboseLog -Message ('Found buildings: {0} Buildings' -f $MappedElements.count) Write-VerboseLog -Message ('Found buildings: {0}' -f (($MappedElements.GetEnumerator() | Select-Object -Property 'Name' -ExpandProperty 'Name' | Sort-Object -Property 'Name') -join ',') ) # Remove element with no mapping, this leaves us with a hash table for containing all mapped subnets Write-VerboseLog -Message 'Find elements that have mappings' $MappedElements.Remove('') Write-VerboseLog -Message ('New building count: {0}' -f $MappedElements.count) Write-VerboseLog -Message ('Found buildings: {0}' -f (($MappedElements.GetEnumerator() | Select-Object -Property 'Name' -ExpandProperty 'Name' | Sort-Object -Property 'Name') -join ',') ) #Get unique list of second reflexive IPs for mapped subnets Write-VerboseLog -Message 'Get unique list of second reflexive IPs for mapped subnets' # There is a -unique parameter for the select-object cmdlet, however this takes *forever* if you have large amounts of data. Large being >10k records. So we're using a different route. But that is much faster. # loop through Hashtable for mapped sites and collect all second reflexive IPs. The hash table will contain a unique list of IPs. foreach ($site in $MappedElements.GetEnumerator()) { # built collection of all second reflexive IPs that don't contain a ':' - those are IPv6 addresses, we don't need them. $SecondReflexiveIPsArray += $site.Value.'second reflexive local ip' | Where-Object {$_ -notlike '*:*' } } # Turning the Array into a hash will get us the unique list of IPs $SecondReflexiveIPsHash = $SecondReflexiveIPsArray | ArrayToHash # now we have a hash list of all second reflexive local IPs, need to flatten this into a single list. foreach($IP in $SecondReflexiveIPsHash.GetEnumerator()) { $MappedSecondReflexiveIPs += $IP.Name } Write-VerboseLog -Message ('Found {0} unique reflexive IPs for mapped subnets.' -f $MappedSecondReflexiveIPs.count) # Find elements that are unmapped, remove all data sets where the Second Reflexive Local IP is not shared with mapped subnets. Write-VerboseLog -Message 'Find elements that are unmapped, remove all data sets where the Second Reflexive Local IP is not shared with mapped subnets. Depending on the amount of data, this might take quite a while.' if($CQDData.count -gt 1000) { Write-Host ('Preparing {0} records for compare, this might take a while' -f $CQDData.count) Write-VerboseLog -Message ('Preparing {0} records for compare, this might take a while' -f $CQDData.count) } $n=0 # a for loop is 5x faster than a foreach for this loop. for($i=0;$i -lt $CQDData.count;$i++) { # nesting if clauses to speed up, we only need to to the costly -in match for a small subset of records if($CQDData[$i].$GroupProperty -eq '') { $n++ if($n -eq '1000'){write-host '.' -nonewline;$n=0} if($CQDData[$i].'Second Reflexive Local IP' -in $MappedSecondReflexiveIPs) { $CQDUnmanagedRows += $CQDData[$i] } } } Write-Host $UnmappedElements = $CQDUnmanagedRows | Group-Object -Property 'Second Reflexive Local IP' -AsHashTable $MappedElementsCount = $MappedElements.Count $i=0 # loop through mappend elements and see if we find a match in the unmapped foreach($MappedElement in $MappedElements.GetEnumerator() | Sort-Object Name) { Write-Progress -Activity "Processing $MappedElementsCount Buildings for unmapped subnets." -Status ('Processing Building {0}' -f $MappedElement.Name) -PercentComplete (($i+1)/$MappedElementsCount*100) Write-VerboseLog -Message ('Processing Building {0}' -f $MappedElement.Name) # Get unique second reflexive local IPs $SecondReflexiveIPs = ($MappedElement.value | Select-Object 'Second Reflexive Local IP' -Unique).'Second Reflexive Local IP' Write-VerboseLog -Message ('Found {0} reflexive IPs: {1}' -f $SecondReflexiveIPs.count, ($SecondReflexiveIPs -join(',') )) # loop through second reflexive IPs and check if we find that in the unmapped elements foreach($SecondReflexiveIP in $SecondReflexiveIPs) { # Get unmapped elements that match the second reflexive IP $PotentialMissedSubnets = @() $PotentialMissedSubnets = $UnmappedElements.$SecondReflexiveIP Write-Debug ('Found {0} subnets for reflexive IP: {1}' -f $PotentialMissedSubnets.count, $SecondReflexiveIP) # loop through all of them and build our Results set foreach($PotentialMissedSubnet in $PotentialMissedSubnets) { $Row = New-Object System.Object $Row | Add-Member -MemberType NoteProperty -Name 'Unmapped Subnet' -Value $PotentialMissedSubnet.'Second Subnet' $Row | Add-Member -MemberType NoteProperty -Name 'Second Reflexive Local IP' -Value $PotentialMissedSubnet.'Second Reflexive Local IP' $Row | Add-Member -MemberType NoteProperty -Name 'Total Stream Count' -Value $PotentialMissedSubnet.'Total Stream Count' $Row | Add-Member -MemberType NoteProperty -Name 'Potentially missed in' -Value $MappedElement.Name $Results += $Row } } $i++ } # Done with the compare, did we find any unmapped elements? if ($Results.Length -eq 0) { # no Write-Host "Did not detect any unmapped subnets using the same public IP (second reflexive local IP) as any mapped subnet." -ForegroundColor Green Write-VerboseLog -Message 'Did not detect any unmapped subnets using the same public IP (second reflexive local IP) as any mapped subnet.' } else { # yes # apply some formatting, and merge multiple matches for a single unmapped subnet into a single row. $Results = $Results | Group-Object -Property 'Unmapped Subnet','Second Reflexive Local IP' $Results = $Results | Select-Object ` @{N='Unmapped Subnet';E={$_.Name.split(',')[0]}},` @{N='Second Reflexive Local IP';E={$_.Name.split(',')[1]}},` @{N='Sites sharing public IP';E={$_.Group.'Potentially missed in' -join ','}},` @{N='Streamcount';E={$_.Group.'Total Stream Count' | Select-Object -unique}} Write-Host ('Found {0} subnets that are potentially missing in the building mapping file' -f $Results.Length) -ForegroundColor Green Write-VerboseLog -Message ('Found {0} subnets that are potentially missing in the building mapping file' -f $Results.Length) Write-Host "Saving Results to $ResultsFilePath" Write-VerboseLog -Message "Saving Results to $ResultsFilePath" if($DisplayResults){$Results} $Results | Export-Csv -NoTypeInformation -Path $ResultsFilePath } #endregion } # SIG # Begin signature block # MIIkAgYJKoZIhvcNAQcCoIIj8zCCI+8CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD8n2CqMm6lRMKj # GYo0Mh2z4f8wOJp2r1083nKxrzI2mqCCDYMwggYBMIID6aADAgECAhMzAAAAxOmJ # +HqBUOn/AAAAAADEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMTcwODExMjAyMDI0WhcNMTgwODExMjAyMDI0WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQCIirgkwwePmoB5FfwmYPxyiCz69KOXiJZGt6PLX4kvOjMuHpF4+nypH4IBtXrL # GrwDykbrxZn3+wQd8oUK/yJuofJnPcUnGOUoH/UElEFj7OO6FYztE5o13jhwVG87 # 7K1FCTBJwb6PMJkMy3bJ93OVFnfRi7uUxwiFIO0eqDXxccLgdABLitLckevWeP6N # +q1giD29uR+uYpe/xYSxkK7WryvTVPs12s1xkuYe/+xxa8t/CHZ04BBRSNTxAMhI # TKMHNeVZDf18nMjmWuOF9daaDx+OpuSEF8HWyp8dAcf9SKcTkjOXIUgy+MIkogCy # vlPKg24pW4HvOG6A87vsEwvrAgMBAAGjggGAMIIBfDAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUy9ZihM9gOer/Z8Jc0si7q7fDE5gw # UgYDVR0RBEswSaRHMEUxDTALBgNVBAsTBE1PUFIxNDAyBgNVBAUTKzIzMDAxMitj # ODA0YjVlYS00OWI0LTQyMzgtODM2Mi1kODUxZmEyMjU0ZmMwHwYDVR0jBBgwFoAU # SG5k5VAF04KqFzc3IrVtqMp1ApUwVAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljQ29kU2lnUENBMjAxMV8yMDEx # LTA3LTA4LmNybDBhBggrBgEFBQcBAQRVMFMwUQYIKwYBBQUHMAKGRWh0dHA6Ly93 # d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljQ29kU2lnUENBMjAxMV8y # MDExLTA3LTA4LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQAG # Fh/bV8JQyCNPolF41+34/c291cDx+RtW7VPIaUcF1cTL7OL8mVuVXxE4KMAFRRPg # mnmIvGar27vrAlUjtz0jeEFtrvjxAFqUmYoczAmV0JocRDCppRbHukdb9Ss0i5+P # WDfDThyvIsoQzdiCEKk18K4iyI8kpoGL3ycc5GYdiT4u/1cDTcFug6Ay67SzL1BW # XQaxFYzIHWO3cwzj1nomDyqWRacygz6WPldJdyOJ/rEQx4rlCBVRxStaMVs5apao # pIhrlihv8cSu6r1FF8xiToG1VBpHjpilbcBuJ8b4Jx/I7SCpC7HxzgualOJqnWmD # oTbXbSD+hdX/w7iXNgn+PRTBmBSpwIbM74LBq1UkQxi1SIV4htD50p0/GdkUieeN # n2gkiGg7qceATibnCCFMY/2ckxVNM7VWYE/XSrk4jv8u3bFfpENryXjPsbtrj4Ns # h3Kq6qX7n90a1jn8ZMltPgjlfIOxrbyjunvPllakeljLEkdi0iHv/DzEMQv3Lz5k # pTdvYFA/t0SQT6ALi75+WPbHZ4dh256YxMiMy29H4cAulO2x9rAwbexqSajplnbI # vQjE/jv1rnM3BrJWzxnUu/WUyocc8oBqAU+2G4Fzs9NbIj86WBjfiO5nxEmnL9wl # iz1e0Ow0RJEdvJEMdoI+78TYLaEEAo5I+e/dAs8DojCCB3owggVioAMCAQICCmEO # kNIAAAAAAAMwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQI # EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv # ZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmlj # YXRlIEF1dGhvcml0eSAyMDExMB4XDTExMDcwODIwNTkwOVoXDTI2MDcwODIxMDkw # OVowfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UE # AxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMTCCAiIwDQYJKoZIhvcN # AQEBBQADggIPADCCAgoCggIBAKvw+nIQHC6t2G6qghBNNLrytlghn0IbKmvpWlCq # uAY4GgRJun/DDB7dN2vGEtgL8DjCmQawyDnVARQxQtOJDXlkh36UYCRsr55JnOlo # XtLfm1OyCizDr9mpK656Ca/XllnKYBoF6WZ26DJSJhIv56sIUM+zRLdd2MQuA3Wr # aPPLbfM6XKEW9Ea64DhkrG5kNXimoGMPLdNAk/jj3gcN1Vx5pUkp5w2+oBN3vpQ9 # 7/vjK1oQH01WKKJ6cuASOrdJXtjt7UORg9l7snuGG9k+sYxd6IlPhBryoS9Z5JA7 # La4zWMW3Pv4y07MDPbGyr5I4ftKdgCz1TlaRITUlwzluZH9TupwPrRkjhMv0ugOG # jfdf8NBSv4yUh7zAIXQlXxgotswnKDglmDlKNs98sZKuHCOnqWbsYR9q4ShJnV+I # 4iVd0yFLPlLEtVc/JAPw0XpbL9Uj43BdD1FGd7P4AOG8rAKCX9vAFbO9G9RVS+c5 # oQ/pI0m8GLhEfEXkwcNyeuBy5yTfv0aZxe/CHFfbg43sTUkwp6uO3+xbn6/83bBm # 4sGXgXvt1u1L50kppxMopqd9Z4DmimJ4X7IvhNdXnFy/dygo8e1twyiPLI9AN0/B # 4YVEicQJTMXUpUMvdJX3bvh4IFgsE11glZo+TzOE2rCIF96eTvSWsLxGoGyY0uDW # iIwLAgMBAAGjggHtMIIB6TAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQUSG5k # 5VAF04KqFzc3IrVtqMp1ApUwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYD # VR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUci06AjGQQ7kU # BU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2NybC5taWNyb3Nv # ZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0MjAxMV8yMDExXzAz # XzIyLmNybDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cu # bWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0MjAxMV8yMDExXzAz # XzIyLmNydDCBnwYDVR0gBIGXMIGUMIGRBgkrBgEEAYI3LgMwgYMwPwYIKwYBBQUH # AgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvZG9jcy9wcmltYXJ5 # Y3BzLmh0bTBABggrBgEFBQcCAjA0HjIgHQBMAGUAZwBhAGwAXwBwAG8AbABpAGMA # eQBfAHMAdABhAHQAZQBtAGUAbgB0AC4gHTANBgkqhkiG9w0BAQsFAAOCAgEAZ/KG # pZjgVHkaLtPYdGcimwuWEeFjkplCln3SeQyQwWVfLiw++MNy0W2D/r4/6ArKO79H # qaPzadtjvyI1pZddZYSQfYtGUFXYDJJ80hpLHPM8QotS0LD9a+M+By4pm+Y9G6XU # tR13lDni6WTJRD14eiPzE32mkHSDjfTLJgJGKsKKELukqQUMm+1o+mgulaAqPypr # WEljHwlpblqYluSD9MCP80Yr3vw70L01724lruWvJ+3Q3fMOr5kol5hNDj0L8giJ # 1h/DMhji8MUtzluetEk5CsYKwsatruWy2dsViFFFWDgycScaf7H0J/jeLDogaZiy # WYlobm+nt3TDQAUGpgEqKD6CPxNNZgvAs0314Y9/HG8VfUWnduVAKmWjw11SYobD # HWM2l4bf2vP48hahmifhzaWX0O5dY0HjWwechz4GdwbRBrF1HxS+YWG18NzGGwS+ # 30HHDiju3mUv7Jf2oVyW2ADWoUa9WfOXpQlLSBCZgB/QACnFsZulP0V3HjXG0qKi # n3p6IvpIlR+r+0cjgPWe+L9rt0uX4ut1eBrs6jeZeRhL/9azI2h15q/6/IvrC4Dq # aTuv/DDtBEyO3991bWORPdGdVk5Pv4BXIqF4ETIheu9BCrE/+6jMpF3BoYibV3FW # TkhFwELJm3ZbCoBIa/15n8G9bW1qyVJzEw16UM0xghXVMIIV0QIBATCBlTB+MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNy # b3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExAhMzAAAAxOmJ+HqBUOn/AAAAAADE # MA0GCWCGSAFlAwQCAQUAoIHIMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwG # CisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCB/nuSw # KOqdiMfgIxS/KXzqmJJTk3JQOAiShr5SjM31vDBcBgorBgEEAYI3AgEMMU4wTKAS # gBAAQwBRAEQAVABvAG8AbABzoTaANGh0dHBzOi8vd3d3LnBvd2Vyc2hlbGxnYWxs # ZXJ5LmNvbS9wYWNrYWdlcy9DUURUb29scyAwDQYJKoZIhvcNAQEBBQAEggEAUVHN # QvegOUfu8eF+kn9WWnK0zbfGze+236aaNKZtTtV2kSikLLhX8Xbwt3Iq5E37pLbU # wgGU4kW8NGT+vNDPGzdGFuB00/zy5Q30TXC4x00GHTA5vGFVBYYKKsZjOjtMIt2i # dTyAvFA5tCvvedQl0GgFYRtE9cRfR51tNUiwkXZJTdxdBqPn0wYib5JVisD/2nlo # cksSNOmSHgDKtOjPKnHVvRbvAobJe0m0Q5mzHYwiAfrp4MUXxaM07tm3OBlO4P4K # QCoN08n6GUwXM9pa2KcLbu6UHk+ICsjjdgMREwvHt5/KeOtX7hdJKbHzGMicJeV+ # bMs8iiRLJe8r1xqAoqGCE0UwghNBBgorBgEEAYI3AwMBMYITMTCCEy0GCSqGSIb3 # DQEHAqCCEx4wghMaAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggE6BgsqhkiG9w0BCRAB # BKCCASkEggElMIIBIQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCBC # B/RMtFsnCxzs+GuWv1zuak4ny6S/a5Y1U8NKzIYcXQIGWylci2rbGBIyMDE4MDYy # NjE2MDA1Mi40OFowBwIBAYACAfSggbekgbQwgbExCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xDDAKBgNVBAsTA0FPQzEmMCQGA1UECxMdVGhhbGVz # IFRTUyBFU046NzBERC00QjVCLTQ1NjgxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1l # LVN0YW1wIFNlcnZpY2Wggg7LMIIGcTCCBFmgAwIBAgIKYQmBKgAAAAAAAjANBgkq # hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 # IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcNMjUwNzAxMjE0NjU1WjB8MQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQg # VGltZS1TdGFtcCBQQ0EgMjAxMDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC # ggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0VBDVpQoAgoX77XxoSyxfxcPlYcJ2tz5m # K1vwFVMnBDEfQRsalR3OCROOfGEwWbEwRA/xYIiEVEMM1024OAizQt2TrNZzMFcm # gqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQedGFnkV+BVLHPk0ySwcSmXdFhE24oxhr5 # hoC732H8RsEnHSRnEnIaIYqvS2SJUGKxXf13Hz3wV3WsvYpCTUBR0Q+cBj5nf/Vm # wAOWRH7v0Ev9buWayrGo8noqCjHw2k4GkbaICDXoeByw6ZnNPOcvRLqn9NxkvaQB # wSAJk3jN/LzAyURdXhacAQVPIk0CAwEAAaOCAeYwggHiMBAGCSsGAQQBgjcVAQQD # AgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7fEYbxTNoWoVtVTAZBgkrBgEEAYI3FAIE # DB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNV # HSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVo # dHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29D # ZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAC # hj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1 # dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0gAQH/BIGVMIGSMIGPBgkrBgEEAYI3LgMw # gYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9QS0kvZG9j # cy9DUFMvZGVmYXVsdC5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8A # UABvAGwAaQBjAHkAXwBTAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQEL # BQADggIBAAfmiFEN4sbgmD+BcQM9naOhIW+z66bM9TG+zwXiqf76V20ZMLPCxWbJ # at/15/B4vceoniXj+bzta1RXCCtRgkQS+7lTjMz0YBKKdsxAQEGb3FwX/1z5Xhc1 # mCRWS3TvQhDIr79/xn/yN31aPxzymXlKkVIArzgPF/UveYFl2am1a+THzvbKegBv # SzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon/VWvL/625Y4zu2JfmttXQOnxzplmkIz/ # amJ/3cVKC5Em4jnsGUpxY517IW3DnKOiPPp/fZZqkHimbdLhnPkd/DjYlPTGpQqW # hqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/fmNZJQ96LjlXdqJxqgaKD4kWumGnEcua # 2A5HmoDF0M2n0O99g/DhO3EJ3110mCIIYdqwUB5vvfHhAN/nMQekkzr3ZUd46Pio # SKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0cs0d9LiFAR6A+xuJKlQ5slvayA1VmXqH # czsI5pgt6o3gMy4SKfXAL1QnIffIrE7aKLixqduWsqdCosnPGUFN4Ib5KpqjEWYw # 07t0MkvfY3v1mYovG8chr1m1rtxEPJdQcdeh0sVV42neV8HR3jDA/czmTfsNv11P # 6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+NR4Iuto229Nfj950iEkSMIIE2DCCA8Cg # AwIBAgITMwAAALf4IhR9AyL++gAAAAAAtzANBgkqhkiG9w0BAQsFADB8MQswCQYD # VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe # MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3Nv # ZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0xNzEwMDIyMzAwNTJaFw0xOTAxMDIy # MzAwNTJaMIGxMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G # A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMQww # CgYDVQQLEwNBT0MxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOjcwREQtNEI1Qi00 # NTY4MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIIBIjAN # BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtIV2Zy5+zQJdUAsSfGQTNy72V6Da # EzWh4oMtVkjQ3K/Iyj7Fa62+ZK2RJcDPnYtRuN3n4uZNzthEjxtfBPp63WjCa5zq # H/nwsF0S4heF3Uzl0CNoM7cPRMFZWJ3X9Hc3SWeO+9cbZiSYwTQNhpABO5iUD1wL # fklCx4fHyB68D98VcE/C8uDTcCVqs5Z9dVNyZtTUrFDOvGXQqocQbkR9BOKzL5nA # 8MY52p84pO86u9aENniLuBxfwqb/4Xu0RtkSmdrgcJKJvGARHYrlAaBLm5FyUgc0 # S9EGUWy1mmA7PX2DM1VefZIzWJEvN334zjprO+2bIw8QJ2o6XL7m2tieOwIDAQAB # o4IBGzCCARcwHQYDVR0OBBYEFG1ir4N7RIZ1cXfSTVuP7kQuYDVpMB8GA1UdIwQY # MBaAFNVjOlyKMZDzQ3t8RhvFM2hahW1VMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6 # Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1RpbVN0YVBD # QV8yMDEwLTA3LTAxLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0 # dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljVGltU3RhUENBXzIw # MTAtMDctMDEuY3J0MAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwgw # DQYJKoZIhvcNAQELBQADggEBAGmkqdLrsaKfiuZ7JHu0J+7eeqI2Vog1VRyH2bzb # +wEZTMRAnnTPjv84cEcI4Q/43zx8MFrIIO9OdodSWig8Zweq8SFiZDb1N/lKG7KY # 2kg2mRXUVKU01txA6c1oRrP/kiGpoIPlFGQviVWBeZjAHe2+mHTMVAPmAHfNtzTR # 3sKVJRG96JOkJWn0/SWXjqrvU08wjnC/qdlnjmdPe3XqsfW3jHeOofSlMqYH9vD6 # Y3UjnIFmUZr4llbum+L+OOeOtDTiAxkA8AYaGDrtVzE/ysY50T968uU6vvrOESNE # HoMKcSieo6WBkP12jYY4ZHIdsKy1gvOHiIzqq1OCldjh+GmhggN2MIICXgIBATCB # 4aGBt6SBtDCBsTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAO # BgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEM # MAoGA1UECxMDQU9DMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo3MERELTRCNUIt # NDU2ODElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIlCgEB # MAkGBSsOAwIaBQADFQDV49D+WTbkwmRZSQfp1yKMx0XqDaCBwTCBvqSBuzCBuDEL # MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v # bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEMMAoGA1UECxMDQU9D # MScwJQYDVQQLEx5uQ2lwaGVyIE5UUyBFU046MjY2NS00QzNGLUM1REUxKzApBgNV # BAMTIk1pY3Jvc29mdCBUaW1lIFNvdXJjZSBNYXN0ZXIgQ2xvY2swDQYJKoZIhvcN # AQEFBQACBQDe3JwdMCIYDzIwMTgwNjI2MTEwMzU3WhgPMjAxODA2MjcxMTAzNTda # MHcwPQYKKwYBBAGEWQoEATEvMC0wCgIFAN7cnB0CAQAwCgIBAAICAfwCAf8wBwIB # AAICF1wwCgIFAN7d7Z0CAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoD # AaAKMAgCAQACAxbjYKEKMAgCAQACAx6EgDANBgkqhkiG9w0BAQUFAAOCAQEAQUaw # MZv5RHlj+j21QNSeFvwRwOwVl44Erq0yzQXSUq6yKXTssn3UQHj+RwebWS3mf6qg # d7s5zviqL1z7gN3/ZhIE7oEDJnDqeEHATmwSX2YuyFEn3iL2W/17XqTK6DGFfHYo # 8RVF73E47ion7UMr+NNKc9ODL1V2TzVGragwjaZXcryAgeiXWojePdxSh9nMfk/r # GHLuH8IGw2TY0hu+Yt+QUnDzS1yxYz+/B4QvnfHzDiiEmLecBgdCPg3TaH6BNXyo # vg+Fuf0WiATvvlCDWdFcrv513sjSKdFBiCln87FSzt5RBsTno1Q10+Rf9xyOGxTi # mXsC1gb57/qSNtg4qjGCAvUwggLxAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwAhMzAAAAt/giFH0DIv76AAAAAAC3MA0GCWCGSAFlAwQCAQUAoIIB # MjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEIEsv # lxXiusAdVU1Jl4xLhgDT31o25VFaJpSWkemA26rHMIHiBgsqhkiG9w0BCRACDDGB # 0jCBzzCBzDCBsQQU1ePQ/lk25MJkWUkH6dcijMdF6g0wgZgwgYCkfjB8MQswCQYD # VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe # MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3Nv # ZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAALf4IhR9AyL++gAAAAAAtzAWBBRA # 4wd/AQ+IP+VPsY7GliCUAVLaijANBgkqhkiG9w0BAQsFAASCAQCEkp55P19cKLZ+ # YTnAk2CMgIU0qaCui9rcl3yuwe+vt+IG/1Jzsy9E8lr/m4e5BAMs7RH6iYhMOjvr # rJN9NBBAzWbdmWAwyPV1LOXqYBcGhRVxDsdn6Wbr3wU8Xw1rZCLuJSAnMNHOPXNt # ki5h6ntxUWBozZ+MLrlVmIaVIYmSDLjoslYsc5h3D1XcsDytzUFWbIi4rO7ASn00 # 79OU/8eKz41v+5YaHE9fNh5o3y3NTJ0oG3OscTb5JDp3HiPqCDIwcNiog5TTXW+b # 4MWANxh8+1vwK2fsOGbUiFS6axMGuRsE+luYQNYQsSiUglANr1fFJg2BJx1fKmwU # jd0uj/uG # SIG # End signature block |