diff --git a/README.md b/README.md index 5c95d87..cad5965 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ Read more about how [custom images](https://maas.io/docs/how-to-customise-images | Windows 2025 | Beta | >= 3.3 | | Windows 10 | Beta | >= 3.3 | | Windows 11 | Beta | >= 3.3 | +| XenServer 8 | Beta | >= 3.3 | +| XCP-ng 8.x | Beta | >= 3.3 | ### Maturity level diff --git a/xenserver8/Makefile b/xenserver8/Makefile new file mode 100644 index 0000000..de62108 --- /dev/null +++ b/xenserver8/Makefile @@ -0,0 +1,49 @@ +#!/usr/bin/make -f + +include ../scripts/check.mk + +PACKER ?= packer +PACKER_LOG ?= 0 +ISO ?= XenServer8_2024-12-09.iso +TIMEOUT ?= 1h +ARCH ?= x86_64 +HEADLESS ?= false + +ifeq ($(wildcard /usr/share/OVMF/OVMF_CODE.fd),) + OVMF_SFX ?= _4M +else + OVMF_SFX ?= +endif + +export PACKER_LOG + +.PHONY: all clean + +all: xenserver8-lvm.dd.gz + +$(eval $(call check_packages_deps)) + +lint: + packer validate . + packer fmt -check -diff . + +format: + packer fmt . + +OVMF_VARS.fd: /usr/share/OVMF/OVMF_VARS${OVMF_SFX}.fd + cp -v $< ${ARCH}_VARS.fd + +SIZE_VARS.fd: + truncate -s 2m ${ARCH}_VARS.fd + +xenserver8-lvm.dd.gz: check-deps clean OVMF_VARS.fd SIZE_VARS.fd + ${PACKER} init xenserver8.pkr.hcl && ${PACKER} build \ + -var architecture=${ARCH} \ + -var headless=${HEADLESS} \ + -var ovmf_suffix=${OVMF_SFX} \ + -var "xenserver8_iso_path=${ISO}" \ + -var timeout=${TIMEOUT} \ + xenserver8.pkr.hcl + +clean: + ${RM} -rf *.fd output-xenserver8 xenserver8-lvm.dd.gz diff --git a/xenserver8/README.md b/xenserver8/README.md new file mode 100644 index 0000000..892b759 --- /dev/null +++ b/xenserver8/README.md @@ -0,0 +1,84 @@ +# XenServer 8 Packer Template for MAAS + +## Introduction + +The Packer template in this directory creates a XenServer 8 AMD64 image for use with MAAS. + +This template is also compatible with [XCP-ng](https://xcp-ng.org/) which is the Open Source equivalent. + +## Prerequisites (to create the image) + +* A machine running Ubuntu 22.04+ with the ability to run KVM virtual machines. +* qemu-utils, libnbd-bin, nbdkit and fuse2fs +* [Packer](https://www.packer.io/intro/getting-started/install.html), v1.11.0 or newer +* The [XenServer 8 ISO](https://www.xenserver.com/downloads) + +## Requirements (to deploy the image) + +* [MAAS](https://maas.io) 3.3+ +* [Curtin](https://launchpad.net/curtin) 22.1+ + +## Customizing the Image + +The deployment image may be customized by modifying the http/xenserver8.xml.pkrtpl.hcl answer file. +See the [XenServer Answer file reference](https://docs.xenserver.com/en-us/xenserver/8/install/advanced-install#create-an-answer-file-for-unattended-installation) for more information. + +For XCP-ng, see the [Answer file page](https://docs.xcp-ng.org/appendix/answerfile/). + +## Building an image + +You can easily build the image using the Makefile: + +```shell +make ISO=/PATH/TO/XenServer8_2024-12-09.iso +``` + +Alternatively you can manually run packer. Your current working directory must +be in packer-maas/xenserver8, where this file is located. Once in packer-maas/xenserver8 +you can generate an image with: + +```shell +packer init +PACKER_LOG=1 packer build -var 'xenserver8_iso_path=/PATH/TO/XenServer8_2024-12-09.iso'. +``` + +The installation process non-interactive. Note this image only supports UEFI boot mode. + +## Network Device Name Compatibility Note + +Both XenServer and XCP-ng ship with a custom Linux kernel 4.19 which uses the traditional +NIC naming schema. This requires commissioning and deployment using the following +kernel paramaters on target machines on MAAS: + +``` +net.ifnames=0 biosdevname=0 +``` + +For additional hardware support details, refer to the [HCL Page](https://hcl.xenserver.com/). + +### Makefile Parameters + +#### HEADLESS + +Defaults to true. Set to false in order to see the VM during the build process. + +### ISO + +The path to the installation ISO image for XenSever or XCP-ng. + +#### TIMEOUT + +The timeout to apply when building the image. The default value is set to 1h. + +## Uploading an image to MAAS + +```shell +maas $PROFILE boot-resources create \ + name='custom/xenserver8' title='XenServer 8' \ + architecture='amd64/generic' filetype='ddgz' \ + base_image='rhel/8' content@=xenserver8-lvm.dd.gz +``` + +## Default Username + +The default username is ```centos```. diff --git a/xenserver8/curtin/curtin-hooks b/xenserver8/curtin/curtin-hooks new file mode 100755 index 0000000..ac9436b --- /dev/null +++ b/xenserver8/curtin/curtin-hooks @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# curtin-hooks - Curtin installation hooks for XenServer 8 +# +# Copyright (C) 2024 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 . + + +import os +import shutil +import platform + +from curtin import distro, util +from curtin.config import load_command_config +from curtin.log import LOG +from curtin.paths import target_path +from curtin.util import load_command_environment, ChrootableTarget +from curtin.commands import curthooks + +def run_hook_in_target(target, hook): + """Look for "hook" in "target" and run in a chroot""" + target_hook = target_path(target, '/curtin/' + hook) + if os.path.isfile(target_hook): + LOG.debug("running %s" % target_hook) + with ChrootableTarget(target=target) as in_chroot: + in_chroot.subp(['/curtin/' + hook]) + return True + return False + +def curthook(cfg, target, state): + """Configure network and bootloader""" + LOG.info('Running curtin builtin curthooks') + state_etcd = os.path.split(state['fstab'])[0] + machine = platform.machine() + + distro_info = distro.get_distroinfo(target=target) + if not distro_info: + raise RuntimeError('Failed to determine target distro') + osfamily = distro_info.family + LOG.info('Configuring target system for distro: %s osfamily: %s', + distro_info.variant, osfamily) + + sources = cfg.get('sources', {}) + dd_image = len(util.get_dd_images(sources)) > 0 + + curthooks.disable_overlayroot(cfg, target) + curthooks.disable_update_initramfs(cfg, target, machine) + + curthooks.apply_networking(target, state) + curthooks.handle_pollinate_user_agent(cfg, target) + + run_hook_in_target(target, 'install-custom-packages') + + # set cloud-init maas datasource + if cfg.get('cloudconfig'): + curthooks.handle_cloudconfig( + cfg['cloudconfig'], + base_dir=target_path(target, + 'etc/cloud/cloud.cfg.d')) + + run_hook_in_target(target, 'setup-bootloader') + +def cleanup(): + """Remove curtin-hooks so its as if we were never here.""" + curtin_dir = os.path.dirname(__file__) + shutil.rmtree(curtin_dir) + + +def main(): + state = load_command_environment() + config = load_command_config(None, state) + target = state['target'] + + curthook(config, target, state) + cleanup() + + +if __name__ == "__main__": + main() diff --git a/xenserver8/curtin/install-custom-packages b/xenserver8/curtin/install-custom-packages new file mode 100755 index 0000000..b4e669d --- /dev/null +++ b/xenserver8/curtin/install-custom-packages @@ -0,0 +1,50 @@ +#!/bin/bash + +log_file="/var/log/post-install.log" + +if [ -f /etc/yum/pluginconf.d/ptoken.conf ]; then + sed -i s/1/0/g /etc/yum/pluginconf.d/ptoken.conf +fi + +mkdir -pv /etc/pki/rpm-gpg/ >> $log_file +curl -o /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 https://vault.centos.org/RPM-GPG-KEY-CentOS-7 >> $log_file + +mkdir -pv /etc/yum.repos.d/ >> $log_file + +cat </etc/yum.repos.d/CentOS-Base.repo +[base] +name=CentOS-$releasever - Base +baseurl=https://vault.centos.org/7.9.2009/os/x86_64/ +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 +enabled=1 + +[updates] +name=CentOS-$releasever - Updates +baseurl=https://vault.centos.org/7.9.2009/updates/x86_64/ +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 +enabled=1 + +[extras] +name=CentOS-$releasever - Extras +baseurl=https://vault.centos.org/7.9.2009/extras/x86_64/ +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 +enabled=1 + +[centosplus] +name=CentOS-$releasever - CentOSPlus +baseurl=https://vault.centos.org/7.9.2009/centosplus/x86_64/ +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 +enabled=0 +EOF + +yum -y install cloud-init python-oauthlib >> $log_file +yum clean all >> $log_file + +# This is no longer required +rm -v /etc/yum.repos.d/CentOS-Base.repo >> $log_file + +exit 0 diff --git a/xenserver8/curtin/setup-bootloader b/xenserver8/curtin/setup-bootloader new file mode 100755 index 0000000..4daa138 --- /dev/null +++ b/xenserver8/curtin/setup-bootloader @@ -0,0 +1,25 @@ +#!/bin/bash + +# Update initrd +#for k in $(find /boot/ -type f -name vmlinuz-* | awk -F 'vmlinuz-' '{print $2}'); do dracut --no-hostonly -f /boot/initrd-${k}.img ${k}; done + +# Generate the GRUB config file +grub-mkconfig -o /boot/grub/grub.cfg + +# Install GRUB and update the configuration +if [ -d /sys/firmware/efi/efivars/ ]; then + # Mount /boot/efi first if not mounted + if [ ! -d /boot/efi/EFI ]; then + mount -L $(blkid | grep vfat | grep -oP 'LABEL="[^"]*"' | cut -d'"' -f2) /boot/efi/ + fi + + grub_dev="/dev/$(lsblk -r | grep 'part /$' | awk '{print $1}' | sed s/[0-9]//g)" + grub_part_num="$(lsblk -r | grep 'part /boot/efi' | awk '{print $1}' | sed s/[a-z]//g)" + + mkdir -p /boot/efi/EFI/BOOT + mkdir -p /boot/efi/boot/grub + + efibootmgr --create --disk ${grub_dev} --part ${grub_part_num} --label "XenServer8" --loader /EFI/xenserver/grubx64.efi + grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=xenserver +fi +exit 0 diff --git a/xenserver8/http/xenserver8.post.sh.pkrtpl.hcl b/xenserver8/http/xenserver8.post.sh.pkrtpl.hcl new file mode 100755 index 0000000..74b972a --- /dev/null +++ b/xenserver8/http/xenserver8.post.sh.pkrtpl.hcl @@ -0,0 +1,3 @@ +#!/bin/bash + +shutdown 0 diff --git a/xenserver8/http/xenserver8.xml.pkrtpl.hcl b/xenserver8/http/xenserver8.xml.pkrtpl.hcl new file mode 100644 index 0000000..2f28728 --- /dev/null +++ b/xenserver8/http/xenserver8.xml.pkrtpl.hcl @@ -0,0 +1,14 @@ + + + vda + vda + us + mypassword + + + http://10.0.2.2:8100/xenserver8.post.sh + + + America/LosAngeles + + diff --git a/xenserver8/post.sh b/xenserver8/post.sh new file mode 100755 index 0000000..c7ae9d6 --- /dev/null +++ b/xenserver8/post.sh @@ -0,0 +1,45 @@ +#!/bin/sh -x +PACKER_OUTPUT=output-${SOURCE:-qemu}/packer-${SOURCE:-qemu} + +TMP_DIR=$(mktemp -d /tmp/packer-maas-XXXX) + +ROOT_DIR="${TMP_DIR}/root" + +qemu-nbd --socket="${TMP_DIR}"/qemu-img.sock \ + --format="${IMG_FMT}" \ + --shared=10 \ + "${PACKER_OUTPUT}" & +sleep 5 + +mkdir -pv "${ROOT_DIR}" + +DEV=${TMP_DIR}/p1 + +mkdir -pv "${DEV}" + +nbdfuse "${DEV}" \ + --command nbdkit -s nbd \ + socket="${TMP_DIR}"/qemu-img.sock \ + --filter=partition partition="1" & + +retries=0 +until [ -f "${DEV}/nbd" ]; do + sleep 1 + if ((++retries > 10)); then + return 1 + fi +done + +echo "Mounting ${DEV}/nbd under ${ROOT_DIR}..." +fuse2fs "${DEV}"/nbd "${ROOT_DIR}" -o fakeroot + +echo 'Adding curtin-hooks to image...' +cp -rv curtin "$ROOT_DIR" +sync + +echo "Unmount and Clean-up $ROOT_DIR..." +umount $DEV/nbd +sleep 3 +umount $DEV +rm -rv $ROOT_DIR +echo 'Done' diff --git a/xenserver8/xenserver8.pkr.hcl b/xenserver8/xenserver8.pkr.hcl new file mode 100644 index 0000000..17273b6 --- /dev/null +++ b/xenserver8/xenserver8.pkr.hcl @@ -0,0 +1,123 @@ +packer { + required_version = ">= 1.11.0" + required_plugins { + qemu = { + version = "~> 1.0" + source = "github.com/hashicorp/qemu" + } + } +} + +variable "filename" { + type = string + default = "xenserver8.tar.gz" + description = "The filename of the tarball to produce" +} + +variable "headless" { + type = bool + default = true +} + +variable "xenserver8_iso_path" { + type = string + default = "${env("xenserver8_ISO_PATH")}" +} + +variable "timeout" { + type = string + default = "1h" + description = "Timeout for building the image" +} + +variable "architecture" { + type = string + default = "amd64" + description = "The architecture to build the image for (amd64 or arm64)" +} + +variable "ovmf_suffix" { + type = string + default = "" + description = "Suffix for OVMF CODE and VARS files. Newer systems such as Noble use _4M." +} + +locals { + qemu_arch = { + "x86_64" = "x86_64" + } + uefi_imp = { + "x86_64" = "OVMF" + } + uefi_sfx = { + "x86_64" = "${var.ovmf_suffix}" + } + qemu_machine = { + "x86_64" = "accel=kvm" + } + qemu_cpu = { + "x86_64" = "host" + } +} + +source "qemu" "xenserver8" { + boot_command = ["e", "", "", "", " answerfile=http://{{.HTTPIP}}:{{.HTTPPort}}/xenserver8.xml "] + boot_wait = "2s" + communicator = "none" + disk_size = "64G" + format = "raw" + headless = var.headless + iso_checksum = "none" + iso_url = var.xenserver8_iso_path + memory = 4096 + cores = 4 + qemu_binary = "qemu-system-${lookup(local.qemu_arch, var.architecture, "")}" + qemuargs = [ + ["-serial", "stdio"], + ["-boot", "strict=off"], + ["-device", "qemu-xhci"], + ["-device", "usb-kbd"], + ["-device", "virtio-net-pci,netdev=net0"], + ["-netdev", "user,id=net0"], + ["-device", "virtio-blk-pci,drive=drive0,bootindex=0"], + ["-device", "virtio-blk-pci,drive=cdrom0,bootindex=1"], + ["-machine", "${lookup(local.qemu_machine, var.architecture, "")}"], + ["-cpu", "${lookup(local.qemu_cpu, var.architecture, "")}"], + ["-device", "virtio-gpu-pci"], + ["-global", "driver=cfi.pflash01,property=secure,value=off"], + ["-drive", "if=pflash,format=raw,unit=0,id=ovmf_code,readonly=on,file=/usr/share/${lookup(local.uefi_imp, var.architecture, "")}/${lookup(local.uefi_imp, var.architecture, "")}_CODE${lookup(local.uefi_sfx, var.architecture, "")}.fd"], + ["-drive", "if=pflash,format=raw,unit=1,id=ovmf_vars,file=${var.architecture}_VARS.fd"], + ["-drive", "file=output-xenserver8/packer-xenserver8,if=none,id=drive0,cache=writeback,discard=ignore,format=raw"], + ["-drive", "file=${var.xenserver8_iso_path},if=none,id=cdrom0,media=cdrom"] + ] + shutdown_timeout = var.timeout + http_port_min = 8100 + http_port_max = 8100 + http_content = { + "/xenserver8.xml" = templatefile("${path.root}/http/xenserver8.xml.pkrtpl.hcl", + { + } + ), + "/xenserver8.post.sh" = templatefile("${path.root}/http/xenserver8.post.sh.pkrtpl.hcl", + { + } + ) + } +} + +build { + sources = ["source.qemu.xenserver8"] + + post-processor "shell-local" { + inline = [ + "SOURCE=xenserver8", + "IMG_FMT=raw", + "source ./post.sh", + ] + inline_shebang = "/bin/bash -e" + } + + post-processor "compress" { + output = "xenserver8-lvm.dd.gz" + } +}