From 562e6413c50a0c231a5593702e0b53a2cd262f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domagoj=20Andri=C4=87?= Date: Sat, 7 Mar 2026 10:45:46 +0100 Subject: [PATCH] Add template for Windows Server 2022 --- ws2022/answer_files/Autounattend.xml.pkrtpl | 346 ++++++++++++++++++++ ws2022/main.pkr.hcl | 115 +++++++ ws2022/scripts/install-cloudbase-init.ps1 | 132 ++++++++ ws2022/scripts/sysprep.ps1 | 24 ++ ws2022/variables.pkr.hcl | 116 +++++++ 5 files changed, 733 insertions(+) create mode 100644 ws2022/answer_files/Autounattend.xml.pkrtpl create mode 100644 ws2022/main.pkr.hcl create mode 100644 ws2022/scripts/install-cloudbase-init.ps1 create mode 100644 ws2022/scripts/sysprep.ps1 create mode 100644 ws2022/variables.pkr.hcl diff --git a/ws2022/answer_files/Autounattend.xml.pkrtpl b/ws2022/answer_files/Autounattend.xml.pkrtpl new file mode 100644 index 0000000..f39b646 --- /dev/null +++ b/ws2022/answer_files/Autounattend.xml.pkrtpl @@ -0,0 +1,346 @@ + + + + + + + + + + + ${locale} + + ${locale} + ${locale} + ${locale} + ${locale} + + + + + + + ${virtio_drive}:\vioscsi\2k22\amd64 + + + ${virtio_drive}:\viostor\2k22\amd64 + + + ${virtio_drive}:\NetKVM\2k22\amd64 + + + + + + + + + + 0 + true + + + 1 + EFI + 100 + + + 2 + MSR + 128 + + + 3 + Primary + true + + + + + 1 + 1 + FAT32 + + + + 2 + 3 + NTFS + + C + + + + + + + + + 0 + 3 + + + + /IMAGE/INDEX + ${image_index} + + + + + + + true + + ${product_key} + Never + + + + + + + + + + + + + ${virtio_drive}:\vioscsi\2k22\amd64 + + + ${virtio_drive}:\viostor\2k22\amd64 + + + ${virtio_drive}:\NetKVM\2k22\amd64 + + + ${virtio_drive}:\Balloon\2k22\amd64 + + + ${virtio_drive}:\pvpanic\2k22\amd64 + + + ${virtio_drive}:\qxldod\2k22\amd64 + + + ${virtio_drive}:\vioserial\2k22\amd64 + + + ${virtio_drive}:\vioinput\2k22\amd64 + + + ${virtio_drive}:\viorng\2k22\amd64 + + + + + + + + + + + * + ${timezone} + + + + false + + + + true + + + + + + + + + + + + ${admin_password} + true</PlainText> + </Password> + <Enabled>true</Enabled> + <Username>Administrator</Username> + </AutoLogon> + + <UserAccounts> + <AdministratorPassword> + <Value>${admin_password}</Value> + <PlainText>true</PlainText> + </AdministratorPassword> + </UserAccounts> + + <FirstLogonCommands> + <SynchronousCommand wcm:action="add"> + <Order>1</Order> + <CommandLine + >cmd /c "${virtio_drive}:\virtio-win-guest-tools.exe /install /norestart -q"</CommandLine> + <Description>Install VirtIO drivers and QEMU Guest Agent</Description> + </SynchronousCommand> +<SynchronousCommand wcm:action="add"> + <Order>2</Order> + <CommandLine + >powershell -NoProfile -Command "Set-ExecutionPolicy Bypass -Scope LocalMachine -Force"</CommandLine> + <Description>Set execution policy</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>3</Order> + <CommandLine + >powershell -NoProfile -Command "Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private"</CommandLine> + <Description>Set network to Private</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>4</Order> + <CommandLine + >powershell -NoProfile -Command "Get-ChildItem WSMan:\localhost\Listener | Remove-Item -Recurse -ErrorAction SilentlyContinue"</CommandLine> + <Description>Remove existing WinRM listeners</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>5</Order> + <CommandLine + >powershell -NoProfile -Command "New-WSManInstance -ResourceURI winrm/config/Listener -SelectorSet @{Address='*';Transport='HTTP'}"</CommandLine> + <Description>Create WinRM HTTP listener</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>6</Order> + <CommandLine + >powershell -NoProfile -Command "Set-Item WSMan:\localhost\Service\AllowUnencrypted -Value True"</CommandLine> + <Description>Allow unencrypted WinRM</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>7</Order> + <CommandLine + >powershell -NoProfile -Command "Set-Item WSMan:\localhost\Service\Auth\Basic -Value True"</CommandLine> + <Description>Allow basic auth</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>8</Order> + <CommandLine + >powershell -NoProfile -Command "Set-Item WSMan:\localhost\MaxEnvelopeSizekb -Value 8192"</CommandLine> + <Description>Increase max envelope size</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>9</Order> + <CommandLine + >powershell -NoProfile -Command "Set-Service WinRM -StartupType Automatic"</CommandLine> + <Description>Set WinRM auto-start</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>10</Order> + <CommandLine + >powershell -NoProfile -Command "New-NetFirewallRule -DisplayName WinRM-HTTP -Direction Inbound -Action Allow -Protocol TCP -LocalPort 5985 -Profile Any"</CommandLine> + <Description>Open firewall port 5985</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>11</Order> + <CommandLine + >powershell -NoProfile -Command "Restart-Service WinRM -Force"</CommandLine> + <Description>Restart WinRM service</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>12</Order> + <CommandLine + >powershell -NoProfile -Command "Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0"</CommandLine> + <Description>Install OpenSSH Server</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>13</Order> + <CommandLine + >powershell -NoProfile -Command "Set-Service sshd -StartupType Automatic; Start-Service sshd"</CommandLine> + <Description>Enable and start sshd</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>14</Order> + <CommandLine + >powershell -NoProfile -Command "New-NetFirewallRule -DisplayName OpenSSH-Server -Direction Inbound -Action Allow -Protocol TCP -LocalPort 22 -Profile Any"</CommandLine> + <Description>Open firewall port 22</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>15</Order> + <CommandLine + >powershell -NoProfile -Command "$conf = 'C:\ProgramData\ssh\sshd_config'; (Get-Content $conf) -replace '#PubkeyAuthentication yes','PubkeyAuthentication yes' -replace 'Match Group administrators','#Match Group administrators' -replace ' AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys','# AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys' | Set-Content $conf; Restart-Service sshd"</CommandLine> + <Description>Configure sshd for pubkey auth</Description> + </SynchronousCommand> + </FirstLogonCommands> + + <OOBE> + <HideEULAPage>true</HideEULAPage> + <HideLocalAccountScreen>true</HideLocalAccountScreen> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <ProtectYourPC>3</ProtectYourPC> + </OOBE> + </component> + + <component + name="Microsoft-Windows-International-Core" + processorArchitecture="amd64" + publicKeyToken="31bf3856ad364e35" + language="neutral" + versionScope="nonSxS" + > + <InputLocale>${locale}</InputLocale> + <SystemLocale>${locale}</SystemLocale> + <UILanguage>${locale}</UILanguage> + <UserLocale>${locale}</UserLocale> + </component> + </settings> + +</unattend> diff --git a/ws2022/main.pkr.hcl b/ws2022/main.pkr.hcl new file mode 100644 index 0000000..2d2bf2a --- /dev/null +++ b/ws2022/main.pkr.hcl @@ -0,0 +1,115 @@ +packer { + required_plugins { + proxmox = { + version = "~> 1" + source = "github.com/hashicorp/proxmox" + } + windows-update = { + version = "0.17.3" + source = "github.com/rgl/windows-update" + } + } +} + +# ----------------------------------------------------------------------------- +# Source: Proxmox VM definition +# ----------------------------------------------------------------------------- +source "proxmox-iso" "ws2022" { + # Proxmox connection + proxmox_url = var.proxmox_url + username = var.proxmox_token_id + token = var.proxmox_token_secret + node = var.proxmox_node + insecure_skip_tls_verify = var.proxmox_skip_tls + + # VM metadata + vm_name = var.vm_name + template_description = "Windows Server 2022 Datacenter - Cloudbase-Init - Built ${formatdate("YYYY-MM-DD", timestamp())}" + vm_id = var.vm_id + + # Hardware + os = "win11" + memory = var.vm_memory + cores = var.vm_cores + cpu_type = "x86-64-v3" + machine = "pc-q35-10.1" + bios = "ovmf" + qemu_agent = true + + efi_config { + efi_storage_pool = var.storage_pool + efi_type = "4m" + pre_enrolled_keys = true + } + + scsi_controller = "virtio-scsi-single" + + disks { + type = "scsi" + disk_size = var.disk_size + storage_pool = var.storage_pool + format = "raw" + } + + network_adapters { + model = "virtio" + bridge = var.network_bridge + firewall = false + } + + # Windows Server 2022 ISO + iso_file = var.iso_file + + # --- KEY: dynamically generated ISO with templated Autounattend.xml --- + additional_iso_files { + cd_content = { + "Autounattend.xml" = templatefile("answer_files/Autounattend.xml.pkrtpl", { + admin_password = var.admin_password + image_index = var.image_index + product_key = var.product_key + locale = var.locale + timezone = var.timezone + virtio_drive = var.virtio_drive_letter + }) + } + cd_label = "OEMDRV" + iso_storage_pool = var.iso_storage_pool + unmount = true + } + + # VirtIO drivers ISO + additional_iso_files { + device = "sata1" + iso_file = var.virtio_iso + iso_storage_pool = var.iso_storage_pool + unmount = true + } + + # WinRM communicator - password injected from variable + communicator = "winrm" + winrm_username = "Administrator" + winrm_password = var.admin_password + winrm_timeout = "90m" + winrm_insecure = true + winrm_use_ssl = false + + boot_wait = "5s" + boot_command = ["<spacebar><spacebar><spacebar>"] +} + +# ----------------------------------------------------------------------------- +# Build: provisioning steps +# ----------------------------------------------------------------------------- +build { + sources = ["source.proxmox-iso.ws2022"] + + # Install and configure Cloudbase-Init + provisioner "powershell" { + script = "scripts/install-cloudbase-init.ps1" + } + + # Sysprep with Cloudbase-Init's unattend + provisioner "powershell" { + script = "scripts/sysprep.ps1" + } +} diff --git a/ws2022/scripts/install-cloudbase-init.ps1 b/ws2022/scripts/install-cloudbase-init.ps1 new file mode 100644 index 0000000..968c0a9 --- /dev/null +++ b/ws2022/scripts/install-cloudbase-init.ps1 @@ -0,0 +1,132 @@ +# install-cloudbase-init.ps1 +# Packer provisioner: downloads, installs and configures Cloudbase-Init +# for Proxmox cloud-init (ConfigDrive metadata service). + +$msiUrl = "https://cloudbase.it/downloads/CloudbaseInitSetup_Stable_x64.msi" +$msiPath = "C:\Windows\Temp\CloudbaseInitSetup.msi" +$cbDir = "C:\Program Files\Cloudbase Solutions\Cloudbase-Init" + +# ---- Download ---- +Write-Host "Downloading Cloudbase-Init..." +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$retries = 3 +for ($i = 1; $i -le $retries; $i++) { + try { + Invoke-WebRequest -Uri $msiUrl -OutFile $msiPath -UseBasicParsing + break + } catch { + Write-Host "Download attempt $i failed: $_" + if ($i -eq $retries) { throw "Failed to download Cloudbase-Init after $retries attempts" } + Start-Sleep -Seconds 10 + } +} +Unblock-File -Path $msiPath + +# ---- Install (silent, Local System account) ---- +Write-Host "Installing Cloudbase-Init..." +$proc = Start-Process msiexec.exe -ArgumentList "/i `"$msiPath`" /qn /norestart /l*v C:\Windows\Temp\cloudbase-install.log RUN_SERVICE_AS_LOCAL_SYSTEM=1" -Wait -PassThru + +Write-Host "MSI exit code: $($proc.ExitCode)" +if ($proc.ExitCode -ne 0) { + # Dump the MSI log on failure + if (Test-Path "C:\Windows\Temp\cloudbase-install.log") { + Get-Content "C:\Windows\Temp\cloudbase-install.log" | Select-Object -Last 50 + } + throw "Cloudbase-Init MSI install failed with exit code $($proc.ExitCode)" +} + +# Wait for MSI to finish writing files +Start-Sleep -Seconds 15 + +# ---- Verify installation ---- +Write-Host "Verifying Cloudbase-Init installation..." + +# Check the service exists +$svc = Get-Service -Name "cloudbase-init" -ErrorAction SilentlyContinue +if (-not $svc) { + Write-Host "Service not found. Listing directory..." + if (Test-Path $cbDir) { + Get-ChildItem $cbDir -Recurse -Depth 2 | Select-Object FullName + } else { + Write-Host "$cbDir does not exist!" + } + throw "Cloudbase-Init service not found after install!" +} + +Write-Host "Cloudbase-Init installed. Service status: $($svc.Status)" +Write-Host "Cloudbase-Init installed successfully." + +# ---- cloudbase-init.conf (main service - runs on normal boot after sysprep) ---- +$mainConf = @" +[DEFAULT] +username=Administrator +groups=Administrators +inject_user_password=true +config_drive_raw_hhd=true +config_drive_cdrom=true +config_drive_vfat=true +bsdtar_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exe +mtools_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\ +verbose=true +debug=true +log_dir=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\ +log_file=cloudbase-init.log +default_log_levels=comtypes=INFO,suds=INFO,iso8601=WARN,requests=WARN +logging_serial_port_settings= +first_logon_behaviour=no +mtu_use_dhcp_config=true +ntp_use_dhcp_config=true +local_scripts_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\LocalScripts\ +check_latest_version=false +plugins=cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin, + cloudbaseinit.plugins.common.networkconfig.NetworkConfigPlugin, + cloudbaseinit.plugins.common.setuserpassword.SetUserPasswordPlugin, + cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin +metadata_services=cloudbaseinit.metadata.services.configdrive.ConfigDriveService +"@ + +Write-Host "Writing cloudbase-init.conf..." +Set-Content -Path "$cbDir\conf\cloudbase-init.conf" -Value $mainConf -Encoding ASCII + +# ---- cloudbase-init-unattend.conf (specialize phase on first boot after sysprep) ---- +$unattendConf = @" +[DEFAULT] +username=Administrator +groups=Administrators +inject_user_password=false +config_drive_raw_hhd=true +config_drive_cdrom=true +config_drive_vfat=true +bsdtar_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exe +mtools_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\ +verbose=true +debug=true +log_dir=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\ +log_file=cloudbase-init-unattend.log +default_log_levels=comtypes=INFO,suds=INFO,iso8601=WARN,requests=WARN +logging_serial_port_settings= +mtu_use_dhcp_config=false +ntp_use_dhcp_config=false +first_logon_behaviour=no +local_scripts_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\LocalScripts\ +check_latest_version=false +plugins=cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin, + cloudbaseinit.plugins.common.networkconfig.NetworkConfigPlugin, + cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin +metadata_services=cloudbaseinit.metadata.services.configdrive.ConfigDriveService +allow_reboot=true +stop_service_on_exit=false +"@ + +Write-Host "Writing cloudbase-init-unattend.conf..." +Set-Content -Path "$cbDir\conf\cloudbase-init-unattend.conf" -Value $unattendConf -Encoding ASCII + +# ---- Service configuration ---- +Write-Host "Configuring Cloudbase-Init service..." +Set-Service -Name "cloudbase-init" -StartupType Automatic + +# ---- Cleanup ---- +Remove-Item $msiPath -Force -ErrorAction SilentlyContinue + +Write-Host "=== Cloudbase-Init setup complete ===" diff --git a/ws2022/scripts/sysprep.ps1 b/ws2022/scripts/sysprep.ps1 new file mode 100644 index 0000000..f239d5b --- /dev/null +++ b/ws2022/scripts/sysprep.ps1 @@ -0,0 +1,24 @@ +$ErrorActionPreference = "Stop" + +$cbUnattend = "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\conf\Unattend.xml" + +if (-not (Test-Path $cbUnattend)) { + throw "Cloudbase-Init Unattend.xml not found at: $cbUnattend" +} + +Write-Host "Removing Packer WinRM firewall rule..." +Remove-NetFirewallRule -DisplayName "WinRM HTTP" -ErrorAction SilentlyContinue + +Write-Host "Cleaning temp files..." +Remove-Item -Path "C:\Windows\Temp\*" -Recurse -Force -ErrorAction SilentlyContinue + +Write-Host "Enabling built-in Administrator account..." +net user Administrator /active:yes +net user Administrator /logonpasswordchg:no + +Write-Host "Running Sysprep /generalize /oobe /shutdown..." +& "$env:SystemRoot\System32\Sysprep\Sysprep.exe" ` + /generalize ` + /oobe ` + /shutdown ` + /unattend:"$cbUnattend" diff --git a/ws2022/variables.pkr.hcl b/ws2022/variables.pkr.hcl new file mode 100644 index 0000000..921d3cf --- /dev/null +++ b/ws2022/variables.pkr.hcl @@ -0,0 +1,116 @@ +variable "proxmox_url" { + type = string + description = "Proxmox API URL (e.g. https://pve.example.com:8006/api2/json)" +} + +variable "proxmox_token_id" { + type = string + sensitive = true + description = "Proxmox API token ID (e.g. user@pam!packer)" +} + +variable "proxmox_token_secret" { + type = string + sensitive = true + description = "Proxmox API token secret" +} + +variable "proxmox_node" { + type = string + description = "Proxmox node name to build on" +} + +variable "proxmox_skip_tls" { + type = bool + default = true +} + +variable "vm_name" { + type = string + default = "ws2022dc-template" +} + +variable "vm_id" { + type = number + default = 1002 +} + +variable "vm_memory" { + type = number + default = 4096 +} + +variable "vm_cores" { + type = number + default = 2 +} + +variable "disk_size" { + type = string + default = "60G" +} + +variable "network_bridge" { + type = string + default = "vmbr0" +} + +variable "storage_pool" { + type = string + default = "local-lvm" + description = "Proxmox storage pool for VM disks and EFI" +} + +variable "iso_storage_pool" { + type = string + default = "local" + description = "Proxmox storage pool where ISOs live and generated ISOs are placed" +} + +variable "iso_file" { + type = string + description = "Path to Windows Server 2022 ISO in Proxmox (e.g. local:iso/WindowsServer2022.iso)" + default = "local-btrfs:iso/en-us_windows_server_2022_updated_july_2023_x64_dvd_541692c3.iso" +} + +variable "virtio_iso" { + type = string + default = "local-btrfs:iso/virtio-win-0.1.285.iso" + description = "Path to VirtIO drivers ISO in Proxmox" +} + +variable "virtio_drive_letter" { + type = string + default = "E" + description = "Drive letter where the VirtIO ISO is mounted inside Windows" +} + +variable "admin_password" { + type = string + sensitive = true + description = "Administrator password used during build (will be cleared by sysprep)" +} + +variable "image_index" { + type = string + default = "4" + description = "Windows image index from eval ISO: 1=Std Core, 2=Std Desktop, 3=DC Core, 4=DC Desktop (verify with dism /Get-ImageInfo)" +} + +variable "product_key" { + type = string + default = "WX4NM-KYWYW-QJJR4-XV3QB-6VM33" + description = "KMS GVLK for Windows Server 2022 Datacenter" +} + +variable "locale" { + type = string + default = "en-US" + description = "Windows locale for UI, input, system (e.g. en-US, hr-HR)" +} + +variable "timezone" { + type = string + default = "Central European Standard Time" + description = "Windows timezone name" +}