modules/SdnDiag.Utilities/SdnDiag.Utilities.psm1

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Using module .\SdnDiag.Utilities.Helper.psm1

# create local variable to store configuration data
$configurationData = Import-PowerShellDataFile -Path "$PSScriptRoot\SdnDiag.Utilities.Config.psd1"

New-Variable -Name 'SdnDiagnostics_Utilities' -Scope 'Script' -Force -Value @{
    Cache = @{
        FilesExcludedFromCleanup = @()
        TraceFilePath = $null
        WorkingDirectory = $null
    }
    Config = $configurationData
}

##### FUNCTIONS AUTO-POPULATED BELOW THIS LINE DURING BUILD #####
function Confirm-DiskSpace {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'GB')]
        [Parameter(Mandatory = $false, ParameterSetName = 'MB')]
        [System.Char]$DriveLetter,

        [Parameter(Mandatory = $true, ParameterSetName = 'GB')]
        $MinimumGB,

        [Parameter(Mandatory = $true, ParameterSetName = 'MB')]
        $MinimumMB
    )

    $drive = Get-PSDrive $DriveLetter -ErrorAction Stop
    if ($null -eq $drive) {
        throw New-Object System.NullReferenceException("Unable to retrieve PSDrive information")
    }

    $freeSpace = Format-ByteSize -Bytes $drive.Free
    switch ($PSCmdlet.ParameterSetName) {
        'GB' {
            "Required: {0} GB | Available: {1} GB" -f ([float]$MinimumGB).ToString(), $freeSpace.GB | Trace-Output -Level:Verbose
            if ([float]$freeSpace.GB -gt [float]$MinimumGB) {
                return $true
            }

            # if we do not have enough disk space, we want to provide what was required vs what was available
            "Required: {0} GB | Available: {1} GB" -f ([float]$MinimumGB).ToString(), $freeSpace.GB | Trace-Output -Level:Error
            return $false
        }

        'MB' {
            "Required: {0} MB | Available: {1} MB" -f ([float]$MinimumMB).ToString(), $freeSpace.MB | Trace-Output -Level:Verbose
            if ([float]$freeSpace.MB -gt [float]$MinimumMB) {
                return $true
            }

            # if we do not have enough disk space, we want to provide what was required vs what was available
            "Required: {0} MB | Available: {1} MB" -f ([float]$MinimumMB).ToString(), $freeSpace.MB | Trace-Output -Level:Error
            return $false
        }
    }
}

function Confirm-IpAddressInRange {
    <#
        .SYNOPSIS
            Uses .NET to compare the IpAddress specified to see if it falls within the StartAddress and EndAddress range specified.
        .PARAMETER IpAddress
            The IP Address that you want to validate.
        .PARAMETER StartAddress
            The lower end of the IP address range that you want to validate against.
        .PARAMETER EndAddress
            The upper end of the IP address range that you want to validate against.
        .EXAMPLE
            PS> Confirm-IpAddressInRange -IpAddress 192.168.0.10 -StartAddress 192.168.0.1 -EndAddress 192.168.0.255
    #>


    param(
        [System.String]$IpAddress,
        [System.String]$StartAddress,
        [System.String]$EndAddress
    )

    # if null ip address is specified, will default to $false that does not exist within range specified
    if([String]::IsNullOrEmpty($IpAddress)) {
        return $false
    }

    $ip = [System.Net.IPAddress]::Parse($IpAddress).GetAddressBytes()
    [array]::Reverse($ip)
    $ip = [System.BitConverter]::ToUInt32($ip, 0)

    $from = [System.Net.IPAddress]::Parse($StartAddress).GetAddressBytes()
    [array]::Reverse($from)
    $from = [System.BitConverter]::ToUInt32($from, 0)

    $to = [System.Net.IPAddress]::Parse($EndAddress).GetAddressBytes()
    [array]::Reverse($to)
    $to = [System.BitConverter]::ToUInt32($to, 0)

    $from -le $ip -and $ip -le $to
}

function Confirm-IsAdmin {
    # ensure that the module is running as local administrator
    $elevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
    if (-NOT $elevated) {
        throw New-Object System.Exception("This function requires elevated permissions. Run PowerShell as an Administrator and import the module again.")
    }
}

function Confirm-IsNetworkController {
    $config = Get-SdnModuleConfiguration -Role 'NetworkController'
    $confirmFeatures = Confirm-RequiredFeaturesInstalled -Name $config.windowsFeature
    if (-NOT ($confirmFeatures)) {
        throw New-Object System.NotSupportedException("The current machine is not a NetworkController, run this on NetworkController or use -NetworkController parameter (if available).")
    }
}

function Confirm-ProvisioningStateSucceeded {
    <#
    .SYNOPSIS
        Used to verify the resource within the NC NB API is succeeded
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.Uri]$Uri,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]$Credential,

        [Parameter(Mandatory = $false)]
        [Switch]$DisableKeepAlive,

        [Parameter(Mandatory = $false)]
        [Switch]$UseBasicParsing,

        [Parameter(Mandatory = $false)]
        [Int]$TimeoutInSec = 120
    )

    $splat = @{
        Uri              = $Uri
        Credential       = $Credential
        DisableKeepAlive = $DisableKeepAlive
        UseBasicParsing  = $UseBasicParsing
        Method           = 'Get'
        ErrorAction      = 'Stop'
    }

    $stopWatch = [System.Diagnostics.Stopwatch]::StartNew()
    while ($true) {
        if ($stopWatch.Elapsed.TotalSeconds -gt $TimeoutInSec) {
            $stopWatch.Stop()
            throw New-Object System.TimeoutException("ProvisioningState for $($result.resourceId) did not succeed within the alloted time")
        }

        $result = Invoke-RestMethodWithRetry @Splat
        switch ($result.properties.provisioningState) {
            'Updating' {
                "ProvisioningState for $($result.resourceId) is updating. Waiting for completion..." | Trace-Output
                Start-Sleep -Seconds 5
            }

            'Succeeded' {
                $stopWatch.Stop()

                "ProvisioningState for $($result.resourceId) succeeded." | Trace-Output
                return $true
            }

            'Failed' {
                $stopWatch.Stop()
                throw New-Object System.Exception("Failed to update $($result.resourceId). Examine Network Controller logs for more information.")
            }

            default {
                throw New-Object System.Exception("Unknown provisioning state $($result.properties.provisioningState)")
            }
        }
    }
}

function Confirm-RequiredFeaturesInstalled {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [System.String[]]$Name
    )

    try {

        if($null -eq $Name){
            return $true
        }
        else {
            foreach($obj in $Name){
                if(!(Get-WindowsFeature -Name $obj).Installed){
                    return $false
                }
            }

            return $true
        }
    }
    catch {
        $_ | Trace-Exception
        return $false
    }
}

function Confirm-RequiredModulesLoaded {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [System.String[]]$Name
    )

    try {

        if($null -eq $Name){
            return $true
        }
        else {
            foreach($obj in $Name){
                if(!(Get-Module -Name $obj)){
                    Import-Module -Name $obj -Force -ErrorAction Stop
                }
            }

            return $true
        }
    }
    catch {
        $_ | Trace-Exception
        return $false
    }
}

function Confirm-UserInput {
    param(
        [Parameter(Position = 0, ValueFromPipeline = $true)]
        [System.String]$Message = "Do you want to continue with this operation? [Y/N]: ",
        [System.String]$BackgroundColor = "Black",
        [System.String]$ForegroundColor = "Yellow"
    )

    $Message | Trace-Output -Level:Verbose
    Write-Host -ForegroundColor:$ForegroundColor -BackgroundColor:$BackgroundColor -NoNewline $Message
    $answer = Read-Host
    if ($answer) {
        $answer | Trace-Output -Level:Verbose
    }
    else {
        "User pressed enter key" | Trace-Output -Level:Verbose
    }

    return ($answer -ieq 'y')
}

function Convert-FileSystemPathToUNC {
    <#
    .SYNOPSIS
        Converts a local file path to a computer specific admin UNC path, such as C:\temp\myfile.txt to \\azs-srng01\c$\temp\myfile.txt
    #>


    param(
        [System.String]$ComputerName,
        [System.String]$Path
    )
    
    $newPath = $path.Replace([System.IO.Path]::GetPathRoot($Path),[System.IO.Path]::GetPathRoot($Path).Replace(':','$'))
    return ("\\{0}\{1}" -f $ComputerName, $newPath)
}
function Copy-FileFromRemoteComputer {
    <#
    .SYNOPSIS
        Copies an item from one location to another using FromSession
    .PARAMETER Path
        Specifies, as a string array, the path to the items to copy. Wildcard characters are permitted.
    .PARAMETER ComputerName
        Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers.
    .PARAMETER Destination
        Specifies the path to the new location. The default is the current directory.
        To rename the item being copied, specify a new name in the value of the Destination parameter.
    .PARAMETER Credential
        Specifies a user account that has permission to perform this action. The default is the current user.
        Type a user name, such as User01 or Domain01\User01, or enter a PSCredential object generated by the Get-Credential cmdlet. If you type a user name, you're prompted to enter the password.
    .PARAMETER Recurse
        Indicates that this cmdlet does a recursive copy.
    .PARAMETER Force
        Indicates that this cmdlet copies items that can't otherwise be changed, such as copying over a read-only file or alias.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.String[]]$Path,

        [Parameter(Mandatory = $true)]
        [System.String[]]$ComputerName,

        [Parameter(Mandatory = $false)]
        [System.IO.FileInfo]$Destination = (Get-WorkingDirectory),

        [Parameter(Mandatory = $false, ValueFromPipeline = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false)]
        [Switch]$Recurse,

        [Parameter(Mandatory = $false)]
        [Switch]$Force
    )

    try {
        foreach ($object in $ComputerName) {
            if (Test-ComputerNameIsLocal -ComputerName $object) {
                "Detected that {0} is local machine" -f $object | Trace-Output
                foreach ($subPath in $Path) {
                    if ($subPath -eq $Destination.FullName) {
                        "Path {0} and Destination {1} are the same. Skipping" -f $subPath, $Destination.FullName | Trace-Output -Level:Warning
                    }
                    else {
                        "Copying {0} to {1}" -f $subPath, $Destination.FullName | Trace-Output
                        Copy-Item -Path $subPath -Destination $Destination.FullName -Recurse -Force -ErrorAction:Continue
                    }
                }
            }
            else {
                # try SMB Copy first and fallback to WinRM
                try {
                    Copy-FileFromRemoteComputerSMB -Path $Path -ComputerName $object -Destination $Destination -Force:($Force.IsPresent) -Recurse:($Recurse.IsPresent) -ErrorAction Stop
                }
                catch {
                    "{0}. Attempting to copy files using WinRM" -f $_ | Trace-Output -Level:Warning

                    try {
                        Copy-FileFromRemoteComputerWinRM -Path $Path -ComputerName $object -Destination $Destination -Force:($Force.IsPresent) -Recurse:($Recurse.IsPresent) -Credential $Credential
                    }
                    catch {
                        # Catch the copy failed exception to not stop the copy for other computers which might success
                        "{0}. Unable to copy files" -f $_ | Trace-Output -Level:Error
                        continue
                    }
                }
            }
        }
    }
    catch {
        $_ | Trace-Exception
        $_ | Write-Error
    }
}

