Compare commits

...

8 Commits

Author SHA1 Message Date
benvin 5df1894803 Merge pull request 'Add inst.ks.sendmac to the iPXE boot script' (#6) from benvin/ipxe-sendmac into main
ci/woodpecker/push/pre-commit Pipeline was successful
ci/woodpecker/push/validate Pipeline was successful
Reviewed-on: #6
2026-08-08 18:16:14 +10:00
unkinben ee81dd9e51 Add inst.ks.sendmac to the iPXE boot script
ci/woodpecker/push/pre-commit Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/push/validate Pipeline was successful
ci/woodpecker/pr/validate Pipeline was successful
Cobbler's per-MAC gPXE script passed inst.ks.sendmac, making anaconda send its
interface MACs as X-RHN-Provisioning-MAC-N headers when fetching the kickstart.
The bootapi iPXE script omitted it, so it was not a full drop-in for Cobbler's
gpxe output.

- Emit inst.ks.sendmac immediately before inst.ks= in the boot iPXE template,
  matching Cobbler ordering.

Claude-Session: https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
2026-08-06 22:19:32 +10:00
benvin bbbb4a4bbc Merge pull request 'Add optiplex-3070 catalog entry' (#4) from benvin/cobbler-host-sync into main
ci/woodpecker/push/pre-commit Pipeline was successful
ci/woodpecker/push/validate Pipeline was successful
Reviewed-on: #4
2026-08-05 19:36:39 +10:00
unkinben 06488b8d18 Add optiplex-3070 catalog entry
ci/woodpecker/push/pre-commit Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/push/validate Pipeline was successful
ci/woodpecker/pr/validate Pipeline was successful
Six new prodnxsr0014-0019 Dell OptiPlex 3070 hosts were registered on the
legacy cobbler server (profile almalinux9-dell_3070) but the fleet had no
matching bootapi catalog entry, so they would fall to the generic almalinux9
autopart-on-sda path.

- Add catalog/optiplex-3070.yaml: auto-nvme, UEFI-aware, fixed ~31G LVM
- Document the entry in catalog/README.md shipped list and per-model table

Claude-Session: https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
2026-08-04 00:24:21 +10:00
benvin 9416722b9a Merge pull request 'Add image-based provisioning (liveimg)' (#3) from benvin/image-provisioning into main
ci/woodpecker/push/pre-commit Pipeline was successful
ci/woodpecker/push/validate Pipeline was successful
Reviewed-on: #3
2026-07-30 21:05:24 +10:00
unkinben 105487c090 Add image-based provisioning (liveimg) templates + catalog
ci/woodpecker/push/pre-commit Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/push/validate Pipeline was successful
ci/woodpecker/pr/validate Pipeline was successful
Adds a fast, reproducible install path that unpacks a prebuilt AlmaLinux 9 node
rootfs onto the device (Anaconda liveimg) instead of resolving packages, with
per-host networking still templated after the unpack. Reuses the OptiPlex storage
vars so image and package installs lay disks out identically. Templates-only per
review; the rootfs build lives in the bootapi-images repo.

- 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 overwrites /etc), hostname, 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 (no behaviour
  change).
- catalog/almalinux9-image.yaml (VM autopart) + optiplex-7080-image.yaml
  (auto-nvme + vg_grow), rootfs_tarball -> artifactapi rootfs-images repo.

Rootfs tarball built+published by https://git.unkin.net/unkin/bootapi-images
(v* tag). Validated with bootapi validate + shellcheck; no bootapi code change.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-30 21:00:33 +10:00
benvin 0e211893db Merge pull request 'Add per-model OptiPlex kickstart variants from live Cobbler' (#2) from benvin/optiplex-models into main
ci/woodpecker/push/pre-commit Pipeline was successful
ci/woodpecker/push/validate Pipeline was successful
Reviewed-on: #2
2026-07-29 21:36:05 +10:00
benvin 4fda9c535c Merge pull request 'Seed bootapi templates + distro catalog' (#1) from benvin/bootapi-templates-initial into main
ci/woodpecker/push/pre-commit Pipeline was successful
ci/woodpecker/push/validate Pipeline was successful
Reviewed-on: #1
2026-07-29 21:33:27 +10:00
8 changed files with 344 additions and 102 deletions
+40 -6
View File
@@ -29,11 +29,11 @@ YAML + template change here — **no bootapi code change**.
`almalinux/<ver>/BaseOS/<arch>/os/`. Generic / VM default (autopart on `sda`).
- **fedora** — artifactapi `fedora` remote, tree
`fedora/releases/<ver>/Everything/<arch>/os/`.
- **optiplex-3050 / optiplex-3060 / optiplex-7080** — AlmaLinux 9 on the Dell
OptiPlex fleet. Same install tree and `almalinux9` kickstart template, but
each passes a `storage_mode` (and, for the 7080, `vg_grow`) var so the one
template lays disks out per model. Ported from the Cobbler profiles
`almalinux9-dell_3050 / _3060 / _7080`. See "Per-model variants" below.
- **optiplex-3050 / optiplex-3060 / optiplex-3070 / optiplex-7080** — AlmaLinux 9
on the Dell OptiPlex fleet. Same install tree and `almalinux9` kickstart
template, but each passes a `storage_mode` (and, for the 7080, `vg_grow`) var
so the one template lays disks out per model. Ported from the Cobbler profiles
`almalinux9-dell_3050 / _3060 / _3070 / _7080`. See "Per-model variants" below.
## Per-model variants (Dell OptiPlex)
@@ -45,6 +45,7 @@ model-specific `storage_mode` var:
|-------|----------------|----------------|----------|--------|
| `optiplex-3050` | `fixed-nvme` | static `nvme0n1` | legacy BIOS/MBR | fixed ~31G |
| `optiplex-3060` | `auto-nvme` | `%pre` picks first NVMe <512G | UEFI-aware (EFI part if booted UEFI) | fixed ~31G |
| `optiplex-3070` | `auto-nvme` | `%pre` picks first NVMe <512G | UEFI-aware | fixed ~31G |
| `optiplex-7080` | `auto-nvme` + `vg_grow: "true"` | `%pre` picks first NVMe <512G | UEFI-aware | grows to fill disk |
The kickstart (`kickstart/almalinux9.ks.tmpl`) reads `.DistroVars.storage_mode`
@@ -54,13 +55,46 @@ autopart-on-`sda` path.
**Model selection (NetBox-driven, no code change):** the entry `name` is the
OptiPlex **device_type slug**. Set the device's `provision_template` custom
field to that slug (`optiplex-3050` / `-3060` / `-7080`) and bootapi's catalog
field to that slug (`optiplex-3050` / `-3060` / `-3070` / `-7080`) and bootapi's catalog
`Select` picks the matching entry (override precedence beats platform/family).
The `match.platforms` slug on each entry is the device_type slug too — not a
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
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 the separate
[bootapi-images](https://git.unkin.net/unkin/bootapi-images) repo (a `v*` tag
builds and uploads `almalinux9-node-<ver>.tar.zst` 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 bootapi-images release +
a one-line `rootfs_tarball` edit here.
## Adding another distro (the intended path)
Add `catalog/<name>.yaml` + `kickstart/<name>.ks.tmpl`. If the OS lives on a
+22
View File
@@ -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 and
# published by the bootapi-images repo (v* tag). Immutable, semver-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-1.0.0.tar.zst"
+23
View File
@@ -0,0 +1,23 @@
# Distro catalog entry: AlmaLinux 9 on the Dell OptiPlex 3070.
#
# Ported for the six prodnxsr0014-0019 bare-metal hosts registered on the legacy
# Cobbler server under profile almalinux9-dell_3070. Mirrors optiplex-3060: a
# %pre script auto-selects the internal NVMe (first under 512G), is UEFI-aware,
# sets bootloader --boot-drive, and uses a fixed ~31G LVM (no grow). The Cobbler
# almalinux9_dell3070 kickstart template currently fails to render on cobbler,
# so the closest working per-model profile (3060, same UEFI/auto-NVMe layout,
# fixed LVM) was used as the source of truth. Select it by setting the device's
# `provision_template` custom field to "optiplex-3070" (the device_type slug).
name: optiplex-3070
match:
platforms: [optiplex-3070]
kickstart: almalinux9
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:
mirror: "{{.ArtifactBase}}/almalinux/{{.Version}}"
storage_mode: auto-nvme
+21
View File
@@ -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-1.0.0.tar.zst"
storage_mode: auto-nvme
vg_grow: "true"
+4 -2
View File
@@ -6,12 +6,14 @@ it. Mirrors how Cobbler generated a per-MAC gPXE script carrying inst.ks=.
KernelURL/InitrdURL/RepoURL come from the selected catalog entry (artifactapi
remote); KernelArgs are the catalog's extra args. KickstartURL uses
BOOTAPI_BASE_URL (http://).
BOOTAPI_BASE_URL (http://). inst.ks.sendmac makes anaconda send its interface
MACs as X-RHN-Provisioning-MAC-N headers when fetching the kickstart (Cobbler
parity; lets /ks resolve a host by MAC as well as by name).
*/ -}}
#!ipxe
echo bootapi: provisioning {{ .FQDN }} ({{ .Platform }})
{{ if and .KernelURL .InitrdURL -}}
kernel {{ .KernelURL }} initrd=initrd.img{{ if .RepoURL }} inst.repo={{ .RepoURL }}{{ end }} inst.ks={{ .KickstartURL }} ip=dhcp{{ range .KernelArgs }} {{ . }}{{ end }}
kernel {{ .KernelURL }} initrd=initrd.img{{ if .RepoURL }} inst.repo={{ .RepoURL }}{{ end }} inst.ks.sendmac inst.ks={{ .KickstartURL }} ip=dhcp{{ range .KernelArgs }} {{ . }}{{ end }}
initrd {{ .InitrdURL }}
boot
{{- else -}}
+90
View File
@@ -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 -}}
+13 -94
View File
@@ -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.
Install source comes from the artifactapi almalinux remote (via the catalog
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 via the shared _storage.ks.tmpl partials (storage_mode /
vg_grow catalog vars) so this and image.ks.tmpl lay disks out identically.
Storage is model-aware: the catalog entry selects a `storage_mode` (and, for
grow-to-fill, `vg_grow`) so one template serves VMs and every Dell OptiPlex
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.
Data model: docs/data-model.md. `.RootPasswordHash` / `.ProvisionToken` come
from Vault/env at render time, never from NetBox.
*/ -}}
{{- $mirror := .DistroVars.mirror -}}
#version=RHEL9
@@ -50,90 +46,13 @@ network --bootproto=static --device={{ .MAC }} --ip={{ .IP }} --netmask={{ .Netm
{{- end }}
{{- end }}
# --- storage (model-aware; selected by the distro catalog's storage_mode var) ---
# storage_mode is set per-model by the catalog entry (catalog/optiplex-*.yaml):
# "" 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 }}
# --- storage (model-aware; shared with image.ks.tmpl via _storage.ks.tmpl) ---
{{ template "storage-block" . }}
# 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'
%end
{{- if eq $storage "auto-nvme" }}
# --- %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 }}
{{ template "storage-pre" . }}
# --- packages ---
%packages --ignoremissing --excludedocs
+131
View File
@@ -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