Obs/bin/ObsDep/content/Powershell/Roles/Common/VhdWim.psm1

using module ..\Common\MountedImage.psm1
Import-Module -Name "$PSScriptRoot\..\..\Common\Helpers.psm1"
Import-Module -Name "$PSScriptRoot\..\..\Common\NetworkHelpers.psm1"
Import-Module -Name "$PSScriptRoot\..\..\Common\StorageHelpers.psm1"
Import-Module -Name "$PSScriptRoot\RoleHelpers.psm1"
$azureStackNugetPath = Get-ASArtifactPath -NugetName "Microsoft.AzureStack.Role.SBE"
Import-Module (Join-Path $azureStackNugetPath "content\CloudDeployment\Roles\OEM\OEM.psm1") -DisableNameChecking

function Get-MountedDisk
{
    Get-Disk | Where-Object Manufacturer -like 'Msft*' | Where-Object Model -like 'Virtual Disk*'
}

function Dismount-MountedDisk
{
    Get-MountedDisk | ForEach-Object { Dismount-DiskImage -DevicePath $_.Path }
}

function Get-MountedDiskDriveLetter
{
    Get-MountedDisk | Get-Partition | ForEach-Object DriveLetter
}

function New-VhdFromWim
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $VhdPath,

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

        [Parameter(Mandatory=$true)]
        [int]
        $WimImageIndex,

        [uint64]
        $SizeBytes = 40GB,

        [string[]]
        $DismFeaturesToInstall,

        [string[]]
        $ServerFeaturesToInstall,

        # Specifies if the VHD will be used for VHD boot rather than a primary boot device.
        # Do not set this if your VHD is going to be a primary boot disk on a VM.
        [switch]
        $VhdBoot,

        [switch]
        $Force
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    if (Test-Path $VhdPath) {
        # the other BVT disk (multi-node) is also a virtual disk but can not be dis-mounted (error will occur)
        # we have to target the only disk mapped to Vhd path
        Dismount-DiskImage -ImagePath $VhdPath
        if ($Force) {
            Remove-Item $VhdPath -Force
        }
        else {
            throw "VHD file $VhdPath already exists."
        }
    }

    # Try to start Hyper-V on the DVM
    $tryCount = 1
    while (($tryCount -lt 4) -and ((Get-Service vmms).Status -ne "Running")) {
        Trace-Execution "Attempt #$tryCount to start VMMS on '$Env:COMPUTERNAME'"
        Start-Service vmms
        Start-Sleep 30
        $tryCount++
    }
    if ((Get-Service vmms).Status -ne "Running") {
        Trace-Error "Failed to start VMMS on $Env:COMPUTERNAME"
    }

    $null = New-VHD -SizeBytes $SizeBytes -Path $VhdPath -Dynamic

    $hwDetectionService = Get-Service -Name ShellHWDetection -ErrorAction Ignore
    if ($hwDetectionService) {
        Trace-Execution "Stopping ShellHWDetection Service"
        Stop-Service ShellHWDetection
    }
    else {
        Trace-Execution "ShellHWDetection Service was not running, no need to stop the service."
    }

    Trace-Execution "Mounting VHD from: $VhdPath"
    Mount-DiskImage -ImagePath $VhdPath

    # Attempt to start defender if not already running
    $service = Get-Service -Name "windefend" -ErrorAction SilentlyContinue
    if ($null -ne $service -and $service.Status -ine "Running") {
        $service | Start-Service -ErrorAction SilentlyContinue | Out-Null
    }

    try
    {
        $virtualDisk = Get-Disk | Where-Object Manufacturer -like 'Msft*' | Where-Object Model -like 'Virtual Disk*' | Where-Object PartitionStyle -like 'RAW'
        $diskNumber = $virtualDisk.Number
        Trace-Execution "Performing initialize on disk number: $diskNumber"
        Initialize-Disk -Number $diskNumber -PartitionStyle MBR
        $partition = New-Partition -DiskNumber $diskNumber -UseMaximumSize -AssignDriveLetter -IsActive
        if ($null -eq $partition) {
            throw "Failed to create new partition during the process of creating WinPE VHD from WIM."
        }

        $driveLetter = $partition.DriveLetter
        $driveName = $driveLetter + ':'
        $diskPath = $driveName + '\'
        $volume = Format-Volume -Partition $partition -FileSystem NTFS -Confirm:$false
        $null = New-PSDrive -PSProvider FileSystem -Root "FileSystem::$diskPath" -Name $driveLetter

        # set exclusion path when windefend is running
        if ($null -ne $service -and $service.Status -eq "Running") {
            Trace-Execution "Exclude mount folders $diskPath from Windows Defender scan to speed up configuration."
            Add-MpPreference -ExclusionPath $diskPath -ErrorAction SilentlyContinue
        }

        # Use a randomly generated log path because Expand-WindowsImage sometimes crashes because of not being able to write to default log file DISM.LOG.
        $dismLogPath = [IO.Path]::GetTempFileName()
        Trace-Execution "Using the following path for the DISM log: $dismLogPath"
        $null = Expand-WindowsImage -ImagePath $WimPath -ApplyPath $diskPath -Index $WimImageIndex -LogPath $dismLogPath
        if (-not $VhdBoot) {
            $null = bcdboot "$driveName\Windows" /S $driveName
        }
    }
    finally
    {
        if ($null -ne $service -and $service.Status -eq "Running") {
            Trace-Execution "Remove Windows Defender exclusions for paths $diskPath"
            Remove-MpPreference -ExclusionPath $diskPath -ErrorAction SilentlyContinue
        }

        Get-PSDrive | Where-Object Name -eq $driveLetter | Remove-PSDrive
        Dismount-DiskImage -ImagePath $VhdPath

        if ($hwDetectionService) {
            Start-Service ShellHWDetection
            Trace-Execution "Started ShellHWDetection Service"
        }
        else {
            Trace-Execution "ShellHWDetection Service was not previously running, no need to restart the service."
        }
    }

    if ($DismFeaturesToInstall) {
        Enable-WindowsOptionalFeature -FeatureName $DismFeaturesToInstall -Online -All
    }

    if ($ServerFeaturesToInstall) {
        Install-WindowsFeature -Vhd $VhdPath -Name $ServerFeaturesToInstall -IncludeAllSubFeature -IncludeManagementTools
    }
}

function New-WimFromVhd
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $WimPath,

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

        [switch]
        $Force
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    Dismount-MountedDisk

    if (Test-Path $WimPath) {
        if ($Force) {
            Remove-Item $WimPath -Force
        }
        else {
            throw "WIM file $WimPath already exists."
        }
    }

    try
    {
        Mount-DiskImage -ImagePath $VhdPath
        $driveLetter = Get-MountedDiskDriveLetter
        $null = New-WindowsImage -ImagePath $WimPath -CapturePath "$driveLetter`:" -Name "CPS Host Backup"
    }
    finally
    {
        Dismount-DiskImage -ImagePath $VhdPath
    }
}

<#
$newVhdFromWimParameters = @{
    VhdPath = 'C:\CloudDeployment\Images\AD-DNS-DHCP.vhdx'
    WimPath = 'C:\CloudDeployment\Images\install.wim'
    WimImageIndex = 4 # Full server
    ServerFeaturesToInstall = 'DHCP', 'DNS', 'AD-Domain-Services'
}
New-VhdFromWim @newVhdFromWimParameters -Force
 
$newVhdFromWimParameters = @{
    VhdPath = 'C:\CloudDeployment\Images\Host.vhdx'
    WimPath = 'C:\CloudDeployment\Images\install.wim'
    WimImageIndex = 4 # Full server
    SizeBytes = 100GB
}
New-VhdFromWim @newVhdFromWimParameters -Force
#>

#Enable-WindowsOptionalFeature -FeatureName Microsoft-Hyper-V-Offline, Microsoft-Hyper-V-Online -Online -all

# Returns back username and password as array count 0 and 1
function Get-UsernameAndPassword
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [PSCredential]
        $Credential
    )

    $networkCredential = $Credential.GetNetworkCredential()
    $username = $networkCredential.UserName
    # Replace any password single quotes with double quotes so that we dont break the powershell syntax
    $password = $networkCredential.Password.Replace("'","''")

    if (-not $networkCredential.Domain) {
        # Localhost is needed to create networkshares from winPE
        $username = 'Localhost\' + $networkCredential.UserName
    }
    elseif (-not $username.Contains('\')) {
        $username = $networkCredential.Domain + '\' + $networkCredential.UserName
    }

    $username
    $password
}

<#
.Synopsis
     Function to output a path to a deploydirect file that is intended to run from a WinPE context on a booting machine. This must only run from the physical machine role.
.Parameter DeployDirectPath
     The template deploydirect path.
.Parameter UsePxeServerRole
    Whether the deployment script should attempt to communicate with the on-stamp PXE server or with the initial deployment DVM.
.Parameter Parameters
    This object is based on the customer manifest. It contains the private information of the Key Vault role, as well as
    public information of all other roles. It is passed down by the deployment engine.
.Example
    New-DeployDirect -DeployDirectPath "$PSScriptRoot\DeployDirect.ps1" -UsePxeServerRole $false -Parameters $Parameters
 
    This will customize the template deploy direct file using entries from the parameters object, and will use the IP of the DVM in various locations.
#>