function Copy-FileFromRemoteComputerSMB {
    <#
    .SYNOPSIS
        Copies an item from one location to another using FromSession
    .PARAMETER Path
        Specifies, as a string array, the path to the items to copy. Wildcard characters are permitted.
    .PARAMETER ComputerName
        Type the NetBIOS name, an IP address, or a fully qualified domain name of the remote computer.
    .PARAMETER Destination
        Specifies the path to the new location. The default is the current directory.
        To rename the item being copied, specify a new name in the value of the Destination parameter.
    .PARAMETER Recurse
        Indicates that this cmdlet does a recursive copy.
    .PARAMETER Force
        Indicates that this cmdlet copies items that can't otherwise be changed, such as copying over a read-only file or alias.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.String[]]$Path,

        [Parameter(Mandatory = $true)]
        [System.String]$ComputerName,

        [Parameter(Mandatory = $false)]
        [System.IO.FileInfo]$Destination = (Get-WorkingDirectory),

        [Parameter(Mandatory = $false, ValueFromPipeline = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false)]
        [Switch]$Recurse,

        [Parameter(Mandatory = $false)]
        [Switch]$Force
    )

    begin {
        $params = @{
            'Path'          = $null
            'Destination'   = $Destination.FullName
            'Force'         = $Force.IsPresent
            'Recurse'       = $Recurse.IsPresent
        }
        if ($Credential -ne [System.Management.Automation.PSCredential]::Empty -and $null -ne $Credential) {
            $params.Add('Credential', $Credential)
        }

        # set this to suppress the information status bar from being displayed
        $Global:ProgressPreference = 'SilentlyContinue'
        $testNetConnection = Test-NetConnection -ComputerName $ComputerName -Port 445 -InformationLevel Quiet
        $Global:ProgressPreference = 'Continue'

        # if we cannot access the remote computer via SMB port, then we want to terminate
        if (-NOT ($testNetConnection)) {
            $msg = "Unable to establish TCP connection to {0}:445" -f $ComputerName
            throw New-Object System.Exception($msg)
        }
    }

    process {
        foreach ($subPath in $Path) {
            $remotePath = Convert-FileSystemPathToUNC -ComputerName $ComputerName -Path $subPath
            if (-NOT (Test-Path -Path $remotePath)) {
                "Unable to find {0}" -f $remotePath | Trace-Output -Level:Error
            }
            else {
                $params.Path = $remotePath

                try {
                    "Copying {0} to {1}" -f $params.Path, $params.Destination | Trace-Output
                    Copy-Item @params
                }
                catch [System.IO.IOException] {
                    if ($_.Exception.Message -ilike "*used by another process*") {
                        "{0}\{1} is in use by another process" -f $remotePath, $_.CategoryInfo.TargetName | Trace-Output -Level:Error
                        continue
                    }

                    if ($_.Exception.Message -ilike "*already exists*") {
                        "{0}\{1} already exists" -f $remotePath, $_.CategoryInfo.TargetName | Trace-Output -Level:Error
                        continue
                    }

                    throw $_
                }
            }
        }
    }
}


function Copy-FileFromRemoteComputerWinRM {
    <#
    .SYNOPSIS
        Copies an item from one location to another using FromSession
    .PARAMETER Path
        Specifies, as a string array, the path to the items to copy. Wildcard characters are permitted.
    .PARAMETER ComputerName
        Type the NetBIOS name, an IP address, or a fully qualified domain name of the computer.
    .PARAMETER Destination
        Specifies the path to the new location. The default is the current directory.
        To rename the item being copied, specify a new name in the value of the Destination parameter.
    .PARAMETER Credential
        Specifies a user account that has permission to perform this action. The default is the current user.
        Type a user name, such as User01 or Domain01\User01, or enter a PSCredential object generated by the Get-Credential cmdlet. If you type a user name, you're prompted to enter the password.
    .PARAMETER Recurse
        Indicates that this cmdlet does a recursive copy.
    .PARAMETER Force
        Indicates that this cmdlet copies items that can't otherwise be changed, such as copying over a read-only file or alias.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.String[]]$Path,

        [Parameter(Mandatory = $true)]
        [System.String]$ComputerName,

        [Parameter(Mandatory = $false)]
        [System.IO.FileInfo]$Destination = (Get-WorkingDirectory),

        [Parameter(Mandatory = $false, ValueFromPipeline = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false)]
        [Switch]$Recurse,

        [Parameter(Mandatory = $false)]
        [Switch]$Force
    )

    $session = New-PSRemotingSession -ComputerName $ComputerName -Credential $Credential
    if ($session) {
        foreach ($subPath in $Path) {
            "Copying {0} to {1} using WinRM Session {2}" -f $subPath, $Destination.FullName, $session.Name | Trace-Output
            Copy-Item -Path $subPath -Destination $Destination.FullName -FromSession $session -Force:($Force.IsPresent) -Recurse:($Recurse.IsPresent) -ErrorAction:Continue
        }
    }
    else {
        $msg = "Unable to copy files from {0} as remote session could not be established" -f $ComputerName
        throw New-Object System.Exception($msg)
    }
}

function Copy-FileToRemoteComputer {
    <#
    .SYNOPSIS
        Copies an item from local path to a path at remote server
    .PARAMETER Path
        Specifies, as a string array, the path to the items to copy. Wildcard characters are permitted.
    .PARAMETER ComputerName
        Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers.
    .PARAMETER Destination
        Specifies the path to the new location. The default is the current directory.
        To rename the item being copied, specify a new name in the value of the Destination parameter.
    .PARAMETER Credential
        Specifies a user account that has permission to perform this action. The default is the current user.
        Type a user name, such as User01 or Domain01\User01, or enter a PSCredential object generated by the Get-Credential cmdlet. If you type a user name, you're prompted to enter the password.
    .PARAMETER Recurse
        Indicates that this cmdlet does a recursive copy.
    .PARAMETER Force
        Indicates that this cmdlet copies items that can't otherwise be changed, such as copying over a read-only file or alias.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.String[]]$Path,

        [Parameter(Mandatory = $true)]
        [System.String[]]$ComputerName,

        [Parameter(Mandatory = $false)]
        [System.IO.FileInfo]$Destination = (Get-WorkingDirectory),

        [Parameter(Mandatory = $false, ValueFromPipeline = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false)]
        [Switch]$Recurse,

        [Parameter(Mandatory = $false)]
        [Switch]$Force
    )

    try {
        foreach ($object in $ComputerName) {
            if (Test-ComputerNameIsLocal -ComputerName $object) {
                "Detected that {0} is local machine" -f $object | Trace-Output
                foreach ($subPath in $Path) {
                    if ($subPath -eq $Destination.FullName) {
                        "Path {0} and Destination {1} are the same. Skipping" -f $subPath, $Destination.FullName | Trace-Output -Level:Warning
                    }
                    else {
                        "Copying {0} to {1}" -f $subPath, $Destination.FullName | Trace-Output
                        Copy-Item -Path $subPath -Destination $Destination.FullName -Recurse -Force
                    }
                }
            }
            else {
                # try SMB Copy first and fallback to WinRM
                try {
                    Copy-FileToRemoteComputerSMB -Path $Path -ComputerName $object -Destination $Destination -Force:($Force.IsPresent) -Recurse:($Recurse.IsPresent) -ErrorAction Stop
                }
                catch {
                    "{0}. Attempting to copy files using WinRM" -f $_ | Trace-Output -Level:Warning

                    try {
                        Copy-FileToRemoteComputerWinRM -Path $Path -ComputerName $object -Destination $Destination -Credential $Credential -Force:($Force.IsPresent) -Recurse:($Recurse.IsPresent)
                    }
                    catch {
                        # Catch the copy failed exception to not stop the copy for other computers which might success
                        "{0}. Unable to copy files" -f $_ | Trace-Output -Level:Error
                        continue
                    }
                }
            }
        }
    }
    catch {
        $_ | Trace-Exception
        $_ | Write-Error
    }
}

function Copy-FileToRemoteComputerSMB {
    <#
    .SYNOPSIS
        Copies an item from local path to a path at remote server via SMB
    .PARAMETER Path
        Specifies, as a string array, the path to the items to copy. Wildcard characters are permitted.
    .PARAMETER ComputerName
        Type the NetBIOS name, an IP address, or a fully qualified domain name of the remote computer.
    .PARAMETER Destination
        Specifies the path to the new location. The default is the current directory.
        To rename the item being copied, specify a new name in the value of the Destination parameter.
    .PARAMETER Credential
        Specifies a user account that has permission to perform this action. The default is the current user.
        Type a user name, such as User01 or Domain01\User01, or enter a PSCredential object generated by the Get-Credential cmdlet. If you type a user name, you're prompted to enter the password.
    .PARAMETER Recurse
        Indicates that this cmdlet does a recursive copy.
    .PARAMETER Force
        Indicates that this cmdlet copies items that can't otherwise be changed, such as copying over a read-only file or alias.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.String[]]$Path,

        [Parameter(Mandatory = $true)]
        [System.String]$ComputerName,

        [Parameter(Mandatory = $false)]
        [System.IO.FileInfo]$Destination = (Get-WorkingDirectory),

        [Parameter(Mandatory = $false, ValueFromPipeline = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false)]
        [Switch]$Recurse,

        [Parameter(Mandatory = $false)]
        [Switch]$Force
    )

    begin {
        $params = @{
            'Path'          = $null
            'Destination'   = $null
            'Force'         = $Force.IsPresent
            'Recurse'       = $Recurse.IsPresent
        }
        if ($Credential -ne [System.Management.Automation.PSCredential]::Empty -and $null -ne $Credential) {
            $params.Add('Credential', $Credential)
        }

        # set this to suppress the information status bar from being displayed
        $Global:ProgressPreference = 'SilentlyContinue'
        $testNetConnection = Test-NetConnection -ComputerName $ComputerName -Port 445 -InformationLevel Quiet
        $Global:ProgressPreference = 'Continue'

        if (-NOT ($testNetConnection)) {
            $msg = "Unable to establish TCP connection to {0}:445" -f $ComputerName
            throw New-Object System.Exception($msg)
        }

        [System.IO.FileInfo]$remotePath = Convert-FileSystemPathToUNC -ComputerName $ComputerName -Path $Destination.FullName
        $params.Destination = $remotePath.FullName
    }
    process {
        foreach ($subPath in $Path) {
            $params.Path = $subPath

            try {
                "Copying {0} to {1}" -f $params.Path, $params.Destination | Trace-Output
                Copy-Item @params
            }
            catch [System.IO.IOException] {
                if ($_.Exception.Message -ilike "*used by another process*") {
                    "{0}\{1} is in use by another process" -f $remotePath, $_.CategoryInfo.TargetName | Trace-Output -Level:Error
                    continue
                }

                if ($_.Exception.Message -ilike "*already exists*") {
                    "{0}\{1} already exists" -f $remotePath, $_.CategoryInfo.TargetName | Trace-Output -Level:Error
                    continue
                }

                throw $_
            }
        }
    }
}

