SkypeForBusinessHybridHealth.psm1

function Get-SkypeForBusinessHybridHealth {
    Write-Host -ForegroundColor Green "This command has been deprecated. Starting new command: Invoke-SkypeForBusinessHybridHealthCheck"
    Invoke-SkypeForBusinessHybridHealthCheck
}


function Invoke-SkypeForBusinessHybridHealthCheck {
    ##############################
    #.SYNOPSIS
    # Performs several standardized tests for Skype for Business Hybrid connectivity to Skype for Business Online.
    #
    #.DESCRIPTION
    # Using XAML with WPF controls and .NET runspaces we build a UI to interface with the various functions'
    # which perfor the necessary tests. Output is displayed by modifying abstracted variables which reference'
    # the XAML controls scraped at runtime. Name tags in the XAML which represent WPF controls are manipulated'
    # using a .NET DispatcherTimer. The application presents a splash screen, then the main UI.
    #
    #.EXAMPLE
    # Invoke-SkypeForBusinessHybridHealthCheck
    #
    #.NOTES
    # Source code available at: https://github.com/jasonshave/HybridHealthChecker
    ##############################

    #primary sync'd hashtable for variables and object references
    $Global:uiHash = [hashtable]::Synchronized(@{})

    #store runspaces in jobs array so we can dispose of them when we're done
    $Jobs = @{}
    $uiHash.Jobs = $Jobs
    $Hosts = @{}
    $uiHash.Hosts = $Hosts
    $RunspaceOutput = @{}
    $uiHash.RunspaceOutput = $RunspaceOutput
    
    #this is where we store all our results from the tests
    $uiHash.resultsHash = $null
    
    #sync'd hash table for variables we need in each runspace
    $Global:variableHash = [hashtable]::Synchronized(@{})

    $variableHash.LyncTools = "https://technet.microsoft.com/en-us/library/gg398665(v=ocs.15).aspx"
    $variableHash.SfbTools = "https://technet.microsoft.com/en-ca/library/dn933921.aspx"
    $variableHash.SfbOTools = "https://www.microsoft.com/en-us/download/details.aspx?id=39366"
    $variableHash.RootPath = Split-Path (Get-module SkypeForBusinessHybridHealth).path
    $variableHash.RequiredModules = @("SkypeOnlineConnector","SkypeForBusiness","Lync")
    $variableHash.Version = (Get-Module SkypeForBusinessHybridHealth).Version.ToString()

    foreach ($moduleName in $variableHash.requiredModules) {
        $variableHash.($moduleName) = Get-Module $moduleName -ListAvailable
    }

    Write-Host "Attempting to start UI..." -ForegroundColor Green
    
    #DISPLAY SPLASH#
    $splashBlock = {
        $uiHash.Hosts.("RspSplashScreen") = $Host
        
        Add-Type -AssemblyName PresentationFramework
        
        $uiContent = Get-Content -Path ($variableHash.rootPath + "\Splash.xaml")
        
        [xml]$xAML = $uiContent -replace 'mc:Ignorable="d"','' -replace "x:N",'N'  -replace '^<Win.*', '<Window'
        $xmlReader = (New-Object System.Xml.XmlNodeReader $xAML)
        $uiHash.Splash = [Windows.Markup.XamlReader]::Load($xmlReader)

        $xAML.SelectNodes("//*[@Name]") | ForEach-Object {
            $uiHash.Add($_.Name, $uiHash.Splash.FindName($_.Name))
        }

        #region EVENTS#

            $uiHash.Splash.Add_SourceInitialized(
                {
                    $uiHash.picSplash1.Source = ($variableHash.rootPath + "\Skype_for_Business_Logo.png")
                }
            )

            $uiHash.Splash.Add_MouseRightButtonDown(
                {
                    $uiHash.Splash.Close()
                }
            )
        #end region EVENTS#

        $uiHash.Splash.ShowDialog() | Out-Null
    }

    Invoke-NewRunspace -codeBlock $splashBlock -RunspaceHandleName RspSplashScreen
    Start-Sleep -Seconds 3

    #DISPLAY MAIN WINDOW#
    $mainWindow = {
        $uiHash.Hosts.("RspMainUi") = $Host
        Add-Type -AssemblyName PresentationFramework
        
        $uiContent = Get-Content -Path ($variableHash.rootPath + "\MainWindow.xaml")
        
        [xml]$xAML = $uiContent -replace 'mc:Ignorable="d"','' -replace "x:N",'N' -replace '^<Win.*', '<Window'
        $xmlReader = (New-Object System.Xml.XmlNodeReader $xAML)
        $uiHash.Window = [Windows.Markup.XamlReader]::Load($xmlReader)

        $xAML.SelectNodes("//*[@Name]") | ForEach-Object {
            $uiHash.Add($_.Name, $uiHash.Window.FindName($_.Name))
        }

        #region EVENTS#
        
            $uiHash.Window.Add_SourceInitialized(
                {
                    $uiHash.picSfb.Source = ($variableHash.rootPath + "\sfb.png")
                    $uiHash.Window.Icon = ($variableHash.rootPath + "\sfb.png")
                    $uiHash.txtVersion.Text = $variableHash.Version

                    $uiHash.comboVersion.ItemsSource = @("Skype for Business Server 2015","Lync Server 2013")
                    $uiHash.comboVersion.SelectedIndex = 0
                    $uiHash.ComboVersionSelectedValue = $uiHash.comboVersion.SelectedValue

                    $uiHash.Status = "Ready"
                    $uiHash.StatusColor = "White"
                    $uiHash.ProgressBarVisibility = "Hidden"
                    $uiHash.ConnectIsEnabled = $false
                    $uiHash.SfboStatusText = "You must provide the required information to connect to Skype for Business Online."
                    $uiHash.AdminDomainIsChecked = $false
                    $uiHash.OnPremModuleNameText = "Skype for Business PowerShell Module"

                    Invoke-CheckModules

                    $updateBlock = {
                        #update the results grid with our data via XAML binding attribute called "resultsData"
                        $uiHash.Window.Resources["resultsData"] = $uiHash.resultsHash

                        #update status bar
                        $uiHash.barStatus.Value = $uiHash.ProgressBarStatus
                        $uiHash.barStatus.Visibility = $uiHash.ProgressBarVisibility

                        #update text blocks
                        $uiHash.txtStatus1.Text = $uiHash.Status
                        $uiHash.txtStatus1.Foreground = $uiHash.StatusColor
                        $uiHash.txtSfboStatus.Text = $uiHash.SfboStatusText
                        $uiHash.txtUserNotify.Text = $uiHash.UserNotifyText
                        $uiHash.txtOnPremModuleName.Text = $uiHash.OnPremModuleNameText

                        #update buttons
                        $uiHash.btnConnect.IsEnabled = $uiHash.ConnectIsEnabled
                        $uiHash.btnAdminInstalled.IsEnabled = $uiHash.AdminInstalledIsEnabled
                        $uiHash.btnAdminInstalled.Content = $uiHash.AdminInstalledContent
                        $uiHash.btnSFBOAdminInstalled.IsEnabled = $uiHash.SFBOAdminInstalledIsEnabled
                        $uiHash.btnSFBOAdminInstalled.Content = $uiHash.SFBOAdminInstalledContent

                        if ($uiHash.SfboSession.State -eq "Opened" -and !$uiHash.SFBOAdminInstalledIsEnabled -and !$uiHash.AdminInstalledIsEnabled) {
                            $uiHash.btnStartTests.IsEnabled = $true
                        }
                    }

                    # Create timer to handle updating the grid
                    $timer = new-object System.Windows.Threading.DispatcherTimer
                    $timer.Interval = [TimeSpan]"0:0:0:0.30"
                    $timer.Add_Tick($updateBlock)
                    $timer.Start()
                }
            )

            $uiHash.Window.Add_Closing(
                {
                    Remove-PSSession $uiHash.SfboSession
                }
            )

            $uiHash.Window.Add_Closed(
                {
                    #this is where we do our cleanup to prevent memory leaks.
                    #we don't want these runspaces to consume memory if they're
                    #not in use even after the window has been closed.
                    foreach ($rsp in $uiHash.Jobs) {
                        $uiHash.Jobs.$rsp.Dispose()
                    }
                    
                    $uiHash = $null
                }
            )

            $uiHash.Window.Add_Loaded(
                {
                    $uiHash.Splash.Dispatcher.Invoke([action]{$uiHash.Splash.Close()})
                }
            )

            $uiHash.btnStartDebug.Add_Click(
                {
                    #testing goes here
                }
            )

            $uiHash.btnConnect.Add_Click(
                {
                    $connectBlock = {
                        InvokeSkypeOnlineConnection -authFromGui
                    }

                    Invoke-NewRunspace -codeBlock $connectBlock -RunspaceHandleName "RspSfboConnect"
                }
            )

            $uiHash.comboVersion.Add_SelectionChanged(
                {
                    $uiHash.ComboVersionSelectedValue = $uihash.comboVersion.SelectedValue
                    Invoke-CheckModules
                }
            )

            $uiHash.btnAdminInstalled.Add_Click(
                {
                    switch ($uiHash.ComboVersionSelectedValue) {
                        "Skype for Business Server 2015" { Start-Process $variableHash['SfbTools'] }
                        "Lync Server 2013" { Start-Process $variableHash['LyncTools'] }
                    }
                }
            )

            $uiHash.btnSFBOAdminInstalled.Add_Click(
                {
                    Start-Process $variableHash['SfbOTools']
                }
            )

            $uiHash.navFeedback.Add_Click(
                {
                    Start-Process $uiHash.navFeedback.NavigateUri
                }
            )

            $uiHash.navGitHub.Add_Click(
                {
                    Start-Process $uiHash.navGitHub.NavigateUri
                }
            )
                
            $uiHash.btnStartTests.Add_Click(
                {
                    #clear previous test results and change the view to the Results tab
                    $uiHash.resultsHash = $null
                    $uiHash.tabMain.SelectedIndex = 1

                    $testCode = {
                        $uiHash.Status = "Running tests..."
                        $uiHash.StartTestButtonIsEnabled = $false
                        $uiHash.ProgressBarVisibility = "Visible"

                        #test execution
                        $uiHash.ProgressBarStatus = 17
                        GetCmsReplicationStatus
                        
                        $uiHash.ProgressBarStatus = 33
                        GetHostingProviderConfiguration
                        
                        $uiHash.ProgressBarStatus = 50
                        GetAccessEdgeConfiguration

                        $uiHash.ProgressBarStatus = 67
                        TestFEToEdgePorts
                        
                        $uiHash.ProgressBarStatus = 84
                        GetSharedSipAddressSpace

                        #re-enable the button :)
                        $uiHash.ProgressBarStatus = 100
                        $uiHash.ProgressBarVisibility = "Hidden"
                        $uiHash.StartTestButtonIsEnabled = $true

                        $uiHash.Status = "Finished!"
                        $uiHash.StatusColor = "White"

                        $uiHash.CodeBlockError = $Error
                    }

                    Invoke-NewRunspace -codeBlock $testCode -RunspaceHandleName "RspStartTests"

                }
            )

            $uiHash.chkAdminDomain.Add_Checked(
                {
                    $uiHash.AdminDomainIsChecked = $uiHash.chkAdminDomain.IsChecked
                    ValidateSfboItems
                }
            )

            $uiHash.chkAdminDomain.Add_UnChecked(
                {
                    $uiHash.AdminDomainIsChecked = $uiHash.chkAdminDomain.IsChecked
                    ValidateSfboItems
                }
            )

            $uiHash.txtTenantDomain.Add_TextChanged(
                {
                    $uiHash.TenantDomainText = $uiHash.txtTenantDomain.Text
                    ValidateSfboItems
                }
            )

            $uiHash.txtUsername.Add_TextChanged(
                {
                    $uiHash.Username = $uiHash.txtUsername.Text
                    ValidateSfboItems                
                }
            )

            $uiHash.txtPassword.Add_TextInput( 
                {
                    ValidateSfboItems   
                }
            )
        

        #end region

        $uiHash.Window.ShowDialog() | Out-Null
        $uiHash.Error = $Error
        
    }

    Invoke-NewRunspace -codeBlock $mainWindow -RunspaceHandleName RspMainUi
}

