Winfield.psm1

# Copyright (c) Microsoft Corporation. All rights reserved.
# Winfield.psm1 0.99.0 2023-08-12 16:46:57
# ASZ-ArcA-Deploy main Debug-x64

#requires -Version 5.1
#requires -RunAsAdministrator

$global:WinfieldInitComplete = $false
$ErrorActionPreference = "Stop"

# This script contains all functions to Restore Winfield from Azure Artifacts.
# The script is standalone so it can be easily copied to different environments without any other dependencies.
# By default checks for required software and hardware capabilities are included.
# Import-Winfield and operator cmdlets are the primary methods exposed for use.

$artifactsOrganizationUri = "https://msazure.visualstudio.com/"
$artifactsFeedName = "AzureStackUniversalBuddy"

function Trace-Execution([string] $message)
{
    $caller = (Get-PSCallStack)[1]
    Write-Verbose -Message "[$([DateTime]::UtcNow.ToString('u'))][$($caller.Command)] $message" -Verbose
}


function Import-Artifact([string] $path, [PSCredential] $credential)
{
    $IPaddressPrefix = "10.0.50"
    $vmSwitch = "Winfield-Ingress"
    $hostIp = "$IPaddressPrefix.1"

    CreateVmNetwork -vmSwitch $vmSwitch -hostIp $hostIp -IPaddressPrefix $IPaddressPrefix

    $dirs = Get-ChildItem -Path $path -Directory # Convention is that every single directory in the artifact represents single VM aka Desired State
    $vmSet = Get-VMSet # List of currently running VMs represents current belief

    foreach ($dir in $dirs)
    {
        $vmName = $dir.Name
        if ((DoesVmExist -vmSet $vmSet -value $vmName) -ne $true)
        {
            Trace-Execution "Importing $vmName from path: $path"
            $vmLocation = Join-Path -Path $path -ChildPath $vmName
            $vmcxLocation = Join-Path -Path $vmLocation -ChildPath 'Virtual Machines'
            # $vmcxFileCount = (Get-ChildItem -Path $vmcxLocation).Count

            # Pick VMCX file and use it for importing VM into Hyper-V
            $vmcxName = (Get-ChildItem -Path $vmcxLocation -Filter "*.vmcx")[0].Name
            $vmcxFilePath = Join-Path -Path $vmcxLocation -ChildPath $vmcxName

            if ($vmName -match "IRVM")
            {
                $PhysicalRAM = (Get-CimInstance -ClassName Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum
                $hostLogicalProcessorCount = (Get-CimInstance -ClassName Win32_Processor | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum

                $irVMRequiredRAM = 45GB
                $hostMinRAM = 4GB
                $requiredRAM = $irVMRequiredRAM + $hostMinRAM

                # Update VMCX if RAM available is below the saved image plus min required for host OS
                if ($PhysicalRAM -le $requiredRAM)
                {
                    $irRAM = $PhysicalRAM - 4GB # 4gb for host OS
                    Trace-Execution "Updating $vmName ($vmcxFilePath) to use $hostLogicalProcessorCount cores, $($irRAM / 1MB)GB ram"
                    ModifyVmcx -vmcxPath $vmcxLocation -vmcxFilename $vmcxFilePath -coreCount $hostLogicalProcessorCount -ramMB ($irRAM/1MB)

                    # get exported guid
                    $updatedVmcx = (Get-ChildItem $vmcxLocation "*.vmcx")[0]
                    $vmcxFilePath = $updatedVmcx.FullName
                    Trace-Execution "Updated vmcx $vmcxFilePath"
                }

                $irVMRequiredCPU = 24
                $hostMinCPU = 2
                $requiredCPUCount = $irVMRequiredCPU + $hostMinCPU

                # Update VMCX if number of logical processors is less than required for the saved image
                if($hostLogicalProcessorCount -lt $requiredCPUCount)
                {
                    $irRAM = 45Gb
                    $irCPUCount = $hostLogicalProcessorCount - $hostMinCPU
                    Trace-Execution "Updating $vmName ($vmcxFilePath) to use $irCPUCount cores, $($irRAM / 1MB)GB ram"
                    ModifyVmcx -vmcxPath $vmcxLocation -vmcxFilename $vmcxFilePath -coreCount $irCPUCount -ramMB ($irRAM/1MB)

                    # get exported guid
                    $updatedVmcx = (Get-ChildItem $vmcxLocation "*.vmcx")[0]
                    $vmcxFilePath = $updatedVmcx.FullName
                    Trace-Execution "Updated vmcx $vmcxFilePath"
                }
            }

            # Windows Server release information
            # https://learn.microsoft.com/en-us/windows/release-health/windows-server-release-info
            $osVersionMin = [Version]'10.0.17763' # Windows Server 2019
            $osVersion = ([Environment]::OSVersion).Version
            Trace-Execution "OS Version: $osVersion"

            if ($osVersion -ge $osVersionMin)
            {
                Trace-Execution "[START] Import-VM"
                Import-VM -Path "$vmcxFilePath" | Out-Null
                Trace-Execution "[END] Import-VM"
            }
            else
            {
                $unsupportedOSMsg = "Import-Winfield supported on Windows Server 2019 and later editions."
                Trace-Execution "$unsupportedOSMsg`r`nSee https://learn.microsoft.com/en-us/windows/release-health/windows-server-release-info for Windows Server releases."
                throw [PlatformNotSupportedException]::new("Error Importing Artifact: $unsupportedOSMsg")
            }
        }
    }

    # Set VM Processor CompatibilityForMigrationEnabled and start all non-running VMs
    $vmSet = Get-VMSet
    Set-VMProcessorCompatibility -vmSet $vmSet -compatibilityForMigration $false
    Start-VMSet $vmSet

    $vmStartWaitDurationSec = 120
    Trace-Execution "Waiting for $vmStartWaitDurationSec sec for VMs to start..."
    Start-Sleep -Seconds 120
    Trace-Execution "Done."
}

function WaitForVMNetwork
{
    $vmSet = Get-VMSet
    foreach ($vm in $vmSet)
    {
        Trace-Execution "Updating DNS server forwarder on $($vm.Name) to $dnsIP"
        $retryUntil = (Get-Date).AddMinutes(5)
        while ($retryUntil -gt (Get-Date))
        {
            try
            {
                Trace-Execution "Testing connection to $($vm.Name)..."
                $testConnectionVM = $false
                if(Test-Connection -ComputerName $vm.Name -Quiet)
                {
                    Trace-Execution "Test-Connection to $($vm.Name) succeeded."
                    $testConnectionVM = $true
                }

                # test TCP connectivity to Portal and SysConfig
                $serviceTcpConnection = $false

                # NOTE: Parameterized to avoid PSScriptAnalyzer error PSAvoidUsingComputerNameHardcoded
                $netConnectionList = @"
"Name","ComputerName","Port","Result"
"Portal","portal.autonomous.cloud.private","443"
"SysConfig","169.254.53.25","8320"
"@
 | ConvertFrom-Csv

                foreach ($netConnectionItem in $netConnectionList)
                {
                    $netConnectionItem.Result = Test-NetConnection -ComputerName $netConnectionItem.ComputerName -Port $netConnectionItem.Port
                    if ($netConnectionItem.Result.TcpTestSucceeded)
                    {
                        Write-Verbose -Message "Test-NetConnection $($netConnectionItem.ComputerName):$($netConnectionItem.Port) succeeded"
                    }
                }

                $serviceTcpConnection = ($netConnectionList[0].Result.TcpTestSucceeded -and $netConnectionList[1].Result.TcpTestSucceeded)

                if($testConnectionVM -and $serviceTcpConnection)
                {
                    Trace-Execution "TCP connectivity test to SysConfig service and Portal passed."
                    break
                }
                else
                {
                    Trace-Execution "Wait for network: vm connectivity: $($testConnectionVM) portal connectivity: $($portalTcpConnection) syscfg connectivity: $($sysCfgTcpConnection)"
                    Start-Sleep -Seconds 15
                }
            }
            catch
            {
                Trace-Execution "Error checking connectivity to IRVM01 $_"
                Start-Sleep -Seconds 15
            }
        }
    }
}

function Get-LKGVersionFromFeed
{
    param
    (
        [string] $ViewName
    )

    $lkgVersion = "*"

    # Sign in with a personal access token (PAT)
    # https://learn.microsoft.com/en-us/azure/devops/cli/log-in-via-pat?view=azure-devops&tabs=windows
    $pat = [System.Environment]::GetEnvironmentVariable('AZURE_DEVOPS_EXT_PAT')

    if([string]::IsNullOrEmpty($pat))
    {
        $ViewName = $ViewName.ToLower()
        $lkgBlobUri = "https://winfieldartifacts.blob.core.windows.net/control/control$ViewName.json"
        Trace-Execution "Obtain LKG version from $lkgBlobUri"
        $lkgContainer = (Invoke-RestMethod -uri $lkgBlobUri -UseBasicParsing).LKGContainer
        $lkgVersion = $lkgContainer.Replace("-",".")
    }
    else
    {
        $B64Pat = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("`:$pat"))
        $headers = @{Authorization = "Basic $B64Pat"; "Content-Type" = "application/json"}

        $FeedId      = 'AzureStackUniversalBuddy'
        $packageName = 'arca.onenode.complete'
        $uri = "https://feeds.dev.azure.com/msazure/_apis/packaging/Feeds/$FeedId/Packages?packageNameQuery=$packageName&protocol%20Type=nuget&api-version=5.1-preview.1&includeAllVersions=true"
        $response = Invoke-WebRequest -Uri $uri -UseBasicParsing -Headers $headers -Verbose
        $data = ($response.Content | ConvertFrom-Json).value | Where-Object name -EQ $PackageName | Select-Object -First 1

        $lkgLatest = $data.versions | Where-Object { $_.views.name -eq $ViewName }| Select-Object version, publishDate, id, storageId | Sort-Object publishDate -Descending | Select-Object -First 1
        Trace-Execution "LKG build version for ViewName: $ViewName = $($lkgLatest.version)"
        $lkgVersion = $lkgLatest.version
    }

    Trace-Execution "LKG version for View $ViewName = $lkgVersion"
    return $lkgVersion
}

<#
.SYNOPSIS
Downloads artifacts from Azure DevOps Universal Feed
#>

function Restore-Artifact([string] $path, [string] $name = "arca.onenode.complete", [string] $ViewName, [string] $version, [bool] $useArtifactTool = $false, [bool] $skipInit = $false)
{
    Trace-Execution "Path = $path ViewName = $ViewName Version = $version"
    Trace-Execution "[START] Downloading artifact from ADO: $name..."
    $ErrorActionPreference = "Continue"

    az cloud set --name 'AzureCloud'

    if ($useArtifactTool -eq $false)
    {
        Trace-Execution "az artifacts universal download --organization $artifactsOrganizationUri --feed $artifactsFeedName --name $name --version $version --path $path"
        az artifacts universal download --organization $artifactsOrganizationUri --feed $artifactsFeedName --name $name --version $version --path $path
    }
    else
    {
        Trace-Execution "ArtifactToolExe universal download --service $artifactsOrganizationUri --feed $artifactsFeedName --package-name $name --package-version $version --path $path --patvar UNIVERSAL_PAT"
        & $global:ArtifactToolExe universal download --service $artifactsOrganizationUri --feed $artifactsFeedName --package-name $name --package-version $version --path $path --patvar UNIVERSAL_PAT
    }

    $ErrorActionPreference = "Stop"
    Trace-Execution "[END] Download from ADO"
}

<#
.SYNOPSIS
Verifies MD5 checksum of downloaded Winfield artifacts.
.PARAMETER Path
Path to artifacthash.json
.EXAMPLE
Test-WinfieldCheckSum -Path "\\su1fileserver\SU1_Infrastructure_1\arca\0714\Winfield\IRVM01\artifacthash.json"
#>

function Test-WinfieldCheckSum
{
    [OutputType([bool])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({Test-Path -Path $_ -PathType Leaf})]
        [ValidatePattern('artifacthash[.]json$')]
        [string] $Path
    )

    Trace-Execution "[START] Test-WinfieldCheckSum Path: $Path"
    $sw = [System.Diagnostics.Stopwatch]::StartNew()

    $artifactHashInfo = (Get-Content -Path $Path | ConvertFrom-Json).ArtifactHashInfo
    $artifactFolder = Split-Path -Parent $Path
    foreach($artifactInfo in $artifactHashInfo)
    {
        # TODO: Handle storage.json
        if ($artifactInfo.FileName -eq 'storage.json')
        {
            continue
        }

        $expectedMd5 = $artifactInfo.MD5
        $localArtifact = (Get-ChildItem -Path $artifactFolder -Include $artifactInfo.FileName -Recurse).FullName
        $actualMD5 = (Get-FileHash -Path $localArtifact -Algorithm MD5).Hash
        Trace-Execution "File: $($artifactInfo.FileFullName) expected checksum: $expectedMd5 actual checksum: $actualMD5"
        if($expectedMd5 -ne $actualMD5)
        {
            $md5CheckFailedMessage = "$($artifactInfo.FileName) MD5 checksum does not match with expected checksum.`r`n"
            Trace-Execution "Download or checksum validation error.`r`n$md5CheckFailedMessage"
            $sw.Stop()
            Trace-Execution "[END] Test-WinfieldCheckSum completed with ERROR in $($sw.Elapsed.TotalSeconds) seconds."
            throw [System.InvalidOperationException]::new($md5CheckFailedMessage)
        }
    }

    $sw.Stop()
    Trace-Execution "[END] Test-WinfieldCheckSum completed with SUCCESS in $($sw.Elapsed.TotalSeconds) seconds."
    return $true
}


