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

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

    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]$ApiKey
        [string]$NodeId
        [string]$ComputerName
        [string]$IpAddress
        [string]$ReflexiveIpAddress
        [string]$NodeVersion
        [decimal]$Latitude
        [decimal]$Longitude

        TeamsAnalyzerNode($DomainName, $ApiKey, $Latitude, $Longitude, $ReflexiveIpAddress, $AllowLocationServices, $AllowPerformanceData) {
            $this.DomainName = $DomainName
            $this.ApiKey = $ApiKey
            $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 {
            Write-Host "Time left: $($timeLimit - $stopWatch.Elapsed.Seconds)"
            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 (!$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 720 -TaskArguments $updateArguments -TaskName $updateTaskName
            } else {
                SchedulerServicing -ServicingType Add -Minutes 720 -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}) -ErrorAction SilentlyContinue
            if ($node.Activated -eq $true) {
                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
            } 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 = ","
            $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 ","
            $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 {
            $objConnTest.TestPassed = $false
            TheKragle -Message "There was a problem reaching all transport relays responsible for media traffic." -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 {
        $counter = 0
        Add-Type -AssemblyName System.Device
        $GeoWatcher = New-Object System.Device.Location.GeoCoordinateWatcher
        $GeoWatcher.Start()

        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(0 c:)\% 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