diff --git a/README.md b/README.md index bcde2702..0f4ac931 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ Read more about how [custom images](https://maas.io/docs/how-to-customise-images | VMWare ESXi 6 | EOL | >= 3.0 | | VMWare ESXi 7 | Stable | >= 3.0 | | VMWare ESXi 8 | Beta | >= 3.0 | +| Windows 2016 | Beta | >= 3.3 | +| Windows 2019 | Beta | >= 3.3 | +| Windows 2022 | Beta | >= 3.3 | +| Windows 10 | Beta | >= 3.3 | ### Maturity level diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 00000000..575c0ea5 --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1 @@ +http/Autounattend.xml diff --git a/windows/Makefile b/windows/Makefile new file mode 100644 index 00000000..35b681bc --- /dev/null +++ b/windows/Makefile @@ -0,0 +1,65 @@ +#!/usr/bin/make -f + +include ../scripts/check.mk + +PACKER ?= packer +PACKER_LOG ?= 0 +export PACKER_LOG + +ISO ?= +VERSION ?= 2022 +BOOT ?= uefi +HEADLESS ?= false + +ifeq ($(strip $(VERSION)),10) + TYPE = Windows + EDIT ?= PRO +else ifeq ($(strip $(VERSION)),11) + TYPE = Windows + EDIT ?= PRO +else + TYPE = Windows Server + EDIT ?= SERVERSTANDARD +endif + +ifeq ($(wildcard /usr/share/OVMF/OVMF_CODE.fd),) + OVMF_SFX ?= _4M +else + OVMF_SFX ?= +endif + +.PHONY: all clean + +all: windows + +$(eval $(call check_packages_deps,cloud-image-utils ovmf,cloud-image-utils ovmf)) + +OVMF_VARS.fd: /usr/share/OVMF/OVMF_VARS*.fd + cp -v $< $@ + +http/Autounattend.xml: http/Autounattend.xml.${BOOT}.template + sed s#@VERSION@#"${TYPE} ${VERSION} ${EDIT}"#g $< > $@ +ifneq ($(strip $(PKEY1)),) + sed -i s#@PKEY@#${PKEY}#g $@ + sed -i 's//<\/ProductKey>/' $@ +endif +ifeq ($(strip $(VERSION)),10) + sed -i 's//<\/LocalAccounts>/' $@ +else ifeq ($(strip $(VERSION)),11) + sed -i 's//<\/LocalAccounts>/' $@ +endif + +extra_drivers: + mkisofs -o drivers.iso -V 'Extra Drivers' -input-charset utf-8 drivers + +.INTERMEDIATE: extra_drivers http/Autounattend.xml + +windows: check-deps clean extra_drivers http/Autounattend.xml OVMF_VARS.fd + ${PACKER} init . && ${PACKER} build -var iso_path=${ISO} \ + -var headless=${HEADLESS} \ + -var ovmf_suffix=${OVMF_SFX} \ + windows.pkr.hcl + +clean: + ${RM} -rf output-* windows.tar.gz http/Autounattend.xml \ + drivers.iso OVMF_VARS.fd diff --git a/windows/README.md b/windows/README.md new file mode 100644 index 00000000..f1e12f93 --- /dev/null +++ b/windows/README.md @@ -0,0 +1,118 @@ +# Packer Template for Microsoft Windows + +## Introduction + +The Packer templates in this directory creates Windows Server images for use with MAAS. + + +## Prerequisites (to create the image) + +* A machine running Ubuntu 18.04+ with the ability to run KVM virtual machines. +* qemu-utils, libnbd-bin, nbdkit and fuse2fs +* qemu-system +* ovmf +* cloud-image-utils +* [Packer](https://www.packer.io/intro/getting-started/install.html), v1.7.0 or newer + + +## Requirements (to deploy the image) + +* [MAAS](https://maas.io) 3.2+ +* [Curtin](https://launchpad.net/curtin) 21.0+ + + +## Supported Microsoft Windows Versions + +This process has been build and deployment tested with the following versions of +Microsoft Windows: + +* Windows Server 2022 +* Windows Server 2019 +* Windows Server 2016 +* Windows 10 Pro 22H2 + + +## Known Issues + +* The current process builds UEFI compatible images only. + + +## windows.json Template + +This template builds a dd.tgz MAAS image from an official Microsoft Windows ISO. +This process also installs the latest VirtIO drivers as well as Cloudbase-init. + + +## Obtaining Microsoft Windows ISO images + +You can obtains Microsoft Windows Evaluation ISO images from the following links: + +* [Windows Server 2022](https://www.microsoft.com/en-us/evalcenter/download-windows-server-2022) +* [Windows Server 2019](https://www.microsoft.com/en-us/evalcenter/download-windows-server-2019) +* [Windows Server 2016](https://www.microsoft.com/en-us/evalcenter/download-windows-server-2016) + + +### Building the image + +The build the image you give the template a script which has all the +customization: + +```shell +sudo make windows ISO= VERSION= windows.json +``` + +### Makefile Parameters + +#### BOOT + +Currently uefi is the only supported value. + +#### EDIT + +The edition of a targeted ISO image. It defaults to PRO for Microsoft Windows 10/11 +and SERVERSTANDARD for Microsoft Windows Servers. Many Microsoft Windows Server ISO +images do contain multiple editions and this prarameter is useful to build a particular +edition such as Standard or Datacenter etc. + +#### HEADLESS + +Whether VNC viewer should not be launched. Default is set to false. + +#### ISO + +Path to Microsoft Windows ISO used to build the image. + +#### PACKER_LOG + +Enable (1) or Disable (0) verbose packer logs. The default value is set to 0. + +#### PKEY + +User supplied Microsoft Windows Product Key. When usimg KMS, you can obtain the +activation keys from the link below: + +* [KMS Client Activation and Product Keys](https://learn.microsoft.com/en-us/windows-server/get-started/kms-client-activation-keys) + +Please note that PKEY is an optional parameter but it might be required during +the build time depending on the type of ISO being used. Evaluation series ISO +images usually do not require a product key to proceed, however this is not +true with Enterprise and Retail ISO images. + +#### VERSION + +Specify the Microsoft Windows Version. Example inputs include: 2022, 2019, 2016 +and 10. + + +## Uploading images to MAAS + +Use MAAS CLI to upload the image: + +```shell +maas admin boot-resources create \ + name='windows/windows-server' \ + title='Windows Server' \ + architecture='amd64/generic' \ + filetype='ddtgz' \ + content@=windows-server-amd64-root-dd.gz +``` diff --git a/windows/TODO.md b/windows/TODO.md new file mode 100644 index 00000000..5965f5fd --- /dev/null +++ b/windows/TODO.md @@ -0,0 +1,7 @@ +## To be implemented (TODO List) + +* Complete the support for build-time driver injection. +* Add support for BIOS based deployments. +* Add support for Windows 11 which requires TPM 2.0. +* Migrate scripts/setup-nbd to use fuse. + diff --git a/windows/curtin/README.md b/windows/curtin/README.md new file mode 100644 index 00000000..cf727c2b --- /dev/null +++ b/windows/curtin/README.md @@ -0,0 +1,3 @@ +This has been cloned from the following Git repository: + +[Windows Curtin Hooks](https://github.com/cloudbase/windows-curtin-hooks.git) diff --git a/windows/curtin/curtin-hooks b/windows/curtin/curtin-hooks new file mode 100644 index 00000000..a9bf588e --- /dev/null +++ b/windows/curtin/curtin-hooks @@ -0,0 +1 @@ +#!/bin/bash diff --git a/windows/curtin/finalize b/windows/curtin/finalize new file mode 100644 index 00000000..246f1b9d --- /dev/null +++ b/windows/curtin/finalize @@ -0,0 +1,26 @@ +#!/bin/bash + +PYTHON2=`/bin/which python` +PYTHON3=`/bin/which python3` + +PYTHON=${PYTHON2:-$PYTHON3} + +if [ -z $PYTHON ] +then + echo "Failed to find python interpretor" + exit 1 +fi + +SCRIPT_DIR=`/usr/bin/dirname $0` +ABSPATH=`/bin/readlink -m $SCRIPT_DIR` + +FINALIZE="$ABSPATH/finalize.py" + +if [ ! -e $FINALIZE ] +then + echo "No finalize script available" + exit 0 +fi + +$PYTHON $FINALIZE + diff --git a/windows/curtin/finalize.py b/windows/curtin/finalize.py new file mode 100644 index 00000000..a7a991f1 --- /dev/null +++ b/windows/curtin/finalize.py @@ -0,0 +1,145 @@ +# +# Copyright 2015 Cloudbase Solutions SRL +# +# 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. + + +import os +import sys +import tempfile +import yaml +import platform +import json + +from curtin.log import LOG +from curtin import util +try: + from curtin.util import load_command_config +except ImportError: + from curtin.config import load_command_config + +CLOUDBASE_INIT_TEMPLATE = """ +metadata_services=cloudbaseinit.metadata.services.maasservice.MaaSHttpService +maas_metadata_url={url} +maas_oauth_consumer_key={consumer_key} +maas_oauth_consumer_secret='' +maas_oauth_token_key={token_key} +maas_oauth_token_secret={token_secret} +""" + +CHANGE_LICENSE_TPL = """ +slmgr /ipk {license_key} +""" + +def get_oauth_data(state): + cfg = state.get("debconf_selections") + if not cfg: + return None + maas = cfg.get("maas") + if not maas: + return None + data = {i.split(None, 3)[1].split("/")[1]: i.split(None, 3)[-1] for i in maas.split("\n") if len(i) > 0} + oauth_data = data.get("maas-metadata-credentials") + metadata_url = data.get("maas-metadata-url") + if oauth_data is None or metadata_url is None: + return None + oauth_dict = {k.split("=")[0].split("_",1)[1]: k.split("=")[1] for k in oauth_data.split("&")} + oauth_dict["url"] = metadata_url + return oauth_dict + +def get_cloudbaseinit_dir(target): + dirs = [ + os.path.join( + target, + "Cloudbase-Init", + ), + os.path.join( + target, + "Program Files", + "Cloudbase Solutions", + "Cloudbase-Init", + ), + os.path.join( + target, + "Program Files (x86)", + "Cloudbase Solutions", + "Cloudbase-Init", + ), + ] + for i in dirs: + if os.path.isdir(i): + return i + raise ValueError("Failed to find cloudbase-init install destination") + +def curthooks(): + state = util.load_command_environment() + config = load_command_config({}, state) + target = state['target'] + cloudbaseinit = get_cloudbaseinit_dir(target) + + if target is None: + sys.stderr.write("Unable to find target. " + "Use --target or set TARGET_MOUNT_POINT\n") + sys.exit(2) + + context = get_oauth_data(config) + local_scripts = os.path.join( + cloudbaseinit, + "LocalScripts", + ) + + networking = config.get("network") + if networking: + curtin_dir = os.path.join(target, "curtin") + networking_file = os.path.join(target, "network.json") + if os.path.isdir(curtin_dir): + networking_file = os.path.join(curtin_dir, "network.json") + with open(networking_file, "wb") as fd: + fd.write(json.dumps(networking, indent=2).encode('utf-8')) + + license_key = config.get("license_key") + if license_key and len(license_key) > 0: + try: + license_script = CHANGE_LICENSE_TPL.format({"license_key": license_key}) + os.makedirs(local_scripts) + licensekey_path = os.path.join(local_scripts, "ChangeLicenseKey.ps1") + with open(licensekey_path, "w") as script: + script.write(license_script) + except Exception as err: + sys.stderr.write("Failed to write LocalScripts: %r", err) + cloudbase_init_cfg = os.path.join( + cloudbaseinit, + "conf", + "cloudbase-init.conf") + cloudbase_init_unattended_cfg = os.path.join( + cloudbaseinit, + "conf", + "cloudbase-init-unattend.conf") + + if os.path.isfile(cloudbase_init_cfg) is False: + sys.stderr.write("Unable to find cloudbase-init.cfg.\n") + sys.exit(2) + + cloudbase_init_values = CLOUDBASE_INIT_TEMPLATE.format(**context) + + fp = open(cloudbase_init_cfg, 'a') + fp_u = open(cloudbase_init_unattended_cfg, 'a') + for i in cloudbase_init_values.splitlines(): + fp.write("%s\r\n" % i) + fp_u.write("%s\r\n" % i) + fp.close() + fp_u.close() + + +curthooks() + diff --git a/windows/drivers/README.md b/windows/drivers/README.md new file mode 100644 index 00000000..b474103d --- /dev/null +++ b/windows/drivers/README.md @@ -0,0 +1,14 @@ +#### Extra Windows drivers +Place all extra Windows drivers under infs directory. + +Please note that these drivers will be installed using +dpinst.exe which does not support scanning sub directories. + +This requires all drivers to be present directly under the +infs directory. + +These can include custom Windows drivers needed by certain +hardware components to function, either becuase the drivers +are not natively availble in Windows or would require extra +optimization. + diff --git a/windows/drivers/infs/.gitignore b/windows/drivers/infs/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/windows/drivers/infs/.gitkeep b/windows/drivers/infs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/windows/http/Autounattend.xml.uefi.template b/windows/http/Autounattend.xml.uefi.template new file mode 100644 index 00000000..60269cd9 --- /dev/null +++ b/windows/http/Autounattend.xml.uefi.template @@ -0,0 +1,161 @@ + + + + + + OnError + + + + 1 + 100 + EFI + + + 2 + 128 + MSR + + + true + 3 + Primary + + + + + + FAT32 + 1 + 1 + + + NTFS + 2 + 3 + + + + 0 + true + + + + + + + 3 + 0 + + + false + OnError + + + /IMAGE/NAME + @VERSION@ + + + + + + + true + + + + + en-US + + en-US + en-US + en-US + en-US + + + + + en-US + en-US + en-US + en-US + + + + ClearType + + + true + 3 + Work + true + + + + + + %SystemRoot%\System32\WindowsPowerShell\v1.0\powershell -NoLogo -NonInteractive -ExecutionPolicy RemoteSigned -Command A:\logon.ps1 + 1 + + + + + Passw0rd + true</PlainText> + </AdministratorPassword> + <!--<LocalAccounts> + <LocalAccount wcm:action="add"> + <Password> + <Value>Passw0rd</Value> + <PlainText>true</PlainText> + </Password> + <Description>Packer Administrator</Description> + <DisplayName>defaultuser</DisplayName> + <Group>Administrators</Group> + <Name>defaultuser</Name> + </LocalAccount> + </LocalAccounts>--> + </UserAccounts> + <AutoLogon> + <Password> + <Value>Passw0rd</Value> + <PlainText>true</PlainText> + </Password> + <Enabled>true</Enabled> + <LogonCount>50</LogonCount> + <Username>Administrator</Username> + </AutoLogon> + <ComputerName>*</ComputerName> + </component> + </settings> + <settings pass="specialize"> + <component name="Microsoft-Windows-TerminalServices-LocalSessionManager" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <fDenyTSConnections>false</fDenyTSConnections> + </component> + <component name="Microsoft-Windows-TerminalServices-RDP-WinStationExtensions" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <UserAuthentication>0</UserAuthentication> + </component> + <component name="Networking-MPSSVC-Svc" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <FirewallGroups> + <FirewallGroup wcm:action="add" wcm:keyValue="RemoteDesktop"> + <Active>true</Active> + <Profile>all</Profile> + <Group>@FirewallAPI.dll,-28752</Group> + </FirewallGroup> + </FirewallGroups> + </component> + <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="NonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <TimeZone>UTC</TimeZone> + <ComputerName>*</ComputerName> + </component> + <component name="Microsoft-Windows-SQMApi" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="NonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <CEIPEnabled>0</CEIPEnabled> + </component> + </settings> +</unattend> diff --git a/windows/http/logon.ps1 b/windows/http/logon.ps1 new file mode 100644 index 00000000..02e5a7ec --- /dev/null +++ b/windows/http/logon.ps1 @@ -0,0 +1,139 @@ +<# +# Upstream Author: +# +# Canonical Ltd. +# +# Copyright: +# +# (c) 2014-2023 Canonical Ltd. +# +# Licence: +# +# If you have an executed agreement with a Canonical group company which +# includes a licence to this software, your use of this software is governed +# by that agreement. Otherwise, the following applies: +# +# Canonical Ltd. hereby grants to you a world-wide, non-exclusive, +# non-transferable, revocable, perpetual (unless revoked) licence, to (i) use +# this software in connection with Canonical's MAAS software to install Windows +# in non-production environments and (ii) to make a reasonable number of copies +# of this software for backup and installation purposes. You may not: use, +# copy, modify, disassemble, decompile, reverse engineer, or distribute the +# software except as expressly permitted in this licence; permit access to the +# software to any third party other than those acting on your behalf; or use +# this software in connection with a production environment. +# +# CANONICAL LTD. MAKES THIS SOFTWARE AVAILABLE "AS-IS". CANONICAL LTD. MAKES +# NO REPRESENTATIONS OR WARRANTIES OF ANY KIND, WHETHER ORAL OR WRITTEN, +# WHETHER EXPRESS, IMPLIED, OR ARISING BY STATUTE, CUSTOM, COURSE OF DEALING +# OR TRADE USAGE, WITH RESPECT TO THIS SOFTWARE. CANONICAL LTD. SPECIFICALLY +# DISCLAIMS ANY AND ALL IMPLIED WARRANTIES OR CONDITIONS OF TITLE, SATISFACTORY +# QUALITY, MERCHANTABILITY, SATISFACTORINESS, FITNESS FOR A PARTICULAR PURPOSE +# AND NON-INFRINGEMENT. +# +# IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +# CANONICAL LTD. OR ANY OF ITS AFFILIATES, BE LIABLE TO YOU FOR DAMAGES, +# INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +# OUT OF THE USE OR INABILITY TO USE THIS SOFTWARE (INCLUDING BUT NOT LIMITED +# TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU +# OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +# PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGES. +#> + +param( + [Parameter()] + [switch]$RunPowershell +) + +$ErrorActionPreference = "Stop" + +try +{ + # Need to have network connection to continue, wait 30 + # seconds for the network to be active. + start-sleep -s 30 + + # Inject extra drivers if the infs directory is present on the attached iso + if (Test-Path -Path "E:\infs") + { + # To install extra drivers the Windows Driver Kit is needed for dpinst.exe. + # Sadly you cannot just download dpinst.exe. The whole driver kit must be + # installed. + # Download the WDK installer. + $Host.UI.RawUI.WindowTitle = "Downloading Windows Driver Kit..." + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest "https://download.microsoft.com/download/8/6/9/86925F0F-D57A-4BA4-8278-861B6876D78E/wdk/wdksetup.exe" -Outfile "c:\wdksetup.exe" + + # Run the installer. + $Host.UI.RawUI.WindowTitle = "Installing Windows Driver Kit..." + $p = Start-Process -PassThru -Wait -FilePath "c:\wdksetup.exe" -ArgumentList "/features OptionId.WindowsDriverKitComplete /q /ceip off /norestart" + if ($p.ExitCode -ne 0) + { + throw "Installing wdksetup.exe failed." + } + + # Run dpinst.exe with the path to the drivers. + $Host.UI.RawUI.WindowTitle = "Injecting Windows drivers..." + $dpinst = "$ENV:ProgramFiles (x86)\Windows Kits\8.1\redist\DIFx\dpinst\EngMui\x64\dpinst.exe" + Start-Process -Wait -FilePath "$dpinst" -ArgumentList "/S /C /F /SA /Path E:\infs" + + # Uninstall the WDK + $Host.UI.RawUI.WindowTitle = "Uninstalling Windows Driver Kit..." + Start-Process -Wait -FilePath "c:\wdksetup.exe" -ArgumentList "/features + /q /uninstall /norestart" + + # Clean-up + Remove-Item -Path c:\wdksetup.exe + } + + $Host.UI.RawUI.WindowTitle = "Installing Cloudbase-Init..." + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest "https://cloudbase.it/downloads/CloudbaseInitSetup_Stable_x64.msi" -Outfile "c:\cloudbase.msi" + $cloudbaseInitLog = "$ENV:Temp\cloudbase_init.log" + $serialPortName = @(Get-WmiObject Win32_SerialPort)[0].DeviceId + $p = Start-Process -Wait -PassThru -FilePath msiexec -ArgumentList "/i c:\cloudbase.msi /qn /norestart /l*v $cloudbaseInitLog LOGGINGSERIALPORTNAME=$serialPortName" + if ($p.ExitCode -ne 0) + { + throw "Installing $cloudbaseInitPath failed. Log: $cloudbaseInitLog" + } + + # Install virtio drivers + $Host.UI.RawUI.WindowTitle = "Installing Virtio Drivers..." + certutil -addstore "TrustedPublisher" A:/rh.cer + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win-gt-x64.msi" -Outfile "c:\virtio.msi" + Invoke-WebRequest "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win-guest-tools.exe" -Outfile "c:\virtio.exe" + $virtioLog = "$ENV:Temp\virtio.log" + $serialPortName = @(Get-WmiObject Win32_SerialPort)[0].DeviceId + $p = Start-Process -Wait -PassThru -FilePath msiexec -ArgumentList "/a c:\virtio.msi /qn /norestart /l*v $virtioLog LOGGINGSERIALPORTNAME=$serialPortName" + $p = Start-Process -Wait -PassThru -FilePath c:\virtio.exe -Argument "/silent" + + # We're done, remove LogonScript, disable AutoLogon + Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -Name Unattend* + Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name AutoLogonCount + + $Host.UI.RawUI.WindowTitle = "Running SetSetupComplete..." + & "$ENV:ProgramFiles\Cloudbase Solutions\Cloudbase-Init\bin\SetSetupComplete.cmd" + + if ($RunPowershell) { + $Host.UI.RawUI.WindowTitle = "Paused, waiting for user to finish work in other terminal" + Write-Host "Spawning another powershell for the user to complete any work..." + Start-Process -Wait -PassThru -FilePath powershell + } + + # Clean-up + Remove-Item -Path c:\cloudbase.msi + Remove-Item -Path c:\virtio.msi + Remove-Item -Path c:\virtio.exe + + # Write success, this is used to check that this process made it this far + New-Item -Path c:\success.tch -Type file -Force + + $Host.UI.RawUI.WindowTitle = "Running Sysprep..." + $unattendedXmlPath = "$ENV:ProgramFiles\Cloudbase Solutions\Cloudbase-Init\conf\Unattend.xml" + & "$ENV:SystemRoot\System32\Sysprep\Sysprep.exe" `/generalize `/oobe `/shutdown `/unattend:"$unattendedXmlPath" +} +catch +{ + $_ | Out-File c:\error_log.txt +} diff --git a/windows/http/rh.cer b/windows/http/rh.cer new file mode 100644 index 00000000..13041635 --- /dev/null +++ b/windows/http/rh.cer @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE1jCCA76gAwIBAgIQXRDLGOs6eQCHg6t0d/nTGTANBgkqhkiG9w0BAQsFADCB +hDELMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8w +HQYDVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMTUwMwYDVQQDEyxTeW1hbnRl +YyBDbGFzcyAzIFNIQTI1NiBDb2RlIFNpZ25pbmcgQ0EgLSBHMjAeFw0xODExMjcw +MDAwMDBaFw0yMjAxMjUyMzU5NTlaMGgxCzAJBgNVBAYTAlVTMRcwFQYDVQQIDA5O +b3J0aCBDYXJvbGluYTEQMA4GA1UEBwwHUmFsZWlnaDEWMBQGA1UECgwNUmVkIEhh +dCwgSW5jLjEWMBQGA1UEAwwNUmVkIEhhdCwgSW5jLjCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAN6tLWiLXZXnYDRc6y9qeQrnN59qP5xutjQ4AHZY/m9E +aNMRzKOONgalW6YTQRrW6emIscqlweRzvDnrF4hv/u/SfIq16XLqdViL0tZjmFWY +hijbtFP1cjEZNeS47m2YnQgTpTsKmZ5A66/oiqzg8ogNbxxilUOojQ+rjzhwsvfJ +AgnaGhOMeR81ca2YsgzFX3Ywf7iy6A/CtjHIOh78wcwR0MaJW6QvOhOaClVhHGtq +8yIUA7k/3k8sCC4xIxci2UqFOXopw0EUvd/xnc5by8m7LYdDO048sOM0lASt2d4P +KniOvUkU/LpqiFSYo/6272j+KRBDYCW2IgPCK5HWlZMCAwEAAaOCAV0wggFZMAkG +A1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMCsGA1UdHwQkMCIwIKAeoByGGmh0dHA6 +Ly9yYi5zeW1jYi5jb20vcmIuY3JsMGEGA1UdIARaMFgwVgYGZ4EMAQQBMEwwIwYI +KwYBBQUHAgEWF2h0dHBzOi8vZC5zeW1jYi5jb20vY3BzMCUGCCsGAQUFBwICMBkM +F2h0dHBzOi8vZC5zeW1jYi5jb20vcnBhMBMGA1UdJQQMMAoGCCsGAQUFBwMDMFcG +CCsGAQUFBwEBBEswSTAfBggrBgEFBQcwAYYTaHR0cDovL3JiLnN5bWNkLmNvbTAm +BggrBgEFBQcwAoYaaHR0cDovL3JiLnN5bWNiLmNvbS9yYi5jcnQwHwYDVR0jBBgw +FoAU1MAGIknrOUvdk+JcobhHdglyA1gwHQYDVR0OBBYEFG9GZUQmGAU3flEwvkNB +0Dhx23xpMA0GCSqGSIb3DQEBCwUAA4IBAQBX36ARUohDOhdV52T3imb+YRVdlm4k +9eX4mtE/Z+3vTuQGeCKgRFo10w94gQrRCRCQdfeyRsJHSvYFbgdGf+NboOxX2MDQ +F9ARGw6DmIezVvNJCnngv19ULo1VrDDH9tySafmb1PFjkYwcl8a/i2MWQqM/erne +y9aHFHGiWiGfWu8GWc1fmnZdG0LjlzLWn+zvYKmRE30v/Hb8rRhXpEAUUvaB4tNo +8ahQCl00nEBsr7tNKLabf9OfxXLp3oiMRfzWLBG4TavH5gWS5MgXBiP6Wxidf93v +MkM3kaYRRj+33lHdchapyKtWzgvhHa8kjDBB5oOXYhc08zqbfMpf9vNm +-----END CERTIFICATE----- \ No newline at end of file diff --git a/windows/scripts/setup-nbd b/windows/scripts/setup-nbd new file mode 100755 index 00000000..5c653a49 --- /dev/null +++ b/windows/scripts/setup-nbd @@ -0,0 +1,60 @@ +#!/bin/bash -e +# +# setup-nbd - Bind Packer qemu output to a free /dev/nbd device. +# +# Author: Lee Trager <lee.trager@canonical.com> +# +# Copyright (C) 2020 Canonical +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +if [ $UID -ne 0 ]; then + echo "ERROR: Must be run as root!" >&2 + exit 1 +fi + +if [ ! -f output-windows_builder/packer-windows_builder ]; then + echo "ERROR: Not in the same path as template!" >&2 + exit +fi + +echo 'Loading nbd...' +shopt -s extglob +modprobe nbd +for nbd in /sys/class/block/nbd+([0-9]); do + if [ "$(cat ${nbd}/size)" -eq 0 ]; then + nbd="/dev/$(basename $nbd)" + echo "Using $nbd" + break + fi +done + +if [ -z "${nbd}" ] || ! echo $nbd | grep -q "/dev"; then + echo "ERROR: Unable to find nbd device to mount image!" >&2 + exit 1 +fi + +echo "Binding image to $nbd..." +qemu-nbd -d $nbd +if [ -n "$IMG_FMT" ]; then + qemu-nbd -c $nbd -f "$IMG_FMT" -n output-windows_builder/packer-windows_builder +else + qemu-nbd -c $nbd -n output-windows_builder/packer-windows_builder +fi +echo 'Waiting for partitions to be created...' +tries=0 +while [ ! -e "${nbd}p1" -a $tries -lt 60 ]; do + sleep 1 + tries=$((tries+1)) +done diff --git a/windows/windows.pkr.hcl b/windows/windows.pkr.hcl new file mode 100644 index 00000000..4241e060 --- /dev/null +++ b/windows/windows.pkr.hcl @@ -0,0 +1,88 @@ +packer { + required_version = ">= 1.7.0" + required_plugins { + qemu = { + version = "~> 1.0" + source = "github.com/hashicorp/qemu" + } + } +} + +variable "headless" { + type = bool + default = false + description = "Whether VNC viewer should not be launched." +} + +variable "iso_path" { + type = string + default = "" +} + +variable "ovmf_suffix" { + type = string + default = "_4M" +} + +variable "filename" { + type = string + default = "windows.dd.gz" + description = "The filename of the tarball to produce" +} + +source "qemu" "windows_builder" { + accelerator = "kvm" + boot_command = ["<enter>"] + boot_wait = "1s" + communicator = "none" + disk_interface = "ide" + disk_size = "20G" + floppy_files = ["./http/Autounattend.xml", "./http/logon.ps1", "./http/rh.cer"] + floppy_label = "flop" + format = "raw" + headless = "${var.headless}" + http_directory = "http" + iso_checksum = "none" + iso_url = "${var.iso_path}" + machine_type = "q35" + memory = "4096" + cpus = "2" + net_device = "e1000" + qemuargs = [ + ["-serial", "stdio"], + ["-drive", "if=pflash,format=raw,id=ovmf_code,readonly=on,file=/usr/share/OVMF/OVMF_CODE${var.ovmf_suffix}.fd"], + ["-drive", "if=pflash,format=raw,id=ovmf_vars,file=OVMF_VARS.fd"], + ["-drive", "file=output-windows_builder/packer-windows_builder,format=raw"], + ["-cdrom", "${var.iso_path}"], + ["-drive", "file=drivers.iso,media=cdrom,index=3"], + ["-boot", "d"] + ] + shutdown_timeout = "45m" + vnc_bind_address = "0.0.0.0" +} + +build { + sources = ["source.qemu.windows_builder"] + + post-processor "shell-local" { + inline = [ + "echo 'Syncing output-windows_builder/packer-windows_builder...'", + "sync -f output-windows_builder/packer-windows_builder", + "IMG_FMT=raw", + "source scripts/setup-nbd", + "TMP_DIR=$(mktemp -d /tmp/packer-maas-XXXX)", + "echo 'Adding curtin-hooks to image...'", + "mount -t ntfs $${nbd}p3 $TMP_DIR", + "mkdir -p $TMP_DIR/curtin", + "cp ./curtin/* $TMP_DIR/curtin/", + "sync -f $TMP_DIR/curtin", + "umount $TMP_DIR", + "qemu-nbd -d $nbd", + "rmdir $TMP_DIR" + ] + inline_shebang = "/bin/bash -e" + } + post-processor "compress" { + output = "${var.filename}" + } +}