daisy_workflows/image_build/windows/post_install.ps1 (656 lines of code) (raw):
# This script can be run after image setup. It contains commands that
# should be run once when creating an image but do not need to be run
# on every subsequent sysprep.
$ErrorActionPreference = 'Stop'
$script:gce_install_dir = 'C:\Program Files\Google\Compute Engine'
$script:hosts_file = "$env:windir\system32\drivers\etc\hosts"
$script:components_path = 'D:\sbomcomponents\components'
# Windows Updates Registry Key Paths
$windows_update_path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
$windows_update_au_path = "$windows_update_path\AU"
function Run-Command {
param (
[Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[string]$Executable,
[Parameter(ValueFromRemainingArguments=$true,
ValueFromPipelineByPropertyName=$true)]
$Arguments = $null
)
Write-Host "Running $Executable with arguments $Arguments."
$out = &$executable $arguments 2>&1 | Out-String
$out.Trim()
}
function Get-MetadataValue {
<#
.SYNOPSIS
Returns a value for a given metadata key.
.PARAMETER $key
The metadata key to retrieve.
.PARAMETER $default
The value to return if the key is not found.
.RETURNS
The value for the key or null.
#>
param (
[parameter(Mandatory=$true)]
[string]$key,
[parameter(Mandatory=$false)]
[string]$default
)
# Returns the provided metadata value for a given key.
$url = "http://169.254.169.254/computeMetadata/v1/instance/attributes/$key"
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) {
return $default
}
else {
Write-Host "Failed to retrieve value for $key."
return $null
}
}
}
function Set-WindowsUpdateServer {
<#
.SYNOPSIS
Set the Windows update server to a WSUS server.
#>
if (-not (Test-Path $windows_update_path)) {
New-Item -Path $windows_update_path -Value ""
New-Item -Path $windows_update_au_path -Value ""
Write-Host "Set-WindowsUpdateServer: Attempting to set Windows update server to $script:wu_server_url`:$script:wu_server_port"
$wu_server_address = $script:wu_server_url.Remove(0,$script:wu_server_url.LastIndexOf('/')+1)
Write-Host "Set-WindowsUpdateServer: Testing connection to $wu_server_address."
if (-not (Test-Connection $wu_server_address -Count 1 -ErrorAction SilentlyContinue)) {
if (-not (Test-Connection download.microsoft.com -Count 1 -ErrorAction SilentlyContinue)) {
throw 'Set-WindowsUpdateServer: Windows update server is not reachable. Cannot complete image build.'
}
Write-Host "Set-WindowsUpdateServer: $wu_server_address not reachable, defaulting to Microsoft servers"
return
}
# Set the registry keys to use a WSUS 6.x server.
New-ItemProperty -Path $windows_update_path -Name WUServer -Value "$script:wu_server_url`:$script:wu_server_port" -PropertyType String
New-ItemProperty -Path $windows_update_path -Name WUStatusServer -Value "$script:wu_server_url`:$script:wu_server_port" -PropertyType String
New-ItemProperty -Path $windows_update_au_path -Name UseWUServer -Value 1 -PropertyType DWord
New-ItemProperty -Path $windows_update_au_path -Name NoAutoUpdate -Value 1 -PropertyType DWord
Write-Host "Set-WindowsUpdateServer: Set Windows update server to $script:wu_server_url`:$script:wu_server_port, rebooting."
shutdown /r /t 00
exit
}
}
function Reset-WindowsUpdateServer {
<#
.SYNOPOSIS
Reset the Windows update server to default settings and enable automatic updates.
#>
Write-Host 'Reset-WindowsUpdateServer: Setting Windows Update to the default value of Microsoft Update.'
if (Test-Path $windows_update_path) {
Remove-Item -Path $windows_update_path -Recurse -Force
}
New-Item -Path $windows_update_path -Value "" | Out-Null
New-Item -Path $windows_update_au_path -Value "" | Out-Null
New-ItemProperty -Path $windows_update_au_path -Name AUOptions -Value 4 -PropertyType DWord | Out-Null
New-ItemProperty -Path $windows_update_au_path -Name ScheduledInstallDay -Value 0 -PropertyType DWord | Out-Null
New-ItemProperty -Path $windows_update_au_path -Name ScheduledInstallTime -Value 3 -PropertyType DWord | Out-Null
}
function Reboot-Required {
return (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired')
}
function Install-WindowsUpdates {
<#
.SYNOPSIS
Check for updates, returns true if restart is required.
#>
# https://support.microsoft.com/en-us/help/4072698/windows-server-guidance-to-protect-against-the-speculative-execution
if (-not (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\QualityCompat')) {
New-Item -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\QualityCompat' -Type Directory | Out-Null
}
New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\QualityCompat' -Name 'cadca5fe-87d3-4b96-b7fb-a231484277cc' -Value 0 -PropertyType DWORD -Force | Out-Null
Write-Host 'Install-WindowsUpdates: Starting Windows update.'
# In 2008R2 the initial search can fail with error 0x80244010. Retrying the search again generally resolves the issueis.
$session = New-Object -ComObject 'Microsoft.Update.Session'
$query = 'IsInstalled=0'
$searcher = $session.CreateUpdateSearcher()
$i = 1
while ($i -lt 10) {
try {
Write-Host "Install-WindowsUpdates: Searching for updates, try $i."
$updates = $searcher.Search($query).Updates
# Skip Windows 7 optional language pack updates
if ($pn -like 'Windows 7*') {
if ($updates.Count -le 37 -and $updates.Count -ge 33) {
Write-Host 'Install-WindowsUpdates: Windows 7 detected. Skipping ~35 language pack updates.'
$query = 'IsInstalled=0 and AutoSelectOnWebsites=1'
continue
}
}
break
} catch {
Write-Host 'Install-WindowsUpdates: Update search failed.'
$i++
if ($i -ge 10) {
Write-Host 'Install-WindowsUpdates: Reseting update server'
Reset-WindowsUpdateServer | Out-Null
Write-Host 'Install-WindowsUpdates: Searching for updates one last time.'
$updates = $searcher.Search($query).Updates
}
}
}
if ($updates.Count -eq 0) {
Write-Host 'Install-WindowsUpdates: No updates required!'
return $false
}
# Windows 7 may enter a loop with a single update remaining
if ($pn -like 'Windows 7*') {
if ($updates.Count -eq 1) {
Write-Host 'Install-WindowsUpdates: Windows 7 detected. Single update remaining. Displaying and continuing install.'
foreach ($update in $updates) {
Write-Host ($update.Description)
}
return $false
}
}
# Windows 11 may get stuck installing KB5007651. $pn for Windows 11 reports as Win 10 Enterprise.
# This is an intended behavior by Microsoft for backwards compatibility.
# As such we skip the KB here instead of trying to target by $pn.
if ($updates.Count -eq 1) {
if([Environment]::OSVersion.Version.Major -eq 10 -and [Environment]::OSVersion.Version.Minor -eq 0 -and [Environment]::OSVersion.Version.Build -ge 22000) {
foreach ($update in $updates) {
if ($update.Title -like '*KB5007651*') {
Write-Host 'Install-WindowsUpdates: KB5007651 detected as a single update remaining. Skipping known issue KB.'
return $false
}
}
}
}
# As of Jan 2024, Server 2022 may get stuck installing KB5034439.
# Temporarily work around this to let the build continue.
if ($updates.Count -eq 1) {
if([Environment]::OSVersion.Version.Major -eq 10 -and [Environment]::OSVersion.Version.Minor -eq 0 -and [Environment]::OSVersion.Version.Build -eq 20348) {
foreach ($update in $updates) {
if ($update.Title -like '*KB5034439*') {
Write-Host 'Install-WindowsUpdates: KB5034439 detected as a single update remaining. Skipping known issue KB.'
return $false
}
}
}
}
foreach ($update in $updates) {
if (-not ($update.EulaAccepted)) {
Write-Host 'The following update required a EULA to be accepted:'
Write-Host '----------------------------------------------------'
Write-Host ($update.Description)
Write-Host '----------------------------------------------------'
Write-Host ($update.EulaText)
Write-Host '----------------------------------------------------'
$update.AcceptEula()
}
}
Write-Host "Install-WindowsUpdates: Downloading and installing $($updates.Count) updates."
foreach ($update in $updates) {
Write-Host "Install-WindowsUpdates: Update - Title:$($update.Title), Description:$($update.Description)"
}
$downloader = $session.CreateUpdateDownloader()
$downloader.Updates = $updates
$download_result = $downloader.Download()
Write-Host "Install-WindowsUpdates: Download complete. Result: $(Get-ResultCodeDescription $download_result.ResultCode). Installing updates."
$installer = $session.CreateUpdateInstaller()
$installer.Updates = $updates
$installer.AllowSourcePrompts = $false
$install_result = $installer.Install()
Write-Host "Install-WindowsUpdates: Update installation completed. Result: $(Get-ResultCodeDescription $install_result.ResultCode)"
return $true
}
function Install-NetFrameworkCore {
<#
.SYNOPSIS
Checks for Windows Server 2012 R2 and enables the .Net version 3.5 Framework.
#>
if([Environment]::OSVersion.Version.Major -eq 6 -and [Environment]::OSVersion.Version.Minor -eq 3) {
Write-Host 'Install-NetFrameworkCore: Enabling .Net Framework version 3.5.'
Install-WindowsFeature Net-Framework-Core
}
else {
Write-Host 'Install-NetFrameworkCore: Windows Server 2012 R2 not detected. Skipping .Net 3.5 install.'
}
}
function Get-ResultCodeDescription {
<#
.SYNOPSIS
Returns the description of the Windows Update download/install ResultCode.
.PARAMETER $ResultCode
The ResultCode to convert.
.RETURNS
The human readable description of the result code.
#>
param (
[Parameter(Mandatory=$true)] [int]$ResultCode
)
$Result = switch ($ResultCode) {
0 { 'Not Started' }
1 { 'In Progress' }
2 { 'Succeeded' }
3 { 'SucceededWithErrors' }
4 { 'Failed' }
5 { 'Aborted' }
default { "Unknown, ResultCode: $ResultCode" }
}
return $Result
}
function Setup-NTP {
<#
.SYNOPSIS
Setup NTP sync for GCE.
#>
Write-Host 'Configure NTP for GCP.'
# 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...
# Stop in case it's running; it probably won't be.
Stop-Service W32time
# w32tm /unregister is flaky, but using sc delete first helps to clean up
# the service reliably.
Run-Command $env:windir\system32\sc.exe delete W32Time
# Unregister and re-register the service.
$w32tm = "$env:windir\System32\w32tm.exe"
Run-Command $w32tm /unregister
Run-Command $w32tm /register
# Get time from GCE NTP server every 15 minutes.
Run-Command $w32tm /config '/manualpeerlist:metadata.google.internal,0x1' /syncfromflags:manual
Start-Sleep -s 300
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-Host 'Configured W32Time to use GCE NTP server.'
# Verify that the W32Time service is correctly installed. This has been
# a source of flakiness in the past.
try {
Get-Service W32Time
}
catch {
throw "Failed to configure NTP. Cannot complete image build: $($_.Exception.Message)"
}
# Sync time now.
Start-Service W32time
Run-Command $w32tm /resync
}
function Configure-Network {
<#
.SYNOPSIS
Apply GCE networking related configuration changes.
#>
Write-Host 'Configuring network.'
# Make sure metadata server is in etc/hosts file.
Add-Content $script:hosts_file @'
# Google Compute Engine metadata server
169.254.169.254 metadata.google.internal metadata
'@
Write-Host 'Changing firewall settings.'
# Change Windows Server firewall settings.
# Allow ICMP ping for IPv4 and IPv6 for Win10/2016 and above using Powershell or IPv4 only for older operating systems using netsh.
if ([System.Environment]::OSVersion.Version.Major -ge 10 -and [System.Environment]::OSVersion.Version.Build -ge 10240) {
Write-Host "Creating firewall rules to allow incoming IPv4 & IPv6 ICMP Echo Request using PowerShell."
New-NetFirewallRule -DisplayName 'ICMP Allow incoming IPv4 echo request' -Enabled True -Direction Inbound -Action Allow -Protocol ICMPv4 -IcmpType "8"
New-NetFirewallRule -DisplayName 'ICMP Allow incoming IPv6 echo request' -Enabled True -Direction Inbound -Action Allow -Protocol ICMPv6 -IcmpType "128"
}
else {
Write-Host "Creating firewall rules to allow incoming V4 ICMP Echo Request using netsh."
Run-Command netsh advfirewall firewall add rule name='ICMP Allow incoming IPv4 echo request' protocol='icmpv4:8,any' dir=in action=allow
}
# Allow inbound/outbound communication from the metadata server. Win10/2016 and above set using PowerShell and older versions set using netsh.
if ([System.Environment]::OSVersion.Version.Major -ge 10 -and [System.Environment]::OSVersion.Version.Build -ge 10240) {
Write-Host "Creating firewall rules to allow inbound/outbound to metadata using PowerShell."
New-NetFirewallRule -DisplayName 'Allow incoming from GCE metadata server' -Enabled True -Action Allow -Protocol ANY -RemoteAddress 169.254.169.254 -Direction Inbound
New-NetFirewallRule -DisplayName 'Allow outgoing to GCE metadata server' -Enabled True -Action Allow -Protocol ANY -RemoteAddress 169.254.169.254 -Direction Outbound
}
else {
Write-Host "Creating firewall rules to allow incoming V4 ICMP Echo Request using netsh."
Run-Command netsh advfirewall firewall add rule name='Allow incoming from GCE metadata server' protocol=ANY remoteip=169.254.169.254 dir=in action=allow
Run-Command netsh advfirewall firewall add rule name='Allow outgoing to GCE metadata server' protocol=ANY remoteip=169.254.169.254 dir=out action=allow
}
# Change KeepAliveTime to 5 minutes.
$tcp_params = 'HKLM:\System\CurrentControlSet\Services\Tcpip\Parameters'
New-ItemProperty -Path $tcp_params -Name 'KeepAliveTime' -Value 300000 -PropertyType DWord
Write-Host '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
}
# Unmount default user hive.
Run-Command reg unload 'HKLM\DefaultUser'
}
function Disable-Hibernate {
Write-Host 'Disabling Hibernate, Hybrid Sleep, and Fast Startup'
powercfg /HIBERNATE OFF
Write-Host 'All avaiable sleep states:'
powercfg /a
}
function Set-PowerProfile {
<#
.SYNOPSIS
Change power plan to High-performance.
#>
Write-Host 'Changing power plan to High performance'
$power_plan = Get-CimInstance -Namespace 'root\cimv2\power' -ClassName 'win32_PowerPlan' -OperationTimeoutSec 5 -Filter "ElementName = 'High performance'" -ErrorAction SilentlyContinue
powercfg /setactive $power_plan.InstanceID.ToString().Replace("Microsoft:PowerPlan\{","").Replace("}","")
$active_plan = powercfg /getactivescheme
if ($active_plan -like '*High performance*') {
Write-Host 'Power plan updated successfully'
}
else {
throw 'Failed to update the power plan'
}
}
function Configure-PowerProfiles {
<#
.SYNOPSIS
Change power settings to never turn off monitor.
#>
Write-Host 'Modify power settings to disable monitor power down.'
Get-CimInstance -Namespace 'root\cimv2\power' -ClassName Win32_PowerSettingDataIndex -ErrorAction SilentlyContinue | ForEach-Object {
$power_setting = $_ | Get-CimAssociatedInstance -ResultClassName 'Win32_PowerSetting' -OperationTimeoutSec 10 -ErrorAction SilentlyContinue
# 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.
if ($power_setting -and $power_setting.ElementName -eq 'Turn off display after') {
Write-Host ('Changing power setting ' + $_.InstanceID)
$_ | Set-CimInstance -Property @{SettingIndexValue = 0}
}
# Set the "Sleep button action" setting to 1 for all power configurations
# so the instance responds to standby requests.
if ($power_setting -and $power_setting.ElementName -eq 'Sleep button action') {
Write-Host ('Changing power setting ' + $_.InstanceID)
$_ | Set-CimInstance -Property @{SettingIndexValue = 1}
}
}
}
function Change-InstanceProperties {
<#
.SYNOPSIS
Apply GCE specific changes.
.DESCRIPTION
Apply GCE specific changes to this instance.
#>
Write-Host '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-Host 'bcdedit option set.'
# Registry fix for PD cluster size issue.
$vioscsi_path = 'HKLM:\SYSTEM\CurrentControlSet\Services\vioscsi\Parameters\Device'
if (-not (Test-Path $vioscsi_path)) {
New-Item -Path $vioscsi_path
}
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-Host ($san_policy -replace '(?<=>)\s+(?=<)') # Remove newline and tabs
# Prevent password from expiring after 42 days.
Run-Command net accounts /maxpwage:unlimited
# Change time zone to Coordinated Universal Time.
Run-Command tzutil /s 'UTC'
# Register netkvmco.dll.
Run-Command rundll32 'netkvmco.dll,RegisterNetKVMNetShHelper'
# Set pagefile to 1GB
Get-CimInstance Win32_ComputerSystem | Set-CimInstance -Property @{AutomaticManagedPageFile=$False}
Get-CimInstance Win32_PageFileSetting | Set-CimInstance -Property @{InitialSize=1024; MaximumSize=1024}
# Disable Administartor user.
Run-Command net user Administrator /ACTIVE:NO
# Set minimum password length.
Run-Command net accounts /MINPWLEN:8
# Enable access to Windows administrative file share.
Set-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'LocalAccountTokenFilterPolicy' -Value 1 -Force
# https://support.microsoft.com/en-us/help/4072698/windows-server-guidance-to-protect-against-the-speculative-execution
# Not enabling by default for now.
#New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management' -Name 'FeatureSettingsOverride' -Value 0 -PropertyType DWORD -Force
#New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management' -Name 'FeatureSettingsOverrideMask' -Value 3 -PropertyType DWORD -Force
}
function Configure-RDP {
Write-Host 'Modifying RDP settings.'
$ts_path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server'
$registryPath = "${ts_path}\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
# Enable remote desktop in registry.
Set-ItemProperty -Path $ts_path -Name 'fDenyTSConnections' -Value 0 -Force
# Disable Ctrl + Alt + Del.
Set-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'DisableCAD' -Value 1 -Force
# Allow RPD inbound. Win10/2016 and above set using PowerShell and older versions set using netsh.
if ([System.Environment]::OSVersion.Version.Major -ge 10 -and [System.Environment]::OSVersion.Version.Build -ge 10240) {
Write-Host "Enabling RDP firewall rules using PowerShell."
Set-NetFirewallRule -DisplayGroup 'Remote Desktop' -Enabled True
}
else {
Write-Host "Enabling RDP firewall rules using netsh."
Run-Command netsh advfirewall firewall set rule group='remote desktop' new enable=Yes
}
}
function Install-DriverPackages {
Write-Host 'Installing GCE Driver Packages...'
# Install each individually in order to catch individual errors
# Install GVNIC GQ in all Windows versions
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install -sources https://packages.cloud.google.com/yuck/repos/google-compute-engine-driver-gvnic-gq-stable google-compute-engine-driver-gvnic
Start-Sleep -s 60
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-compute-engine-driver-vioscsi
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-compute-engine-driver-netkvm
Start-Sleep -s 60
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-compute-engine-driver-pvpanic
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-compute-engine-driver-balloon
# Google Graphics Array not supported on 2008R2/7 (6.1)
if ($pn -notlike 'Windows Server 2008*' -or $pn -notlike 'Windows 7*') {
Write-Host 'Installing GCE virtual display driver...'
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-compute-engine-driver-gga
}
# todo: Change this to exclude clients OS instead of allowlist server versions. (Get-ComputerInfo).OsProductType -eq "Server", but needs to be validated.
if ($pn -match 'Windows (Web )?Server (2008 R2|2012 R2|2016|2019|2022|2025|Standard|Datacenter)') {
Write-Host 'Installing GCE VSS agent and provider...'
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-compute-engine-vss
}
}
function Update-Edge {
$taskExistEdgeUpdateCore = Get-ScheduledTask | Where-Object {$_.TaskName -like 'MicrosoftEdgeUpdateTaskMachineCore*' }
if($taskExistEdgeUpdateCore) {
Start-ScheduledTask -TaskName $taskExistEdgeUpdateCore.TaskName
Write-Host 'Microsoft Edge Core updater started.'
} else {
Write-Host 'Microsoft Edge updater task MicrosoftEdgeUpdateTaskMachineCore not present.'
}
$taskExistEdgeUpdateUA = Get-ScheduledTask | Where-Object {$_.TaskName -like 'MicrosoftEdgeUpdateTaskMachineUA*' }
if($taskExistEdgeUpdateUA) {
Start-ScheduledTask -TaskName $taskExistEdgeUpdateUA.TaskName
Write-Host 'Microsoft Edge UA updater started.'
} else {
Write-Host 'Microsoft Edge updater task MicrosoftEdgeUpdateTaskMachineUA not present.'
}
# Check if the Edge update is finished before continuing
if (Test-Path "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe") {
while ((Get-Item "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe").LastWriteTime -lt (Get-Date).AddMonths(-1)) {
Write-Host "Microsoft Edge updater not completed; version found: $((Get-Item "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe").VersionInfo.ProductVersion)"
Start-Sleep -s 30
}
Write-Host "Microsoft Edge updater completed; version found: $((Get-Item "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe").VersionInfo.ProductVersion)"
}
}
function Install-GCEAppPackages {
Write-Host 'Installing GCE Appplication 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-powershell
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-compute-engine-sysprep
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install certgen
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-compute-engine-diagnostics
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' -noconfirm install google-osconfig-agent
}
function Set-Repos {
Write-Host 'Setting GooGet repos to stable.'
Remove-Item 'C:\ProgramData\GooGet\repos' -Recurse -Force
Run-Command 'C:\ProgramData\GooGet\googet.exe' -root 'C:\ProgramData\GooGet' addrepo 'google-compute-engine-stable' 'https://packages.cloud.google.com/yuck/repos/google-compute-engine-stable'
}
function Optimize-Image {
<#
.SYNOPSIS
Runs storage optimizations operations on the system volume.
.DESCRIPTION
Runs Defrag and Retrim commands on the system volume.
This can reduce disk usage and increase HDD performance.
#>
# No trim support in Windows versions prior to Windows 2012.
# As defrag can possibly enlarge the image without trim,
# skipping this scenario.
if ($pn -like 'Windows Server 2008*' -or $pn -like 'Windows 7*') {
return
}
Write-Host "Defragging $($env:SystemDrive)"
# Defrags C:
# This should increase performance on HDD disks.
Optimize-Volume -Verbose -Defrag -DriveLetter $env:SystemDrive[0]
Write-Host "Retrimming $($env:SystemDrive)"
# Retrims C:
# This can reduce disk usage of PD disks, and subsequently of images.
Optimize-Volume -Verbose -ReTrim -DriveLetter $env:SystemDrive[0]
}
function Generate-NativeImage {
<#
.SYOPSIS
Generates .Net Framework native image.
.DESCRIPTION
When .Net framework is updated during windows monthly update,
the native image should also be regenerated during image build.
It reduces the CPU load when a new VM is launched from the image.
.NOTES
Using PowerShell simple function to match the style.
#>
# Searching for ngen.exe location using the .Net CLR default path
# Ref: https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/versions-and-dependencies
Write-Host 'Native Image Generation for .Net Framework'
Write-Host 'Searching for ngen.exe'
$ngenPath = (Get-ChildItem -Path "$env:SystemRoot\Microsoft.NET" -Recurse -Filter 'ngen.exe' -ErrorAction Stop).FullName
Write-Host "Found $($ngenPath.count): $(($ngenPath -join ';'))"
foreach ($ngen in $ngenPath) {
Write-Host "NGEN start: [$ngen]"
&$ngen executeQueuedItems /verbose
&$ngen update /force
Write-Host "NGEN finish: [$ngen]"
}
}
function Enable-WinRM {
if ($pn -like '*Enterprise') {
Write-Host 'Windows Client detected, enabling WinRM (including on Public networks).'
& winrm quickconfig -quiet -force
}
}
function Install-PowerShell {
if (!(Test-Path HKLM:\SOFTWARE\Microsoft\PowerShellCore\InstalledVersions\)) {
Write-Host 'Installing PowerShell v7.'
Start-Process -FilePath msiexec.exe -ArgumentList '/i',"$script:components_path\PowerShell.msi",'/quiet','REGISTER_MANIFEST=1' -Wait
Write-Host 'PowerShell v7 installed, rebooting.'
shutdown /r /t 120
exit
}
}
try {
Write-Host 'Beginning post install powershell script.'
$script:x86 = (Get-MetadataValue -key 'x86-build').ToLower() -eq 'true'
$script:outs_dir = Get-MetadataValue -key 'daisy-outs-path'
$script:wu_server_url = Get-MetadataValue -key 'wu_server_url' -default 'none'
$script:wu_server_port = Get-MetadataValue -key 'wu_server_port' -default '0'
# Set to High Performance Power Profile so the machines never go to sleep on their own.
Set-PowerProfile
# Disable Hibernate
Disable-Hibernate
# Windows Product Name https://renenyffenegger.ch/notes/Windows/versions/index
$pn = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name ProductName).ProductName
# Install x64 Driver Packages
if (!$script:x86) {
Install-DriverPackages
}
Install-PowerShell
Update-Edge
# Remove with Win2012 R2 EOL in Oct 2023. Temporary fix for issue following June 2023 .Net update.
Install-NetFrameworkCore
if ($script:wu_server_url.StartsWith('http') -and $script:wu_server_port -notlike '0') {
Set-WindowsUpdateServer
}
if (Install-WindowsUpdates) {
Write-Host 'Install-WindowsUpdates installed updates, rebooting.'
shutdown /r /t 00
exit
}
if (Reboot-Required) {
Write-Host 'Reboot-Required returned true, rebooting.'
shutdown /r /t 00
exit
}
Reset-WindowsUpdateServer
Change-InstanceProperties
Configure-Network
Configure-PowerProfiles
Configure-RDP
Setup-NTP
# Install x64 Application Packages
if (!$script:x86) {
Install-GCEAppPackages
Set-Repos
}
Enable-WinRM
Generate-NativeImage
# Only needed and applicable for 2008.
if([Environment]::OSVersion.Version.Major -eq 6 -and [Environment]::OSVersion.Version.Minor -le 1) {
Write-Host 'Windows Server 2008/2008R2 detected. Setting IPv4 DNS Server source to DHCP.'
& netsh interface ipv4 set dnsservers 'Local Area Connection' source=dhcp | Out-Null
}
# Required for WMF 5.1 on Windows Server 2008R2
# https://sccm-zone.com/fix-sysprep-error-on-windows-2008-r2-after-windows-management-framework-5-0-installation-b9e86b4c41e4
if ($pn -like 'Windows Server 2008*') {
New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\StreamProvider' -Name LastFullPayloadTime -Value 0 -PropertyType DWord -Force
}
# Remove netsh helpers on Windows 11
if ((Get-WmiObject Win32_OperatingSystem).Caption -Match "Windows 11") {
if ((Get-Item -Path 'HKLM:\SOFTWARE\Microsoft\NetSh').GetValue('gvnichelper',$null) -ne $null) {
Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NetSh' -Name 'gvnichelper' -Force
}
if ((Get-Item -Path 'HKLM:\SOFTWARE\Microsoft\NetSh').GetValue('NetKVM',$null) -ne $null) {
Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NetSh' -Name 'NetKVM' -Force
}
}
Optimize-Image
Write-Host 'Launching sysprep.'
& "$script:gce_install_dir\sysprep\gcesysprep.bat"
}
catch {
Write-Host 'Exception caught in script:'
Write-Host $_.InvocationInfo.PositionMessage
Write-Host "Message: $($_.Exception.Message)"
Write-Host 'Windows build failed.'
exit 1
}