80-Get-UpdateStatus-Admin.ps1 prints whether the Windows 11 guest is upgrading or downloading updates (policy, pending markers, services, processes, BITS jobs, update events, network use); the .bat launcher elevates and keeps the window open. The seed ISO and Configure-Windows.ps1 now take every NN-*.bat and NN-*.ps1, and the first-logon copy goes to Desktop\Tools instead of Desktop\Installers. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
234 lines
8.7 KiB
PowerShell
234 lines
8.7 KiB
PowerShell
param(
|
|
[switch]$InstallVirtIO,
|
|
[switch]$EnableAutomaticLogon,
|
|
[switch]$DisableAutoUpdate,
|
|
[switch]$CopyInstallers,
|
|
[switch]$InstallOpenSSH,
|
|
[switch]$NoPause
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
Set-StrictMode -Version Latest
|
|
|
|
function Test-Administrator {
|
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
|
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
}
|
|
|
|
if (-not (Test-Administrator)) {
|
|
if (-not $PSCommandPath) {
|
|
throw 'Run this script from a PowerShell window opened as Administrator.'
|
|
}
|
|
|
|
$arguments = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"")
|
|
foreach ($name in $PSBoundParameters.Keys) {
|
|
$arguments += "-$name"
|
|
}
|
|
|
|
$elevated = Start-Process `
|
|
-FilePath 'powershell.exe' `
|
|
-ArgumentList $arguments `
|
|
-Verb RunAs `
|
|
-Wait `
|
|
-PassThru
|
|
exit $elevated.ExitCode
|
|
}
|
|
|
|
# The guest tools installer is an x86 program and the VirtIO ISO has no Arm64
|
|
# package for it, so on Arm64 Windows the Arm64 drivers are installed from their
|
|
# folders on the ISO instead. NetKVM is the network driver; without it the guest
|
|
# has no network.
|
|
function Install-VirtIOArm64Drivers {
|
|
$roots = (Get-PSDrive -PSProvider FileSystem).Root
|
|
$drivers = @(
|
|
@{ Folder = 'NetKVM'; Inf = 'netkvm.inf' },
|
|
@{ Folder = 'vioserial'; Inf = 'vioser.inf' },
|
|
@{ Folder = 'Balloon'; Inf = 'balloon.inf' },
|
|
@{ Folder = 'viorng'; Inf = 'viorng.inf' },
|
|
@{ Folder = 'vioinput'; Inf = 'vioinput.inf' },
|
|
@{ Folder = 'viogpudo'; Inf = 'viogpudo.inf' }
|
|
)
|
|
|
|
$installed = 0
|
|
foreach ($driver in $drivers) {
|
|
$inf = $roots |
|
|
ForEach-Object { Join-Path $_ "$($driver.Folder)\w11\ARM64\$($driver.Inf)" } |
|
|
Where-Object { Test-Path -LiteralPath $_ } |
|
|
Select-Object -First 1
|
|
if (-not $inf) {
|
|
if ($driver.Folder -eq 'NetKVM') {
|
|
throw 'The Arm64 NetKVM driver was not found on the attached media.'
|
|
}
|
|
continue
|
|
}
|
|
|
|
& pnputil.exe /add-driver $inf /install | Out-Host
|
|
# 3010 asks for a restart; 259 means that no device uses the driver.
|
|
if ($LASTEXITCODE -notin 0, 259, 3010) {
|
|
throw "pnputil failed to install $inf (exit code $LASTEXITCODE)."
|
|
}
|
|
$installed++
|
|
}
|
|
Write-Host "Installed $installed VirtIO Arm64 driver packages."
|
|
}
|
|
|
|
function Install-VirtIOGuestTools {
|
|
if ([Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') {
|
|
Install-VirtIOArm64Drivers
|
|
return
|
|
}
|
|
|
|
$installer = Get-ChildItem `
|
|
-Path (Get-PSDrive -PSProvider FileSystem).Root `
|
|
-Filter 'virtio-win-guest-tools.exe' `
|
|
-File `
|
|
-ErrorAction SilentlyContinue |
|
|
Select-Object -First 1
|
|
|
|
if (-not $installer) {
|
|
throw 'virtio-win-guest-tools.exe was not found on the attached media.'
|
|
}
|
|
|
|
$process = Start-Process `
|
|
-FilePath $installer.FullName `
|
|
-ArgumentList '/quiet', '/norestart' `
|
|
-Wait `
|
|
-PassThru
|
|
|
|
if ($process.ExitCode -notin 0, 3010) {
|
|
throw "VirtIO guest tools installer exited with code $($process.ExitCode)."
|
|
}
|
|
}
|
|
|
|
function Enable-AutomaticLogon {
|
|
$winlogonPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
|
|
|
|
Set-ItemProperty -Path $winlogonPath -Name 'AutoAdminLogon' -Value '1' -Type String
|
|
Set-ItemProperty -Path $winlogonPath -Name 'DefaultUserName' -Value 'guofang' -Type String
|
|
Set-ItemProperty -Path $winlogonPath -Name 'DefaultPassword' -Value 'wang' -Type String
|
|
Set-ItemProperty -Path $winlogonPath -Name 'DefaultDomainName' -Value $env:COMPUTERNAME -Type String
|
|
Remove-ItemProperty -Path $winlogonPath -Name 'AutoLogonCount' -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
function Disable-AutomaticUpdate {
|
|
$windowsUpdatePath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
|
|
$autoUpdatePath = Join-Path $windowsUpdatePath 'AU'
|
|
$storePath = 'HKLM:\SOFTWARE\Policies\Microsoft\WindowsStore'
|
|
$currentVersion = Get-ItemProperty `
|
|
-Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
|
|
$releaseVersion = $currentVersion.DisplayVersion
|
|
|
|
New-Item -Path $autoUpdatePath -Force | Out-Null
|
|
New-Item -Path $storePath -Force | Out-Null
|
|
|
|
# Never check automatically; manual checks and installs keep working.
|
|
Set-ItemProperty -Path $autoUpdatePath -Name 'NoAutoUpdate' -Value 1 -Type DWord
|
|
Set-ItemProperty -Path $autoUpdatePath -Name 'NoAutoRebootWithLoggedOnUsers' -Value 1 -Type DWord
|
|
Set-ItemProperty -Path $windowsUpdatePath -Name 'ExcludeWUDriversInQualityUpdate' -Value 1 -Type DWord
|
|
|
|
# Stay on the installed feature release instead of upgrading automatically.
|
|
if ($releaseVersion) {
|
|
Set-ItemProperty -Path $windowsUpdatePath -Name 'TargetReleaseVersion' -Value 1 -Type DWord
|
|
Set-ItemProperty -Path $windowsUpdatePath -Name 'TargetReleaseVersionInfo' -Value $releaseVersion -Type String
|
|
}
|
|
|
|
# 2 = never download Microsoft Store app updates automatically.
|
|
Set-ItemProperty -Path $storePath -Name 'AutoDownload' -Value 2 -Type DWord
|
|
|
|
# Windows may re-enable this task, so the policies above are the real switch.
|
|
Disable-ScheduledTask `
|
|
-TaskPath '\Microsoft\Windows\UpdateOrchestrator\' `
|
|
-TaskName 'Schedule Scan' `
|
|
-ErrorAction SilentlyContinue | Out-Null
|
|
}
|
|
|
|
function Copy-InstallerScripts {
|
|
# Numbered scripts (NN-*.bat / NN-*.ps1: installers and tools) go to Desktop\Tools.
|
|
$scripts = @(Get-ChildItem -Path $PSScriptRoot -File |
|
|
Where-Object { $_.Name -match '^\d\d-.*\.(bat|ps1)$' })
|
|
if ($scripts.Count -eq 0) {
|
|
throw "No installer batch files were found next to $PSCommandPath."
|
|
}
|
|
|
|
$target = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Tools'
|
|
New-Item -ItemType Directory -Path $target -Force | Out-Null
|
|
Copy-Item -Path $scripts.FullName -Destination $target -Force
|
|
}
|
|
|
|
# Installs the OpenSSH Server from the Win32-OpenSSH zip on the answer media,
|
|
# so no Windows Update download is needed.
|
|
function Install-OpenSSHServer {
|
|
$zip = Get-ChildItem -Path $PSScriptRoot -Filter 'OpenSSH-*.zip' -File |
|
|
Select-Object -First 1
|
|
if (-not $zip) {
|
|
throw "No OpenSSH-*.zip was found next to $PSCommandPath."
|
|
}
|
|
|
|
$installDir = Join-Path $env:ProgramFiles 'OpenSSH'
|
|
if (-not (Test-Path -LiteralPath (Join-Path $installDir 'install-sshd.ps1'))) {
|
|
$temp = Join-Path $env:TEMP 'openssh'
|
|
Remove-Item $temp -Recurse -Force -ErrorAction SilentlyContinue
|
|
Expand-Archive -LiteralPath $zip.FullName -DestinationPath $temp
|
|
Remove-Item $installDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
Move-Item (Get-ChildItem $temp -Directory | Select-Object -First 1).FullName $installDir
|
|
Remove-Item $temp -Recurse -Force
|
|
}
|
|
|
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $installDir 'install-sshd.ps1') | Out-Host
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "install-sshd.ps1 failed (exit code $LASTEXITCODE)."
|
|
}
|
|
|
|
Set-Service -Name sshd -StartupType Automatic
|
|
Start-Service -Name sshd
|
|
|
|
if (-not (Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue)) {
|
|
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' `
|
|
-Direction Inbound -Action Allow -Protocol TCP -LocalPort 22 -Profile Private, Public | Out-Null
|
|
}
|
|
|
|
# build-seed.sh puts the host's public key on the media; without it, only
|
|
# password login works.
|
|
$key = Join-Path $PSScriptRoot 'authorized_keys'
|
|
if (-not (Test-Path -LiteralPath $key)) {
|
|
Write-Warning "No authorized_keys was found next to $PSCommandPath; skipping the administrator key."
|
|
return
|
|
}
|
|
$keyFile = Join-Path $env:ProgramData 'ssh\administrators_authorized_keys'
|
|
Copy-Item -LiteralPath $key -Destination $keyFile -Force
|
|
& icacls.exe $keyFile /inheritance:r /grant '*S-1-5-32-544:F' /grant '*S-1-5-18:F' | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "icacls failed (exit code $LASTEXITCODE)."
|
|
}
|
|
}
|
|
|
|
$exitCode = 0
|
|
try {
|
|
if ($InstallVirtIO) {
|
|
Install-VirtIOGuestTools
|
|
}
|
|
if ($EnableAutomaticLogon) {
|
|
Enable-AutomaticLogon
|
|
}
|
|
if ($DisableAutoUpdate) {
|
|
Disable-AutomaticUpdate
|
|
}
|
|
if ($CopyInstallers) {
|
|
Copy-InstallerScripts
|
|
}
|
|
if ($InstallOpenSSH) {
|
|
Install-OpenSSHServer
|
|
}
|
|
Write-Host 'Done.' -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host "Failed: $($_.Exception.Message)" -ForegroundColor Red
|
|
$exitCode = 1
|
|
}
|
|
|
|
if (-not $NoPause) {
|
|
Read-Host 'Press Enter to exit' | Out-Null
|
|
}
|
|
exit $exitCode
|