# INTERNAL FUNCTIONS #

function ValidateSfboItems {

    if ([string]::IsNullOrEmpty($uiHash.TenantDomainText) -or [string]::IsNullOrEmpty($uiHash.Username)) {
        $uiHash.ConnectIsEnabled = $false
        return
    }

    #override admin domain
    #if ($uiHash.AdminDomainIsChecked -and (![string]::IsNullOrEmpty($uiHash.Username)) -and (![string]::IsNullOrEmpty($uiHash.TenantDomainText))) {
    if ($uiHash.AdminDomainIsChecked) {
        try {
            $validateUsername = [System.Net.Mail.MailAddress]($uiHash.Username)
            $uiHash.UserNotifyText = "We'll use: " + $validateUsername
            $uihash.ValidatedUserName = $validateUsername
            $uihash.ConnectIsEnabled = $true
            return
        } catch {
            $uiHash.UserNotifyText = "NOTE: The username should be in UPN format (i.e. username@domain.com)"
            return
        }
    }
    #use standard login method
    if (!$uiHash.AdminDomainIsChecked) {
            try {
                if (($uiHash.Username).Contains('@') -or [System.Net.Mail.MailAddress]($uiHash.Username)){
                    #username shouldn't be in this format
                    $uiHash.UserNotifyText = "There is a problem with your username. It should be without an @ symbol."
                    $uiHash.ConnectIsEnabled = $false
                } else {
                    #username shouldn't be in this format
                    $uiHash.UserNotifyText = "Please enter just your username without the @ symbol."
                    $uiHash.ConnectIsEnabled = $false
                }
            } catch {
                $uiHash.UserNotifyText = "We'll use: " + $uiHash.Username + "@" + $uiHash.TenantDomainText + ".onmicrosoft.com"
                $uiHash.ValidatedUserName = $uiHash.Username + "@" + $uiHash.TenantDomainText + ".onmicrosoft.com"
                $uiHash.ConnectIsEnabled = $true
                return
            }
    } else {
        $uiHash.UserNotifyText = ""
    }
}

