Add image-based provisioning (liveimg) + shared storage partial
Adds a fast, reproducible install path that unpacks a prebuilt AlmaLinux 9 node
rootfs onto the disk instead of resolving packages, with per-host networking
still templated after the unpack. Storage reuses the OptiPlex model vars so
image and package installs lay disks out identically.
- kickstart/image.ks.tmpl: liveimg --url={{ .DistroVars.rootfs_tarball }};
%post renders per-host NetworkManager keyfiles from NetBox interface data
(the image is generic and liveimg clobbers /etc), sets hostname, the
puppet-initial PUPPETCA_URL env, and the provisioned callback.
- kickstart/_storage.ks.tmpl: shared storage-block/storage-pre partials
(storage_mode / vg_grow); almalinux9.ks.tmpl now uses them too (no behaviour
change) so both installs share one storage layout.
- catalog/almalinux9-image.yaml (generic autopart) + optiplex-7080-image.yaml
(auto-nvme + vg_grow) point liveimg at the artifactapi rootfs-images repo.
- .woodpecker/build-image.yaml: builds the rootfs via dnf --installroot,
tars almalinux9-node-<ver>.tar.zst, uploads to the rootfs-images local repo
on tag node-image-<ver>.
Validated with bootapi validate + shellcheck; no bootapi code change (DistroVars
+ existing interface data suffice).
Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
This commit is contained in:
@@ -0,0 +1,81 @@
|
|||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
ref: refs/tags/node-image-*
|
||||||
|
|
||||||
|
# Builds the AlmaLinux 9 node rootfs with dnf --installroot, tars it as
|
||||||
|
# almalinux9-node-<ver>.tar.zst and PUTs it to the artifactapi rootfs-images
|
||||||
|
# local (generic) repo, where the almalinux9-image catalog entry's liveimg
|
||||||
|
# points. Tag as node-image-<ver> (e.g. node-image-20260729); the tarball
|
||||||
|
# version is that suffix. Baked here = everything the image %post assumes is
|
||||||
|
# already installed (kernel/grub/dracut, NetworkManager, openssh, chrony,
|
||||||
|
# kexec-tools, curl, puppet-agent, ...). Per-host config stays in image.ks.tmpl.
|
||||||
|
steps:
|
||||||
|
- name: build-rootfs
|
||||||
|
image: git.unkin.net/unkin/almalinux9-base:20260606
|
||||||
|
commands:
|
||||||
|
- dnf -y install tar zstd dnf-plugins-core
|
||||||
|
- VER="${CI_COMMIT_TAG#node-image-}"
|
||||||
|
- ROOT="$${CI_WORKSPACE}/rootfs"
|
||||||
|
- mkdir -p "$$ROOT"
|
||||||
|
# Base system + boot chain (BIOS + UEFI), storage, networking, node tools.
|
||||||
|
- |
|
||||||
|
dnf -y --installroot="$$ROOT" --releasever=9 --setopt=install_weak_deps=False install \
|
||||||
|
@core kernel \
|
||||||
|
grub2-pc grub2-efi-x64 shim-x64 grub2-tools grub2-tools-efi efibootmgr \
|
||||||
|
dracut dracut-config-generic \
|
||||||
|
lvm2 xfsprogs e2fsprogs dosfstools \
|
||||||
|
NetworkManager selinux-policy-targeted policycoreutils \
|
||||||
|
openssh-server chrony kexec-tools bind-utils vim-minimal tmux git curl \
|
||||||
|
glibc-langpack-en
|
||||||
|
# Puppet agent baked in; image.ks.tmpl %post only configures it.
|
||||||
|
- dnf -y --installroot="$$ROOT" install https://yum.puppet.com/puppet8-release-el-9.noarch.rpm
|
||||||
|
- dnf -y --installroot="$$ROOT" install puppet-agent
|
||||||
|
- dnf -y --installroot="$$ROOT" clean all
|
||||||
|
- rm -rf "$$ROOT"/var/cache/dnf/* "$$ROOT"/var/log/dnf* "$$ROOT"/etc/machine-id
|
||||||
|
# Reproducible, ownership/xattr/SELinux-preserving tarball.
|
||||||
|
- tar --numeric-owner --acls --xattrs --selinux -C "$$ROOT" -caf "almalinux9-node-$${VER}.tar.zst" .
|
||||||
|
- ls -lh "almalinux9-node-$${VER}.tar.zst"
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
serviceAccountName: default
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 2Gi
|
||||||
|
cpu: 2
|
||||||
|
ephemeral-storage: 8Gi
|
||||||
|
limits:
|
||||||
|
memory: 4Gi
|
||||||
|
cpu: 4
|
||||||
|
ephemeral-storage: 16Gi
|
||||||
|
|
||||||
|
- name: upload
|
||||||
|
image: git.unkin.net/unkin/almalinux9-base:20260606
|
||||||
|
commands:
|
||||||
|
- VER="${CI_COMMIT_TAG#node-image-}"
|
||||||
|
- |
|
||||||
|
HOST="https://artifactapi.k8s.syd1.au.unkin.net"
|
||||||
|
REPO="rootfs-images"
|
||||||
|
FILE="almalinux9-node-$${VER}.tar.zst"
|
||||||
|
# Local generic repo: PUT stores the raw file; overwrites 409-reject, so
|
||||||
|
# skip if this version already exists (probe the GET path).
|
||||||
|
code=$$(curl -s -o /dev/null -w '%{http_code}' "$$HOST/api/v2/remotes/$$REPO/files/$$FILE" || true)
|
||||||
|
if [ "$$code" = "200" ]; then
|
||||||
|
echo "$$FILE already exists (HTTP $$code); skipping upload"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
curl -f -X PUT "$$HOST/api/v2/remotes/$$REPO/files/$$FILE" \
|
||||||
|
-H "Content-Type: application/zstd" \
|
||||||
|
--data-binary @"$$FILE"
|
||||||
|
depends_on: [build-rootfs]
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
serviceAccountName: default
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 512Mi
|
||||||
|
cpu: 500m
|
||||||
|
ephemeral-storage: 8Gi
|
||||||
|
limits:
|
||||||
|
memory: 1Gi
|
||||||
|
cpu: 1
|
||||||
|
ephemeral-storage: 16Gi
|
||||||
@@ -61,6 +61,37 @@ real NetBox platform slug — so these entries are *only* reachable via the
|
|||||||
override and never hijack a plain `almalinux9` host. To add another model, copy
|
override and never hijack a plain `almalinux9` host. To add another model, copy
|
||||||
one of these files, change the `name`/slug and `storage_mode`.
|
one of these files, change the `name`/slug and `storage_mode`.
|
||||||
|
|
||||||
|
## Image (liveimg) installs
|
||||||
|
|
||||||
|
`image.ks.tmpl` unpacks a prebuilt rootfs tarball with Anaconda `liveimg` instead
|
||||||
|
of resolving packages — faster and reproducible. An image catalog entry looks
|
||||||
|
like a normal one but sets `kickstart: image` and a `rootfs_tarball` var:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: almalinux9-image
|
||||||
|
match: {platforms: [almalinux9-image]}
|
||||||
|
kickstart: image
|
||||||
|
kernel_url: "{{.ArtifactBase}}/almalinux/{{.Version}}/BaseOS/{{.Arch}}/os/images/pxeboot/vmlinuz" # the Anaconda installer kernel, unchanged
|
||||||
|
initrd_url: "...initrd.img"
|
||||||
|
vars:
|
||||||
|
rootfs_tarball: "https://artifactapi.k8s.syd1.au.unkin.net/api/v2/remotes/rootfs-images/files/almalinux9-node-<ver>.tar.zst"
|
||||||
|
# storage_mode / vg_grow work exactly as for package installs (omit for VM autopart).
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Boot** is still the AlmaLinux installer kernel/initrd; `liveimg` only changes
|
||||||
|
the payload. **Storage** reuses the shared `kickstart/_storage.ks.tmpl` partials
|
||||||
|
(`storage_mode` / `vg_grow`), so image and package installs lay disks out
|
||||||
|
identically — including the OptiPlex NVMe modes (`optiplex-7080-image` shows an
|
||||||
|
image + `auto-nvme` + `vg_grow` combination).
|
||||||
|
- **Networking is templated per-host in `%post`** (NetworkManager keyfiles from
|
||||||
|
the same NetBox interface data), because the generic image has no per-host
|
||||||
|
identity and the `liveimg` unpack overwrites `/etc`.
|
||||||
|
- The tarball is built by `.woodpecker/build-image.yaml` (tag `node-image-<ver>`)
|
||||||
|
and uploaded to the artifactapi `rootfs-images` local repo. Baked into the
|
||||||
|
image = everything the `%post` assumes present (kernel/grub/dracut,
|
||||||
|
NetworkManager, openssh, chrony, kexec-tools, curl, puppet-agent). Bump an
|
||||||
|
image = new tag + one-line `rootfs_tarball` edit here.
|
||||||
|
|
||||||
## Adding another distro (the intended path)
|
## Adding another distro (the intended path)
|
||||||
|
|
||||||
Add `catalog/<name>.yaml` + `kickstart/<name>.ks.tmpl`. If the OS lives on a
|
Add `catalog/<name>.yaml` + `kickstart/<name>.ks.tmpl`. If the OS lives on a
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Distro catalog entry: AlmaLinux 9, IMAGE install (liveimg).
|
||||||
|
#
|
||||||
|
# Boots the same AlmaLinux installer kernel/initrd as almalinux9, but the
|
||||||
|
# kickstart (image.ks.tmpl) uses liveimg to unpack the prebuilt node rootfs
|
||||||
|
# tarball instead of resolving packages -- much faster, reproducible. Generic
|
||||||
|
# storage (autopart on sda). Select via a device's provision_template custom
|
||||||
|
# field = "almalinux9-image".
|
||||||
|
name: almalinux9-image
|
||||||
|
match:
|
||||||
|
platforms: [almalinux9-image]
|
||||||
|
kickstart: image
|
||||||
|
version_default: "9"
|
||||||
|
kernel_url: "{{.ArtifactBase}}/almalinux/{{.Version}}/BaseOS/{{.Arch}}/os/images/pxeboot/vmlinuz"
|
||||||
|
initrd_url: "{{.ArtifactBase}}/almalinux/{{.Version}}/BaseOS/{{.Arch}}/os/images/pxeboot/initrd.img"
|
||||||
|
kernel_args:
|
||||||
|
- inst.text
|
||||||
|
- net.ifnames=0
|
||||||
|
vars:
|
||||||
|
# Prebuilt node rootfs on the artifactapi rootfs-images local repo, built by
|
||||||
|
# .woodpecker/build-image.yaml. Immutable, date-versioned; bump this one line
|
||||||
|
# to roll the fleet forward (overwrites are 409-rejected).
|
||||||
|
rootfs_tarball: "https://artifactapi.k8s.syd1.au.unkin.net/api/v2/remotes/rootfs-images/files/almalinux9-node-20260729.tar.zst"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Distro catalog entry: OptiPlex 7080, IMAGE install (liveimg).
|
||||||
|
#
|
||||||
|
# The image counterpart of optiplex-7080: same liveimg payload as
|
||||||
|
# almalinux9-image, but with the 7080's auto-NVMe / grow-to-fill storage
|
||||||
|
# (storage_mode auto-nvme + vg_grow). Proves the image kickstart reuses the
|
||||||
|
# OptiPlex model storage exactly as the package install does. Select via a
|
||||||
|
# device's provision_template custom field = "optiplex-7080-image".
|
||||||
|
name: optiplex-7080-image
|
||||||
|
match:
|
||||||
|
platforms: [optiplex-7080-image]
|
||||||
|
kickstart: image
|
||||||
|
version_default: "9"
|
||||||
|
kernel_url: "{{.ArtifactBase}}/almalinux/{{.Version}}/BaseOS/{{.Arch}}/os/images/pxeboot/vmlinuz"
|
||||||
|
initrd_url: "{{.ArtifactBase}}/almalinux/{{.Version}}/BaseOS/{{.Arch}}/os/images/pxeboot/initrd.img"
|
||||||
|
kernel_args:
|
||||||
|
- inst.text
|
||||||
|
- net.ifnames=0
|
||||||
|
vars:
|
||||||
|
rootfs_tarball: "https://artifactapi.k8s.syd1.au.unkin.net/api/v2/remotes/rootfs-images/files/almalinux9-node-20260729.tar.zst"
|
||||||
|
storage_mode: auto-nvme
|
||||||
|
vg_grow: "true"
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{{- /*
|
||||||
|
Shared model-aware storage partials, used by both almalinux9.ks.tmpl (package
|
||||||
|
install) and image.ks.tmpl (liveimg). One place for the storage layout so the
|
||||||
|
classic and image installs stay identical on disk.
|
||||||
|
|
||||||
|
Driven by the distro catalog vars:
|
||||||
|
storage_mode "" generic/VM -> autopart on sda
|
||||||
|
"fixed-nvme" OptiPlex 3050 -> static nvme0n1, BIOS/MBR, fixed VG
|
||||||
|
"auto-nvme" OptiPlex 3060/7080 -> %pre picks the NVMe, EFI-aware
|
||||||
|
vg_grow "true" grow the LVM PV to fill the disk (OptiPlex 7080)
|
||||||
|
|
||||||
|
liveimg works with this unchanged: Anaconda creates these filesystems, untars the
|
||||||
|
rootfs into them, writes fstab, installs grub and regenerates the initramfs.
|
||||||
|
*/ -}}
|
||||||
|
{{- define "storage-block" -}}
|
||||||
|
{{- $storage := index .DistroVars "storage_mode" -}}
|
||||||
|
{{- $grow := eq (index .DistroVars "vg_grow") "true" -}}
|
||||||
|
{{- if eq $storage "fixed-nvme" }}
|
||||||
|
# OptiPlex 3050: single known NVMe, legacy BIOS boot (no EFI, no boot-drive).
|
||||||
|
ignoredisk --only-use=nvme0n1
|
||||||
|
clearpart --all --initlabel --drives=nvme0n1
|
||||||
|
part /boot --fstype="xfs" --ondisk=nvme0n1 --size=1024
|
||||||
|
part pv.01 --fstype="lvmpv" --ondisk=nvme0n1 --size=31743
|
||||||
|
volgroup rootvg --pesize=4096 pv.01
|
||||||
|
logvol / --fstype="xfs" --size=10240 --name=root --vgname=rootvg
|
||||||
|
logvol swap --fstype="swap" --size=2048 --name=swap --vgname=rootvg
|
||||||
|
logvol /home --fstype="xfs" --size=9207 --name=home --vgname=rootvg
|
||||||
|
logvol /var/log --fstype="xfs" --size=10240 --name=varlog --vgname=rootvg
|
||||||
|
bootloader --location=mbr
|
||||||
|
{{- else if eq $storage "auto-nvme" }}
|
||||||
|
# OptiPlex 3060 / 7080: partition table generated in %pre (see storage-pre)
|
||||||
|
# into /tmp/bootapi-partitions, so the OS lands on whichever internal NVMe is
|
||||||
|
# present and an EFI partition is added only when booted UEFI.
|
||||||
|
%include /tmp/bootapi-partitions
|
||||||
|
{{- else }}
|
||||||
|
# Generic / VM default: single virtual disk, autopart.
|
||||||
|
ignoredisk --only-use=sda
|
||||||
|
clearpart --all --initlabel --drives=sda
|
||||||
|
bootloader --location=mbr --boot-drive=sda --append="crashkernel=auto"
|
||||||
|
autopart --type=lvm --nohome
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "storage-pre" -}}
|
||||||
|
{{- $storage := index .DistroVars "storage_mode" -}}
|
||||||
|
{{- $grow := eq (index .DistroVars "vg_grow") "true" -}}
|
||||||
|
{{- if eq $storage "auto-nvme" }}
|
||||||
|
# --- %pre: pick the internal NVMe and emit the partition table -----------------
|
||||||
|
# Ported from Cobbler almalinux9_dell3060 / almalinux9_dell7080. Anaconda reads
|
||||||
|
# every section before executing, so /tmp/bootapi-partitions (pulled in by the
|
||||||
|
# %include above) is populated in time. Picks the first NVMe under 512GB, wipes
|
||||||
|
# any prior rootvg so re-provisioning is idempotent, and adds an EFI System
|
||||||
|
# Partition only when the host actually booted UEFI.
|
||||||
|
%pre --interpreter=/usr/bin/bash --log=/root/bootapi-pre.log
|
||||||
|
set -x
|
||||||
|
lsblk -d -b -n -o NAME,SIZE
|
||||||
|
|
||||||
|
# First internal NVMe under 512GB (skips large data disks and USB installers).
|
||||||
|
OSDISK=$(lsblk -d -b -n -o NAME,SIZE | awk '$1 ~ /^nvme/ && $2 < 549755813888 { print $1; exit }')
|
||||||
|
|
||||||
|
# Tear down a pre-existing rootvg so a re-install starts from a clean disk.
|
||||||
|
if vgs rootvg >/dev/null 2>&1; then
|
||||||
|
lvchange -an rootvg || true
|
||||||
|
vgchange -an rootvg || true
|
||||||
|
vgremove -y rootvg || true
|
||||||
|
for pv in $(pvs --noheadings -o pv_name,vg_name | awk '$2 == "rootvg" { print $1 }'); do
|
||||||
|
pvremove "$pv" --force --force -y || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
wipefs -a "/dev/${OSDISK}"
|
||||||
|
dd if=/dev/zero of="/dev/${OSDISK}" bs=512 count=100
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "ignoredisk --only-use=${OSDISK}"
|
||||||
|
echo "clearpart --all --initlabel --drives=${OSDISK}"
|
||||||
|
if [ -d /sys/firmware/efi ]; then
|
||||||
|
echo "part /boot/efi --fstype=vfat --ondisk=${OSDISK} --size=200"
|
||||||
|
fi
|
||||||
|
echo "part /boot --fstype=xfs --ondisk=${OSDISK} --size=1024"
|
||||||
|
echo "part pv.01 --fstype=lvmpv --ondisk=${OSDISK} --size=31743{{ if $grow }} --grow{{ end }}"
|
||||||
|
echo "volgroup rootvg --pesize=4096 pv.01"
|
||||||
|
echo "logvol / --fstype=xfs --size=10240 --name=root --vgname=rootvg"
|
||||||
|
echo "logvol swap --fstype=swap --size=2048 --name=swap --vgname=rootvg"
|
||||||
|
echo "logvol /home --fstype=xfs --size=9207 --name=home --vgname=rootvg"
|
||||||
|
echo "logvol /var/log --fstype=xfs --size=10240 --name=varlog --vgname=rootvg"
|
||||||
|
echo "bootloader --location=mbr --boot-drive=${OSDISK}"
|
||||||
|
} > /tmp/bootapi-partitions
|
||||||
|
%end
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
{{- /*
|
{{- /*
|
||||||
AlmaLinux 9 kickstart, ported from the Cobbler default.ks contract.
|
AlmaLinux 9 kickstart (package install), ported from the Cobbler default.ks
|
||||||
|
contract. Install source is the artifactapi almalinux remote (catalog mirror
|
||||||
|
var). The %post installs the Puppet agent, points it at the k8s puppetserver,
|
||||||
|
writes the puppet-initial PUPPETCA_URL env file, then posts back to bootapi so
|
||||||
|
pxe_enabled flips off (Cobbler's netboot_enabled flow).
|
||||||
|
|
||||||
Rendered by bootapi from NetBox data + render-time secrets + the distro catalog.
|
Storage is model-aware via the shared _storage.ks.tmpl partials (storage_mode /
|
||||||
Install source comes from the artifactapi almalinux remote (via the catalog
|
vg_grow catalog vars) so this and image.ks.tmpl lay disks out identically.
|
||||||
mirror var). The %post installs the Puppet agent and points it at the k8s
|
|
||||||
puppetserver (puppet.k8s.syd1.au.unkin.net / puppetca.k8s...), writes the
|
|
||||||
puppet-initial PUPPETCA_URL env file, then posts back to bootapi so pxe_enabled
|
|
||||||
flips off (Cobbler's netboot_enabled flow).
|
|
||||||
|
|
||||||
Storage is model-aware: the catalog entry selects a `storage_mode` (and, for
|
Data model: docs/data-model.md. `.RootPasswordHash` / `.ProvisionToken` come
|
||||||
grow-to-fill, `vg_grow`) so one template serves VMs and every Dell OptiPlex
|
from Vault/env at render time, never from NetBox.
|
||||||
model. See the "storage" section and catalog/optiplex-*.yaml.
|
|
||||||
|
|
||||||
Data model: see docs/data-model.md. `.RootPasswordHash` and `.ProvisionToken`
|
|
||||||
come from Vault/env at render time, never from NetBox.
|
|
||||||
*/ -}}
|
*/ -}}
|
||||||
{{- $mirror := .DistroVars.mirror -}}
|
{{- $mirror := .DistroVars.mirror -}}
|
||||||
#version=RHEL9
|
#version=RHEL9
|
||||||
@@ -50,90 +46,13 @@ network --bootproto=static --device={{ .MAC }} --ip={{ .IP }} --netmask={{ .Netm
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
# --- storage (model-aware; selected by the distro catalog's storage_mode var) ---
|
# --- storage (model-aware; shared with image.ks.tmpl via _storage.ks.tmpl) ---
|
||||||
# storage_mode is set per-model by the catalog entry (catalog/optiplex-*.yaml):
|
{{ template "storage-block" . }}
|
||||||
# "" generic/VM default -> autopart on sda (unchanged legacy path)
|
|
||||||
# "fixed-nvme" OptiPlex 3050 -> static nvme0n1, BIOS/MBR, fixed 31G VG
|
|
||||||
# "auto-nvme" OptiPlex 3060 / 7080 -> %pre picks the internal NVMe, EFI-aware
|
|
||||||
# vg_grow ("true") grows the LVM PV to fill the disk (OptiPlex 7080 only).
|
|
||||||
# Layout (explicit LVM: /boot, root, swap, /home, /var/log) mirrors the Cobbler
|
|
||||||
# profiles almalinux9-dell_3050 / _3060 / _7080. Ported from the real templates
|
|
||||||
# almalinux9_dell3050 / almalinux9_dell3060 / almalinux9_dell7080.
|
|
||||||
{{- $storage := index .DistroVars "storage_mode" }}
|
|
||||||
{{- $grow := eq (index .DistroVars "vg_grow") "true" }}
|
|
||||||
{{- if eq $storage "fixed-nvme" }}
|
|
||||||
# OptiPlex 3050: single known NVMe, legacy BIOS boot (no EFI, no boot-drive).
|
|
||||||
ignoredisk --only-use=nvme0n1
|
|
||||||
clearpart --all --initlabel --drives=nvme0n1
|
|
||||||
part /boot --fstype="xfs" --ondisk=nvme0n1 --size=1024
|
|
||||||
part pv.01 --fstype="lvmpv" --ondisk=nvme0n1 --size=31743
|
|
||||||
volgroup rootvg --pesize=4096 pv.01
|
|
||||||
logvol / --fstype="xfs" --size=10240 --name=root --vgname=rootvg
|
|
||||||
logvol swap --fstype="swap" --size=2048 --name=swap --vgname=rootvg
|
|
||||||
logvol /home --fstype="xfs" --size=9207 --name=home --vgname=rootvg
|
|
||||||
logvol /var/log --fstype="xfs" --size=10240 --name=varlog --vgname=rootvg
|
|
||||||
bootloader --location=mbr
|
|
||||||
{{- else if eq $storage "auto-nvme" }}
|
|
||||||
# OptiPlex 3060 / 7080: partition table is generated in %pre (below) into
|
|
||||||
# /tmp/bootapi-partitions and pulled in here, so the OS lands on whichever
|
|
||||||
# internal NVMe is present and the EFI partition is added only when booted UEFI.
|
|
||||||
%include /tmp/bootapi-partitions
|
|
||||||
{{- else }}
|
|
||||||
# Generic / VM default: single virtual disk, autopart.
|
|
||||||
ignoredisk --only-use=sda
|
|
||||||
clearpart --all --initlabel --drives=sda
|
|
||||||
bootloader --location=mbr --boot-drive=sda --append="crashkernel=auto"
|
|
||||||
autopart --type=lvm --nohome
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
# kdump: reserve crash memory + install kexec-tools (Cobbler com_redhat_kdump).
|
# kdump: reserve crash memory (Cobbler com_redhat_kdump; kexec-tools baked below).
|
||||||
%addon com_redhat_kdump --enable --reserve-mb='auto'
|
%addon com_redhat_kdump --enable --reserve-mb='auto'
|
||||||
%end
|
%end
|
||||||
{{- if eq $storage "auto-nvme" }}
|
{{ template "storage-pre" . }}
|
||||||
|
|
||||||
# --- %pre: pick the internal NVMe and emit the partition table -----------------
|
|
||||||
# Ported from Cobbler almalinux9_dell3060 / almalinux9_dell7080. Anaconda runs
|
|
||||||
# %pre before partitioning, so /tmp/bootapi-partitions (pulled in by the
|
|
||||||
# %include above) is populated in time. Picks the first NVMe under 512GB, wipes
|
|
||||||
# any prior rootvg so re-provisioning is idempotent, and adds an EFI System
|
|
||||||
# Partition only when the host actually booted UEFI.
|
|
||||||
%pre --interpreter=/usr/bin/bash --log=/root/bootapi-pre.log
|
|
||||||
set -x
|
|
||||||
lsblk -d -b -n -o NAME,SIZE
|
|
||||||
|
|
||||||
# First internal NVMe under 512GB (skips large data disks and USB installers).
|
|
||||||
OSDISK=$(lsblk -d -b -n -o NAME,SIZE | awk '$1 ~ /^nvme/ && $2 < 549755813888 { print $1; exit }')
|
|
||||||
|
|
||||||
# Tear down a pre-existing rootvg so a re-install starts from a clean disk.
|
|
||||||
if vgs rootvg >/dev/null 2>&1; then
|
|
||||||
lvchange -an rootvg || true
|
|
||||||
vgchange -an rootvg || true
|
|
||||||
vgremove -y rootvg || true
|
|
||||||
for pv in $(pvs --noheadings -o pv_name,vg_name | awk '$2 == "rootvg" { print $1 }'); do
|
|
||||||
pvremove "$pv" --force --force -y || true
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
wipefs -a "/dev/${OSDISK}"
|
|
||||||
dd if=/dev/zero of="/dev/${OSDISK}" bs=512 count=100
|
|
||||||
|
|
||||||
{
|
|
||||||
echo "ignoredisk --only-use=${OSDISK}"
|
|
||||||
echo "clearpart --all --initlabel --drives=${OSDISK}"
|
|
||||||
if [ -d /sys/firmware/efi ]; then
|
|
||||||
echo "part /boot/efi --fstype=vfat --ondisk=${OSDISK} --size=200"
|
|
||||||
fi
|
|
||||||
echo "part /boot --fstype=xfs --ondisk=${OSDISK} --size=1024"
|
|
||||||
echo "part pv.01 --fstype=lvmpv --ondisk=${OSDISK} --size=31743{{ if $grow }} --grow{{ end }}"
|
|
||||||
echo "volgroup rootvg --pesize=4096 pv.01"
|
|
||||||
echo "logvol / --fstype=xfs --size=10240 --name=root --vgname=rootvg"
|
|
||||||
echo "logvol swap --fstype=swap --size=2048 --name=swap --vgname=rootvg"
|
|
||||||
echo "logvol /home --fstype=xfs --size=9207 --name=home --vgname=rootvg"
|
|
||||||
echo "logvol /var/log --fstype=xfs --size=10240 --name=varlog --vgname=rootvg"
|
|
||||||
echo "bootloader --location=mbr --boot-drive=${OSDISK}"
|
|
||||||
} > /tmp/bootapi-partitions
|
|
||||||
%end
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
# --- packages ---
|
# --- packages ---
|
||||||
%packages --ignoremissing --excludedocs
|
%packages --ignoremissing --excludedocs
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
{{- /*
|
||||||
|
Image-based AlmaLinux 9 kickstart. Anaconda `liveimg` unpacks a prebuilt rootfs
|
||||||
|
tarball (catalog var rootfs_tarball, on the artifactapi rootfs-images local repo)
|
||||||
|
onto the disk instead of resolving+installing packages: it formats the KS-defined
|
||||||
|
filesystems, untars the rootfs, installs grub and regenerates the initramfs. The
|
||||||
|
package set (kernel, grub2, openssh, chrony, puppet-agent, ...) is baked into the
|
||||||
|
tarball by .woodpecker/build-image.yaml.
|
||||||
|
|
||||||
|
The image is GENERIC (no per-host identity). liveimg overwrites /etc with the
|
||||||
|
tarball, so all per-host config is applied in %post AFTER the unpack: static
|
||||||
|
NetworkManager keyfiles from the SAME NetBox interface data the classic template
|
||||||
|
uses, the hostname, the puppet-initial env, and the provisioned callback.
|
||||||
|
|
||||||
|
Storage reuses the shared _storage.ks.tmpl partials (storage_mode / vg_grow), so
|
||||||
|
image and package installs land on disk identically (VM autopart, OptiPlex NVMe).
|
||||||
|
Select via a device's provision_template custom field (e.g. almalinux9-image).
|
||||||
|
*/ -}}
|
||||||
|
#version=RHEL9
|
||||||
|
# Rendered by bootapi (image install) for {{ .FQDN }} (platform {{ .Platform }})
|
||||||
|
text
|
||||||
|
firstboot --disable
|
||||||
|
reboot
|
||||||
|
|
||||||
|
# --- payload: unpack the prebuilt rootfs tarball ---
|
||||||
|
liveimg --url={{ .DistroVars.rootfs_tarball }}
|
||||||
|
|
||||||
|
# --- localization ---
|
||||||
|
keyboard --xlayouts='au'
|
||||||
|
lang en_AU.UTF-8
|
||||||
|
timezone Australia/Sydney --utc
|
||||||
|
|
||||||
|
# --- security ---
|
||||||
|
{{ if .RootPasswordHash -}}
|
||||||
|
rootpw --iscrypted {{ .RootPasswordHash }}
|
||||||
|
{{- else -}}
|
||||||
|
rootpw --lock
|
||||||
|
{{- end }}
|
||||||
|
selinux --enforcing
|
||||||
|
firewall --enabled --service=ssh
|
||||||
|
|
||||||
|
# --- install-time networking (primary NIC only, so Anaconda can fetch the
|
||||||
|
# tarball). The installed system's network is written authoritatively in %post
|
||||||
|
# below, because the liveimg unpack overwrites whatever Anaconda configures. ---
|
||||||
|
{{- $primary := .PrimaryInterface }}
|
||||||
|
{{- if and $primary $primary.IP }}
|
||||||
|
network --bootproto=static --device={{ $primary.MAC }} --ip={{ $primary.IP }} --netmask={{ $primary.Netmask }}{{ if $primary.Gateway }} --gateway={{ $primary.Gateway }}{{ end }}{{ range .Nameservers }} --nameserver={{ . }}{{ end }} --hostname={{ $.FQDN }} --activate --onboot=on --noipv6
|
||||||
|
{{- else }}
|
||||||
|
network --bootproto=dhcp --hostname={{ .FQDN }} --activate
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
# --- storage (model-aware; shared with almalinux9.ks.tmpl via _storage.ks.tmpl) ---
|
||||||
|
{{ template "storage-block" . }}
|
||||||
|
|
||||||
|
# kdump: reserve crash memory (kexec-tools is baked into the image).
|
||||||
|
%addon com_redhat_kdump --enable --reserve-mb='auto'
|
||||||
|
%end
|
||||||
|
{{ template "storage-pre" . }}
|
||||||
|
|
||||||
|
# NOTE: no %packages section - liveimg provides the package set from the tarball.
|
||||||
|
|
||||||
|
# --- %post: apply per-host config AFTER the generic image is unpacked ---
|
||||||
|
%post --log=/root/bootapi-post.log
|
||||||
|
set -x
|
||||||
|
|
||||||
|
# Hostname (the image is generic).
|
||||||
|
echo "{{ .FQDN }}" > /etc/hostname
|
||||||
|
|
||||||
|
# Static per-host networking as NetworkManager keyfiles, from NetBox interface
|
||||||
|
# data. Written here (not via `network`) because the liveimg unpack clobbers
|
||||||
|
# /etc. Match on MAC so NIC renaming can't misapply an address.
|
||||||
|
install -d -m0755 /etc/NetworkManager/system-connections
|
||||||
|
rm -f /etc/NetworkManager/system-connections/*.nmconnection
|
||||||
|
{{- range .Interfaces }}
|
||||||
|
{{- if .IP }}
|
||||||
|
cat > "/etc/NetworkManager/system-connections/{{ .Name }}.nmconnection" <<'EOF'
|
||||||
|
[connection]
|
||||||
|
id={{ .Name }}
|
||||||
|
type=ethernet
|
||||||
|
interface-name={{ .Name }}
|
||||||
|
autoconnect=true
|
||||||
|
|
||||||
|
[ethernet]
|
||||||
|
mac-address={{ .MAC }}
|
||||||
|
|
||||||
|
[ipv4]
|
||||||
|
method=manual
|
||||||
|
address1={{ .IP }}/{{ .PrefixLen }}{{ if .Gateway }},{{ .Gateway }}{{ end }}
|
||||||
|
dns={{ range $.Nameservers }}{{ . }};{{ end }}
|
||||||
|
may-fail=false
|
||||||
|
|
||||||
|
[ipv6]
|
||||||
|
method=disabled
|
||||||
|
EOF
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
chmod 600 /etc/NetworkManager/system-connections/*.nmconnection
|
||||||
|
|
||||||
|
# chrony + ssh + NetworkManager come from the image; ensure they're enabled.
|
||||||
|
systemctl enable chronyd sshd NetworkManager
|
||||||
|
|
||||||
|
{{ if .SSHAuthorizedKeys -}}
|
||||||
|
install -d -m0700 /root/.ssh
|
||||||
|
cat > /root/.ssh/authorized_keys <<'EOF'
|
||||||
|
{{ range .SSHAuthorizedKeys }}{{ . }}
|
||||||
|
{{ end }}EOF
|
||||||
|
chmod 0600 /root/.ssh/authorized_keys
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
# Puppet agent is baked into the image; just point it at the k8s server/CA.
|
||||||
|
PUPPET_BIN=/opt/puppetlabs/bin/puppet
|
||||||
|
"$PUPPET_BIN" config set --section main certname "{{ .FQDN }}"
|
||||||
|
"$PUPPET_BIN" config set --section main server "{{ .PuppetServer }}"
|
||||||
|
"$PUPPET_BIN" config set --section main ca_server "{{ .PuppetCAServer }}"
|
||||||
|
"$PUPPET_BIN" config set --section main report_server "{{ .PuppetServer }}"
|
||||||
|
"$PUPPET_BIN" config set --section main environment production
|
||||||
|
|
||||||
|
install -d -m0755 /etc/sysconfig
|
||||||
|
cat > /etc/sysconfig/puppet-initial <<'EOF'
|
||||||
|
PUPPETCA_URL={{ .PuppetCAURL }}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl enable puppet
|
||||||
|
|
||||||
|
{{ if and .ProvisionToken .CallbackURL -}}
|
||||||
|
# Clear pxe_enabled in NetBox so the next PXE boots local disk (plain HTTP; the
|
||||||
|
# token authenticates). Non-fatal on failure.
|
||||||
|
curl -fsS -m 15 -X POST \
|
||||||
|
-H "Authorization: Bearer {{ .ProvisionToken }}" \
|
||||||
|
"{{ .CallbackURL }}" || echo "bootapi: provisioned callback failed (non-fatal)"
|
||||||
|
{{- end }}
|
||||||
|
%end
|
||||||
Reference in New Issue
Block a user