diff --git a/README.md b/README.md index b1714c3..139e940 100644 --- a/README.md +++ b/README.md @@ -174,9 +174,12 @@ VirtIO ISO with `pnputil` on arm64), without which the guest has no network on arm64. The installer includes the VirtIO drivers, SPICE vdagent, and QEMU Guest Agent. The launcher exposes the vdagent channel for host/guest clipboard integration and the QEMU Guest Agent at `.vm/windows11/qemu-guest-agent.sock`. -Change the password after first login. The OpenSSH Server is downloaded through -Windows Update, which can be slow, so it is not installed automatically: run -`05-Install-OpenSSH-Admin.bat` from the `Installers` folder on the desktop as +Change the password after first login. The OpenSSH Server is not installed +automatically. Run `05-Install-OpenSSH-Admin.bat` from the `Installers` folder on +the desktop to install it through Windows Update, which can be slow, or +`06-Install-OpenSSH-Zip-Admin.bat` to install it from the Win32-OpenSSH zip for +the host architecture, which the seed ISO carries (downloaded by +`vm-images/download-images.sh openssh-win`), as administrator, then connect with `ssh guofang@localhost -p 2222`; it also installs the host's configured SSH public key for passwordless administrator login. The Windows VM hardware (8G memory, 8 CPUs, system disk, devices) diff --git a/run-windows11.py b/run-windows11.py index 4e530ff..8e28e21 100755 --- a/run-windows11.py +++ b/run-windows11.py @@ -304,11 +304,17 @@ if installing_from_iso: seed_sources = [seed_dir / "Autounattend.xml", seed_dir / "Configure-Windows.ps1", *sorted(seed_dir.glob("[0-9][0-9]-*.bat"))] + # The seed carries the OpenSSH zip for this architecture; build-seed.sh + # downloads it when missing. + seed_sources.append(Path(capture( + [str(image_downloader), "--print-path", "openssh-win"]))) + def newer_than_seed(source): # Mirrors bash `-nt`: true if source exists and is newer than seed_iso. return source.exists() and source.stat().st_mtime > seed_iso.stat().st_mtime - if not seed_iso.is_file() or any(newer_than_seed(s) for s in seed_sources): + if not seed_iso.is_file() or not seed_sources[-1].is_file() \ + or any(newer_than_seed(s) for s in seed_sources): run([str(script_dir / "seediso" / "windows11" / "build-seed.sh"), str(seed_iso), host_arch]) if pid_file.is_file(): diff --git a/seediso/windows11/06-Install-OpenSSH-Zip-Admin.bat b/seediso/windows11/06-Install-OpenSSH-Zip-Admin.bat new file mode 100644 index 0000000..89ad403 --- /dev/null +++ b/seediso/windows11/06-Install-OpenSSH-Zip-Admin.bat @@ -0,0 +1,65 @@ +@echo off +setlocal +title Install OpenSSH Server from zip + +fltmc >nul 2>&1 +if errorlevel 1 ( + echo Please right-click this file and choose "Run as administrator". + pause + exit /b 1 +) + +set "ZIP=" +for %%Z in ("%~dp0OpenSSH-*.zip") do set "ZIP=%%~fZ" +if not defined ZIP ( + echo No OpenSSH-*.zip was found next to this file. + pause + exit /b 1 +) + +echo Installing the OpenSSH Server from %ZIP%... +if not exist "%ProgramFiles%\OpenSSH\install-sshd.ps1" ( + powershell.exe -NoProfile -ExecutionPolicy Bypass -Command ^ + "$t = Join-Path $env:TEMP 'openssh'; Remove-Item $t -Recurse -Force -ErrorAction SilentlyContinue; Expand-Archive -LiteralPath $env:ZIP -DestinationPath $t; Remove-Item (Join-Path $env:ProgramFiles 'OpenSSH') -Recurse -Force -ErrorAction SilentlyContinue; Move-Item (Get-ChildItem $t -Directory | Select-Object -First 1).FullName (Join-Path $env:ProgramFiles 'OpenSSH'); Remove-Item $t -Recurse -Force" + if errorlevel 1 ( + echo Extracting the zip failed. + pause + exit /b 1 + ) +) +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%ProgramFiles%\OpenSSH\install-sshd.ps1" +if errorlevel 1 ( + echo install-sshd.ps1 failed. + pause + exit /b 1 +) + +echo Starting the sshd service... +sc.exe config sshd start= auto +net.exe start sshd >nul 2>&1 +sc.exe query sshd | find "RUNNING" >nul +if errorlevel 1 ( + echo The sshd service is not running. + pause + exit /b 1 +) + +netsh.exe advfirewall firewall show rule name="OpenSSH-Server-In-TCP" >nul 2>&1 +if errorlevel 1 ( + netsh.exe advfirewall firewall add rule name="OpenSSH-Server-In-TCP" ^ + dir=in action=allow protocol=TCP localport=22 profile=private,public +) + +echo Installing the administrator authorized key... +set "KEYFILE=%ProgramData%\ssh\administrators_authorized_keys" +> "%KEYFILE%" echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCwsF2ZDSHDaXM9BdDq3Vxkp6T+DJEakKaVZQJUEHexS34mbrxYAxmGBUPCrddfE01dS31kGS4XHVBa+wGUYodY+nPwnrfD1Q4phKVSzXr65K1Y/sCcjSiANBPWpv2Xd7IMOfLlrdLtmf5QcKWroIqO2LaIDGECwzUUHrfprgeaF9N0FGcmrgbmm23JUk+f6guRQ4br88L4rWIWEBvL9bTdwErNkdfsYE2F/U8aIFpMcVy2eJIOgy8ZjZvkGdm4r730YkTDL1VmAjiHH3/m2R1EOvwLzFhyWKoa4ULk/IQW+8iNnG42QEcaWRM+hMwlVdnNhPJP6WzS+fAEbibD3zM0q2X/4ex+cXRNOCd2/0KhnD2+3p1BM5QeFTzDappkk8W2meQkDE3hvTjtIQCzygFO6LNWtEEa7BTvvlMqF3CZWh88J9XiP750SjESe3Ge5o9A0T2hcJa+KZ1LxVEmNt4nD6E/OD4EymoaLIJZ1fH5ksSFjHzJ+mJouSVjc/H/VSE= guofang@GUOFANG-OFFICE +icacls.exe "%KEYFILE%" /inheritance:r /grant "*S-1-5-32-544:F" /grant "*S-1-5-18:F" >nul +if errorlevel 1 ( + echo icacls exited with code %ERRORLEVEL%. + pause + exit /b 1 +) + +echo Done. SSH is available on port 22 of this machine. +pause +exit /b 0 diff --git a/seediso/windows11/Configure-Windows.ps1 b/seediso/windows11/Configure-Windows.ps1 index c9e459b..de3b459 100644 --- a/seediso/windows11/Configure-Windows.ps1 +++ b/seediso/windows11/Configure-Windows.ps1 @@ -151,6 +151,12 @@ function Copy-InstallerScripts { $target = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Installers' New-Item -ItemType Directory -Path $target -Force | Out-Null Copy-Item -Path $scripts.FullName -Destination $target -Force + + # 05-Install-OpenSSH-Admin.bat installs from this zip when it is present. + $zips = @(Get-ChildItem -Path $PSScriptRoot -Filter 'OpenSSH-*.zip' -File) + if ($zips.Count -gt 0) { + Copy-Item -Path $zips.FullName -Destination $target -Force + } } $exitCode = 0 diff --git a/seediso/windows11/build-seed.sh b/seediso/windows11/build-seed.sh index de2cd79..0419a22 100755 --- a/seediso/windows11/build-seed.sh +++ b/seediso/windows11/build-seed.sh @@ -10,6 +10,13 @@ case "$arch" in *) printf 'Error: unsupported architecture: %s\n' "$arch" >&2; exit 1 ;; esac +# The OpenSSH zip for the target architecture comes from the shared image +# helper, which downloads it when missing. +image_helper="${VM_IMAGE_HELPER:-$script_dir/../../vm-images/download-images.sh}" +openssh_zip="$(VM_IMAGE_ARCH="$answer_arch" "$image_helper" --print-path openssh-win)" +[[ -s "$openssh_zip" ]] || VM_IMAGE_ARCH="$answer_arch" "$image_helper" openssh-win +[[ -s "$openssh_zip" ]] || { printf 'Error: %s was not downloaded.\n' "$openssh_zip" >&2; exit 1; } + # The answer file's components name the processor architecture, so render a # copy for the target architecture. work_dir="$(mktemp -d)" @@ -20,6 +27,7 @@ sed "s/processorArchitecture=\"amd64\"/processorArchitecture=\"${answer_arch}\"/ seed_files=( "$work_dir/Autounattend.xml" "$script_dir/Configure-Windows.ps1" + "$openssh_zip" ) # Numbered installer scripts (10-Install-Git-Admin.bat, 20-..., in install order). shopt -s nullglob diff --git a/vm-images b/vm-images index f5881cd..5e58722 160000 --- a/vm-images +++ b/vm-images @@ -1 +1 @@ -Subproject commit f5881cd5c8bec4a64f1a2d7e0af6fc4128d2817a +Subproject commit 5e58722aca71e30feab903cd2c36ba827ef2bad6