TeamsAnalyzerNode.psm1

#region INITIAL VARIABLES
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    [string]$ProductName = "TeamsAnalyzerNode"
    [string]$modulePath = (get-module $ProductName -ListAvailable).modulebase    
    [string]$toolSourcePath = $modulePath + "\MicrosoftSkypeForBusinessNetworkAssessmentTool.exe"
    [string]$toolInstallArgs = "/install /quiet /norestart /log $env:TEMP\ToolInstall.txt"
    [string]$toolUninstallArgs = "/uninstall /quiet /norestart /log $env:TEMP\ToolUninstall.txt"
    [string]$toolName = "Microsoft Skype for Business Network Assessment Tool"
    [string]$networkAssessmentToolPath = "${env:ProgramFiles(x86)}\Microsoft Skype for Business Network Assessment Tool\NetworkAssessmentTool.exe"
    [string]$networkAssessmentToolConfig = "${env:ProgramFiles(x86)}\Microsoft Skype for Business Network Assessment Tool\NetworkAssessmentTool.exe.config"
    [string]$networkAssessmentToolWorkingDirectory = "${env:ProgramFiles(x86)}\Microsoft Skype for Business Network Assessment Tool\"
    [string]$QualityResultsFilePath = "$($env:ProgramFiles)\TeamsAnalyzer\TeamsAnalyzerNode\quality_results.csv"
    [string]$ConnectivityResultsFilePath = "$($env:ProgramFiles)\TeamsAnalyzer\TeamsAnalyzerNode\connectivity_results.txt"
    [string]$ResultsFilePath = "$($env:ProgramFiles)\TeamsAnalyzer\TeamsAnalyzerNode\"
    [string]$taskIdentity = "SYSTEM"
    [string]$taskPath = "\TeamsAnalyzerNode\"
    [string]$taskAction = "PowerShell.exe"
    [string]$qualityCaptureTaskName = 'Teams Analyzer Node (Quality Test)'
    [string]$connectivityTestTaskName = 'Teams Analyzer Node (Connectivity Test)'
    [string]$controllerTaskName = 'Teams Analyzer Node (Controller)'
    [string]$updateTaskName = 'Teams Analyzer Node (Updater)'
    [string]$qualityArguments = '-NoProfile -WindowStyle Hidden -Command "& {Invoke-TaQualityTest}"'
    [string]$connectivityArguments = '-NoProfile -WindowStyle Hidden -Command "& {Invoke-TaConnectivityTest}"'
    [string]$controllerArguments = '-NoProfile -WindowStyle Hidden -Command "& {Invoke-TaController}"'
    [string]$updateArguments = '-NoProfile -WindowStyle Hidden -Command "&{Invoke-TaNodeUpgrade}"'
    [string]$keyPath = "HKLM:\SOFTWARE\TeamsAnalyzer\TeamsAnalyzerNode"
    [string]$baseUrl = "https://api.teamsanalyzer.com/beta"
    

#endregion