function Copy-FileToRemoteComputerWinRM {
    <#
    .SYNOPSIS
        Copies an item from one location to another using ToSession
    .PARAMETER Path
        Specifies, as a string array, the path to the items to copy. Wildcard characters are permitted.
    .PARAMETER ComputerName
        Type the NetBIOS name, an IP address, or a fully qualified domain name of one remote computer.
    .PARAMETER Destination
        Specifies the path to the new location. The default is the current directory.
        To rename the item being copied, specify a new name in the value of the Destination parameter.
    .PARAMETER Credential
        Specifies a user account that has permission to perform this action. The default is the current user.
        Type a user name, such as User01 or Domain01\User01, or enter a PSCredential object generated by the Get-Credential cmdlet. If you type a user name, you're prompted to enter the password.
    .PARAMETER Recurse
        Indicates that this cmdlet does a recursive copy.
    .PARAMETER Force
        Indicates that this cmdlet copies items that can't otherwise be changed, such as copying over a read-only file or alias.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.String[]]$Path,

        [Parameter(Mandatory = $true)]
        [System.String]$ComputerName,

        [Parameter(Mandatory = $false)]
        [System.IO.FileInfo]$Destination = (Get-WorkingDirectory),

        [Parameter(Mandatory = $false, ValueFromPipeline = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false)]
        [Switch]$Recurse,

        [Parameter(Mandatory = $false)]
        [Switch]$Force
    )

    $session = New-PSRemotingSession -ComputerName $ComputerName -Credential $Credential
    if ($session) {
        # copy the files to the destination using WinRM
        foreach ($subPath in $Path) {
            "Copying {0} to {1} using WinRM Session {2}" -f $subPath, $Destination.FullName, $session.Name | Trace-Output
            Copy-Item -Path $subPath -Destination $Destination.FullName -ToSession $session -Force:($Force.IsPresent) -Recurse:($Recurse.IsPresent) -ErrorAction:Continue
        }
    }
    else {
        $msg = "Unable to copy files to {0} as remote session could not be established" -f $ComputerName
        throw New-Object System.Exception($msg)
    }
}

function Export-ObjectToFile {
    <#
    .SYNOPSIS
        Save an object to a file in a consistent format.
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [Object[]]$Object,

        [Parameter(Mandatory = $true)]
        [System.IO.FileInfo]$FilePath,

        [Parameter(Mandatory = $false)]
        [System.String]$Prefix,

        [Parameter(Mandatory = $true)]
        [System.String]$Name,

        [Parameter(Mandatory = $false)]
        [ValidateSet("json","csv","txt")]
        [System.String]$FileType = "json",

        [Parameter(Mandatory = $false)]
        [ValidateSet("Table","List")]
        [System.String]$Format,

        [Parameter(Mandatory = $false)]
        [System.String]$Depth = 2
    )

    begin {
        $arrayList = [System.Collections.ArrayList]::new()

        # if object is null, then exit
        if ($null -eq $Object) {
            return
        }
    }
    process {
        foreach ($obj in $Object) {
            [void]$arrayList.add($obj)
        }
    }
    end {
        try {
            # build the file directory and name that will be used to export the object out
            if($Prefix){
                [System.String]$formattedFileName = "{0}\{1}_{2}.{3}" -f $FilePath.FullName, $Prefix, $Name, $FileType
            }
            else {
                [System.String]$formattedFileName = "{0}\{1}.{2}" -f $FilePath.FullName, $Name, $FileType
            }

            [System.IO.FileInfo]$fileName = $formattedFileName

            # create the parent directory structure if does not already exist
            if(!(Test-Path -Path $fileName.Directory -PathType Container)){
                "Creating directory {0}" -f $fileName.Directory | Trace-Output -Level:Verbose
                $null = New-Item -Path $fileName.Directory -ItemType Directory
            }

            "Creating file {0}" -f $fileName | Trace-Output -Level:Verbose
            switch($FileType){
                "json" {
                    $arrayList | ConvertTo-Json -Depth $Depth | Out-File -FilePath $fileName -Force
                }
                "csv" {
                    $arrayList | Export-Csv -NoTypeInformation -Path $fileName -Force
                }
                "txt" {
                    $FormatEnumerationLimit = 500
                    switch($Format){
                        'Table' {
                            $arrayList | Format-Table -AutoSize -Wrap | Out-String -Width 4096 | Out-File -FilePath $fileName -Force
                        }
                        'List' {
                            $arrayList | Format-List -Property * | Out-File -FilePath $fileName -Force
                        }
                        default {
                            $arrayList | Out-File -FilePath $fileName -Force
                        }
                    }
                }
            }
        }
        catch {
            $_ | Trace-Exception
            $_ | Write-Error
        }
    }
}

function Format-ByteSize {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [double]$Bytes
    )

    $gb = [string]::Format([System.Globalization.CultureInfo]::InvariantCulture, "{0}", $Bytes / 1GB)
    $mb = [string]::Format([System.Globalization.CultureInfo]::InvariantCulture, "{0}", $Bytes / 1MB)

    return ([PSCustomObject]@{
        GB = $gb
        MB = $mb
    })
}

function Format-MacAddress {
    <#
    .SYNOPSIS
        Returns a consistent MAC address back formatted with or without dashes
    .PARAMETER MacAddress
        MAC Address to canonicalize into standard format
    .PARAMETER Dashes
        Optional. If specified, the MAC address will be formatted with dashes
    #>

    param (
        [System.String]$MacAddress,
        [Switch]$Dashes
    )

    if ($Dashes) {
        return (Format-MacAddressWithDashes -MacAddress $MacAddress)
    }
    else {
        return (Format-MacAddressNoDashes -MacAddress $MacAddress)
    }
}

function Format-MacAddressNoDashes {
    <#
    .SYNOPSIS
        Returns a consistent MAC address back formatted without dashes
    .PARAMETER MacAddress
        MAC Address to canonicalize into standard format
    #>

    param (
        [System.String]$MacAddress
    )

    "Processing {0}" -f $MacAddress | Trace-Output -Level:Verbose

    if($MacAddress.Split('-').Count -eq 6){
        foreach($obj in $MacAddress.Split('-')){
            if($obj.Length -ne 2){
                throw New-Object System.ArgumentOutOfRangeException("Invalid MAC Address. Unable to split into expected pairs")
            }
        }
    }

    $MacAddress = $MacAddress.Replace('-','').Trim().ToUpper()
    return ($MacAddress.ToString())
}

function Format-MacAddressWithDashes {
    <#
    .SYNOPSIS
        Returns a consistent MAC address back formatted with dashes
    .PARAMETER MacAddress
        MAC Address to canonicalize into standard format
    #>

    param (
        [System.String]$MacAddress
    )

    "Processing {0}" -f $MacAddress | Trace-Output -Level:Verbose

    if($MacAddress.Split('-').Count -eq 6){
        foreach($obj in $MacAddress.Split('-')){
            if($obj.Length -ne 2){
                throw New-Object System.ArgumentOutOfRangeException("Invalid MAC Address. Unable to split into expected pairs")
            }
        }

        return ($MacAddress.ToString().ToUpper())
    }

    if($MacAddress.Length -ne 12){
        throw New-Object System.ArgumentOutOfRangeException("Invalid MAC Address. Length is not equal to 12 ")
    }
    else {
        $MacAddress = $MacAddress.Insert(2,"-").Insert(5,"-").Insert(8,"-").Insert(11,"-").Insert(14,"-").Trim().ToUpper()
        return ($MacAddress.ToString())
    }
}

function Format-NetshTraceProviderAsString {
    <#
        .SYNOPSIS
            Formats the netsh trace providers into a string that can be passed to a netsh command
        .PARAMETER Provider
            The ETW provider in GUID format
        .PARAMETER Level
            Optional. Specifies the level to enable for the corresponding provider.
        .PARAMETER Keywords
            Optional. Specifies the keywords to enable for the corresponding provider.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [guid]$Provider,

        [Parameter(Mandatory=$false)]
        [string]$Level,

        [Parameter(Mandatory=$false)]
        [string]$Keywords
    )

    try {
        [guid]$guid = [guid]::Empty
        if(!([guid]::TryParse($Provider,[ref]$guid))){
            throw "The value specified in the Provider argument must be in GUID format"
        }
        [string]$formattedString = $null
        foreach($param in $PSBoundParameters.GetEnumerator()){
            if($param.Value){
                if($param.Key -ieq "Provider"){
                    $formattedString += "$($param.Key)='$($param.Value.ToString("B"))' "
                }
                elseif($param.Key -ieq "Level" -or $param.Key -ieq "Keywords") {
                    $formattedString += "$($param.Key)=$($param.Value) "
                }
            }
        }

        return $formattedString.Trim()
    }
    catch {
        $_ | Trace-Exception
        $_ | Write-Error
    }
}

function Get-ComputerNameFQDNandNetBIOS {
    <#
    .SYNOPSIS
        Returns back the NetBIOS and FQDN name of the computer
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline)]
        [System.String]$ComputerName
    )

    # we know Windows has some strict requirements around NetBIOS/DNS name of the computer
    # so we can safely make some assumptions that if period (.) exists, then assume the ComputerName being passed into function
    # is a FQDN in which case we want to split the string and assign the NetBIOS name
    if ($ComputerName.Contains('.')) {
        [System.String]$computerNameNetBIOS = $ComputerName.Split('.')[0]
        [System.String]$computerNameFQDN = $ComputerName
    }

    # likewise, if no period (.) specified as part of the ComputerName we can assume we were passed a NetBIOS name of the object
    # in which case we will try to resolve via DNS. If any failures when resolving the HostName from DNS, will catch and default to
    # current user dns domain in best effort
    else {
        [System.String]$computerNameNetBIOS = $ComputerName
        try {
            [System.String]$computerNameFQDN = [System.Net.Dns]::GetHostByName($ComputerName).HostName
        }
        catch {
            [System.String]$computerNameFQDN = "$($ComputerName).$($env:USERDNSDOMAIN)"
        }
    }

    return [PSCustomObject]@{
        ComputerNameNetBIOS = $computerNameNetBIOS
        ComputerNameFQDN    = $computerNameFQDN
    }
}

function Get-FolderSize {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
        [System.IO.FileInfo]$Path,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [System.IO.FileInfo[]]$FileName,

        [Parameter(Mandatory = $false, ParameterSetName = 'File')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Path')]
        [Switch]$Total
    )

    try {
        $arrayList = [System.Collections.ArrayList]::new()

        switch ($PSCmdlet.ParameterSetName) {
            'File' {
                $items = Get-Item -Path $FileName -Force
            }
            'Path' {
                $items = Get-ChildItem -Path $Path -Force
            }
        }

        foreach ($item in $items) {
            if ($item.PSIsContainer -eq $true) {
                $subFolderItems = Get-ChildItem $item.FullName -Recurse | Where-Object { $_.PSIsContainer -eq $false } | Measure-Object -Property Length -Sum | Select-Object Sum
                $folderSize = Format-ByteSize -Bytes $subFolderItems.sum

                [void]$arrayList.Add([PSCustomObject]@{
                    Name     = $item
                    SizeInGB = $folderSize.GB
                    SizeInMB = $folderSize.MB
                    Size     = $subFolderItems.sum
                    Type     = "Folder"
                    FullName = $item.FullName
                })

            }
            else {
                $fileSize = Format-ByteSize -Bytes $item.Length
                [void]$arrayList.Add([PSCustomObject]@{
                    Name     = $item.Name
                    SizeInGB = $fileSize.GB
                    SizeInMB = $fileSize.MB
                    Size     = $item.Length
                    Type     = "File"
                    FullName = $item.FullName
                })
            }
        }

        if ($Total) {
            $totalSize = $arrayList | Measure-Object -Property Size -Sum
            $totalSizeFormatted = Format-ByteSize -Bytes $totalSize.Sum

            return $totalSizeFormatted
        }

        return ($arrayList | Sort-Object Type, Size)
    }
    catch {
        $_ | Trace-Exception
        $_ | Write-Error
    }
}