function Test-ImportWinfieldParameters
{
    param
    (
        $InputParameters,
        $InputArgs
    )

    Trace-Execution "Following input parameters were specified:"
    foreach($key in $InputParameters.Keys)
    {
        $parameterStr = "[$Key] = "
        if($Key -eq 'code')
        {
            $parameterStr += "*" * 8
        }
        else
        {
            $parameterStr += $InputParameters[$Key]
        }
        Trace-Execution "$parameterStr"
    }

    $i = 0
    foreach($inputArg in $InputArgs)
    {
        Trace-Execution "$i $inputArg"
        $i++
    }

    if($InputParameters.ContainsKey("code"))
    {
        Trace-Execution "Validating input code"
        $sasToken = $InputParameters["code"]
        Invoke-WebRequest -Uri "https://winfieldartifacts.blob.core.windows.net/control/controlrelease.json$sasToken" -UseBasicParsing | Out-Null
        Trace-Execution "validated input code"
    }

    if($InputParameters.ContainsKey("version"))
    {
        $version = [Version] $InputParameters["version"]
        Trace-Execution "Validated version format = $version"
    }

    $pathInfo = [System.Uri] $InputParameters["Path"]
    if($pathInfo.IsUnc)
    {
        $errMsg = "UNC paths are not yet supported."
        Trace-Execution "$($pathInfo.OriginalString)`: $errMsg"
        throw [System.NotSupportedException]::new($errMsg)
    }
}


function Get-WinfieldHostInfo
{
    Trace-Execution "[START] Get-ComputerInfo"
    $computerInfo = Get-ComputerInfo
    Trace-Execution "Computer Info: $($computerInfo | Out-String)"
    Trace-Execution "[END] Get-ComputerInfo"
}


function Show-PostInstall($DeployResult)
{
    $art = @"
______ _ _ _ _ _____ _ _ ______ _____ _____ _ ______
| ___ \ (_) | | | | | |_ _| \ | || ___|_ _| ___| | | _ \
| |_/ / __ ___ _ ___ ___| |_ | | | | | | | \| || |_ | | | |__ | | | | | |
| __/ '__/ _ \| |/ _ \/ __| __| | |/\| | | | | . ` || _| | | | __|| | | | | |
| | | | | (_) | | __/ (__| |_ \ /\ /_| |_| |\ || | _| |_| |___| |___| |/ /
\_| |_| \___/| |\___|\___|\__| \/ \/ \___/\_| \_/\_| \___/\____/\_____/___/
              _/ |
             |__/
 
"@

    $fontColor = 'White'
    if($DeployResult)
    {
        $fontColor = 'Green'
    }
    Write-Host -ForegroundColor $fontColor "`r`n$art`r`n"
    Trace-Execution "DeployResult = $DeployResult"
    Trace-Execution "If automated install has failed, Winfield portal doesn't load, try to download Winfield manually and import the VM."
    Trace-Execution "Open browser at https://portal.autonomous.cloud.private to try out Winfield."
    Trace-Execution "Use Azure CLI to try out various user scenarios."
    Trace-Execution "Refer to user guide at: https://aka.ms/winfield-userguide"
    Trace-Execution "`r`n"
}


<#
.SYNOPSIS
Connects to system config endpoint to retrieve system application health state. Retries every 30s until specified $TimeoutSec.
#>

function Wait-SystemReady
{
    param
    (
        [Parameter(Mandatory = $false)]
        [int]
        $TimeoutSec = 300
    )

    Trace-Execution "[START] Wait-SystemReady"
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $intervalSec = 30
    $iterations = [Math]::Ceiling($TimeoutSec / $intervalSec)

    for($i = 1; $i -le $iterations; $i++)
    {
        Trace-Execution "[CHECK][Attempt $i] System readiness..."
        try
        {
            $healthState = Get-WinfieldHealthState
            Trace-Execution "$($healthState | Out-String)"
            if($healthState.ReadinessStatusDetails.Services -eq 100)
            {
                Trace-Execution "System is ready to use."
                $sw.Stop()
                Trace-Execution "[END] Wait-SystemReady completed in $($sw.Elapsed.TotalSeconds) seconds."
                return $true
            }
            else
            {
                Trace-Execution "[WAIT] for $intervalSec sec before retry"
                Start-Sleep -Seconds $intervalSec
            }
        }
        catch
        {
            Trace-Execution "[ERROR] connecting to system config endpoint. $($_) `r`nWaiting for $intervalSec sec before retry."
            Start-Sleep -Seconds $intervalSec
        }
    }

    Trace-Execution "[ERROR] System hasn't converged in $TimeoutSec seconds."
    $sw.Stop()
    Trace-Execution "[END] Wait-SystemReady completed with error in $($sw.Elapsed.TotalSeconds) seconds."
    return $false
}


<#
.SYNOPSIS
    Validates Winfield installation.
#>

function Test-Winfield
{
    param
    (
        [Parameter(Mandatory = $false)]
        [int]
        $TimeoutSec = 300
    )

    Trace-Execution "START: Validating Winfield installation"
    $adapters = Get-VM IRVM01 | Select-Object -ExpandProperty NetworkAdapters
    Trace-Execution "Winfield NICs: $($adapters | Out-String)"
    foreach($adapter in $adapters)
    {
        if($adapter.Status -ne 'Ok')
        {
            throw [System.InvalidOperationException]::new("Winfield NIC $($adapter.Name) status is not ok")
        }
    }

    $systemReady = Wait-SystemReady -TimeoutSec $TimeoutSec

    if(-not $systemReady)
    {
        throw [System.TimeoutException]::new("System has not converged in $TimeoutSec. Check diagnostic logs for details.")
    }

    # basic portal test
    Trace-Execution "Testing if Portal is accessible"
    $portalPingUrl = 'https://portal.autonomous.cloud.private/api/ping'
    $pingResponseFile = "$env:APPDATA\Winfield\pingresponse.json"
    Remove-Item -Path $pingResponseFile -Force -ErrorAction SilentlyContinue
    DownloadWithRetry -url $portalPingUrl -downloadLocation $pingResponseFile -retries 30

    if(-not (Test-path $pingResponseFile))
    {
        throw [System.InvalidOperationException]::new("Portal /api/ping status is $($response.StatusCode) instead of http/200")
    }
    Trace-Execution "END: Validating Winfield installation"
}


function Show-SystemConfiguration
{
    try
    {
        New-Item -Path "$env:APPDATA\Winfield" -ItemType Directory -Force | Out-Null
        $sysCfgFile = "$env:APPDATA\Winfield\WinfieldVersion.json"
        DownloadWithRetry -url 'http://169.254.53.25:8320/SystemConfiguration' -downloadLocation $sysCfgFile
        $sysCfgContent = Get-Content -Path $sysCfgFile
        Trace-Execution "System Configuration: $sysCfgContent"
    }
    catch
    {
        Trace-Execution "Error calling http://169.254.53.25:8320/SystemConfiguration: $_`r`n$($_.Exception)."
    }
}


function Set-ObsSettingsInternal
{
    param
    (
        [Parameter(Mandatory = $true)]
        $webRequestParams
    )

    try
    {
        $response = Invoke-WebRequest @webRequestParams
    }
    catch
    {
        if($null -ne $_.Exception.Response)
        {
            $responseStatusCode = [int] $_.Exception.Response.StatusCode
        }
        Trace-Execution "Error setting observability configuration. $($_) Status code: http/$responseStatusCode"
        throw $_.Exception
    }

    if($response.StatusCode -ne 202)
    {
        $obsErr = "Error setting observability configuration. Status code: $(response.StatusCode)"
        Trace-Execution "$obsErr"
        throw [System.InvalidOperationException]::new($obsErr)
    }
}


<#
.SYNOPSIS
Set default Winfield Observability configuration
#>

function Set-DefaultObservabilityConfiguration
{
    [CmdletBinding()]
    param
    (
    )

    $webRequestParams = @{
        Uri = "http://169.254.53.25:8320/ObservabilityConfiguration"
        Method = 'PUT'
        Body = (@{} | ConvertTo-Json)
        ContentType = 'application/json'
        UseBasicParsing = $true
        TimeOutSec = 30
    }

    Trace-Execution "[START] Set Default Observability configuration: $($webRequestParams)"
    Set-ObsSettingsInternal -webRequestParams $webRequestParams
    Trace-Execution "[END] Set Set Observability configuration"
}

<#
.SYNOPSIS
Set Winfield Observability configuration
#>

function Set-ObservabilityConfiguration
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [string] $ResouceGroupName,

        [Parameter(Mandatory = $true)]
        [string] $TenantId,

        [Parameter(Mandatory = $true)]
        [string] $Location,

        [Parameter(Mandatory = $true)]
        [string] $SubscriptionId,

        [Parameter(Mandatory = $true)]
        [string] $ServicePrincipalId,

        [Parameter(Mandatory = $true)]
        [securestring] $ServicePrincipalSecret
    )

    $obsCfgParams = @{
        ResourceGroup = $ResouceGroupName
        TenantId = $TenantId
        Location = $Location
        SubscriptionId = $SubscriptionId
        ServicePrincipalId = $ServicePrincipalId
        ServicePrincipalSecret = ([System.Net.NetworkCredential]::new("", $ServicePrincipalSecret).Password)
    }

    $webRequestParams = @{
        Uri = "http://169.254.53.25:8320/ObservabilityConfiguration"
        Method = 'PUT'
        Body = ($obsCfgParams | ConvertTo-Json)
        ContentType = 'application/json'
        UseBasicParsing = $true
    }

    Trace-Execution "[START] Set Observability configuration"
    Set-ObsSettingsInternal -webRequestParams $webRequestParams
    Trace-Execution "[END] Set Observability configuration"
}


<#
.SYNOPSIS
Get Winfield Observability configuration
#>

function Get-ObservabilityConfiguration
{
    [CmdletBinding()]
    param
    (
    )

    $obsCfg = (Invoke-WebRequest -Uri "http://169.254.53.25:8320/ObservabilityConfiguration" -Method 'Get' -UseBasicParsing -TimeoutSec 30).Content
    Trace-Execution "Observability configuration: $obsCfg"
    return $obsCfg
}


<#
.SYNOPSIS
Exports Winfield root certificate
.EXAMPLE
Export-WinfieldRootCert -FilePath c:\winfield\winfieldRoot.cer
#>

function Export-WinfieldRootCert
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        [ValidateScript({(-not [String]::IsNullOrEmpty([System.IO.Path]::GetFileName($_)))})]
        $FilePath = "$env:APPDATA\Winfield\winfieldRoot.cer"
    )

    Trace-Execution "[START] Export Winfield root cert to $FilePath"
    $retries = 20
    $waitSec = 30

    for($attempt = 1; $attempt -le $retries; $attempt++)
    {
        try
        {
            Trace-Execution "Attempt: $attempt"
            $response = Invoke-RestMethod http://169.254.53.25:8320/PublicRootCertificate -UseBasicParsing -TimeoutSec 30 -Verbose
            if($response.Status -eq 'ok')
            {
                break;
            }
        }
        catch
        {
            Trace-Execution "Error: $($_)"
            Trace-Execution "Retry in $waitSec seconds."
            Start-Sleep -Seconds $waitSec
        }
    }

    if($null -eq $response.certificate)
    {
        throw "Failed to download Winfield certificate from sys config endpoint"
    }

    $certFolder = [System.IO.Path]::GetDirectoryName($FilePath)
    New-Item -Path $certFolder -ItemType Directory -Force | Out-Null

    # note this cert is already base64 encoded format
    $response.certificate | Out-File $FilePath
    Trace-Execution "Winfield root cert exported to $FilePath"
    Trace-Execution "[END] Export Winfield root cert"
}

<#
.SYNOPSIS
    Imports Winfield certificates into local cert store and Python cert store (if Azure CLI is installed)
.EXAMPLE
Import-WinfieldRootCert -FilePath c:\winfield\winfieldRoot.cer
#>

function Import-WinfieldRootCert
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        [ValidateScript({Test-Path $_})]
        $FilePath = "$env:APPDATA\Winfield\winfieldRoot.cer"
    )

    Trace-Execution "[START] Import Winfield root cert"
    Import-Certificate -FilePath $FilePath -CertStoreLocation Cert:\LocalMachine\Root | Out-Null
    Import-Certificate -FilePath $FilePath -CertStoreLocation Cert:\CurrentUser\Root  | Out-Null
    Trace-Execution  "$(Get-ChildItem "$env:APPDATA\Winfield" | Out-String)" -Verbose

    if (Test-CliInstalled)
    {
        UpdatePythonCertStore -WinfieldRootCertPath $FilePath
    }
    
    Trace-Execution "[END] Import Winfield cert"
}