#region CLASSES

    class AudioTest {
        [string]$DomainName
        [string]$NodeId
        [string]$DialogId
        [string]$ComputerName
        [string]$PacketLossRate
        [string]$RoundTripLatencyInMs
        [string]$PacketsSent
        [string]$PacketsReceived
        [string]$AverageJitterInMs
        [string]$PacketReorderRatio        
        [string]$IpAddress
        [string]$ReflexiveIpAddress
        [decimal]$Latitude
        [decimal]$Longitude
        [int]$PercentCpu
        [decimal]$PercentDiskTime

        AudioTest($keys, $csvCallData, $deviceLocation, $perfData, $reflexiveIpAddress) {
            $this.DomainName = $keys.DomainName
            $this.NodeId = $keys.NodeId
            $this.DialogId = [guid]::NewGuid()
            $this.ComputerName = $($env:COMPUTERNAME)
            $this.IpAddress = $((Get-NetIPConfiguration | Where-Object {$_.IPv4DefaultGateway -ne $null -and $_.NetAdapter.Status -ne "Disconnected"}).IPv4Address.IPAddress)
            $this.ReflexiveIpAddress = $reflexiveIpAddress
            $this.PacketLossRate = $csvCallData.PacketLossRate
            $this.RoundTripLatencyInMs = $csvCallData.RoundTripLatencyInMs
            $this.PacketsSent = $csvCallData.PacketsSent
            $this.PacketsReceived = $csvCallData.PacketsReceived
            $this.AverageJitterInMs = $csvCallData.AverageJitterInMs
            $this.PacketReorderRatio = $csvCallData.PacketReorderRatio
            $this.Latitude = $deviceLocation.Latitude
            $this.Longitude = $deviceLocation.Longitude
            $this.PercentCpu = $perfData.PercentCpu
            $this.PercentDiskTime = $perfData.PercentDiskTime
        }
    }

    class ConnectivityTestModel {
        [string]$ComputerName
        [string]$NodeId
        [bool]$TestPassed
        [array]$ConnectivityErrors

        ConnectivityTestModel() {
            $this.ComputerName = $($env:COMPUTERNAME)
            $this.NodeId = $(GetNodeRegistryEntries).NodeId
        }

        Add($errorItem){
            $this.ConnectivityErrors += ,$errorItem
        }
    }

    class KragleLogger {
        [string]$DomainName
        [guid]$NodeId
        [string]$ComputerName
        [string]$Message
        [string]$EventId
        [string]$LogName
        [string]$EntryType
        

        KragleLogger($keys, $Message, $EventId, $LogName, $EntryType) {
            $this.DomainName = $keys.DomainName
            $this.NodeId = $keys.NodeId
            $this.ComputerName = $($env:COMPUTERNAME)
            $this.Message = $Message
            $this.EventId = $EventId
            $this.LogName = $LogName
            $this.EntryType = $EntryType
        }
    }

    class TeamsAnalyzerNode {
        [string]$DomainName
        [string]$NodeId
        [string]$ComputerName
        [string]$IpAddress
        [string]$ReflexiveIpAddress
        [string]$NodeVersion
        [decimal]$Latitude = 0.00
        [decimal]$Longitude = 0.00

        TeamsAnalyzerNode($DomainName, $ApiKey, $Latitude, $Longitude, $ReflexiveIpAddress, $AllowLocationServices, $AllowPerformanceData) {
            $this.DomainName = $DomainName
            $this.NodeId = [guid]::NewGuid()
            $this.ComputerName = $($env:COMPUTERNAME)
            $this.IpAddress = $((Get-NetIPConfiguration | Where-Object {$_.IPv4DefaultGateway -ne $null -and $_.NetAdapter.Status -ne "Disconnected"} -ErrorAction SilentlyContinue).IPv4Address.IPAddress)
            $this.ReflexiveIpAddress = $ReflexiveIpAddress
            $this.NodeVersion = $(Get-Module -ListAvailable TeamsAnalyzerNode).Version.ToString()
            $this.Latitude = $Latitude
            $this.Longitude = $Longitude
        }
    }

#endregion