function Invoke-NewRunspace {
    [cmdletbinding()]
    param(
        [parameter(Mandatory=$true)][scriptblock]$codeBlock,
        [parameter(Mandatory=$true)][string]$RunspaceHandleName
    )

    $testingRunspace = [runspacefactory]::CreateRunspace()
    $testingRunspace.ApartmentState = "STA"
    $testingRunspace.ThreadOptions = "ReuseThread"
    $testingRunspace.Open()
    $testingRunspace.SessionStateProxy.SetVariable("uiHash",$uiHash)
    $testingRunspace.SessionStateProxy.SetVariable("variableHash",$variableHash)

    $uiHash.RunspaceOutput.($RunspaceHandleName) = New-Object System.Management.Automation.PSDataCollection[psobject]

    $testingCmd = [PowerShell]::Create().AddScript($codeBlock)
    $testingCmd.Runspace = $testingRunspace
    
    $testingHandle = $testingCmd.BeginInvoke($uiHash.RunspaceOutput.($RunspaceHandleName),$uiHash.RunspaceOutput.($RunspaceHandleName))

    #store the handle in the global sync'd hashtable; arraylist we'll use the window.closed() event to clean up
    $uiHash.Jobs.($RunspaceHandleName) = $testingHandle
}

function Invoke-CheckModules {
    #detect modules for on-prem pieces
    switch ($uiHash.ComboVersionSelectedValue) {
        "Skype for Business Server 2015" { 
            $mName = "SkypeForBusiness"
            $uiHash.OnPremModuleNameText = "Skype for Business PowerShell Module"
        }
        "Lync Server 2013" {
            $mName = "Lync"
            $uiHash.OnPremModuleNameText = "Lync Server 2013 PowerShell Module"
        }
        Default {
            $uiHash.Status = "Could not locate either module for Skype or Lync admin tools."
        }
    }

    if (!($variableHash.$mName)) {
        $uiHash.AdminInstalledIsEnabled = $true
        $uiHash.AdminInstalledContent = "More Info"
    } else {
        $uiHash.AdminInstalledIsEnabled = $false
        $uiHash.AdminInstalledContent = "Installed"
    }

    #detect if SFBO module is available
    if (!$variableHash.SkypeOnlineConnector) {
        $uiHash.SFBOAdminInstalledIsEnabled = $true
        $uiHash.SFBOAdminInstalledContent = "More Info"
    } else {
        #make sure we have the right version installed
        if ($variableHash.SkypeOnlineConnector.Version.Major -eq 7) {
            $uiHash.SFBOAdminInstalledIsEnabled = $false
            $uiHash.SFBOAdminInstalledContent = "Installed"
        } else {
            $uiHash.SFBOAdminInstalledContent = "Needs update"
            $uiHash.SFBOAdminInstalledIsEnabled = $true
        }
    }
}