function UpdatePythonCertStore
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        [ValidateScript({Test-Path $_})]
        $WinfieldRootCertPath = "$env:APPDATA\Winfield\winfieldRoot.cer"
    )

    Trace-Execution "[START] Updating CLI cert store with Winfield root cert at $WinfieldRootCertPath"
    $cerFile = $WinfieldRootCertPath
    Trace-Execution "Updating Python cert store with $cerFile"
    $pythonCertStore = "${env:ProgramFiles(x86)}\Microsoft SDKs\Azure\CLI2\Lib\site-packages\certifi\cacert.pem"
    Trace-Execution "Python cert store location $pythonCertStore"

    $root = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2

    if(Test-Path $cerFile)
    {
        $root.Import($cerFile)
        Trace-Execution "$(Get-Date) Extracting required information from the cert file"
        $md5Hash    = (Get-FileHash -Path $cerFile -Algorithm MD5).Hash.ToLower()
        $sha1Hash   = (Get-FileHash -Path $cerFile -Algorithm SHA1).Hash.ToLower()
        $sha256Hash = (Get-FileHash -Path $cerFile -Algorithm SHA256).Hash.ToLower()
        $issuerEntry  = [string]::Format("# Issuer: {0}", $root.Issuer)
        $subjectEntry = [string]::Format("# Subject: {0}", $root.Subject)
        $labelEntry   = [string]::Format("# Label: {0}", $root.Subject.Split('=')[-1])
        $serialEntry  = [string]::Format("# Serial: {0}", $root.GetSerialNumberString().ToLower())
        $md5Entry     = [string]::Format("# MD5 Fingerprint: {0}", $md5Hash)
        $sha1Entry    = [string]::Format("# SHA1 Fingerprint: {0}", $sha1Hash)
        $sha256Entry  = [string]::Format("# SHA256 Fingerprint: {0}", $sha256Hash)
        $certText = (Get-Content -Path $cerFile -Raw).ToString().Replace("`r`n","`n")
        $rootCertEntry = "`n" + $issuerEntry + "`n" + $subjectEntry + "`n" + $labelEntry + "`n" + `
                        $serialEntry + "`n" + $md5Entry + "`n" + $sha1Entry + "`n" + $sha256Entry + "`n" + $certText
        Trace-Execution "Adding the certificate content to Python Cert store"
        Add-Content $pythonCertStore $rootCertEntry
        Trace-Execution "Python Cert store was updated to allow the Azure Stack CA root certificate"
    }
    else
    {
        $errorMessage = "$cerFile required to update CLI was not found."
        Trace-Execution "ERROR: $errorMessage"
        throw "UpdatePythonCertStore: $errorMessage"
    }

    Trace-Execution "[END] Updating CLI cert store"
}

function GetExpectedVmFiles
{
    param
    (
        [string] $lkgBlobUri,
        [string] $code,
        [string[]] $pattern,
        [int] $expectedFileCount
    )

    # azcopy list
    # https://learn.microsoft.com/en-us/azure/storage/common/storage-ref-azcopy-list
    # Adding these [flags] to support automation scenarios.
    # --skip-version-check Do not perform the version check at startup. Intended for automation scenarios & airgapped use.
    # --machine-readable Lists file sizes in bytes.
    $output = Invoke-AzCopy -operation "blob list pattern: $pattern" -azCopyParameters @('list', "$lkgBlobUri$code", '--skip-version-check', '--machine-readable')

    $vmFiles = @()
    $matchInfo = $output | Select-String -Pattern $pattern
    foreach($match in $matchInfo)
    {
        # INFO: ArcA_ABData.vhdx; Content Length: 2.54 GiB
        # INFO: ArcA_ABData.vhdx; Content Length: 2722103296 (--machine-readable)
        $vmFiles += $match.Line.Split(";")[0].Split(":")[1].Trim()
    }

    # e.g. @('86E04238-2615-4EF7-9769-8C472512FF6D.vmcx', '86E04238-2615-4EF7-9769-8C472512FF6D.vmgs', '86E04238-2615-4EF7-9769-8C472512FF6D.VMRS')
    Trace-Execution "Following VM files are located in blob location:`r`n$($vmFiles | ConvertTo-Json)"

    if($vmFiles.count -ne $expectedFileCount)
    {
        throw "Did not find expected VM files (guid.vmcx, guid.vmgs, guid.VMRS) at blob location. Check the SAS token used."
    }

    return $vmFiles
}

function Invoke-AzCopy
{
      [CmdletBinding()]
      param
      (
          [string] $operation,
          [string[]] $azCopyParameters
      )

      Trace-Execution "[START][$operation]"
      $azCopyExe = DownloadAzCopy
      try
      {
          if($azCopyParameters.Length -lt 2)
          {
              throw "expected atleast 2 azcopy parameters"
          }
          $azCopyCommand = $azCopyParameters[0]
          Trace-Execution "Executing $azCopyExe $azCopyCommand"

          $azcliOutputFile = "azcli_out.txt"
          $azcliErrFile = "azcli_err.txt"

          Start-Process -FilePath $azCopyExe -ArgumentList $azCopyParameters -RedirectStandardOutput $azcliOutputFile -RedirectStandardError $azcliErrFile -Wait -NoNewWindow
          Trace-Execution "LASTEXITCODE = $LASTEXITCODE."

          $azcliOutput = Get-Content -Path $azcliOutputFile -ErrorAction SilentlyContinue
          $azcliError = Get-Content -Path $azcliErrFile -ErrorAction SilentlyContinue
          Trace-Execution "AzCLI output: $azcliOutput"

          if(-not [string]::IsNullOrEmpty($azcliError))
          {
              Trace-Execution "AzCLI error: $azcliError"
              throw "Error executing azcopy command $azCopyCommand. See azcopy logs in $env:HOMEDRIVE\$env:HOMEPATH\.azcopy for details."
          }
          else
          {
              Trace-Execution "azcopy command $azCopyCommand executed successfully."
          }

        # if($LASTEXITCODE -ne 0)
        # {
        # throw "Error executing azcopy command $azCopyCommand.`r`n`r`nSee azcopy logs for details."
        # }
        # else
        # {
        # Trace-Execution "azcopy command $azCopyCommand executed successfully."
        # }
      }
      catch
      {
          if($ErrorActionPreference -eq 'Continue')
          {
              Trace-Execution "Ignoring error $($_)"
          }
          else
          {
              throw $_.Exception
          }
      }
      Trace-Execution "[END][$operation]"
      return $azcliOutput
}


function Resolve-BlobUrl {
    param
    (
        [string] $blobUrl,
        [string] $ViewName = 'release',
        [string] $version,
        [string] $code
    )

    $blobContainer = ""

    if([string]::IsNullOrEmpty($version))
    {
        # download and read control data file
        New-Item -Path "$env:APPDATA\Winfield" -ItemType Directory -Force | Out-Null
        $downloadedControlDataFile = Join-Path -Path "$env:APPDATA\Winfield" -ChildPath "localControl.json"
        $controlDataBlobUrl = "$blobUrl/control/control$ViewName.json"
        Trace-Execution "Get blob container info from: $controlDataBlobUrl"
        $output = Invoke-AzCopy -operation "cp $controlDataBlobUrl" -azCopyParameters @('cp', "$controlDataBlobUrl$code", $downloadedControlDataFile)
        Trace-Execution "downloadedControlDataFile contents:`r`n$(Get-Content $downloadedControlDataFile)`r`n"
        $controlData = Get-Content $downloadedControlDataFile | ConvertFrom-Json
        if($controlData.CopyInProgress)
        {
            Trace-Execution "LKG build copy is in progress."
            $blobContainer = $controlData.PreviousLKGContainer
            Trace-Execution "Use previous LKG container: $blobContainer"
        }
        else
        {
            $blobContainer = $controlData.LKGContainer
        }
    }
    else
    {
        Trace-Execution "Version $version specified"
        $blobContainer = $version.Replace('.','-')
        Trace-Execution "Will download from blob container $blobContainer"
    }

    $resolvedBlobUrl = "$blobUrl/$blobContainer"
    Trace-Execution "BlobUrl resolved to: $resolvedBlobUrl"
    return $resolvedBlobUrl
}

function DownloadArtifactFromBlob
{
    param
    (
        [string] $blobUrl,
        [string] $code,
        [string[]] $artifacts,
        [string] $downloadFolder,
        [string] $destination
    )

    Trace-Execution "START: DownloadArtifactFromBlob: $blobUrl"

    # download or copy VHDX files
    foreach($artifact in $artifacts)
    {
        if(Test-Path -Path "$localFolder\$artifact")
        {
            # it was previously downloaded
            Trace-Execution "$artifact was previously downloaded, moving it to $destination"
            Move-Item -Path "$downloadFolder\$artifact" -Destination $destination
        }
        else
        {
            $artifactInBlob = "$blobUrl/$artifact$code"
            $artifactDestination = "$destination\$artifact"

            if(Test-Path $artifactDestination)
            {
                Trace-Execution "$artifactDestination exists, skip download"
            }
            else
            {
                Trace-Execution "Invoking AzCopy to download from $blobUrl/$artifact to $downloadFolder ..."
                $output = Invoke-AzCopy -operation "copy $artifact" -azCopyParameters @('cp', $artifactInBlob, $downloadFolder)
                Trace-Execution "Moving $artifact to destination: $artifactDestination"
                Move-Item -Path "$downloadFolder\$artifact" -Destination $artifactDestination -Verbose
            }
        }
    }
    Trace-Execution "END: DownloadArtifactFromBlob"
}


function Set-VMHostVMPath
{
    param
    (
        [string] $path
    )

    Import-Module Hyper-V -ErrorAction Stop
    Trace-Execution "Setting VMHost VM Path location = $path"
    Set-VMHost -VirtualMachinePath $path
}

function Test-Hardware
{
    param
    (
        [string] $path
    )

    Trace-Execution "[START] Hardware check"
    Trace-Execution "Checking hardware requirements: minimum 8 vCPU, 24GB RAM, install path 200GB SSD drive space, Hyper-V path 50GB drive space. Recommended: 24 vCPU, 48GB RAM"

    # Check Processor
    Trace-Execution "[START] Checking processor"
    # NumberOfLogicalProcessors in Win32_Processor is an array for each socket in the case
    # of multi socket systems. In the case of single socket systems it is an Int32.
    $hostLogicalProcessorCount = (Get-CimInstance -ClassName Win32_Processor | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum
    if ($hostLogicalProcessorCount -lt 8 -and -not $bestFit)
    {
        throw "This machine has $hostLogicalProcessorCount cores. Winfield requires a minimum of 8 cores."
    }
    Trace-Execution "[END] Checking processor"

    # Check RAM
    Trace-Execution "[START] Checking RAM"
    $PhysicalRAM = (Get-CimInstance -ClassName Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum
    if ($PhysicalRAM -lt 24GB)
    {
        if ($bestFit)
        {
            Trace-Execution "Attempting to restore using available RAM. This deployment may encounter stability/health issues due to using below the recommeded specs."
        }
        else
        {
            throw "Winfield is recommended to run with a minimum of 24GB RAM. Add '-bestFit' to attempt to restore using available RAM."
        }
    }
    Trace-Execution "[END] Checking RAM"

    # Check disk space
    Trace-Execution "[START] Checking Disk space"
    $drive = (Get-Item $path).Root.FullName
    if ($drive.StartsWith("\\"))
    {
        throw "You cannot restore Winfield to a network share."
    }

    $driveLetter = $drive.Substring(0, 1)
    $volume = Get-Volume -DriveLetter $driveLetter

    if ($volume.SizeRemaining -lt 200GB)
    {
        $available = [int] ($volume.SizeRemaining / 1GB)
        Trace-Execution "The path '$path' has $($available)GB available."
        throw "Winfield requires at least 200GB space on the drive you restore to."
    }

    # Check disk is an SSD - fails on lab env
# if (-not (IsSsdDrive -path $path))
# {
# throw "Winfield requires an installation path on an SSD drive."
# }

    # Check Hyper-V default storage location disk space
    Import-Module Hyper-V -ErrorAction Stop
    $vmHost = Get-VMHost
    $hvDrive = $vmHost.VirtualMachinePath.Substring(0, 1)
    $hvVolume = Get-Volume -DriveLetter $hvDrive
    $restoreSpace = [int] ($volume.SizeRemaining / 1GB)
    $defaultSpace = [int] ($hvVolume.SizeRemaining / 1GB)
    Trace-Execution "[END] Checking Disk space"
    Trace-Execution "Hardware check passed - $hostLogicalProcessorCount proc, $($PhysicalRam)GB RAM, restore space $($restoreSpace)GB, Hyper-V space $($defaultSpace)GB"
    Trace-Execution "[END] Hardware check"
    return $true
}


<#
.SYNOPSIS
Downloads Winfield Appliance
.EXAMPLE
    #To download latest Winfield release artifacts from blob storage to the current working directory.
    Invoke-DownloadWinfieldAppliance -path . -code 'SAS_TOKEN'
.EXAMPLE
    #To download latest Winfield release artifacts from ADO universal feed to the current working directory.
    #Download from ADO universal feed option requires Azure CLI to be installed, and logged into Azure CLI.
    #Optionally, set AZURE_DEVOPS_EXT_PAT environment variable. See Example 4 for details.
    Invoke-DownloadWinfieldAppliance -path .
.EXAMPLE
    #To download latest Winfield prerelease artifacts from blob storage to the f:\winfield. Folder will be created if it does not exist.
    Invoke-DownloadWinfieldAppliance -path f:\winfield -code 'SAS_TOKEN' -ViewName 'prerelease'
.EXAMPLE
    #To download specific Winfield release version from ADO universal feed to the current working directory.
    #If AZURE_DEVOPS_EXT_PAT environment variable is set, there's no need to log into Azure CLI.
    $env:AZURE_DEVOPS_EXT_PAT = 'ADO_PAT'
    Invoke-DownloadWinfieldAppliance -path . -version '...'
#>

function Invoke-DownloadWinfieldAppliance
{
    param
    (
        [Parameter(Mandatory=$true, ParameterSetName = 'DefaultSet')]
        [Parameter(Mandatory=$true, ParameterSetName = 'Code')]
        [ValidateScript({Test-Path $_})]
        [string] $Path,

        [Parameter(Mandatory=$false, ParameterSetName = 'DefaultSet')]
        [Parameter(Mandatory=$false, ParameterSetName = 'Code')]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('release', 'prerelease')]
        [string] $ViewName = "release",

        [Parameter(Mandatory=$false, ParameterSetName = 'DefaultSet')]
        [Parameter(Mandatory=$false, ParameterSetName = 'Code')]
        [ValidateNotNullOrEmpty()]
        [string] $version,

        [Parameter(Mandatory=$true, ParameterSetName = 'Code', HelpMessage = 'Specify Azure Blob container SAS token')]
        [ValidateNotNullOrEmpty()]
        [string] $code
    )

    Trace-Execution "[START] Invoke-DownloadWinfieldAppliance"

    Initialize-Download -code $code

    $sw = [System.Diagnostics.Stopwatch]::StartNew()

    $downloadFolder = $Path
    New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null
    Trace-Execution "Winfield will be downloaded to $downloadFolder"

    if(-not [string]::IsNullOrEmpty($code))
    {
        $blobBaseUrl = "https://winfieldartifacts.blob.core.windows.net"
        $blobUrl = Resolve-BlobUrl -blobUrl $blobBaseUrl -code $code -ViewName $ViewName
        Trace-Execution "Winfield LKG Blob URL for ViewName $ViewName = $blobUrl"

        # create fixed local folder for downloading the artifact
        $irvmFolder = Join-Path -Path $downloadFolder -ChildPath "IRVM01"
        New-Item -ItemType Directory -Path $irvmFolder -Force | Out-Null
        Trace-Execution "Winfield artifacts will be downloaded from blob container $blobBaseUrl to $downloadFolder and moved to $irvmFolder"

        # create destination folder layout
        New-Item -ItemType Directory -Path "$irvmFolder\Snapshots" -Force | Out-Null
        New-Item -ItemType Directory -Path "$irvmFolder\Virtual Hard Disks" -Force | Out-Null
        New-Item -ItemType Directory -Path "$irvmFolder\Virtual Machines" -Force | Out-Null

        <#
        $expectedVhdxFiles = @('IRVM01.vhdx',
                               'ArcA_EphemeralData_IRVM01_1.vhdx',
                               'ArcA_LocalData_IRVM01_1.vhdx',
                               'ArcA_SharedData_IRVM01.vhdx',
                               'Docker_IRVM01_1.vhdx'
                             )
        #>


        # TODO: determine list from storage.json
        $expectedVhdxFiles = GetExpectedVmFiles -lkgBlobUri $blobUrl -code $code -pattern @(".vhdx") -expectedFileCount 12
        $expectedVmFiles = GetExpectedVmFiles -lkgBlobUri $blobUrl -code $code -pattern @(".VMRS",".vmcx",".vmgs") -expectedFileCount 3

        # download artifacts from LKG Blob
        DownloadArtifactFromBlob -blobUrl $blobUrl -artifacts $expectedVhdxFiles -code $code -downloadFolder $downloadFolder -destination "$irvmFolder\Virtual Hard Disks"
        DownloadArtifactFromBlob -blobUrl $blobUrl -artifacts $expectedVmFiles -code $code -downloadFolder $downloadFolder -destination "$irvmFolder\Virtual Machines"
        DownloadArtifactFromBlob -blobUrl $blobUrl -artifacts "artifacthash.json" -code $code -downloadFolder $downloadFolder -destination "$irvmFolder"
        DownloadArtifactFromBlob -blobUrl $blobUrl -artifacts "Version.json" -code $code -downloadFolder $downloadFolder -destination "$irvmFolder"

        $importVmPath = (Get-Item -Path $irvmFolder).Parent.FullName
        Trace-Execution "Winfield artifacts are in $importVmPath"
        Trace-Execution "$((Get-ChildItem $importVmPath -Recurse).FullName | Out-String)" -Verbose

        # TODO: wire up validate downloaded files
        #$artifactHashFile = (Resolve-Path -Path "$path\artifacthash.json").Path
        #Trace-Execution "Validating MD5 checksum of downloaded files using $artifactHashFile"
        #$result = Test-WinfieldCheckSum -Path $artifactHashFile
        $result = $true
        Trace-Execution "Validation result: $result"
    }
    else
    {
        Trace-Execution "Downloading from ADO artifact feed."
        $name = "arca.onenode.complete"
        Trace-Execution "Winfield artifacts will be downloaded from ADO artifact feed $name to $downloadFolder"

        if([string]::IsNullOrEmpty($version))
        {
            Trace-Execution "Version number not specified, get LKG version from ADO Universal Feed for ViewName = $ViewName"
            $version = Get-LKGVersionFromFeed -ViewName $ViewName
        }

        Trace-Execution "Using version: $version"
        $result = Restore-Artifact -path $downloadFolder -name $name -ViewName $ViewName -version $version
    }

    $sw.Stop()
    Trace-Execution "Download result: $result"
    Trace-Execution "Download completed in $($sw.Elapsed.TotalSeconds) seconds."
    Trace-Execution "[END] Invoke-DownloadWinfieldAppliance"
    return $result
}


<#
.SYNOPSIS
Helper function to cleanup existing VMs
#>

function Invoke-CleanupVM
{
    param
    (
        [bool] $clean
    )

    Trace-Execution "[START] Invoke-CleanupVM"
    $isClean = $false
    $existingVMs = Get-VMSet
    if ($null -ne $existingVMs -and $existingVMs.Count -gt 0)
    {
        Trace-Execution "Appliance VM exists"
        if (-not $clean)
        {
            $notCleanMessage = "Remove any existing Winfield VMs prior to restoring or add '-clean' to the Import-Winfield command."
            Trace-Execution "$notCleanMessage"
            throw "$notCleanMessage"
        }
        else
        {
            Trace-Execution "Removing existing Winfield virtual machines..."
            RemoveVMs $existingVMs
            Trace-Execution "Appliance VM removed"
            $isClean = $true
        }
    }
    else
    {
        Trace-Execution "Appliance VM does not exist"
        $isClean = $true
    }

    Trace-Execution "Environment IsClean: $isClean"
    Trace-Execution "[END] Invoke-CleanupVM"
    return $isClean
}


function Test-DownloadWinfield
{
    param
    (
        [Parameter(Mandatory=$true)]
        $Path,

        [Parameter(Mandatory=$false)]
        [bool]
        $VerifyCheckSum
    )

    Trace-Execution "[START] Test-DownloadWinfield folder: $Path"

    $downloadValid = $false
    if(-not (Test-Path -Path (Join-Path -Path $Path -ChildPath "IRVM01")))
    {
        Trace-Execution "[ERROR] IRVM01 folder not found under $Path, specify path to folder that contains IRVM01."
    }
    else
    {
        $expectedVmFiles = @('*.VMCX', '*.VMGS', '*.VMRS')

        # TODO: verify from storage.json file which will be included soon
        $expectedVhdxFiles = @(
            'ArcA_ABData.vhdx'
            'ArcA_EphemeralData_A.vhdx'
            'ArcA_EphemeralData_B.vhdx'
            'ArcA_LocalData_A.vhdx'
            'ArcA_LocalData_B.vhdx'
            'ArcA_SharedData_A.vhdx'
            'ArcA_SharedData_B.vhdx'
            'BCDR_1.vhdx'
            'OSAndDocker_A.vhdx'
            'OSAndDocker_B.vhdx'
            'Reserved_2.vhdx'
            'Reserved_3.vhdx'
        )

        foreach($expectedVmFile in $expectedVmFiles)
        {
            $vmFilePath = Join-Path -Path $Path -ChildPath "IRVM01\Virtual Machines\$expectedVmFile"
            Trace-Execution "Checking for $vmFilePath"
            if(-not (Test-Path -Path $vmFilePath))
            {
                Trace-Execution "[ERROR] Did not find expected VM File $expectedVmFile under $Path"
                return $false
            }
        }

        foreach($expectedVhdxFile in $expectedVhdxFiles)
        {
            $vhdxFilePath = Join-Path -Path $Path -ChildPath "IRVM01\Virtual Hard Disks\$expectedVhdxFile"
            Trace-Execution "Checking for $vhdxFilePath"
            if(-not (Test-Path -Path $vhdxFilePath))
            {
                Trace-Execution "[ERROR] Did not find expected VHD File $expectedVhdxFile under $Path"
                return $false
            }
        }

        $downloadValid = $true
    }

    if($VerifyCheckSum)
    {
        $artifacthashPath = (Get-ChildItem -Path $Path -Filter 'artifacthash.json' -Recurse).FullName
        Trace-Execution "ArtifactHashFile path = $artifacthashPath"
        if($null -eq $artifacthashPath)
        {
            Trace-Execution "[ERROR] Could not locate artifacthash.json under $Path"
        }
        else
        {
            Trace-Execution "Note: Checksum verification can take upto 30 min to complete."
            $checkSumResult = Test-WinfieldCheckSum -Path $artifacthashPath
            Trace-Execution "Checksum result: $checkSumResult"
        }

        $downloadValid = $checkSumResult
    }
    else
    {
        Trace-Execution "[WARN] Consider specifying -VerifyCheckSum flag to verify checksum. Currently only supported for blob downloads."
    }

    if($downloadValid)
    {
        Trace-Execution "[SUCCESS] Winfield artifacts at $Path verified."
    }
    else
    {
        $errorMsg = "[ERROR] Winfield artifacts at $Path is not valid download."
        Trace-Execution "$errorMsg"
    }

    Trace-Execution "[END] Test-DownloadWinfield folder: $Path"
    return $downloadValid
}


<#
.SYNOPSIS
Imports Winfield and completes the installation process that includes networking setup, installing root cert, and validating install.
.EXAMPLE
#Installs Winfield appliance using files at f:\winfield directory. This this the top level folder that contains IRVM01 folder.
Install-WinfieldAppliance -path f:\winfield
.EXAMPLE
#Installs Winfield appliance using files in the current folder and cleanup existing VM
Install-WinfieldAppliance -path . -clean
.EXAMPLE
#Installs Winfield appliance using files in the current folder and cleanup existing VM and use bestfit option. Use bestfit option if recommended hardware spec aren't available.
Install-WinfieldAppliance -path . -clean -bestfit
#>

function Install-WinfieldAppliance
{
    param
    (
        [Parameter(Mandatory=$true)]
        [string] $Path,
        [switch] $VerifyCheckSum,
        [switch] $clean,
        [switch] $bestFit,
        [int] $TimeoutSec = 1800
    )

    # START Transcript
    $timestamp = [DateTime]::UtcNow.ToString("yyyyMMdd-HHmmss")
    $logPath = (New-Item -Path "$env:ProgramData\Microsoft\Winfield\Logs" -ItemType Directory -Force).FullName
    $logFile = Join-Path -Path $logPath -ChildPath "ImportWinField_${timestamp}.txt"
    try { Start-Transcript -Path $logFile -Force | Out-String | Write-Verbose -Verbose } catch { Write-Warning -Message $_.Exception.Message }

    Trace-Execution "[START] Install-Winfield"
    Test-ImportWinfieldParameters -InputParameters $PsBoundParameters -InputArgs $args
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $installSuccessful = $false

    try
    {
        # Verify downloaded artifacts
        $validDownload = Test-DownloadWinfield -Path $Path -VerifyCheckSum $VerifyCheckSum.IsPresent
        if(-not $validDownload)
        {
            $errorMsg = "Error verifying Winfield artificats. Check logs for details."
            throw [System.InvalidOperationException]::new($errorMsg)
        }

        # Initialize VM Host
        Initialize-VMHost

        # Update hosts file
        Update-HostsFile

        # Get host info
        Get-WinfieldHostInfo

        # Import
        Import-WinfieldAppliance -path $Path -clean $clean.IsPresent -bestFit $bestFit.IsPresent

        # Networking setup
        NetworkSetupPostImport
        WaitForVMNetwork

        # Install CLI
        Install-AzCLI

        # Get root cert public key from SysConfig service and install it in the local cert store and python cert store
        $rootCertPath = "$env:APPDATA\Winfield\winfieldRoot.cer"
        Export-WinfieldRootCert -FilePath $rootCertPath
        Import-WinfieldRootCert -FilePath $rootCertPath

        # Validate install
        Test-Winfield -TimeoutSec $TimeoutSec

        $installSuccessful = $true
    }
    catch
    {
        Trace-Execution "Error Installing Winfield:`r`n$($_)"
    }
    finally
    {
        Show-SystemConfiguration
        Show-PostInstall -DeployResult $installSuccessful
        $sw.Stop()
        Trace-Execution "Install-WinfieldAppliance completed execution in $($sw.Elapsed.TotalSeconds) seconds."
        # STOP Transcript
        try { Stop-Transcript | Out-String | Write-Verbose -Verbose } catch { Write-Warning -Message $_.Exception.Message }
    }

    Trace-Execution "[END] Install-Winfield"
    return $installSuccessful
}


<#
.SYNOPSIS
Imports the downloaded Winfield appliance VM to local hyper-v server
#>

function Import-WinfieldAppliance
{
    param
    (
        [Parameter(Mandatory=$true)]
        [string] $Path,
        [bool] $clean,
        [bool] $bestFit
    )

    Trace-Execution "[START] Import-WinfieldAppliance"
    Test-ImportWinfieldParameters -InputParameters $PSBoundParameters -InputArgs $args
    $importSuccessful = $false

    $sw = [System.Diagnostics.Stopwatch]::StartNew()

    # cleanup prior deployment
    if(Invoke-CleanupVM -clean $clean)
    {
        Trace-Execution "Winfield appliance doesn't exist, proceeding with hardware pre-req test."
    }
    else
    {
        throw "Cleanup failed, please remove prior appliance VM manually."
    }

    # hardware pre-req test
    $vmHostVMPath = (Resolve-Path -Path $Path).Path
    Set-VMHostVMPath -path $vmHostVMPath
    $hwTestResult = Test-Hardware -path $Path
    if($hwTestResult)
    {
        Trace-Execution "Hardware test passed."
    }
    else
    {
        throw "Hardware test failed"
    }

    # import VM
    Import-Artifact -path $vmHostVMPath -bestFit $bestFit
    $importSuccessful = $true

    $sw.Stop()

    Trace-Execution "Import success: $importSuccessful"
    Trace-Execution "Imported appliance VM in $($sw.Elapsed.TotalSeconds) seconds."
    Trace-Execution "[END] Import-WinfieldAppliance"

    return $importSuccessful
}

function Get-VMSet() {
    $vmSet = Get-VM | Where-Object { $_.Name -like "IRVM*" }
    return $vmSet
}


function Set-VMProcessorCompatibility($vmSet, $compatibilityForMigration)
{
    Trace-Execution "Setting VM Processor CompatibilityForMigrationEnabled = $compatibilityForMigration"

    foreach ($vm in $vmSet) {
        $name = $vm.Name
        Trace-Execution "Start: Set VM Processor CompatibilityForMigrationEnabled $name"
        if (($vm.State -eq "Off")) {
            Get-VM -name $name  | Set-VMProcessor -CompatibilityForMigrationEnabled $compatibilityForMigration
        }
        Trace-Execution "Complete: Set VM Processor CompatibilityForMigrationEnabled $name"
    }
}

function Start-VMSet($vmSet)
{
    Trace-Execution "Starting all VMs"

    foreach ($vm in $vmSet) {
        $name = $vm.Name
        Trace-Execution "Start: start VM $name"
        if (($vm.State -eq "Off") -or ($vm.State -eq "Saved")) {
            Start-VM -Name $name
        }
        Trace-Execution "Complete: start VM $name"
    }
}

function Stop-VMSet($vmSet, [bool]$turnOff = $false)
{
    foreach ($vm in $vmSet) {
        $name = $vm.Name
        Trace-Execution "Stopping $name..."
        if ($vm.State -eq "Running") {

            # Check if we should use graceful shutdown/stop
            if ($turnOff -eq $false)
            {
                try {
                    Stop-VM -Name $name -Force
                }
                catch {
                    Trace-Execution "Failed to save VM; $_"
                }

                # Hyper-V will wait up to 5 minutes for guest to shutdown
                $stopWaitTime = (Get-Date).AddMinutes(6)
                while (($vm.State -ne "Off" -or $vm.OperationalStatus -match "MergingDisks") -and $stopWaitTime -gt (Get-Date)) {
                    Trace-Execution "Waiting for VM to stop / merge to complete, current state $($vm.State)"
                    Start-Sleep -Seconds 30
                    $vm = Get-VM -Name $name
                }

                # Wait up to 5 additional minutes if merging
                $stopWaitTime = (Get-Date).AddMinutes(5)
                while (($vm.OperationalStatus -match "MergingDisks") -and $stopWaitTime -gt (Get-Date))
                {
                    Trace-Execution "Waiting for VM merge to complete..."
                    Start-Sleep -Seconds 30
                    $vm = Get-VM -Name $name
                }
            }

            # If guest is still running force power off
            if ($vm.State -ne "Off") {
                Trace-Execution "Force turning off $name in state $($vm.State)"
                Stop-VM -Name $name -TurnOff -Force
            }
        }
        Trace-Execution "Stopped $name"
    }
}

function DoesVmExist($vmSet, [string]$value)
{
    foreach ($vm in $vmSet)
    {
        if ($vm.Name -eq $value)
        {
            return $true
        }
    }
    return $false
}

function Show-NetIPAddressInfo
{
    $netIpCfgs = Get-NetIPConfiguration | Where-Object{$_.InterfaceDescription -inotlike "*hyper-v*"}
    foreach($netIpCfg in $netIpCfgs)
    {
        Trace-Execution "$($netIpCfg | Out-String)"
        Trace-Execution "NetIpAddress: $(Get-NetIPAddress -InterfaceIndex $netIpCfg.InterfaceIndex | Out-String)"
    }
}

function CreateVmNetwork([string] $vmSwitch, [string] $hostIp, [string]$IPaddressPrefix)
{
    Trace-Execution "[BEFORE] CreateVmNetwork"
    Show-NetIPAddressInfo

    Trace-Execution "Cleaning up existing Winfield VMSwitches"
    Get-VMSwitch "winfield*" | Remove-VMSwitch -Force -Verbose -ErrorAction SilentlyContinue
    Get-VMSwitch "*devenv*" | Remove-VMSwitch -Force -Verbose -ErrorAction SilentlyContinue

    Trace-Execution "Creating vmswitch Winfield-Ingress"
    New-VMSwitch -SwitchName "Winfield-Ingress" -SwitchType Internal | Out-Null

    Trace-Execution "Setting new New-NetIPAddress on adapter Winfield-Ingress"
    $adapter = Get-NetAdapter -Name "*(Winfield-Ingress)"
    New-NetIPAddress -IPAddress 10.0.50.1 -PrefixLength 24 -InterfaceIndex $adapter.ifIndex -ErrorAction SilentlyContinue | Out-Null
    Trace-Execution "Waiting for network changes...."
    Start-Sleep -Seconds 10

    Trace-Execution "Creating NAT Winfield-Ingress-NAT"
    Get-NetNat | Remove-NetNat -Confirm:$false -ErrorAction SilentlyContinue -Verbose
    New-NetNat -Name "Winfield-Ingress-NAT" -InternalIPInterfaceAddressPrefix 10.0.50.0/24 -ErrorAction SilentlyContinue | Out-Null
    Trace-Execution "Waiting for network connection...."
    Start-Sleep -Seconds 30

    Trace-Execution "Creating vmswitch Winfield-Management"
    New-VMSwitch -SwitchName "Winfield-Management" -SwitchType Internal | Out-Null

    Trace-Execution "Setting IP address on Winfield-Management"
    $managementAdapter = Get-NetAdapter -Name "*(Winfield-Management)"

    Trace-Execution "Cleanup NetIPAddress if it exists"
    Get-NetIPAddress -IPAddress 169.254.53.20 -ErrorAction SilentlyContinue | Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue

    Trace-Execution "Setting IP address 169.254.53.20 on interface $($managementAdapter.Name)"
    New-NetIPAddress -IPAddress 169.254.53.20 -PrefixLength 16 -InterfaceIndex $managementAdapter.ifIndex | Out-Null

    Trace-Execution "[AFTER] CreateVmNetwork"
    Show-NetIPAddressInfo
}

function NetworkSetupPostImport
{
    Trace-Execution "[BEFORE] NetworkSetupPostImport"
    Show-NetIPAddressInfo

    Trace-Execution "[START] Post VM import networking setup"
    Trace-Execution "Connecting IRVM01 ingress network adapter to Winfield-Ingress switch."
    Get-VM -Name "IRVM01" | Get-VMNetworkAdapter -Name "Winfield-Ingress" | Connect-VMNetworkAdapter -SwitchName "Winfield-Ingress" -ErrorAction SilentlyContinue

    Trace-Execution "Connecting IRVM01 management network adapter to Winfield-Management switch."
    Get-VM -Name "IRVM01" | Get-VMNetworkAdapter -Name "Winfield-Management" | Connect-VMNetworkAdapter -SwitchName "Winfield-Management" -ErrorAction SilentlyContinue

    $networkAdapters = Get-VMNetworkAdapter -VMName 'IRVM01'
    Trace-Execution "IRVM01 network adapters: $($networkAdapters | Out-String)"
    foreach($networkAdapter in $networkAdapters)
    {
        if(($null -eq $networkAdapter.SwitchName) -or ($networkAdapter.Name -ne $networkAdapter.SwitchName))
        {
            throw "IRVM01 network adapter $($networkAdapter.Name) is not connected to Switch or doesn't match switch name."
        }
    }

    Trace-Execution "[END] Post VM import networking setup"
    Trace-Execution "[AFTER] NetworkSetupPostImport"
    Show-NetIPAddressInfo
}

function DownloadWithRetry([string] $url, [string] $downloadLocation, [int] $retries)
{
    while($true)
    {
        try
        {
            Invoke-WebRequest $url -OutFile $downloadLocation -Verbose
            break
        }
        catch
        {
            $exceptionMessage = $_.Exception.Message
            Trace-Execution "Failed to download '$url': $exceptionMessage"
            if ($retries -gt 0) {
                $retries--
                Trace-Execution "Waiting 10 seconds before retrying. Retries left: $retries"
                Start-Sleep -Seconds 10
            }
            else
            {
                $exception = $_.Exception
                throw $exception
            }
        }
    }
}

function Add-HostsFileEntry
{
    param
    (
        [string] $hostName,
        [string] $ip = "127.0.0.1"
    )
    $hostsPath = "$env:windir\System32\drivers\etc\hosts"
    $hosts = Get-Content -Path $hostsPath
    $escapedHost = $hostName -replace '[.]', '\.'
    $exists = $false
    $hosts = $hosts | ForEach-Object {
        if ($_ -match $escapedHost)
        {
            $exists = $true
            "$ip`t$hostName"
        }
        else
        {
            $_
        }
    }

    if ($exists -eq $false) {
        $hosts += "$ip`t$hostName";
    }

    $hosts | Out-File -FilePath $hostsPath -Encoding ascii
}