function Get-FormattedDateTimeUTC {
    return ([DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss'))
}

function Get-FunctionFromFile {
    <#
    .SYNOPSIS
        Enumerates a ps1 file to identify the functions defined within
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.IO.FileInfo]$FilePath,

        [Parameter(Mandatory = $true)]
        [System.String]$Verb
    )

    try {
        # get the raw content of the script
        $code = Get-Content -Path $FilePath.FullName -Raw

        # list all the functions in ps1 using language namespace parser
        $functionName = [Management.Automation.Language.Parser]::ParseInput($code, [ref]$null, [ref]$null).EndBlock.Statements.FindAll([Func[Management.Automation.Language.Ast,bool]]{$args[0] -is [Management.Automation.Language.FunctionDefinitionAst]}, $false) `
            | Select-Object -ExpandProperty Name

        if($functionName){
            return ($functionName | Where-Object {$_ -like "$Verb-*"})
        }
        else {
            return $null
        }
    }
    catch {
        $_ | Trace-Exception
        $_ | Write-Error
    }
}

function Get-SdnCache {
    <#
        .SYNOPSIS
            Returns the cache results stored with the global SdnDiagnostics cache variable
    #>


    param (
        [System.String]$Name
    )

    return $Global:SdnDiagnostics.Cache[$Name]
}

function Get-TraceOutputFile {
    return [System.String]$Script:SdnDiagnostics_Utilities.Cache.TraceFilePath
}

function Get-UserInput {
    <#
    .SYNOPSIS
        Used in scenarios where you need to prompt the user for input
    .PARAMETER Message
        The message that you want to display to the user
    .EXAMPLE
        $choice = Get-UserInput -Message "Do you want to proceed with operation? [Y/N]: "
        Switch($choice){
            'Y' {Do action}
            'N' {Do action}
            default {Do action}
        }
    #>


    param
    (
        [Parameter(Position = 0, ValueFromPipeline = $true)]
        [string]$Message,
        [string]$BackgroundColor = "Black",
        [string]$ForegroundColor = "Yellow"
    )

    Write-Host -ForegroundColor:$ForegroundColor -BackgroundColor:$BackgroundColor -NoNewline $Message;
    return Read-Host
}

function Get-WorkingDirectory {

    # check to see if the working directory has been configured into cache
    # otherwise set the cache based on what we have defined within our configuration file
    if ([String]::IsNullOrEmpty($Script:SdnDiagnostics_Utilities.Cache.WorkingDirectory)) {
        $Script:SdnDiagnostics_Utilities.Cache.WorkingDirectory = $Script:SdnDiagnostics_Utilities.Config.WorkingDirectory
    }

    return [System.String]$Script:SdnDiagnostics_Utilities.Cache.WorkingDirectory
}

function Get-WSManCredSSPState {
    if (Test-Path -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation') {
        if (Test-Path -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentials') {
            $allowFreshCredentials = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation' -Name 'AllowFreshCredentials' | Select-Object -ExpandProperty 'AllowFreshCredentials'
            if ($allowFreshCredentials -eq 1) {
                return $true
            }
        }
    }

    return $false
}

function Initialize-DataCollection {
    <#
    .SYNOPSIS
        Prepares the environment for data collection that logs will be saved to.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'GB')]
        [Parameter(Mandatory = $false, ParameterSetName = 'MB')]
        [SdnModules]$Role,

        [Parameter(Mandatory = $true, ParameterSetName = 'GB')]
        [Parameter(Mandatory = $true, ParameterSetName = 'MB')]
        [System.IO.DirectoryInfo]$FilePath,

        [Parameter(Mandatory = $true, ParameterSetName = 'GB')]
        [System.Int32]$MinimumGB,

        [Parameter(Mandatory = $true, ParameterSetName = 'MB')]
        [System.Int32]$MinimumMB
    )

    # ensure that the appropriate windows feature is installed and ensure module is imported
    if ($Role) {
        $config = Get-SdnModuleConfiguration -Role $Role.ToString()
        $confirmFeatures = Confirm-RequiredFeaturesInstalled -Name $config.WindowsFeature
        if (-NOT ($confirmFeatures)) {
            "Required feature is missing: {0}" -f ($config.WindowsFeature -join ', ') | Trace-Output -Level:Error
            return $false
        }

        $confirmModules = Confirm-RequiredModulesLoaded -Name $config.requiredModules
        if (-NOT ($confirmModules)) {
            "Required module is not loaded: {0}" -f ($config.requiredModules -join ', ')| Trace-Output -Level:Error
            return $false
        }
    }

    # create the directories if does not already exist
    if (-NOT (Test-Path -Path $FilePath.FullName -PathType Container)) {
        "Creating {0}" -f $FilePath.FullName | Trace-Output -Level:Verbose
        $null = New-Item -Path $FilePath.FullName -ItemType Directory -Force
    }

    # confirm sufficient disk space
    [System.Char]$driveLetter = (Split-Path -Path $FilePath.FullName -Qualifier).Replace(':','')
    switch ($PSCmdlet.ParameterSetName) {
        'GB' {
            $diskSpace = Confirm-DiskSpace -DriveLetter $driveLetter -MinimumGB $MinimumGB
        }
        'MB' {
            $diskSpace = Confirm-DiskSpace -DriveLetter $driveLetter -MinimumMB $MinimumMB
        }
    }

    if (-NOT ($diskSpace)) {
        "Insufficient disk space detected." | Trace-Output -Level:Error
        return $false
    }

    return $true
}

function Invoke-PSRemoteCommand {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String[]]$ComputerName,

        [Parameter(Mandatory = $false)]
        [bool]$ImportModuleOnRemoteSession,

        [Parameter(Mandatory = $true)]
        [ScriptBlock]$ScriptBlock,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false)]
        [Object[]]$ArgumentList = $null,

        [Parameter(Mandatory = $false, ParameterSetName = 'AsJob')]
        [Switch]$AsJob,

        [Parameter(Mandatory = $false, ParameterSetName = 'AsJob')]
        [Switch]$PassThru,

        [Parameter(Mandatory = $false, ParameterSetName = 'AsJob')]
        [System.String]$Activity,

        [Parameter(Mandatory = $false, ParameterSetName = 'AsJob')]
        [int]$ExecutionTimeout = 600
    )

    $params = @{
        ScriptBlock = $ScriptBlock
    }

    $psSessionParams = @{
        ComputerName = $ComputerName
        Credential = $Credential
    }

    if ($PSBoundParameters.ContainsKey('ImportModuleOnRemoteSession')) {
        $psSessionParams.Add('ImportModuleOnRemoteSession', $ImportModuleOnRemoteSession)
    }

    $session = New-PSRemotingSession @psSessionParams
    if ($session) {
        $params.Add('Session', $session)
        "ComputerName: {0}, ScriptBlock: {1}" -f ($session.ComputerName -join ', '), $ScriptBlock.ToString() | Trace-Output -Level:Verbose
        if ($ArgumentList) {
            $params.Add('ArgumentList', $ArgumentList)
            "ArgumentList: {0}" -f ($ArgumentList | ConvertTo-Json).ToString() | Trace-Output -Level:Verbose
        }

        if ($AsJob) {
            $params += @{
                AsJob = $true
                JobName = "SdnDiag-{0}" -f $(Get-Random)
            }

            $result = Invoke-Command @params
            if ($PassThru) {
                if ($Activity) {
                    $result = Wait-PSJob -Name $result.Name -ExecutionTimeOut $ExecutionTimeout -Activity $Activity
                }
                else {
                    $result = Wait-PSJob -Name $result.Name -ExecutionTimeOut $ExecutionTimeout
                }
            }

            return $result
        }
        else {
            return (Invoke-Command @params)
        }
    }
}

function Invoke-RestMethodWithRetry {
    param(
        [Parameter(Mandatory = $true)]
        [System.Uri]$Uri,

        [Parameter(Mandatory = $false)]
        [Microsoft.PowerShell.Commands.WebRequestMethod]$Method = 'Get',

        [Parameter(Mandatory = $false)]
        [System.Collections.IDictionary]$Headers,

        [Parameter (Mandatory = $false)]
        [System.String]$ContentType,

        [Parameter(Mandatory = $false)]
        [System.Object]$Body,

        [Parameter(Mandatory = $false)]
        [Switch] $DisableKeepAlive,

        [Parameter(Mandatory = $false)]
        [Switch] $UseBasicParsing,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]$Credential,

        [Parameter(Mandatory = $false)]
        [int]$TimeoutInSec = 600,

        [Parameter(Mandatory = $false)]
        [Switch]$Retry,

        [Parameter(Mandatory = $false, ParameterSetName = 'Retry')]
        [Int]$MaxRetry = 3,

        [Parameter(Mandatory = $false, ParameterSetName = 'Retry')]
        [Int]$RetryIntervalInSeconds = 30
    )

    $params = @{
        'Headers'     = $Headers;
        'ContentType' = $ContentType;
        'Method'      = $Method;
        'Uri'         = $Uri;
        'TimeoutSec'  = $TimeoutInSec
    }

    if ($null -ne $Body) {
        $params.Add('Body', $Body)
    }

    if ($DisableKeepAlive.IsPresent) {
        $params.Add('DisableKeepAlive', $true)
    }

    if ($UseBasicParsing.IsPresent) {
        $params.Add('UseBasicParsing', $true)
    }

    if ($Credential -ne [System.Management.Automation.PSCredential]::Empty -and $null -ne $Credential) {
        $params.Add('Credential', $Credential)
    }
    else {
        $params.Add('UseDefaultCredentials', $true)
    }

    $counter = 0
    while ($true) {
        $counter++

        try {
            "Performing {0} request to uri {1}" -f $Method, $Uri | Trace-Output -Level:Verbose
            if ($Body) {
                if ($Body -is [Hashtable]) {
                    "Body:`n`t{0}" -f ($Body | ConvertTo-Json -Depth 10) | Trace-Output -Level:Verbose
                }
                else {
                    "Body:`n`t{0}" -f ($Body) | Trace-Output -Level:Verbose
                }
            }

            $result = Invoke-RestMethod @params

            break
        }
        catch {
            if (($counter -le $MaxRetry) -and $Retry) {
                "Retrying operation in {0} seconds. Retry count: {1}." - $RetryIntervalInSeconds, $counter | Trace-Output
                Start-Sleep -Seconds $RetryIntervalInSeconds
            }
            else {
                $_ | Trace-Exception
                throw $_
            }
        }
    }

    return $result
}

function Invoke-WebRequestWithRetry {
    param(
        [Parameter(Mandatory = $true)]
        [System.Uri]$Uri,

        [Parameter(Mandatory = $false)]
        [Microsoft.PowerShell.Commands.WebRequestMethod]$Method = 'Get',

        [Parameter(Mandatory = $false)]
        [System.Collections.IDictionary]$Headers,

        [Parameter (Mandatory = $false)]
        [System.String]$ContentType,

        [Parameter(Mandatory = $false)]
        [System.Object] $Body,

        [Parameter(Mandatory = $false)]
        [Switch] $DisableKeepAlive,

        [Parameter(Mandatory = $false)]
        [Switch] $UseBasicParsing,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]$Credential,

        [Parameter(Mandatory = $false)]
        [int]$TimeoutInSec = 600,

        [Parameter(Mandatory = $false, ParameterSetName = 'Retry')]
        [Switch]$Retry,

        [Parameter(Mandatory = $false, ParameterSetName = 'Retry')]
        [Int]$MaxRetry = 3,

        [Parameter(Mandatory = $false, ParameterSetName = 'Retry')]
        [Int]$RetryIntervalInSeconds = 30
    )

    $params = @{
        'Headers'     = $Headers;
        'ContentType' = $ContentType;
        'Method'      = $Method;
        'Uri'         = $Uri;
        'TimeoutSec'  = $TimeoutInSec
    }

    if ($null -ne $Body) {
        $params.Add('Body', $Body)
    }

    if ($DisableKeepAlive.IsPresent) {
        $params.Add('DisableKeepAlive', $true)
    }

    if ($UseBasicParsing.IsPresent) {
        $params.Add('UseBasicParsing', $true)
    }

    if ($Credential -ne [System.Management.Automation.PSCredential]::Empty -and $null -ne $Credential) {
        $params.Add('Credential', $Credential)
    }
    else {
        $params.Add('UseDefaultCredentials', $true)
    }

    $counter = 0
    while ($true) {
        $counter++

        try {
            "Performing {0} request to uri {1}" -f $Method, $Uri | Trace-Output -Level:Verbose
            if ($Body) {
                "Body:`n`t{0}" -f $Body | Trace-Output -Level:Verbose
            }

            $result = Invoke-WebRequest @params

            break
        }
        catch {
            if (($counter -le $MaxRetry) -and $Retry) {
                "Retrying operation in {0} seconds. Retry count: {1}." - $RetryIntervalInSeconds, $counter | Trace-Output
                Start-Sleep -Seconds $RetryIntervalInSeconds
            }
            else {
                $_ | Trace-Exception
                throw $_
            }
        }
    }

    "StatusCode: {0} StatusDescription: {1}" -f $result.StatusCode, $result.StatusDescription | Trace-Output -Level:Verbose
    return $result
}

function New-PSRemotingSession {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.String[]]$ComputerName,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false)]
        [bool]$ImportModuleOnRemoteSession = $Global:SdnDiagnostics.Config.ImportModuleOnRemoteSession,

        [Parameter(Mandatory = $false)]
        [System.String]$ModuleName = $Global:SdnDiagnostics.Config.ModuleName,

        [Parameter(Mandatory = $false)]
        [Switch]$Force
    )

    begin {
        $importRemoteModule = {
            param([string]$arg0, $arg1)
            try {
                Import-Module $arg0 -ErrorAction Stop
                $Global:SdnDiagnostics.Config = $arg1
            }
            catch {
                throw $_
            }
        }

        $confirmRemoteModuleImported = {
            param([string]$arg0)
            $moduleExists = Get-Module -Name $arg0 -ListAvailable -ErrorAction Ignore
            if ($moduleExists) {
                return $true
            }

            return $false
        }

        $remoteSessions = @()

        # return a list of current sessions on the computer
        # return only the sessions that are opened and available as this will allow new sessions to be opened
        # without having to wait for existing sessions to move from Busy -> Available
        $currentActiveSessions = Get-PSSession -Name "SdnDiag-*" | Where-Object { $_.State -ieq 'Opened' -and $_.Availability -ieq 'Available' }
    }
    process {
        $ComputerName | ForEach-Object {
            $objectName = $PSItem

            # check to see if session is already opened
            if ($currentActiveSessions.ComputerName -contains $objectName -and !$Force) {
                $session = ($currentActiveSessions | Where-Object { $_.ComputerName -eq $objectName })[0]
                "Located existing powershell session {0} for {1}" -f $session.Name, $objectName | Trace-Output -Level:Verbose

                # if we have to import the module on the remote session, we need to check if the module is already imported
                # if not, we will import the module on the remote session
                if ($ImportModuleOnRemoteSession) {
                    $moduleImported = Invoke-Command -Session $session -ScriptBlock $confirmRemoteModuleImported -ArgumentList @($ModuleName) -ErrorAction Stop
                    if (-NOT $moduleImported) {
                        "Importing module {0} on remote session {1}" -f $ModuleName, $session.Name | Trace-Output -Level:Verbose
                        Invoke-Command -Session $session -ScriptBlock $importRemoteModule -ArgumentList @($ModuleName, $Global:SdnDiagnostics.Config) -ErrorAction Stop
                    }
                }

                # add the session to the array and skip further processing
                $remoteSessions += $session
                return # stop processing
            }

            # determine if an IP address was passed for the destination
            # if using IP address it needs to be added to the trusted hosts
            $isIpAddress = ($objectName -as [IPAddress]) -as [Bool]
            if ($isIpAddress) {
                try {
                    Confirm-IsAdmin

                    "{0} is an ip address" -f $objectName | Trace-Output -Level:Verbose
                    $trustedHosts = Get-Item -Path "WSMan:\localhost\client\TrustedHosts"
                    if ($trustedHosts.Value -notlike "*$objectName*" -and $trustedHosts.Value -ne "*") {
                        "Adding {0} to {1}" -f $objectName, $trustedHosts.PSPath | Trace-Output
                        Set-Item -Path "WSMan:\localhost\client\TrustedHosts" -Value $objectName -Concatenate
                    }
                }
                catch {
                    $_ | Trace-Output -Level:Error
                    return # stop processing
                }
            }

            try {
                if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) {
                    "PSRemotingSession use user-defined credential" | Trace-Output -Level:Verbose
                    $session = New-PSSession -Name "SdnDiag-$(Get-Random)" -ComputerName $objectName -Credential $Credential -SessionOption (New-PSSessionOption -Culture en-US -UICulture en-US -IdleTimeout 86400000) -ErrorAction Stop
                }
                else {
                    # if the credential is not defined, we want to check if we
                    if ($PSSenderInfo -and !(Get-WSManCredSSPState)) {
                        throw New-Object System.NotSupportedException("Unable to create PSSession to $objectName. This operation is not supported in a remote session without supplying -Credential.")
                    }

                    # if we need to create a new remote session, need to check to ensure that if using an IP Address that credentials are specified
                    # which is a requirement from a WinRM perspective. Will throw a warning and skip session creation for this computer.
                    if ($isIpAddress -and $Credential -eq [System.Management.Automation.PSCredential]::Empty) {
                        throw New-Object System.NotSupportedException("Unable to create PSSession to $objectName. The Credential parameter is required when using an IP Address.")
                    }

                    "PSRemotingSession use default credential" | Trace-Output -Level:Verbose
                    $session = New-PSSession -Name "SdnDiag-$(Get-Random)" -ComputerName $objectName -SessionOption (New-PSSessionOption -Culture 'en-US' -UICulture 'en-US' -IdleTimeout 86400000) -ErrorAction Stop
                }

                "Created powershell session {0} to {1}" -f $session.Name, $objectName | Trace-Output -Level:Verbose
                if ($ImportModuleOnRemoteSession) {
                    "Importing module {0} on remote session {1}" -f $ModuleName, $session.Name | Trace-Output -Level:Verbose
                    Invoke-Command -Session $session -ScriptBlock $importRemoteModule -ArgumentList @($ModuleName, $Global:SdnDiagnostics.Config) -ErrorAction Stop
                }

                # add the session to the array
                $remoteSessions += $session
            }
            catch {
                "Unable to create powershell session to {0}`n`t{1}" -f $objectName, $_.Exception.Message | Trace-Output -Level:Error
                return # stop processing
            }
        }
    }
    end {
        return ($remoteSessions | Sort-Object -Unique)
    }
}

function New-TraceOutputFile {

    try {
        # make sure that directory path exists, else create the folder structure required
        $workingDir = Get-WorkingDirectory
        if (-NOT (Test-Path -Path $workingDir -PathType Container)) {
            $null = New-Item -Path $workingDir -ItemType Directory -Force
        }

        # build the trace file path and set global variable
        [System.String]$fileName = "SdnDiagnostics_TraceOutput_{0}.csv" -f (Get-Date).ToString('yyyyMMdd')
        [System.IO.FileInfo]$filePath = Join-Path -Path $workingDir -ChildPath $fileName
        Set-TraceOutputFile -Path $filePath.FullName

        # configure the cache to not cleanup the trace file
        $SdnDiagnostics_Utilities.Cache.FilesExcludedFromCleanup += $filePath.Name
        "TraceFile: {0}" -f $filePath.FullName | Trace-Output -Level:Verbose
    }
    catch {
        $_.Exception | Write-Error
    }
}

function New-WorkingDirectory {
    [CmdletBinding()]
    param ()

    try {
        [System.String]$path = (Get-WorkingDirectory)

        if(-NOT (Test-Path -Path $path -PathType Container)){
            $null = New-Item -Path $path -ItemType Directory -Force
        }

        # create the trace file
        New-TraceOutputFile
    }
    catch {
        $_.Exception | Write-Error
    }
}

function Remove-PSRemotingSession {
    <#
    .SYNOPSIS
        Gracefully removes any existing PSSessions
    .PARAMETER ComputerName
        The computer name(s) that should have any existing PSSessions removed
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [System.String[]]$ComputerName
    )

    try {
        [int]$timeOut = 120
        $stopWatch = [System.Diagnostics.Stopwatch]::StartNew()

        if ($PSBoundParameters.ContainsKey('ComputerName')) {
            $sessions = Get-PSSession -Name "SdnDiag-*" | Where-Object { $_.ComputerName -iin $ComputerName }
        }
        else {
            $sessions = Get-PSSession -Name "SdnDiag-*"
        }

        while ($sessions) {
            if ($stopWatch.Elapsed.TotalSeconds -gt $timeOut) {
                throw New-Object System.TimeoutException("Unable to drain PSSessions")
            }

            foreach ($session in $sessions) {
                if ($session.Availability -ieq 'Busy') {
                    "{0} is currently {1}. Waiting for PSSession.. {2} seconds" -f $session.Name, $session.Availability, $stopWatch.Elapsed.TotalSeconds | Trace-Output
                    Start-Sleep -Seconds 5
                    continue
                }
                else {
                    "Removing PSSession {0} for {1}" -f $session.Name, $session.ComputerName | Trace-Output -Level:Verbose

                    try {
                        $session | Remove-PSSession -ErrorAction Stop
                    }
                    catch {
                        "Unable to remove PSSession {0} for {1}. Error: {2}" -f $session.Name, $session.ComputerName, $_.Exception.Message | Trace-Output -Level:Warning
                        continue
                    }
                }
            }

            if ($PSBoundParameters.ContainsKey('ComputerName')) {
                $sessions = Get-PSSession -Name "SdnDiag-*" | Where-Object { $_.ComputerName -iin $ComputerName }
            }
            else {
                $sessions = Get-PSSession -Name "SdnDiag-*"
            }
        }

        $stopWatch.Stop()
    }
    catch {
        $stopWatch.Stop()
        $_ | Trace-Exception
    }
}

function Remove-SdnDiagnosticJob {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [System.String[]]$State = @("Completed","Failed"),

        [Parameter(Mandatory = $false)]
        [System.String]$Name
    )

    if (-NOT ([string]::IsNullOrEmpty($Name))) {
        $filteredJobs = Get-Job -Name $Name
    }
    else {
        $filteredJobs = Get-Job -Name "SdnDiag-*" | Where-Object {$_.State -iin $State}
    }

    if ($filteredJobs ) {
        $filteredJobs | Remove-Job -Force -ErrorAction SilentlyContinue
    }
}

function Set-TraceOutputFile {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]$Path
    )

    $Script:SdnDiagnostics_Utilities.Cache.TraceFilePath = $Path
}

function Test-ComputerNameIsLocal {
    <##>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]$ComputerName
    )

    try {
        # detect if the ComputerName passed is an IP address
        # if so, need to enumerate the IP addresses on the system to compare with ComputerName to determine if there is a match
        $isIpAddress = ($ComputerName -as [IPAddress]) -as [Bool]
        if($isIpAddress){
            $ipAddresses = Get-NetIPAddress
            foreach($ip in $ipAddresses){
                if([IPAddress]$ip.IpAddress -eq [IPAddress]$ComputerName){
                    return $true
                }
            }
        }

        # check to determine if the ComputerName matches the NetBIOS name of the computer
        if($env:COMPUTERNAME -ieq $ComputerName){
            return $true
        }

        # check to determine if ComputerName matches the FQDN name of the computer
        if(([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME).HostName) -ieq $ComputerName){
            return $true
        }

        return $false
    }
    catch {
        $_ | Trace-Exception
        $_ | Write-Error
    }
}

function Test-Ping {
    <#
    .SYNOPSIS
        Sends ICMP echo request packets.
    .PARAMETER DestinationAddress
        Specifies the destination IP address to use.
    .PARAMETER SourceAddress
        Specifies the source IP address to use.
    .PARAMETER CompartmentId
        Specifies an ID of compartment to perform the ping from within.
    .PARAMETER BufferSize
        Specifies the size, in bytes, of the buffer sent with this command. The default value is 1472.
    .PARAMETER DontFragment
        This parameter sets the Don't Fragment flag in the IP header. You can use this parameter with the BufferSize parameter to test the Path MTU size.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [IPAddress]$DestinationAddress,

        [Parameter(Mandatory = $true)]
        [IPAddress]$SourceAddress,

        [Parameter(Mandatory = $false)]
        [int]$CompartmentId = (Get-NetCompartment | Where-Object {$_.CompartmentDescription -ieq 'Default Compartment'}).CompartmentId,

        [Parameter()]
        [int[]]$BufferSize = 1472,

        [Parameter(Mandatory = $false)]
        [switch]$DontFragment
    )

    try {
        $arrayList = [System.Collections.ArrayList]::new()

        foreach($size in $BufferSize){
            $Global:LASTEXITCODE = 0
            if($DontFragment){
                $ping = ping $DestinationAddress.IPAddressToString -c $CompartmentId -l $size -S $SourceAddress.IPAddressToString -n 2-f
            }
            else {
                $ping = ping $DestinationAddress.IPAddressToString -c $CompartmentId -l $size -S $SourceAddress.IPAddressToString -n 2
            }

            if($LASTEXITCODE -ieq 0){
                $status = 'Success'
            }
            else {
                $status = 'Failure'
            }

            $result = [PSCustomObject]@{
                SourceAddress = $SourceAddress.IPAddressToString
                DestinationAddress = $DestinationAddress.IPAddressToString
                CompartmentId = $CompartmentId
                BufferSize = $size
                Status = $status
                Result = $ping
            }

            [void]$arrayList.Add($result)
        }

        return $arrayList
    }
    catch {
        $_ | Trace-Exception
        $_ | Write-Error
    }
}

function Trace-Exception {
    <#
    .SYNOPSIS
        Extracts information out of exceptions to write to the log file.
        Pipe exceptions to this command in a catch block.
 
    .PARAMETER Exception
        Any exception inherited from [System.Exception]
 
    .EXAMPLE
        try
        {
            1 / 0 #divide by 0 exception
        }
        catch
        {
            $_ | Trace-Exception
        }
    #>

    param(
        [parameter(Mandatory = $True, ValueFromPipeline = $true)]
        $Exception
    )

    Trace-Output -Exception $Exception -FunctionName (Get-PSCallStack)[1].Command -Level 'Exception'
}

function Trace-Output {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Message')]
        [System.String]$Message,

        [Parameter(Mandatory = $false, ParameterSetName = 'Message')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Exception')]
        [TraceLevel]$Level = 'Information',

        [Parameter(Mandatory = $false, ParameterSetName = 'Message')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Exception')]
        [System.String]$FunctionName = (Get-PSCallStack)[0].Command,

        [parameter(Mandatory = $true, ParameterSetName = 'Exception')]
        $Exception
    )

    begin {
        $traceFile = (Get-TraceOutputFile)
        if ([string]::IsNullOrEmpty($traceFile)) {
            New-WorkingDirectory

            $traceFile = (Get-TraceOutputFile)
        }
    }
    process {
        # create custom object for formatting purposes
        $traceEvent = [PSCustomObject]@{
            Computer = $env:COMPUTERNAME.ToUpper().ToString()
            TimestampUtc = [DateTime]::UtcNow.ToString('yyyy-MM-dd HH-mm-ss')
            FunctionName = $FunctionName
            Level = $Level.ToString()
            Message = $null
        }

        switch ($PSCmdlet.ParameterSetName) {
            'Message' {
                $traceEvent.Message = $Message
            }
            'Exception' {
                if ($Exception -is [System.Management.Automation.ErrorRecord]) {
                    $traceEvent.Message = "{0}`n{1}" -f $Exception.Exception, $Exception.ScriptStackTrace
                }
                # this is for when we capture a terminating exception generated by throw, in which
                # it will not include the ScriptStackTrace or Exception details like the ErrorRecord
                else {
                    $traceEvent.Message = $Exception.ToString()
                }
            }
        }

        $formattedMessage = "[{0}] {1}" -f $traceEvent.Computer, $traceEvent.Message

        # write the message to the console
        switch($Level){
            'Error' {
                $formattedMessage | Write-Host -ForegroundColor:Red
            }

            'Exception' {
                # do nothing here, as the exception should be written to the console by the caller using Write-Error
                # as this will preserve the proper call stack tracing
            }

            'Success' {
                $formattedMessage  | Write-Host -ForegroundColor:Green
            }

            'Verbose' {
                if($VerbosePreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) {
                    $formattedMessage | Write-Verbose
                }
            }

            'Warning' {
                $formattedMessage | Write-Warning
            }

            default {
                $formattedMessage | Write-Host -ForegroundColor:Cyan
            }
        }

        # write the event to trace file to be used for debugging purposes
        $mutexInstance = Wait-OnMutex -MutexId 'SDN_TraceLogging' -ErrorAction Continue
        if ($mutexInstance) {
            $traceEvent | Export-Csv -Append -NoTypeInformation -Path $traceFile
        }
    }
    end {
        if ($mutexInstance) {
            $mutexInstance.ReleaseMutex()
        }
    }
}