function GetCmsReplicationStatus {

    $testExpectedValue = "None"

    $cmsReplicationResult = Get-CsManagementStoreReplicationStatus -ErrorVariable testMessage
    if (!$cmsReplicationResult) {
        $testValue = "Failed to execute test"
    } else {
        $failedReplicas = $cmsReplicationResult | Where-Object UpToDate -eq $false

        if ($failedReplicas) {
            #we have failed replication servers
            [string]$testValue = $failedReplicas.ReplicaFqdn | ForEach-Object {$_ + "`r"}
            $testMessage = "CMS replica not up to date"
        } else {
            #all is okay
            $testValue = "None"
            $testMessage = "All CMS replicas are up to date"
        }
    }

    ProcessResult -testName "GetCmsReplicationStatus" -testMessage $testMessage -testExpectedValue $testExpectedValue -testValue $testValue
}

function GetAccessEdgeConfiguration {
    #### get Access Edge Configuration ###
    $accessEdgeConfig = Get-CsAccessEdgeConfiguration

    #check AllowOutsideUsers
    ProcessResult -testName 'Access Edge: AllowOutsideUsers' -testExpectedValue $true -testValue $accessEdgeConfig.AllowOutsideUsers
    #check AllowFederatedUsers
    ProcessResult -testName 'Access Edge: AllowFederatedUsers' -testExpectedValue $true -testValue $accessEdgeConfig.AllowFederatedUsers
    #check EnableParnterDiscovery
    ProcessResult -testName 'Access Edge: PartnerDiscovery' -testExpectedValue $true -testValue $accessEdgeConfig.EnablePartnerDiscovery
    #checkUseDnsSrvRouting
    ProcessResult -testName 'Access Edge: RoutingMethod' -testExpectedValue UseDnsSrvRouting -testValue $accessEdgeConfig.RoutingMethod        
}