function New-DeployDirect
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String]
        $DeployDirectPath,

        [Parameter(Mandatory=$true)]
        [bool]
        $UsePxeServerRole,

        [Parameter(Mandatory=$false)]
        [PSCredential]
        $PXEAccountCredential,

        [Parameter(Mandatory=$false)]
        [PSCredential]
        $VHDShareAccountCredential,

        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters,

        [Parameter(Mandatory=$false)]
        [bool]
        $UseVersionedWim = $true,

        [Parameter(Mandatory=$true)]
        [string]
        $HypervisorSchedulerType

    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    $cloudRole = $Parameters.Roles["Cloud"].PublicConfiguration
    $physicalMachinesRole = $Parameters.Roles["BareMetal"].PublicConfiguration
    $physicalMachinesRolePrivateInfo = $Parameters.Configuration.Role.PrivateInfo
    $securityInfo = $cloudRole.PublicInfo.SecurityInfo

    #
    # $PXEAccountCredential is the account used to write logs back to the PXE server or the DVM. It can be either specified by the caller of
    # this function, or (by default) the local administrator of the DVM.
    #
    # $VHDShareAccountCredential is the account used by WinPE to download the VHD to the local host. It can be either the local administrator of
    # the DVM, or an account specified by the caller of this function (mandatory when an on-stamp PXE server is used)
    #
    if ($UsePxeServerRole) {
        # For domain-joined PXE servers, using the local administrator is not an option, so credentials for both accounts must be specified
        if ($null -eq $PXEAccountCredential) {
            throw "Must specify PXEAccountCredential when using an on-stamp PXE server"
        }
        if ($null -eq $VHDShareAccountCredential) {
            throw "Must specify VHDShareAccountCredential when using an on-stamp PXE server"
        }
    }
    else {
        # For the DVM, we can use the local builtin administrator account, so explicit credentials are not needed
        $administratorUser = $securityInfo.LocalUsers.User | Where-Object Role -eq $physicalMachinesRolePrivateInfo.Accounts.BuiltInAdminAccountID
        if ($PXEAccountCredential -eq $null) {
            $PXEAccountCredential = $Parameters.GetCredential($administratorUser.Credential)
        }
        if ($VHDShareAccountCredential -eq $null) {
            $VHDShareAccountCredential = $Parameters.GetCredential($administratorUser.Credential)
        }
    }

    if ($UsePxeServerRole) {
        $pxeRole = $Parameters.Roles["PXE"].PublicConfiguration

        $pxeServerName = $pxeRole.Nodes.Node.Name

        $vmRoleDefinition = $Parameters.Roles["VirtualMachines"].PublicConfiguration
        $pxeNode = $vmRoleDefinition.Nodes.Node | Where-Object { $_.Name -eq $pxeServerName }

        $pxeIPAddress = @($pxeNode.NICs.NIC)[0].IPv4Address.Split("/")[0]
    }
    else {
        $deploymentMachineRole = $Parameters.Roles["DeploymentMachine"].PublicConfiguration
        $pxeIPAddress = $deploymentMachineRole.Nodes.Node.IPv4Address
        $pxeServerName = $Env:COMPUTERNAME
    }

    if ($UsePxeServerRole) {
        # Take image content from the PXE server
        $hostImageDir = "\\$pxeServerName\Images"
    }
    else {
        # Install Image is generated.
        $hostImageDir = $ExecutionContext.InvokeCommand.ExpandString($physicalMachinesRole.PublicInfo.VhdImageTargetDir.Path)
        if ($UseVersionedWim) {
            $hostImageDir = Get-AzSVersionedPath -Path $hostImageDir
        }

        if ($hostImageDir -like "*{*}*") {
            $clusterName = Get-ManagementClusterName $Parameters
            $hostImageDir = Get-SharePath $Parameters $hostImageDir $clusterName
        }
    }
    Trace-Execution "Using host image folder $hostImageDir for DeployDirect script"

    if ([Environment]::GetEnvironmentVariable("OSImageType") -eq "ISO") {
        Trace-Execution "OSImageType is ISO"
        $hostImagePath = $hostImageDir + '\' + $physicalMachinesRole.PublicInfo.InstallImage.ISOName
        Trace-Execution "Using host ISO $hostImagePath for DeployDirect script"

        # Copy SetupComplete.cmd script needed for ISO-based OS deployments to RemoteUnattend folder
        $isoSetupCompleteScript = "C:\CloudDeployment\Roles\Common\SetupComplete_ISO.cmd"
        if (Test-Path -Path $isoSetupCompleteScript) {
            Trace-Execution "Copy '$($isoSetupCompleteScript)' to RemoteUnattend folder"
            $dest = "C:\RemoteUnattend\SetupComplete.cmd"
            if (-not(Test-Path -Path (Split-Path -Path $dest -Parent))) {
                $null = New-Item -ItemType File -Path $dest -Force
            }
            Copy-Item -Path $isoSetupCompleteScript -Destination $dest -Force
            $version = ([string]($Parameters.Roles["Cloud"].PublicConfiguration.PublicInfo.Version)).Trim()
            # Add line to SetupComplete.cmd to create file on the host when deployment has completed
            Add-Content -Path $dest -Value "echo $version > C:\CloudBuilderTemp\$($physicalMachinesRole.PublicInfo.DeploymentGuid).txt"
        }
        else {
            Trace-Execution "Could not find 'C:\CloudDeployment\Roles\Common\SetupComplete_ISO.cmd'"
        }
    }
    else {
        Trace-Execution "OSImageType is VHD"
        if ($physicalMachinesRole.PublicInfo.InstallImage.Index) {
            $hostImagePath = $hostImageDir + '\' + $physicalMachinesRole.PublicInfo.InstallImage.VHDName
        }
        else {
            # Use the initial Image.
            $hostImagePath = $ExecutionContext.InvokeCommand.ExpandString($physicalMachinesRole.PublicInfo.InstallImage.Path)
        }
        Trace-Execution "Using host VHD $hostImagePath for DeployDirect script"
    }

    # This VHD should be unused
    $winPEVHDImagePath = $hostImageDir + '\' + $physicalMachinesRole.PublicInfo.BootImage.VHDName

    $deployScriptContent = Get-Content $DeployDirectPath

    $deployScriptContent = $deployScriptContent.Replace('[DeploymentGuid]', $physicalMachinesRole.PublicInfo.DeploymentGuid)
    $deployScriptContent = $deployScriptContent.Replace('[DVMIPAddress]', $pxeIPAddress)
    $deployScriptContent = $deployScriptContent.Replace('[DVMName]', $pxeServerName)
    $deployScriptContent = $deployScriptContent.Replace('[HostImagePath]',$hostImagePath)
    $deployScriptContent = $deployScriptContent.Replace('[WinPEVHDImagePath]', $winPEVHDImagePath)

    if ($UsePxeServerRole) {
        # On the PXE server C:\RemoteUnattend must be accessed as \\PXE\RemoteUnattend
        $deployScriptContent = $deployScriptContent.Replace('[RemoteUnattendShare]', "RemoteUnattend")
    }
    else {
        # Local administrator credentials are used on the DVM, so the C$ share is available
        $deployScriptContent = $deployScriptContent.Replace('[RemoteUnattendShare]', "C$\RemoteUnattend")
    }

    if (-not $hostImagePath.StartsWith('\\')) {
        # Used to create a host entry in WinPE.
        $deployScriptContent = $deployScriptContent.Replace('[HostShareIPAddress]', $pxeIPAddress)
        $deployScriptContent = $deployScriptContent.Replace('[HostShareName]', $pxeServerName)
    }
    else {
        # Get the IP and Hostname from the share.
        $shareComputerName = $hostImagePath.Substring(2).Split('\')[0]
        $shareIPv4 = Resolve-DnsName $shareComputerName | Where-Object QueryType -eq 'A'
        $ip = $null

        foreach ($shareIp in $shareIPv4.IPAddress) {
            $succcess = Test-Connection $shareIp -ErrorAction SilentlyContinue -ErrorVariable connectionError

            if ($succcess) {
                $ip = $shareIp
                break
            }
            else {
                Trace-Warning "Share endpoint failed with: $connectionError"
            }
        }

        Trace-Execution "Resolved: '$shareComputerName' to IP: '$ip'"

        # Used to create a host entry in WinPE
        $deployScriptContent = $deployScriptContent.Replace('[HostShareIPAddress]', $ip)
        $deployScriptContent = $deployScriptContent.Replace('[HostShareName]',$shareComputerName)
    }

    $user = Get-UsernameAndPassword -Credential $PXEAccountCredential

    Trace-Execution "Using the account '$($user[0])' for DVM share."
    $deployScriptContent = $deployScriptContent.Replace('[DVMUser]', $user[0])
    $deployScriptContent = $deployScriptContent.Replace('[DVMPassword]', $user[1])
    $deployScriptContent = $deployScriptContent.Replace('[HypervisorSchedulerType]', $HypervisorSchedulerType)

    if ($VHDShareAccountCredential) {
        $user = Get-UsernameAndPassword -Credential $vhdShareAccountCredential

        Trace-Execution "Using the account '$($user[0])' for host VHD share."
        $deployScriptContent = $deployScriptContent.Replace('[VHDSharePathUser]', $user[0])
        $deployScriptContent = $deployScriptContent.Replace('[VHDSharePathPassword]',$user[1])
    }
    else {
        Trace-Execution "Not setting host VHD share credentials."
    }

    $networkDefinition = Get-NetworkDefinitions -Parameters $Parameters
    $networks = $networkDefinition.Networks.Network
    $hostNicNetworkName = Get-NetworkNameForCluster -ClusterName "s-cluster" -NetworkName "HostNIC"

    foreach ($network in $networks) {
        if ($network.Name -eq $hostNicNetworkName) {
            $deployScriptContent = $deployScriptContent.Replace('[HostNetworkBeginAddress]', $network.IPv4.StartAddress)
            $deployScriptContent = $deployScriptContent.Replace('[HostNetworkEndAddress]', $network.IPv4.EndAddress)
        }
    }
    $deployScriptContent = $deployScriptContent.Replace('[RebootOnComplete]', '$true')

    $oneNodeRestore = [Environment]::GetEnvironmentVariable("OneNodeRestore", "Machine")
    Trace-Execution "OneNodeRestore: $($oneNodeRestore)"

    if (($UsePxeServerRole) -or (-not [System.String]::IsNullOrEmpty($oneNodeRestore)))
    {
        Trace-Execution "Setting ClearExistingStorage to false."
        $deployScriptContent = $deployScriptContent.Replace('[ClearExistingStorage]', '$false')
    }
    else
    {
        Trace-Execution "Setting ClearExistingStorage to true."
        $deployScriptContent = $deployScriptContent.Replace('[ClearExistingStorage]', '$true')
    }

    $isAnacortesBuild = [System.Environment]::GetEnvironmentVariable('ANACORTES_DEPLOYMENT', [System.EnvironmentVariableTarget]::Machine)
    if(((Get-Random -Maximum 100)% 2 -eq 0) -and ([System.String]::IsNullOrWhiteSpace($isAnacortesBuild)))
    {
        $deployScriptContent = $deployScriptContent.Replace('[IsOneDrive]', '$true')
    }
    else {
        $deployScriptContent = $deployScriptContent.Replace('[IsOneDrive]', '$false')
    }

    $newDeployDirectPath = "$PSScriptRoot\DeployDirectReplaced.ps1"
    Set-Content -Value $deployScriptContent -Path $newDeployDirectPath -Force

    return $newDeployDirectPath
}