#region EXPORTED FUNCTIONS
    function Invoke-TaQualityTest {
        TheKragle -Message "Starting audio quality test on $($env:COMPUTERNAME) at $(Get-Date)." -EntryType Information -LogName Application -EventId 601 -DoNotPostToTenant
        #set maximum time limit for the process to run. It should only take about 17s to execute
        $stopWatch = [Diagnostics.Stopwatch]::StartNew()
        [int]$timeLimit = 60
        $assessmentInstance = Start-Process -FilePath $networkAssessmentToolPath -WorkingDirectory $networkAssessmentToolWorkingDirectory -NoNewWindow -PassThru
        
        do {
            Start-Sleep -Milliseconds 250
        } until (($assessmentInstance.HasExited -eq $true) -or ($stopWatch.Elapsed.TotalSeconds -ge $timeLimit))

        $stopWatch.Stop()
        
        #did it complete or did we have to exit?
        if ($assessmentInstance.HasExited) {
            TheKragle -Message "Completed audio quality test on $($env:COMPUTERNAME) at $(Get-Date)." -EntryType Information -LogName Application -EventId 602 -DoNotPostToTenant
            ProcessCallData
        } else {
            #kill it
            $assessmentInstance.Kill()
            TheKragle -Message "The audio quality test took longer than $($timeLimit) to complete and was killed." -EntryType Error -LogName Application -EventId 610
            break
        }
    }
    function Invoke-TaConnectivityTest {
        #set maximum time limit for the process to run. It should only take about 17s to execute
        $stopWatch = [Diagnostics.Stopwatch]::StartNew()
        [int]$timeLimit = 120 #2mins limit

        $connTestArgs = "/connectivitycheck /verbose"
        TheKragle -Message "Starting transport relay connectivity test." -EntryType Information -LogName Application -EventId 705 -DoNotPostToTenant
        $assessmentInstance = Start-Process -FilePath $networkAssessmentToolPath -WorkingDirectory $networkAssessmentToolWorkingDirectory -ArgumentList $connTestArgs -NoNewWindow -PassThru
        
        do {
            Start-Sleep -Milliseconds 200
        } until (($assessmentInstance.HasExited -eq $true) -or ($stopWatch.Elapsed.TotalSeconds -ge $timeLimit))

        $stopWatch.Stop()
        
        #did it complete or did we have to exit?
        if ($assessmentInstance.HasExited) {
            ProcessConnectivityData
        } else {
            #kill it
            $assessmentInstance.Kill()
            TheKragle -Message "The network assessment tool's connectivity check took longer than $timeLimit to complete and was killed." -EntryType Error -LogName Application -EventId 701
        }
    }
    function New-TaNetworkNode {
        [CmdletBinding()]
        param(
            [parameter(Mandatory=$true)]$ApiKey,
            [parameter(Mandatory=$true)]$DomainName,
            [parameter(Mandatory=$false)]$AutoUpdateNode = $true
        )

        begin {
            if ([system.version](Get-CimInstance -ClassName Win32_OperatingSystem).Version -lt [system.version]6.2) {
                Write-Error -Message "This module requires Windows 8 or higher."
                break
            }

            if (!$ApiKey -or !$DomainName) {
                Write-Error -Message "Please specify both a DomainName and ApiKey variable."
                break
            }
            if ((IsAdministrator) -eq $false) { 
                Write-Error -Message "Please re-run this command in an (Administrator) elevated prompt."
                break
            }
            
            $keys = GetNodeRegistryEntries
            if ($keys.ApiKey -or $keys.DomainName -or $keys.NodeId){
                Write-Error -Message "A previous installation of the node was detected. Please remove the node first by running 'Remove-TaNetworkNode'."
                break
            }

            try {
                #get Tenant settings
                $tenantSettings = Invoke-RestMethod -Uri $($BaseUrl + "/Tenant/" + $DomainName) -Method Get -ContentType 'application/json' -Headers $(@{'Ocp-Apim-Subscription-Key' = $ApiKey})
                if (!$tenantSettings){
                    Write-Error -Message "Could not retrieve tenant settings. Please check your Internet connection and try again."
                    break
                }
            }
            catch {
                Write-Error -Message "There was a problem executing the REST method to retrieve the tenant settings."
                break
            }

            if ($tenantSettings.AllowLocationServices -eq $true){
                $deviceLocation = GetDeviceLocation
                $publicIP = GetPublicIp
            }
        }

        process {

            try {
                #install the SFB NAT tool itself
                ToolServicing -ServicingType Install

                #customize the NetworkAssessmentTool.exe.config file
                ToolConfig

                $newNode = [TeamsAnalyzerNode]::New($DomainName, $ApiKey, $deviceLocation.Latitude, $deviceLocation.Longitude, $publicIP, $tenantSettings.AllowLocationServices, $tenantSettings.AllowPerformanceData)

                New-Item -Path $KeyPath -Force | Out-Null
                New-ItemProperty -Path $KeyPath -Name "DomainName" -Value $DomainName | Out-Null
                New-ItemProperty -Path $KeyPath -Name "ApiKey" -Value $ApiKey | Out-Null
                New-ItemProperty -Path $KeyPath -Name "NodeId" -Value $newNode.NodeId | Out-Null
                New-ItemProperty -Path $KeyPath -Name "AllowLocationServices" -Value $tenantSettings.AllowLocationServices -PropertyType Dword | Out-Null
                New-ItemProperty -Path $KeyPath -Name "AllowPerformanceData" -Value $tenantSettings.AllowPerformanceData -PropertyType Dword | Out-Null

                Write-Verbose -Message "Successfully created registry entries on $($newNode.ComputerName) for $($DomainName)." -Verbose
            } catch {
                Write-Error -Message "Could not create new network node. Please contact support."
                Write-Verbose -Message "Removing registry entries." -Verbose
                Remove-Item -Path "HKLM:\SOFTWARE\TeamsAnalyzer" -Recurse -Confirm:$false
                throw
            }

            # NODE REGISTRATION #
            try {
                Invoke-RestMethod -Uri $($baseUrl + "/NetworkNode/" + $newNode.DomainName + "/" + $newNode.NodeId + "/Register") -Method Post -Body $($newNode | ConvertTo-Json) -ContentType 'application/json' -Headers $(@{'Ocp-Apim-Subscription-Key' = $ApiKey}) | Out-Null
                TheKragle -Message "Successfully registered new node $($newNode.NodeId) for $($newNode.DomainName) on $(Get-Date)." -EntryType Information -LogName Application -EventId 210 
            }
            catch {
                TheKragle -Message "There was a problem generating a new object for the node $($newNode.NodeId) for $($newNode.DomainName)." -EntryType Error -LogName Application -EventId 505
                throw
            }

            #add scheduled task for calling, connectivity, controller tests
            SchedulerServicing -ServicingType Add -Minutes 2 -TaskArguments $controllerArguments -TaskName $controllerTaskName
            SchedulerServicing -ServicingType Add -Minutes 5 -TaskArguments $qualityArguments -TaskName $qualityCaptureTaskName
            SchedulerServicing -ServicingType Add -Minutes 120 -TaskArguments $connectivityArguments  -TaskName $connectivityTestTaskName
            
            if ($AutoUpdateNode) {
                SchedulerServicing -ServicingType Add -Minutes 45 -TaskArguments $updateArguments -TaskName $updateTaskName
            } else {
                SchedulerServicing -ServicingType Add -Minutes 45 -TaskArguments $updateArguments -TaskName $updateTaskName -Enabled:$false
            }
        }

        end{}
    }
    function Remove-TaNetworkNode {
        [CmdletBinding()]
        param(
            [switch]$Force
        )

        if ((IsAdministrator) -eq $false) {
            Write-Warning -Message "Please re-run this command in an (Administrator) elevated prompt."
            break
        }

        $keys = GetNodeRegistryEntries
        if (!$keys.DomainName -eq $null -and !$keys.ApiKey -and !$keys.NodeId) {
            Write-Error -Message "This node appears to not be installed correctly or it is missing critical Registry entries."
            break
        }

        #check if node has been deactivated or not and quit if -Force hasn't been specified
        if ($Force -eq $false) {
            $node = Invoke-RestMethod -Uri $($BaseUrl + "/NetworkNode/" + $keys.DomainName + "/" + $keys.NodeId) -Method Get -ContentType 'application/json' -Headers $(@{'Ocp-Apim-Subscription-Key' = $keys.ApiKey})
            if ($node.Activated -ne $false) {
                TheKragle -Message "An attempt was made to remove this network node installation without first deactivating it. Please deactivate this node before attempting to remove it, or specify the -Force switch on the Remove-TaNetworkNode command." -EntryType Error -LogName Application -EventId 413
                break
            }
        }

        ToolServicing -ServicingType Uninstall
        SchedulerServicing -ServicingType Remove -TaskName $qualityCaptureTaskName
        SchedulerServicing -ServicingType Remove -TaskName $connectivityTestTaskName
        SchedulerServicing -ServicingType Remove -TaskName $controllerTaskName
        SchedulerServicing -ServicingType Remove -TaskName $updateTaskName

        try {
            Write-Verbose -Message "Attempting to remove network node from website using REST API."
            Invoke-RestMethod -Uri $($BaseUrl + '/NetworkNode/' + $keys.DomainName + "/" + $keys.NodeId + "/Delete") -Method Post -ContentType 'application/json' -Headers $(@{'Ocp-Apim-Subscription-Key' = $keys.ApiKey}) | Out-Null
            TheKragle -Message "Successfully removed node from the website at $(Get-Date)." -EntryType Information -LogName Application -EventId 211 -DoNotPostToTenant
        } catch {
            TheKragle -Message "There was a problem removing node $($keys.NodeId) from $($env:COMPUTERNAME). The error was $($_.Exception.Message)." -EntryType Information -LogName Application -EventId 409
        }

        TheKragle -Message "Removing local registry entries from $($env:COMPUTERNAME)." -EntryType Information -LogName Application -EventId 208 -DoNotPostToTenant
        Remove-Item -Path "HKLM:\SOFTWARE\TeamsAnalyzer" -Recurse -Confirm:$false -ErrorAction SilentlyContinue
    }
    function Invoke-TaNodeUpgrade {
        [CmdletBinding()]
        param()
        
        $keys = GetNodeRegistryEntries
        $module = Get-InstalledModule -Name $ProductName -AllVersions
        if ($module -is [array]) {
            for ($i = 0; $i -le $module.Count -2; $i++) {
                #remove old modules before upgrade as there should only be one.
                #since the index is zero-based, we need to subtract two so that we don't remove the current, most recent version.
                try {
                    Uninstall-Module $ProductName -RequiredVersion $module[$i].Version -Force -Confirm:$false -Verbose
                    TheKragle -Message "Successfully removed module version $($module[$i].Version.ToString())." -EntryType Information -LogName Application -EventId 214
                } catch {
                    TheKragle -Message "There was a problem removing version $($module[$i].Version.ToString())" -EntryType Error -LogName Application -EventId 508
                }
            }
            #get most recent version
            [system.version]$moduleVersion = ($module[0]).Version            
        } else {
            [system.version]$moduleVersion = (Get-Module -Name $ProductName -ListAvailable).Version
        }

        if (!$moduleVersion) {
            TheKragle -Message "Could not determine the local version while checking for updates." -EntryType Error -LogName Application -EventId 417
            break
        }
        
        [system.version]$onlineVersion = (Find-Module -Name $ProductName).Version
        if (!$onlineVersion) {
            TheKragle -Message "Could not determine the online version while checking for updates." -EntryType Error -LogName Application -EventId 416
            break
        }

        if ($moduleVersion -lt $onlineVersion) {
            TheKragle -Message "New version found! Upgrading from $($moduleVersion) to $($onlineVersion)." -EntryType Information -LogName Application -EventId 212
            Install-Module -Name $ProductName -RequiredVersion $onlineVersion -AllowClobber -Confirm:$false -Force -Verbose
            try {
                if (!(Get-Module $ProductName -ListAvailable | Where-Object Version -eq $onlineVersion)) {
                    TheKragle -Message "Although a new version of the PowerShell module was detected, there was a problem updating it." -EntryType Error -LogName Application -EventId 507
                    break
                }
                #update node properties
                $body = @{
                    "NodeId" = $keys.NodeId
                    "NodeVersion" = $onlineVersion.ToString()
                }
                Invoke-RestMethod -Uri $($BaseUrl + "/NetworkNode/" + $keys.DomainName) -Method Put -Body $($body | ConvertTo-Json) -ContentType 'application/json' -Headers $(@{'Ocp-Apim-Subscription-Key' = $keys.ApiKey})
                TheKragle -Message "Successfully upgraded PowerShell module from $($moduleVersion) to $($onlineVersion)." -EntryType Information -LogName Application -EventId 299

                #customize the NetworkAssessmentTool.exe.config file just to make sure
                ToolConfig

                #clean up old versions
                # $allVersions = Get-InstalledModule -Name $ProductName -AllVersions
                # $toRemove = $allVersions | Where-Object {$_.Version -ne $onlineVersion}
            } catch {
                TheKragle -Message "There was a problem updating the PowerShell module. The error was: $($_exception.Message)." -EntryType Error -LogName Application -EventId 506
                break
            }
        } else {
            TheKragle -Message "Automatic upgrade check complete. The detected version online was $($onlineVersion) and the installed version is $($moduleVersion)." -EntryType Information -LogName Application -EventId 213
        }
    }
    function Invoke-TaController {
        [CmdletBinding()]
        param()

        try {
            $keys = GetNodeRegistryEntries
        } catch {
            TheKragle -Message "There was a problem getting registry keys for the application. Please try removing and re-installing this module." -EntryType Error -LogName Application -EventId 411 -DoNotPostToTenant
            break
        }

        try {
            $tenantSettings = Invoke-RestMethod -Uri $($baseUrl + "/Tenant/" + $keys.DomainName) -Method Get -ContentType 'application/json' -Headers $(@{'Ocp-Apim-Subscription-Key' = $keys.ApiKey})
            $node = Invoke-RestMethod -Uri $($baseUrl + "/NetworkNode/" +$keys.DomainName + "/" + $keys.NodeId) -Method Get -ContentType 'application/json' -Headers $(@{'Ocp-Apim-Subscription-Key' = $keys.ApiKey})
            Write-Verbose -Message "Results from website query for this node: $($node)"
        } catch {
            TheKragle -Message "There was a problem connecting to the web API to get the settings for this node. Please contact us using the email address on the website for assistance." -EntryType Error -LogName Application -EventId 412
            break
        }

        #set performance/location registry entries
        if ($tenantSettings) {
            Set-ItemProperty -Path $keyPath -Name "AllowLocationServices" -Value $tenantSettings.AllowLocationServices | Out-Null
            Set-ItemProperty -Path $keyPath -Name "AllowPerformanceData" -Value $tenantSettings.AllowPerformanceData | Out-Null
        }

        #check for node activation through WebAPI
        if ($node.Activated) {
            Get-ScheduledTask -TaskName $qualityCaptureTaskName | Enable-ScheduledTask | Out-Null
            Get-ScheduledTask -TaskName $connectivityTestTaskName | Enable-ScheduledTask | Out-Null
        } else {
            Get-ScheduledTask -TaskName $qualityCaptureTaskName | Disable-ScheduledTask | Out-Null
            Get-ScheduledTask -TaskName $connectivityTestTaskName | Disable-ScheduledTask | Out-Null
        }
        
        try {
            Write-Verbose "Setting trigger interval for Quality task to $($node.QualityTestFrequency) minutes."
            SchedulerServicing -ServicingType Update -Minutes $node.QualityTestFrequency -TaskArguments $qualityArguments -TaskName $qualityCaptureTaskName | Out-Null
            
            Write-Verbose "Setting trigger interval for Connectivity task to $($node.ConnectivityTestFrequency) minutes."
            SchedulerServicing -ServicingType Update -Minutes $node.ConnectivityTestFrequency -TaskArguments $connectivityArguments  -TaskName $connectivityTestTaskName | Out-Null
        } catch {
            TheKragle -Message "There was a problem modifying the scheduled tasks on $($env:COMPUTERNAME) at $(Get-Date). $($_.Exception.Message)" -EntryType Warning -LogName Application -EventId 410
        }
    }
 #endregion