function GetHostingProviderConfiguration {
    [cmdletbinding()]
    Param()
    
    begin {
        [string]$tenantDomain = $uiHash.TenantDomainText + ".onmicrosoft.com"
        $uiHash.tenantInfo = GetTenantInfo -TenantDomain $tenantDomain
        if ([string]::IsNullOrEmpty($uiHash.tenantInfo)){
            ProcessResult -testName 'Obtain hosting provider URL' -testMessage "Error retrieving tenant domain using GetTenantInfo function!" -testExpectedValue "(Office 365 Admin URL)" -testValue "Null"
            return
        }
    }
    process {
        #### get Hosting Provider Configuration ###
        $hostingProviderConfig = Get-CsHostingProvider | Where-Object ProxyFqdn -eq 'sipfed.online.lync.com'

        #we will accept this alternate:
        if ($hostingProviderConfig.AutoDiscoverUrl -eq "https://webdir.online.lync.com/AutoDiscover/AutoDiscoverService.svc/root") {
            $uiHash.tenantInfo = $hostingProviderConfig.AutoDiscoverUrl
        }

        #since we can get back multiple objects from Get-CsHostingProvider we perform the filter above. Since the 'Identity' and 'Name' values for this object are subject to change, we just need to verify the ProxyFqdn is set correctly on one of the objects returned.
        if ($hostingProviderConfig) {
            #check Proxy FQDN
            ProcessResult -testName 'Hosting Provider: ProxyFqdn'  -testExpectedValue 'sipfed.online.lync.com' -testValue $hostingProviderConfig.ProxyFqdn
            #check Enablement
            ProcessResult -testName 'Hosting Provider: Enabled' -testExpectedValue $true -testValue $hostingProviderConfig.Enabled
            #check Shared Address Space
            ProcessResult -testName 'Hosting Provider: SharedAddressSpace' -testExpectedValue $true -testValue $hostingProviderConfig.EnabledSharedAddressSpace
            #check Hosts OCS Users
            ProcessResult -testName 'Hosting Provider: HostOcsUsers' -testExpectedValue $true -testValue $hostingProviderConfig.HostsOCSUsers
            #check Verification level
            ProcessResult -testName 'Hosting Provider: VerificationLevel' -testExpectedValue 'UseSourceVerification' -testValue $hostingProviderConfig.VerificationLevel
            #check IsLocal
            ProcessResult -testName 'Hosting Provider: IsLocal' -testExpectedValue $false -testValue $hostingProviderConfig.IsLocal
            ### NOTE:check AutoDiscoverUrl obtained from GetTenantInfo function
            ProcessResult -testName 'Hosting Provider: AutoDiscoverUrl' -testExpectedValue $uiHash.tenantInfo -testValue $hostingProviderConfig.AutoDiscoverUrl
        } else {
            #we didn't find a match for the Hosting Provider
            ProcessResult -testName 'Hosting Provider: Error' -testMessage "There was an error obtaining the hosting provider information"
        }

    }
    end {}
}

function GetSharedSipAddressSpace{
        #need to check the PSSession and import it
        $uiHash.Status = "Starting GetSharedSipAddressSpace..."
        Start-Sleep -Seconds 10
        if ($uiHash.SfboSession.State -eq "Opened" -and $uiHash.SfboSession.Availability -eq "Available") {
            #import the PSSession since it's healthy
            $uiHash.Status = "Attempting to import the PSSession..."
            Start-Sleep -Seconds 10
            
            try {
                $uiHash.PSSession = Import-PSSession $uiHash.SfboSession -Prefix Sfbo -AllowClobber
            } catch {
                $uiHash.Status = $_.Exception.Message
            }
            
        } else {
            $uiHash.Status = "Error importing remote PowerShell session due to a broken or stale session. Try to re-authenticate by closing the application and trying again."
            Start-Sleep -Seconds 5
            ProcessResult -testName GetSharedSipAddressSpace -testExpectedValue $true -testValue "Error" -testMessage $uiHash.Status
        }

        $tenantFedConfig = Get-SfboCsTenantFederationConfiguration
        ProcessResult -testName GetSharedSipAddressSpace -testExpectedValue $true -testValue $tenantFedConfig.SharedSipAddressSpace
}

function InvokeSkypeOnlineConnection{
    if (!$uiHash.ValidatedUserName) {
        $uiHash.Status = "Error detected. Cannot determine username."
        return
    }

    $uihash.ConnectIsEnabled = $false
    $uiHash.ProgressBarStatus = "50"
    $uiHash.ProgressBarVisibility = "Visible"
    $uiHash.Status = "Attempting to authenticate to Skype for Business Online..."
    $uiHash.SfboStatusText = "Connecting..."

    try {

        if ($uiHash.AdminDomainIsChecked) {
            $uiHash.SfboSession = New-CsOnlineSession -UserName $uiHash.ValidatedUserName -OverrideAdminDomain ($uiHash.TenantDomainText + ".onmicrosoft.com") -ErrorAction SilentlyContinue -WarningAction SilentlyContinue                
        } else {
            $uiHash.SfboSession = New-CsOnlineSession -UserName $uiHash.ValidatedUserName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
        }

        $uiHash.SfboStatusText = "Successfully authenticated to Skype for Business Online. Waiting to start tests before importing the PSSession..."

    } catch [System.ArgumentNullException] {

        $uiHash.SfboStatusText = "Could not create the session. Possibly due to a bad username/password."
        $uiHash.ConnectIsEnabled = $true
        $uiHash.Status = "Ready"
        return

    } catch {

        $uihash.SfboStatusText = $_.Exception.Message
        $uiHash.ConnectIsEnabled = $true

    }

    $uiHash.ProgressBarVisibility = "Hidden"
    $uiHash.ProgressBarStatus = "0"
    $uiHash.Status = "Ready"

}