function Wait-OnMutex {
    param (
        [Parameter(Mandatory = $true)]
        [System.String]$MutexId
    )

    try {
        $MutexInstance = New-Object System.Threading.Mutex($false, $MutexId)
        if ($MutexInstance.WaitOne(3000)) {
            return $MutexInstance
        }
        else {
            throw New-Object -TypeName System.TimeoutException("Failed to acquire Mutex")
        }
    }

    catch [System.Threading.AbandonedMutexException] {
        $MutexInstance = New-Object System.Threading.Mutex($false, $MutexId)
        return (Wait-OnMutex -MutexId $MutexId)
    }
    catch {
        $MutexInstance.ReleaseMutex()
        $_ | Write-Error
    }
}

function Wait-PSJob {
    <#
    .SYNOPSIS
        Monitors jobs to ensure they complete or terminate if any particular job is taking too long
    .PARAMETER Name
        The job name to monitor
    .PARAMETER Activity
        Description of the job that is being performed
    .PARAMETER ExecutionTimeOut
        Total period to wait for jobs to complete before stopping jobs and progressing forward in scripts. If omitted, defaults to 600 seconds
    .PARAMETER PollingInterval
        How often you want to query job status. If omitted, defaults to 1 seconds
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.String]$Name,

        [Parameter(Mandatory = $false)]
        [System.String]$Activity = (Get-PSCallStack)[1].Command,

        [Parameter(Mandatory = $false)]
        [int]$ExecutionTimeOut = 600,

        [Parameter(Mandatory = $false)]
        [int]$PollingInterval = 1
    )

    try {
        $stopWatch = [System.Diagnostics.Stopwatch]::StartNew()
        "JobName: {0} PollingInterval: {1} seconds ExecutionTimeout: {2} seconds" -f $Name, $PollingInterval, $ExecutionTimeOut | Trace-Output -Level:Verbose

        # Loop while there are running jobs
        while ((Get-Job -Name $Name).State -ieq 'Running') {

            # get the job details and write progress
            $job = Get-Job -Name $Name
            $runningChildJobs = $job.ChildJobs | Where-Object { $_.State -ieq 'Running' }
            $jobCount = $job.ChildJobs.Count
            $runningJobCount = $runningChildJobs.Count
            $percent = [math]::Round((($jobcount - $runningJobCount) / $jobCount * 100), 2)

            $status = "Progress: {0}%. Waiting for {1}" -f $percent, ($runningChildJobs.Location -join ', ')
            Write-Progress -Activity $Activity -Status $status -PercentComplete $percent -Id $job.Id

            # check the stopwatch and break out of loop if we hit execution timeout limit
            if ($stopWatch.Elapsed.TotalSeconds -ge $ExecutionTimeOut) {
                Get-Job -Name $Name | Stop-Job -Confirm:$false
                throw New-Object System.TimeoutException("Unable to complete operation within the specified timeout period")
            }

            # pause the loop per polling interval value
            Start-Sleep -Seconds $PollingInterval
        }

        $stopWatch.Stop()
        $job = Get-Job -Name $Name

        # Ensure that we complete all jobs for write-progress to clear the progress bars
        Write-Progress -Activity $Activity -Id $job.Id -Completed

        # Output results of the job status to the operator
        if ($job.State -ne "Completed") {
            [System.String]$outputFolder = "{0}\PSRemoteJob_Failures\{1}" -f (Get-WorkingDirectory), $Name

            "[{0}] Operation {1}. Total Elapsed Time: {2}" -f $Name, $job.State, $stopwatch.Elapsed.TotalSeconds | Trace-Output -Level:Warning

            # Identify all failed child jobs and present to the operator
            $failedChildJobs = $job.ChildJobs | Where-Object { $_.State -ine 'Completed' }
            foreach ($failedChildJob in $failedChildJobs) {
                "[{0}] {1} for {2} is reporting state: {3}." -f $Name, $failedChildJob.Name, $failedChildJob.Location, $failedChildJob.State | Trace-Output -Level:Warning

                # do our best to capture the failing exception that was returned from the remote job invocation
                # due to ps remoting bug as outlined in https://github.com/PowerShell/PowerShell/issues/9585 we may not capture everything and may add additional details to screen
                $failedChildJob | Receive-Job -Keep -ErrorAction Continue *>&1 | Export-ObjectToFile -FilePath $outputFolder -Name $failedChildJob.Name -FileType 'txt'
            }
        }
        else {
            "[{0}] Operation {1}. Total Elapsed Time: {2}" -f $Name, $job.State, $stopwatch.Elapsed.TotalSeconds | Trace-Output -Level:Verbose
        }

        return (Get-Job -Name $Name | Receive-Job)
    }
    catch {
        $_ | Trace-Exception
        $_ | Write-Error
    }
}