function InstallServerFeature($name)
{
    $r = Install-WindowsFeature -Name $name
    if ($r.RestartNeeded -ne "No")
    {
        throw "Restart your machine to complete installing $name before re-running this script. $($r.RestartNeeded)"
    }
}

function InstallClientFeature($name)
{
    $f = Get-WindowsOptionalFeature -FeatureName $name -Online
    if ($f.State -ne "Enabled")
    {
        $r = Enable-WindowsOptionalFeature -FeatureName $name -NoRestart -Online
        if ($r.RestartNeeded -ne "No")
        {
            throw "Restart your machine to complete installing $name before re-running this script. $($r.RestartNeeded)"
        }
    }
}


# Installs HyperV features on the VM Host where Winfield appliance will be imported
function Initialize-VMHost
{
    param
    (
    )

    # Microsoft Windows 10 Enterprise
    # Microsoft Azure Stack HCI
    Trace-Execution "[START] Check Hyper-V pre-req"
    $osName = (Get-CimInstance -ClassName Win32_OperatingSystem).Caption
    if ($osName -match 'Windows Server')
    {
        Trace-Execution "Checking prereqs..."
        InstallServerFeature "Hyper-V-Tools"
        InstallServerFeature "Hyper-V-PowerShell"
    }
    elseif ($osName -match "Windows 11 Pro")
    {
        Trace-Execution "Checking prereqs..."
        InstallClientFeature "Microsoft-Hyper-V"
        InstallClientFeature "Microsoft-Hyper-V-Tools-All"
    }
    Trace-Execution "[END] Check Hyper-V pre-req"
}

