Skip to content

Commit

Permalink
feat: add Windows template (#240)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Gabriel Ramirez <[email protected]>
Co-authored-by: Alan Baghumian <[email protected]>
  • Loading branch information
3 people authored Jun 18, 2024
1 parent 3a4660f commit 6682b3b
Show file tree
Hide file tree
Showing 17 changed files with 860 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions windows/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
http/Autounattend.xml
65 changes: 65 additions & 0 deletions windows/Makefile
Original file line number Diff line number Diff line change
@@ -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>/<ProductKey>/;s/<\/ProductKey>-->/<\/ProductKey>/' $@
endif
ifeq ($(strip $(VERSION)),10)
sed -i 's/<!--<LocalAccounts>/<LocalAccounts>/;s/<\/LocalAccounts>-->/<\/LocalAccounts>/' $@
else ifeq ($(strip $(VERSION)),11)
sed -i 's/<!--<LocalAccounts>/<LocalAccounts>/;s/<\/LocalAccounts>-->/<\/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
118 changes: 118 additions & 0 deletions windows/README.md
Original file line number Diff line number Diff line change
@@ -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=<path-to-iso> VERSION=<windows-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
```
7 changes: 7 additions & 0 deletions windows/TODO.md
Original file line number Diff line number Diff line change
@@ -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.

3 changes: 3 additions & 0 deletions windows/curtin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
This has been cloned from the following Git repository:

[Windows Curtin Hooks](https://github.com/cloudbase/windows-curtin-hooks.git)
1 change: 1 addition & 0 deletions windows/curtin/curtin-hooks
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#!/bin/bash
26 changes: 26 additions & 0 deletions windows/curtin/finalize
Original file line number Diff line number Diff line change
@@ -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

145 changes: 145 additions & 0 deletions windows/curtin/finalize.py
Original file line number Diff line number Diff line change
@@ -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()

14 changes: 14 additions & 0 deletions windows/drivers/README.md
Original file line number Diff line number Diff line change
@@ -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.

Empty file added windows/drivers/infs/.gitignore
Empty file.
Empty file added windows/drivers/infs/.gitkeep
Empty file.
Loading

0 comments on commit 6682b3b

Please sign in to comment.