function TestFEToEdgePorts {
        #find all Edge servers to test FE to EDGE association
        [array]$edgeServers = ((Get-CsService -EdgeServer).Identity).Replace("EdgeServer:","")

        #note: we don't do a Get-CsService -Registrar here because we want the associated FE's for all Edge servers. Some FE's might not have an Edge association defined.
        [array]$poolServers = (((Get-CsService -EdgeServer).DependentServiceList) | Where-Object {$_ -like "Registrar:*"}).Replace("Registrar:","")
        $registrarServers = ($poolServers | ForEach-Object {Get-CsPool -Identity $_}).Computers
        [array]$frontEndToEdgePorts = 443,4443,5061,5062,8057,50001,50002,50003

        $edgeServerTestResults = TestTcpPortConnection -Ports $frontEndToEdgePorts -Source $registrarServers -Destination $edgeServers
        $edgeServerTestResults | ForEach-Object {

            if ($_.TestResult -eq "Connected") {
                #success; write it out
                $testMessage = "Successfully connected to {0} from {1} on port {2}." -f $_.Destination, $_.Source, $_.Port
            } else {
                $testMessage = "Unable to establish TCP connection from {0} to {1} on {2}." -f $_.Source, $_.Destination, $_.Port
            }
            
            ProcessResult -testName TestFEtoEdgePorts -sourceComputerName $_.PSComputerName -destinationComputerName $_.Destination -testMessage $testMessage -testValue $_.TestResult -testExpectedValue "Connected"
        }
}

function TestTcpPortConnection{
    [cmdletbinding()]
    param(
        [Parameter(mandatory=$true)][array]$Source,
        [Parameter(mandatory=$true)][array]$Destination,
        [Parameter(mandatory=$true)][array]$Ports,
        [Parameter(Mandatory=$false)][int32]$TimeoutInMs = 1000
    )
    begin {}
    process {
        ForEach ($d in $Destination) {
            #we should remove the source server from the array just in case since we don't want to test from/to the same server
            If ($Source.Contains($d)) {
                [System.Collections.ArrayList]$NewSource = $Source #alternatively we could use $NewSource = $Source -ne $d
                $NewSource.Remove($d)
            } else {
                $NewSource = $Source
            }
            ForEach ($p in $Ports) {
                try {
                    $uiHash.Status = "Testing TCP connection from Front-End servers to {0} using port {1}..." -f $d, $p
                    [array]$portTestResult += Invoke-Command -ScriptBlock {
                        $Socket = New-Object System.Net.Sockets.TCPClient;
                        $Connection = $Socket.BeginConnect($args[0],$args[1],$null,$null);
                        $Connection.AsyncWaitHandle.WaitOne($args[2],$false)  | Out-Null;
                        $bucket = [PSCustomObject]@{
                            Source = ($env:COMPUTERNAME).ToLower()
                            Destination = ($args[0]).ToLower()
                            Port = $args[1]
                            TestResult = $(if($Socket.Connected -eq $true){"Connected"}else{"Not Connected"})
                        }
                        $Socket.Close | Out-Null;
                        Return $bucket;
                        $bucket = $null;
                    } -ComputerName $NewSource -Args $d,$p,$TimeoutInMs -ErrorAction SilentlyContinue -ErrorVariable testTcpError
                } catch {
                    ProcessResult -testName $PSCmdlet.CommandRuntime -sourceComputerName $_.PSComputerName -destinationComputerName $d -testErrorMessage $_ -testExpectedValue $resources.PortTestExpected -testValue "Exception"
                } finally {
                    if ($testTcpError){
                        ProcessResult -testName $PSCmdlet.CommandRuntime -sourceComputerName $_.PSComputerName -destinationComputerName $d -testErrorMessage $_ -testExpectedValue $resources.PortTestExpected -testValue "Error"
                    }
                }
            }

            
        }
    }
    end{
        return $portTestResult
    }
}

function TestServerPatchVersion {
    [cmdletbinding()]
    Param()
    begin {
        #get ServicesToPatch from resources file. to scan for more simply add them to the resources file then add to the switch block below for each service's text to display.
        [array]$servicesToPatch = $resources.ServicesToPatch.split(",")
    }
    process {
        foreach ($serviceItem in $servicesToPatch){
            switch ($serviceItem){
                Registrar {$serviceFriendlyName = $resources.FeServiceFriendlyName;$serviceName = $resources.FeServiceName;$patchTestId = $resources.FeServerPatchVersionTestId;$patchErrorMessage = $($resources.FeServerPatchErrorMessage + $resources.FeServiceFriendlyName);$patchSuccessMessage = $($resources.FeServerPatchSuccessMessage + $resources.FeServiceFriendlyName);$patchVersion = $resources.FeServerPatchVersion}
                MediationServer {$serviceFriendlyName = $resources.MedServiceFriendlyName;$serviceName = $resources.MedServiceName;$patchTestId = $resources.MedServerPatchVersionTestId;$patchErrorMessage = $($resources.MedServerPatchErrorMessage = $resources.MedServiceFriendlyName);$patchSuccessMessage = $($resources.MedServerPatchSuccessMessage + $resources.MedServiceFriendlyName);$patchVersion = $resources.MedServerPatchVersion}
            }
            
            $expr = "Get-CsService -$serviceItem"
            $servers = (Invoke-Expression $expr).Identity.Replace($serviceItem + ":","")
            try{
                #powershell remoting used to fan out for speed
                $patchResult = (Invoke-Command -ScriptBlock {Get-CsServerPatchVersion} -ComputerName $servers -ErrorAction SilentlyContinue -ErrorVariable patchError | Where-Object ComponentName -eq $serviceFriendlyName)
            }catch{
                ProcessResult -testName $PSCmdlet.CommandRuntime -sourceComputerName $_.PSComputerName -testErrorMessage $_ -testExpectedValue $patchVersion -testValue "Exception"
            }finally{
                if ($patchError){
                    ProcessResult -testName $PSCmdlet.CommandRuntime -sourceComputerName $_.PSComputerName -testErrorMessage $patchError -testExpectedValue $patchVersion -testValue "Error"
                }
            }
                      
            #process results for this service item
            $patchResult | ForEach-Object {
                ProcessResult -testName $PSCmdlet.CommandRuntime -sourceComputerName $_.PSComputerName -testErrorMessage $patchErrorMessage -testSuccessMessage $patchSuccessMessage -testExpectedValue $patchVersion -testValue $_.Version
            }
        }
    }
    end {}
}

