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"
+ }
+}