# Updates hosts file on the VM Host where Winfield appliance will be imported
function Update-HostsFile
{
    Trace-Execution "[START] Update hosts file"
    $hostEntries = @(
        @{name = "irvm01"; ip = "10.0.50.4"},
        @{name = "his.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "login.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "hosting.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "portal.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "graph.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "armmanagement.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "adminmanagement.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "catalogapi.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "artifacts.blob.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "acrmanagedaccount0.blob.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "azgns-dev-autonomous-00.servicebus.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "mycontainerregistry.edgeacr.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "guestnotificationservice.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "autonomous.dp.kubernetesconfiguration.autonomous.cloud.private"; ip = "10.0.50.4"},
        @{name = "agentserviceapi.autonomous.cloud.private"; ip = "10.0.50.4"}
    )

    foreach ($entry in $hostEntries)
    {
        Trace-Execution "Adding hosts entry: $($entry.ip) $($entry.name)"
        Add-HostsFileEntry -hostName $entry.name -ip $entry.ip | Out-Null
    }

    Trace-Execution "[END] Update hosts file"
}


function Test-CliInstalled 
{
    $installed = Get-Command "az.cmd" -ErrorAction "SilentlyContinue"
    return ($null -ne $installed)
}

# Installs Azure CLI and azure-devops CLI extension
function Install-AzCLI
{
    param
    (
    )

    if (-not (Test-CliInstalled))
    {
        Trace-Execution "Install/Update Azure-CLI"
        DownloadWithRetry -url "https://aka.ms/install-winfield-cli-windows" -downloadLocation ".\AzureCLI.msi" -retries 6
        Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /quiet'
        $env:Path += ";c:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin"
    }
    else 
    {
        Trace-Execution "Azure CLI is already installed."
    }

    $devopsInstalled = az.cmd extension list --output json | ConvertFrom-Json | Where-Object {$_.name -ieq "azure-devops"}
    if (-not $devopsInstalled)
    {
        Trace-Execution "Install/Update Azure-Devops"
        az.cmd extension add --name azure-devops
    }
    else 
    {
        Trace-Execution "Azure DevOps CLI extension is already installed."
    }
}

function Initialize-Download([bool] $skipInit = $false, [string]$code)
{
    if ($global:WinfieldInitComplete -ne $true)
    {
        if ($skipInit -eq $false)
        {
            if([string]::IsNullOrEmpty($code))
            {
                Trace-Execution "CLI login is required to download artifacts. Install CLI if needed ..."
                Install-AzCLI
                $pat = [System.Environment]::GetEnvironmentVariable('AZURE_DEVOPS_EXT_PAT')
                if([string]::IsNullOrEmpty($pat))
                {
                    Trace-Execution "Switch cloud to AzureCloud"
                    az cloud set -n AzureCloud
                    Trace-Execution "Verify CLI login works ..."
                    $output = (az account show)

                    if ($null -eq $output)
                    {
                        Trace-Execution "Logging in to Azure..."
                        az login --use-device-code --allow-no-subscriptions | Out-Null
                    }

                    # verify if user is logged in
                    Trace-Execution "Verifying if logged into Azure using CLI..."
                    try
                    {
                        $subscriptions = az rest -u 'https://management.azure.com/subscriptions?api-version=2022-12-01'
                        if($null -eq $subscriptions)
                        {
                            Trace-Execution "Logging in to Azure..."
                            az login --use-device-code --allow-no-subscriptions | Out-Null
                        }
                        else
                        {
                            Trace-Execution "CLI login sucessful. List of subscriptions: $subscriptions"
                        }
                    }
                    catch
                    {
                        Trace-Execution "Logging in to Azure..."
                        az login --use-device-code --allow-no-subscriptions | Out-Null
                    }
                }
                else
                {
                    Trace-Execution "AZURE_DEVOPS_EXT_PAT will be used for downloading from ADO Universal feed."
                }
            }
            else
            {
                Trace-Execution "code entered, will download artifiacts from blob location."
            }
        }

        $global:WinfieldInitComplete = $true
    }
}

function RemoveVMs($vms)
{
    $vhds = $vms | ForEach-Object VMID | Get-VHD
    Stop-VMSet -vmSet $vms -turnOff $true
    $vms | ForEach-Object { $_ | Remove-VM -Force }
    $vhds | ForEach-Object { Remove-Item -Path $_.Path -Force }
}

function ProcessResult($result, $successString, $failureString)
{
    #Return success if the return value is "0"
    if ($result.ReturnValue -eq 0) {
        Trace-Execution "$successString"
    #If the return value is not "0" or "4096" then the operation failed
    } elseif ($result.ReturnValue -ne 4096) {
        Trace-Execution "$failureString Error value: $($result.ReturnValue)"
    } else {
        #Get the job object
        $job=[WMI]$result.job

        #Provide updates if the jobstate is "3" (starting) or "4" (running)
        while ($job.JobState -eq 3 -or $job.JobState -eq 4) {
            Trace-Execution "$($job.PercentComplete) % complete"
            Start-Sleep 1

            #Refresh the job object
            $job=[WMI]$result.job
        }

        #A jobstate of "7" means success
        if ($job.JobState -eq 7) {
            Trace-Execution "$successString"
        } else {
            Trace-Execution "$failureString"
            Trace-Execution "ErrorCode: $($job.ErrorCode)"
            Trace-Execution "ErrorDescription: $($job.ErrorDescription)"
        }
    }
}

<#
.SYNOPSIS
    Modifies Core and Memory configuration of Virtual Machine (VM).
#>

function ModifyVmcx
{
    param
    (
        [string] $vmcxPath,
        [ValidatePattern('[.]vmcx$')]
        [string] $vmcxFilename,
        [int] $coreCount = 0,
        [int] $ramMB = 0
    )

    #Retrieve the virtual system management service
    $VSMS = Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_VirtualSystemManagementService

    # Import the VM, referencing the VM configuration
    # Second parameter is the snapshot folder - but we are not editing snapshots so set it to null
    # Third parameter says whether to generate a new VM ID or not
    $importResult = $VSMS.ImportSystemDefinition($vmcxFilename, $null, $true)

    ProcessResult -result $importResult `
        -successString "Virtual machine configuration loaded into memory." `
        -failureString "Failed to load virtual machine configuration into memory."

    #Retrieve the object referencing the planned VM (in memory VM)
    $plannedVM = [WMI]$importResult.ImportedSystem

    #Retrieve the setting data for the planned VM
    $PVSD = ($plannedVM.GetRelated("Msvm_VirtualSystemSettingData", `
        "Msvm_SettingsDefineState", `
        $null, `
        $null, `
        "SettingData", `
        "ManagedElement", `
        $false, $null) | ForEach-Object {$_})

    #Modify the memory setting of the VM
    $MemSetting = $PVSD.getRelated("Msvm_MemorySettingData") | Select-Object -First 1
    $MemSetting.DynamicMemoryEnabled = 0
    $MemSetting.Reservation = $ramMB
    $MemSetting.VirtualQuantity = $ramMB
    $MemSetting.Limit = $ramMB
    $MemSetting.Weight = 100

    $memoryChangeResult = $VSMS.ModifyResourceSettings($MemSetting.GetText(1))
    ProcessResult -result $memoryChangeResult `
        -successString "Memory settings have been updated to $ramMB." `
        -failureString "Failed to update memory settings."

    if ($coreCount -gt 0)
    {
        $ProcSetting = $PVSD.getRelated("Msvm_ProcessorSettingData") | Select-Object -First 1
        $ProcSetting.VirtualQuantity = $coreCount

        $procChangeResult = $VSMS.ModifyResourceSettings($ProcSetting.GetText(1))
        ProcessResult -result $procChangeResult `
            -successString "Processor settings have been updated to $coreCount." `
            -failureString "Failed to update processor settings."
    }

    # Edit the Msvm_VirtualSystemExportSettingData to make sure we export only the VM configuration
    $VMExportSD = ($plannedVM.GetRelated("Msvm_VirtualSystemExportSettingData",`
                                        "Msvm_SystemExportSettingData", `
                                        $null, $null, $null, $null, $false, $null)`
                                        | ForEach-Object {$_})
    #CopySnapshotConfiguration - 1: ExportNoSnapshots - No snapshots will be exported with the VM.
    $VMExportSD.CopySnapshotConfiguration = 1
    #Indicates whether the VM runtime information will be copied when the VM is exported. (i.e. saved state)
    $VMExportSD.CopyVmRuntimeInformation = $false
    #Indicates whether the VM storage will be copied when the VM is exported. (i.e. VHDs/VHDx files)
    $VMExportSD.CopyVmStorage = $false
    #Indicates whether a subdirectory with the name of the VM will be created when the VM is exported.
    $VMExportSD.CreateVmExportSubdirectory = $false

    Remove-Item $vmcxFilename -Force -ErrorAction Ignore
    Remove-Item (Join-Path $vmcxPath "*.vm*") -Force -ErrorAction Ignore

    #Export the edited virtual machine to a new file.
    $exportResult = $VSMS.ExportSystemDefinition($plannedVM, $vmcxPath, $VMExportSD.GetText(1))

    ProcessResult -result $exportResult `
        -successString "Created new virtual machine confguration file." `
        -failureString "Failed to create new virtual machine confguration file."

    #Export places vm* files in a subdir, move them up one level
    Copy-Item (Join-Path $vmcxPath "Virtual Machines\*") $vmcxPath
    Remove-Item (Join-Path $vmcxPath "Virtual Machines") -Force -Recurse

    Trace-Execution "Virtual machine exported to $($vmcxPath)"
}

function IsSsdDrive([string] $path)
{
    $driveLetter = $path[0]
    foreach ($drive in Get-PhysicalDisk) {
        if (($drive | Get-Disk | Get-Partition).DriveLetter -Contains $driveLetter) {
            Return $drive.MediaType -eq 'SSD'
        }
    }
}

function DownloadAzCopy
{
    param
    (
        $DownloadURL = "https://aka.ms/downloadazcopy-v10-windows",
        $OutputPath = "C:\AzCopy\"
    )

    Trace-Execution "START: Installing AzCopy"
    $azCopyExe = Join-Path -Path $OutputPath -ChildPath "azcopy.exe"

    if(Test-Path $azCopyExe)
    {
        Trace-Execution "Azcopy is already installed"
    }
    else
    {
        Remove-Item -Path $OutputPath -Recurse -Force -ErrorAction SilentlyContinue
        New-Item -Path $OutputPath -ItemType Directory -Force -Verbose | Out-Null
        $azcopyZipFile = Join-Path -Path $OutputPath -ChildPath "AzCopy.zip"

        # Download AzCopy and extract the zip file
        Trace-Execution "Downloading Azcopy.exe..."
        DownloadWithRetry -url $DownloadURL -downloadLocation $azcopyZipFile -retries 6
        Expand-Archive -Path $azcopyZipFile -DestinationPath $OutputPath -Force | Out-Null

        # Rename the AzCopy executable to "AzCopy.exe"
        $azCopyExeExtracted = Get-ChildItem -Path $OutputPath -Filter "azcopy.exe" -Recurse
        Move-Item -Path $azCopyExeExtracted.FullName -Destination $OutputPath | Out-Null

        if(-not (Test-Path $azCopyExe))
        {
            throw "$OutputPath\azcopy.exe not found."
        }
    }
    Trace-Execution "END: Installing AzCopy"
    return $azCopyExe
}


# Currently only desktop edition is supported. specifically, PSEdition = Core version is not supported
function Test-PSEdition
{
    $versionTable = $PSVersionTable
    Trace-Execution "PSVersionTable:`r`n $($versionTable | Out-String)`r`n"
    if($versionTable.PSEdition -ne 'Desktop')
    {
        $errorMsg = 'Winfield module only supports PowerShell Desktop Edition.'
        throw [System.InvalidOperationException]::new($errorMsg)
    }
}

<#
    .SYNOPSIS
    Verifies connectivity to management endpoint and gets the current WinfieldAppliance state - current vs desired.
 
    .PARAMETER endpointIp
    IP address for the management endpoint
 
    .PARAMETER endpointPort
    Port for the management endpoint
    .OUTPUTS
    Object. The current appliance state grouped in functional areas.
#>

function Get-WinfieldApplianceSettings {
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param (
        [Parameter(Position = 0, Mandatory = $false, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [string]
        $endpointIp = '169.254.53.25',
        [Parameter(Position = 1, Mandatory = $false, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [string]
        $endpointPort = '8320'
    )
    begin {
        $test = Test-NetConnection -ComputerName $endpointIp -Port $endpointPort
        if (!$test.TcpTestSucceeded) {
            Write-Error "Unable to connect to configuration endpoint $endpointIp on port $endpointPort!"
            Write-Error "Make sure you are on a VM connected to the management network and there is no firewall blocking "
            exit 1;
        }
        $systemConfigServiceUri = "http://$($endpointIp):$($endpointPort)/SystemConfiguration"
        $observabilityUri = "http://$($endpointIp):$($endpointPort)/ObservabilityConfiguration"
    }
    process {
        try {
            Write-Verbose "Getting configuration for observability.. "
            $diagnostics = Invoke-RestMethod -Method Get $observabilityUri -ContentType "application/json"
        }
        catch {
            $diagnostics = @{}
        }
        try {
            Write-Verbose "Getting configuration for network.. "
            $network = Invoke-RestMethod -Method get $systemConfigServiceUri -ContentType "application/json"
        }
        catch {
            $network = @{}
        }

    }
    end {
        return @{
            "NetworkSettings" = $network;
            "Diagnostics"     = $diagnostics
        }
    }

}
<#
    .SYNOPSIS
    Set the current WinfieldAppliance desired state based on config or settings file (if path is given)
 
    .PARAMETER path
    Path to settings file (Json)
 
    .PARAMETER config
    Alternative config object containing settings for the desired state.
 
    .EXAMPLE
    Create a settings object using interactive mode and set the state
    $settings = New-WinfieldSettings -interactive
    Set-WinfieldApplianceDesiredState -configuration $settings -verbose
 
    .EXAMPLE
    Import settings from file and configure the appliance
    $settings = New-WinfieldSettings -path c:\winfield\applianceConfig.json
    Set-WinfieldApplianceDesiredState -configuration $settings -verbose
 
    .OUTPUTS
    Object. The returned values from the management endpoint.
#>

function Set-WinfieldApplianceDesiredState {
    [CmdletBinding(DefaultParameterSetName = "FromConfigObject", ConfirmImpact = "High")]
    [OutputType([PSCustomObject[]])]
    param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True, ParameterSetName = "FromFile")]
        [string]
        $path,

        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True, ParameterSetName = "FromConfigObject")]
        [object]
        $configuration,

        [Parameter(Position = 1, Mandatory = $false, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [switch]
        $skipDiagnostics,

        [Parameter(Position = 2, Mandatory = $false, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [string]
        $endpointIp = '169.254.53.25',
        [Parameter(Position = 3, Mandatory = $false, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [string]
        $endpointPort = '8320'
    )
    begin {
        if ($PSBoundParameters.ContainsKey('Path')) {
            $configuration = Import-WinfieldSettingsFromFile -Path $path
        }
        $valid = Test-WinfieldSettings -configuration $configuration -skipDiagnosticsValidation:$skipDiagnostics.IsPresent;
        if (!$valid) {
            Write-Error "Settings is invalid! Unable to finalize configuration"
            exit 1;
        }
        $systemConfigServiceUri = "http://$($endpointIp):$($endpointPort)/SystemConfiguration"
        $observabilityUri = "http://$($endpointIp):$($endpointPort)/ObservabilityConfiguration"

    }
    process {
        if(!$skipDiagnostics.IsPresent){
            Write-Verbose "Applying configuration for observability.. "
            $diagnostics = Invoke-RestMethod -Method Put $observabilityUri -ContentType "application/json" -Body ($configuration.Diagnostics | ConvertTo-Json)
        }

        Write-Verbose "Applying configuration for network.. "
        $network = Invoke-RestMethod -Method Put $systemConfigServiceUri -ContentType "application/json" -Body ($configuration.NetworkSettings | ConvertTo-Json)
    }
    end {
        if(!$skipDiagnostics.IsPresent){
            return @($diagnostics, $network)
        } else {
            return @($network)
        }
    }
}

<#
    .SYNOPSIS
    Fills in the Settings config object in an interactive fashion
 
    .PARAMETER config
    The inital configuration with defaults
 
    .OUTPUTS
    Object. The configuration with settings set by interactive mode.
#>

function Get-WinfieldInteractiveSettings {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [Object]
        $configuration
    )
    $skipDiagnostics = $false;
    $newConfig = $configuration.Clone();
    do {
        $i = Read-Host -Prompt "[Networking] DnsForwarderIpAddress [$($newConfig.NetworkSettings.DnsForwarderIpAddress)]"
        if ($i.Length) {
            $newConfig.NetworkSettings.DnsForwarderIpAddress = $i;
        }
        $i = Read-Host -Prompt "[Networking] IngressNICDefaultGateway [$($newConfig.NetworkSettings.IngressNICDefaultGateway)]"
        if ($i.Length) {
            $newConfig.NetworkSettings.IngressNICDefaultGateway = $i;
        }
        $i = Read-Host -Prompt "[Networking] IngressNICIPAddress [$($newConfig.NetworkSettings.IngressNICIPAddress)]"
        if ($i.Length) {
            $newConfig.NetworkSettings.IngressNICIPAddress = $i;
        }
        $i = Read-Host -Prompt "[Networking] IngressNICPrefixLength [$($newConfig.NetworkSettings.IngressNICPrefixLength)]"
        if ($i.Length) {
            $newConfig.NetworkSettings.IngressNICPrefixLength = $i;
        }
        $i = Read-Host -Prompt "[Networking] IsTelemetryOptOut [$($newConfig.NetworkSettings.IsTelemetryOptOut)]"
        if ($i.Length -gt 4) {
            $newConfig.NetworkSettings.IsTelemetryOptOut = $i;
        }
        $i = Read-Host -Prompt "Would you like to configure diagnostics? (Y/N)"
        if($i -and $i.Length -gt 0 -and $i -eq "y"){
            $skipDiagnostics = $false
            $i = Read-Host -Prompt "[Diagnostics] ResourceGroup [$($newConfig.Diagnostics.ResourceGroup)]"
            if ($i.Length) {
                $newConfig.Diagnostics.ResourceGroup = $i;
            }
            $i = Read-Host -Prompt "[Diagnostics] TenantId [$($newConfig.Diagnostics.TenantId)]"
            if ($i.Length) {
                $newConfig.Diagnostics.TenantId = $i;
            }
            $i = Read-Host -Prompt "[Diagnostics] Location [$($newConfig.Diagnostics.Location)]"
            if ($i.Length) {
                $newConfig.Diagnostics.Location = $i;
            }
            $i = Read-Host -Prompt "[Diagnostics] SubscriptionId [$($newConfig.Diagnostics.SubscriptionId)]"
            if ($i.Length) {
                $newConfig.Diagnostics.SubscriptionId = $i;
            }
            $i = Read-Host -Prompt "[Diagnostics] ServicePrincipalId [$($newConfig.Diagnostics.ServicePrincipalId)]"
            if ($i.Length) {
                $newConfig.Diagnostics.ServicePrincipalId = $i;
            }
            $i = Read-Host -Prompt "[Diagnostics] ServicePrincipalSecret [*******]" -MaskInput
            if ($i.Length) {
                $newConfig.Diagnostics.ServicePrincipalSecret = $i;
            }
        } else {
            $skipDiagnostics = $true
        }
    } while (-not (Test-WinfieldSettings -configuration $newConfig -skipDiagnosticsValidation:$skipDiagnostics -ErrorAction Continue));

    return $newConfig;
}

<#
    .SYNOPSIS
    Get the Winfield appliance health state (and convergence state)
 
    .OUTPUTS
    Json Object containing the health state and system convergence.
#>

function Get-WinfieldHealthState {
    [OutputType([PSCustomObject[]])]
    param (
        [Parameter(Position = 0, Mandatory = $false, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [string]
        $endpointIp = '169.254.53.25',
        [Parameter(Position = 1, Mandatory = $false, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [string]
        $endpointPort = '8320'
    )
    $endpoint = "http://$($endpointIp):$($endpointPort)/SystemReadiness"
    $result = Invoke-RestMethod -Method Get -Uri $endpoint -ContentType 'application/json'
    return $result
}

<#
    .SYNOPSIS
    Creates a Winfield configuration object - that can be exported to a file (as json)
 
    .PARAMETER path
    Path to export as settings file (Json). If not specified, returns settings objects
 
    .OUTPUTS
    Object. The returned settings object that can be edited, used to configure the appliance or exported as a file.
#>

function New-WinfieldSettings {
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param (
        [Parameter(Position = 0, Mandatory = $false)]
        [switch]
        $interactive
    )
    $networkSettings = @{
        "DnsForwarderIpAddress"    = "10.50.10.50";
        "IngressNICDefaultGateway" = "10.0.50.1";
        "IngressNICIPAddress"      = "10.0.50.4";
        "IngressNICPrefixLength"   = 24;
        "IsTelemetryOptOut"        = $false;
    }
    $observabilitySettings = @{
        "ResourceGroup"          = "WinfieldPreview";
        "TenantId"               = "<REPLACE ME>";
        "Location"               = "westus";
        "SubscriptionId"         = "<REPLACE ME>";
        "ServicePrincipalId"     = "<REPLACE ME>";
        "ServicePrincipalSecret" = "<REPLACE ME>";
    }

    $format = @{
        "NetworkSettings" = $networkSettings;
        "Diagnostics"     = $observabilitySettings;
    }

    if ($interactive.IsPresent) {
        $format = Get-WinfieldInteractiveSettings -configuration $format
    }

    return $format

}

<#
.SYNOPSIS
Exports the Winfield settings configuration object to file
.PARAMETER config
Configuration object
.PARAMETER path
Path to settings file (Json).
.OUTPUTS
Object. The exported config object
#>

function Export-WinfieldSettingsToFile {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [object]
        $configuration,
        [Parameter(Position = 1, Mandatory = $true, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [string]
        $Path
    )

    Write-Verbose "Writing settings to file $path"
    $configuration | ConvertTo-Json | Set-Content -Path $path
    return $configuration
}

<#
    .SYNOPSIS
    Gets a Winfield settings configuration object from file or default settings
 
    .PARAMETER path
    Path to settings file (Json).
 
    .OUTPUTS
    Object. The returned settings object that can be edited, used to configure the appliance or exported as a file.
#>


function Import-WinfieldSettingsFromFile {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [string]
        $Path
    )

    if ($PSBoundParameters.ContainsKey('path')) {
        if (-not (Test-Path $path)) {
            Write-Error "Settings file does not exist"
            exit 1;
        }
        $rawContent = Get-Content -Path $path -raw
        if ($rawContent.Length -lt 2) {
            Write-Error "Empty config file"
            exit 1;
        }
        $config = $rawContent | ConvertFrom-Json
        if (!$?) {
            Write-Error "Invalid JSON format"
            exit 1;
        }

        return $config
    }
    else {
        Write-Error "Path not specified - returning default settings object"
        exit 1;
    }
}

<#
    .SYNOPSIS
    Verifies that the configuration settings are valid.
 
    .PARAMETER configuration
    Configuration object containing settings to validate.
 
    .PARAMETER skipDiagnosticsValidation
    Skips diagnostics validation.
 
    .OUTPUTS
    Boolean. true if configuration is valid. False if there is any issue.
#>

function Test-WinfieldSettings {
    [CmdletBinding()]
    [OutputType([Boolean])]
    param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [object]
        $configuration,
        [Parameter(Position = 1, Mandatory = $false, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [switch]
        $skipDiagnosticsValidation
    )
    $valid = $true
    $network = $configuration.NetworkSettings
    if (!$network) {
        Write-Error "Network settings not present"
        $valid = $false;
    }
    else {
        if (!([ipaddress]$network.DnsForwarderIpAddress)) {
            Write-Error "Network setting DnsForwarderIpAddress not valid IP Address"
            $valid = $false;
        }
        if (!([ipaddress]$network.IngressNICDefaultGateway)) {
            Write-Error "Network setting IngressNICDefaultGateway not valid IP"
            $valid = $false;
        }
        if (!([ipaddress]$network.IngressNICIPAddress)) {
            Write-Error "Network setting IngressNICIPAddress not valid IP"
            $valid = $false;
        }
        if ($network.IngressNICPrefixLength -lt 8 -or $network.IngressNICPrefixLength -gt 31) {
            Write-Error "Network setting IngressNICPrefixLength must be > 8 and < 32"
            $valid = $false;
        }
        $t = $false;
        if(!$network.IsTelemetryOptOut.GetType() -eq [bool]){
            if ([bool]::TryParse($network.IsTelemetryOptOut, [ref]$t)) {
                Write-Error "Network setting IsTelemetryOptOut must be true or false (is : $($network.IsTelemetryOptOut))"
                $valid = $false;
            }
        }
    }
    if (!$skipDiagnosticsValidation.IsPresent) {
        # [ipaddress]
        $diagnostics = $configuration.Diagnostics

        if (!$diagnostics) {
            Write-Error "Diagnostics settings not present"
            $valid = $false;
        }
        else {
            $g = [guid]::NewGuid();

            if (!$diagnostics.ResourceGroup -or $diagnostics.ResourceGroup.Length -lt 1) {
                Write-Error "Diagnostics settings - resource group is invalid"
                $valid = $false;
            }
            if (!$diagnostics.TenantId -or $diagnostics.TenantId -eq '<REPLACE ME>' -or ![guid]::tryParse($diagnostics.TenantId, [ref]$g)) {
                Write-Error "Diagnostics settings - TenantId is invalid. Must be set and must be a guid "
                $valid = $false;
            }
            if (!$diagnostics.Location -or $diagnostics.Location.Length -lt 5) {
                Write-Error "Diagnostics settings - Location is invalid. Must be set to a valid Azure location, e.g. westus "
                $valid = $false;
            }
            if (!$diagnostics.SubscriptionId -or $diagnostics.SubscriptionId -eq '<REPLACE ME>' -or ![guid]::tryParse($diagnostics.SubscriptionId, [ref]$g)) {
                Write-Error "Diagnostics settings - SubscriptionId is invalid. Must be set and must be a guid "
                $valid = $false;
            }
            if (!$diagnostics.ServicePrincipalId -or $diagnostics.ServicePrincipalId -eq '<REPLACE ME>' -or ![guid]::tryParse($diagnostics.ServicePrincipalId, [ref]$g)) {
                Write-Error "Diagnostics settings - ServicePrincipalId is invalid. Must be set and must be a guid "
                $valid = $false;
            }
            if (!$diagnostics.ServicePrincipalSecret -or $diagnostics.ServicePrincipalSecret -eq '<REPLACE ME>') {
                Write-Error "Diagnostics settings - ServicePrincipalSecret is invalid. Secret must be provided "
                $valid = $false;
            }
        }
    }
    return $valid;
}

<#
    .SYNOPSIS
    Gets the cloud configurations required for Azure CLI's "az cloud register" command.
 
    .PARAMETER ArmEndpoint
    The Azure Resource Management endpoint used for retrieving the environment's various endpoints.
 
    .PARAMETER OutputFolder
    The optional output folder path to output.
 
    .PARAMETER ApiVersion
    The API version of the ARM metadata endpoints to get.
 
    .OUTPUTS
    String. cloud configuration string to be used for the "az cloud register".
    If OutputFolder was passed, it also creates a "cloudconfig.json" file in that location.
 
    .EXAMPLE
    PS> Get-AzCliCloudConfig -ArmEndpoint "<ARM ENDPOINT>"
    {
        "suffixes": {
                        "keyvaultDns": "...",
                        "storageEndpoint": "...",
                        "acrLoginServerEndpoint": "..."
                    },
        "endpoints": {
                        "activeDirectory": "...",
                        "activeDirectoryGraphResourceId": "...",
                        "resourceManager": "...",
                        "microsoftGraphResourceId": "...",
                        "activeDirectoryResourceId": "..."
                    }
    }
#>

function Get-AzCliCloudConfig
{
    [CmdletBinding()]
    [OutputType([String])]
    param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [string]
        $ArmEndpoint,

        [Parameter(Position = 1, Mandatory = $false, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [string]
        $OutputFolder,

        [Parameter(Position = 2, Mandatory = $false, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [string]
        $ApiVersion = "2022-09-01"
    )
    $armMetadataUrl = "$($armEndpoint.TrimEnd('/'))/metadata/endpoints?api-version=${ApiVersion}"
    try
    {
        $response = Invoke-WebRequest $armMetadataUrl `
            -Method 'GET' `
            -ContentType "application/json" `
            -UseBasicParsing
    }
    catch
    {
        Write-Error "Failed to get ARM metadata endpoints at '$armMetadataUrl'."
        throw $_
    }

    $cloudEndpoints = $response.Content | ConvertFrom-Json
    $cloudConfig = @{
        endpoints = @{
            activeDirectory = "$($cloudEndpoints.authentication.loginEndpoint.TrimEnd('/'))/adfs"
            activeDirectoryGraphResourceId = $cloudEndpoints.graph
            activeDirectoryResourceId = $cloudEndpoints.authentication.audiences[0]
            resourceManager = $cloudEndpoints.resourceManager
            microsoftGraphResourceId = $cloudEndpoints.graph
        }
        suffixes = @{
            storageEndpoint = $cloudEndpoints.suffixes.storage
            keyvaultDns = $cloudEndpoints.suffixes.keyvaultDns
            acrLoginServerEndpoint = $cloudEndpoints.suffixes.acrLoginServer
        }
    }
    $cloudConfigJson = $cloudConfig | ConvertTo-Json
    if ($OutputFolder)
    {
        $cloudConfigJson | Set-Content -Path "$OutputFolder\cloudconfig.json"
    }
    return $cloudConfigJson
}


Test-PSEdition
Write-Host "Use Invoke-DownloadWinfieldAppliance to download Winfield and Install-WinfieldAppliance to install Winfield."

# Installation cmdlets
Export-ModuleMember Invoke-DownloadWinfieldAppliance
Export-ModuleMember Install-WinfieldAppliance
Export-ModuleMember Export-WinfieldRootCert
Export-ModuleMember Import-WinfieldRootCert
Export-ModuleMember Test-Winfield
Export-ModuleMember Test-WinfieldCheckSum
Export-ModuleMember Test-DownloadWinfield
# Added module members for operator experience
Export-ModuleMember Get-WinfieldApplianceSettings
Export-ModuleMember Set-WinfieldApplianceDesiredState
Export-ModuleMember Get-WinfieldHealthState
Export-ModuleMember New-WinfieldSettings
Export-ModuleMember Import-WinfieldSettingsFromFile
Export-ModuleMember Test-WinfieldSettings
Export-ModuleMember Get-WinfieldSettings
Export-ModuleMember Test-WinfieldSettings
Export-ModuleMember Get-ObservabilityConfiguration
Export-ModuleMember Set-ObservabilityConfiguration
Export-ModuleMember Set-DefaultObservabilityConfiguration
Export-ModuleMember Get-AzCliCloudConfig

Export-ModuleMember Copy-WinfieldDiagnosticData
Export-ModuleMember Get-ObservabilityStampId
Export-ModuleMember Send-WinfieldDiagnosticData

# SIG # Begin signature block
# MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCk0YikllHb3bgF
# J1v60wbxL7Ur0aTn5f7Mhr1lK59RFaCCDXYwggX0MIID3KADAgECAhMzAAADTrU8
# esGEb+srAAAAAANOMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI5WhcNMjQwMzE0MTg0MzI5WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDdCKiNI6IBFWuvJUmf6WdOJqZmIwYs5G7AJD5UbcL6tsC+EBPDbr36pFGo1bsU
# p53nRyFYnncoMg8FK0d8jLlw0lgexDDr7gicf2zOBFWqfv/nSLwzJFNP5W03DF/1
# 1oZ12rSFqGlm+O46cRjTDFBpMRCZZGddZlRBjivby0eI1VgTD1TvAdfBYQe82fhm
# WQkYR/lWmAK+vW/1+bO7jHaxXTNCxLIBW07F8PBjUcwFxxyfbe2mHB4h1L4U0Ofa
# +HX/aREQ7SqYZz59sXM2ySOfvYyIjnqSO80NGBaz5DvzIG88J0+BNhOu2jl6Dfcq
# jYQs1H/PMSQIK6E7lXDXSpXzAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUnMc7Zn/ukKBsBiWkwdNfsN5pdwAw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMDUxNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD21v9pHoLdBSNlFAjmk
# mx4XxOZAPsVxxXbDyQv1+kGDe9XpgBnT1lXnx7JDpFMKBwAyIwdInmvhK9pGBa31
# TyeL3p7R2s0L8SABPPRJHAEk4NHpBXxHjm4TKjezAbSqqbgsy10Y7KApy+9UrKa2
# kGmsuASsk95PVm5vem7OmTs42vm0BJUU+JPQLg8Y/sdj3TtSfLYYZAaJwTAIgi7d
# hzn5hatLo7Dhz+4T+MrFd+6LUa2U3zr97QwzDthx+RP9/RZnur4inzSQsG5DCVIM
# pA1l2NWEA3KAca0tI2l6hQNYsaKL1kefdfHCrPxEry8onJjyGGv9YKoLv6AOO7Oh
# JEmbQlz/xksYG2N/JSOJ+QqYpGTEuYFYVWain7He6jgb41JbpOGKDdE/b+V2q/gX
# UgFe2gdwTpCDsvh8SMRoq1/BNXcr7iTAU38Vgr83iVtPYmFhZOVM0ULp/kKTVoir
# IpP2KCxT4OekOctt8grYnhJ16QMjmMv5o53hjNFXOxigkQWYzUO+6w50g0FAeFa8
# 5ugCCB6lXEk21FFB1FdIHpjSQf+LP/W2OV/HfhC3uTPgKbRtXo83TZYEudooyZ/A
# Vu08sibZ3MkGOJORLERNwKm2G7oqdOv4Qj8Z0JrGgMzj46NFKAxkLSpE5oHQYP1H
# tPx1lPfD7iNSbJsP6LiUHXH1MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# 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
# /Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAANOtTx6wYRv6ysAAAAAA04wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIFvgMYjUA/oxfqt9mWN4I30e
# LkHWtCO2Zym8KVnsmBjVMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEANKqppAEg3oRr0QSmxex3anXUuQfBpn/NydHvFM/SA1+SJpAph2s8p0RA
# X6/1VsQOSwd7g+8gLetatSiSid6zn8K3ZF6y61pUgZk8V2UM++YrAA5XkhXleeWc
# +nQ7nojSIU/3jOeJTVMJFyHXcCutvO+g1YbB2t0o1857chSQmftLb5YL2g9ZvAai
# Zr8uLjiOebXgqZaeCSskepxpnaNdSpxE3G8+IYBJLv/UcbREXW0TZArYpeON2zjF
# iJQnMvE+YW7hEUbA1OyfajkB3oGiGbemGNtKSSAUOZg7lx4TPYyfg6u/sjcm1Yr+
# ljsHCGp5hFuUZmgFAqcpD4lQaJ47HaGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC
# F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq
# hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCBnSIx+qqz7BPSuM/1FllIFX2enjOiISXaV8jqRG1RzNwIGZNT7QQN0
# GBMyMDIzMDgxMjE2NDg0OC42NTNaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTYwMC0w
# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg
# ghHqMIIHIDCCBQigAwIBAgITMwAAAdj8SzOlHdiFFQABAAAB2DANBgkqhkiG9w0B
# AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzA1MjUxOTEy
# NDBaFw0yNDAyMDExOTEyNDBaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z
# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTYwMC0wNUUwLUQ5NDcxJTAjBgNV
# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQDNeOsp0fXgAz7GUF0N+/0EHcQFri6wliTbmQNmFm8D
# i0CeQ8n4bd2td5tbtzTsEk7dY2/nmWY9kqEvavbdYRbNc+Esv8Nfv6MMImH9tCr5
# Kxs254MQ0jmpRucrm3uHW421Cfva0hNQEKN1NS0rad1U/ZOme+V/QeSdWKofCThx
# f/fsTeR41WbqUNAJN/ml3sbOH8aLhXyTHG7sVt/WUSLpT0fLlNXYGRXzavJ1qUOe
# Pzyj86hiKyzQJLTjKr7GpTGFySiIcMW/nyK6NK7Rjfy1ofLdRvvtHIdJvpmPSze3
# CH/PYFU21TqhIhZ1+AS7RlDo18MSDGPHpTCWwo7lgtY1pY6RvPIguF3rbdtvhoyj
# n5mPbs5pgjGO83odBNP7IlKAj4BbHUXeHit3Da2g7A4jicKrLMjo6sGeetJoeKoo
# j5iNTXbDwLKM9HlUdXZSz62ftCZVuK9FBgkAO9MRN2pqBnptBGfllm+21FLk6E3v
# VXMGHB5eOgFfAy84XlIieycQArIDsEm92KHIFOGOgZlWxe69leXvMHjYJlpo2VVM
# tLwXLd3tjS/173ouGMRaiLInLm4oIgqDtjUIqvwYQUh3RN6wwdF75nOmrpr8wRw1
# n/BKWQ5mhQxaMBqqvkbuu1sLeSMPv2PMZIddXPbiOvAxadqPkBcMPUBmrySYoLTx
# wwIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFPbTj0x8PZBLYn0MZBI6nGh5qIlWMB8G
# A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG
# Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy
# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w
# XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy
# dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG
# A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD
# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQCunA6aSP48oJ1VD+SMF1/7SFiTGD6zyLC3
# Ju9HtLjqYYq1FJWUx10I5XqU0alcXTUFUoUIUPSvfeX/dX0MgofUG+cOXdokaHHS
# lo6PZIDXnUClpkRix9xCN37yFBpcwGLzEZlDKJb2gDq/FBGC8snTlBSEOBjV0eE8
# ICVUkOJzIAttExaeQWJ5SerUr63nq6X7PmQvk1OLFl3FJoW4+5zKqriY/PKGssOa
# A5ZjBZEyU+o7+P3icL/wZ0G3ymlT+Ea4h9f3q5aVdGVBdshYa/SehGmnUvGMA8j5
# Ct24inx+bVOuF/E/2LjIp+mEary5mOTrANVKLym2kW3eQxF/I9cj87xndiYH55Xf
# rWMk9bsRToxOpRb9EpbCB5cSyKNvxQ8D00qd2TndVEJFpgyBHQJS/XEK5poeJZ5q
# gmCFAj4VUPB/dPXHdTm1QXJI3cO7DRyPUZAYMwQ3KhPlM2hP2OfBJIr/VsDsh3sz
# LL2ZJuerjshhxYGVboMud9aNoRjlz1Mcn4iEota4tam24FxDyHrqFm6EUQu/pDYE
# DquuvQFGb5glIck4rKqBnRlrRoiRj0qdhO3nootVg/1SP0zTLC1RrxjuTEVe3PKr
# ETbtvcODoGh912Xrtf4wbMwpra8jYszzr3pf0905zzL8b8n8kuMBChBYfFds916K
# Tjc4TGNU9TCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI
# hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy
# MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg
# M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF
# dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6
# GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp
# Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu
# yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E
# XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0
# lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q
# GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ
# +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA
# PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw
# EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG
# NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV
# MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK
# BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG
# 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x
# M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC
# VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449
# xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM
# nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS
# PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d
# Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn
# GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs
# QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL
# jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL
# 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNN
# MIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjk2MDAtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQBI
# p++xUJ+f85VrnbzdkRMSpBmvL6CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6IIcszAiGA8yMDIzMDgxMjE0NTg1
# OVoYDzIwMjMwODEzMTQ1ODU5WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDoghyz
# AgEAMAcCAQACAg/YMAcCAQACAhLCMAoCBQDog24zAgEAMDYGCisGAQQBhFkKBAIx
# KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI
# hvcNAQELBQADggEBAFoe+kCiMHCxa+OcLGzebLQn/AM74r6mE2CaRsd6RRr2VaTF
# UhKZ0z6CyCPx1LLSpUqZMwzfCgNeDg1eUgi+GsSgnD3qLRIK0AzpnCOfB+PnSa+J
# oaO5pD1g2vZgrch+wSx2gW/J3wGtVgPxRa3sNx8E1vD0zosXgWN/zkfaEL0yq0lW
# gRFSb93Q5VrKSSXdOew3MpRg14dwDJeRFx5eHCtGDMK5mL0gDznQ351ufaJYo2Se
# A/koONIbhJg8Sbjas8xAh8UMhs5HMnOLeBl/1bLvo4Tj3kosgAho16KwPGLprCBN
# dL9pn/fguc/ap/VofV1f+IPpVQ7glpCzhX4YJCExggQNMIIECQIBATCBkzB8MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy
# b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAdj8SzOlHdiFFQABAAAB2DAN
# BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G
# CSqGSIb3DQEJBDEiBCA3y89o1k6XsJOIrAKhfdKSpcPsaJj7IC+w7VSz8YANkzCB
# +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIDrjIX/8CZN3RTABMNt5u73Mi3o3
# fmvq2j8Sik+2s75UMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh
# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw
# MTACEzMAAAHY/EszpR3YhRUAAQAAAdgwIgQgvOtgxDLd2V1H83Ves/5zK5EsZhKM
# FR80wPDMjduK8JcwDQYJKoZIhvcNAQELBQAEggIAmONNs0t3nuztq6wHaviiqZ3L
# hh8OR+2iat61SEaVTzECxH9pQ8yqkawIXxUDhNnqbaVyVinij/qt4gWgsNf+eu6h
# +alVIC2ajvuN4+nN0ud/xPBXv1XW0LnkpqGYhYG9VpzyStTJ8s1mt9PgAoAuq5HB
# /eJFTbVAFSZT5KzdUkCJTnmBOcBtR10fSlMi65ycEfaSmXhVohLbCcEhEF0nqwLw
# WSU0UvY0zKuqRVWyuzITUXX5IrGZsNT16MkVTUVEr5+IBn8untQYWkh5+x2NzzaP
# PLbHmVoTmp87bymIGNA2O+nYLzdu5XDzzDkVfE3oOipmo1fIIQqbR470FUzxNccB
# lilKUAe/rpepMITDUJ8QzA1ZsmWZrs7WCnYhhM1NIDmGOgerBkLb7X7KtDZeod7I
# PpnT0WeCR7F873NWHbj4z31sl67cKS9SweFcv0WO9Ja0e8aLzNaD2o8iPvQb88er
# +a3xAk1EXiusAYQ19LYZ1QvKk5vFg4WTX0w94eJPTuQvnABD5S3uXDW3JM1wtlxd
# iiDf4eOyRzIT+aGeBEKHHWrBbuP5WPlwTDHy6Il/a6nEWtZEWWUTeEVPu5D0ckxs
# muatGU8W0P9QoATL8HSLzxBB2RuGScYJlG4BCBBPb/CPJ0vY8Oz7f/iJLD0bca87
# JyzFjdZzXsMo4SZZC8Q=
# SIG # End signature block