function GetTenantInfo {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, Position = 1)][ValidateNotNull()][string] $TenantDomain,
        [Parameter(Mandatory = $false)][Switch] $Edog = $false,
        [Parameter(Mandatory = $false)][string] $ForestFQDN,
        [Parameter(Mandatory = $false)][string] $altForestFQDN = $null,
        [Parameter(Mandatory = $false)][string] $acsFQDN = $null
    )
    begin{
        
    }

    process{
        if ([System.String]::IsNullOrEmpty($ForestFQDN)){
            if ($Edog)
            {
                $ForestFQDN = "webdir.tip.lync.com"
            }
            else
            {
                $ForestFQDN = "webdir.online.lync.com"
            }
        }

        if ([System.String]::IsNullOrEmpty($acsFQDN)){
            if ($Edog)
            {
                $acsFQDN = "accounts.accesscontrol.windows-ppe.net"
            }
            else
            {
                $acsFQDN = "accounts.accesscontrol.windows.net"
            }
        }

        # First validate that domain is provisioned on O365 ACS and output Domain and tenant ID
        try{
            $req = [Net.WebRequest]::Create("https://$acsFQDN/metadata/json/1?realm=$TenantDomain")
            Write-Verbose ("Getting ACS json document from {0} ..." -f $req.RequestUri)
            $rsp = $req.GetResponse()
            $str = (new-object System.IO.StreamReader ($rsp.GetResponseStream())).ReadToEnd()
            $TenantID = ($str | ConvertFrom-Json).realm
        }catch [System.Net.WebException]{
            $webEx = ($Error[0].Exception.InnerException) -as [System.Net.WebException]
            if (($webEx -ne $null) -and ($webEx.Status -eq [System.Net.WebExceptionStatus]::ProtocolError)){
                $uiHash.Status = "Domain $TenantDomain is not registired with ACS/O365"
                return;
            } else {
                $uiHash.Status = "There was an exception getting the URL for the admin domain in Office 365!"
                return;
            }
        }catch{
            $uiHash.Status = "There was an exception getting the URL for the admin domain in Office 365!"
            return;
        }

        # Now get response from Lync SfB autodiscover service
        $req = [Net.WebRequest]::Create("https://$ForestFQDN/AutoDiscover/AutoDiscoverservice.svc/root?originalDomain=$TenantDomain")
        $rsp = $req.GetResponse()
        $str = (new-object System.IO.StreamReader ($rsp.GetResponseStream())).ReadToEnd()
        #Write-Verbose $str

        $json = ($str | ConvertFrom-Json)
        $self = ($json._links.self.href -as [System.URI]).Host
        if ([System.String]::IsNullOrEmpty($json._links.redirect.href)){
            # Since we were not redirected to a different forest, we need to make sure
            # that domain is actually in Lync/SfB online by asking some other forest
            if ([System.String]::IsNullOrEmpty($altForestFQDN)){
                switch ($self){
                    # Production directors
                    "webdir0a.online.lync.com" {$altForestFQDN = "webdir0b.online.lync.com"}
                    "webdir0b.online.lync.com" {$altForestFQDN = "webdir0e.online.lync.com"}
                    "webdir0e.online.lync.com" {$altForestFQDN = "webdir0f.online.lync.com"}
                    "webdir0f.online.lync.com" {$altForestFQDN = "webdir0m.online.lync.com"}
                    "webdir0m.online.lync.com" {$altForestFQDN = "webdir0a.online.lync.com"}
                    "webdir1a.online.lync.com" {$altForestFQDN = "webdir1b.online.lync.com"}
                    "webdir1b.online.lync.com" {$altForestFQDN = "webdir1e.online.lync.com"}
                    "webdir1e.online.lync.com" {$altForestFQDN = "webdir2a.online.lync.com"}
                    "webdir2a.online.lync.com" {$altForestFQDN = "webdir0a.online.lync.com"}
                    "webdirAU1.online.lync.com" {$altForestFQDN = "webdirIN1.online.lync.com"}
                    "webdirIN1.online.lync.com" {$altForestFQDN = "webdirJP1.online.lync.com"}
                    "webdirJP1.online.lync.com" {$altForestFQDN = "webdirAU1.online.lync.com"}

                    # EDOG directors
                    "webdir0d.tip.lync.com" {$altForestFQDN = "webdir1d.tip.lync.com"}
                    "webdir1d.tip.lync.com" {$altForestFQDN = "webdir0d.tip.lync.com"}

                    # Unkown servers
                    default{
                        if ($self.EndsWith("online.lync.com")){
                            $altForestFQDN = "webdir0m.online.lync.com"
                        }elseif ($self.EndsWith("tip.lync.com")){
                            $altForestFQDN = "webdir0d.tip.lync.com"
                        }else{
                            throw "Unknown forest FQDN: $self"
                        }
                    }
                }
                Write-Verbose "Selected forest $altForestFQDN for second check"
            }else{
                Write-Verbose "Using forest $altForestFQDN for second check"
            }

            $req = [Net.WebRequest]::Create("https://$altForestFQDN/AutoDiscover/AutoDiscoverservice.svc/root?originalDomain=$TenantDomain")
            $rsp = $req.GetResponse()
            $str = (new-object System.IO.StreamReader ($rsp.GetResponseStream())).ReadToEnd()
            #Write-Verbose $str

            $json = ($str | ConvertFrom-Json)
            $altSelf = ($json._links.self.href -as [System.URI]).Host
        }
        if ([System.String]::IsNullOrEmpty($json._links.redirect.href)){
            throw "Domain $TenantDomain is not in any known SfB/Lync online forest (reported by $self and $altSelf)"
        }

        $redirect = ($json._links.redirect.href -as [System.URI]).Host
        Write-Verbose "Domain $TenantDomain is in $redirect, reported by $self"
        $tenantForest = $redirect

        $req = [Net.WebRequest]::Create("https://$redirect/WebTicket/WebTicketService.svc/mex")
        $req.Headers.Add("X-User-Identity", (-join "user@",$TenantDomain))
        $rsp = $req.GetResponse()
        $str = (new-object System.IO.StreamReader ($rsp.GetResponseStream())).ReadToEnd()
        #Write-Verbose $str

        $namespace = @{
            wsdl="http://schemas.xmlsoap.org/wsdl/"; 
            wsx="http://schemas.xmlsoap.org/ws/2004/09/mex"; 
            wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
            wsa10="http://www.w3.org/2005/08/addressing";
            wsp="http://schemas.xmlsoap.org/ws/2004/09/policy";
            wsap="http://schemas.xmlsoap.org/ws/2004/08/addressing/policy";
            msc="http://schemas.microsoft.com/ws/2005/12/wsdl/contract";
            soap12="http://schemas.xmlsoap.org/wsdl/soap12/";
            wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"; 
            wsam="http://www.w3.org/2007/05/addressing/metadata"; 
            xsd="http://www.w3.org/2001/XMLSchema"; 
            tns="http://tempuri.org/"; 
            soap="http://schemas.xmlsoap.org/wsdl/soap/"; 
            wsaw="http://www.w3.org/2006/05/addressing/wsdl"; 
            soapenc="http://schemas.xmlsoap.org/soap/encoding/";
            af="urn:component:Microsoft.Rtc.WebAuthentication.2010"
        }

        $TenantOAuth = (Select-Xml -Namespace $namespace -Content $str -XPath "//af:OAuth/@af:authorizationUri").Node.Value

        #Write Output Oject
        $properties = @{
            'TenantDomain'=$TenantDomain;
            'TenantID'=$TenantID;
            'TenantForest'=$TenantForest;
            'TenantOAuth'=$TenantOAuth;
        }
        $object = New-Object -TypeName PSObject -Property $properties

        #prepare autodiscoverurl
        $autodiscoverUrl = "https://$($object.TenantForest)/Autodiscover/AutodiscoverService.svc/root"
    }
    end{
        return $autodiscoverUrl
    }
}

function ProcessResult{
    [cmdletbinding()]
    Param
    (
        [Parameter(Mandatory=$true)][string]$testName,
        [Parameter(Mandatory=$false)]$sourceComputerName,
        [Parameter(Mandatory=$false)]$destinationComputerName,
        [Parameter(Mandatory=$false)][string]$testErrorMessage,
        [Parameter(Mandatory=$false)][string]$testSuccessMessage,
        [Parameter(Mandatory=$true)]$testExpectedValue,    
        [Parameter(Mandatory=$true)]$testValue,
        [Parameter(Mandatory=$false)][string]$testMessage
    )

    begin {}
    process {
        [array]$outputResult = [PSCustomObject][ordered]@{
            'Test Name' = $testName
            'Result' = $(if ($testExpectedValue -ne $testValue){"FAIL"}else{"PASS"})
            'Expected Value' = $testExpectedValue
            'Tested Value' = $testValue
            'Message' = $testMessage
            'Source Computer' = $sourceComputerName
            'Destination Computer' = $destinationComputerName
            'Test Date' = $(Get-Date)
        }
    }
    end {
        $uiHash.Status = "Completed processing result for: $testName"
        $uiHash.resultsHash += $outputResult
        return $outputResult
    }
}