function Test-SkipDriverInjection
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [bool]
        $SkipDriverInjection
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    $driverPath = "$env:SystemDrive\CloudDeployment\Drivers"

    if ($skipDriverInjection) {
        Trace-Warning 'Driver injection will be skipped as instructed by $CloudBuilder.SkipDriverInjection setting.'
    }
    elseif (-not (Test-Path $driverPath)) {
        Trace-Execution "Driver injection will be skipped as the driver path '$driverPath' does not exist."
        $skipDriverInjection = $true
    }
    elseif (-not (Get-ChildItem $driverPath)) {
        Trace-Execution "Driver injection will be skipped as the driver path '$driverPath' does not contain any files."
        $skipDriverInjection = $true
    }
    elseif (-not (Get-ChildItem $driverPath -Filter *.inf -Recurse)) {
        Trace-Execution "Driver injection will be skipped as the driver path '$driverPath' does not contain any drivers (no *.inf files found)."
        $skipDriverInjection = $true
    }

    return $skipDriverInjection
}

function New-PxeUnattendFile
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String]
        $ComputerName,

        [Parameter(Mandatory=$false)]
        [String]
        $SLPKey,

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

        [Parameter(Mandatory=$true)]
        [PSCredential]
        $LocalAdministratorCredential,

        [Parameter(Mandatory=$false)]
        [String]
        $DomainJoinBlob,

        [Parameter(Mandatory=$false)]
        [String]
        $DomainFqdn,

        [Parameter(Mandatory=$false)]
        [PSCredential]
        $DomainAdminCredential,

        [Parameter(Mandatory=$false)]
        [PSCredential]
        $LocalAccountCredential,

        [Parameter(Mandatory=$false)]
        [PSCredential]
        $LocalCloudAdminCredential,

        [Parameter(Mandatory=$true)]
        [String]
        $UnattendFilePath,

        [Parameter(Mandatory=$true)]
        [String]
        $OutputPath
    )

    $outputGuid = [Guid]::NewGuid();
    if (-not [Guid]::TryParse($MachineIdentifier, [ref] $outputGuid)) {
        # $OFS which is a special variable in powershell . OFS stands for Output field separator .
        $ofs = '-'
        $MacAddressString = "$(([System.Net.NetworkInformation.PhysicalAddress]$MachineIdentifier).GetAddressBytes() | ForEach-Object {'{0:X2}' -f $_})"
        $MachineIdentifier = $MacAddressString
    }

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    $unattendContent = (Get-Content $UnattendFilePath)

    $unattendContent = $unattendContent.Replace('[ComputerName]', $ComputerName)

    # Commented out code is not needed for AsZ reset
    # Clean up later to have AsZ specific method

    #if ($SLPKey) {
    # $unattendContent = $unattendContent.Replace('[SLPKey]', $SLPKey)
    #}
    #else {
     # $unattendContent = $unattendContent -replace '<ProductKey>.*</ProductKey>', ''
    #}

    $unattendContent = $unattendContent.Replace('[LocalAdministratorPassword]', [System.Security.SecurityElement]::Escape(([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($LocalAdministratorCredential.Password)))))
    #$unattendContent = $unattendContent.Replace('[LocalAdministratorName]', $LocalAdministratorCredential.UserName)

    # Multi-node tokens
    #$unattendContent = $unattendContent.Replace('[DomainJoinBlob]', $DomainJoinBlob)
    #$unattendContent = $unattendContent.Replace('[DomainFqdn]', $DomainFqdn)

    #if ($DomainAdminCredential) {
    # $unattendContent = $unattendContent.Replace('[DomainAdminUsername]', $DomainAdminCredential.UserName)
    # $unattendContent = $unattendContent.Replace('[DomainAdminPassword]', [System.Security.SecurityElement]::Escape(([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($DomainAdminCredential.Password)))))
    #}

    #if ($LocalAccountCredential) {
    # $unattendContent = $unattendContent.Replace('[LocalAccountUsername]', $LocalAccountCredential.UserName)
    # $unattendContent = $unattendContent.Replace('[LocalAccountPassword]', [System.Security.SecurityElement]::Escape(([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($LocalAccountCredential.Password)))))
    #}

    #if ($LocalCloudAdminCredential) {
    # $unattendContent = $unattendContent.Replace('[LocalCloudAdminUsername]', $LocalCloudAdminCredential.UserName)
    # $unattendContent = $unattendContent.Replace('[LocalCloudAdminPassword]', [System.Security.SecurityElement]::Escape(([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($LocalCloudAdminCredential.Password)))))
    #}

    $unattendFilePath = "$OutputPath\$MachineIdentifier.xml"
    if (Test-Path $unattendFilePath) {
        Remove-Item $unattendFilePath -Force
    }

    Set-Content -Value $unattendContent -Path "$OutputPath\$MachineIdentifier.xml" -Force
}

