diff --git a/Makefile b/Makefile
index 2e6a395c..03572d30 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,12 @@ FULL_NAME := packer-maas-$(shell git describe --dirty)
default:
$(error Change your working directory to the image name you want to build!)
+lint:
+ make -C ubuntu lint
+
+format:
+ make -C ubuntu format
+
tar:
git ls-files --recurse-submodules LICENSE vmware-esxi centos* rhel* | \
tar -cJf $(FULL_NAME).tar.xz --transform="s,^,$(FULL_NAME)/," -T -
diff --git a/README.md b/README.md
index 92e803e6..7d7f105e 100644
--- a/README.md
+++ b/README.md
@@ -20,21 +20,3 @@ If you wish to use QEMU's UI also remove "headless": true
If you keep "headless": true you can connect using VNC. Packer will output the
IP and port to connect to when run.
-
-## Makefiles
-
-Each directory contains a Makefile to help build the image with the correct
-arguments. The default make target will remove the output-qemu directory and
-previously generated image before building the new image.
-
-The path to the Packer binary can be overridden with the `PACKER` variable:
-
-```
-$ make PACKER=/home/user/go/bin/packer
-```
-
-Images which require a user specified ISO can be set with the `ISO` variable:
-
-```
-$ make ISO=/path/to/iso
-```
diff --git a/ubuntu/Makefile b/ubuntu/Makefile
index 780a05ae..03614078 100644
--- a/ubuntu/Makefile
+++ b/ubuntu/Makefile
@@ -4,6 +4,16 @@ PACKER ?= packer
all: custom-ubuntu-lvm.dd.gz
+lint:
+ packer validate -var customize_script=scripts/cloudimg/setup-boot.sh ubuntu-cloudimg.pkr.hcl
+ packer fmt -check -diff .
+
+format:
+ packer fmt .
+
+seeds-cloudimg.iso: user-data-cloudimg meta-data
+ cloud-localds $@ $^
+
seeds-lvm.iso: user-data-lvm meta-data
cloud-localds $@ $^
diff --git a/ubuntu/README.md b/ubuntu/README.md
index 00f3ec70..e800e08f 100644
--- a/ubuntu/README.md
+++ b/ubuntu/README.md
@@ -1,8 +1,8 @@
-# Ubuntu Packer Template for MAAS
+# Ubuntu Packer Templates for MAAS
## Introduction
-The Packer template in this directory creates a Ubuntu AMD64 image for use with MAAS.
+The Packer templates in this directory creates Ubuntu images for use with MAAS.
## Prerequisites (to create the image)
@@ -18,15 +18,95 @@ The Packer template in this directory creates a Ubuntu AMD64 image for use with
* [MAAS](https://maas.io) 3.0+
* [Curtin](https://launchpad.net/curtin) 21.0+
-## Customizing the Image
+## ubuntu-cloudimg.pkr.hcl
+
+This template builds a tgz image from the official Ubuntu cloud images. This
+results in an image that is very close to the ones that are on
+https://images.maas.io/.
+
+### Prerequisites
+
+Before the template can be used, `seeds-cloudimg.iso` needs to be created:
+
+```shell
+$ make seeds-cloudimg.iso
+```
+
+This combines user-data-cloudimg and meta-data into a seeds image that sets up
+the VM for image building. Most importantly it sets up ssh access for the root user. A the end of the image build process, this is change is reverted.
+
+You shouldn't modify any of those files.
+
+### Building the image
+
+The build the image you give the template a script which has all the
+customizations:
+
+```shell
+$ sudo packer -var customize_script=my-changes.sh -var ubuntu_series=jammy \
+ ubuntu-cloudimg.pkr.hcl
+```
+
+`my-changes.sh` is a script you write which customizes the image from within
+the VM. For example, you can install packages using `apt-get`, call out to
+ansible, or whatever you want.
+
+#### Accessing external files from you script
+
+If you want to put or use some files in the image, you can put those in the `http` directory.
+
+Whatever file you put there, you can access from within your script like this:
+
+```shell
+$ wget http://${PACKER_HTTP_IP}:${PACKER_HTTP_PORT}:/my-file
+```
+
+### Installing a kernel
+
+Usually, images used by MAAS don't include a kernel. When a machine is deployed
+in MAAS, the approriate kernel is chosen for that machine and installed on top
+of the chosen image.
+
+If you do want to force an image to always use a specific kernel, you can
+include it in the image.
+
+The easiest way of doing this is to use the `kernel` parameter:
+
+```shell
+$ sudo packer build -var kernel=linux-lowlatency -var customize_script=my-changes.sh \
+ ubuntu-cloudimg.pkr.hcl
+```
+
+You can also install the kernel manually in your `my-changes.sh` script, but in
+that case you also need to write the name of the kernel package to
+`/curtin/CUSTOM_KERNEL`. This is to ensure that MAAS won't install another
+kernel on deploy.
+
+
+### Building different architectures
+
+By default, images are produces for amd64. You can build for arm64 as well if
+you specify the `architecture` parameter:
+
+```shell
+$ sudo packer build -var architecture=arm64 -var customize_script=my-changes.sh \
+ ubuntu-cloudimg.pkr.hcl
+```
+
+## ubuntu-flat.json and ubuntu-lvm.json
+
+These templates use an Ubuntu server image to install the image to the VM. It
+takes longer than using a cloud image, but can be useful for certain use cases.
+
+### Customizing the Image
It is possible to customize the image either during the Ubuntu installation or afterwards, before packing the final image. The former is done by providing [autoinstall config](https://ubuntu.com/server/docs/install/autoinstall), editing the _user-data-flat_ and _user-data-lvm_ files. The latter is performed by the _install-custom-packages_ script.
-## Building the image using a proxy
+### Building the image using a proxy
The Packer template downloads the Ubuntu net installer from the Internet. To tell Packer to use a proxy set the HTTP_PROXY environment variable to your proxy server. Alternatively you may redefine iso_url to a local file, set iso_checksum_type to none to disable checksuming, and remove iso_checksum_url.
-## Building an image
+### Building an image
You can easily build the image using the Makefile:
@@ -54,18 +134,12 @@ Note: ubuntu-lvm.json and ubuntu-flat.json are configured to run Packer in headl
Installation is non-interactive. Note that the installation will attempt an SSH connection to the QEMU VM where the newly-built image is being booted. This is the final provisioning step in the process. Packer uses SSH to discover that the image has, in fact, booted, so there may be a number of failed tries -- over 3-5 minutes -- until the connection is successful. This is normal behaviour for packer.
-## Uploading images to MAAS
-LVM raw image
+### Default Username
-```shell
-$ maas admin boot-resources create \
- name='custom/ubuntu-raw' \
- title='Ubuntu Custom RAW' \
- architecture='amd64/generic' \
- filetype='ddgz' \
- content@=custom-ubuntu-lvm.dd.gz
-```
+The default username is ```ubuntu```
+
+## Uploading images to MAAS
TGZ image
@@ -78,6 +152,13 @@ $ maas admin boot-resources create \
content@=custom-ubuntu.tar.gz
```
-## Default Username
+LVM raw image
-The default username is ```ubuntu```
+```shell
+$ maas admin boot-resources create \
+ name='custom/ubuntu-raw' \
+ title='Ubuntu Custom RAW' \
+ architecture='amd64/generic' \
+ filetype='ddgz' \
+ content@=custom-ubuntu-lvm.dd.gz
+```
diff --git a/ubuntu/meta-data b/ubuntu/meta-data
index e69de29b..0551428a 100644
--- a/ubuntu/meta-data
+++ b/ubuntu/meta-data
@@ -0,0 +1,2 @@
+instance-id: iid-local01
+local-hostname: packer-ubuntu
diff --git a/ubuntu/scripts/cloudimg/cleanup.sh b/ubuntu/scripts/cloudimg/cleanup.sh
new file mode 100644
index 00000000..0d2fd31c
--- /dev/null
+++ b/ubuntu/scripts/cloudimg/cleanup.sh
@@ -0,0 +1,34 @@
+#!/bin/bash -e
+#
+# cleanup.sh - Clean up what we did to be able to build the image.
+#
+# Copyright (C) 2022 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 .
+
+
+# cloud-init put networking in place on initial boot. Let's remove that, to
+# allow MAAS to configure the networking on deploy.
+rm /etc/netplan/50-cloud-init.yaml
+
+# Everything in /run/packer_backup should be restored.
+find /run/packer_backup
+cp --preserve -r /run/packer_backup/ /
+rm -rf /run/packer_backup
+
+# We had to allow root to ssh for the image setup. Let's try to revert that.
+sed -i s/^root:[^:]*/root:*/ /etc/shadow
+rm -r /root/.ssh
+rm -r /root/.cache
+rm -r /etc/ssh/ssh_host_*
diff --git a/ubuntu/scripts/cloudimg/curtin-hooks b/ubuntu/scripts/cloudimg/curtin-hooks
new file mode 100644
index 00000000..e103bb36
--- /dev/null
+++ b/ubuntu/scripts/cloudimg/curtin-hooks
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+# curtin-hooks - Curtin installation hooks for Ubuntu
+#
+# Copyright (C) 2022 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
+
+from curtin.commands.curthooks import builtin_curthooks
+from curtin.config import load_command_config
+from curtin.util import load_command_environment
+
+
+def configure_custom_kernel(config):
+ """Amend the curtin config to explicity specify the kernel to install.
+
+ The name of the kernel to install should already have been written to the
+ CUSTOM_KERNEL file in the same directory as this file.
+ """
+ custom_kernel_path = os.path.join(
+ os.path.dirname(__file__), "CUSTOM_KERNEL")
+ with open(custom_kernel_path, "r") as custom_kernel_file:
+ custom_kernel_package = custom_kernel_file.read().strip()
+ kernel_config = config.setdefault("kernel", {})
+ kernel_config["package"] = custom_kernel_package
+ return config
+
+
+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']
+
+ config = configure_custom_kernel(config)
+ builtin_curthooks(config, target, state)
+ cleanup()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ubuntu/scripts/cloudimg/install-custom-kernel.sh b/ubuntu/scripts/cloudimg/install-custom-kernel.sh
new file mode 100755
index 00000000..d8a26888
--- /dev/null
+++ b/ubuntu/scripts/cloudimg/install-custom-kernel.sh
@@ -0,0 +1,31 @@
+#!/bin/bash -e
+#
+# install-custom-kernel.sh - Install custom kernel, if specified
+#
+# Copyright (C) 2021 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 .
+export DEBIAN_FRONTEND=noninteractive
+
+if [ -z "${CLOUDIMG_CUSTOM_KERNEL}" ]; then
+ echo "Not installing custom kernel, since none was specified."
+ exit 0
+fi
+
+echo "Installing custom kernel ${CLOUDIMG_CUSTOM_KERNEL}"
+apt-get install -y ${CLOUDIMG_CUSTOM_KERNEL}
+
+# Record the installed kernel version, so that the curtin hook knows about it.
+mkdir -p /curtin
+echo -n "${CLOUDIMG_CUSTOM_KERNEL}" > /curtin/CUSTOM_KERNEL
diff --git a/ubuntu/scripts/cloudimg/setup-boot.sh b/ubuntu/scripts/cloudimg/setup-boot.sh
new file mode 100644
index 00000000..9b26087b
--- /dev/null
+++ b/ubuntu/scripts/cloudimg/setup-boot.sh
@@ -0,0 +1,38 @@
+#!/bin/bash -e
+#
+# setup-boot.sh - Set up the image after initial boot
+#
+# Copyright (C) 2022 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 .
+
+
+# Configure apt proxy if needed.
+packer_apt_proxy_config="/etc/apt/apt.conf.d/packer-proxy.conf"
+if [ ! -z "${http_proxy}" ]; then
+ echo "Acquire::http::Proxy \"${http_proxy}\";" >> ${packer_apt_proxy_config}
+fi
+if [ ! -z "${https_proxy}" ]; then
+ echo "Acquire::https::Proxy \"${https_proxy}\";" >> ${packer_apt_proxy_config}
+fi
+
+# Reset cloud-init, so that it can run again when MAAS deploy the image.
+cloud-init clean --logs
+
+# The cloud image for qemu has a kernel already. Remove it, since the user
+# should either install a kernel in the customize script, or let MAAS install
+# the right kernel when deploying.
+apt-get remove --purge -y linux-virtual 'linux-image-*'
+apt-get autoremove --purge -yq
+apt-get clean -yq
diff --git a/ubuntu/scripts/cloudimg/setup-curtin.sh b/ubuntu/scripts/cloudimg/setup-curtin.sh
new file mode 100755
index 00000000..98b2ff16
--- /dev/null
+++ b/ubuntu/scripts/cloudimg/setup-curtin.sh
@@ -0,0 +1,31 @@
+#!/bin/bash -e
+#
+# cloud-img-setup-curtin.sh - Set up curtin curthooks, if needed.
+#
+# Copyright (C) 2022 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 .
+
+if [[ ! -f "/curtin/CUSTOM_KERNEL" ]]; then
+ echo "Skipping curtin setup, since no custom kernel is used."
+ exit 0
+fi
+
+echo "Configuring curtin to install custom kernel"
+
+mkdir -p /curtin
+
+FILENAME=curtin-hooks
+mv "/tmp/${FILENAME}" /curtin/
+chmod 750 "/curtin/${FILENAME}"
diff --git a/ubuntu/ubuntu-cloudimg.pkr.hcl b/ubuntu/ubuntu-cloudimg.pkr.hcl
new file mode 100644
index 00000000..e7247c55
--- /dev/null
+++ b/ubuntu/ubuntu-cloudimg.pkr.hcl
@@ -0,0 +1,178 @@
+variable "ubuntu_series" {
+ type = string
+ default = "focal"
+ description = "The codename of the Ubuntu series to build."
+}
+
+variable "filename" {
+ type = string
+ default = "custom-cloudimg.tar.gz"
+ description = "The filename of the tarball to produce"
+}
+
+variable "kernel" {
+ type = string
+ default = ""
+ description = "The package name of the kernel to install. May include version string, e.g linux-image-generic-hwe-22.04=5.15.0.41.43"
+}
+
+variable "customize_script" {
+ type = string
+ description = "The filename of the script that will run in the VM to customize the image."
+}
+
+variable "architecture" {
+ type = string
+ default = "amd64"
+ description = "The architecture to build the image for (amd64 or arm64)"
+}
+
+variable "headless" {
+ type = bool
+ default = true
+ description = "Whether VNC viewer should not be launched."
+}
+
+variable "http_directory" {
+ type = string
+ default = "http"
+ description = "Directory for files to be accessed over http in the VM."
+}
+
+variable "http_proxy" {
+ type = string
+ default = "${env("http_proxy")}"
+ description = "HTTP proxy to use when customizing the image inside the VM. The http_proxy enviroment is set, and apt is configured to use the http proxy"
+}
+
+variable "https_proxy" {
+ type = string
+ default = "${env("https_proxy")}"
+ description = "HTTPS proxy to use when customizing the image inside the VM. The https_proxy enviroment is set, and apt is configured to use the https proxy"
+}
+
+variable "no_proxy" {
+ type = string
+ default = "${env("no_proxy")}"
+ description = "NO_PROXY environment to use when customizing the image inside the VM."
+}
+
+variable "ssh_password" {
+ type = string
+ default = "ubuntu"
+ description = "SSH password to use to connect to the VM to customize the image. Needs to match the hashed password in user-data-cloudimg."
+}
+
+variable "ssh_username" {
+ type = string
+ default = "root"
+ description = "SSH user to use to connect to the VM to customize the image. Needs to match the user in user-data-cloudimg."
+}
+
+locals {
+ qemu_arch = {
+ "amd64" = "x86_64"
+ "arm64" = "aarch64"
+ }
+ uefi_imp = {
+ "amd64" = "OVMF"
+ "arm64" = "AAVMF"
+ }
+ qemu_machine = {
+ "amd64" = "ubuntu,accel=kvm"
+ "arm64" = "virt"
+ }
+ qemu_cpu = {
+ "amd64" = "host"
+ "arm64" = "cortex-a57"
+ }
+
+ proxy_env = [
+ "http_proxy=${var.http_proxy}",
+ "https_proxy=${var.https_proxy}",
+ "no_proxy=${var.https_proxy}",
+ ]
+}
+
+
+source "qemu" "qemu" {
+ boot_wait = "2s"
+ cpus = 2
+ disk_image = true
+ disk_size = "4G"
+ format = "qcow2"
+ headless = var.headless
+ http_directory = "${var.http_directory}"
+ iso_checksum = "file:https://cloud-images.ubuntu.com/${var.ubuntu_series}/current/SHA256SUMS"
+ iso_url = "https://cloud-images.ubuntu.com/${var.ubuntu_series}/current/${var.ubuntu_series}-server-cloudimg-${var.architecture}.img"
+ memory = 2048
+ qemu_binary = "qemu-system-${lookup(local.qemu_arch, var.architecture, "")}"
+ qemu_img_args {
+ create = ["-F", "qcow2"]
+ }
+ qemuargs = [
+ ["-machine", "${lookup(local.qemu_machine, var.architecture, "")}"],
+ ["-cpu", "${lookup(local.qemu_cpu, var.architecture, "")}"],
+ ["-device", "virtio-gpu-pci"],
+ ["-drive", "if=pflash,format=raw,id=ovmf_code,readonly=on,file=/usr/share/${lookup(local.uefi_imp, var.architecture, "")}/${lookup(local.uefi_imp, var.architecture, "")}_CODE.fd"],
+ ["-drive", "if=pflash,format=raw,id=ovmf_vars,readonly=on,file=/usr/share/${lookup(local.uefi_imp, var.architecture, "")}/${lookup(local.uefi_imp, var.architecture, "")}_VARS.fd"],
+ ["-drive", "file=output-qemu/packer-qemu,format=qcow2"],
+ ["-drive", "file=seeds-cloudimg.iso,format=raw"]
+ ]
+ shutdown_command = "sudo -S shutdown -P now"
+ ssh_handshake_attempts = 500
+ ssh_password = "${var.ssh_password}"
+ ssh_timeout = "45m"
+ ssh_username = "${var.ssh_username}"
+ ssh_wait_timeout = "45m"
+ use_backing_file = true
+}
+
+build {
+ sources = ["source.qemu.qemu"]
+
+ provisioner "shell" {
+ environment_vars = concat(local.proxy_env, ["DEBIAN_FRONTEND=noninteractive"])
+ scripts = ["${path.root}/scripts/cloudimg/setup-boot.sh"]
+ }
+
+
+ provisioner "shell" {
+ environment_vars = concat(local.proxy_env, ["DEBIAN_FRONTEND=noninteractive"])
+ expect_disconnect = true
+ scripts = ["${var.customize_script}"]
+ }
+
+ provisioner "shell" {
+ environment_vars = [
+ "CLOUDIMG_CUSTOM_KERNEL=${var.kernel}",
+ "DEBIAN_FRONTEND=noninteractive"
+ ]
+ scripts = ["${path.root}/scripts/cloudimg/install-custom-kernel.sh"]
+ }
+
+ provisioner "file" {
+ destination = "/tmp/"
+ sources = ["${path.root}/scripts/cloudimg/curtin-hooks"]
+ }
+
+ provisioner "shell" {
+ environment_vars = ["CLOUDIMG_CUSTOM_KERNEL=${var.kernel}"]
+ scripts = ["${path.root}/scripts/cloudimg/setup-curtin.sh"]
+ }
+
+ provisioner "shell" {
+ environment_vars = ["DEBIAN_FRONTEND=noninteractive"]
+ scripts = ["${path.root}/scripts/cloudimg/cleanup.sh"]
+ }
+
+ post-processor "shell-local" {
+ inline = [
+ "IMG_FMT=qcow2",
+ "source ../scripts/setup-nbd",
+ "OUTPUT=$${OUTPUT:-${var.filename}}",
+ "source ./scripts/cloudimg/tar-rootfs"
+ ]
+ inline_shebang = "/bin/bash -e"
+ }
+}
diff --git a/ubuntu/user-data-cloudimg b/ubuntu/user-data-cloudimg
new file mode 100644
index 00000000..1d02918f
--- /dev/null
+++ b/ubuntu/user-data-cloudimg
@@ -0,0 +1,20 @@
+#cloud-config
+users:
+ - name: root
+ lock_passwd: false
+ hashed_passwd: "$6$canonical.$0zWaW71A9ke9ASsaOcFTdQ2tx1gSmLxMPrsH0rF0Yb.2AEKNPV1lrF94n6YuPJmnUy2K2/JSDtxuiBDey6Lpa/"
+ ssh_redirect_user: false
+ssh_pwauth: True
+disable_root: false
+preserve_hostname: true
+runcmd:
+ - sed -i -e '/^[#]*PermitRootLogin/s/^.*$/PermitRootLogin yes/' /etc/ssh/sshd_config
+ - systemctl restart ssh
+
+bootcmd:
+ - mkdir /run/packer_backup
+ - mkdir /run/packer_backup/etc
+ - mkdir /run/packer_backup/etc/apt
+ - mkdir /run/packer_backup/etc/ssh
+ - cp --preserve /etc/apt/sources.list /run/packer_backup/etc/apt/
+ - cp --preserve /etc/ssh/sshd_config /run/packer_backup/etc/ssh/