diff --git a/README.md b/README.md index f282b89d..bb06abb5 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,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..4869a1ac --- /dev/null +++ b/windows/Makefile @@ -0,0 +1,49 @@ +#!/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 + +.PHONY: all clean + +all: windows + +$(eval $(call check_packages_deps,cloud-image-utils ovmf,cloud-image-utils ovmf)) + +http/Autounattend.xml: http/Autounattend.xml.${BOOT}.template + sed s#@VERSION@#"${TYPE} ${VERSION} ${EDIT}"#g $< > $@ +ifneq ($(strip $(PKEY)),) + 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 + +.INTERMEDIATE: http/Autounattend.xml + +windows: check-deps clean http/Autounattend.xml + ${PACKER} init . && ${PACKER} build -var iso_path=${ISO} -var headless=${HEADLESS} windows.pkr.hcl + +clean: + ${RM} -rf output-* windows.dd.gz http/Autounattend.xml 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/finalize b/windows/curtin/finalize index 22d7e168..246f1b9d 100644 --- a/windows/curtin/finalize +++ b/windows/curtin/finalize @@ -1,11 +1,26 @@ #!/bin/bash -export PYTHONPATH='/curtin' - -# Ubuntu 16.04 only ships with Python 3 while previous versions only ship -# with Python 2. -if type -p python > /dev/null; then - exec python "$0.py" "$@" -else - exec python3 "$0.py" "$@" +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 index cb8ba033..a7a991f1 100644 --- a/windows/curtin/finalize.py +++ b/windows/curtin/finalize.py @@ -1,163 +1,145 @@ -#!/usr/bin/env python +# +# 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. + -from __future__ import ( - absolute_import, - print_function, - unicode_literals, - ) -from curtin import util 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_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} -""" - +""" -LICENSE_KEY_SCRIPT = """\ +CHANGE_LICENSE_TPL = """ slmgr /ipk {license_key} -Remove-Item $MyInvocation.InvocationName """ - -def load_config(path): - """Loads the curtin config.""" - with open(path, 'r') as stream: - return json.load(stream) - - -def get_maas_debconf_selections(config): - """Gets the debconf selections from the curtin config.""" - try: - return config['debconf_selections']['maas'] - except KeyError: +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", + ) -def extract_maas_parameters(config): - """Extracts the needed values from the debconf - entry.""" - params = {} - for line in config.splitlines(): - cloud, key, type, value = line.split()[:4] - if key == "cloud-init/maas-metadata-url": - params['url'] = value - elif key == "cloud-init/maas-metadata-credentials": - values = value.split("&") - for oauth in values: - key, value = oauth.split('=') - if key == 'oauth_token_key': - params['token_key'] = value - elif key == 'oauth_token_secret': - params['token_secret'] = value - elif key == 'oauth_consumer_key': - params['consumer_key'] = value - return params - - -def get_cloudbase_init_config(params): - """Returns the cloudbase-init config file.""" - config = CLOUDBASE_INIT_CONFIG.format(**params) - output = "" - for line in config.splitlines(): - output += "%s\r\n" % line - return output - - -def write_cloudbase_init(target, params): - """Writes the configuration files for cloudbase-init.""" + 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( - target, - "Program Files", - "Cloudbase Solutions", - "Cloudbase-Init", + cloudbaseinit, "conf", "cloudbase-init.conf") cloudbase_init_unattended_cfg = os.path.join( - target, - "Program Files", - "Cloudbase Solutions", - "Cloudbase-Init", + cloudbaseinit, "conf", "cloudbase-init-unattend.conf") - config = get_cloudbase_init_config(params) - with open(cloudbase_init_cfg, 'a') as stream: - stream.write(config) - with open(cloudbase_init_unattended_cfg, 'a') as stream: - stream.write(config) + 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() -def get_license_key(config): - """Return license_key from the curtin config.""" - try: - license_key = config['license_key'] - except KeyError: - return None - if license_key is None: - return None - license_key = license_key.strip() - if license_key == '': - return None - return license_key - - -def write_license_key_script(target, license_key): - local_scripts_path = os.path.join( - target, - "Program Files", - "Cloudbase Solutions", - "Cloudbase-Init", - "LocalScripts") - script_path = os.path.join(local_scripts_path, 'set_license_key.ps1') - set_key_script = LICENSE_KEY_SCRIPT.format(license_key=license_key) - with open(script_path, 'w') as stream: - for line in set_key_script.splitlines(): - stream.write("%s\r\n" % line) - - -def write_network_config(target, config): - network_config_path = os.path.join(target, 'network.json') - config_json = config.get('network', None) - if config_json is not None: - config_json = json.dumps(config_json) - with open(network_config_path, 'w') as stream: - for line in config_json.splitlines(): - stream.write("%s\r\n" % line) - - -def main(): - state = util.load_command_environment() - target = state['target'] - if target is None: - print("Target was not provided in the environment.") - sys.exit(1) - config_f = state['config'] - if config_f is None: - print("Config was not provided in the environment.") - sys.exit(1) - config = load_config(config_f) - - debconf = get_maas_debconf_selections(config) - if debconf is None: - print("Failed to get the debconf_selections.") - sys.exit(1) - - params = extract_maas_parameters(debconf) - write_cloudbase_init(target, params) - write_network_config(target, config) - - license_key = get_license_key(config) - if license_key is not None: - write_license_key_script(target, license_key) - - -if __name__ == "__main__": - main() diff --git a/windows/curtin/python_wrapper b/windows/curtin/python_wrapper deleted file mode 100644 index 22d7e168..00000000 --- a/windows/curtin/python_wrapper +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -export PYTHONPATH='/curtin' - -# Ubuntu 16.04 only ships with Python 3 while previous versions only ship -# with Python 2. -if type -p python > /dev/null; then - exec python "$0.py" "$@" -else - exec python3 "$0.py" "$@" -fi diff --git a/windows/http/Autounattend.xml b/windows/http/Autounattend.xml.uefi.template similarity index 85% rename from windows/http/Autounattend.xml rename to windows/http/Autounattend.xml.uefi.template index e992a299..60269cd9 100644 --- a/windows/http/Autounattend.xml +++ b/windows/http/Autounattend.xml.uefi.template @@ -4,7 +4,6 @@ OnError - @@ -40,7 +39,6 @@ 0 true - @@ -55,14 +53,17 @@ /IMAGE/NAME - Windows Server 2016 SERVERSTANDARD + @VERSION@ + true - @@ -76,6 +77,12 @@ + + en-US + en-US + en-US + en-US + ClearType @@ -91,19 +98,29 @@ 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> diff --git a/windows/http/cloudbase-init/cloudbase_init.zip b/windows/http/cloudbase-init/cloudbase_init.zip deleted file mode 100644 index 58623fb9..00000000 Binary files a/windows/http/cloudbase-init/cloudbase_init.zip and /dev/null differ diff --git a/windows/http/logon.ps1 b/windows/http/logon.ps1 index 0a2f22a5..89d47291 100644 --- a/windows/http/logon.ps1 +++ b/windows/http/logon.ps1 @@ -5,7 +5,7 @@ # # Copyright: # -# (c) 2014-2017 Canonical Ltd. +# (c) 2014-2023 Canonical Ltd. # # Licence: # @@ -48,7 +48,6 @@ param( $ErrorActionPreference = "Stop" - try { # Need to have network connection to continue, wait 30 @@ -61,20 +60,17 @@ try # 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..." - $webclient = New-Object System.Net.WebClient - $wdksetup = [IO.Path]::GetFullPath("$ENV:TEMP\wdksetup.exe") - $wdkurl = "http://download.microsoft.com/download/0/8/C/08C7497F-8551-4054-97DE-60C0E510D97A/wdk/wdksetup.exe" - $webclient.DownloadFile($wdkurl, $wdksetup) + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest "http://download.microsoft.com/download/0/8/C/08C7497F-8551-4054-97DE-60C0E510D97A/wdk/wdksetup.exe" -Outfile "c:\wdksetup.exe" # Run the installer. $Host.UI.RawUI.WindowTitle = "Installing Windows Driver Kit..." - $p = Start-Process -PassThru -Wait -FilePath "$wdksetup" -ArgumentList "/features OptionId.WindowsDriverKitComplete /q /ceip off /norestart" + $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 failed." + throw "Installing wdksetup.exe failed." } # Run dpinst.exe with the path to the drivers. @@ -84,66 +80,60 @@ try # Uninstall the WDK $Host.UI.RawUI.WindowTitle = "Uninstalling Windows Driver Kit..." - Start-Process -Wait -FilePath "$wdksetup" -ArgumentList "/features + /q /uninstall /norestart" + 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..." - wget "https://cloudbase.it/downloads/CloudbaseInitSetup_Stable_x64.msi" -outfile "c:\cloudbase.msi" - $cloudbaseInitPath = "c:\cloudbase.msi" + [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 $cloudbaseInitPath /qn /norestart /l*v $cloudbaseInitLog LOGGINGSERIALPORTNAME=$serialPortName" + $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" } - if (Test-Path -Path "E:\cloudbase\cloudbase_init.zip") - { - Add-Type -AssemblyName System.IO.Compression.FileSystem - New-Item -Path "$ENV:TEMP\cloudbase-init" -Type directory - [System.IO.Compression.ZipFile]::ExtractToDirectory("E:\cloudbase\cloudbase_init.zip", "$ENV:TEMP\cloudbase-init") - - Remove-Item -Recurse -Force "$ENV:ProgramFiles\Cloudbase Solutions\Cloudbase-Init\Python\Lib\site-packages\cloudbaseinit" - Copy-Item -Recurse -Path "$ENV:TEMP\cloudbase-init\cloudbaseinit" -Destination "$ENV:ProgramFiles\Cloudbase Solutions\Cloudbase-Init\Python\Lib\site-packages\" - } - - # install virtio drivers + # Install virtio drivers + $Host.UI.RawUI.WindowTitle = "Installing Virtio Drivers..." certutil -addstore "TrustedPublisher" A:/rh.cer - [Net.ServicePointManager]::SecurityProtocol = "Tls, Tls11, Tls12, Ssl3" - wget "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.185-2/virtio-win-gt-x64.msi" -outfile "c:\virtio.msi" - wget "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.185-2/virtio-win-guest-tools.exe" -outfile "c:\virtio.exe" - $virtioPath = "c:\virtio.msi" + [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 $virtioPath /qn /quiet /norestart /l*v $virtioLog LOGGINGSERIALPORTNAME=$serialPortName" - Start-Process -Wait -FilePath C:\virtio.exe -Argument "/silent" -PassThru - + $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" - # Write success, this is used to check that this process made it this far - New-Item -Path C:\success.tch -Type file -Force - 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" - stop-computer - } catch { - $_ | Out-File C:\error_log.txt + $_ | Out-File c:\error_log.txt } 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.json b/windows/windows.json deleted file mode 100644 index bc4f5f13..00000000 --- a/windows/windows.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - - "builders": [{ - "type": "qemu", - "iso_url": "iso/win2016.iso", - "iso_checksum_type": "none", - "communicator": "none", - "accelerator" : "kvm", - "floppy_files": ["./http/Autounattend.xml", - "./http/logon.ps1", - "./http/rh.cer"], - "floppy_label" : "flop", - "boot_command": [ - "<enter><wait><f6><wait><esc><wait>", - "<enter>" - ], - "format": "raw", - "disk_interface" : "ide", - - "boot_wait": "2s", - "disk_size": "15G", - "headless": false, - "memory": 30000, - "net_device": "e1000", - "machine_type" : "q35", - "vnc_bind_address" : "0.0.0.0", - "http_directory": "http", - "qemuargs": [ - ["-serial", "stdio"], - ["-bios", "/usr/share/edk2-ovmf/x64/OVMF_CODE.fd"] - ], - "shutdown_timeout": "45m" - -}], - - "post-processors": [ - { - "type": "shell-local", - "inline_shebang": "/bin/bash -e", - "inline": [ - "echo 'Syncing output-qemu/packer-qemu...'", - "sync -f output-qemu/packer-qemu", - "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/.", - "cp -r ./http/cloudbase-init $TMP_DIR/.", - "unzip $TMP_DIR/cloudbase-init/cloudbase_init.zip -d $TMP_DIR/'Program Files'/'Cloudbase Solutions'/Cloudbase-Init/", - "sync -f $TMP_DIR/curtin", - "sync -f $TMP_DIR/cloudbase-init", - "umount $TMP_DIR", - "qemu-nbd -d $nbd", - "rmdir $TMP_DIR" - ] - }, - { - "type": "compress", - "output": "windows.dd.gz" - } - ] -} - diff --git a/windows/windows.pkr.hcl b/windows/windows.pkr.hcl new file mode 100644 index 00000000..5c7acbeb --- /dev/null +++ b/windows/windows.pkr.hcl @@ -0,0 +1,71 @@ +variable "iso_path" { + type = string + default = "" +} + +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." +} + +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"], ["-bios", "/usr/share/OVMF/OVMF_CODE.fd"] + ] + 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 = "windows.dd.gz" + } +}