Obs/bin/ObsDep/content/Powershell/Roles/Common/DeployDirectCommon.psm1
<###################################################
# # # Copyright (c) Microsoft. All rights reserved. # # # ##################################################> $HostFile = "$Env:SystemRoot\System32\Drivers\Etc\Hosts" # Set manually if needed $ENABLE_DEBUGGING = $false $DEBUG_CONNECTION_TYPE = '[DebugConnectionType]' $DEBUG_SERIAL_PORT = '[DebugSerialPort]' $DEBUG_SERIAL_BAUD_RATE = '[DebugSerialBaudRate]' $DEBUG_NET_PORT_MAP_STRING = '[DebugNetPortMapString]' $DEBUG_NET_HOST_IP = '[DebugNetHostIP]' $DEBUG_NET_KEY = '[DebugNetKey]' $DEBUG_NET_BUS_PARAMS = '[DebugNetBusParams]' $ENABLE_SERIAL_CONSOLE = $false $CONSOLE_SERIAL_PORT = '[ConsoleSerialPort]' $CONSOLE_SERIAL_BAUD_RATE = '[ConsoleSerialBaudRate]' # Starts all the services needed to intialize deployment on win PE function Set-WinPEDeploymentPrerequisites { $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop if (-not (Get-Command wpeutil*)) { Write-Warning "This script is intended to be execute in WinPE only." return } $null = wpeutil InitializeNetwork $null = wpeutil EnableFirewall $null = wpeutil WaitForNetwork $null = Start-Service -Name LanmanWorkstation } function New-NetworkDrive { param ( [Parameter(Mandatory=$true)] [string] $IPv4Address, [Parameter(Mandatory=$true)] [string] $HostName, [Parameter(Mandatory=$true)] [string] $ShareRoot, [Parameter(Mandatory=$true)] [PSCredential] $Credential, [Parameter(Mandatory=$true)] [string] $DriveLetter ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop # Add Host Entry $hostEntry = "$IPv4Address $HostName" if (-not (Get-Content $HostFile).Contains($hostEntry)) { Write-Verbose "Add host entry: '$hostEntry'." -Verbose $hostEntry | Out-File -FilePath $HostFile -Append -Encoding ascii } if (Get-PSDrive | Where-Object Name -eq $DriveLetter) { throw [System.InvalidOperationException]::new("The letter $DriveLetter is already assigned to an existing PSDrive.") } $maxRetries = 5 $retries = 1 $successful = $false while ($retries -le $maxRetries) { try { # Set PS Drive if (-not (Get-PSDrive | Where-Object Name -eq $DriveLetter)) { Write-Verbose "Create PSDrive '$DriveLetter' to '$ShareRoot'." -Verbose $null = New-PSDrive -Name $DriveLetter -PSProvider FileSystem -Root $ShareRoot -Credential $Credential -Persist -Scope Global $successful = $true break } } catch { Write-Warning $_ Write-Verbose "Failed to create PSDrive '$DriveLetter' to '$ShareRoot'. Sleep 60 seconds and retry $retries/$maxRetries." -Verbose Start-Sleep -Seconds 60 } $retries ++ } if ($successful) { Write-Verbose "Create PSDrive '$DriveLetter' to '$ShareRoot' successfully." -Verbose } else { throw "Failed to create PSDrive '$DriveLetter' to '$ShareRoot' after $maxRetries retries." } } # Returns back the SystemDrive function Set-DiskConfiguration { [CmdletBinding()] [OutputType([String])] param ( [Parameter(Mandatory=$true)] [string] $LogPath, [Parameter(Mandatory=$false)] [string] $BootDiskConfigPath, [Parameter(Mandatory=$false)] [bool] $ClearExisting=$true, [Parameter(Mandatory=$false)] [bool] $BootFromPhysicalDisk=$false, [Parameter(Mandatory=$false)] [bool] $IsOneDrive=$false ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop (Get-Date).ToString('yyyy/MM/dd HH:mm:ss') | Add-Content $LogPath if ($ClearExisting) { "Reset the disks and clean them of all data." | Add-Content $LogPath Get-Partition | Remove-Partition -Confirm:$false -ErrorAction SilentlyContinue # account for change in Reset-PhysicalDisk parameters in WinPE with Windows cumulative update $PDParam = @{} if ((Get-Command -Name 'Reset-PhysicalDisk').Parameters['Confirm']) { $PDParam.Add('Confirm',$false) } Get-PhysicalDisk | Reset-PhysicalDisk @PDParam Get-Disk | Where-Object PartitionStyle -ne RAW | ForEach-Object { $_ | Set-Disk -IsOffline:$false -ErrorAction SilentlyContinue $_ | Set-Disk -IsReadOnly:$false -ErrorAction SilentlyContinue $_ | Clear-Disk -RemoveData -RemoveOEM -Confirm:$false -ErrorAction SilentlyContinue } } else { "Keeping the existing partitions." | Add-Content $LogPath } Get-Disk | ForEach-Object { $_ | Set-Disk -IsReadOnly:$true -ErrorAction SilentlyContinue $_ | Set-Disk -IsOffline:$true -ErrorAction SilentlyContinue } Update-StorageProviderCache -DiscoveryLevel Full (Get-Date).ToString('yyyy/MM/dd HH:mm:ss') | Add-Content $LogPath "Select the disk to boot from." | Add-Content $LogPath Get-PhysicalDisk | Sort-Object DeviceId | Format-Table DeviceId, Model, BusType, MediaType, Size | Out-String | Add-Content $LogPath Get-Disk | Out-String | Add-Content $LogPath $allbootCandidateDisks = Get-PhysicalDisk if (-not $allbootCandidateDisks) { throw 'No suitable boot candidate disks found.' } # log the data about physical disks that filtering uses "All disks." | Add-Content $LogPath $allbootCandidateDisks | Sort-Object DeviceId | Select-Object FriendlyName,SerialNumber,BusType,DeviceId,Manufacturer,Model,MediaType,Size | Format-Table | Out-String | Add-Content $LogPath if ($bootDiskConfigPath -and (Test-Path $bootDiskConfigPath)) { "Boot disk configuration file '$bootDiskConfigPath' exists." | Add-Content $LogPath [xml] $config = Get-Content $bootDiskConfigPath $bootDiskConfigs = $config.disks.disk $filteredBootCandidateDisks = $null foreach ($bootDiskConfig in $bootDiskConfigs) { # only apply each filter if the previous filter did not return any disks if (-not $filteredBootCandidateDisks) { # log what if being used as a filter "Filter - BusType: $($bootDiskConfig.BusType), DeviceId: $($bootDiskConfig.DeviceId), Manufacturer: $($bootDiskConfig.Manufacturer), Model: $($bootDiskConfig.Model), MediaType: $($bootDiskConfig.MediaType), Size: $($bootDiskConfig.Size)" | Add-Content $LogPath $filteredBootCandidateDisks = $allbootCandidateDisks | Where-Object { ($_.BusType -like $bootDiskConfig.BusType) -and ($_.DeviceId -like $bootDiskConfig.DeviceId) -and ($_.Manufacturer -like $bootDiskConfig.Manufacturer) -and ($_.Model -like $bootDiskConfig.Model) -and ($_.MediaType -like $bootDiskConfig.MediaType) -and ($_.Size -like $bootDiskConfig.Size) } # if this filter returns disks, set the busTypeFilter so we can filter further for * below if ($filteredBootCandidateDisks) { $busTypeFilter = $bootDiskConfig.BusType } } else { break } } # if no filtered disks after attempting all filters, we must fail if (-not $filteredBootCandidateDisks) { throw 'After filtering, no suitable boot candidate disks found.' } "Filtered disks." | Add-Content $LogPath $filteredBootCandidateDisks | Sort-Object DeviceId | Select-Object FriendlyName,SerialNumber,BusType,DeviceId,Manufacturer,Model,MediaType,Size | Format-Table | Out-String | Add-Content $LogPath ############################################################################################### # Temporary; after OEM extention is workin E2E, it will be removed $allTypeString = "*" if ($busTypeFilter -eq $allTypeString) { $filteredBootCandidateDisks = $filteredBootCandidateDisks | Where-Object BusType -in 'SATA', 'SAS', 'RAID' } "Filtered disks after the bus type filter 'SATA', 'SAS', 'RAID'." | Add-Content $LogPath $filteredBootCandidateDisks | Out-String | Add-Content $LogPath ############################################################################################### $bootCandidateDisks = $filteredBootCandidateDisks } else { $bootCandidateDisks = $allbootCandidateDisks | Where-Object BusType -in 'SATA', 'SAS', 'RAID' } if (-not $bootCandidateDisks) { throw 'No suitable boot candidate disk found.' } $bootCandidateDisks = $bootCandidateDisks | Where-Object DeviceId -in (Get-Disk).Number $bootCandidateDisks = $bootCandidateDisks | Sort-Object Size, DeviceId foreach ($currentDisk in $bootCandidateDisks) { $found = $(Get-Disk -Number $currentDisk.DeviceId | Get-Partition | Where-Object {$_.Type -eq "System"}) if ($found) { "Found boot disk with system partition type" | Add-Content $LogPath $found | Out-String | Add-Content $LogPath $bootCandidateDisk = $currentDisk break } } if ($null -eq $bootCandidateDisk) { "Select the first candidate disk as boot disk" | Add-Content $LogPath $bootCandidateDisk = $bootCandidateDisks | Select-Object -First 1 } $bootDiskNumber = $bootCandidateDisk.DeviceId if (-not $bootDiskNumber) { throw 'Not able to get the boot disk number.' } "Disk $bootDiskNumber will be used for boot partition." | Add-Content $LogPath if (-not($ClearExisting)) { # Remove the disk partition Get-Partition -DiskNumber $bootDiskNumber | Remove-Partition -Confirm:$false -ErrorAction SilentlyContinue # Reset only the boot disk $PDParam = @{} if ((Get-Command -Name 'Reset-PhysicalDisk').Parameters['Confirm']) { $PDParam.Add('Confirm',$false) } $bootCandidateDisk | Reset-PhysicalDisk @PDParam $disk = Get-Disk | Where-Object Number -eq $bootDiskNumber | Where-Object PartitionStyle -ne RAW "Disk about to be cleared:" | Add-Content $LogPath $disk | Out-String | Add-Content $LogPath $disk | ForEach-Object { $_ | Set-Disk -IsOffline:$false -ErrorAction SilentlyContinue $_ | Set-Disk -IsReadOnly:$false -ErrorAction SilentlyContinue $_ | Clear-Disk -RemoveData -RemoveOEM -Confirm:$false -ErrorAction SilentlyContinue $_ | Set-Disk -IsReadOnly:$true -ErrorAction SilentlyContinue $_ | Set-Disk -IsOffline:$true -ErrorAction SilentlyContinue } } ############################################################################################### # Temporary for R730; after OEM extention is working E2E, this will be removed [9282124] $firstDisk = $bootCandidateDisk $secondDisk = $bootCandidateDisks | Where-Object { $($_.DeviceId -ne $bootCandidateDisk.DeviceId) -and $($_.Size -ge $bootCandidateDisk.Size) } | Sort-Object Size, DeviceId | Select-Object -First 1 if ($firstDisk.Size -eq $secondDisk.Size) { $secondDiskNumber = $secondDisk.DeviceId $ssdDisks = Get-PhysicalDisk | Where-Object BusType -in 'SATA', 'SAS', 'RAID', 'NVMe' | Where-Object MediaType -eq SSD $nonOnboardSsdDisks = $ssdDisks | Where-Object DeviceId -notin $firstDisk.DeviceId, $secondDisk.DeviceId | Sort-Object Size if ($nonOnboardSsdDisks -and ($secondDisk.Size -lt $nonOnboardSsdDisks[0].Size / 1.01)) { throw "Disk $secondDiskNumber appears to be a secondary on-board drive; it needs to be removed." } } ############################################################################################## wpeutil UpdateBootInfo | Add-Content $LogPath $remove = @(Get-Volume | Where-Object DriveType -ne Fixed) foreach ($item in $remove) { $vol = Get-CimInstance -ClassName Win32_Volume -Filter "DriveLetter = '$($item.DriveLetter):'" if ($null -ne $vol) { try { "Remove drive letter assignment '$($item.DriveLetter):' from '$($item.DriveType)'." | Add-Content $LogPath $vol | Set-CimInstance -Property @{DriveLetter=$null} } catch { "WARNING: Failed to remove drive letter assignment '$($item.DriveLetter):' from '$($item.DriveType)'." | Add-Content $LogPath } } } $peFirmwareType = (Get-ItemProperty HKLM:\SYSTEM\CurrentControlSet\Control).PEFirmwareType # Returns 0x1 if the PC is booted into BIOS mode, or 0x2 if the PC is booted in UEFI mode. $isLegacyBoot = $peFirmwareType -eq 1 if ($isLegacyBoot) { "Create new partitions for Legacy Boot." | Add-Content $LogPath $null = Initialize-Disk -Number $bootDiskNumber -PartitionStyle MBR -ErrorAction SilentlyContinue if ($true -eq $BootFromPhysicalDisk) { $winPartition = New-Partition -DiskNumber $bootDiskNumber -Size 60GB -DriveLetter C -IsActive if (-not $winPartition) { throw 'Unable to create partition for physical drive OS Installation.' } $partition = New-Partition -DiskNumber $bootDiskNumber -UseMaximumSize -DriveLetter D } else { $partition = New-Partition -DiskNumber $bootDiskNumber -UseMaximumSize -AssignDriveLetter -IsActive } if (-not $partition) { throw 'Unable to create partition for OS Installation.' } $systemDrive = $partition.DriveLetter + ':' if ($true -eq $BootFromPhysicalDisk) { $null = Format-Volume -Partition $winPartition -FileSystem NTFS -Confirm:$false } $osVolume = Format-Volume -Partition $partition -FileSystem NTFS -Confirm:$false } else { "Create new partitions for UEFI." | Add-Content $LogPath $null = Initialize-Disk -Number $bootDiskNumber -ErrorAction SilentlyContinue $msrPartition = New-Partition -DiskNumber $bootDiskNumber -Size 128MB -GptType "{e3c9e316-0b5c-4db8-817d-f92df00215ae}" # MSR if ($true -eq $BootFromPhysicalDisk) { "Create partitions for Boot From Physical Disk scenario." | Add-Content $LogPath $sysPartition = New-Partition -DiskNumber $bootDiskNumber -Size 350MB -DriveLetter E -GptType "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" # ESP if(!$IsOneDrive) { $winPartition = New-Partition -DiskNumber $bootDiskNumber -Size 60GB -DriveLetter C -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}" # WIN } else { $winPartition = New-Partition -DiskNumber $bootDiskNumber -UseMaximumSize -DriveLetter C -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}" # WIN } if (-not $winPartition) { throw 'Unable to create partition for physical drive OS Installation.' } } else { "Create partitions for Boot From Virtual Disk scenario." | Add-Content $LogPath $espPartition = New-Partition -DiskNumber $bootDiskNumber -Size 200MB -GptType "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" # ESP $espPartition | Add-PartitionAccessPath -AccessPath Q: $null = format Q: /fs:FAT32 /v:EFS /Y } if((!$IsOneDrive) -Or (!$BootFromPhysicalDisk)) { $osPartition = New-Partition -DiskNumber $bootDiskNumber -UseMaximumSize -DriveLetter D -GptType "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}" # OS if (-not $osPartition) { throw 'Unable to create the required partitions for OS Installation.' } $osVolume = Format-Volume -Partition $osPartition -FileSystem NTFS -Confirm:$false $systemDrive = $osPartition.DriveLetter + ':' "System drive letter is set to '$($systemDrive)'" | Add-Content $LogPath } if ($null -ne $winPartition) { $null = Format-Volume -Partition $sysPartition -FileSystem FAT32 -Confirm:$false $null = Format-Volume -Partition $winPartition -FileSystem NTFS -Confirm:$false $env:WINDOWS_DRIVE = $winPartition.DriveLetter + ':' "Windows OS drive letter is set to '$($env:WINDOWS_DRIVE)'" | Add-Content $LogPath } } return [string]$systemDrive } function Set-HostVHDBoot { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $BootVHDFilePath, [Parameter(Mandatory=$true)] [string] $UnattendPath, [Parameter(Mandatory=$true)] [string] $DeploymentId, [Parameter(Mandatory=$true)] [string] $SystemDrive, [Parameter(Mandatory=$false)] [string] $MacAddress=$null, [Parameter(Mandatory=$true)] [string] $LogPath, [Parameter(Mandatory=$false)] [switch] $AddDeploymentIdToken, [Parameter(Mandatory=$false)] [string] $HypervisorSchedulerType='Core' ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop Import-Module "$PSScriptRoot\Helpers.psm1" try { "Mounting VHD '$BootVHDFilePath'." | Add-Content $LogPath $null = Mount-DiskImage -ImagePath $BootVHDFilePath $virtualDiskDriveLetter = Get-Disk | Where-Object BusType -like 'File Backed Virtual' | Get-Partition | Where-Object Size -gt 2Gb | ForEach-Object DriveLetter $bootDrive = $virtualDiskDriveLetter + ':\' # Test binary hashes at WinPE 2nd mount start Test-BinaryHash -FileSystemRoot $bootDrive -OutputFileName 'winPE2ndMountStartHash.json' -BaselineFileName 'baselineHash.json' *>&1 | Add-Content $logPath # Workaround for issue where script cannot find drive $null = New-PSDrive -Name $virtualDiskDriveLetter -Root $bootDrive -PSProvider FileSystem if ($AddDeploymentIdToken) { # Add a token file at a predefined location to detect host has booted up. # This will be relevant only in case of one-node stamp when it goes through baremetal deployment using WinPE $tempPath = "$($bootDrive)CloudBuilderTemp" "Inject deploymentId file '$tempPath\$($DeploymentId).txt'." | Add-Content $LogPath $null = New-Item -Path $tempPath -ItemType Directory -Force $null = Set-Content -Path "$tempPath\$($DeploymentId).txt" -Value '' } "Use-WindowsUnattend file '$UnattendPath' for offline values." | Add-Content $LogPath $null = Use-WindowsUnattend -Path $bootDrive -UnattendPath $UnattendPath $unattendDirectory = "$($bootDrive)Windows\Panther\Unattend" "Inject Unattend file '$UnattendPath' to '$unattendDirectory'." | Add-Content $LogPath $null = New-Item -Path $unattendDirectory -ItemType Directory -Force if ($MacAddress) { $unattendContent = Get-Content $unattendPath "Writing MAC Address '$MacAddress' to Unattend file" | Add-Content $LogPath $unattendContent = $unattendContent.Replace('[MacAddress]', $MacAddress.Replace(':','-')) $null = Set-Content -Path "$unattendDirectory\unattend.xml" -Value $unattendContent } else { $null = Copy-Item -Path $unattendPath -Destination "$unattendDirectory\unattend.xml" } $computerName = Get-ComputerNameFromUnattend -UnattendPath "$unattendDirectory\unattend.xml" Set-Debugging -LogPath $LogPath -ComputerName $computerName $peFirmwareType = (Get-ItemProperty HKLM:\SYSTEM\CurrentControlSet\Control).PEFirmwareType # Returns 0x1 if the PC is booted into BIOS mode, or 0x2 if the PC is booted in UEFI mode. $isLegacyBoot = $peFirmwareType -eq 1 if ($isLegacyBoot) { "Set BCD Boot Legacy." | Add-Content $LogPath bcdboot "$($bootDrive)Windows" /s $SystemDrive | Add-Content $LogPath } else { "Set BCD Boot UEFI." | Add-Content $LogPath bcdboot "$($bootDrive)Windows" /s Q: /f UEFI /d /addlast /v | Add-Content $LogPath # Remove invalid Windows Boot Manager entries, left from the previous deployment. $bcdFirmware = bcdedit /enum firmware $bcdFirmware | Add-Content $LogPath $bcdFirmware = $bcdFirmware -join "`n" if ($bcdFirmware -match 'identifier\s*({\w*-[0-9a-z-]*})[^-]*?description\s*Windows Boot Manager') { for ($i = 0; $i -lt $matches.Count; $i++) { if ($matches[$i] -like '{*') { bcdedit /delete $matches[$i] } } } bcdedit /enum firmware | Add-Content $LogPath } # https://docs.microsoft.com/en-us/windows-server/virtualization/hyper-v/manage/manage-hyper-v-scheduler-types "Setting hypervisor scheduler type to $HypervisorSchedulerType" | Add-Content $LogPath $bcdOutput = & bcdedit /set `{default`} hypervisorschedulertype $HypervisorSchedulerType | Out-String if (-not ($bcdOutput -ilike "*successfully*")) { throw "BCDEdit failed to update the hypervisor scheduler type. Output: $bcdOutput" } # Output boot store entries to validate settings $bcdEditEnum = & bcdedit /enum | Out-String $bcdEdit | Add-Content $LogPath } finally { $mountedImages = Get-DiskImage -ImagePath $BootVHDFilePath if ($mountedImages) { # Test binary hashes at WinPE 2nd mount end Test-BinaryHash -FileSystemRoot $bootDrive -OutputFileName 'winPE2ndMountEndHash.json' -BaselineFileName 'baselineHash.json' *>&1 | Add-Content $logPath $null = Dismount-DiskImage -ImagePath $BootVHDFilePath } } } function Set-HostPhysicalDiskBoot { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $SystemDrive, [Parameter(Mandatory=$true)] [string] $UnattendPath, [Parameter(Mandatory=$true)] [string] $DeploymentId, [Parameter(Mandatory=$false)] [string] $MacAddress=$null, [Parameter(Mandatory=$true)] [string] $LogPath, [Parameter(Mandatory=$false)] [switch] $AddDeploymentIdToken, [Parameter(Mandatory=$false)] [string] $HypervisorSchedulerType='Core' ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop try { $bootDrive = $SystemDrive if ($AddDeploymentIdToken) { # Add a token file at a predefined location to detect host has booted up. # This will be relevant only in case of one-node stamp when it goes through baremetal deployment using WinPE $tempPath = "$($bootDrive)CloudBuilderTemp" "Inject deploymentId file '$tempPath\$($DeploymentId).txt'." | Add-Content $LogPath $null = New-Item -Path $tempPath -ItemType Directory -Force $null = Set-Content -Path "$tempPath\$($DeploymentId).txt" -Value '' } "Use-WindowsUnattend file '$UnattendPath' for offline values." | Add-Content $LogPath $null = Use-WindowsUnattend -Path $bootDrive -UnattendPath $UnattendPath $unattendDirectory = "$($bootDrive)Windows\Panther\Unattend" "Inject Unattend file '$UnattendPath' to '$unattendDirectory'." | Add-Content $LogPath $null = New-Item -Path $unattendDirectory -ItemType Directory -Force if ($MacAddress) { $unattendContent = Get-Content $unattendPath "Writing MAC Address '$MacAddress' to Unattend file" | Add-Content $LogPath $unattendContent = $unattendContent.Replace('[MacAddress]', $MacAddress.Replace(':','-')) $null = Set-Content -Path "$unattendDirectory\unattend.xml" -Value $unattendContent } else { $null = Copy-Item -Path $unattendPath -Destination "$unattendDirectory\unattend.xml" } $computerName = Get-ComputerNameFromUnattend -UnattendPath "$unattendDirectory\unattend.xml" Set-Debugging -LogPath $LogPath -ComputerName $computerName $peFirmwareType = (Get-ItemProperty HKLM:\SYSTEM\CurrentControlSet\Control).PEFirmwareType # Returns 0x1 if the PC is booted into BIOS mode, or 0x2 if the PC is booted in UEFI mode. $isLegacyBoot = $peFirmwareType -eq 1 if ($isLegacyBoot) { "Set BCD Boot Legacy." | Add-Content $LogPath bcdboot "$($bootDrive)\Windows" /s $bootDrive | Add-Content $LogPath } else { "Set BCD Boot UEFI." | Add-Content $LogPath bcdboot "$($bootDrive)\Windows" /s E: /f UEFI /v | Add-Content $LogPath bcdedit /enum firmware | Add-Content $LogPath } # https://docs.microsoft.com/en-us/windows-server/virtualization/hyper-v/manage/manage-hyper-v-scheduler-types "Setting hypervisor scheduler type to $HypervisorSchedulerType" | Add-Content $LogPath $bcdOutput = & bcdedit /set `{default`} hypervisorschedulertype $HypervisorSchedulerType | Out-String if (-not ($bcdOutput -ilike "*successfully*")) { throw "BCDEdit failed to update the hypervisor scheduler type. Output: $bcdOutput" } # Output boot store entries to validate settings $bcdEditEnum = & bcdedit /enum | Out-String $bcdEdit | Add-Content $LogPath } catch { throw $PSItem } } function Get-ComputerNameFromUnattend { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $UnattendPath ) # Get host name from unattend.xml $computerNameSearch = Get-ChildItem -Path $unattendPath | Select-String '\<ComputerName\>([^<]*)\<' $computerName = $computernameSearch.Matches.Groups[1].Value return $computerName } <# .Synopsis Function to test if an IP address is between two other IP addresses, inclusive .Parameter IPAddress The address to test. .Parameter BeginAddress The IP address at the begining of the range to test. .Parameter EndAddress The IP address at the begining of the range to test. .Example Test-IpAddressBetween -IPAddress 10.0.0.5 -BeginAddress 10.0.0.1 -EndAddress 10.0.0.10 Returns True #> function Test-IpAddressBetween { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [String] $IPAddress, [Parameter(Mandatory=$true)] [String] $BeginAddress, [Parameter(Mandatory=$true)] [String] $EndAddress ) # Convert all the addresses to Int32 $originalAddressBytes = ([IPAddress]$IPAddress).GetAddressBytes() [array]::Reverse($originalAddressBytes) $ipAddressInt = [System.BitConverter]::ToInt32($originalAddressBytes, 0) $originalAddressBytes = ([IPAddress]$BeginAddress).GetAddressBytes() [array]::Reverse($originalAddressBytes) $beginAddressInt = [System.BitConverter]::ToInt32($originalAddressBytes, 0) $originalAddressBytes = ([IPAddress]$EndAddress).GetAddressBytes() [array]::Reverse($originalAddressBytes) $endAddressInt = [System.BitConverter]::ToInt32($originalAddressBytes, 0) $ipAddressInt -le $endAddressInt -and $ipAddressInt -ge $beginAddressInt } function Get-LogFilePath { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $UnattendPath ) $computerName = Get-ComputerNameFromUnattend -UnattendPath $UnattendPath $logPath = [System.IO.Path]::GetDirectoryName($UnattendPath) $logFilePath = "$($logPath)\$($computerName).Log" return $logFilePath } function Set-Debugging { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $LogPath, [Parameter(Mandatory=$true)] [string] $ComputerName ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop if ($ENABLE_DEBUGGING) { Add-Content $LogPath "Current BCD settings:" bcdedit /enum | Add-Content $LogPath Add-Content $LogPath "Enabling debugging and setting dbgsettings..." bcdedit /debug '{default}' on | Add-Content $LogPath bcdedit /bootdebug '{default}' on | Add-Content $LogPath if ($DEBUG_CONNECTION_TYPE -eq 'serial') { bcdedit /dbgsettings serial debugport:$DEBUG_SERIAL_PORT baudrate:$DEBUG_SERIAL_BAUD_RATE | Add-Content $LogPath } elseif ($DEBUG_CONNECTION_TYPE -eq 'net') { $portMap = @{} $DEBUG_NET_PORT_MAP_STRING -split ';' | Where-Object { $_ } | ForEach-Object { $portRecord = $_ -split ',' $portMap.($portRecord[0]) = $portRecord[1] } $port = $portMap.$ComputerName Add-Content $LogPath "Port: $port" bcdedit /dbgsettings net hostip:$DEBUG_NET_HOST_IP port:$port key:$DEBUG_NET_KEY | Add-Content $LogPath bcdedit /set '{dbgsettings}' busparams "$DEBUG_NET_BUS_PARAMS" | Add-Content $LogPath } else { Add-Content $LogPath "Debugging connection type '$DEBUG_CONNECTION_TYPE' is not expected." } Add-Content $LogPath "Debug settings are now set:" bcdedit /enum | Add-Content $LogPath bcdedit /dbgsettings | Add-Content $LogPath } if ($ENABLE_SERIAL_CONSOLE) { Add-Content $LogPath "Current BCD settings:" bcdedit /enum | Add-Content $LogPath Add-Content $LogPath "Enabling Emergency Management Services..." bcdedit /ems '{default}' on | Add-Content $LogPath bcdedit /emssettings emsport:$CONSOLE_SERIAL_PORT emsbaudrate:$CONSOLE_SERIAL_BAUD_RATE | Add-Content $LogPath Add-Content $LogPath "EMS settings are now set:" bcdedit /enum | Add-Content $LogPath } } <# .Synopsis Function to get the local machine System Management BIOS Guid without dashes or brackets .Example Get-LocalSMBIOSGuid Returns 1742B000CD3611E10000AC162D024F2F #> function Get-LocalSMBIOSGuid { $smBiosGuid = Get-WmiObject Win32_ComputerSystemProduct UUID $smBiosGuid.UUID.Replace('-', '') } <# .Synopsis Function to get a path to the file that will contain the MAC address on a deploying machine .Example Get-MACAddressOutputPath -RemoteUnattendPath \\foo\bar Returns \\foo\bar\1742B000CD3611E10000AC162D024F2F.MACAddress.txt #> function Get-MACAddressOutputPath { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $RemoteUnattendPath ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop # Always use the smbios guid, since this is an externally discoverable identifier $smBiosGuid = Get-LocalSMBIOSGuid $macAddressOutputFilePath = $smBiosGuid | ForEach-Object { "$RemoteUnattendPath\$_.MACAddress.txt" } return $macAddressOutputFilePath } function Get-UnattendFilePath { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $RemoteUnattendPath ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop $macAddress = Get-CimInstance Win32_NetworkAdapterConfiguration | Where-Object MACAddress | ForEach-Object MACAddress | ForEach-Object { $_ -replace ':', '-' } | Where-Object { "$remoteUnattendPath\$_.xml" | Where-Object {Test-Path $_} } if ($macAddress) { $unattendPath = $macAddress | ForEach-Object { "$RemoteUnattendPath\$_.xml" } } else { # No unattend file matching the MAC address was found, attempt to find an unattend file with the smbios guid instead $smBiosGuid = Get-LocalSMBIOSGuid $unattendPath = $smBiosGuid | ForEach-Object { "$RemoteUnattendPath\$_.xml" } } return $unattendPath } <# .Synopsis This function returns the node specific RemoteUnattend path e.g. when passed RemoteUnattend, we will get back RemoteUnattend\<NodeName>. If node's unattend files are in RemoteUnattend *and* RemoteUnattend\NodeName, we will return the path containing the latest files. This scenario can happen during the following sequence Old build + FRU X (writes to RemoteUnattend) + PnU (new build that writes to NodeName) + FRU X during PnU #> function Get-NodeSpecificRemoteUnattendPath { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $UnattendPathToResolve ) $macAddresses = Get-CimInstance Win32_NetworkAdapterConfiguration | Where-Object MACAddress | ForEach-Object MACAddress | ForEach-Object { $_ -replace ':', '-' } foreach ($macAddress in $macAddresses) { $unattendFile = Get-ChildItem -Path $UnattendPathToResolve -Include "$macAddress.xml" -Recurse -Force -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($unattendFile) { return $unattendFile.Directory.FullName } } $smBiosGuid = Get-LocalSMBIOSGuid $unattendFile = Get-ChildItem -Path $UnattendPathToResolve -Include "$smBiosGuid.xml" -Recurse -Force -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($unattendFile) { return $unattendFile.Directory.FullName } return $UnattendPathToResolve } function Get-BootDiskConfigPath { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $RemoteUnattendPath ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop $macAddress = Get-CimInstance Win32_NetworkAdapterConfiguration | Where-Object MACAddress | ForEach-Object MACAddress | ForEach-Object { $_ -replace ':', '-' } | Where-Object { "$remoteUnattendPath\$_.BootDisk.xml" | Where-Object {Test-Path $_} } if ($macAddress) { $bootDiskConfigPath = $macAddress | ForEach-Object { "$RemoteUnattendPath\$_.BootDisk.xml" } } else { # No boot disk file matching the MAC address was found, attempt to find a boot disk file with the smbios guid instead $smBiosGuid = Get-LocalSMBIOSGuid $bootDiskConfigPath = $smBiosGuid | ForEach-Object { "$RemoteUnattendPath\$_.BootDisk.xml" } } return $bootDiskConfigPath } function Test-AzsHostTPMOwnership { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $LogPath ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop "Testing TPM ownership for AzureStack host" | Add-Content $LogPath try { "Getting Win32_TPM object in namespace root\cimv2\security\microsofttpm" | Add-Content $LogPath $tpmWMIObject = Get-WmiObject -Namespace "root\cimv2\security\microsofttpm" -Class Win32_TPM if ($null -eq $tpmWMIObject) { throw "Failed to get WMI object Win32_TPM" } "Calling TPM method IsOwned() to check if TPM is properly owned" | Add-Content $LogPath $tpmIsOwnedRet = $tpmWMIObject.IsOwned() if ($null -eq $tpmIsOwnedRet) { throw "Failed call to tpm method IsOwned()" } if ($tpmIsOwnedRet.ReturnValue -ne 0) { throw "Failed call to tpm method IsOwned(). ReturnValue is $($tpmIsOwnedRet.ReturnValue)" } if ($tpmIsOwnedRet.IsOwned -eq $true) { "TPM IsOwned() method confirmed that TPM is owned. No need to explicitly call TPM method TakeOwnerShip()" | Add-Content $LogPath return } "TPM is NOT owned. Will explicitly call TPM method TakeOwnerShip()" | Add-Content $LogPath "Calling TPM method TakeOwnerShip()" | Add-Content $LogPath $tpmTakeOwnerShipRet = $tpmWMIObject.TakeOwnerShip() if ($null -eq $tpmTakeOwnerShipRet) { throw "Failed call to TPM method TakeOwnerShip()" } if ($tpmTakeOwnerShipRet.ReturnValue -ne 0) { throw "Failed call to TPM method TakeOwnerShip(). ReturnValue is $($tpmTakeOwnerShipRet.ReturnValue)" } "Calling TPM method IsOwned() to check if TPM is properly owned after calling TPM method TakeOwnerShip()" | Add-Content $LogPath $tpmIsOwnedRet = $tpmWMIObject.IsOwned() if ($null -eq $tpmIsOwnedRet) { throw "Failed call to tpm method IsOwned()" } if ($tpmIsOwnedRet.ReturnValue -ne 0) { throw "Failed call to tpm method IsOwned(). ReturnValue is $($tpmIsOwnedRet.ReturnValue)" } if ($tpmIsOwnedRet.IsOwned -eq $false) { throw "Failure: TPM method IsOwned() shows that TPM is NOT owned even after calling TPM method TakeOwnerShip()" } "TPM is now owned after calling TPM method TakeOwnerShip()" | Add-Content $LogPath } catch { "Azs host test TPM ownership did not complete. Exception: $($_.Exception)" | Add-Content $LogPath "Test TPM ownership did not complete becaue of the following error/warning: $($_.Exception.Message)" | Add-Content $LogPath } } # Function Clear-AzsHostTPM # It is a precautionary mechanism to avoid issues with # TPM. If TPM could not be cleared # an error will be reported but deployment # should proceed. A common reason for TPM not to be cleared # is a host that requies user confirmation upon reboot. # In this case an error will be reported but deployment # should proceed function Clear-AzsHostTPM { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $LogPath ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop (Get-Date).ToString('yyyy/MM/dd HH:mm:ss') | Add-Content $LogPath "Clear TPM for AzureStack host" | Add-Content $LogPath try { "Retrieving a WMI object win32_TPM in namespace root\cimv2\security\microsofttpm" | Add-Content $LogPath $tpmWMIObject = Get-WmiObject -Namespace "root\cimv2\security\microsofttpm" -Class Win32_TPM if ($null -eq $tpmWMIObject) { throw "Aborting clear TPM. Failed to retrieve TPM WMI object. Make sure TPM is available and ready." } "Checking status of TPM" | Add-Content $LogPath if ((!$tpmWMIObject.IsActivated_InitialValue) -or (!$tpmWMIObject.IsEnabled_InitialValue)) { throw "Aborting clear TPM. TPM is either not activated or is not enabled" } "Testing TPM ownership. Calling Test-AzsHostTPMOwnership" | Add-Content $LogPath Test-AzsHostTPMOwnership -LogPath $LogPath "Retrieving a new WMI win32_TPM object after testing ownership in namespace root\cimv2\security\microsofttpm" | Add-Content $LogPath $tpmWMIObject = Get-WmiObject -Namespace "root\cimv2\security\microsofttpm" -Class Win32_TPM if ($null -eq $tpmWMIObject) { throw "Aborting clear TPM. Failed to retrieve TPM WMI object. Make sure TPM is available and ready." } "Starting process to clear TPM" | Add-Content $LogPath "Calling WMI TPM method GetPhysicalPresenceConfirmationStatus() with Operation input parameter set to 22 (Enable + Activate + Clear + Enable + Activate)" | Add-Content $LogPath $physicalPresenceConfRet = $tpmWMIObject.GetPhysicalPresenceConfirmationStatus(22) if ($null -eq $physicalPresenceConfRet) { throw "Aborting clear TPM. Failed call to WMI TPM method GetPhysicalPresenceConfirmationStatus()" } "Checking ReturnValue of WMI TPM method GetPhysicalPresenceConfirmationStatus()" | Add-Content $LogPath "ReturnValue of WMI TPM method GetPhysicalPresenceConfirmationStatus() is $($physicalPresenceConfRet.ReturnValue)" | Add-Content $LogPath if ($physicalPresenceConfRet.ReturnValue -ne 0) { throw "Aborting clear TPM. Failed call to TPM method GetPhysicalPresenceConfirmationStatus() with ReturnValue $($physicalPresenceConfRet.ReturnValue)" } "Checking ConfirmationStatus of WMI TPM method GetPhysicalPresenceConfirmationStatus()" | Add-Content $LogPath "ConfirmationStatus of WMI TPM method GetPhysicalPresenceConfirmationStatus() is $($physicalPresenceConfRet.ConfirmationStatus)" | Add-Content $LogPath if ($physicalPresenceConfRet.ConfirmationStatus -ne 4) { throw "Aborting clear TPM. Call to WMI TPM method GetPhysicalPresenceConfirmationStatus() returned ConfirmationStatus $($physicalPresenceConfRet.ConfirmationStatus). Host may require user intervention or BIOS change to enable clear TPM." } "Clearing TPM by calling WMI TPM method SetPhysicalPresenceRequest() with request input parameter set to 22 (Enable + Activate + Clear + Enable + Activate)" | Add-Content $LogPath $physicalPresenceRequestRet = $tpmWMIObject.SetPhysicalPresenceRequest(22) if ($null -eq $physicalPresenceRequestRet) { throw "Aborting clear TPM. Failed call to WMI TPM method SetPhysicalPresenceRequest()" } "Checking ReturnValue of WMI TPM method SetPhysicalPresenceRequest()" | Add-Content $LogPath "ReturnValue of WMI TPM method SetPhysicalPresenceRequest() is $($physicalPresenceRequestRet.ReturnValue)" | Add-Content $LogPath if ($physicalPresenceRequestRet.ReturnValue -ne 0) { throw "Aborting clear TPM. Failed call to WMI TPM method SetPhysicalPresenceRequest() with ReturnValue $($physicalPresenceRequestRet.ReturnValue)" } "Clearing host TPM configuration has completed. Changes will be applied after reboot" | Add-Content $LogPath } catch { "Azs host clear TPM did not complete. Exception: $($_.Exception)" | Add-Content $LogPath "Clear TPM did not complete becaue of the following error/warning: $($_.Exception.Message)" | Add-Content $LogPath } } Export-ModuleMember -Function Get-BootDiskConfigPath Export-ModuleMember -Function Get-ComputerNameFromUnattend Export-ModuleMember -Function Get-LocalSMBIOSGuid Export-ModuleMember -Function Get-LogFilePath Export-ModuleMember -Function Get-MACAddressOutputPath Export-ModuleMember -Function Get-UnattendFilePath Export-ModuleMember -Function Get-NodeSpecificRemoteUnattendPath Export-ModuleMember -Function New-NetworkDrive Export-ModuleMember -Function Set-Debugging Export-ModuleMember -Function Set-DiskConfiguration Export-ModuleMember -Function Set-HostVHDBoot Export-ModuleMember -Function Set-HostPhysicalDiskBoot Export-ModuleMember -Function Set-WinPEDeploymentPrerequisites Export-ModuleMember -Function Test-IpAddressBetween Export-ModuleMember -Function Clear-AzsHostTPM # SIG # Begin signature block # MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCjSsh3XX2NmiqH # ZHpJbsEyFTBD+hqgpr0KN2JaPsNKlaCCDXYwggX0MIID3KADAgECAhMzAAADrzBA # DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA # hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG # 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN # xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL # go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB # tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd # mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ # 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY # 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp # XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn # TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT # e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG # OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O # PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk # ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx # HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt # CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 # IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg # Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC # CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03 # a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr # rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg # OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy # 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9 # sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh # dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k # A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB # w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn # Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90 # lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w # ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o # ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD # VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa # BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny # bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG # AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV # HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG # AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl # AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb # C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l # hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6 # I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0 # wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560 # STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam # ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa # J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah # XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA # 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt # Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr # /Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIPOQujwPOPQ34E2oN8KRwLGG # CSx4+Yp/sBD/e62FgMTMMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAUhN0ErXj6Gqf8AxtFnDL13O0rRHPebTwBpkbSTuMKGLESootWGM3AyHN # fxWHotgDrk/jbdn9v/pWtidzrt5RNkYnjv7bqrBvaFGGdYd7HrdeLoTe78eVjN8m # xAdiR9hkIbSI0Zw0/nFpUZ7CeYLuaPFHcSgnvD5M+DNqtHCvyS+mS1HH+ULhdDEH # qpKU8y0oKvYVVp/hdhuYGbexb0AdFsxhXTojgT5pXTQ4LdMiB6/wY2twLRDBEn2x # pov+TPORDT6Vg0nBvLJpyyYxjI+ze84S4iV9RQWGAAPE+ng8bpp9QjhS3HNEeDhK # P9f5g5Sb5NuZ/LH6uTwPk9INuVh/XaGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC # F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq # hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCBjwfSYPBDeQpVlOrwKXGZs44w4OEckGsr/wnUh/3G80gIGZpVeHW9e # GBMyMDI0MDcyMzExMDMzMC45OTNaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l # cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046QTAwMC0w # NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg # ghHqMIIHIDCCBQigAwIBAgITMwAAAevgGGy1tu847QABAAAB6zANBgkqhkiG9w0B # AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzEyMDYxODQ1 # MzRaFw0yNTAzMDUxODQ1MzRaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz # aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z # MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046QTAwMC0wNUUwLUQ5NDcxJTAjBgNV # BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQDBFWgh2lbgV3eJp01oqiaFBuYbNc7hSKmktvJ15NrB # /DBboUow8WPOTPxbn7gcmIOGmwJkd+TyFx7KOnzrxnoB3huvv91fZuUugIsKTnAv # g2BU/nfN7Zzn9Kk1mpuJ27S6xUDH4odFiX51ICcKl6EG4cxKgcDAinihT8xroJWV # ATL7p8bbfnwsc1pihZmcvIuYGnb1TY9tnpdChWr9EARuCo3TiRGjM2Lp4piT2lD5 # hnd3VaGTepNqyakpkCGV0+cK8Vu/HkIZdvy+z5EL3ojTdFLL5vJ9IAogWf3XAu3d # 7SpFaaoeix0e1q55AD94ZwDP+izqLadsBR3tzjq2RfrCNL+Tmi/jalRto/J6bh4f # PhHETnDC78T1yfXUQdGtmJ/utI/ANxi7HV8gAPzid9TYjMPbYqG8y5xz+gI/SFyj # +aKtHHWmKzEXPttXzAcexJ1EH7wbuiVk3sErPK9MLg1Xb6hM5HIWA0jEAZhKEyd5 # hH2XMibzakbp2s2EJQWasQc4DMaF1EsQ1CzgClDYIYG6rUhudfI7k8L9KKCEufRb # K5ldRYNAqddr/ySJfuZv3PS3+vtD6X6q1H4UOmjDKdjoW3qs7JRMZmH9fkFkMzb6 # YSzr6eX1LoYm3PrO1Jea43SYzlB3Tz84OvuVSV7NcidVtNqiZeWWpVjfavR+Jj/J # OQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFHSeBazWVcxu4qT9O5jT2B+qAerhMB8G # A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG # Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy # MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w # XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy # dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG # A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD # AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQCDdN8voPd8C+VWZP3+W87c/QbdbWK0sOt9 # Z4kEOWng7Kmh+WD2LnPJTJKIEaxniOct9wMgJ8yQywR8WHgDOvbwqdqsLUaM4Nre # rtI6FI9rhjheaKxNNnBZzHZLDwlkL9vCEDe9Rc0dGSVd5Bg3CWknV3uvVau14F55 # ESTWIBNaQS9Cpo2Opz3cRgAYVfaLFGbArNcRvSWvSUbeI2IDqRxC4xBbRiNQ+1qH # XDCPn0hGsXfL+ynDZncCfszNrlgZT24XghvTzYMHcXioLVYo/2Hkyow6dI7uULJb # KxLX8wHhsiwriXIDCnjLVsG0E5bR82QgcseEhxbU2d1RVHcQtkUE7W9zxZqZ6/jP # maojZgXQO33XjxOHYYVa/BXcIuu8SMzPjjAAbujwTawpazLBv997LRB0ZObNckJY # yQQpETSflN36jW+z7R/nGyJqRZ3HtZ1lXW1f6zECAeP+9dy6nmcCrVcOqbQHX7Zr # 8WPcghHJAADlm5ExPh5xi1tNRk+i6F2a9SpTeQnZXP50w+JoTxISQq7vBij2nitA # sSLaVeMqoPi+NXlTUNZ2NdtbFr6Iir9ZK9ufaz3FxfvDZo365vLOozmQOe/Z+pu4 # vY5zPmtNiVIcQnFy7JZOiZVDI5bIdwQRai2quHKJ6ltUdsi3HjNnieuE72fT4eWh # xtmnN5HYCDCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI # 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 # MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOkEwMDAtMDVFMC1EOTQ3MSUwIwYDVQQD # ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQCA # Bol1u1wwwYgUtUowMnqYvbul3qCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6km/OTAiGA8yMDI0MDcyMzA1MzM0 # NVoYDzIwMjQwNzI0MDUzMzQ1WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDqSb85 # AgEAMAcCAQACAg7YMAcCAQACAhIzMAoCBQDqSxC5AgEAMDYGCisGAQQBhFkKBAIx # KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI # hvcNAQELBQADggEBAIfsFSixvoWQq6ZNdQ7HFZ989N2uUL4TlwI7Xfmoilh5REDz # Cqzlm7AgRfdyrjkDvNa5v8Gj+mTkKV+tpj6QW1MWWJSWrJffC7pKtHFiGNd6iLjw # 4p6E1K8PYZWK5cmhin//1WsBnwcheWQnHvBmw5IMHfVjoCCqXL9NUeLq6WeYHsC7 # tXAYDmCXV0NVNcTEwdGE6AlhaAe8cyC0rL36wa3In1JEAwbzb8H/BCB2i+NCS2Rf # qLuwJRZ2j7IQghujz16I8Y6/hQJ7OyAprEKG28bfmHiiRZiIoBkQFqoQW8BuIooW # TgdIw73K36oFl/7lxLAPi7qksXFbAbHHl+RUDN0xggQNMIIECQIBATCBkzB8MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy # b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAevgGGy1tu847QABAAAB6zAN # BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G # CSqGSIb3DQEJBDEiBCBKPTXEG39vUkgq+b675g4oUMfndgX7jpcfnLKB0zcafjCB # +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIM63a75faQPhf8SBDTtk2DSUgIbd # izXsz76h1JdhLCz4MIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh # c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw # MTACEzMAAAHr4BhstbbvOO0AAQAAAeswIgQgMFr7sEQ98XQXSx82avgA7yFETzFD # ShP7AykyP1AKou4wDQYJKoZIhvcNAQELBQAEggIANcRVXMQwUVqWknDdd6X3+MQQ # 52CJwuK/rZYDfPog+8eJ90CKUZhDZpHDjl/lpRj2kDXCwKxmyZIS8Zpz/czXSldb # HMZH78W+sbKSjDbtvbfcrc+FMH+vWJ6DAd6zlyiTKZ35Vi3YJi+itkksWIUDAcUP # l/0Kmqe0du285E0W4DSPpZ+yuetEQpTZV5B07y/TQXSEUokBcS5Z4uHlWP8lUuP1 # klbC/W85YKsoVb+KTt+7/fPiZFBPe4dJ4QanKOaLSqIhYjwO4703Ij2l3oPSSLwf # BgFkg1W/VD/cG+LrgOY1Y+BhYC9NCnLpF/DtLUYXJqnQCsHXk5y5ZWCk41+jrwuM # mtJ+f7iATbFHl3S1th5i23E+CEADkD29QQUdqW3d2WWayts4TgXamXK20XwRav6B # uXAXD2ZFRK3p+0sdIcGIq60ETSWdcN2fSSBrPYSojzCJE6KNWXZvgcMw+I41VLpT # YbBwKb7I6HRdvWcwXY6PAJjzdfSVs29pCIoCSrNmoY8/cPZyhwL7cNHs9bihYDV7 # JcMzF542WK7dHLGfdTfcv37wL5+0QDyn+NufiUONgXvG60OiClg+Mush3rkEvyDi # bHI2ZYWuq1JXJyDU8n7FSgJxToO7SLM5M+LcHSKu5+rAqeYwZCLA+TkDWjV/exsR # 8uyeAZr39KjVnjheqFs= # SIG # End signature block |