daisy_workflows/image_build/windows/bootstrap_install.ps1 (302 lines of code) (raw):

$ErrorActionPreference = 'Stop' function Get-MetadataValue { <# .SYNOPSIS Attempt to retrieve the value for a given metadata key. Returns null if not found. .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) { Write-Host "Failed to retrieve value for $key, returning default of $default." return $default } else { Write-Host "Failed to retrieve value for $key." return $null } } } function Download-Drivers { <# .SYNOPSIS Downloads the drivers from GCE to the local drivers directory. #> $gs_path = Get-MetadataValue -key 'drivers-path' if ($gs_path) { Write-Output "Downloading drivers from $gs_path." & 'gsutil' -m cp -r "${gs_path}/*" $script:driver_dir Write-Output 'Driver download complete.' } else { throw "Download-Drivers() Failed to find metadata key 'drivers-path'. Build cannot continue." } } function Download-Components { <# .SYNOPSIS Downloads the components from GCE to the local components directory. #> $gs_path = Get-MetadataValue -key 'components-path' if ($gs_path) { Write-Output "Downloading components from $gs_path." & 'gsutil' -m cp -r "${gs_path}/*" $script:components_dir Write-Output 'Components download complete.' } else { throw "Download-Components() Failed to find metadata key 'components-path'. Build cannot continue." } } function Download-Sbomutil { <# .SYNOPSIS Downloads sbomutil from GCE to the local components directory. #> $gs_path = Get-MetadataValue -key 'sbom-util-gcs-root' if (!$gs_path) { Write-Output "No metadata sbom-util-gcs-root set, skipping sbomutil download." return } $gs_path = "${gs_path}/windows" $latest = gsutil ls "${gs_path}" | Select -Last 1 if (!$latest) { Write-Output "Could not determine sbomutil's latest release, skipping sbomutil download." return } # The variable $latest already has a backslash at the end, as a result of gsutil ls. Write-Output "Downloading sbomutil from $gs_path." & 'gsutil' -m cp "${latest}sbomutil.exe" "C:\sbomutil.exe" Write-Output 'Components download complete.' } function Generate-Sbom { <# .SYNOPSIS Generates sbom and upload the result to a gcs bucket. #> $gs_path = Get-MetadataValue -key 'sbom-destination' if (!$gs_path) { Write-Output "No metadata sbom-destination set, skipping sbom generation." return } if (!(Test-Path "C:\sbomutil.exe")) { Write-Output "Could not find sbomutil tool, skipping sbom generation." return } # Comp name is a short descriptor at the top of the sbom file for the software. $comp_name = Get-MetadataValue -key 'edition' $local_sbom_file = "image.sbom.json" Write-Output "Generating sbom." & "C:\sbomutil.exe" -archetype=windows-image -googet_path 'D:\ProgramData\GooGet' -extra_content="${script:sbom_dir}\" -comp_name="${comp_name}" -output "${local_sbom_file}" if (!(Test-Path $local_sbom_file)) { Write-Output "sbom generation failed, file not found" return } & 'gsutil' cp $local_sbom_file $gs_path Write-Output "Sbom file uploaded to $gs_path." } function Format-InstallDiskUEFI { <# .SYNOPSIS Clears and initializes disk 1 as GPT. Formats the install disk as D: and system partition as S: for UEFI boot. #> Write-Host 'Formatting install disk for UEFI.' Set-Disk -Number 1 -IsOffline $false Clear-Disk -Number 1 -RemoveData -Confirm:$false -ErrorAction SilentlyContinue Initialize-Disk -Number 1 -PartitionStyle GPT Write-Host 'Creating FAT32 system partition of 100MB and assigning volume drive S.' New-Partition -DiskNumber 1 -Size 100MB -DriveLetter S | Format-Volume -FileSystem 'FAT32' -Confirm:$false Write-Host 'Creating NTFS Windows partition and assigning volume drive D.' New-Partition -DiskNumber 1 -UseMaximumSize -DriveLetter D | Format-Volume -FileSystem 'NTFS' -Confirm:$false Write-Host 'Formatting UEFI install disk complete.' } function Format-InstallDiskMBR { <# .SYNOPSIS Clears and initializes disk 1 as MBR. Formats disk as NFTS and assigns as D: for MBR boot. #> Write-Host 'Formatting install disk for MBR.' Set-Disk -Number 1 -IsOffline $false Clear-Disk -Number 1 -RemoveData -Confirm:$false -ErrorAction SilentlyContinue Initialize-Disk -Number 1 -PartitionStyle MBR Write-Host 'Creating NTFS Windows partition and assigning volume drive D.' New-Partition -DiskNumber 1 -UseMaximumSize -DriveLetter D -IsActive | Format-Volume -FileSystem 'NTFS' -Confirm:$false Write-Host 'Formatting MBR install disk complete.' } function Get-Wim { <# .SYNOPSIS Mount the ISO and return the absolute path of the install.wim. #> $iso = "${script:components_dir}\windows.iso" if (Test-Path $iso) { $mount_result = Mount-DiskImage -ImagePath $iso -PassThru $iso_drive = ($mount_result | Get-Volume).DriveLetter } else { throw "Get-Wim: Failed to find iso:'$iso'. Boostrapping cannot continue." } Write-Host "ISO mounted as $iso_drive" $wim = "${iso_drive}:\sources\install.wim" if (-not (Test-Path $wim)) { throw "Get-Wim: Failed to find wim:'$wim'. Boostrapping cannot continue." } return $wim } function Bootstrap-InstallDisk { <# .SYNOPSIS Apply the image and do all the offline modifications to prepare the system to boot. .DESCRIPTION The following steps are completed in Bootstrap-InstallDisk: 1. Disable Defender for increased performance. 2. Apply the specified OS edition WIM to the install disk. 3. Apply the offline updates to the image applied to the install disk. 4. Update the autounattend.xml file and apply it to the image. 5. Copy the setupcomplete.cmd. 6. Copy netkvmco.dll and add the drivers to the image. 7. Setup bootloader. 8. Disable login animation. #> $slipstream_max_attemps = 3 $edition = Get-MetadataValue -key 'edition' # Disable Defender real time monitoring to greatly increase image # expansion and patch deployment speed. Set-MpPreference -DisableRealtimeMonitoring $true Write-Output "Applying $edition wim image to install disk." Expand-WindowsImage -ImagePath $(Get-Wim) -Name $edition -ApplyPath D:\ if (Test-Path $script:updates_dir) { Write-Output 'Slipstreaming updates into image in alphabetical order.' Get-ChildItem $script:updates_dir | ForEach-Object { Write-Output "Slipstreaming '$($_.Name)' into image." $path = $_.FullName for ($i=1; $i -le $slipstream_max_attemps; $i++) { try { Add-WindowsPackage -Path D:\ -PackagePath $path -ErrorAction Stop Write-Output "Successfully slipstreamed update $path on attempt $i." break } catch { Write-Output "Failed to slipstream update $path on attempt $i of $slipstream_max_attemps attempts." if ($i -eq 3) { throw "ERROR: Failed to slipstream update $path with max retry count exceeded." } } } } Write-Output 'Slipstreaming updates completed.' } Write-Output 'Populating Autounattend.xml file.' $autounattend_template = "${script:components_dir}\Autounattend-template.xml" $autounattend_file = "${script:components_dir}\Autounattend.xml" if (-not (Test-Path $autounattend_template)) { throw "Failed to find '${autounattend_template}'. Builder cannot continue." } $xml = [xml](Get-Content $autounattend_template) $product_key = Get-MetadataValue -key 'product-key' if ($product_key.length -gt 20) { Write-Output "Adding product key $product_key to Autounattend.xml." $new_element = $xml.CreateElement('ProductKey') $new_element.set_InnerText($product_key) $xml.unattend.settings[1].component[0].AppendChild($new_element) } $xml.Save($autounattend_file) Write-Output "autounattend-template.xml has been updated and saved to $autounattend_file." $panther = 'D:\Windows\Panther' if (Test-Path $panther) { Remove-Item $panther -Recurse -Force } New-Item $panther -Type Directory | Out-Null Copy-Item $autounattend_file "${panther}\unattend.xml" Write-Output 'Applying Unattend settings.' Use-WindowsUnattend -Path D:\ -UnattendPath $autounattend_file | Out-Null Write-Output 'Copying SetupComplete.cmd.' $scripts = 'D:\Windows\Setup\Scripts' New-Item $scripts -Type Directory | Out-Null Copy-Item "${script:components_dir}\SetupComplete.cmd" -Destination "${scripts}\SetupComplete.cmd" Write-Output 'Copying netkvmco.dll.' Copy-Item $script:driver_dir\netkvmco.dll D:\Windows\System32\netkvmco.dll # Add-WindowsDriver only works for Windows 10 x86 if ($script:x86) { Write-Output 'Slipstreaming drivers using DISM' DISM /Image:D: /Add-Driver /Driver:$script:driver_dir } else { Write-Output 'Slipstreaming drivers using Add-WindowsDriver' Add-WindowsDriver -Path D:\ -Driver $script:driver_dir -Recurse -Verbose } Write-Output 'Done applying image.' if (-not (Test-Path D:\Windows)) { throw 'Windows not installed!' } if ($script:uefi) { Write-Output 'Setting up UEFI bootloader.' & bcdboot D:\Windows /s S: /f UEFI Set-Partition -DriveLetter S -GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' } else { Write-Output 'Setting up MBR bootloader.' & bcdboot D:\Windows /s D: /f BIOS } Write-Output 'Disabling startup animation.' # See http://support.microsoft.com/kb/2955372/en-us reg load HKLM\MountedSoftware D:\Windows\System32\config\SOFTWARE reg add HKLM\MountedSoftware\Microsoft\Windows\CurrentVersion\Authentication\LogonUI /v AnimationDisabled /t REG_DWORD /d 1 /f reg unload HKLM\MountedSoftware } try { # Setup directories to store files which are added to the sbom, by storing files in sbom_dir. # Changing this path requires changing the path in image_build/windows/components/SetupComplete.cmd as well. $script:sbom_dir = 'C:\sbomcomponents' $script:components_dir = "$script:sbom_dir\components" $script:updates_dir = "$script:components_dir\updates" $script:driver_dir = "$script:components_dir\drivers" New-Item $script:sbom_dir -Type directory New-Item $script:updates_dir -Type directory New-Item $script:driver_dir -Type directory $script:uefi = (Get-MetadataValue -key 'uefi-build').ToLower() -eq 'true' $script:x86 = (Get-MetadataValue -key 'x86-build').ToLower() -eq 'true' Write-Output 'Boostrapping Windows install disk.' Download-Components Download-Drivers Download-Sbomutil if ($script:uefi) { Format-InstallDiskUEFI } else { Format-InstallDiskMBR } Bootstrap-InstallDisk $repo = Get-MetadataValue -key 'google-cloud-repo' Write-Output "Setting up GooGet repo $repo." & 'C:\ProgramData\GooGet\googet.exe' -root 'D:\ProgramData\GooGet' addrepo "google-compute-engine-${repo}" "https://packages.cloud.google.com/yuck/repos/google-compute-engine-${repo}" Generate-Sbom Write-Output 'Bootstrapping Complete, shutting down...' Stop-Computer } catch { Write-Output 'Exception caught in script:' Write-Output $_.InvocationInfo.PositionMessage Write-Output "Message: $($_.Exception.Message)" Write-Output 'Windows build failed.' exit 1 }