#region INTERNAL FUNCTIONS

    function GetNodeRegistryEntries {
        $keys =  (Get-ItemProperty -Path $keyPath -ErrorAction SilentlyContinue)
        return [PSCustomObject]@{
            DomainName = $keys.DomainName
            ApiKey = $keys.ApiKey
            NodeId = $keys.NodeId
            AllowLocationServices = $keys.AllowLocationServices
            AllowPerformanceData = $keys.AllowPerformanceData
            NewNodeWebHook = $keys.NewNodeWebHook
            NodeAlertWebHook = $keys.NodeAlertWebHook
        }
    }
    function TheKragle {
        [CmdletBinding()]
        param(
            [parameter(Mandatory)][string]$Message,
            [parameter(Mandatory)][string][validateset("Error","Information","Warning")]$EntryType,
            [parameter(Mandatory)][string][validateset("Application","Security","System")]$LogName,
            [parameter(Mandatory)][int]$EventId,
            [parameter()][switch]$DoNotPostToTenant
        )
        
        try {
            New-EventLog -LogName Application -Source $ProductName -ErrorAction SilentlyContinue
            Write-Verbose -Message $Message -Verbose
            Write-EventLog -LogName $LogName -Source $ProductName -EntryType $EntryType -Category 0 -EventId $EventId -Message $Message -ErrorAction SilentlyContinue -ErrorVariable errVar    
        } catch {
            Write-Error $_.Exception.Message
        }

        if (!$DoNotPostToTenant.IsPresent) {
            try {
                $keys = GetNodeRegistryEntries
                $kraggleObject = [KragleLogger]::new($keys, $Message, $EventId, $LogName, $EntryType)
                $activityPostResult = Invoke-RestMethod -Uri $($baseUrl + "/ActivityLog/" + $keys.DomainName + "/" + $keys.NodeId) -Method Post -Body $($kraggleObject | ConvertTo-Json) -ContentType 'application/json' -Headers $(@{'Ocp-Apim-Subscription-Key' = $keys.ApiKey})
                Write-Verbose -Message $activityPostResult
            } catch {
                Write-EventLog -LogName $LogName -Source $productName -EntryType Error -Category 0 -EventId 1000 -Message "Error executing logging activity on $($env:COMPUTERNAME) at $(Get-Date). $($_.Exception.Message)."
            }
        }
    }
    function IsAdministrator {
        $user = [Security.Principal.WindowsIdentity]::GetCurrent();
        if ((New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) {
            return $true
        } else {
            return $false
        }
    }
    function GetSaveLocation {
        #read xml file to get save location for call tests and connectivity tests
        [xml]$xmlFile = Get-Content -Path $networkAssessmentToolConfig

        [array]$saveLocations = [PSCustomObject][ordered]@{
            AudioTestSavePath = ($xmlFile.Configuration.Appsettings.Add | Where-Object Key -eq ResultsFilePath).value
            ConnectionTestSavePath = ($xmlFile.Configuration.Appsettings.Add | Where-Object Key -eq OutputFilePath).value
        }

        return $saveLocations
    }
    function ToolServicing {
        [CmdletBinding()]
        param(
            [parameter()][validateset("Install","Uninstall")]$ServicingType)

        begin{}

        process {
            if ($ServicingType -eq "Uninstall") {
                #is it installed already?
                $installedResult = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object {$_.DisplayName -eq $toolName -and $_.BundleProviderKey -ne $null}

                if (!$installedResult) {
                    #not found
                    return
                } else {
                    #yes, remove it first
                    TheKragle -Message "Attempting to remove $($toolName) from $($env:COMPUTERNAME)." -EntryType Information -LogName Application -EventId 209

                    try {
                        $uninstallResult = Start-Process -FilePath $installedResult.BundleCachePath -ArgumentList $toolUninstallArgs -NoNewWindow -PassThru
                        do {
                            Write-Verbose "Waiting for the tool uninstall to complete..."
                            Start-Sleep -Seconds 3
                        } while ($uninstallResult.HasExited -eq $false)
                    } catch {
                        Write-Error -Message "Could not remove $($toolName) using BundleCachePath property."
                        throw
                    }

                    $installedResult = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object {$_.DisplayName -eq $toolName -and $_.BundleProviderKey -ne $null}
                    if (!$installedResult) {
                            TheKragle -Message "Successfully removed $($toolName) from $($env:COMPUTERNAME)." -EntryType Information -LogName Application -EventId 207
                    } else {
                        throw
                    }
                }

                #clean up saved test results directory if the default was used (the user didn't specify a location)
                if ([System.IO.Directory]::Exists((Split-Path -Path $ResultsFilePath))) {
                    try {
                        Write-Verbose "Attempting to remove $ResultsFilePath..."
                        [System.IO.Directory]::Delete((Split-Path -Path $ResultsFilePath), $true)
                        Write-Verbose "Successfully removed $ResultsFilePath."
                    } catch [System.Management.Automation.MethodInvocationException] {
                        $msg = "Could not remove $($ResultsFilePath). This may be due to an explorer or command window locking the location from deletion."
                        Write-Error -Message $msg
                    }
                }
            }
            
            if ($ServicingType -eq "Install") {
                if (!(Test-Path -Path $ResultsFilePath)) {
                    New-Item -ItemType Directory -Path $ResultsFilePath -ErrorAction SilentlyContinue | Out-Null
                    if (!(Get-Item -Path $ResultsFilePath)) {
                        TheKragle -Message "There was a problem creating the directory $($ResultsFilePath) on $($env:COMPUTERNAME)." -EntryType Error -LogName Application -EventId 406
                    }
                }

                if ($installedResult = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object DisplayName -eq $toolName){
                    TheKragle -Message "A request was made to install the $($toolName) however it was already detected. Please remove the tool first by running 'Remove-TaNetworkNode'." -EntryType Error -LogName Application -EventId 502
                    break
                }

                if (!(Test-Path $toolSourcePath)) {
                    TheKragle -Message "Could not find the source files for $($toolName) in $($toolSourcePath)" -EntryType Error -LogName Application -EventId 501
                    throw
                }

                try {
                    TheKragle -Message "Attempting to install the $($toolName) on $($env:COMPUTERNAME)." -EntryType Information -LogName Application -EventId 201
                    $installProcess = Start-Process -FilePath $toolSourcePath -ArgumentList $toolInstallArgs -NoNewWindow -PassThru
                    do {
                        Start-Sleep -Seconds 3
                    } while ($installProcess.HasExited -eq $false)
                } catch {
                    TheKragle -Message "There was a problem installing the $($toolName). The exception was: $($_.Exception.Message)." -EntryType Error -LogName Application -EventId 503
                    throw
                }

                #need to verify it has been installed
                $installedResult = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object DisplayName -eq $toolName

                if ($installedResult) {
                    TheKragle -Message "The $($toolName) was installed successfully." -EntryType Information -LogName Application -EventId 202
                } else {
                    TheKragle -Message "The $($toolName) installation was attempted. There was no specific error, however the installation could not be verified." -EntryType Warning -LogName Application -EventId 404
                }
            }
        }
        
        end{}
    }
    function SchedulerServicing {
        [CmdletBinding()]
        param(
            [parameter(Mandatory = $true)][validateset("Add","Remove","Update")][string]$ServicingType,
            [parameter(Mandatory = $false)][int]$Minutes,
            [parameter()]$TaskArguments,
            [parameter()]$TaskName,
            [parameter()][bool]$Enabled = $true
        )

        begin {}

        process {
            $timeSpan = New-TimeSpan -Minutes $Minutes
            $scheduledTask = Get-ScheduledTask -TaskName $TaskName -TaskPath $taskPath -ErrorAction SilentlyContinue
            #create scheduled task
            switch ($ServicingType) {
                "Add" { 
                    if ($scheduledTask){
                        TheKragle -Message "An attempt was made to add a scheduled task which already exists. The task name is: $($taskName)." -EntryType Error -LogName Application -EventId 401
                        break
                    }
    
                    $action = New-ScheduledTaskAction -Execute $taskAction -Argument $TaskArguments
                    $trigger = New-ScheduledTaskTrigger -At (Get-Date) -Once -RepetitionInterval $timeSpan
                    if ($Enabled) {
                        $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2)
                    } else {
                        $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2) -Disable
                    }
                    
                    $registration = Register-ScheduledTask -TaskPath $taskPath -TaskName $TaskName -Settings $settings -Action $action -Trigger $trigger -User $taskIdentity -RunLevel Highest -ErrorAction SilentlyContinue
                    
                    #should return .State = "Ready"
                    if ($registration.State -eq "Ready") {
                        TheKragle -Message "Successfully registered scheduled task: $($taskName) to run every $($minutes) minute(s) indefinitely." -EntryType Information -LogName Application -EventId 203 -DoNotPostToTenant
                    } else {
                        TheKragle -Message "Could not create scheduled task for task name: $($taskName) on $($env:COMPUTERNAME)" -EntryType Error -LogName Application -EventId 504
                    }
                 }
                "Remove" {
                    if (!$scheduledTask) {
                        #does not exist
                        TheKragle -Message "The scheduled task $($taskName) was not found on this system. It may have been removed already." -EntryType Warning -LogName Application -EventId 402
                    } else {
                        Unregister-ScheduledTask -TaskName $taskName -TaskPath $taskPath -Confirm:$false -ErrorAction SilentlyContinue
                        TheKragle -Message "Successfully removed scheduled task: $($taskName) on $($env:COMPUTERNAME)." -EntryType Information -LogName Application -EventId 204
                    }
    
                    #verify removal
                    $scheduledTask = Get-ScheduledTask -TaskName $taskName -TaskPath $taskPath -ErrorAction SilentlyContinue
                    if ($scheduledTask){
                        TheKragle -Message "There was a problem un-registering the scheduled task: $($taskName) from $($env:COMPUTERNAME)." -EntryType Warning -LogName Application -EventId 405
                    }  
                 }
                "Update" {
                    $scheduledTask.Triggers.Repetition.Interval = "PT$($Minutes)M"
                    Set-ScheduledTask -InputObject $scheduledTask
                 }
                Default {}
            }
        }

        end {}
    }
    function ToolConfig {
        #alter the default config file settings during installation. later we'll grab settings from the cloud set by an admin user.
        try {
            [xml]$xmlFile = Get-Content -Path $networkAssessmentToolConfig

            #Quality test output location
            ($xmlFile.Configuration.Appsettings.Add | Where-Object Key -eq ResultsFilePath).value = $QualityResultsFilePath
    
            #Connectivity test output location
            ($xmlFile.Configuration.Appsettings.Add | Where-Object Key -eq OutputFilePath).value = $ConnectivityResultsFilePath
            
            #Delimiter for quality results
            ($xmlFile.Configuration.Appsettings.Add | Where-Object Key -eq Delimiter).Value = "`t"
            $xmlFile.Save($networkAssessmentToolConfig)
        } catch {
            TheKragle -Message "Error saving $($networkAssessmentToolConfig)." -EntryType Error -LogName Application -EventId 407
        }

        TheKragle -Message "Completed customization of the config file: $($networkAssessmentToolConfig)." -EntryType Information -LogName Application -EventId 205
    }
    function ProcessCallData {
        $keys = GetNodeRegistryEntries

        #check registry to see if we can get physical location
        if ($keys.AllowLocationServices -eq 1) {
            $deviceLocation = GetDeviceLocation
            $reflexiveIpAddress = GetPublicIp
        }

        #check registry to see if we can collect perf data
        if ($keys.AllowPerformanceData -eq 1) {
            $perfData = GetPerformanceData
        }

        try {
            $csvCallData = Get-Content -Path $QualityResultsFilePath | ConvertFrom-Csv -Delimiter "`t"
            $csvCallData.PSObject.Properties | ForEach-Object {
                $_.Value = $_.Value.Replace(",",".")
            }

            $audioTest = [AudioTest]::new($keys, $csvCallData, $deviceLocation, $perfData, $reflexiveIpAddress)
        } catch {
            TheKragle -Message $_.Exception.Message -EntryType Error -LogName Application -EventId 999
            throw
        }

        try {
            Invoke-RestMethod -Uri $($baseUrl + "/AudioTest/" + $keys.DomainName + "/" + $keys.NodeId) -Method Post -Body $($audioTest | ConvertTo-Json) -ContentType 'application/json' -Headers $(@{'Ocp-Apim-Subscription-Key' = $keys.ApiKey})
            TheKragle -Message "Successfully posted audio test to tenant for $($env:COMPUTERNAME) at $(Get-Date)." -EntryType Information -LogName Application -EventId 603 -DoNotPostToTenant
        } catch {
            TheKragle -Message "There was a problem posting the audio test. The error was: $($_.Exception.Message)." -EntryType Error -LogName Application -EventId 611 
            throw
        }
    }
    function ProcessConnectivityData {

        $keys = GetNodeRegistryEntries
        $objConnTest = [ConnectivityTestModel]::new()

        try {
            $connTestResults = Get-Content -Path $ConnectivityResultsFilePath
        } catch {
            TheKragle -Message "Could not read connectivity results data from $($ConnectivityResultsFilePath)" -EntryType Error -LogName Application -EventId 702
            throw
        }
    
        if ($connTestResults.Contains("Verifications completed successfully")) {
            $objConnTest.TestPassed = $true
            TheKragle -Message "Connectivity test completed successfully. All transport relays are reachable on all required ports." -EntryType Information -LogName Application -EventId 703
        } else {
            #parse results file
            $objConnTest.TestPassed = $false
            $regexConn = "Please check if.*"
            foreach($line in Get-Content -Path $ConnectivityResultsFilePath) {
                if($line -match $regexConn){
                    $objConnTest.Add($line)
                }
            }
            TheKragle -Message $("There was a problem reaching all transport relays responsible for media traffic. " + $objConnTest.ConnectivityErrors -join ",") -EntryType Error -LogName Application -EventId 703
        }

        Invoke-RestMethod -Uri $($baseUrl + "/ConnectivityTest/" + $keys.DomainName + "/" + $keys.NodeId) -Method Post -Body $($objConnTest | ConvertTo-Json) -ContentType 'application/json' -Headers $(@{'Ocp-Apim-Subscription-Key' = $keys.ApiKey})
        Remove-Item -Path $ConnectivityResultsFilePath -Force -Confirm:$false
    }
    function GetDeviceLocation {        
        Add-Type -AssemblyName System.Device
        $GeoWatcher = New-Object System.Device.Location.GeoCoordinateWatcher
        $GeoWatcher.Start()
        if ($GeoWatcher.Permission -ne "Granted") {
            Write-Verbose -Message "Permission to use Windows Location Services was denied. Click the start menu, settings, and search for 'location' to enable it. You will need to remove this node and install it again to obtain the node's location." -Verbose
            break
        }

        $counter = 0
        while (($GeoWatcher.Status -ne "Ready") -and ($GeoWatcher.Permission -ne "Denied") -or $counter -ge 5000) {
            Start-Sleep -Milliseconds 100
            $counter ++
        }

        return $GeoWatcher.Position.Location
    }
    function GetPerformanceData {
        return [PSCustomObject]@{
            PercentDiskTime = $(("\PhysicalDisk(_total)\% Disk Time" | Get-Counter).CounterSamples.CookedValue)
            PercentCpu = $(Get-WmiObject Win32_Processor | Measure-Object -Property LoadPercentage -Average | Select-Object -ExpandProperty Average)
        }
    }
    function GetPublicIp {
        try {
            return Invoke-RestMethod http://ipinfo.io/json -ErrorAction SilentlyContinue | Select-Object -exp ip
        } catch {
            #probably over daily limit of queries to this free service
        }
    }
#endregion