sysprep/instance_setup.ps1 (267 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. <# .SYNOPSIS Setup GCE instance. .DESCRIPTION This powershell script setups a GCE instance post sysprep. Some of the task performed by the scripts are: Change the hostname to match the GCE hostname Activate the GCE instance #> #requires -version 3.0 [CmdletBinding()] param ( [Parameter(HelpMessage = 'Sysprep specialize phase.')] [switch] $specialize=$false ) Set-StrictMode -Version Latest $global:logger = 'GCEInstanceSetup' $script:gce_install_dir = 'C:\Program Files\Google\Compute Engine' $script:gce_base_loc = "$script:gce_install_dir\sysprep\gce_base.psm1" $script:activate_instance_script_loc = "$script:gce_install_dir\sysprep\activate_instance.ps1" $script:setupcomplete_loc = "$env:WinDir\Setup\Scripts\SetupComplete.cmd" $script:write_to_serial = $false $script:metadata_script_loc = "$script:gce_install_dir\metadata_scripts\GCEMetadataScripts.exe" $script:compatRunner = "$script:gce_install_dir\metadata_scripts\GCECompatMetadataScripts.exe" $script:runnerV2 = "$script:gce_install_dir\agent\GCEMetadataScriptRunner.exe" if (Test-Path $script:runnerV2) { $script:metadata_script_loc = $script:runnerV2 } if (Test-Path $script:compatRunner) { $script:metadata_script_loc = $script:compatRunner } try { Import-Module $script:gce_base_loc -ErrorAction Stop 3> $null } catch [System.Management.Automation.ActionPreferenceStopException] { Write-Host $_.Exception.GetBaseException().Message Write-Host ("Unable to import GCE module $script:gce_base_loc. " + 'Check error message, or ensure module is present.') exit 2 } function Write-GuestAttributes { param ( [Parameter(Mandatory=$true)] $Key, [Parameter(Mandatory=$true)] $Property ) $request_url = '/computeMetadata/v1/instance/guest-attributes/' $url = "http://$global:metadata_server$request_url$Key" $client = _GetWebClient $client.Headers.Add('Metadata-Flavor', 'Google') $client.UploadString($url, 'PUT', $Property) } function Change-InstanceName { <# .SYNOPSIS Changes the machine name for GCE Instance .DESCRIPTION If metadata server is reachable get the instance name for the machine and rename. #> Write-Log 'Getting hostname from metadata server.' if ((Get-CimInstance Win32_BIOS).Manufacturer -cne 'Google') { if (-not (Test-Connection -Count 1 169.254.169.254 -ErrorAction SilentlyContinue)) { Write-Log 'Not running in a Google Compute Engine VM.' -error return } } $count = 1 do { $hostname_parts = (Get-Metadata -property 'hostname') -split '\.' if ($hostname_parts.Length -le 1) { Write-Log "Waiting for metadata server, attempt $count." Start-Sleep -Seconds 1 } if ($count++ -ge 60) { Write-Log 'There is likely a problem with the network.' -error return } } while ($hostname_parts.Length -le 1) $new_hostname = $hostname_parts[0] # Change computer name to match GCE hostname. # This will take effect after reboot. try { (Get-WmiObject Win32_ComputerSystem).Rename($new_hostname) Write-Log "Renamed from $global:hostname to $new_hostname." $global:hostname = $new_hostname } catch { Write-Log 'Unable to change hostname.' Write-LogError } } function Change-InstanceProperties { <# .SYNOPSIS Apply GCE specific changes. .DESCRIPTION Apply GCE specific changes to this instance. #> # Set all Adapters to get IP from DHCP. $nics = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True" $nics | Invoke-CimMethod -Name EnableDHCP # A null argument sets this to just use DHCP $nics | Invoke-CimMethod -Name SetDNSServerSearchOrder -Arguments @{DNSServerSearchOrder=$null} Write-Log 'All networks set to DHCP.' # Find which interface type is being used $netkvm = Get-CimInstance Win32_NetworkAdapter -filter "ServiceName='netkvm'" $gvnic = Get-CimInstance Win32_NetworkAdapter -filter "ServiceName='gvnic'" if ($netkvm -ne $null) { $interface = $netkvm Write-Log 'VirtIO network adapter detected.' } elseif ($gvnic -ne $null) { $interface = $gvnic Write-Log 'gVNIC network adapter detected.' $gvnicVersion = "0.0" $gvnicDriver = "$env:SystemRoot\System32\drivers\gvnic.sys" if (Test-Path $gvnicDriver) { $gvnicVersion = (Get-Item $gvnicDriver).VersionInfo.FileVersion } # Disable IPv4 Large Send Offload (LSO) on Win 10, 11, and server 2022. $productMajorVersion = [Environment]::OSVersion.Version.Major $productMinorVersion = [Environment]::OSVersion.Version.Minor $productBuildNumber = [Environment]::OSVersion.Version.Build $productType = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\ProductOptions' -Name 'ProductType').ProductType $isWin10ClientOrLater = ($productMajorVersion -eq 10 -and $productMinorVersion -eq 0 -and $productBuildNumber -ge 10240 -and $productType -notmatch 'server') $isWinServer2022OrLater = ($productMajorVersion -eq 10 -and $productMinorVersion -eq 0 -and $productBuildNumber -gt 17763 -and $productType -match 'server') if (($isWin10ClientOrLater -or $isWinServer2022OrLater) -and $gvnicVersion -lt 2.0 ) { Write-Log 'Disabling GVNIC IPv4 Large Send Offload (LSO)' Set-NetAdapterAdvancedProperty -InterfaceDescription 'Google Ethernet Adapter' -RegistryKeyword '*LSOV2Ipv4' -RegistryValue 0 Write-Log 'Disabling GVNIC IPv6 Large Send Offload (LSO)' Set-NetAdapterAdvancedProperty -InterfaceDescription 'Google Ethernet Adapter' -RegistryKeyword '*LSOV2Ipv6' -RegistryValue 0 } } else { Write-Log 'Error retrieving network adapter, no gVNIC or VirtIO network adapter found.' } if ($interface -ne $null) { $interface | ForEach-Object { if ([System.Environment]::OSVersion.Version.Build -ge 10240) { Set-NetIPInterface -InterfaceIndex $_.InterfaceIndex -NlMtuBytes 1460 Write-Log "MTU set to 1460 for IPv4 and IPv6 using PowerShell for interface $($_.InterfaceIndex) - $($_.Name). Build $([System.Environment]::OSVersion.Version.Build)" } else { Invoke-ExternalCommand netsh interface ipv4 set interface $_.NetConnectionID mtu=1460 | Out-Null Invoke-ExternalCommand netsh interface ipv6 set interface $_.NetConnectionID mtu=1460 | Out-Null Write-Log "MTU set to 1460 for IPv4 and IPv6 using netsh for interface $($_.NetConnectionID) - $($_.Name)." } } Invoke-ExternalCommand route /p add 169.254.169.254 mask 255.255.255.255 0.0.0.0 if $interface[0].InterfaceIndex metric 1 -ErrorAction SilentlyContinue Write-Log "Added persistent route to metadata netblock to $($interface.ServiceName) adapter." } else { Write-Log 'Error identifying network adapter as gVNIC or VirtIO, unable to set MTU and route to metadata server.' } } function Configure-WinRM { <# .SYNOPSIS Setup WinRM on the instance. .DESCRIPTION Create a self signed cert to use with a HTTPS WinRM endpoint and restart the WinRM service. #> Write-Log 'Configuring WinRM...' if (Get-Command Import-PfxCertificate -ErrorAction SilentlyContinue) { $tempDir = "${env:TEMP}\cert" New-Item $tempDir -Type Directory Invoke-ExternalCommand $script:gce_install_dir\tools\certgen.exe -outDir $tempDir -hostname $global:hostname if (-not (Test-Path "${tempDir}\cert.p12")) { Write-Log 'Error creating cert, unable to setup WinRM' return } $cert = Import-PfxCertificate -FilePath $tempDir\cert.p12 -CertStoreLocation cert:\LocalMachine\My Remove-Item $tempDir -Recurse } else { # SHA1 self signed cert using hostname as the SubjectKey and name installed to LocalMachine\My store # with enhanced key usage object identifiers of Server Authentication and Client Authentication. # https://msdn.microsoft.com/en-us/library/windows/desktop/aa386968(v=vs.85).aspx $eku = '1.3.6.1.5.5.7.3.1,1.3.6.1.5.5.7.3.2' & $script:gce_install_dir\tools\makecert.exe -r -a SHA1 -sk "${global:hostname}" -n "CN=${global:hostname}" -ss My -sr LocalMachine -eku $eku $cert = Get-ChildItem 'Cert:\LocalMachine\My' | Where-Object {$_.Subject -eq "CN=${global:hostname}"} | Select-Object -First 1 } $xml = @" <p:Listener xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/config/listener.xsd"> <p:Enabled>True</p:Enabled> <p:URLPrefix>wsman</p:URLPrefix> <p:CertificateThumbPrint>$($cert.Thumbprint)</p:CertificateThumbPrint> </p:Listener> "@ try { Write-Log 'Waiting for WinRM to be running...' $svcTimeout = '00:02:00' $svc = Get-Service -name "WinRM" $svc.WaitForStatus('Running',$svcTimeout) } catch { Write-Log 'Error - Could not start WinRM service' return } $sess = (New-Object -ComObject 'WSMAN.Automation').CreateSession() try { $sess.Create('winrm/config/listener?Address=*+Transport=HTTPS', $xml) } catch { $sess.Put('winrm/config/listener?Address=*+Transport=HTTPS', $xml) } Restart-Service WinRM Write-Log 'Setup of WinRM complete.' } function Write-Certs { $rdp_cert = Get-ChildItem 'Cert:\LocalMachine\Remote Desktop\' | Where-Object {$_.Subject -eq "CN=${global:hostname}"} | Select-Object -First 1 $winrm_cert = Get-ChildItem 'Cert:\LocalMachine\My' | Where-Object {$_.Subject -eq "CN=${global:hostname}"} | Select-Object -First 1 Write-Log "WinRM certificate details: Subject: $($winrm_cert.Subject), Thumbprint: $($winrm_cert.Thumbprint)" Write-Log "RDP certificate details: Subject: $($winrm_cert.Subject), Thumbprint: $($rdp_cert.Thumbprint)" # We ignore any errors as guest attributes may not be enabled. Write-GuestAttributes -Key 'hostkeys/winrm' -Property $winrm_cert.Thumbprint -ErrorAction SilentlyContinue Write-GuestAttributes -Key 'hostkeys/rdp' -Property $rdp_cert.Thumbprint -ErrorAction SilentlyContinue } # Check if COM1 exists. if (-not ($global:write_to_serial)) { Write-Log 'COM1 does not exist on this machine. Logs will not be written to GCE console.' } Write-Log 'Enable google_osconfig_agent during the specialize configuration pass.' Set-Service google_osconfig_agent -StartupType Automatic -Verbose -ErrorAction Continue if ($specialize) { Write-Log 'Starting sysprep specialize phase.' Change-InstanceProperties Change-InstanceName Configure-WinRM try { Write-Log "Launching specialize phase scripts from $script:metadata_script_loc" # Call startup script during sysprep specialize phase. & $script:metadata_script_loc 'specialize' } catch { Write-LogError } Write-Log 'Finished with sysprep specialize phase, restarting...' } else { Write-Certs if (Test-Path $script:setupcomplete_loc) { Remove-Item -Path $script:setupcomplete_loc -Force } & $script:activate_instance_script_loc | ForEach-Object { Write-Log $_ } Invoke-ExternalCommand schtasks /change /tn GCEStartup /enable -ErrorAction SilentlyContinue Invoke-ExternalCommand schtasks /run /tn GCEStartup Write-Log "Instance setup finished. $global:hostname is ready to use." -important }