function New-WinPEImage
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String]
        $DeployDirectPath,

        [Parameter(Mandatory=$false)]
        [bool] $UseVersionedWim = $true,

        [Parameter(Mandatory=$false)]
        [PSCredential]
        $PXEAccountCredential,

        [Parameter(Mandatory=$false)]
        [PSCredential]
        $VHDShareAccountCredential,

        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters,

        [Switch]$PostDeployment
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    $physicalMachinesRole = $Parameters.Roles["BareMetal"].PublicConfiguration
    $physicalMachinesRolePrivateInfo = $Parameters.Configuration.Role.PrivateInfo
    $cloudRole = $Parameters.Roles["Cloud"].PublicConfiguration

    $OEMRole = $Parameters.Roles["OEM"].PublicConfiguration
    $OEMModel = $OEMRole.PublicInfo.UpdatePackageManifest.UpdateInfo.Model

    $clusterName = Get-ManagementClusterName $Parameters

    $winPEBootImage = $physicalMachinesRole.PublicInfo.BootImage
    if ($PostDeployment.IsPresent) {
        $UsePxeServerRole = $true
        $bootImageSrcDir = $physicalMachinesRolePrivateInfo.LibraryShareImageFolder.Path
    }
    else {
        $UsePxeServerRole = $false
        $bootImageSrcDir = $ExecutionContext.InvokeCommand.ExpandString($winPEBootImage.SrcDir)
    }

    if ($bootImageSrcDir -like "*{*}*") {
        $bootImageSrcDir = Get-SharePath $Parameters $bootImageSrcDir $clusterName
    }

    # Use versioned path if specified
    if ($UseVersionedWim) {
        $bootImageSrcDir = Get-AzSVersionedPath -Path $bootImageSrcDir
    }

    $bootImagePath = Join-Path $bootImageSrcDir $winPEBootImage.BaseImage
    $bootImageIndex = $winPEBootImage.Index

    $vhdTargetDirPath = $ExecutionContext.InvokeCommand.ExpandString($physicalMachinesRole.PublicInfo.VhdImageTargetDir.Path)
    if ($vhdTargetDirPath -like "*{*}*") {
        $vhdTargetDirPath = Get-SharePath $Parameters $vhdTargetDirPath $clusterName
    }

    # Use versioned path if specified
    if ($UseVersionedWim) {
        $vhdTargetDirPath = Get-AzSVersionedPath -Path $vhdTargetDirPath
    }

    $winpeVhdImagePath = Join-Path $vhdTargetDirPath -ChildPath $winPEBootImage.VHDName
    $mountPath = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ([Guid]::NewGuid())

    if ($PostDeployment.IsPresent) {
        $wimStagingPath = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ([Guid]::NewGuid())
        $GenerateWim = $true
        $GenerateVhd = $false
    }
    else {
        $wimStagingPath = $bootImageSrcDir
        $skipCopy = $true
        $Script:IsIdempotentRun = [Bool]::Parse($physicalMachinesRolePrivateInfo.ExecutionContext.IdempotentRun)
        $GenerateWim = ((-not $Script:IsIdempotentRun) -or (-not (Test-Path $winpeVhdImagePath)))
        $GenerateVhd = $GenerateWim
    }

    if ($GenerateWim) {
        # Attempt to start defender if not already running
        $service = Get-Service -Name "windefend" -ErrorAction SilentlyContinue
        if ($null -ne $service -and $service.Status -ine "Running") {
            $service | Start-Service -ErrorAction SilentlyContinue | Out-Null
        }

        # On virtual environments to avoid infra VM reboots change hypervisor scheduler type to Classic.
        # On physical enviornments the scheduler type will be Core
        if (IsVirtualAzureStack($Parameters)) {
            $hypervisorSchedulerType = "Classic"
        }
        else {
            $hypervisorSchedulerType = "Core"
        }

        try
        {
            $newDeployDirectPath = New-DeployDirect `
                                        -DeployDirectPath $deployDirectPath `
                                        -PXEAccountCredential $PXEAccountCredential `
                                        -VHDShareAccountCredential $VHDShareAccountCredential `
                                        -UsePxeServerRole $UsePxeServerRole `
                                        -Parameters $Parameters `
                                        -UseVersionedWim $UseVersionedWim `
                                        -HypervisorSchedulerType $hypervisorSchedulerType
            Dismount-AllMountedImages

            if (-not (Test-Path $mountPath)) {
                $null =  New-Item -Path $mountPath -ItemType Directory
            }
            if (-not (Test-Path $wimStagingPath)) {
                $null =  New-Item -Path $wimStagingPath -ItemType Directory
            }

            # set exclusion path when windefend is running
            if ($null -ne $service -and $service.Status -eq "Running") {
                Trace-Execution "Exclude mount folders $mountPath and $wimStagingPath from Windows Defender scan to speed up configuration."
                Add-MpPreference -ExclusionPath $mountPath, $wimStagingPath -ErrorAction SilentlyContinue
            }

            Trace-Execution "Copying required content from '$bootImageSrcDir' to '$wimStagingPath'."
            if (-not $skipCopy) {
                $out = Robocopy.exe $bootImageSrcDir $wimStagingPath $physicalMachinesRole.PublicInfo.BootImage.BaseImage /R:2 /W:10
                # Check for exit code. If exit code is greater than 7, it means an error occured while peforming a copy operation.
                if ($LASTEXITCODE -ge 8) {
                    $message = "There were errors copying files from '$bootImageSrcDir' to '$wimStagingPath'. Robocopy exit code $LASTEXITCODE.`n"
                    $message += ($out | Out-String)
                    throw $message
                }
            }
            $wimStagingFile = Join-Path $wimStagingPath $physicalMachinesRole.PublicInfo.BootImage.BaseImage
            Trace-Execution "Mount '$wimStagingFile' to '$mountPath'."

            Mount-WindowsImageWithRetry -ImagePath $wimStagingFile -Path $mountPath -Index $bootImageIndex
            ######### This section is associated with Work Item 15253266 and should be removed after the data corruption bug is fixed. #########
            Trace-Execution "Mount '$wimStagingFile' successful."
            $registryPath = Join-Path $mountPath "\Windows\System32\config\SYSTEM"
            reg load HKLM\OFFLINE $registryPath
            reg add "HKLM\OFFLINE\ControlSet001\Control\Session Manager\Memory Management" /v VerifyDrivers /t REG_SZ /d * /f
            reg add "HKLM\OFFLINE\ControlSet001\Control\Session Manager\Memory Management" /v VerifyDriverLevel /t REG_DWORD /d 0x89 /f
            reg unload HKLM\OFFLINE
            Trace-Execution "Temporary registry setting complete."
            #########

            Copy-Item "$newDeployDirectPath" "$mountPath\DeployDirect.ps1" -Recurse -Force
            Copy-Item "$PSScriptRoot\..\Common\DeployDirectCommon.psm1" "$mountPath\DeployDirectCommon.psm1" -Recurse -Force
            Copy-Item "$PSScriptRoot\..\Common\startnet.cmd" "$mountPath\Windows\System32\startnet.cmd" -Recurse -Force
            Copy-Item "$PSScriptRoot\..\..\Common\Helpers.psm1" "$mountPath\Helpers.psm1" -Force
            Copy-Item "$PSScriptRoot\..\..\Common\Tracer.psm1" "$mountPath\Tracer.psm1" -Force

            Trace-Execution "Dismount and save mounted image."
            Dismount-WindowsImage -Path $mountPath -Save -Verbose:$false

            if (-not $skipCopy) {
                Trace-Execution "Copying updated boot image '$wimStagingFile' back to '$bootImageSrcDir'."
                $out = Robocopy.exe $wimStagingPath $bootImageSrcDir $physicalMachinesRole.PublicInfo.BootImage.BaseImage /R:2 /W:10
                # Check for exit code. If exit code is greater than 7, it means an error occured while performing a copy operation.
                if ($LASTEXITCODE -ge 8) {
                    $message = "There were errors copying files from '$wimStagingPath' to '$bootImageSrcDir'. Robocopy exit code $LASTEXITCODE.`n"
                    $message += ($out | Out-String)
                    throw $message
                }
                Remove-Item -Path $wimStagingFile -Force -Confirm:$false -ErrorAction Ignore
            }
        }
        finally
        {
            Dismount-AllMountedImages

            if ($null -ne $service -and $service.Status -eq "Running") {
                Trace-Execution "Remove Windows Defender exclusions for paths $MountPath and $wimStagingPath"
                Remove-MpPreference -ExclusionPath $MountPath, $wimStagingPath -ErrorAction SilentlyContinue
            }
        }

        if ($GenerateVhd) {
            Trace-Execution "Converting WIM image '$bootImagePath' to VHDBoot image '$winpeVhdImagePath'."

            $newVhdFromWimParameters = @{
                WimPath = $BootImagePath
                VhdPath = $winpeVhdImagePath
                WimImageIndex = $BootImageIndex
                SizeBytes = 120GB
                VhdBoot = $true
            }

            New-VhdFromWim @newVhdFromWimParameters -Force -Verbose
        }
    }
}

<#
Mounts the Wim file and injects desired content into it
#>

function Update-BootWimFile
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters,

        [Parameter(Mandatory=$false)]
        [bool] $UseVersionedWim = $true
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    $cloudRole = $Parameters.Roles["Cloud"].PublicConfiguration

    Trace-Execution "Update boot.wim file"
    $physicalMachinesRole = $Parameters.Roles["BareMetal"].PublicConfiguration
    $virtualMachinesRole = $Parameters.Roles["VirtualMachines"].PublicConfiguration
    $clusterName = Get-ManagementClusterName $Parameters
    # Windows Update staging folder
    $windowsUpdateStagingFolder = $physicalMachinesRole.PublicInfo.WindowsUpdateStagingFolder.Path
    if ($windowsUpdateStagingFolder -like "*{*}*") {
        $windowsUpdateStagingFolder = Get-SharePath $Parameters $windowsUpdateStagingFolder $clusterName
    }
    else {
        $windowsUpdateStagingFolder = $ExecutionContext.InvokeCommand.ExpandString($windowsUpdateStagingFolder)
    }

    # Boot image
    $winPEBootImage = $physicalMachinesRole.PublicInfo.BootImage
    $bootImageSrcDir = $ExecutionContext.InvokeCommand.ExpandString($winPEBootImage.SrcDir)
    if ($bootImageSrcDir -like "*{*}*") {
        $bootImageSrcDir = Get-SharePath $Parameters $bootImageSrcDir $clusterName
    }

    if ($UseVersionedWim) {
        $bootImagePath = Join-Path -Path (Get-AzSVersionedPath -Path $bootImageSrcDir) -ChildPath $winPEBootImage.BaseImage
    }
    else {
        $bootImagePath = Join-Path $bootImageSrcDir $winPEBootImage.BaseImage
    }

    $bootImageIndex = $winPEBootImage.Index

    if (Test-Path -Path $bootImagePath) {
        # Get rid of any half-built previous WIM.
        Clear-ImageMount -Path $bootImagePath -AlternateFileServerName $clusterName
    }

    # Mount the WIM file
    $mountedImage = [MountedImage]::new($bootImagePath, $bootImageIndex)

    # Get the image build and associated drivers path
    $windowsBuild = $mountedImage.GetBuild()
    $driverPath = Get-OEMDriverPath -Parameter $Parameters -Build $windowsBuild

    # Update the WIM file
    try
    {
        # Whenever Build runs on a seed ring node, there is very limited disk space.
        # Use a scratch drive for applying KBs.
        [string[]] $seedRingNodeNames = $Parameters.Roles["SeedRing"].PublicConfiguration.Nodes.Node | ForEach-Object Name
        if ($seedRingNodeNames -icontains $env:COMPUTERNAME) {
            Import-Module -Name "$PSScriptRoot\..\..\Roles\Common\Servicing\Scripts\Modules\HostTempDrive.psm1"
            $physicalHostName = Get-NodeRefNodeId $Parameters -RoleName "VirtualMachines" -NodeName $env:COMPUTERNAME
            $drive = New-ScratchDiskForNonHAVM -ComputerName $env:COMPUTERNAME -HyperVHosts @($physicalHostName)
            try
            {
                Trace-Execution "Running UpdateImageWithWindowsUpdates on a seed ring, using update scratch directory."
                # Inject Windows updates
                # TEMP: Ignore applicability check for packages when adding them to WinPE image.
                $mountedImage.UpdateImageWithWindowsUpdates($windowsUpdateStagingFolder, "$drive`:\UpdateTemp", $true)
            }
            finally
            {
                Remove-ScratchDiskForNonHAVM -ComputerName $env:COMPUTERNAME -HyperVHosts @($physicalHostName)
            }
        }
        else {
            # Inject Windows updates
            Trace-Execution "Injecting updates from non-seedring orchestrator."
            $mountedImage.UpdateImageWithWindowsUpdates($windowsUpdateStagingFolder)
        }

        # Inject drivers
        $skipDriverInjection = [Bool]::Parse($physicalMachinesRole.PublicInfo.SkipDriverInjection)
        $mountedImage.AddDriversToImage($driverPath, $skipDriverInjection)

        # Inject deployment content
        if ($Parameters.Configuration.Role.PrivateInfo.DeploymentContent) {
            $libraryShareNugetStorePath = Get-SharePath $Parameters $virtualMachinesRole.PublicInfo.LibraryShareNugetStoreFolder.Path $clusterName
            if (Test-Path -Path $libraryShareNugetStorePath) {
                $mountedImage.ExpandDeploymentArtifacts($Parameters.Configuration.Role.PrivateInfo.DeploymentContent, $libraryShareNugetStorePath)
            }
            else {
                $mountedImage.ExpandDeploymentArtifacts($Parameters.Configuration.Role.PrivateInfo.DeploymentContent)
            }
        }

        if ($UseVersionedWim) {
            # Get current or UpdateVersion in case of update
            $updateVersion = Get-InProgressUpdateVersion -Parameters $Parameters
            if ($null -ne $updateVersion) {
                $version = $updateVersion
            }
            else {
                $version = ([string]($cloudRole.PublicInfo.Version)).Trim()
            }

            $mountedImage.UpdateImageStatusFile($version, "BuildCompleted")
        }
    }
    finally
    {
        $mountedImage.Dispose()
    }
}

