daisy_workflows/image_import/windows/translate.ps1 (372 lines of code) (raw):
# Copyright 2017 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
$ErrorActionPreference = 'Stop'
$script:gce_install_dir = 'C:\Program Files\Google\Compute Engine'
$script:hosts_file = "$env:windir\system32\drivers\etc\hosts"
function Run-Command {
[CmdletBinding(SupportsShouldProcess=$true)]
param (
[Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[string]$Executable,
[Parameter(ValueFromRemainingArguments=$true,
ValueFromPipelineByPropertyName=$true)]
$Arguments = $null
)
Write-Output "Running $Executable with arguments $Arguments."
$out = &$executable $arguments 2>&1 | Out-String
$out.Trim()
}
function Get-MetadataValue {
param (
[parameter(Mandatory=$true)]
[string]$key,
[parameter(Mandatory=$false)]
[string]$default
)
# Returns the provided metadata value for a given key.
$url = "http://metadata.google.internal/computeMetadata/v1/instance/attributes/$key"
$max_attemps = 30
for ($i=0; $i -le $max_attemps; $i++) {
try {
$client = New-Object Net.WebClient
$client.Headers.Add('Metadata-Flavor', 'Google')
$value = ($client.DownloadString($url)).Trim()
Write-Host "Retrieved metadata for key $key with value $value."
return $value
}
catch [System.Net.WebException] {
if ($default) {
Write-Host "Failed to retrieve metadata for $key, returning default $default."
return $default
}
# Sleep after each failure with no default value to give the network adapters time to become functional.
Start-Sleep -s 1
}
}
Write-Host "Failed $max_attemps times to retrieve value from metadata for $key, returning null."
return $null
}
function Wait-Service {
<#
.SYNOPSIS
Wait until the service is not in a pending state.
.DESCRIPTION
This function waits until the specified service is not in a pending state
as you cannot start or stop a service in a pending state.
.PARAMETER ServiceName
The name of the service to check the state of.
#>
param (
[parameter(Mandatory=$true)]
[string]$ServiceName
)
#
while((Get-Service -Name $ServiceName).Status -match 'pending') {
Write-Host "$ServiceName service is in a pending state, waiting 1 second."
Start-Sleep -s 1
}
}
function Remove-VMWareTools {
Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall | Foreach-Object {
if ((Get-ItemProperty $_.PSPath).DisplayName -eq 'VMWare Tools') {
$log_file = [System.IO.Path]::GetTempFileName()
Write-Output 'Translate: Found VMWare Tools installed, removing...'
$process = Start-Process msiexec.exe -ArgumentList @('/x', $_.PSChildName, '/quiet', '/norestart', '/l*V', $log_file) -Wait -PassThru
# There are three sucessful return codes for msiexec:
# https://docs.microsoft.com/en-us/windows/win32/msi/error-codes
# 0 - ERROR_SUCCESS
# 1641 - ERROR_SUCCESS_REBOOT_INITIATED
# 3010 - ERROR_SUCCESS_REBOOT_REQUIRED
if (($process.ExitCode -eq 0) -or ($process.ExitCode -eq 1641) -or ($process.ExitCode -eq 3010)) {
Restart-Computer -Force
exit 0
} else {
Write-Output 'Translate: Unable to remove VMWare Tools. Consider manually removing.'
Get-Content $log_file
}
}
}
}
function Setup-NTP {
Write-Output 'Translate: Setting up NTP.'
# Set the CMOS clock to use UTC.
$tzi_path = 'HKLM:\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
Set-ItemProperty -Path $tzi_path -Name RealTimeIsUniversal -Value 1
# Set up time sync...
Wait-Service W32time
# Stop in case it's running; it probably won't be.
Stop-Service W32time
$w32tm = "$env:windir\System32\w32tm.exe"
# Get time from GCE NTP server every 15 minutes.
Run-Command $w32tm /config '/manualpeerlist:metadata.google.internal,0x1' /syncfromflags:manual
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpClient' `
-Name SpecialPollInterval -Value 900
# Set in Control Panel -- Append to end of list, set default.
$server_key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DateTime\Servers'
$server_item = Get-Item $server_key
$server_num = ($server_item.GetValueNames() | Measure-Object -Maximum).Maximum + 1
Set-ItemProperty -Path $server_key -Name $server_num -Value 'metadata.google.internal'
Set-ItemProperty -Path $server_key -Name '(Default)' -Value $server_num
# Configure to run automatically on every start.
Set-Service W32Time -StartupType Automatic
Run-Command $env:windir\system32\sc.exe triggerinfo w32time start/networkon
Write-Output 'Configured W32Time to use GCE NTP server.'
Wait-Service W32time
Start-Service W32time
# Sync time now.
Run-Command $w32tm /resync
}
function Configure-Network {
Write-Output 'Translate: Configuring network.'
# Register netkvmco.dll.
Run-Command rundll32 'netkvmco.dll,RegisterNetKVMNetShHelper'
# Make sure metadata server is in etc/hosts file, -Force in case the host file is read-only.
Add-Content -Force -Path $script:hosts_file -value @'
# Google Compute Engine metadata server
169.254.169.254 metadata.google.internal metadata
'@
# Change KeepAliveTime to 5 minutes.
$tcp_params = 'HKLM:\System\CurrentControlSet\Services\Tcpip\Parameters'
New-ItemProperty -Path $tcp_params -Name 'KeepAliveTime' -Value 300000 -PropertyType DWord -Force
# Disable IPv6
Write-Output 'Disabling IPv6.'
$ipv_path = 'HKLM:\SYSTEM\CurrentControlSet\services\TCPIP6\Parameters'
Set-ItemProperty -Path $ipv_path -Name 'DisabledComponents' -Value 0xFF
Write-Output 'Disabling WPAD.'
# Mount default user registry hive at HKLM:\DefaultUser.
Run-Command reg load 'HKLM\DefaultUser' 'C:\Users\Default\NTUSER.DAT'
# Loop over default user and current (SYSTEM) user.
foreach ($reg_base in 'HKLM\DefaultUser', 'HKCU') {
# Disable Web Proxy Auto Discovery.
$WPAD = "$reg_base\Software\Microsoft\Windows\CurrentVersion\Internet Settings"
# Make change with reg add, because it will work with the mounted hive and
# because it will recursively add any necessary subkeys.
Run-Command reg add $WPAD /v AutoDetect /t REG_DWORD /d 0 /f
}
# Delay to ensure registry changes are completed.
Start-Sleep -s 5
# Unmount default user hive.
Run-Command reg unload 'HKLM\DefaultUser'
$netkvm = Get-WMIObject Win32_NetworkAdapter -filter "ServiceName='netkvm'"
$netkvm | ForEach-Object {
Run-Command netsh interface ipv4 set interface "$($_.NetConnectionID)" mtu=1460 | Out-Null
}
Write-Output 'MTU set to 1460.'
Run-Command route /p add 169.254.169.254 mask 255.255.255.255 0.0.0.0 if $netkvm[0].InterfaceIndex metric 1 -ErrorAction SilentlyContinue
Write-Output 'Added persistent route to metadata netblock via first netkvm adapter.'
}
function Configure-Power {
# Change power configuration to never turn off monitor. If Windows turns
# off its monitor, it will respond to power button pushes by turning it back
# on instead of shutting down as GCE expects. We fix this by switching the
# "Turn off display after" setting to 0 for all power configurations.
#
# Use WMI since Cim fails on Windows 7 and 2008r2.
try {
$pplan = Get-WMIObject `
-Namespace 'root\cimv2\power' `
-Class Win32_PowerSetting `
-ErrorAction SilentlyContinue `
| where {$_.ElementName -eq 'Turn off display after'}
} catch {
Write-Output "Skipping Configure-Power; Get-WMIObject Win32_PowerSetting failed: " + $_
return
}
$pplan | ForEach-Object {
$_.GetRelated('Win32_PowerSettingDataIndex') | ForEach-Object {
if ($_.SettingIndexValue -ne 0) {
Write-Output ('Disabling "turn off display after"; current value: ' + $_.SettingIndexValue)
$_ | Set-WmiInstance -Arguments @{SettingIndexValue = 0}
}
}
}
}
function Change-InstanceProperties {
Write-Output 'Translate: Setting instance properties.'
# Enable EMS.
Run-Command bcdedit /emssettings EMSPORT:2 EMSBAUDRATE:115200
Run-Command bcdedit /ems on
# Ignore boot failures.
Run-Command bcdedit /set '{current}' bootstatuspolicy ignoreallfailures
Write-Output 'bcdedit option set.'
# Registry fix for PD cluster size issue.
$vioscsi_path = 'HKLM:\SYSTEM\CurrentControlSet\Services\vioscsi\Parameters\Device'
New-Item -Path $vioscsi_path -Force
New-ItemProperty -Path $vioscsi_path -Name EnableQueryAccessAlignment -Value 1 -PropertyType DWord -Force
# Change SanPolicy. Setting is persistent even after sysprep. This helps in
# ensuring all attached disks are online when instance is built.
$san_policy = 'san policy=OnlineAll' | diskpart | Select-String 'San Policy'
Write-Output ($san_policy -replace '(?<=>)\s+(?=<)') # Remove newline and tabs
# Change time zone to Coordinated Universal Time.
Run-Command tzutil /s 'UTC'
# Disable hibernate via the registry, the c:\hibernation.sys if present file will be removed on the next reboot.
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Power" -Name HibernateEnabled -PropertyType DWord -Value 0 -Force
}
function Configure-RDPSecurity {
$registryPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'
# Set minimum encryption level to "High"
New-ItemProperty -Path $registryPath -Name MinEncryptionLevel -Value 3 -PropertyType DWORD -Force
# Specifies that Network-Level user authentication is required.
New-ItemProperty -Path $registryPath -Name UserAuthentication -Value 1 -PropertyType DWORD -Force
# Specifies that the Transport Layer Security (TLS) protocol is used by the server and the client
# for authentication before a remote desktop connection is established.
New-ItemProperty -Path $registryPath -Name SecurityLayer -Value 2 -PropertyType DWORD -Force
}
function Enable-RemoteDesktop {
$ts_path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server'
if (-not (Test-Path $ts_path)) {
return
}
# Enable remote desktop in registry.
Set-ItemProperty -Path $ts_path -Name 'fDenyTSConnections' -Value 0 -Force
Write-Output 'Disabling Ctrl + Alt + Del.'
Set-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'DisableCAD' -Value 1 -Force
Write-Output 'Enable RDP firewall rules.'
Run-Command netsh advfirewall firewall set rule group='@FirewallAPI.dll,-28752' new enable=Yes
}
function Install-Packages {
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install googet
# We always install google-compute-engine-sysprep because it is required for instance activation, it gets removed later
# if install_packages is set to false.
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-compute-engine-sysprep
if ($script:install_packages.ToLower() -eq 'true') {
Write-Output 'Translate: Installing GCE packages...'
# Install each individually in order to catch individual errors
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-compute-engine-windows
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-compute-engine-driver-balloon -ErrorAction SilentlyContinue
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-compute-engine-driver-pvpanic -ErrorAction SilentlyContinue
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-compute-engine-vss -ErrorAction SilentlyContinue
if (([System.Environment]::OSVersion.Version) -ge "6.2") {
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-osconfig-agent
} else {
Write-Output 'Skipping installation of OS Config agent. Requires Windows 2012 or newer.'
}
}
}
function Install-32bitPackages {
& C:\ProgramData\GooGet\googet.exe -root C:\ProgramData\GooGet -noconfirm install C:\ProgramData\GooGet\components\googet-x86.x86_32.2.16.3@1.goo
# We always install google-compute-engine-sysprep because it is required for instance activation, it gets removed later
# if install_packages is set to false.
& C:\ProgramData\GooGet\googet.exe -root C:\ProgramData\GooGet -noconfirm install C:\ProgramData\GooGet\components\google-compute-engine-powershell.noarch.1.1.1@4.goo
& C:\ProgramData\GooGet\googet.exe -root C:\ProgramData\GooGet -noconfirm install C:\ProgramData\GooGet\components\certgen-x86.x86_32.1.0.0@2.goo
& C:\ProgramData\GooGet\googet.exe -root C:\ProgramData\GooGet -noconfirm install C:\ProgramData\GooGet\components\google-compute-engine-sysprep.noarch.3.10.1@1.goo
if (([System.Environment]::OSVersion.Version) -ge "10.0") {
& C:\ProgramData\GooGet\googet.exe -root C:\ProgramData\GooGet -noconfirm install -reinstall C:\ProgramData\GooGet\components\google-compute-engine-metadata-scripts-x86.x86_32.4.2.1@1.goo
}
if ($script:install_packages.ToLower() -eq 'true') {
Write-Output 'Translate: Installing GCE packages...'
# Install each individually in order to catch individual errors
& C:\ProgramData\GooGet\googet.exe -root C:\ProgramData\GooGet -noconfirm install C:\ProgramData\GooGet\components\google-compute-engine-windows-x86.x86_32.4.6.0@1.goo
}
}
function Enable-WinRM {
if ($script:pn -like '*Enterprise') {
Write-Host 'Translate: Windows Client detected, enabling WinRM (including on Public networks).'
& winrm quickconfig -quiet -force
}
}
function Add-Warning {
param (
[parameter(Mandatory=$true)]
[string]$message
)
if ($script:warnings -ne '') {
$script:warnings += ' '
}
$script:warnings += $message
"Translate: Warning - $message"
}
try {
$script:warnings = ''
Write-Output 'Translate: Beginning translate PowerShell script.'
# Aborting restart triggered in run_startup_scripts.cmd.
Start-Process -FilePath "shutdown.exe" -ArgumentList '/a' -ErrorAction SilentlyContinue
Write-Output "Scheduled restart aborted with return code $LASTEXITCODE. 0=Restart aborted 1116=No restart scheduled."
$script:pn = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name ProductName).ProductName
# As we're triggering the same process for windows 11 as windows 10.
$logsOsName = $script:pn
if ($logsOsName -like "*Windows 10*") {
$logsOsName = $logsOsName -replace 'Windows 10', 'Windows 10/11'
}
Write-Host "Translate: OS is $logsOsName, version $([System.Environment]::OSVersion.Version.ToString())"
Remove-VMWareTools
Change-InstanceProperties
Configure-Network
Setup-NTP
Configure-RDPSecurity
$script:install_packages = Get-MetadataValue -key 'install-gce-packages'
$script:sysprep = Get-MetadataValue -key 'sysprep'
$script:is_byol = Get-MetadataValue -key 'is_byol'
$script:is_x86 = Get-MetadataValue -key 'is_x86'
if ($script:install_packages -eq $null -or $script:sysprep -eq $null -or $script:is_byol -eq $null -or $script:is_x86 -eq $null) {
Write-Output "Translate: failed to obtain at least one of the required values from metadata, rebooting in an attempt to resolve issue. install_packages=$script:install_packages, sysprep=$script:sysprep, is_byol=$script:is_byol, is_x86=$script:is_x86"
if (-not (Test-Path -Path $env:TEMP\translate_metadata_reboot.txt -PathType Leaf)) {
New-Item -Path $env:TEMP\translate_metadata_reboot.txt
Restart-Computer -Force
exit 0
}
else {
Throw "Failed to obtain at least one of the required values from metadata and a reboot has already been attempted."
}
}
Configure-Power
if ($script:is_x86.ToLower() -ne 'true') {
Install-Packages
}
else {
# Since 32-bit GooGet packages are not provided via repository, the only option is to install them from a local source.
Install-32bitPackages
}
# Only needed and applicable to 2008R2.
$netkvm = Get-WMIObject Win32_NetworkAdapter -filter "ServiceName='netkvm'"
$netkvm | ForEach-Object {
& netsh interface ipv4 set dnsservers "$($_.NetConnectionID)" dhcp | Out-Null
}
Enable-RemoteDesktop
Enable-WinRM
if ($script:sysprep.ToLower() -ne 'true') {
if ($script:is_byol.ToLower() -ne 'true') {
Write-Output 'Translate: Setting up KMS activation'
. 'C:\Program Files\Google\Compute Engine\sysprep\activate_instance.ps1' | Out-Null
}
if ($script:install_packages.ToLower() -ne 'true') {
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm remove google-compute-engine-metadata-scripts
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm remove google-compute-engine-powershell
}
} else {
Write-Output 'Translate: Launching sysprep.'
& 'C:\Program Files\Google\Compute Engine\sysprep\gcesysprep.bat' -NoShutdown
}
if ($script:is_byol.ToLower() -eq 'true') {
'Image imported into GCE using BYOL worklfow' > 'C:\Program Files\Google\Compute Engine\sysprep\byol_image'
}
# Cleanup
if (Test-Path -Path "$Env:Programfiles\Google\Compute Engine\metadata_scripts\network.ps1") {
Write-Output "Deleting file $Env:Programfiles\Google\Compute Engine\metadata_scripts\network.ps1"
Remove-Item -Path "$Env:Programfiles\Google\Compute Engine\metadata_scripts\network.ps1" -Force
}
Write-Output 'Translate complete.'
Stop-Computer -force
exit 0
}
catch {
Write-Output 'Exception caught in script:'
Write-Output $_.InvocationInfo.PositionMessage
$previous_warnings = ''
if ($script:warnings -ne '') {
$previous_warnings = " Previous warning(s): $script:warnings"
}
Write-Output "TranslateFailed: $($_.Exception.Message).$previous_warnings"
exit 1
}