function Clear-SdnWorkingDirectory {
    <#
    .SYNOPSIS
        Clears the contents of the directory specified
    .PARAMETER ComputerName
        Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers.
    .PARAMETER Credential
        Specifies a user account that has permission to perform this action. The default is the current user.
        Type a user name, such as User01 or Domain01\User01, or enter a PSCredential object generated by the Get-Credential cmdlet. If you type a user name, you're prompted to enter the password.
    .PARAMETER Path
        Specifies a path of the items being removed. Wildcard characters are permitted. If ommitted, defaults to (Get-WorkingDirectory).
    .PARAMETER Recurse
        Indicates that this cmdlet deletes the items in the specified locations and in all child items of the locations.
    .PARAMETER Force
        Forces the cmdlet to remove items that cannot otherwise be changed, such as hidden or read-only files or read-only aliases or variables.
    .EXAMPLE
        PS> Clear-SdnWorkingDirectory
    .EXAMPLE
        PS> Clear-SdnWorkingDirectory -ComputerName PREFIX-NC01 -Path 'C:\Temp\SDN2'
    .EXAMPLE
        PS> Clear-SdnWorkingDirectory -ComputerName PREFIX-NC01,PREFIX-SLB01 -Credential (Get-Credential)
    .EXAMPLE
        PS> Clear-SdnWorkingDirectory -Force -Recurse
    .EXAMPLE
        PS> Clear-SdnWorkingDirectory -Path 'C:\Temp\SDN1','C:\Temp\SDN2' -Force -Recurse
    #>


    [CmdletBinding(DefaultParameterSetName = 'Local')]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'Remote')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Local')]
        [System.String[]]$Path = (Get-WorkingDirectory),

        [Parameter(Mandatory = $false, ParameterSetName = 'Remote')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Local')]
        [Switch]$Recurse,

        [Parameter(Mandatory = $false, ParameterSetName = 'Remote')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Local')]
        [Switch]$Force,

        [Parameter(Mandatory = $true, ParameterSetName = 'Remote')]
        [System.String[]]$ComputerName,

        [Parameter(Mandatory = $false, ParameterSetName = 'Remote')]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty
    )

    function Clear-WorkingDirectory {
        [CmdletBinding()]
        param (
            [System.String[]]$Path,
            [bool]$Recurse,
            [bool]$Force
        )

        $filteredPaths = @()
        foreach($obj in $Path) {

            # if the path does not exist, lets skip
            if (-NOT (Test-Path -Path $obj)) {
                continue
            }

            # enumerate through the allowed folder paths for cleanup to make sure the paths specified can be cleaned up
            foreach ($allowedFolderPath in $Script:SdnDiagnostics_Utilities.Config.FolderPathsAllowedForCleanup) {
                if ($obj -ilike $allowedFolderPath) {
                    $filteredPaths += $obj
                }
            }
        }

        if ($filteredPaths) {
            "Cleaning up: {0}" -f ($filteredPaths -join ', ') | Trace-Output -Level:Verbose
            Remove-Item -Path $filteredPaths -Exclude $Script:SdnDiagnostics_Utilities.Cache.FilesExcludedFromCleanup -Force:$Force -Recurse:$Recurse -ErrorAction Continue
        }
    }

    $params = @{
        Path = $Path
        Recurse = $Recurse.IsPresent
        Force = $Force.IsPresent
    }

    try {
        if ($PSCmdlet.ParameterSetName -eq 'Remote') {
            Invoke-PSRemoteCommand -ComputerName $ComputerName -Credential $Credential -ScriptBlock {
                param([Parameter(Position = 1)]$Path, [Parameter(Position = 2)]$Recurse, [Parameter(Position = 3)]$Force)
                Clear-SdnWorkingDirectory -Path $Path -Recurse:$Recurse -Force:$Force
            } -ArgumentList @($params.Path, $params.Recurse, $params.Force)
        }
        else {
            Clear-WorkingDirectory @params
        }
    }
    catch {
        $_ | Trace-Exception
        $_ | Write-Error
    }
}

    function Copy-SdnFileFromComputer {

    <#
    .SYNOPSIS
        Copies an item from one location to another using FromSession
    .PARAMETER Path
        Specifies, as a string array, the path to the items to copy. Wildcard characters are permitted.
    .PARAMETER ComputerName
        Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers.
    .PARAMETER Destination
        Specifies the path to the new location. The default is the current directory.
        To rename the item being copied, specify a new name in the value of the Destination parameter.
    .PARAMETER Credential
        Specifies a user account that has permission to perform this action. The default is the current user.
        Type a user name, such as User01 or Domain01\User01, or enter a PSCredential object generated by the Get-Credential cmdlet. If you type a user name, you're prompted to enter the password.
    .PARAMETER Recurse
        Indicates that this cmdlet does a recursive copy.
    .PARAMETER Force
        Indicates that this cmdlet copies items that can't otherwise be changed, such as copying over a read-only file or alias.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.String[]]$Path,

        [Parameter(Mandatory = $true)]
        [System.String[]]$ComputerName,

        [Parameter(Mandatory = $false)]
        [System.IO.FileInfo]$Destination = (Get-WorkingDirectory),

        [Parameter(Mandatory = $false, ValueFromPipeline = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false)]
        [Switch]$Recurse,

        [Parameter(Mandatory = $false)]
        [Switch]$Force
    )

    Copy-FileFromRemoteComputer @PSBoundParameters
}

