From b98eae4b015fd9cc2b33ff9c06ad47cbd16e78e7 Mon Sep 17 00:00:00 2001 From: Jonathon Anderson Date: Mon, 30 Jun 2025 23:09:49 -0600 Subject: [PATCH] Refactor ignition support during dracut - Control root device using the node/profile root field - Add support for `/warewulf/wwinit.d` to run scripts during first stage - Move first-stage ignition support to `/warewulf/wwinit.d/` - Add `wwctl set --parttype` - Add overlay template functions `SystemdEscape` and `SystemdEscapePath` - Support configuring ignition with resources Signed-off-by: Jonathon Anderson --- CHANGELOG.md | 6 +- Makefile | 3 +- dracut/modules.d/90wwinit/load-wwinit.sh | 89 ++--- dracut/modules.d/90wwinit/module-setup.sh | 4 +- dracut/modules.d/90wwinit/parse-wwinit.sh | 53 +-- etc/grub/grub.cfg.ww | 20 +- etc/ipxe/default.ipxe | 2 +- go.mod | 2 +- internal/pkg/node/constructors.go | 13 + internal/pkg/node/datastructure.go | 11 +- internal/pkg/node/mergo.go | 2 +- internal/pkg/node/{ignition.go => storage.go} | 93 ++++- internal/pkg/overlay/datastructure.go | 9 +- internal/pkg/overlay/funcmap.go | 2 +- internal/pkg/overlay/overlay.go | 12 +- overlays/ignition/internal/ignition_test.go | 325 ++++++++++++++---- overlays/ignition/internal/nodes.conf | 19 - .../etc/systemd/system/ww4-disks.target.ww | 28 +- .../rootfs/etc/systemd/system/ww4-mounts.ww | 61 +++- .../ignition/rootfs/warewulf/ignition.json.ww | 2 +- .../rootfs/warewulf/wwinit.d/10-ignition.sh | 23 ++ overlays/persistent/ignition.json.ww | 1 - overlays/wwinit/rootfs/init | 82 +++-- overlays/wwinit/rootfs/warewulf/run-init | 40 +++ overlays/wwinit/rootfs/warewulf/run-wwinit.d | 18 + overlays/wwinit/rootfs/warewulf/wwprescripts | 37 +- userdocs/overlays/overlays.rst | 41 ++- userdocs/overlays/templates.rst | 33 +- userdocs/server/bootloaders.rst | 110 +++--- 29 files changed, 787 insertions(+), 354 deletions(-) rename internal/pkg/node/{ignition.go => storage.go} (61%) delete mode 100644 overlays/ignition/internal/nodes.conf create mode 100644 overlays/ignition/rootfs/warewulf/wwinit.d/10-ignition.sh delete mode 100644 overlays/persistent/ignition.json.ww create mode 100755 overlays/wwinit/rootfs/warewulf/run-init create mode 100755 overlays/wwinit/rootfs/warewulf/run-wwinit.d mode change 100755 => 120000 overlays/wwinit/rootfs/warewulf/wwprescripts diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f734c4..1d98da7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added override.conf for nm-wait-online-initrd.service with dracut. - Added userdocs for `wwctl node import` from yaml/csv. - Added uid, gid, and permissions to OverlayFile in REST API. #1925 +- (preview) Support provisioning to local storage with `wwctl set --root=/path/to/disk`. #1894 +- (preview) Support disk provisioning during dracut boot stage with Ignition. #1894 +- Support configuring Ignition with resources. #1894 +- Added `wwctl set --parttype`. #1894 ### Fixed @@ -258,8 +262,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add man pages and command reference to userdocs. #1488 - Document building images from scratch with Apptainer. #1485 - Added warewulfd:/overlay-file/{overlay}/{path...}?render={id} -- Add `wwct configure rsync` and the template `rsyncd.conf.ww` -- Make dracut aware of `root==perisitent` which will install the node image to the parition `rootfs` ### Changed diff --git a/Makefile b/Makefile index d604f955..8837ffe4 100644 --- a/Makefile +++ b/Makefile @@ -199,7 +199,8 @@ install: build docs ## Install Warewulf from source (cd overlays && find * -path '*/internal' -prune -o -type d -exec mkdir -pv $(DESTDIR)$(DATADIR)/warewulf/overlays/{} \;) (cd overlays && find * -path '*/internal' -prune -o -type l -exec cp -av {} $(DESTDIR)$(DATADIR)/warewulf/overlays/{} \;) chmod 0755 $(DESTDIR)$(DATADIR)/warewulf/overlays/wwinit/rootfs/init - chmod 0755 $(DESTDIR)$(DATADIR)/warewulf/overlays/wwinit/rootfs/$(WWCLIENTDIR)/wwprescripts + chmod 0755 $(DESTDIR)$(DATADIR)/warewulf/overlays/wwinit/rootfs/$(WWCLIENTDIR)/run-init + chmod 0755 $(DESTDIR)$(DATADIR)/warewulf/overlays/wwinit/rootfs/$(WWCLIENTDIR)/run-wwinit.d chmod 0600 $(DESTDIR)$(DATADIR)/warewulf/overlays/wwinit/rootfs/$(WWCLIENTDIR)/config.ww chmod 0600 $(DESTDIR)$(DATADIR)/warewulf/overlays/ssh.host_keys/rootfs/etc/ssh/ssh* chmod 0644 $(DESTDIR)$(DATADIR)/warewulf/overlays/ssh.host_keys/rootfs/etc/ssh/ssh*.pub.ww diff --git a/dracut/modules.d/90wwinit/load-wwinit.sh b/dracut/modules.d/90wwinit/load-wwinit.sh index 6190f608..4bee160c 100644 --- a/dracut/modules.d/90wwinit/load-wwinit.sh +++ b/dracut/modules.d/90wwinit/load-wwinit.sh @@ -1,52 +1,15 @@ #!/bin/bash -function setup_disks() { - # format and prepare the disk(s) - /usr/bin/ignition --root=/sysroot --platform=metal --stage=disks || die "warewulf: failed to partition/format disk" - # mount /dev/disk/by-partlabel/root "$NEWROOT" || die "warewulf: failed to mount new root" - /usr/bin/ignition --root=/sysroot --platform=metal --stage=mount || die "warewulf: failed to mount disk" - -} - - -info "warewulf: Running warewulf v4 dracut init ${wwinit_persistent}" -archives="image system runtime" -if [ ${wwinit_persistent} -ne 1 ] ; then - info "warewulf: Mounting tmpfs at $NEWROOT" - mount -t tmpfs -o mpol=interleave ${wwinit_tmpfs_size_option} tmpfs "$NEWROOT" -else - info "warewulf: Using persistent setup ${wwinit_ignition}/ignition.json.ww?render=${wwinit_id}" - # get the igintion config and store in /run/igintion.ign, don't use igntion as so we can - # construct the download link - curl --location --silent --get ${localport} \ - --retry 60 --retry-delay 1 \ - "${wwinit_ignition}/ignition.json.ww?render=${wwinit_id}" -o /run/ignition-rootfs.json || die "warewulf: failed to fetch ignition configuration from ${wwinit_ignition}/ignition.json.ww?render=${wwinit_id}" - jq '.storage.filesystems |= map(select(.device=="/dev/disk/by-partlabel/rootfs").path="/")' /run/ignition-rootfs.json > /run/ignition.json || die "warewulf: failed to rewrite ignition configuration" - setup_disks -# if [ -e ${NEWROOT}/warewulf/ww_${wwinit_imagename} ] ; then -# info "warewulf: found ${NEWROOT}/warewulf/ww_${wwinit_imagename} running rsync for update" -# # we need only the overlays as we update the root -# # with rsync when the same image is used -# archives="system runtime" -# rsync -aux --delete --exclude=/proc/ --exclude=/sys/ --exclude=/dev rsync://${wwinit_ip}/${wwinit_imagename} $NEWROOT || die "warewulf: failed to run rsync" -# elif [ -e ${NEWROOT}/warewulf/ww_* ] ; then -# info "warewulf: found $(ls ${NEWROOT}/warewulf/ww_*) but image is ${wwinit_imagename}, wiping $NEWROOT" -# # we didn't have the wanted image but provisioned with warewulf, wipe the rootfs -# /usr/bin/ignition --root=/sysroot --platform=metal --stage=umount || die "warewulf: failed to mount disk" -# jq '.storage.filesystems |= map(select(.device=="/dev/disk/by-partlabel/rootfs").wipeFilesystem=true)' /run/ignition.json > /run/ignition.json.mod || die "couldn't reformation ignition configuration" -# mv /run/ignition.json.mod /run/ignition.json || die "warewulf: couldn't mv ignition configuration back" -# setup_disks -# fi -fi -for stage in $archives ; do - info "warewulf: Loading ${stage}" - # Load runtime overlay from a static privledged port. - # Others use default settings. - localport="" - if [[ "${stage}" == "runtime" ]] ; then +get_stage() { + stage="${1}" + info "warewulf: loading stage: ${stage}" + # Load runtime overlay from a static privledged port. + # Others use default settings. + localport="" + if [ "${stage}" = "runtime" ]; then localport="--local-port 1-1023" - fi - ( + fi + ( curl --location --silent --get ${localport} \ --retry 60 --retry-connrefused --retry-delay 1 \ --data-urlencode "assetkey=${wwinit_assetkey}" \ @@ -55,19 +18,29 @@ for stage in $archives ; do --data-urlencode "compress=gz" \ "${wwinit_uri}" \ | gzip -d \ - | cpio -im --directory="${NEWROOT}" + | cpio -ium --directory="${NEWROOT}" ) || die "Unable to load stage: ${stage}" -done +} -if [ ${wwinit_persistent} -eq 1 ] ; then - # this avoids that ignition runs a second time - info "warewulf: removing ignition config from wwinit image" - rm -rf ${NEWROOT}/warewulf/ignition.json - echo "Container name of persistent install: ${wwinit_imagename}" > ${NEWROOT}/warewulf/ww_${wwinit_imagename} - echo "# created from ${wwinit_ignition_uri}/fstab.ww?render=${winit_id}" > ${NEWROOT}/etc/fstab - curl --location --silent --get ${localport} \ - --retry 60 --retry-delay 1 \ - "${wwinit_ignition}/fstab.ww?render=${wwinit_id}" >> ${NEWROOT}/etc/fstab || die "warewulf: failed to write correct fstab" +mkdir /tmp/wwinit +( + # fetch the system overlay into /tmp/wwinit + local NEWROOT=/tmp/wwinit + get_stage "system" +) +if [ -x /tmp/wwinit/warewulf/run-wwinit.d ]; then + PREFIX=/tmp/wwinit /tmp/wwinit/warewulf/run-wwinit.d fi -info "warewulf: Finished warewulf v4 dracut" +info "warewulf: mounting ${wwinit_root_device} at ${NEWROOT}" +( + if [ "${wwinit_root_device}" = "tmpfs" ]; then + mount -t tmpfs -o mpol=interleave ${wwinit_tmpfs_size_option} "${wwinit_root_device}" "${NEWROOT}" + else + mount "${wwinit_root_device}" "${NEWROOT}" + fi +) || die "warewulf: failed to mount ${wwinit_root_device} at ${NEWROOT}" + +for stage in "image" "system" "runtime"; do + get_stage "${stage}" +done diff --git a/dracut/modules.d/90wwinit/module-setup.sh b/dracut/modules.d/90wwinit/module-setup.sh index 9aa5dc65..aeec152c 100644 --- a/dracut/modules.d/90wwinit/module-setup.sh +++ b/dracut/modules.d/90wwinit/module-setup.sh @@ -9,12 +9,12 @@ check() { } depends() { - echo network ignition + echo network return 0 } install() { - inst_multiple cpio curl dmidecode rsync jq + inst_multiple cpio curl dmidecode inst_hook cmdline 30 "$moddir/parse-wwinit.sh" inst_hook pre-mount 30 "$moddir/load-wwinit.sh" if dracut_module_included "network-manager" && dracut_module_included "systemd" diff --git a/dracut/modules.d/90wwinit/parse-wwinit.sh b/dracut/modules.d/90wwinit/parse-wwinit.sh index d46dbd64..255508a4 100644 --- a/dracut/modules.d/90wwinit/parse-wwinit.sh +++ b/dracut/modules.d/90wwinit/parse-wwinit.sh @@ -1,29 +1,42 @@ #!/bin/sh -# root=wwinit +# root=wwinit|wwinit:* [ -z "$root" ] && root=$(getarg root=) -if [ "${root}" = "wwinit" ] || [ "${root}" = "persistent" ] ; then +case "${root}" in +wwinit|wwinit:*) info "warewulf: root=${root}" - export wwinit_uuid=$(dmidecode -s system-uuid) - export wwinit_assetkey=$(dmidecode -s chassis-asset-tag) + export wwinit_uri="$(getarg wwinit.uri)" - export wwinit_ignition="$(getarg wwinit.ignition)" - export wwinit_ip="$(getarg wwinit.ip)" - export wwinit_imagename="$(getarg wwinit.imagename)" - export wwinit_id=$(getarg wwinit.id) - wwinit_tmpfs_size=$(getarg wwinit.tmpfs.size=) - if [ -n "$wwinit_tmpfs_size" ] ; then - info "warewulf: wwinit.tmpfs.size=${wwinit_tmpfs_size}" - export wwinit_tmpfs_size_option="-o size=${wwinit_tmpfs_size}" - fi - if [ "${root}" = "persistent" ] ; then - export wwinit_persistent="1"; info "warewulf: wwinit_persistent=$wwinit_persistent" - fi - if [ -n "${wwinit_uri}" ] ; then - info "warewulf: Found root=${root} and a Warewulf server uri. Will boot from Warewulf." + if [ -n "${wwinit_uri}" ]; then + info "warewulf: Found root=${root} and wwinit.uri=${wwinit_uri}. Will boot from Warewulf." rootok=1 else - die "warewulf: Found root=${root} but no Warewulf server uri. Cannot boot from Warewulf." + die "warewulf: Found root=${root} but no wwinit.uri. Cannot boot from Warewulf." fi -fi + + export wwinit_uuid=$(dmidecode -s system-uuid) + export wwinit_assetkey=$(dmidecode -s chassis-asset-tag) + + wwinit_tmpfs_size="$(getarg wwinit.tmpfs.size)" + if [ -n "$wwinit_tmpfs_size" ]; then + export wwinit_tmpfs_size_option="-o size=${wwinit_tmpfs_size}" + fi + + case "${root}" in + wwinit) + export wwinit_root_device="tmpfs" + ;; + wwinit:*) + export wwinit_root_device="${root#wwinit:}" + ;; + esac + + case "${wwinit_root_device}" in + initramfs|rootfs) + info "warewulf: using tmpfs in stead of ${wwinit_root_device}" + export wwinit_root_device=tmpfs + ;; + esac + ;; +esac diff --git a/etc/grub/grub.cfg.ww b/etc/grub/grub.cfg.ww index 6ee3ce3e..5bdf0d71 100644 --- a/etc/grub/grub.cfg.ww +++ b/etc/grub/grub.cfg.ww @@ -31,10 +31,7 @@ smbios --type 3 --get-string 8 --set assetkey uri="(http,{{.Ipaddr}}:{{.Port}})/provision/${net_default_mac}?assetkey=${assetkey}" kernel="${uri}&stage=kernel" -{{- $defentry := "singlestage" }} -{{- if eq .Root "persistent"}}{{ $defentry = "dracut" }}{{ end }} -{{- if .Tags.GrubMenuEntry }}{{ $defentry = .Tags.GrubMenuEntry }}{{ end }} -set default={{ $defentry}} +set default={{ or .Tags.GrubMenuEntry "single-stage" }} set timeout=2 menuentry "Single-stage boot" --id single-stage { @@ -117,13 +114,15 @@ menuentry "Single-stage boot (no compression)" --id single-stage-nocompress { echo "!! Unable to load images." echo "!! Rebooting in 15s..." echo "!!" + sleep 15 + reboot fi echo "Booting..." boot } -menuentry "Two stage boot with dracut {{ if eq .Root "persistent"}}(persistent){{ end }}" --id dracut { +menuentry "Two stage boot with dracut" --id dracut { echo "Warewulf Server:" echo "* Ipaddr: {{.Ipaddr}}" echo "* Port: {{.Port}}" @@ -143,18 +142,11 @@ menuentry "Two stage boot with dracut {{ if eq .Root "persistent"}}(persistent){ wwinit_uri="http://{{.Ipaddr}}:{{.Port}}/provision/${net_default_mac}" net_args="rd.neednet=1 {{range $devname, $netdev := .NetDevs}}{{if and $netdev.Hwaddr $netdev.Device}} ifname={{$netdev.Device}}:{{$netdev.Hwaddr}} {{end}}{{end}}" - {{- if eq .Root "persistent"}} - ignition_args="wwinit.ignition=http://{{.Ipaddr}}:{{.Port}}/overlay-file/persistent wwinit.imagename={{ .ImageName }} wwinit.id={{ .Id }} wwinit.ip={{ .Ipaddr }}" - root="persistent" - echo "* Persistent install" - {{- else }} - root="wwinit" - {{- end }} - wwinit_args="root=$root wwinit.uri=${wwinit_uri} init=/init" + wwinit_args="root=wwinit:{{default "tmpfs" .Root}} wwinit.uri=${wwinit_uri} init=/warewulf/run-init" echo echo "Downloading kernel image..." - linux $kernel wwid=${net_default_mac} {{.KernelArgs}} $net_args $wwinit_args $ignition_args + linux $kernel wwid=${net_default_mac} {{.KernelArgs}} $net_args $wwinit_args if [ $? != 0 ] then echo "!!" diff --git a/etc/ipxe/default.ipxe b/etc/ipxe/default.ipxe index 92909a4d..69bf0848 100644 --- a/etc/ipxe/default.ipxe +++ b/etc/ipxe/default.ipxe @@ -88,7 +88,7 @@ echo echo Downloading dracut initramfs... initrd --name initramfs ${uri}&stage=initramfs || goto error_reboot set dracut_net rd.neednet=1 {{range $devname, $netdev := .NetDevs}}{{if and $netdev.Hwaddr $netdev.Device}} ifname={{$netdev.Device}}:{{$netdev.Hwaddr}} ip={{$netdev.Device}}:dhcp {{end}}{{end}} -set dracut_wwinit root=wwinit wwinit.uri=${baseuri} init=/init +set dracut_wwinit root=wwinit:{{default "tmpfs" .Root}} wwinit.uri=${baseuri} init=/warewulf/run-init goto boot_two_stage_dracut :boot_single_stage diff --git a/go.mod b/go.mod index 9f79d7f5..bdd4ef25 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/containers/image/v5 v5.32.2 github.com/containers/storage v1.57.1 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e + github.com/coreos/go-systemd/v22 v22.5.0 github.com/coreos/ignition/v2 v2.20.0 github.com/coreos/vcontext v0.0.0-20230201181013-d72178a18687 github.com/creasty/defaults v1.8.0 @@ -58,7 +59,6 @@ require ( github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/containers/ocicrypt v1.2.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f // indirect github.com/cyphar/filepath-securejoin v0.3.6 // indirect diff --git a/internal/pkg/node/constructors.go b/internal/pkg/node/constructors.go index 73162e91..dad38b3a 100644 --- a/internal/pkg/node/constructors.go +++ b/internal/pkg/node/constructors.go @@ -194,3 +194,16 @@ func (config *NodesYaml) FindDiscoverableNode() (Node, string, error) { return EmptyNode(), "", ErrNoUnconfigured } + +func (node *Node) SetIds(id string) { + node.id = id + for diskId, disk := range node.Disks { + disk.id = diskId + for partitionId, partition := range disk.Partitions { + partition.id = partitionId + } + } + for fsId, fs := range node.FileSystems { + fs.id = fsId + } +} diff --git a/internal/pkg/node/datastructure.go b/internal/pkg/node/datastructure.go index 5fa2c8f4..d7825e15 100644 --- a/internal/pkg/node/datastructure.go +++ b/internal/pkg/node/datastructure.go @@ -92,6 +92,7 @@ type NetDev struct { Holds the disks of a node */ type Disk struct { + id string `yaml:"-" json:"-"` WipeTable bool `yaml:"wipe_table,omitempty" json:"wipe_table,omitempty" lopt:"diskwipe" comment:"whether or not the partition tables shall be wiped"` Partitions map[string]*Partition `yaml:"partitions,omitempty" json:"partitions,omitempty"` } @@ -101,13 +102,14 @@ partition definition, the label must be uniq so its used as the key in the Partitions map */ type Partition struct { - Number string `yaml:"number,omitempty" json:"number,omitempty" lopt:"partnumber" comment:"set the partition number, if not set next free slot is used" type:"uint"` - SizeMiB string `yaml:"size_mib,omitempty" json:"size_mib,omitempty" lopt:"partsize" comment:"set the size of the partition, if not set maximal possible size is used" type:"uint"` + id string `yaml:"-" json:"-"` + Number string `yaml:"number,omitempty" json:"number,omitempty" lopt:"partnumber" comment:"Set the partition number, if not set next free slot is used" type:"uint"` + SizeMiB string `yaml:"size_mib,omitempty" json:"size_mib,omitempty" lopt:"partsize" comment:"Set the size of the partition, if not set maximal possible size is used" type:"uint"` StartMiB string `yaml:"start_mib,omitempty" json:"start_mib,omitempty" comment:"the start of the partition" type:"uint"` - TypeGuid string `yaml:"type_guid,omitempty" json:"type_guid,omitempty" comment:"Linux filesystem data will be used if empty"` + TypeGuid string `yaml:"type_guid,omitempty" json:"type_guid,omitempty" lopt:"parttype" comment:"Set the partition type GUID"` Guid string `yaml:"guid,omitempty" json:"guid,omitempty" comment:"the GPT unique partition GUID"` WipePartitionEntry bool `yaml:"wipe_partition_entry,omitempty" json:"wipe_partition_entry,omitempty" comment:"if true, Ignition will clobber an existing partition if it does not match the config"` - ShouldExist bool `yaml:"should_exist,omitempty" json:"should_exist,omitempty" lopt:"partcreate" comment:"create partition if not exist"` + ShouldExist bool `yaml:"should_exist,omitempty" json:"should_exist,omitempty" lopt:"partcreate" comment:"Create partition if it does not exist"` Resize bool `yaml:"resize,omitempty" json:"resize,omitempty" comment:"whether or not the existing partition should be resize"` } @@ -115,6 +117,7 @@ type Partition struct { Definition of a filesystem. The device is uniq so its used as key */ type FileSystem struct { + id string `yaml:"-" json:"-"` Format string `yaml:"format,omitempty" json:"format,omitempty" lopt:"fsformat" comment:"format of the file system"` Path string `yaml:"path,omitempty" json:"path,omitempty" lopt:"fspath" comment:"the mount point of the file system"` WipeFileSystem bool `yaml:"wipe_filesystem,omitempty" json:"wipe_filesystem,omitempty" lopt:"fswipe" comment:"wipe file system at boot"` diff --git a/internal/pkg/node/mergo.go b/internal/pkg/node/mergo.go index efeefc29..452bc53b 100644 --- a/internal/pkg/node/mergo.go +++ b/internal/pkg/node/mergo.go @@ -157,7 +157,7 @@ func (config *NodesYaml) MergeNode(id string) (node Node, fields fieldMap, err e delete(fields, "Profiles") } - node.id = id + node.SetIds(id) node.valid = true node.updatePrimaryNetDev() node.Profile.cleanLists() diff --git a/internal/pkg/node/ignition.go b/internal/pkg/node/storage.go similarity index 61% rename from internal/pkg/node/ignition.go rename to internal/pkg/node/storage.go index b4d84742..6e1993e0 100644 --- a/internal/pkg/node/ignition.go +++ b/internal/pkg/node/storage.go @@ -10,10 +10,97 @@ import ( "github.com/warewulf/warewulf/internal/pkg/wwlog" ) +func (node *Node) DiskList() (disks []*Disk) { + names := make([]string, 0, len(node.Disks)) + for name := range node.Disks { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + disk := node.Disks[name] + if disk != nil { + disks = append(disks, disk) + } + } + return disks +} + +func (node *Node) FileSystemList() (fs []*FileSystem) { + names := make([]string, 0, len(node.FileSystems)) + for name := range node.FileSystems { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + fsys := node.FileSystems[name] + if fsys != nil { + fs = append(fs, fsys) + } + } + return fs +} + +func (disk *Disk) Id() string { + return disk.id +} + +func (disk *Disk) PartitionList() (partitions []*Partition) { + names := make([]string, 0, len(disk.Partitions)) + for name := range disk.Partitions { + names = append(names, name) + } + sort.SliceStable(names, func(i, j int) bool { + pi := disk.Partitions[names[i]] + pj := disk.Partitions[names[j]] + + // Try to compare by Number (as int) + ni, erri := strconv.Atoi(pi.Number) + nj, errj := strconv.Atoi(pj.Number) + if erri == nil && errj == nil { + if ni != nj { + return ni < nj + } + } else if erri == nil { + return true // i has a number, j does not + } else if errj == nil { + return false // j has a number, i does not + } + + // Next, compare by id if set + if pi.id != "" && pj.id != "" { + if pi.id != pj.id { + return pi.id < pj.id + } + } else if pi.id != "" { + return true // i has id, j does not + } else if pj.id != "" { + return false // j has id, i does not + } + + // Fallback: compare by name (map key) + return names[i] < names[j] + }) + for _, name := range names { + part := disk.Partitions[name] + if part != nil { + partitions = append(partitions, part) + } + } + return partitions +} + +func (partition *Partition) Id() string { + return partition.id +} + +func (fs *FileSystem) Id() string { + return fs.id +} + /* Create a ignition struct class for ignition */ -func (node *Node) GetStorage() (stor types_3_4.Storage, rep string, err error) { +func (node *Node) GetIgnitionStorage() (stor types_3_4.Storage, rep string, err error) { var fileSystems []types_3_4.Filesystem for fsdevice, fs := range node.FileSystems { var mountOptions []types_3_4.MountOption @@ -141,8 +228,8 @@ type SimpleIgnitionConfig struct { /* Get a simple config which can be marshalled to json */ -func (node *Node) GetConfig() (conf SimpleIgnitionConfig, rep string, err error) { - conf.Storage, rep, err = node.GetStorage() +func (node *Node) GetIgnitionConfig() (conf SimpleIgnitionConfig, rep string, err error) { + conf.Storage, rep, err = node.GetIgnitionStorage() conf.Ignition.Version = "3.1.0" return } diff --git a/internal/pkg/overlay/datastructure.go b/internal/pkg/overlay/datastructure.go index 11961933..25779c25 100644 --- a/internal/pkg/overlay/datastructure.go +++ b/internal/pkg/overlay/datastructure.go @@ -8,7 +8,6 @@ import ( "time" warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" - "github.com/warewulf/warewulf/internal/pkg/image" "github.com/warewulf/warewulf/internal/pkg/kernel" "github.com/warewulf/warewulf/internal/pkg/node" ) @@ -39,7 +38,6 @@ type TemplateStruct struct { Tftp warewulfconf.TFTPConf Paths warewulfconf.BuildConfig AllNodes []node.Node - ImageList []string node.Node // backward compatiblity Container string @@ -50,7 +48,8 @@ type TemplateStruct struct { /* Initialize an TemplateStruct with the given node.NodeInfo */ -func InitStruct(overlayName string, nodeData node.Node, allNodes []node.Node) (tstruct TemplateStruct, err error) { +func InitStruct(overlayName string, nodeData node.Node, allNodes []node.Node) (TemplateStruct, error) { + var tstruct TemplateStruct tstruct.Overlay = overlayName hostname, _ := os.Hostname() tstruct.BuildHost = hostname @@ -77,10 +76,6 @@ func InitStruct(overlayName string, nodeData node.Node, allNodes []node.Node) (t // init some convenience vars tstruct.Id = nodeData.Id() tstruct.Hostname = nodeData.Id() - tstruct.ImageList, err = image.ListSources() - if err != nil { - return tstruct, err - } tstruct.Container = nodeData.ImageName tstruct.ContainerName = nodeData.ImageName // Backwards compatibility for templates using "Keys" diff --git a/internal/pkg/overlay/funcmap.go b/internal/pkg/overlay/funcmap.go index 717eadee..06a0c1a0 100644 --- a/internal/pkg/overlay/funcmap.go +++ b/internal/pkg/overlay/funcmap.go @@ -102,7 +102,7 @@ func templateImageFileInclude(imagename string, filepath string) string { // Don't return an error as we use this function for template evaluation, so // error will turn up there as the return string func createIgnitionJson(node *node.Node) string { - conf, rep, err := node.GetConfig() + conf, rep, err := node.GetIgnitionConfig() if len(conf.Storage.Disks) == 0 && len(conf.Storage.Filesystems) == 0 { wwlog.Debug("no disks or filesystems present, don't create a json object") return "" diff --git a/internal/pkg/overlay/overlay.go b/internal/pkg/overlay/overlay.go index 4b9d80d1..ea75bae9 100644 --- a/internal/pkg/overlay/overlay.go +++ b/internal/pkg/overlay/overlay.go @@ -14,6 +14,7 @@ import ( "text/template" "github.com/Masterminds/sprig/v3" + "github.com/coreos/go-systemd/v22/unit" "github.com/warewulf/warewulf/internal/pkg/config" "github.com/warewulf/warewulf/internal/pkg/node" @@ -496,12 +497,7 @@ func RenderTemplateFile(fileName string, data TemplateStruct) ( "softlink": softlink, "readlink": filepath.EvalSymlinks, "IgnitionJson": func() string { - str := createIgnitionJson(data.ThisNode) - if str != "" { - return str - } - writeFile = false - return "" + return createIgnitionJson(data.ThisNode) }, "abort": func() string { wwlog.Debug("abort file called in %s", fileName) @@ -513,7 +509,9 @@ func RenderTemplateFile(fileName string, data TemplateStruct) ( backupFile = false return "" }, - "UniqueField": UniqueField, + "UniqueField": UniqueField, + "SystemdEscape": unit.UnitNameEscape, + "SystemdEscapePath": unit.UnitNamePathEscape, } // Merge sprig.FuncMap with our FuncMap diff --git a/overlays/ignition/internal/ignition_test.go b/overlays/ignition/internal/ignition_test.go index c64b6e1c..89a0edfa 100644 --- a/overlays/ignition/internal/ignition_test.go +++ b/overlays/ignition/internal/ignition_test.go @@ -11,66 +11,36 @@ import ( ) func Test_ignitionOverlay(t *testing.T) { - env := testenv.New(t) - defer env.RemoveAll() - env.ImportFile("etc/warewulf/nodes.conf", "nodes.conf") - env.ImportFile("var/lib/warewulf/overlays/ignition/rootfs/etc/systemd/system/ww4-disks.target.ww", "../rootfs/etc/systemd/system/ww4-disks.target.ww") - env.ImportFile("var/lib/warewulf/overlays/ignition/rootfs/etc/systemd/system/ww4-mounts.ww", "../rootfs/etc/systemd/system/ww4-mounts.ww") - env.ImportFile("var/lib/warewulf/overlays/ignition/rootfs/warewulf/ignition.json.ww", "../rootfs/warewulf/ignition.json.ww") - - tests := []struct { - name string - args []string - log string - json bool + tests := map[string]struct { + args []string + nodesConf string + output string + isJson bool }{ - { - name: "ignition:ww4-disks.target", - args: []string{"--render", "node1", "ignition", "etc/systemd/system/ww4-disks.target.ww"}, - log: ignition_disks, - json: false, - }, - { - name: "ignition:ww4-mounts", - args: []string{"--render", "node1", "ignition", "etc/systemd/system/ww4-mounts.ww"}, - log: ignition_mounts, - json: false, - }, - { - name: "ignition:ignition.json", - args: []string{"--quiet", "--render", "node1", "ignition", "warewulf/ignition.json.ww"}, - log: ignition_json, - json: true, - }, - } + "ignition:ww4-disks.target": { + args: []string{"--quiet", "--render", "node1", "ignition", "etc/systemd/system/ww4-disks.target.ww"}, + nodesConf: ` +nodes: + node1: + disks: + /dev/vda: + wipe_table: true + partitions: + scratch: + should_exist: true + swap: + number: "1" + size_mib: "1024" + filesystems: + /dev/disk/by-partlabel/scratch: + format: btrfs + path: /scratch + wipe_filesystem: true + /dev/disk/by-partlabel/swap: + format: swap + path: swap`, + output: `# This file is autogenerated by warewulf - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cmd := show.GetCommand() - cmd.SetArgs(tt.args) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - logbuf := bytes.NewBufferString("") - cmd.SetOut(stdout) - cmd.SetErr(stderr) - wwlog.SetLogWriter(logbuf) - err := cmd.Execute() - assert.NoError(t, err) - assert.Empty(t, stdout.String()) - assert.Empty(t, stderr.String()) - if tt.json { - assert.JSONEq(t, tt.log, logbuf.String()) - } else { - assert.Equal(t, tt.log, logbuf.String()) - } - }) - } -} - -const ignition_disks string = `backupFile: true -writeFile: true -Filename: etc/systemd/system/ww4-disks.target -# This file is autogenerated by warewulf [Unit] Description=mount ww4 disks # make sure that the disks are available @@ -80,14 +50,76 @@ Requisite=ignition-ww4-disks.service # Get the mounts Wants=scratch.mount Wants=dev-disk-by\x2dpartlabel-swap.swap -` +`, + isJson: false, + }, + "ignition:ww4-disks.target (resources)": { + args: []string{"--quiet", "--render", "node1", "ignition", "etc/systemd/system/ww4-disks.target.ww"}, + nodesConf: ` +nodes: + node1: + resources: + ignition: + storage: + disks: + - device: /dev/vda + wipeTable: true + partitions: + - label: scratch + shouldExist: true + - label: swap + number: 1 + sizeMiB: 1024 + filesystems: + - device: /dev/disk/by-partlabel/scratch + format: btrfs + path: /scratch + wipeFilesystem: true + - device: /dev/disk/by-partlabel/swap + format: swap + path: swap`, + output: `# This file is autogenerated by warewulf -const ignition_mounts string = `backupFile: true +[Unit] +Description=mount ww4 disks +# make sure that the disks are available +Requires=ignition-ww4-disks.service +After=ignition-ww4-disks.service +Requisite=ignition-ww4-disks.service +# Get the mounts +Wants=scratch.mount +Wants=dev-disk-by\x2dpartlabel-swap.swap +`, + isJson: false, + }, + "ignition:ww4-mounts": { + args: []string{"--quiet=false", "--render", "node1", "ignition", "etc/systemd/system/ww4-mounts.ww"}, + nodesConf: ` +nodes: + node1: + disks: + /dev/vda: + wipe_table: true + partitions: + scratch: + should_exist: true + swap: + number: "1" + size_mib: "1024" + filesystems: + /dev/disk/by-partlabel/scratch: + format: btrfs + path: /scratch + wipe_filesystem: true + /dev/disk/by-partlabel/swap: + format: swap + path: swap`, + output: `backupFile: true writeFile: true Filename: scratch.mount - # This file is autogenerated by warewulf + [Unit] ConditionPathExists=/warewulf/ignition.json Before=local-fs.target @@ -99,11 +131,11 @@ What=/dev/disk/by-partlabel/scratch Type=btrfs [Install] RequiredBy=local-fs.target - backupFile: true writeFile: true Filename: dev-disk-by\x2dpartlabel-swap.swap # This file is autogenerated by warewulf + [Unit] ConditionPathExists=/warewulf/ignition.json Requires=ignition-ww4-disks.service @@ -113,9 +145,91 @@ Before=swap.target What=/dev/disk/by-partlabel/swap [Install] RequiredBy=swap.target -` +`, + isJson: false, + }, + "ignition:ww4-mounts (resources)": { + args: []string{"--quiet=false", "--render", "node1", "ignition", "etc/systemd/system/ww4-mounts.ww"}, + nodesConf: ` +nodes: + node1: + resources: + ignition: + storage: + disks: + - device: /dev/vda + wipeTable: true + partitions: + scratch: + shouldExist: true + swap: + number: 1 + sizeMiB: 1024 + filesystems: + - device: /dev/disk/by-partlabel/scratch + format: btrfs + path: /scratch + wipeFilesystem: true + - device: /dev/disk/by-partlabel/swap + format: swap + path: swap`, + output: `backupFile: true +writeFile: true +Filename: scratch.mount -const ignition_json string = `{ +# This file is autogenerated by warewulf + +[Unit] +ConditionPathExists=/warewulf/ignition.json +Before=local-fs.target +Requires=ignition-ww4-disks.service +After=ignition-ww4-disks.service +[Mount] +Where=/scratch +What=/dev/disk/by-partlabel/scratch +Type=btrfs +[Install] +RequiredBy=local-fs.target +backupFile: true +writeFile: true +Filename: dev-disk-by\x2dpartlabel-swap.swap +# This file is autogenerated by warewulf + +[Unit] +ConditionPathExists=/warewulf/ignition.json +Requires=ignition-ww4-disks.service +After=ignition-ww4-disks.service +Before=swap.target +[Swap] +What=/dev/disk/by-partlabel/swap +[Install] +RequiredBy=swap.target +`, + isJson: false, + }, + "ignition:ignition.json": { + args: []string{"--quiet", "--render", "node1", "ignition", "warewulf/ignition.json.ww"}, + nodesConf: ` +nodes: + node1: + disks: + /dev/vda: + wipe_table: true + partitions: + scratch: + should_exist: true + swap: + number: "1" + size_mib: "1024" + filesystems: + /dev/disk/by-partlabel/scratch: + format: btrfs + path: /scratch + wipe_filesystem: true + /dev/disk/by-partlabel/swap: + format: swap + path: swap`, + output: `{ "ignition": { "version": "3.1.0" }, @@ -155,4 +269,85 @@ const ignition_json string = `{ } ] } -}` +}`, + isJson: true, + }, + "ignition:ignition.json (resources)": { + args: []string{"--quiet", "--render", "node1", "ignition", "warewulf/ignition.json.ww"}, + nodesConf: ` +nodes: + node1: + resources: + ignition: + storage: + disks: + - device: /dev/vda + partitions: + - label: rootfs + number: 1 + shouldExist: true + wipePartitionEntry: false + wipeTable: true + filesystems: + - device: /dev/disk/by-partlabel/rootfs + format: ext4 + path: / + wipeFilesystem: false`, + output: `{ + "storage": { + "disks": [ + { + "device": "/dev/vda", + "partitions": [ + { + "label": "rootfs", + "number": 1, + "shouldExist": true, + "wipePartitionEntry": false + } + ], + "wipeTable": true + } + ], + "filesystems": [ + { + "device": "/dev/disk/by-partlabel/rootfs", + "format": "ext4", + "path": "/", + "wipeFilesystem": false + } + ] + } +}`, + isJson: true, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + env := testenv.New(t) + defer env.RemoveAll() + env.ImportFile("var/lib/warewulf/overlays/ignition/rootfs/etc/systemd/system/ww4-disks.target.ww", "../rootfs/etc/systemd/system/ww4-disks.target.ww") + env.ImportFile("var/lib/warewulf/overlays/ignition/rootfs/etc/systemd/system/ww4-mounts.ww", "../rootfs/etc/systemd/system/ww4-mounts.ww") + env.ImportFile("var/lib/warewulf/overlays/ignition/rootfs/warewulf/ignition.json.ww", "../rootfs/warewulf/ignition.json.ww") + env.WriteFile("etc/warewulf/nodes.conf", tt.nodesConf) + cmd := show.GetCommand() + cmd.SetArgs(tt.args) + stdout := bytes.NewBufferString("") + stderr := bytes.NewBufferString("") + logbuf := bytes.NewBufferString("") + cmd.SetOut(stdout) + cmd.SetErr(stderr) + wwlog.SetLogWriter(logbuf) + err := cmd.Execute() + assert.NoError(t, err) + assert.Empty(t, stdout.String()) + assert.Empty(t, stderr.String()) + if tt.isJson { + assert.JSONEq(t, tt.output, logbuf.String()) + } else { + assert.Equal(t, tt.output, logbuf.String()) + } + }) + } +} diff --git a/overlays/ignition/internal/nodes.conf b/overlays/ignition/internal/nodes.conf deleted file mode 100644 index c3b365ac..00000000 --- a/overlays/ignition/internal/nodes.conf +++ /dev/null @@ -1,19 +0,0 @@ -nodes: - node1: - disks: - /dev/vda: - wipe_table: true - partitions: - scratch: - should_exist: true - swap: - number: "1" - size_mib: "1024" - filesystems: - /dev/disk/by-partlabel/scratch: - format: btrfs - path: /scratch - wipe_filesystem: true - /dev/disk/by-partlabel/swap: - format: swap - path: swap diff --git a/overlays/ignition/rootfs/etc/systemd/system/ww4-disks.target.ww b/overlays/ignition/rootfs/etc/systemd/system/ww4-disks.target.ww index 13ed0045..d61a184c 100644 --- a/overlays/ignition/rootfs/etc/systemd/system/ww4-disks.target.ww +++ b/overlays/ignition/rootfs/etc/systemd/system/ww4-disks.target.ww @@ -1,4 +1,5 @@ # This file is autogenerated by warewulf + [Unit] Description=mount ww4 disks # make sure that the disks are available @@ -6,19 +7,20 @@ Requires=ignition-ww4-disks.service After=ignition-ww4-disks.service Requisite=ignition-ww4-disks.service # Get the mounts -{{- range $fsdevice,$fs := .FileSystems }} -{{- $prefix := $fsdevice }} -{{- $prefix = replace "-" "\\x2d" $fsdevice }} -{{- $prefix = replace "/" "-" $prefix }} -{{- $prefix = substr 1 -1 $prefix }}{{ $suffix := "mount" }} -{{- if eq $fs.Format "swap"}} -{{- $prefix = replace "/dev/disk/by-partlabel/" "dev-disk-by\\x2dpartlabel-" $fsdevice }}{{ $suffix = "swap"}} -{{- else }} -{{- if ne $fs.Path "" }} -{{- $prefix = replace "-" "\\x2d" $fs.Path }} -{{- $prefix = replace "/" "-" $prefix }} -{{- $prefix = substr 1 -1 $prefix }} +{{- if .FileSystems }} +{{- range $fsdevice, $fs := .FileSystems }} +{{- if eq $fs.Format "swap" }} +Wants={{ $fsdevice | SystemdEscapePath }}.swap +{{- else if ne $fs.Path "" }} +Wants={{ $fs.Path | SystemdEscapePath }}.mount +{{- end }} +{{- end }} +{{- else if .Resources.ignition.storage.filesystems }} +{{- range $fs := .Resources.ignition.storage.filesystems }} +{{- if eq $fs.format "swap" }} +Wants={{ $fs.device | SystemdEscapePath }}.swap +{{- else if ne $fs.path "" }} +Wants={{ $fs.path | SystemdEscapePath }}.mount {{- end }} {{- end }} -Wants={{ print $prefix "." $suffix }} {{- end }} diff --git a/overlays/ignition/rootfs/etc/systemd/system/ww4-mounts.ww b/overlays/ignition/rootfs/etc/systemd/system/ww4-mounts.ww index 71b00de9..bc9cca5f 100644 --- a/overlays/ignition/rootfs/etc/systemd/system/ww4-mounts.ww +++ b/overlays/ignition/rootfs/etc/systemd/system/ww4-mounts.ww @@ -1,13 +1,9 @@ -{{- range $fsdevice,$fs := .FileSystems }} -{{- $prefix := replace "-" "\\x2d" $fs.Path }} -{{- $prefix = replace "/" "-" $prefix }} -{{- $prefix = substr 1 -1 $prefix }} -{{ $suffix := "mount"}} -{{- if $fs.Label }}{{ $prefix = $fs.Label }}{{ end }} -{{- if eq $fs.Format "swap" }}{{ $prefix = replace "/dev/disk/by-partlabel/" "dev-disk-by\\x2dpartlabel-" $fsdevice }}{{ $suffix = "swap" }}{{ end }} -{{ file (print $prefix "." $suffix) }} +{{- if .FileSystems }} +{{- range $fsdevice, $fs := .FileSystems }} +{{- if eq $fs.Format "swap" }} +{{ file (print ($fsdevice | SystemdEscapePath) ".swap") }} # This file is autogenerated by warewulf -{{- if eq $fs.Format "swap"}} + [Unit] ConditionPathExists=/warewulf/ignition.json Requires=ignition-ww4-disks.service @@ -17,7 +13,10 @@ Before=swap.target What={{ $fsdevice }} [Install] RequiredBy=swap.target -{{- else }} +{{- else if ne $fs.Path "" }} +{{ file (print ($fs.Path | SystemdEscapePath) ".mount") }} +# This file is autogenerated by warewulf + [Unit] ConditionPathExists=/warewulf/ignition.json Before=local-fs.target @@ -28,9 +27,49 @@ Where={{ $fs.Path }} What={{ $fsdevice }} Type={{ $fs.Format }} {{- if $fs.MountOptions }} -Options={{range $index,$opt := $fs.MountOptions }}{{if $index }},{{ end }}{{ $opt }}{{ end }} +Options={{ $fs.MountOptions | join "." }} {{- end }} [Install] RequiredBy=local-fs.target +{{- else }} +{{- continue }} +{{- end }} +{{- end }} +{{- else if .Resources.ignition.storage.filesystems }} +{{- range $fs := .Resources.ignition.storage.filesystems }} +{{- if eq $fs.format "swap" }} +{{ file (print ($fs.device | SystemdEscapePath) ".swap") }} +# This file is autogenerated by warewulf + +[Unit] +ConditionPathExists=/warewulf/ignition.json +Requires=ignition-ww4-disks.service +After=ignition-ww4-disks.service +Before=swap.target +[Swap] +What={{ $fs.device }} +[Install] +RequiredBy=swap.target +{{- else if ne $fs.path "" }} +{{ file (print ($fs.path | SystemdEscapePath) ".mount") }} +# This file is autogenerated by warewulf + +[Unit] +ConditionPathExists=/warewulf/ignition.json +Before=local-fs.target +Requires=ignition-ww4-disks.service +After=ignition-ww4-disks.service +[Mount] +Where={{ $fs.path }} +What={{ $fs.device }} +Type={{ $fs.format }} +{{- if $fs.mountOptions }} +Options={{ $fs.mountOptions | join "." }} +{{- end }} +[Install] +RequiredBy=local-fs.target +{{- else }} +{{- continue }} +{{- end }} {{- end }} {{- end }} diff --git a/overlays/ignition/rootfs/warewulf/ignition.json.ww b/overlays/ignition/rootfs/warewulf/ignition.json.ww index 7b800e85..bda25de0 100644 --- a/overlays/ignition/rootfs/warewulf/ignition.json.ww +++ b/overlays/ignition/rootfs/warewulf/ignition.json.ww @@ -1 +1 @@ -{{ IgnitionJson }} +{{ coalesce IgnitionJson (.Resources.ignition | toJson) }} \ No newline at end of file diff --git a/overlays/ignition/rootfs/warewulf/wwinit.d/10-ignition.sh b/overlays/ignition/rootfs/warewulf/wwinit.d/10-ignition.sh new file mode 100644 index 00000000..77856ce6 --- /dev/null +++ b/overlays/ignition/rootfs/warewulf/wwinit.d/10-ignition.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +PATH=$PATH:/sbin:/usr/sbin:/bin:/usr/bin + +if ! command -v info >/dev/null; then + info() { + printf '%s\n' "$*" + } +fi + +if ! command -v die >/dev/null; then + die() { + printf '%s\n' "$*" >&2 + exit 1 + } +fi + +if command -v ignition >/dev/null; then : + info "warewulf: ignition: partition and format disks" + ignition --config-cache="${PREFIX}/warewulf/ignition.json" --platform=metal --stage=disks || die "warewulf: ignition: failed to partition/format disk" +else + info "warewulf: ignition not found" +fi diff --git a/overlays/persistent/ignition.json.ww b/overlays/persistent/ignition.json.ww deleted file mode 100644 index de2ece76..00000000 --- a/overlays/persistent/ignition.json.ww +++ /dev/null @@ -1 +0,0 @@ -{{ IgnitionJson }} \ No newline at end of file diff --git a/overlays/wwinit/rootfs/init b/overlays/wwinit/rootfs/init index 13b15196..239822e4 100755 --- a/overlays/wwinit/rootfs/init +++ b/overlays/wwinit/rootfs/init @@ -1,53 +1,65 @@ #!/bin/sh -echo "Warewulf pre-init (/init)" -echo -echo "Mounting kernel file systems..." +if ! command -v info >/dev/null; then + info() { + printf '%s\n' "$*" + } +fi + +if ! command -v die >/dev/null; then + die() { + printf '%s\n' "$*" >&2 + echo + echo "Rebooting in 1 minute..." + sleep 60 + echo b > /proc/sysrq-trigger || /sbin/reboot -f + } +fi + +info "warewulf: running pre-init wwinit process..." + +info "warewulf: mounting kernel file systems..." mountpoint -q /proc || (mkdir -p /proc && mount -t proc proc /proc && echo "Mounted /proc") mountpoint -q /dev || (mkdir -p /dev && mount -t devtmpfs devtmpfs /dev && echo "Mounted /dev") mountpoint -q /sys || (mkdir -p /sys && mount -t sysfs sysfs /sys && echo "Mounted /sys") # https://systemd.io/INITRD_INTERFACE/ mountpoint -q /run || (mkdir -p /run && mount -t tmpfs -o mode=0755,nodev,nosuid,strictatime tmpfs /run && echo "Mounted /run") -echo -echo "Loading /warewulf/config..." + +info "warewulf: loading /warewulf/config..." if [ -f "/warewulf/config" ]; then . /warewulf/config else - echo "ERROR: Warewulf configuration file not found." - echo "Rebooting in 1 minute..." - sleep 60 - echo b > /proc/sysrq-trigger || /sbin/reboot -f + die "warewulf: ERROR: /warewulf/config not found" fi -echo -echo "Configuring root file system..." -WWPRESCRIPTS=/warewulf/wwprescripts -chmod +rx "${WWPRESCRIPTS}" +if [ -x /warewulf/run-wwinit.d ]; then + /warewulf/run-wwinit.d +fi +NEXT_INIT=/warewulf/run-init +chmod +rx "${NEXT_INIT}" + +info "warewulf: configuring root file system..." WWROOT="${WWROOT:-initramfs}" if [ "${WWROOT}" = "initramfs" ]; then - echo "WWROOT=${WWROOT}: using initial rootfs and invoking ${WWPRESCRIPTS}..." - exec "${WWPRESCRIPTS}" -elif [ "${WWROOT}" = "persistent" ] ; then - echo "WWROOT=${WWROOT}: booting from DISK" - exec "${WWPRESCRIPTS}" -elif [ "${WWROOT}" = "ramfs" -o "${WWROOT}" = "tmpfs" ]; then - echo "WWROOT=${WWROOT}: setting up new ${WWROOT} rootfs (/newroot)..." - mkdir /newroot - mount wwroot /newroot -t ${WWROOT} -o mpol=interleave # mpol ignored for ramfs - tar -cf - --exclude ./proc --exclude ./sys --exclude ./dev --exclude --exclude ./newroot . | tar -xf - -C /newroot - mkdir /newroot/proc /newroot/dev /newroot/sys 2>/dev/null - echo "Switching to new rootfs (/newroot) and invoking ${WWPRESCRIPTS}..." - exec /sbin/switch_root /newroot "${WWPRESCRIPTS}" + info "warewulf: using initial rootfs and invoking ${NEXT_INIT}..." + exec "${NEXT_INIT}" else - echo "ERROR: Unrecognized rootfs type requested: ${WWROOT}" - echo "Rebooting in 1 minute..." - sleep 60 - echo b > /proc/sysrq-trigger || /sbin/reboot -f + mkdir /newroot + info "warewulf: mounting ${WWROOT} at /newroot" + if [ "${WWROOT}" = "ramfs" -o "${WWROOT}" = "tmpfs" ]; then + mount wwroot /newroot -t ${WWROOT} -o mpol=interleave # mpol ignored for ramfs + else + mount ${WWROOT} /newroot + fi + + info "warewulf: copying image to /newroot..." + tar -cf - --exclude ./proc --exclude ./sys --exclude ./dev --exclude --exclude ./newroot . | tar -xf - -C /newroot + + mkdir /newroot/proc /newroot/dev /newroot/sys 2>/dev/null + + info "warewulf: switching to /newroot and invoking ${NEXT_INIT}..." + exec /sbin/switch_root /newroot "${NEXT_INIT}" fi -echo -echo "ERROR: wwinit encountered a problem." -echo "Rebooting in 1 minute..." -sleep 60 -echo b > /proc/sysrq-trigger || /sbin/reboot -f +die "warewulf: ERROR: wwinit encountered a problem." diff --git a/overlays/wwinit/rootfs/warewulf/run-init b/overlays/wwinit/rootfs/warewulf/run-init new file mode 100755 index 00000000..aaac47b8 --- /dev/null +++ b/overlays/wwinit/rootfs/warewulf/run-init @@ -0,0 +1,40 @@ +#!/bin/sh + +if ! command -v info >/dev/null; then + info() { + printf '%s\n' "$*" + } +fi + +. /warewulf/config + +scriptdir=/warewulf/init.d +echo "warewulf: running scripts in ${scriptdir}..." +ls -1 "${scriptdir}/" | while read -r name; do + info "warewulf: ${name}" + sh "${scriptdir}/${name}" +done + +init="${WWINIT}" +if [ -z "${init}" ] +then + for candidate in /sbin/init /etc/init /bin/init + do + if [ -x "${candidate}" ] + then + init="${candidate}" + break + fi + done +fi + +if [ -n "${init}" ] +then + info "warewulf: invoking ${init}..." + exec "${init}" +else + echo "ERROR: init not defined or not found." + echo "Rebooting in 1 minute..." + sleep 60 + echo b > /proc/sysrq-trigger || /sbin/reboot -f +fi diff --git a/overlays/wwinit/rootfs/warewulf/run-wwinit.d b/overlays/wwinit/rootfs/warewulf/run-wwinit.d new file mode 100755 index 00000000..22afd8b1 --- /dev/null +++ b/overlays/wwinit/rootfs/warewulf/run-wwinit.d @@ -0,0 +1,18 @@ +#!/bin/sh + +if ! command -v info >/dev/null; then + info() { + printf '%s\n' "$*" + } +fi + +scriptdir="${PREFIX}/warewulf/wwinit.d" +if [ -d "${scriptdir}" ]; then + info "warewulf: running scripts in ${scriptdir}..." + ls -1 "${scriptdir}/" | while read -r name; do + info "warewulf: ${name}" + PREFIX=$PREFIX sh "${scriptdir}/${name}" + done +else + info "warewulf: ${scriptdir} not found" +fi diff --git a/overlays/wwinit/rootfs/warewulf/wwprescripts b/overlays/wwinit/rootfs/warewulf/wwprescripts deleted file mode 100755 index ce472025..00000000 --- a/overlays/wwinit/rootfs/warewulf/wwprescripts +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/sh - -. /warewulf/config - -echo "Warewulf prescript runner (/warewulf/wwprescripts)" -scriptdir=/warewulf/init.d -echo "Looking for prescripts in /warewulf/init.d/..." -ls -1 "${scriptdir}/" | while read -r name; do - echo "Running prescript: ${name}..." - sh "${scriptdir}/${name}" -done - -init="${WWINIT}" -if [ -z "${init}" ] -then - for candidate in /sbin/init /etc/init /bin/init - do - if [ -x "${candidate}" ] - then - init="${candidate}" - break - fi - done -fi - -if [ -n "${init}" ] -then - echo - echo "Running ${init}..." - exec "${init}" -else - echo "ERROR: init not defined and not found." - echo "Rebooting in 1 minute..." - sleep 60 - echo b > /proc/sysrq-trigger || /sbin/reboot -f -fi diff --git a/overlays/wwinit/rootfs/warewulf/wwprescripts b/overlays/wwinit/rootfs/warewulf/wwprescripts new file mode 120000 index 00000000..624cd3ad --- /dev/null +++ b/overlays/wwinit/rootfs/warewulf/wwprescripts @@ -0,0 +1 @@ +run-init \ No newline at end of file diff --git a/userdocs/overlays/overlays.rst b/userdocs/overlays/overlays.rst index 3d5f557b..422ddcd7 100644 --- a/userdocs/overlays/overlays.rst +++ b/userdocs/overlays/overlays.rst @@ -192,8 +192,15 @@ In particular: - Update PAM configuration to allow missing shadow entries - Relabel the file system for SELinux -Other overlays may place additional scripts in ``/warewulf/init.d/`` to affect -node configuration in this pre-boot environment. +Other overlays can place scripts in one of two locations for additional pre-init +provisioning actions: + +- **/warewulf/wwinit.d/:** executed in the initial root final system before the + image is loaded into its final location. In a two-stage boot, these scripts + are executed in the Dracut initramfs. + +- **/warewulf/init.d/:** executed in the final root file system but before + calling ``init``. wwclient -------- @@ -309,6 +316,36 @@ ignition -------- The **ignition** overlay defines partitions and file systems on local disks. +Configuration may be provided via native disk, partition, and filesystem fields +or via an ``ignition`` resource. + +.. code-block:: yaml + + ignition: + storage: + disks: + - device: /dev/vda + partitions: + - label: scratch + shouldExist: true + wipePartitionEntry: true + wipeTable: true + filesystems: + - device: /dev/disk/by-partlabel/scratch + format: btrfs + path: /scratch + wipeFilesystem: false + +If any disk/partition/filesystem configuration is provided for a node with +explicit arguments to ``wwctl set``, the ``ignition`` resource is +ignored. + +To use ignition during Dracut (so that the root file system may be provisioned +before the image is loaded) include Ignition in the Dracut image. + +.. code-block:: shell + + wwctl image exec rockylinux-9 -- /usr/bin/dracut --force --no-hostonly --add wwinit --add ignition --regenerate-all debug ----- diff --git a/userdocs/overlays/templates.rst b/userdocs/overlays/templates.rst index 5818574e..9d7a806b 100644 --- a/userdocs/overlays/templates.rst +++ b/userdocs/overlays/templates.rst @@ -148,7 +148,13 @@ symlink. IgnitionJson ------------ -Generates JSON suitable for use by Ignition to create +Generates JSON suitable for use by Ignition to create partitions and file +systems, from the data stored in a node's native ``disks`` and ``filesystems`` +fields. + +.. code-block:: + + {{ IgnitionJson }} abort ----- @@ -174,10 +180,9 @@ nobackup UniqueField ----------- -UniqueField returns a filtered version of a multi-line input string. input is -expected to be a field-separated format with one record per line (terminated by -`\n`). Order of lines is preserved, with the first matching line taking -precedence. +Returns a filtered version of a multi-line input string. input is expected to be +a field-separated format with one record per line (terminated by `\n`). Order of +lines is preserved, with the first matching line taking precedence. For example, the following template snippet has been used in the ``syncuser`` overlay to generate a combined ``/etc/passwd``. @@ -191,6 +196,24 @@ to generate a combined ``/etc/passwd``. | UniqueField ":" 0 | trim }} +SystemdEscape +------------- + +Escapes a string for use in a systemd unit file. + +Escape rules are documented at `systemd.unit. `_ + +SystemdEscapePath +----------------- + +Escapes a path for use in a systemd unit file. + +.. code-block:: + + {{ file (print ($fs.path | SystemdEscapePath) ".mount") }} + +Escape rules are documented at `systemd.unit. `_ + Examples ======== diff --git a/userdocs/server/bootloaders.rst b/userdocs/server/bootloaders.rst index 6c2941d6..c9bf9b2f 100644 --- a/userdocs/server/bootloaders.rst +++ b/userdocs/server/bootloaders.rst @@ -159,7 +159,7 @@ For example, the ``imgextract`` command can be `explicitly enabled`_. support for ZLIB and GZIP archive image formats. Configuring Warewulf (>= v4.5.0) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In Warewulf v4.5.0, Warewulf can be configured to use these files using the ``tftp.ipxe`` and ``paths.ipxesource`` configuration parameters in @@ -321,8 +321,8 @@ directly from the node's assigned image. .. _booting with dracut: -Booting with dracut -=================== +Two-stage boot: dracut +====================== Some systems, typically due to limitations in their BIOS or EFI firmware, are unable to load image of a certain size directly with a traditional bootloader, @@ -337,21 +337,15 @@ must be installed in the image. With the ``warewulf-dracut`` package installed in the image, you can then build an initramfs inside the image. -EL installation ---------------- .. code-block:: shell + # Enterprise Linux wwctl image exec rockylinux-9 --build=false -- /usr/bin/dnf -y install https://github.com/warewulf/warewulf/releases/download/v4.6.1/warewulf-dracut-4.6.1-1.el9.noarch.rpm wwctl image exec rockylinux-9 -- /usr/bin/dracut --force --no-hostonly --add wwinit --regenerate-all - -SUSE installation ------------------ -.. code-block:: shell - - zypper -y install warewulf-dracut - dracut --force --no-hostonly --add wwinit --regenerate-all - + # SUSE + wwctl image exec leap-15 --build=false -- /usr/bin/zypper -y install https://github.com/warewulf/warewulf/releases/download/v4.6.1/warewulf-dracut-4.6.1-1.suse.lp155.noarch.rpm + wwctl image exec leap-15 -- /usr/bin/dracut --force --no-hostonly --add wwinit --regenerate-all .. note:: @@ -403,42 +397,70 @@ to 50% of physical memory. This size limit may be adjusted using the kernel argument `wwinit.tmpfs.size`. (This parameter is passed to the `size` option during tmpfs mount. See ``tmpfs(5)`` for more details.) -Persistent installation -======================== -With the `dracut` installation enabled warewulf can also install -the node image to a harddrive. On the first boot of the node the -compressed node image is simply dumped onto the configured partition. +.. _provision to disk: +Provision to disk +================= + +*New in Warewulf v4.6.2* + +As a tech preview, the Warewulf two-stage boot process can provision the node +image to local storage. .. warning:: - warewulf doesn't install the bootloader to the disk and add UEFI - entries. In order to boot the node network booting is required and - at every boot the kernel and the initrd is transfered over the network. -Configuration -------------- + This functionality is a technology preview and should be used with care. Pay + specific attention to ``wipeFilesytem`` (ignition) and ``overwrite`` (mkfs) + settings. -The node image will be installed to the partition called `rootfs`. You -can add add a rootfs with e.g. following command - -.. code-block:: shell - - wwctl node set n01 \ - --diskname /dev/vda --diskwipe \ - --partname rootfs --partcreate --partnumber 1 \ - --fsname rootfs --fsformat ext4 --fspath /rootfs - -Now the node boot method has to be set to persitent with following command - -.. code-block:: shell - - wwctl node set n01 --root persistent - -and the node rebooted. - .. note:: - If the boot mode is perisiten the configured partion labeled `rootfs` will - be mounted as `/`. With any other boot method mount point confiured (here `/roofs`) - will be used. + Warewulf doesn't install a bootloader to the disk or add UEFI entries. Nodes + still request an image and configuration from the Warewulf server on every + boot. +.. note:: + + While provisioning to disk should be possible during a single-stage boot, not + all features are available: + + - Warewulf does not perform hardware detection to ensure that necessary + kernel modules are loaded prior to init. + - Warewulf does not load udev to ensure that ``/dev/disk/by-*`` symlinks are + available prior to init. + +With Ignition +------------- + +Warewulf needs a prepared file system to deploy the image to. Warewulf can +provision this file system using Ignition. To use Ignition, include ``ignition`` +in your system overlay. The ignition overlay provisions disks during init and, +optionally, during the first stage of a two-stage boot. This allows the +root file system to be provisioned before the image is loaded. + +.. code-block:: shell + + wwctl node set wwnode1 \ + --diskname /dev/vda --diskwipe \ + --partname rootfs --partcreate --partnumber 1 \ + --fsname rootfs --fsformat ext4 --fspath / + +In order to allow Dracut to provision the disk, partition, and file system, +Ignition must be included in the Dracut image. + +.. code-block:: shell + + wwctl image exec rockylinux-9 -- /usr/bin/dracut --force --no-hostonly --add wwinit --add ignition --regenerate-all + +The necessary file system may alternatively be prepared out-of-band. + + +Configuring the root device +--------------------------- + +Set the desired storage device for the node image using the ``--root`` +parameter. + +.. code-block:: shell + + wwctl node set wwnode1 --root /dev/disk/by-partlabel/rootfs