<#
.SYNOPSIS
 Dismounts all the mounted images
 
.DESCRIPTION
 Dismounts all the mounted images
#>

function Dismount-AllMountedImages
{
    Import-Module Dism
    Clear-WindowsCorruptMountPoint
    $mountedImages = Get-WindowsImage -Mounted -Verbose:$false
    if ($mountedImages) {
        Trace-Execution "Found the following mounted images:`r`n$($mountedImages | Out-String)"
        Trace-Execution "Discard previously mounted images."
        $null = $mountedImages | ForEach-Object {
            $mountPath = $_.MountPath
            try
            {
                Trace-Execution "Unmounting '$mountPath'"
                Dismount-WindowsImage -Path $mountPath -Discard -Verbose:$false
            }
            catch
            {
                # It is possible we may have hit mount point invalid mount point state due to ECE failover
                # so applying remediation and retrying. See VSO 8588300 for more details.
                Trace-Execution "Failed to unmount '$mountPath' so unloading mounted keys and retrying"
                $mountPathKey = $mountPath.Replace('\','/')
                $(reg query HKLM) | ForEach-Object { if ($_ -like "*$mountPathKey*") { reg unload $_ } }
                Dismount-WindowsImage -Path $mountPath -Discard -Verbose:$false
            }
        }
    }

    # If the mounted image was undergoing a DISM operation while ECE service was failed over or crashed,
    # it's possible that the image is still mounted. We can look for any such disks mounted from the SU1FileServer
    # and clean them up.
    $mountedDisks = Get-Disk | Where-Object Location -match "SU1FileServer"
    if ($mountedDisks) {
        Trace-Execution "Found the following mounted disks:`r`n$($mountedDisks | Format-List Number, FriendlyName, Size, Location)"
        foreach ($disk in $mountedDisks) {
            Dismount-VHD -DiskNumber $disk.Number
        }
    }

    Import-Module Storage
    Get-Volume | Where-Object FileSystemLabel -eq 'Deployment' | ForEach-Object { Get-DiskImage -DevicePath $_.Path.TrimEnd('\') } | Dismount-DiskImage
}

<#
 .Synopsis
  Reliable atomic copy of file.
 
 .Description
  This function performs a reliable copy of a file by using temporary staging copy and hash validation mechanism. This function is designed to be re-entrant.
#>

function Copy-FileAtomic
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, HelpMessage="Literal path to the source file which is to be copied.")]
        [string]
        $SourceFilePath,

        [Parameter(Mandatory = $true, HelpMessage="Literal path to the destination file where the source file would be copied to.")]
        [string]
        $DestinationFilePath
    )

    $destFile = Split-Path -path $DestinationFilePath -Leaf
    $destFolder = Split-Path -path $DestinationFilePath -Parent

    $newFileName = '{0}.new' -f $destFile
    $prevFileName = '{0}.prev' -f $destFile

    $newPath = Join-Path $destFolder $newFileName
    $prevPath = Join-Path $destFolder $prevFileName

    # Handle code re-entrancy
    if (Test-Path $prevPath)
    {
        if (Test-Path $DestinationFilePath)
        {
            Write-Verbose "Removing stale file: $prevPath"
            Remove-Item -Path $prevPath
        }
        else
        {
            Write-Verbose "Renaming stale file: $prevPath to $DestinationFilePath"
            Rename-Item -Path $prevPath -NewName $DestinationFilePath
        }
    }

    # Copy file to a temp file in destination folder
    Write-Verbose "Copying file $SourceFilePath to $newPath"
    Copy-Item -Path $SourceFilePath -Destination $newPath -Force -ErrorAction Stop

    # Validate copied staging file against original source file
    if ((Get-FileHash $SourceFilePath).hash -ne (Get-FileHash $newPath).hash)
    {
        throw "$newPath is not copied correctly."
    }

    # Handle atomicity and overwrite issues
    $overwrite = Test-Path $DestinationFilePath

    if ($overwrite)
    {
        Write-Verbose "Renaming existing destination file: $DestinationFilePath to $prevPath"
        Rename-Item -Path $DestinationFilePath -NewName $prevPath -ErrorAction Stop
    }

    Write-Verbose "Renaming new file: $newPath to $DestinationFilePath"
    Rename-Item -Path $newPath -NewName $DestinationFilePath -ErrorAction Stop

    if ($overwrite)
    {
        Write-Verbose "Removing stale file: $prevPath"
        Remove-Item -Path $prevPath
    }
}

<#
.SYNOPSIS
 Copy base images into the verioned folder
 
.DESCRIPTION
  Creates copies of the base images in the versioned folder.
#>

function New-VersionedImages
{
    [CmdletBinding()]
    param (
        [string] $InProgressImageFolder,
        [string] $BaseImageFolder,
        [string[]] $Images
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    # If the build folder exists, delete it and start again
    if (Test-Path $InProgressImageFolder) {
        Trace-Execution "Clean up old content as build did not complete during previous run."
        Remove-Item -Path $InProgressImageFolder -Recurse -Force
    }

    Trace-Execution "Creating build folder $InProgressImageFolder"
    $null = New-Item -Path $InProgressImageFolder -ItemType Directory -Force

    # Loop through all name of all the images needed
    foreach ($imageName in $Images) {
        $newImagePath = Join-Path -Path $InProgressImageFolder -ChildPath $imageName
        if (Test-Path $newImagePath) {
            Trace-Execution "The new image: $newImagePath already exists. Skipping creation of this image"
        }
        else {
            $sourceImagePath = Join-Path -Path $BaseImageFolder -ChildPath $imageName
            if (-not(Test-Path -Path $sourceImagePath)) {
                $sourceImagePath = Join-Path -Path 'C:\CustomArtifacts\Core\Image' -ChildPath $imageName
                if (-not(Test-Path -Path $sourceImagePath)) {
                    throw "Unable to find '$($imageName)' in any known source image path."
                }
            }
            Trace-Execution "Copying source image at '$sourceImagePath' to destination image path '$newImagePath'."
            Copy-FileAtomic -SourceFilePath $sourceImagePath -DestinationFilePath $newImagePath -Verbose -ErrorAction Stop
        }
    }
}

<#
.SYNOPSIS
 Get names of the images needed for deployment and updates
 
.DESCRIPTION
  Creates copies of the base images in the versioned folder.
 
.PARAMETER Parameters
 The EceInterfaceParameters object which is populated by ECE
#>

function Get-SourceImageNames
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [CloudEngine.Configurations.EceInterfaceParameters]
        $Parameters,

        [switch] $SkipWim,

        [switch] $SkipArtifacts
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    $images = @()
    # Roles
    $physicalMachinesRole = $Parameters.Roles["BareMetal"].PublicConfiguration
    $virtualMachinesRole = $Parameters.Roles["VirtualMachines"].PublicConfiguration

    if ($SkipWim.IsPresent -eq $false) {
        # Image to be used for host
        # Boot Image (boot.wim)
        $bootImageName = $physicalMachinesRole.PublicInfo.BootImage.BaseImage
        if ($bootImageName) {
            $images += $bootImageName
        }
    }

    # Image to be used for host OS.
    $hostOSImageName = $physicalMachinesRole.PublicInfo.InstallImage.BaseImage
    if ($hostOSImageName -and $images -notcontains $hostOSImageName) {
        $images += $hostOSImageName
    }

    if (($SkipWim.IsPresent -eq $false) -and ([Environment]::GetEnvironmentVariable("OSImageType") -eq "ISO")) {
        $images += $physicalMachinesRole.PublicInfo.InstallImage.ISOName
    }

    # Identify names of the VHDs to be copied, update the base images to be used for the guest VMs
    $guestVMs =  $virtualMachinesRole.Nodes.Node
    foreach ($vm in $guestVMs) {
        $osImageName = $vm.Vhds.Vhd | Where-Object Index -eq 0 | ForEach-Object Path
        if ($images -notcontains $osImageName) {
            # Add it to the array to avoid configuration of the same image multiple times
            $images += $osImageName
        }

        # Add Artifacts VHD to list of required images if specified in any VM role definition.
        $artifactsVhdName = $vm.Vhds.Vhd | Where-Object Path -eq "Artifacts.vhdx" | ForEach-Object Path

        if (![string]::IsNullOrEmpty($artifactsVhdName)) {
            Trace-Execution "Artifacts VHD '$artifactsVhdName' is requested by role definition of VM '$($vm.Name)'."
            Trace-Execution "SkipArtifacts switch is '$SkipArtifacts'."
            Trace-Execution "Current image list: '$images'."

            if (!$SkipArtifacts.IsPresent -and $images -notcontains $artifactsVhdName) {
                Trace-Execution "Add Artifacts VHD '$artifactsVhdName' to list of images required for deployment or update."
                $images += $artifactsVhdName

                Trace-Execution "Updated image list: '$images'."
            }
        }
    }
    $images = $images | where {$_ -inotmatch 'WindowsServerCore.vhdx'}
    return $images
}