function Copy-SdnFileToComputer {
    <#
    .SYNOPSIS
        Copies an item from local path to a path at remote server
    .PARAMETER Path
        Specifies, as a string array, the path to the items to copy. Wildcard characters are permitted.
    .PARAMETER ComputerName
        Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers.
    .PARAMETER Destination
        Specifies the path to the new location. The default is the current directory.
        To rename the item being copied, specify a new name in the value of the Destination parameter.
    .PARAMETER Credential
        Specifies a user account that has permission to perform this action. The default is the current user.
        Type a user name, such as User01 or Domain01\User01, or enter a PSCredential object generated by the Get-Credential cmdlet. If you type a user name, you're prompted to enter the password.
    .PARAMETER Recurse
        Indicates that this cmdlet does a recursive copy.
    .PARAMETER Force
        Indicates that this cmdlet copies items that can't otherwise be changed, such as copying over a read-only file or alias.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.String[]]$Path,

        [Parameter(Mandatory = $true)]
        [System.String[]]$ComputerName,

        [Parameter(Mandatory = $false)]
        [System.IO.FileInfo]$Destination = (Get-WorkingDirectory),

        [Parameter(Mandatory = $false, ValueFromPipeline = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false)]
        [Switch]$Recurse,

        [Parameter(Mandatory = $false)]
        [Switch]$Force
    )

    Copy-FileToRemoteComputer @PSBoundParameters
}

function Get-SdnModuleConfiguration {
    <#
    .SYNOPSIS
        Returns the configuration data related to the sub modules within SdnDiagnostics.
    .PARAMETER Role
        The SDN role that you want to return configuration data for.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [SdnModules]$Role
    )

    if ($Role.ToString().Contains('_')) {
        [string]$Role = $Role -replace '_', '.'
    }

    $path = "SdnDiag.{0}\SdnDiag.{0}.Config.psd1" -f $Role
    $moduleConfig = Get-Item -Path $PSScriptRoot\..\$path -ErrorAction SilentlyContinue
    if ($moduleConfig) {
        $configurationData = Import-PowerShellDataFile -Path $moduleConfig.FullName
    }

    return $configurationData
}

function Install-SdnDiagnostics {
    <#
    .SYNOPSIS
        Install SdnDiagnostic Module to remote computers if not installed or version mismatch.
    .PARAMETER ComputerName
        Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers.
    .PARAMETER Credential
        Specifies a user account that has permission to perform this action. The default is the current user.
        Type a user name, such as User01 or Domain01\User01, or enter a PSCredential object generated by the Get-Credential cmdlet. If you type a user name, you're prompted to enter the password.
    .PARAMETER Path
        Specifies the path to the module where it should be installed. If not specified, the default path will be used.
    .PARAMETER Force
        Forces a cleanup and re-install of the module on the remote computer.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String[]]$ComputerName,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false)]
        [System.String]$Path = $Script:SdnDiagnostics_Utilities.Config.DefaultModuleDirectory,

        [Parameter(Mandatory = $false)]
        [switch]$Force
    )

    begin {
        $moduleName = $Global:SdnDiagnostics.Config.ModuleName
        # if we have configured automatic seeding of module to remote nodes, we will want to skip this operation
        if ($Global:SdnDiagnostics.Config.DisableModuleSeeding) {
            return
        }

        # if we have multiple modules installed on the current workstation,
        # abort the operation because side by side modules can cause some interop issues to the remote nodes
        $localModule = Get-Module -Name 'SdnDiagnostics'
        if ($localModule.Count -gt 1) {
            throw "Detected more than one module version of SdnDiagnostics. Remove all versions of module from runspace and re-import the module."
        }

        $getModuleVersionSB = {
            param ([string]$arg0)
            try {
                # Get the latest version of SdnDiagnostics Module installed
                $version = (Get-Module -Name $arg0 -ListAvailable -ErrorAction Ignore | Sort-Object Version -Descending)[0].Version.ToString()
            }
            catch {
                # in some instances, the module will not be available and as such we want to skip the noise and return
                # a string back to the remote call command which we can do proper comparison against
                $version = '0.0.0.0'
            }
            return $version
        }

        # typically PowerShell modules will be installed in the following directory configuration:
        # $env:ProgramFiles\WindowsPowerShell\Modules\SdnDiagnostics\{version}
        # $env:USERPROFILE\Documents\WindowsPowerShell\Modules\SdnDiagnostics\{version}
        # so we default to Leaf of the path being SdnDiagnostics as PSGet will handle the versioning so we only ever do import in the following format:
        # Import-Module SdnDiagnostics (if using default PowerShell module path)
        # Import-Module C:\{path}\SdnDiagnostics (if using custom PowerShell module path)
        # so we need to ensure that we are copying the module to the correct path on the remote computer
        [System.String]$destinationPathDir = Join-Path $Path -ChildPath $localModule.Version.ToString()
        "Verifying {0} is running SdnDiagnostics version {1}" -f $($ComputerName -join ', '), $localModule.Version.ToString() | Trace-Output -Level:Verbose
    }
    process {
        $ComputerName | ForEach-Object {
            $computer = $_

            try {
                # check to see if the computer is local, if so, we will skip the operation
                if (Test-ComputerNameIsLocal -ComputerName $computer) {
                    "Detected that {0} is local machine. Skipping update operation for {0}." -f $computer | Trace-Output -Level:Verbose
                    return
                }

                if (!$Force) {
                    "Getting current installed version of SdnDiagnostics on {0}" -f $computer | Trace-Output -Level:Verbose

                    # use Invoke-Command here, as we do not want to create a cached session for the remote computers
                    # as it will impact scenarios where we need to import the module on the remote computer for remote sessions
                    try {
                        $remoteModuleVersion = Invoke-Command -ComputerName $computer -Credential $Credential -ScriptBlock $getModuleVersionSB -ArgumentList @($moduleName) -ErrorAction Stop
                    }
                    catch {
                        # if we are unable to connect to the remote computer, we will skip the operation
                        $_ | Trace-Exception
                        "Unable to connect to {0}. Skipping update operation for {0}." -f $computer | Trace-Output -Level:Verbose
                        return
                    }

                    if ($remoteModuleVersion) {
                        # if the remote module version is greater or equal to the local module version, then we do not need to update
                        "{0} is currently using version: {1}" -f $computer, $remoteModuleVersion | Trace-Output -Level:Verbose
                        if ([version]$remoteModuleVersion -ge [version]$localModule.Version) {
                            "No update is required for {0}" -f $computer | Trace-Output -Level:Verbose
                            return
                        }
                    }
                }

                "SdnDiagnostics {0} will be installed to {1}" -f $localModule.Version.ToString(), $computer | Trace-Output
                Copy-FileToRemoteComputer -Path $localModule.ModuleBase -ComputerName $computer -Destination $destinationPathDir -Credential $Credential -Recurse -Force

                # ensure that we destroy the current pssessions for the computer to prevent any caching issues
                # we will want to remove any existing PSSessions for the remote computers
                Remove-PSRemotingSession -ComputerName $computer
            }
            catch {
                $_ | Trace-Exception
                $_ | Write-Error
            }
        }
    }
    end {
        # do nothing here
    }
}

function Invoke-SdnCommand {
    <#
    .SYNOPSIS
        Runs commands on local and remote computers.
    .PARAMETER ComputerName
        Type the NetBIOS name, an IP address, or a fully qualified domain name a remote computer.
    .PARAMETER Credential
        Specifies a user account that has permission to perform this action. The default is the current user.
        Type a user name, such as User01 or Domain01\User01, or enter a PSCredential object generated by the Get-Credential cmdlet. If you type a user name, you're prompted to enter the password.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String[]]$ComputerName,

        [Parameter(Mandatory = $true)]
        [ScriptBlock]$ScriptBlock,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty
    )

    try {
        Invoke-PSRemoteCommand -ComputerName $ComputerName -Credential $Credential -ScriptBlock $ScriptBlock
    }
    catch {
        $_ | Trace-Exception
        $_ | Write-Error
    }
}


# SIG # Begin signature block
# MIIoRgYJKoZIhvcNAQcCoIIoNzCCKDMCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCLDUtsHQI69P+k
# L1RGUra773s1Pu0S96sPlN39WZuf1aCCDXYwggX0MIID3KADAgECAhMzAAADrzBA
# DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA
# hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG
# 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN
# xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL
# go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB
# tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd
# mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ
# 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY
# 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp
# XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn
# TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT
# e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG
# OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O
# PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk
# ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx
# HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt
# CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGiYwghoiAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIL35VxbfJ5+KqgNz2HUfCHzP
# A2g1d0WF2KeKc5ulrdUxMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEANLE+FMTKXl9dsHc33qGQ9bAyc98ygpKEDcfrf2x6R7D2PRwWx6sC+Ms3
# PM08fo0wxwqjah95qd1KN0dVqqGgHZk0582OpmXJdKOhJQ4F93F0zEGQz6YA0Z5x
# JMhGQk476GSQj5+kWyvRT/lQ6Vz3WXHUiadhbeOPEJJWU4Ck8AiApWaQLrX/XStV
# CUDRsj4+FNtF/zqYB2cPwKXm8aHIqEAwIuP/7h5rq1quOuDZqxHjqSViBzah1VNt
# IuWC+pN8Kyrnk/F4t/AFc1Zc7gELwelRpNaxxzg9H3Z5/k6SO/ihMGPTokHQ2vDs
# Brj72ZU/8iTfLrvm4N6lCnvOmHyNOaGCF7AwghesBgorBgEEAYI3AwMBMYIXnDCC
# F5gGCSqGSIb3DQEHAqCCF4kwgheFAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsq
# hkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCB29rjErGmzn6Kl4IhafjZIbKvsJaN98vPj3Gh58xGWGAIGZsZGj77h
# GBMyMDI0MDgyNjIxMjkyNi44NTFaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT
# TjozMjFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# U2VydmljZaCCEf4wggcoMIIFEKADAgECAhMzAAAB+KOhJgwMQEj+AAEAAAH4MA0G
# CSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI0
# MDcyNTE4MzEwOFoXDTI1MTAyMjE4MzEwOFowgdMxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9w
# ZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjMyMUEt
# MDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl
# MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxR23pXYnD2BuODdeXs2C
# u/T5kKI+bAw8cbtN50Cm/FArjXyL4RTqMe6laQ/CqeMTxgckvZr1JrW0Mi4F15rx
# /VveGhKBmob45DmOcV5xyx7h9Tk59NAl5PNMAWKAIWf270SWAAWxQbpVIhhPWCnV
# V3otVvahEad8pMmoSXrT5Z7Nk1RnB70A2bq9Hk8wIeC3vBuxEX2E8X50IgAHsyaR
# 9roFq3ErzUEHlS8YnSq33ui5uBcrFOcFOCZILuVFVTgEqSrX4UiX0etqi7jUtKyp
# gIflaZcV5cI5XI/eCxY8wDNmBprhYMNlYxdmQ9aLRDcTKWtddWpnJtyl5e3gHuYo
# j8xuDQ0XZNy7ESRwJIK03+rTZqfaYyM4XSK1s0aa+mO69vo/NmJ4R/f1+KucBPJ4
# yUdbqJWM3xMvBwLYycvigI/WK4kgPog0UBNczaQwDVXpcU+TMcOvWP8HBWmWJQIm
# TZInAFivXqUaBbo3wAfPNbsQpvNNGu/12pg0F8O/CdRfgPHfOhIWQ0D8ALCY+Lsi
# wbzcejbrVl4N9fn2wOg2sDa8RfNoD614I0pFjy/lq1NsBo9V4GZBikzX7ZjWCRgd
# 1FCBXGpfpDikHjQ05YOkAakdWDT2bGSaUZJGVYtepIpPTAs1gd/vUogcdiL51o7s
# huHIlB6QSUiQ24XYhRbbQCECAwEAAaOCAUkwggFFMB0GA1UdDgQWBBS9zsZzz57Q
# lT5nrt/oitLv1OQ7tjAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBf
# BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz
# L2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmww
# bAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29m
# dC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0El
# MjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUF
# BwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAYfk8GzzpEVnG
# l7y6oXoytCb42Hx6TOA0+dkaBI36ftDE9tLubUa/xMbHB5rcNiRhFHZ93RefdPpc
# 4+FF0DAl5lP8xKAO+293RWPKDFOFIxgtZY08t8D9cSQpgGUzyw3lETZebNLEA17A
# /CTpA2F9uh8j84KygeEbj+bidWDiEfayoH2A5/5ywJJxIuLzFVHacvWxSCKoF9hl
# SrZSG5fXWS3namf4tt690UT6AGyWLFWe895coFPxm/m0UIMjjp9VRFH7nb3Ng2Q4
# gPS9E5ZTMZ6nAlmUicDj0NXAs2wQuQrnYnbRAJ/DQW35qLo7Daw9AsItqjFhbMcG
# 68gDc4j74L2KYe/2goBHLwzSn5UDftS1HZI0ZRsqmNHI0TZvvUWX9ajm6SfLBTEt
# oTo6gLOX0UD/9rrhGjdkiCw4SwU5osClgqgiNMK5ndk2gxFlDXHCyLp5qB6BoPpc
# 82RhO0yCzoP9gv7zv2EocAWEsqE5+0Wmu5uarmfvcziLfU1SY240OZW8ld4sS8fn
# ybn/jDMmFAhazV1zH0QERWEsfLSpwkOXaImWNFJ5lmcnf1VTm6cmfasScYtElpjq
# Z9GooCmk1XFApORPs/PO43IcFmPRwagt00iQSw+rBeIH00KQq+FJT/62SB70g9g/
# R8TS6k6b/wt2UWhqrW+Q8lw6Xzgex/YwggdxMIIFWaADAgECAhMzAAAAFcXna54C
# m0mZAAAAAAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZp
# Y2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMy
# MjVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV
# BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0B
# AQEFAAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51
# yMo1V/YBf2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY
# 6GB9alKDRLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9
# cmmvHaus9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN
# 7928jaTjkY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDua
# Rr3tpK56KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74
# kpEeHT39IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2
# K26oElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5
# TI4CvEJoLhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZk
# i1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9Q
# BXpsxREdcu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3Pmri
# Lq0CAwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUC
# BBYEFCqnUv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJl
# pxtTNRnpcjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9y
# eS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUA
# YgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
# 1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2Ny
# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIw
# MTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0w
# Ni0yMy5jcnQwDQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/yp
# b+pcFLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulm
# ZzpTTd2YurYeeNg2LpypglYAA7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM
# 9W0jVOR4U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECW
# OKz3+SmJw7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4
# FOmRsqlb30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3Uw
# xTSwethQ/gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPX
# fx5bRAGOWhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVX
# VAmxaQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGC
# onsXHRWJjXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU
# 5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEG
# ahC0HVUzWLOhcGbyoYIDWTCCAkECAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT
# TjozMjFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# U2VydmljZaIjCgEBMAcGBSsOAwIaAxUAtkQt/ebWSQ5DnG+aKRzPELCFE9GggYMw
# gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsF
# AAIFAOp3XF4wIhgPMjAyNDA4MjYxOTU2MTRaGA8yMDI0MDgyNzE5NTYxNFowdzA9
# BgorBgEEAYRZCgQBMS8wLTAKAgUA6ndcXgIBADAKAgEAAgI+6gIB/zAHAgEAAgIT
# kTAKAgUA6nit3gIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAow
# CAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQCVfmfHusfB
# ze7zMHO/NUjD0TYdMTFRhS/STZWPpAULP0awJPWPv23WVSKk5fMqw20Onv01SFKo
# mXFkhcd0gFjpulU8pQ5jEF7e8gMz7cVLiaL7ys7CWDagir+5xb7iyxomn/PfoRV2
# gmNtlR5USrehpMXwhw/CeanHPo9s31p+7DZKgDbmlp2nnxThqER8fcf5vSjoqVi2
# b2PP3+wYomuwH2RH1I0CnMtDauexalFiaTuxgPBQH5XjJVD+c2TU7MFP8QFQz5y+
# ulJqNGebTAmA7LqewRKz2wAHnJ7emxR57k5DQAGHlooMmGom0r9+Q+CGsRJ4pzqy
# 1/Mo0d6bJVsHMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB
# IDIwMTACEzMAAAH4o6EmDAxASP4AAQAAAfgwDQYJYIZIAWUDBAIBBQCgggFKMBoG
# CSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgf7muhlVi
# 8IzW22WBIZRf1KWN4/Ox5K1XAUWS/DT+tzQwgfoGCyqGSIb3DQEJEAIvMYHqMIHn
# MIHkMIG9BCDvzDPyXw1UkAUFYt8bR4UdjM90Qv5xnVaiKD3I0Zz3WjCBmDCBgKR+
# MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT
# HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB+KOhJgwMQEj+AAEA
# AAH4MCIEIItsgLx4VNWyYJAQAR6rUETJtj98oTwSaDl522XlI7/QMA0GCSqGSIb3
# DQEBCwUABIICAJNUCrf/GvTq6O5fPfNfYMKiSCOk2sg/jbCZ7UxD84/BlD2fNZ0X
# 8PCQuzcnRS21FW6AvEdOg1TvQPI5nqFM71a3+z0MPa1ZvXxOHlFFOdDrxyQGRUqD
# liWK2qEc/2jrmwq71vchrdyyCznsYsZgTEDqTzoEgKrdsER4yS1e0p7MCXAdBg46
# 3Z0yfEmZcpkCXwnuUO6cgSRNb0QsG3X53MiBYBNjAkhlZdXzIr7t8ZITV5O2aV5I
# CcysWOZAnB73ffPK9VnPUszp2I1LOPQq8TSWyCqhVzGcSMcwoHwUqY9iEqnUwS0A
# tDODf8jZrfTbEHQUJa3XmCEOoggPIHhuJgfMTs2/dAHbqPv9rwyyTilg/pzmBSGw
# +Ml/mZiV+26oYUMo9WeuJGl0ektros0YpcbRnOe31vruDDbj/lxEnewtB7cYzAhQ
# 8d+oHspN8qWQA2Rp5NL9xcSZD1djaCfurgd78ahZonsmNrUiy8EE9pxCXLmsGBLL
# B5eBzLOC72DDav0qudZH23a9eiBnlo8+Hewo6mzFzLfS48NeP1TXLFH6Lu1ycuk2
# +rvv5+7Ly/CzE/45BOWEcY4weTgiKxEOTMmmLRDqct7hwCtH8rver51BmuSQu17s
# fV/DDIja+ktuziE1HPsa/N9OZ5Zt0eyfM7fG/zC0UwbMB/+uWy6k9WQG
# SIG # End signature block