# This function clears the mounted image.
function Clear-ImageMount
{
    param (
        # This will be \\XXXX\XXVM_temp*.vhdx
        [Parameter(Mandatory=$true)]
        [Alias("UncPath")]
        [string]
        $Path,

        [switch]
        $DeleteImage,

        # If the path is on the file server this script will attempt to close any open SMB handles.
        # Doing so requires a PS session to the file server. However the file server name (SU1FileServer) can resolve
        # to changing IPs, which can break the underlying WinRM session. As a workaround, an alternate target server name
        # can be provided by the caller (typically the cluster name), which always resolves to the same IP address.
        [string]
        $AlternateFileServerName
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    # Check if file or its dism mount exists.
    if (-not (Test-Path $Path*)) {
        Trace-Execution "The specified image '$Path' does not exist, no cleanup is necessary."
        return
    }

    if (Get-WindowsImage -Mounted | Where-Object MountStatus -eq 'Invalid') {
        Trace-Execution "Clear corrupt WindowsImage/DISM mount points."
        $null = Clear-WindowsCorruptMountPoint -Verbose:$False
    }

    Get-Disk | Where-Object Location -like $Path | ForEach-Object {
        Trace-Execution "Dismount image mounted by Mount-DiskImage on the local host."
        Dismount-DiskImage -ImagePath $_.Location -Confirm:$false
    }

    if ((Get-ChildItem "$Path.dism*" -Force) -or ((Get-ChildItem -Path $Path).Extension -eq '.wim')) {
        Trace-Execution "The file appears to be mounted by Mount-WindowsImage command, attempting to dismount it gracefully."
        Get-WindowsImage -Mounted -Verbose:$False | Where-Object ImagePath -like $Path | ForEach-Object {
            if ($_.MountStatus -eq 'NeedsRemount') {
                $null = New-Item -Path $_.Path -ItemType Directory -Force
                Mount-WindowsImage -Path $_.Path -Remount -Verbose:$False
            }
            $null = Dismount-WindowsImage -Path $_.Path -Discard -Verbose:$False
        }
    }

    # Check if path points to a network share.
    $uri = [System.Uri]$Path
    if ($uri.IsUnc) {
        $serverName = $uri.Host

        # WinNextWorkaround: do not use SU1FileServer as the target name for remote sessions. Use the cluster name instead.
        if ($serverName -eq "SU1FileServer" -and $AlternateFileServerName) {
            Trace-Execution "WinNextWorkaround: Known issues with creating remote sessions to SU1FileServer. Connecting to $AlternateFileServerName instead."
            $serverName = $AlternateFileServerName
        }

        $sharePath = [System.IO.Path]::GetPathRoot($Path)
        $shareRelativePath = $Path.Replace($sharePath, '').TrimStart('\')
        Trace-Execution "Establish remote connection to file server '$serverName' to remove potential locks on the file '$Path'."
        Invoke-Command $serverName {
            $verbose = $using:VerbosePreference -eq [System.Management.Automation.ActionPreference]::Continue
            Trace-Execution "Get SMB file handles to '$using:shareRelativePath'." -Verbose:$verbose
            $smbOpenFiles = Get-SmbOpenFile | Where-Object ShareRelativePath -like "$using:shareRelativePath*"
            if ($smbOpenFiles) {
                Trace-Execution "Remove SMB file handles to $($smbOpenFiles.ShareRelativePath -join ', ')." -Verbose:$verbose
                $smbOpenFiles | Close-SmbOpenFile -Force -Verbose:$verbose
            }
        }
    }

    # If the mounted image was undergoing a DISM operation it's possible that the image is still mounted.
    # We can look for any such disks mounted from the SU1FileServer and clean them up.
    $mountedDisks = $null
    try
    {
        $mountedDisks = Get-Disk | Where-Object Location -match $Path
    }
    catch
    {
        Trace-Execution "Failed to get disk using location match. '$_' . Trying exact match."

        try
        {
            $mountedDisks = Get-Disk | Where-Object Location -eq $Path
        }
        catch
        {
            Trace-Execution "Failed to get disk using exact match. '$_'."
        }
    }

    try
    {
        if ($mountedDisks) {
            Trace-Execution "Found the following mounted disks:`r`n$($mountedDisks | Format-List Number, FriendlyName, Size, Location | Out-String)"
            foreach ($disk in $mountedDisks) {
                Dismount-VHD -DiskNumber $disk.Number
            }
        }
    }
    catch
    {
        Trace-Execution "Best-effort error during dismount share disks: $_"
    }

    if (Get-ChildItem "$Path.dism*" -Force) {
        Trace-Execution "Failed to dismount image gracefully, attempting to forcefully remove the file."
        $dismFilePath = Get-ChildItem "$Path.dism*" -Force
        Trace-Execution "Remove '$dismFilePath'."
        $dismFilePath | Remove-Item -Force
    }

    if ($DeleteImage -and (Test-Path $Path)) {
        Trace-Execution "Delete image '$Path'."
        Remove-Item -Path $Path -Force
    }
}


Export-ModuleMember -Function Add-GuestCluster
Export-ModuleMember -Function Add-IDnsConfiguration
Export-ModuleMember -Function Add-IPAddress
Export-ModuleMember -Function Add-LoadBalancerToNetworkAdapter
Export-ModuleMember -Function Add-NetworkAdapterToNetwork
Export-ModuleMember -Function Clear-ImageMount
Export-ModuleMember -Function ConnectPSSession
Export-ModuleMember -Function ConvertFrom-IPAddress
Export-ModuleMember -Function Convert-IPv4IntToString
Export-ModuleMember -Function Convert-IPv4StringToInt
Export-ModuleMember -Function ConvertTo-IPAddress
Export-ModuleMember -Function ConvertTo-MacAddress
Export-ModuleMember -Function ConvertTo-PrefixLength
Export-ModuleMember -Function ConvertTo-SubnetMask
Export-ModuleMember -Function Dismount-AllMountedImages
Export-ModuleMember -Function Dismount-MountedDisk
Export-ModuleMember -Function Expand-DeploymentArtifacts
Export-ModuleMember -Function Expand-NugetContent
Export-ModuleMember -Function Expand-UpdateContent
Export-ModuleMember -Function Get-BareMetalCredential
Export-ModuleMember -Function Get-BroadcastAddress
Export-ModuleMember -Function Get-ClusterShare
Export-ModuleMember -Function Get-ClusterShareNames
Export-ModuleMember -Function Get-DomainCredential
Export-ModuleMember -Function Get-DomainIPMapping
Export-ModuleMember -Function Get-ExecutionContextNodeName
Export-ModuleMember -Function Get-GatewayAddress
Export-ModuleMember -Function Get-HostUpdateShare
Export-ModuleMember -Function Get-InProgressUpdateVersion
Export-ModuleMember -Function Get-IsVirtualNetworkAlreadyConfigured
Export-ModuleMember -Function Get-JeaSession
Export-ModuleMember -Function Get-LocalCsvPathFromSharePath
Export-ModuleMember -Function Get-MacAddress
Export-ModuleMember -Function Get-MacAddressString
Export-ModuleMember -Function Get-MountedDisk
Export-ModuleMember -Function Get-MountedDiskDriveLetter
Export-ModuleMember -Function Get-NCAccessControlList
Export-ModuleMember -Function Get-NCCredential
Export-ModuleMember -Function Get-NCGateway
Export-ModuleMember -Function Get-NCGatewayPool
Export-ModuleMember -Function Get-NCIPPool
Export-ModuleMember -Function Get-NCLoadBalancer
Export-ModuleMember -Function Get-NCLoadbalancerManager
Export-ModuleMember -Function Get-NCLoadBalancerMux
Export-ModuleMember -Function Get-NCLogicalNetwork
Export-ModuleMember -Function Get-NCLogicalNetworkSubnet
Export-ModuleMember -Function Get-NCMACPool
Export-ModuleMember -Function Get-NCNetworkInterface
Export-ModuleMember -Function Get-NCNetworkInterfaceInstanceId
Export-ModuleMember -Function Get-NCNetworkInterfaceResourceId
Export-ModuleMember -Function Get-NCPublicIPAddress
Export-ModuleMember -Function Get-NCServer
Export-ModuleMember -Function Get-NCSwitch
Export-ModuleMember -Function Get-NCVirtualGateway
Export-ModuleMember -Function Get-NCVirtualNetwork
Export-ModuleMember -Function Get-NCVirtualServer
Export-ModuleMember -Function Get-NCVirtualSubnet
Export-ModuleMember -Function Get-NetworkAddress
Export-ModuleMember -Function Get-NetworkDefinitionForCluster
Export-ModuleMember -Function Get-NetworkDefinitions
Export-ModuleMember -Function Get-NetworkMap
Export-ModuleMember -Function Get-NetworkNameForCluster
Export-ModuleMember -Function Get-PortProfileId
Export-ModuleMember -Function Get-RangeEndAddress
Export-ModuleMember -Function Get-ScopeRange
Export-ModuleMember -Function Get-ServerResourceId
Export-ModuleMember -Function Get-SharePath
Export-ModuleMember -Function Get-SourceImageNames
Export-ModuleMember -Function Get-StorageEndpointName
Export-ModuleMember -Function Get-UsernameAndPassword
Export-ModuleMember -Function Initialize-ECESession
Export-ModuleMember -Function Invoke-ECECommand
Export-ModuleMember -Function Invoke-PSDirectOnVM
Export-ModuleMember -Function Invoke-ScriptBlockInParallel
Export-ModuleMember -Function Invoke-ScriptBlockWithRetries
Export-ModuleMember -Function Invoke-WebRequestWithRetries
Export-ModuleMember -Function IsIpPoolRangeValid
Export-ModuleMember -Function IsIpWithinPoolRange
Export-ModuleMember -Function JSONDelete
Export-ModuleMember -Function JSONGet
Export-ModuleMember -Function JSONPost
Export-ModuleMember -Function Mount-WindowsImageWithRetry
Export-ModuleMember -Function New-ACL
Export-ModuleMember -Function New-Credential
Export-ModuleMember -Function New-DeployDirect
Export-ModuleMember -Function New-LoadBalancerVIP
Export-ModuleMember -Function New-NCAccessControlList
Export-ModuleMember -Function New-NCAccessControlListRule
Export-ModuleMember -Function New-NCBgpPeer
Export-ModuleMember -Function New-NCBgpRouter
Export-ModuleMember -Function New-NCBgpRoutingPolicy
Export-ModuleMember -Function New-NCBgpRoutingPolicyMap
Export-ModuleMember -Function New-NCCredential
Export-ModuleMember -Function New-NCGateway
Export-ModuleMember -Function New-NCGatewayPool
Export-ModuleMember -Function New-NCGreTunnel
Export-ModuleMember -Function New-NCIPPool
Export-ModuleMember -Function New-NCIPSecTunnel
Export-ModuleMember -Function New-NCL3Tunnel
Export-ModuleMember -Function New-NCLoadBalancer
Export-ModuleMember -Function New-NCLoadBalancerBackendAddressPool
Export-ModuleMember -Function New-NCLoadBalancerFrontEndIPConfiguration
Export-ModuleMember -Function New-NCLoadBalancerLoadBalancingRule
Export-ModuleMember -Function New-NCLoadBalancerMux
Export-ModuleMember -Function New-NCLoadBalancerMuxPeerRouterConfiguration
Export-ModuleMember -Function New-NCLoadBalancerOutboundNatRule
Export-ModuleMember -Function New-NCLoadBalancerProbe
Export-ModuleMember -Function New-NCLoadBalancerProbeObject
Export-ModuleMember -Function New-NCLogicalNetwork
Export-ModuleMember -Function New-NCLogicalNetworkSubnet
Export-ModuleMember -Function New-NCLogicalSubnet
Export-ModuleMember -Function New-NCMACPool
Export-ModuleMember -Function New-NCNetworkInterface
Export-ModuleMember -Function New-NCPublicIPAddress
Export-ModuleMember -Function New-NCServer
Export-ModuleMember -Function New-NCServerConnection
Export-ModuleMember -Function New-NCServerNetworkInterface
Export-ModuleMember -Function New-NCSlbState
Export-ModuleMember -Function New-NCSwitch
Export-ModuleMember -Function New-NCSwitchPort
Export-ModuleMember -Function New-NCVirtualGateway
Export-ModuleMember -Function New-NCVirtualNetwork
Export-ModuleMember -Function New-NCVirtualServer
Export-ModuleMember -Function New-NCVirtualSubnet
Export-ModuleMember -Function New-NCVpnClientAddressSpace
Export-ModuleMember -Function New-PxeUnattendFile
Export-ModuleMember -Function New-VersionedImages
Export-ModuleMember -Function New-VhdFromWim
Export-ModuleMember -Function New-WimFromVhd
Export-ModuleMember -Function New-WinPEImage
Export-ModuleMember -Function NormalizeIPv4Subnet
Export-ModuleMember -Function PublishAndStartDscConfiguration
Export-ModuleMember -Function PublishAndStartDscForJea
Export-ModuleMember -Function Remove-LoadBalancerFromNetworkAdapter
Export-ModuleMember -Function Remove-NCAccessControlList
Export-ModuleMember -Function Remove-NCCredential
Export-ModuleMember -Function Remove-NCGateway
Export-ModuleMember -Function Remove-NCGatewayPool
Export-ModuleMember -Function Remove-NCIPPool
Export-ModuleMember -Function Remove-NCLoadBalancer
Export-ModuleMember -Function Remove-NCLoadBalancerMux
Export-ModuleMember -Function Remove-NCLogicalNetwork
Export-ModuleMember -Function Remove-NCMACPool
Export-ModuleMember -Function Remove-NCNetworkInterface
Export-ModuleMember -Function Remove-NCPublicIPAddress
Export-ModuleMember -Function Remove-NCServer
Export-ModuleMember -Function Remove-NCSwitch
Export-ModuleMember -Function Remove-NCVirtualGateway
Export-ModuleMember -Function Remove-NCVirtualNetwork
Export-ModuleMember -Function Remove-NCVirtualServer
Export-ModuleMember -Function Remove-PortProfileId
Export-ModuleMember -Function Set-MacAndIPAddress
Export-ModuleMember -Function Set-NCConnection
Export-ModuleMember -Function Set-NCLoadBalancerManager
Export-ModuleMember -Function Set-PortProfileId
Export-ModuleMember -Function Set-PortProfileIdHelper
Export-ModuleMember -Function Test-IPConnection
Export-ModuleMember -Function Test-NetworkMap
Export-ModuleMember -Function Test-PSSession
Export-ModuleMember -Function Test-SkipDriverInjection
Export-ModuleMember -Function Test-WSManConnection
Export-ModuleMember -Function Trace-Error
Export-ModuleMember -Function Trace-Execution
Export-ModuleMember -Function Trace-Warning
Export-ModuleMember -Function Update-BootWimFile
Export-ModuleMember -Function Update-JEAEndpointsForUpdate
Export-ModuleMember -Function Update-NCCredential
Export-ModuleMember -Function Update-NCServer
Export-ModuleMember -Function Update-NCVirtualServer
Export-ModuleMember -Function Wait-Result
Export-ModuleMember -Function Wait-VirtualNetwork

# SIG # Begin signature block
# MIIn0QYJKoZIhvcNAQcCoIInwjCCJ74CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBW7frEEy9LDFln
# B2FNjd9HOu0VLVpEEoWhT/Q9gxCqrqCCDYUwggYDMIID66ADAgECAhMzAAADri01
# UchTj1UdAAAAAAOuMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwODU5WhcNMjQxMTE0MTkwODU5WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQD0IPymNjfDEKg+YyE6SjDvJwKW1+pieqTjAY0CnOHZ1Nj5irGjNZPMlQ4HfxXG
# yAVCZcEWE4x2sZgam872R1s0+TAelOtbqFmoW4suJHAYoTHhkznNVKpscm5fZ899
# QnReZv5WtWwbD8HAFXbPPStW2JKCqPcZ54Y6wbuWV9bKtKPImqbkMcTejTgEAj82
# 6GQc6/Th66Koka8cUIvz59e/IP04DGrh9wkq2jIFvQ8EDegw1B4KyJTIs76+hmpV
# M5SwBZjRs3liOQrierkNVo11WuujB3kBf2CbPoP9MlOyyezqkMIbTRj4OHeKlamd
# WaSFhwHLJRIQpfc8sLwOSIBBAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUhx/vdKmXhwc4WiWXbsf0I53h8T8w
# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzUwMTgzNjAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# AGrJYDUS7s8o0yNprGXRXuAnRcHKxSjFmW4wclcUTYsQZkhnbMwthWM6cAYb/h2W
# 5GNKtlmj/y/CThe3y/o0EH2h+jwfU/9eJ0fK1ZO/2WD0xi777qU+a7l8KjMPdwjY
# 0tk9bYEGEZfYPRHy1AGPQVuZlG4i5ymJDsMrcIcqV8pxzsw/yk/O4y/nlOjHz4oV
# APU0br5t9tgD8E08GSDi3I6H57Ftod9w26h0MlQiOr10Xqhr5iPLS7SlQwj8HW37
# ybqsmjQpKhmWul6xiXSNGGm36GarHy4Q1egYlxhlUnk3ZKSr3QtWIo1GGL03hT57
# xzjL25fKiZQX/q+II8nuG5M0Qmjvl6Egltr4hZ3e3FQRzRHfLoNPq3ELpxbWdH8t
# Nuj0j/x9Crnfwbki8n57mJKI5JVWRWTSLmbTcDDLkTZlJLg9V1BIJwXGY3i2kR9i
# 5HsADL8YlW0gMWVSlKB1eiSlK6LmFi0rVH16dde+j5T/EaQtFz6qngN7d1lvO7uk
# 6rtX+MLKG4LDRsQgBTi6sIYiKntMjoYFHMPvI/OMUip5ljtLitVbkFGfagSqmbxK
# 7rJMhC8wiTzHanBg1Rrbff1niBbnFbbV4UDmYumjs1FIpFCazk6AADXxoKCo5TsO
# zSHqr9gHgGYQC2hMyX9MGLIpowYCURx3L7kUiGbOiMwaMIIHejCCBWKgAwIBAgIK
# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm
# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw
# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD
# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la
# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc
# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D
# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+
# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk
# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6
# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd
# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL
# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd
# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3
# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS
# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI
# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL
# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD
# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h
# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA
# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn
# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7
# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b
# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/
# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy
# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp
# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi
# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb
# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS
# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL
# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX
# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGaIwghmeAgEBMIGVMH4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p
# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAOuLTVRyFOPVR0AAAAA
# A64wDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIAbq
# Go6n2JnnkcMzo9PLqhAPL3WelPHJNNUXriYNKJpJMEIGCisGAQQBgjcCAQwxNDAy
# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20wDQYJKoZIhvcNAQEBBQAEggEA47MKKRdtPbp/ojRtYKkUF15UHTdUnHo0faf5
# eSjUzHV+dCjXCZt6g1+6k6byvnWLOMl+sIZowCJ7j0c1+fMfBk/YmRWTf9Oy5c8X
# rxXm44tGxNRleZO+JQxwZxVF9NELnip15hYIwg1DdgxfbenX78a8DUj+/lDxnt5Q
# Z+zOSVGl6wAGtlNQZmayvEz5MUWR1d4PF7zz165tL0nAvPp1Ii7XIJk7s+u7oxml
# ghtTdv5pJggF0UY6OmvtGPepX9wPWYVQ/93Lo/VNlb1m0wA6wNhY2I9MHs18+FjP
# xRQReX6pcghE5084FttDRRAUf8PCEjKitjiCiI9lC18mI4PpbKGCFywwghcoBgor
# BgEEAYI3AwMBMYIXGDCCFxQGCSqGSIb3DQEHAqCCFwUwghcBAgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggFZBgsqhkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGE
# WQoDATAxMA0GCWCGSAFlAwQCAQUABCCwgRYKktAm6quYSxvvehSLHr1claPgX88J
# 8SpBawpf+AIGZdX+rHU7GBMyMDI0MDMxMTE4MTcxNi44MDZaMASAAgH0oIHYpIHV
# MIHSMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsT
# HVRoYWxlcyBUU1MgRVNOOjNCRDQtNEI4MC02OUMzMSUwIwYDVQQDExxNaWNyb3Nv
# ZnQgVGltZS1TdGFtcCBTZXJ2aWNloIIRezCCBycwggUPoAMCAQICEzMAAAHlj2rA
# 8z20C6MAAQAAAeUwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# UENBIDIwMTAwHhcNMjMxMDEyMTkwNzM1WhcNMjUwMTEwMTkwNzM1WjCB0jELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9z
# b2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjozQkQ0LTRCODAtNjlDMzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKl7
# 4Drau2O6LLrJO3HyTvO9aXai//eNyP5MLWZrmUGNOJMPwMI08V9zBfRPNcucreIY
# SyJHjkMIUGmuh0rPV5/2+UCLGrN1P77n9fq/mdzXMN1FzqaPHdKElKneJQ8R6cP4
# dru2Gymmt1rrGcNe800CcD6d/Ndoommkd196VqOtjZFA1XWu+GsFBeWHiez/Pllq
# cM/eWntkQMs0lK0zmCfH+Bu7i1h+FDRR8F7WzUr/7M3jhVdPpAfq2zYCA8ZVLNgE
# izY+vFmgx+zDuuU/GChDK7klDcCw+/gVoEuSOl5clQsydWQjJJX7Z2yV+1KC6G1J
# VqpP3dpKPAP/4udNqpR5HIeb8Ta1JfjRUzSv3qSje5y9RYT/AjWNYQ7gsezuDWM/
# 8cZ11kco1JvUyOQ8x/JDkMFqSRwj1v+mc6LKKlj//dWCG/Hw9ppdlWJX6psDesQu
# QR7FV7eCqV/lfajoLpPNx/9zF1dv8yXBdzmWJPeCie2XaQnrAKDqlG3zXux9tNQm
# z2L96TdxnIO2OGmYxBAAZAWoKbmtYI+Ciz4CYyO0Fm5Z3T40a5d7KJuftF6CTocc
# c/Up/jpFfQitLfjd71cS+cLCeoQ+q0n0IALvV+acbENouSOrjv/QtY4FIjHlI5zd
# JzJnGskVJ5ozhji0YRscv1WwJFAuyyCMQvLdmPddAgMBAAGjggFJMIIBRTAdBgNV
# HQ4EFgQU3/+fh7tNczEifEXlCQgFOXgMh6owHwYDVR0jBBgwFoAUn6cVXQBeYl2D
# 9OXSZacbUzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3Nv
# ZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUy
# MDIwMTAoMSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1l
# LVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUB
# Af8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQAD
# ggIBADP6whOFjD1ad8GkEJ9oLBuvfjndMyGQ9R4HgBKSlPt3pa0XVLcimrJlDnKG
# gFBiWwI6XOgw82hdolDiMDBLLWRMTJHWVeUY1gU4XB8OOIxBc9/Q83zb1c0RWEup
# gC48I+b+2x2VNgGJUsQIyPR2PiXQhT5PyerMgag9OSodQjFwpNdGirna2rpV23EU
# wFeO5+3oSX4JeCNZvgyUOzKpyMvqVaubo+Glf/psfW5tIcMjZVt0elswfq0qJNQg
# oYipbaTvv7xmixUJGTbixYifTwAivPcKNdeisZmtts7OHbAM795ZvKLSEqXiRUjD
# YZyeHyAysMEALbIhdXgHEh60KoZyzlBXz3VxEirE7nhucNwM2tViOlwI7EkeU5hu
# dctnXCG55JuMw/wb7c71RKimZA/KXlWpmBvkJkB0BZES8OCGDd+zY/T9BnTp8si3
# 6Tql84VfpYe9iHmy7PqqxqMF2Cn4q2a0mEMnpBruDGE/gR9c8SVJ2ntkARy5Sflu
# uJ/MB61yRvT1mUx3lyppO22ePjBjnwoEvVxbDjT1jhdMNdevOuDeJGzRLK9HNmTD
# C+TdZQlj+VMgIm8ZeEIRNF0oaviF+QZcUZLWzWbYq6yDok8EZKFiRR5otBoGLvaY
# FpxBZUE8mnLKuDlYobjrxh7lnwrxV/fMy0F9fSo2JxFmtLgtMIIHcTCCBVmgAwIB
# AgITMwAAABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UE
# BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc
# BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0
# IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1
# WhcNMzAwOTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O
# 1YLT/e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZn
# hUYjDLWNE893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t
# 1w/YJlN8OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxq
# D89d9P6OU8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmP
# frVUj9z6BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSW
# rAFKu75xqRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv
# 231fgLrbqn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zb
# r17C89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYcten
# IPDC+hIK12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQc
# xWv2XFJRXRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17a
# j54WcmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQAB
# MCMGCSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQU
# n6cVXQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEw
# QTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9E
# b2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQB
# gjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/
# MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJ
# oEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01p
# Y1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYB
# BQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9v
# Q2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3h
# LB9nATEkW+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x
# 5MKP+2zRoZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74p
# y27YP0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1A
# oL8ZthISEV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbC
# HcNhcy4sa3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB
# 9s7GdP32THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNt
# yo4JvbMBV0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3
# rsjoiV5PndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcV
# v7TOPqUxUYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A24
# 5oyZ1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lw
# Y1NNje6CbaUFEMFxBmoQtB1VM1izoXBm8qGCAtcwggJAAgEBMIIBAKGB2KSB1TCB
# 0jELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMk
# TWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1U
# aGFsZXMgVFNTIEVTTjozQkQ0LTRCODAtNjlDMzElMCMGA1UEAxMcTWljcm9zb2Z0
# IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUA942iGuYFrsE4wzWD
# d85EpM6RiwqggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAN
# BgkqhkiG9w0BAQUFAAIFAOmZhn4wIhgPMjAyNDAzMTEyMTMyNDZaGA8yMDI0MDMx
# MjIxMzI0NlowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA6ZmGfgIBADAKAgEAAgIA
# rAIB/zAHAgEAAgIRTzAKAgUA6ZrX/gIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgor
# BgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUA
# A4GBAIgAHeCDxKLqK/XLPfgL0M+GuWTuSzdJB6qLPyzZxGCpgCB9mPiGjjOa/RVN
# t/9oszm1E5qBLdvRTY8clHNH71MdmZSUs1beMMGN4tSygaTjKYB4aigZJt3KaIlk
# Cfc3Z1x9lQh7Yp1wPxP018ITd68WtyNaskGHepceY+gQkhqmMYIEDTCCBAkCAQEw
# gZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE
# AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAHlj2rA8z20C6MA
# AQAAAeUwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0B
# CRABBDAvBgkqhkiG9w0BCQQxIgQgJnWloAsgyHqojbpA6AvfZhMfDM8+c9YBkFSu
# o1w9wqowgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCAVqdP//qjxGFhe2Ybo
# EXeb8I/pAof01CwhbxUH9U697TCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwAhMzAAAB5Y9qwPM9tAujAAEAAAHlMCIEIHLeIT3zielfgWew98Co
# TTohktJqX0b2m4aWSu4BtAuGMA0GCSqGSIb3DQEBCwUABIICAE3VfImcYMEbcXu4
# rb9cnNBqwFZpDCiKZc5ccvYU/HWIhrvZHegz+pijw0HhE3OvlTAqJgHfJqJvF533
# +NEJlLsFt002yCcv/aWrI35uqGjAhKxU8HrRwvWs13a/NR14cmTk0auC93fD57Zp
# uX425geHHwCagxqwrjyPzc4jnlTieYJ6Ec5/ip61ciG5okHWtbAmQTrrE3XEKkft
# 0CXW6E94+0GE7Eqn2+lkiLabfOtj/drJ3irNEq1DBVymjvkNcSPE4OsdzuHXdQ0n
# lraF7CohQ4N8iAHrmnN7AL1Z3BkjGqLEqtWIxGe4crb+d6PwXleR/mJUTBhE317G
# vOxOYjcfxe/tIq0FwtMbuhFKza2cVlDXrAbTX4inJx5B7KHGwboW1hp949WrVrZc
# t5v09esGKgqddx0wI6BOtzL4IzoWcg28Zizbbz8tJLbjcBxM1TGNeE0hcP0unn7K
# HZ3hGlKeteRmuJHX8yHKlf4pZxD6MRx+EdnjHpQ5SAUstTFSbpdwazPDBev9nf2w
# Z4fIv0FdIXna9cmcegPavbZpyc29dL4D6bBig/Zwbj0m/2U0S+4mvrogNLz8t9gg
# L/zFatbD+T5R6MZpGDmGpioQ5aTI/Y34By2oFGDlAsIWLgtKBA5lCJ10V7t6AE6g
# zcPOyuFDMkNd/VpVYKZlH+4iTnAv
# SIG # End signature block