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 <node|profile> set --parttype`
- Add overlay template functions `SystemdEscape` and `SystemdEscapePath`
- Support configuring ignition with resources

Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Jonathon Anderson
2025-06-30 23:09:49 -06:00
parent 608ef89d40
commit b98eae4b01
29 changed files with 787 additions and 354 deletions

View File

@@ -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 override.conf for nm-wait-online-initrd.service with dracut.
- Added userdocs for `wwctl node import` from yaml/csv. - Added userdocs for `wwctl node import` from yaml/csv.
- Added uid, gid, and permissions to OverlayFile in REST API. #1925 - Added uid, gid, and permissions to OverlayFile in REST API. #1925
- (preview) Support provisioning to local storage with `wwctl <node|partition> 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 <node|partition> set --parttype`. #1894
### Fixed ### 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 - Add man pages and command reference to userdocs. #1488
- Document building images from scratch with Apptainer. #1485 - Document building images from scratch with Apptainer. #1485
- Added warewulfd:/overlay-file/{overlay}/{path...}?render={id} - 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 ### Changed

View File

@@ -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 d -exec mkdir -pv $(DESTDIR)$(DATADIR)/warewulf/overlays/{} \;)
(cd overlays && find * -path '*/internal' -prune -o -type l -exec cp -av {} $(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/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/wwinit/rootfs/$(WWCLIENTDIR)/config.ww
chmod 0600 $(DESTDIR)$(DATADIR)/warewulf/overlays/ssh.host_keys/rootfs/etc/ssh/ssh* 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 chmod 0644 $(DESTDIR)$(DATADIR)/warewulf/overlays/ssh.host_keys/rootfs/etc/ssh/ssh*.pub.ww

View File

@@ -1,52 +1,15 @@
#!/bin/bash #!/bin/bash
function setup_disks() { get_stage() {
# format and prepare the disk(s) stage="${1}"
/usr/bin/ignition --root=/sysroot --platform=metal --stage=disks || die "warewulf: failed to partition/format disk" info "warewulf: loading stage: ${stage}"
# mount /dev/disk/by-partlabel/root "$NEWROOT" || die "warewulf: failed to mount new root" # Load runtime overlay from a static privledged port.
/usr/bin/ignition --root=/sysroot --platform=metal --stage=mount || die "warewulf: failed to mount disk" # Others use default settings.
localport=""
} if [ "${stage}" = "runtime" ]; then
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
localport="--local-port 1-1023" localport="--local-port 1-1023"
fi fi
( (
curl --location --silent --get ${localport} \ curl --location --silent --get ${localport} \
--retry 60 --retry-connrefused --retry-delay 1 \ --retry 60 --retry-connrefused --retry-delay 1 \
--data-urlencode "assetkey=${wwinit_assetkey}" \ --data-urlencode "assetkey=${wwinit_assetkey}" \
@@ -55,19 +18,29 @@ for stage in $archives ; do
--data-urlencode "compress=gz" \ --data-urlencode "compress=gz" \
"${wwinit_uri}" \ "${wwinit_uri}" \
| gzip -d \ | gzip -d \
| cpio -im --directory="${NEWROOT}" | cpio -ium --directory="${NEWROOT}"
) || die "Unable to load stage: ${stage}" ) || die "Unable to load stage: ${stage}"
done }
if [ ${wwinit_persistent} -eq 1 ] ; then mkdir /tmp/wwinit
# this avoids that ignition runs a second time (
info "warewulf: removing ignition config from wwinit image" # fetch the system overlay into /tmp/wwinit
rm -rf ${NEWROOT}/warewulf/ignition.json local NEWROOT=/tmp/wwinit
echo "Container name of persistent install: ${wwinit_imagename}" > ${NEWROOT}/warewulf/ww_${wwinit_imagename} get_stage "system"
echo "# created from ${wwinit_ignition_uri}/fstab.ww?render=${winit_id}" > ${NEWROOT}/etc/fstab )
curl --location --silent --get ${localport} \ if [ -x /tmp/wwinit/warewulf/run-wwinit.d ]; then
--retry 60 --retry-delay 1 \ PREFIX=/tmp/wwinit /tmp/wwinit/warewulf/run-wwinit.d
"${wwinit_ignition}/fstab.ww?render=${wwinit_id}" >> ${NEWROOT}/etc/fstab || die "warewulf: failed to write correct fstab"
fi 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

View File

@@ -9,12 +9,12 @@ check() {
} }
depends() { depends() {
echo network ignition echo network
return 0 return 0
} }
install() { install() {
inst_multiple cpio curl dmidecode rsync jq inst_multiple cpio curl dmidecode
inst_hook cmdline 30 "$moddir/parse-wwinit.sh" inst_hook cmdline 30 "$moddir/parse-wwinit.sh"
inst_hook pre-mount 30 "$moddir/load-wwinit.sh" inst_hook pre-mount 30 "$moddir/load-wwinit.sh"
if dracut_module_included "network-manager" && dracut_module_included "systemd" if dracut_module_included "network-manager" && dracut_module_included "systemd"

View File

@@ -1,29 +1,42 @@
#!/bin/sh #!/bin/sh
# root=wwinit # root=wwinit|wwinit:*
[ -z "$root" ] && root=$(getarg root=) [ -z "$root" ] && root=$(getarg root=)
if [ "${root}" = "wwinit" ] || [ "${root}" = "persistent" ] ; then case "${root}" in
wwinit|wwinit:*)
info "warewulf: root=${root}" 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_uri="$(getarg wwinit.uri)"
export wwinit_ignition="$(getarg wwinit.ignition)" if [ -n "${wwinit_uri}" ]; then
export wwinit_ip="$(getarg wwinit.ip)" info "warewulf: Found root=${root} and wwinit.uri=${wwinit_uri}. Will boot from Warewulf."
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."
rootok=1 rootok=1
else 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
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

View File

@@ -31,10 +31,7 @@ smbios --type 3 --get-string 8 --set assetkey
uri="(http,{{.Ipaddr}}:{{.Port}})/provision/${net_default_mac}?assetkey=${assetkey}" uri="(http,{{.Ipaddr}}:{{.Port}})/provision/${net_default_mac}?assetkey=${assetkey}"
kernel="${uri}&stage=kernel" kernel="${uri}&stage=kernel"
{{- $defentry := "singlestage" }} set default={{ or .Tags.GrubMenuEntry "single-stage" }}
{{- if eq .Root "persistent"}}{{ $defentry = "dracut" }}{{ end }}
{{- if .Tags.GrubMenuEntry }}{{ $defentry = .Tags.GrubMenuEntry }}{{ end }}
set default={{ $defentry}}
set timeout=2 set timeout=2
menuentry "Single-stage boot" --id single-stage { 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 "!! Unable to load images."
echo "!! Rebooting in 15s..." echo "!! Rebooting in 15s..."
echo "!!" echo "!!"
sleep 15
reboot
fi fi
echo "Booting..." echo "Booting..."
boot 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 "Warewulf Server:"
echo "* Ipaddr: {{.Ipaddr}}" echo "* Ipaddr: {{.Ipaddr}}"
echo "* Port: {{.Port}}" 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}" 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}}" 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"}} wwinit_args="root=wwinit:{{default "tmpfs" .Root}} wwinit.uri=${wwinit_uri} init=/warewulf/run-init"
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"
echo echo
echo "Downloading kernel image..." 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 ] if [ $? != 0 ]
then then
echo "!!" echo "!!"

View File

@@ -88,7 +88,7 @@ echo
echo Downloading dracut initramfs... echo Downloading dracut initramfs...
initrd --name initramfs ${uri}&stage=initramfs || goto error_reboot 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_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 goto boot_two_stage_dracut
:boot_single_stage :boot_single_stage

2
go.mod
View File

@@ -12,6 +12,7 @@ require (
github.com/containers/image/v5 v5.32.2 github.com/containers/image/v5 v5.32.2
github.com/containers/storage v1.57.1 github.com/containers/storage v1.57.1
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e 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/ignition/v2 v2.20.0
github.com/coreos/vcontext v0.0.0-20230201181013-d72178a18687 github.com/coreos/vcontext v0.0.0-20230201181013-d72178a18687
github.com/creasty/defaults v1.8.0 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/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
github.com/containers/ocicrypt v1.2.0 // indirect github.com/containers/ocicrypt v1.2.0 // indirect
github.com/coreos/go-semver v0.3.1 // 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/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f // indirect github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/cyphar/filepath-securejoin v0.3.6 // indirect

View File

@@ -194,3 +194,16 @@ func (config *NodesYaml) FindDiscoverableNode() (Node, string, error) {
return EmptyNode(), "", ErrNoUnconfigured 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
}
}

View File

@@ -92,6 +92,7 @@ type NetDev struct {
Holds the disks of a node Holds the disks of a node
*/ */
type Disk struct { 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"` 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"` 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 Partitions map
*/ */
type Partition struct { 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"` id string `yaml:"-" json:"-"`
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"` 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"` 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"` 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"` 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"` 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 Definition of a filesystem. The device is uniq so its used as key
*/ */
type FileSystem struct { type FileSystem struct {
id string `yaml:"-" json:"-"`
Format string `yaml:"format,omitempty" json:"format,omitempty" lopt:"fsformat" comment:"format of the file system"` 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"` 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"` WipeFileSystem bool `yaml:"wipe_filesystem,omitempty" json:"wipe_filesystem,omitempty" lopt:"fswipe" comment:"wipe file system at boot"`

View File

@@ -157,7 +157,7 @@ func (config *NodesYaml) MergeNode(id string) (node Node, fields fieldMap, err e
delete(fields, "Profiles") delete(fields, "Profiles")
} }
node.id = id node.SetIds(id)
node.valid = true node.valid = true
node.updatePrimaryNetDev() node.updatePrimaryNetDev()
node.Profile.cleanLists() node.Profile.cleanLists()

View File

@@ -10,10 +10,97 @@ import (
"github.com/warewulf/warewulf/internal/pkg/wwlog" "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 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 var fileSystems []types_3_4.Filesystem
for fsdevice, fs := range node.FileSystems { for fsdevice, fs := range node.FileSystems {
var mountOptions []types_3_4.MountOption var mountOptions []types_3_4.MountOption
@@ -141,8 +228,8 @@ type SimpleIgnitionConfig struct {
/* /*
Get a simple config which can be marshalled to json Get a simple config which can be marshalled to json
*/ */
func (node *Node) GetConfig() (conf SimpleIgnitionConfig, rep string, err error) { func (node *Node) GetIgnitionConfig() (conf SimpleIgnitionConfig, rep string, err error) {
conf.Storage, rep, err = node.GetStorage() conf.Storage, rep, err = node.GetIgnitionStorage()
conf.Ignition.Version = "3.1.0" conf.Ignition.Version = "3.1.0"
return return
} }

View File

@@ -8,7 +8,6 @@ import (
"time" "time"
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" 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/kernel"
"github.com/warewulf/warewulf/internal/pkg/node" "github.com/warewulf/warewulf/internal/pkg/node"
) )
@@ -39,7 +38,6 @@ type TemplateStruct struct {
Tftp warewulfconf.TFTPConf Tftp warewulfconf.TFTPConf
Paths warewulfconf.BuildConfig Paths warewulfconf.BuildConfig
AllNodes []node.Node AllNodes []node.Node
ImageList []string
node.Node node.Node
// backward compatiblity // backward compatiblity
Container string Container string
@@ -50,7 +48,8 @@ type TemplateStruct struct {
/* /*
Initialize an TemplateStruct with the given node.NodeInfo 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 tstruct.Overlay = overlayName
hostname, _ := os.Hostname() hostname, _ := os.Hostname()
tstruct.BuildHost = hostname tstruct.BuildHost = hostname
@@ -77,10 +76,6 @@ func InitStruct(overlayName string, nodeData node.Node, allNodes []node.Node) (t
// init some convenience vars // init some convenience vars
tstruct.Id = nodeData.Id() tstruct.Id = nodeData.Id()
tstruct.Hostname = nodeData.Id() tstruct.Hostname = nodeData.Id()
tstruct.ImageList, err = image.ListSources()
if err != nil {
return tstruct, err
}
tstruct.Container = nodeData.ImageName tstruct.Container = nodeData.ImageName
tstruct.ContainerName = nodeData.ImageName tstruct.ContainerName = nodeData.ImageName
// Backwards compatibility for templates using "Keys" // Backwards compatibility for templates using "Keys"

View File

@@ -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 // Don't return an error as we use this function for template evaluation, so
// error will turn up there as the return string // error will turn up there as the return string
func createIgnitionJson(node *node.Node) 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 { if len(conf.Storage.Disks) == 0 && len(conf.Storage.Filesystems) == 0 {
wwlog.Debug("no disks or filesystems present, don't create a json object") wwlog.Debug("no disks or filesystems present, don't create a json object")
return "" return ""

View File

@@ -14,6 +14,7 @@ import (
"text/template" "text/template"
"github.com/Masterminds/sprig/v3" "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/config"
"github.com/warewulf/warewulf/internal/pkg/node" "github.com/warewulf/warewulf/internal/pkg/node"
@@ -496,12 +497,7 @@ func RenderTemplateFile(fileName string, data TemplateStruct) (
"softlink": softlink, "softlink": softlink,
"readlink": filepath.EvalSymlinks, "readlink": filepath.EvalSymlinks,
"IgnitionJson": func() string { "IgnitionJson": func() string {
str := createIgnitionJson(data.ThisNode) return createIgnitionJson(data.ThisNode)
if str != "" {
return str
}
writeFile = false
return ""
}, },
"abort": func() string { "abort": func() string {
wwlog.Debug("abort file called in %s", fileName) wwlog.Debug("abort file called in %s", fileName)
@@ -513,7 +509,9 @@ func RenderTemplateFile(fileName string, data TemplateStruct) (
backupFile = false backupFile = false
return "" return ""
}, },
"UniqueField": UniqueField, "UniqueField": UniqueField,
"SystemdEscape": unit.UnitNameEscape,
"SystemdEscapePath": unit.UnitNamePathEscape,
} }
// Merge sprig.FuncMap with our FuncMap // Merge sprig.FuncMap with our FuncMap

View File

@@ -11,66 +11,36 @@ import (
) )
func Test_ignitionOverlay(t *testing.T) { func Test_ignitionOverlay(t *testing.T) {
env := testenv.New(t) tests := map[string]struct {
defer env.RemoveAll() args []string
env.ImportFile("etc/warewulf/nodes.conf", "nodes.conf") nodesConf string
env.ImportFile("var/lib/warewulf/overlays/ignition/rootfs/etc/systemd/system/ww4-disks.target.ww", "../rootfs/etc/systemd/system/ww4-disks.target.ww") output string
env.ImportFile("var/lib/warewulf/overlays/ignition/rootfs/etc/systemd/system/ww4-mounts.ww", "../rootfs/etc/systemd/system/ww4-mounts.ww") isJson bool
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
}{ }{
{ "ignition:ww4-disks.target": {
name: "ignition:ww4-disks.target", args: []string{"--quiet", "--render", "node1", "ignition", "etc/systemd/system/ww4-disks.target.ww"},
args: []string{"--render", "node1", "ignition", "etc/systemd/system/ww4-disks.target.ww"}, nodesConf: `
log: ignition_disks, nodes:
json: false, node1:
}, disks:
{ /dev/vda:
name: "ignition:ww4-mounts", wipe_table: true
args: []string{"--render", "node1", "ignition", "etc/systemd/system/ww4-mounts.ww"}, partitions:
log: ignition_mounts, scratch:
json: false, should_exist: true
}, swap:
{ number: "1"
name: "ignition:ignition.json", size_mib: "1024"
args: []string{"--quiet", "--render", "node1", "ignition", "warewulf/ignition.json.ww"}, filesystems:
log: ignition_json, /dev/disk/by-partlabel/scratch:
json: true, 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] [Unit]
Description=mount ww4 disks Description=mount ww4 disks
# make sure that the disks are available # make sure that the disks are available
@@ -80,14 +50,76 @@ Requisite=ignition-ww4-disks.service
# Get the mounts # Get the mounts
Wants=scratch.mount Wants=scratch.mount
Wants=dev-disk-by\x2dpartlabel-swap.swap 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 writeFile: true
Filename: scratch.mount Filename: scratch.mount
# This file is autogenerated by warewulf # This file is autogenerated by warewulf
[Unit] [Unit]
ConditionPathExists=/warewulf/ignition.json ConditionPathExists=/warewulf/ignition.json
Before=local-fs.target Before=local-fs.target
@@ -99,11 +131,11 @@ What=/dev/disk/by-partlabel/scratch
Type=btrfs Type=btrfs
[Install] [Install]
RequiredBy=local-fs.target RequiredBy=local-fs.target
backupFile: true backupFile: true
writeFile: true writeFile: true
Filename: dev-disk-by\x2dpartlabel-swap.swap Filename: dev-disk-by\x2dpartlabel-swap.swap
# This file is autogenerated by warewulf # This file is autogenerated by warewulf
[Unit] [Unit]
ConditionPathExists=/warewulf/ignition.json ConditionPathExists=/warewulf/ignition.json
Requires=ignition-ww4-disks.service Requires=ignition-ww4-disks.service
@@ -113,9 +145,91 @@ Before=swap.target
What=/dev/disk/by-partlabel/swap What=/dev/disk/by-partlabel/swap
[Install] [Install]
RequiredBy=swap.target 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": { "ignition": {
"version": "3.1.0" "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())
}
})
}
}

View File

@@ -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

View File

@@ -1,4 +1,5 @@
# This file is autogenerated by warewulf # This file is autogenerated by warewulf
[Unit] [Unit]
Description=mount ww4 disks Description=mount ww4 disks
# make sure that the disks are available # make sure that the disks are available
@@ -6,19 +7,20 @@ Requires=ignition-ww4-disks.service
After=ignition-ww4-disks.service After=ignition-ww4-disks.service
Requisite=ignition-ww4-disks.service Requisite=ignition-ww4-disks.service
# Get the mounts # Get the mounts
{{- range $fsdevice,$fs := .FileSystems }} {{- if .FileSystems }}
{{- $prefix := $fsdevice }} {{- range $fsdevice, $fs := .FileSystems }}
{{- $prefix = replace "-" "\\x2d" $fsdevice }} {{- if eq $fs.Format "swap" }}
{{- $prefix = replace "/" "-" $prefix }} Wants={{ $fsdevice | SystemdEscapePath }}.swap
{{- $prefix = substr 1 -1 $prefix }}{{ $suffix := "mount" }} {{- else if ne $fs.Path "" }}
{{- if eq $fs.Format "swap"}} Wants={{ $fs.Path | SystemdEscapePath }}.mount
{{- $prefix = replace "/dev/disk/by-partlabel/" "dev-disk-by\\x2dpartlabel-" $fsdevice }}{{ $suffix = "swap"}} {{- end }}
{{- else }} {{- end }}
{{- if ne $fs.Path "" }} {{- else if .Resources.ignition.storage.filesystems }}
{{- $prefix = replace "-" "\\x2d" $fs.Path }} {{- range $fs := .Resources.ignition.storage.filesystems }}
{{- $prefix = replace "/" "-" $prefix }} {{- if eq $fs.format "swap" }}
{{- $prefix = substr 1 -1 $prefix }} Wants={{ $fs.device | SystemdEscapePath }}.swap
{{- else if ne $fs.path "" }}
Wants={{ $fs.path | SystemdEscapePath }}.mount
{{- end }} {{- end }}
{{- end }} {{- end }}
Wants={{ print $prefix "." $suffix }}
{{- end }} {{- end }}

View File

@@ -1,13 +1,9 @@
{{- range $fsdevice,$fs := .FileSystems }} {{- if .FileSystems }}
{{- $prefix := replace "-" "\\x2d" $fs.Path }} {{- range $fsdevice, $fs := .FileSystems }}
{{- $prefix = replace "/" "-" $prefix }} {{- if eq $fs.Format "swap" }}
{{- $prefix = substr 1 -1 $prefix }} {{ file (print ($fsdevice | SystemdEscapePath) ".swap") }}
{{ $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) }}
# This file is autogenerated by warewulf # This file is autogenerated by warewulf
{{- if eq $fs.Format "swap"}}
[Unit] [Unit]
ConditionPathExists=/warewulf/ignition.json ConditionPathExists=/warewulf/ignition.json
Requires=ignition-ww4-disks.service Requires=ignition-ww4-disks.service
@@ -17,7 +13,10 @@ Before=swap.target
What={{ $fsdevice }} What={{ $fsdevice }}
[Install] [Install]
RequiredBy=swap.target RequiredBy=swap.target
{{- else }} {{- else if ne $fs.Path "" }}
{{ file (print ($fs.Path | SystemdEscapePath) ".mount") }}
# This file is autogenerated by warewulf
[Unit] [Unit]
ConditionPathExists=/warewulf/ignition.json ConditionPathExists=/warewulf/ignition.json
Before=local-fs.target Before=local-fs.target
@@ -28,9 +27,49 @@ Where={{ $fs.Path }}
What={{ $fsdevice }} What={{ $fsdevice }}
Type={{ $fs.Format }} Type={{ $fs.Format }}
{{- if $fs.MountOptions }} {{- if $fs.MountOptions }}
Options={{range $index,$opt := $fs.MountOptions }}{{if $index }},{{ end }}{{ $opt }}{{ end }} Options={{ $fs.MountOptions | join "." }}
{{- end }} {{- end }}
[Install] [Install]
RequiredBy=local-fs.target 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 }}
{{- end }} {{- end }}

View File

@@ -1 +1 @@
{{ IgnitionJson }} {{ coalesce IgnitionJson (.Resources.ignition | toJson) }}

View File

@@ -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

View File

@@ -1 +0,0 @@
{{ IgnitionJson }}

View File

@@ -1,53 +1,65 @@
#!/bin/sh #!/bin/sh
echo "Warewulf pre-init (/init)" if ! command -v info >/dev/null; then
echo info() {
echo "Mounting kernel file systems..." 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 /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 /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") mountpoint -q /sys || (mkdir -p /sys && mount -t sysfs sysfs /sys && echo "Mounted /sys")
# https://systemd.io/INITRD_INTERFACE/ # 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") 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 if [ -f "/warewulf/config" ]; then
. /warewulf/config . /warewulf/config
else else
echo "ERROR: Warewulf configuration file not found." die "warewulf: ERROR: /warewulf/config not found"
echo "Rebooting in 1 minute..."
sleep 60
echo b > /proc/sysrq-trigger || /sbin/reboot -f
fi fi
echo
echo "Configuring root file system..."
WWPRESCRIPTS=/warewulf/wwprescripts if [ -x /warewulf/run-wwinit.d ]; then
chmod +rx "${WWPRESCRIPTS}" /warewulf/run-wwinit.d
fi
NEXT_INIT=/warewulf/run-init
chmod +rx "${NEXT_INIT}"
info "warewulf: configuring root file system..."
WWROOT="${WWROOT:-initramfs}" WWROOT="${WWROOT:-initramfs}"
if [ "${WWROOT}" = "initramfs" ]; then if [ "${WWROOT}" = "initramfs" ]; then
echo "WWROOT=${WWROOT}: using initial rootfs and invoking ${WWPRESCRIPTS}..." info "warewulf: using initial rootfs and invoking ${NEXT_INIT}..."
exec "${WWPRESCRIPTS}" exec "${NEXT_INIT}"
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}"
else else
echo "ERROR: Unrecognized rootfs type requested: ${WWROOT}" mkdir /newroot
echo "Rebooting in 1 minute..." info "warewulf: mounting ${WWROOT} at /newroot"
sleep 60 if [ "${WWROOT}" = "ramfs" -o "${WWROOT}" = "tmpfs" ]; then
echo b > /proc/sysrq-trigger || /sbin/reboot -f 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 fi
echo die "warewulf: ERROR: wwinit encountered a problem."
echo "ERROR: wwinit encountered a problem."
echo "Rebooting in 1 minute..."
sleep 60
echo b > /proc/sysrq-trigger || /sbin/reboot -f

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
run-init

View File

@@ -192,8 +192,15 @@ In particular:
- Update PAM configuration to allow missing shadow entries - Update PAM configuration to allow missing shadow entries
- Relabel the file system for SELinux - Relabel the file system for SELinux
Other overlays may place additional scripts in ``/warewulf/init.d/`` to affect Other overlays can place scripts in one of two locations for additional pre-init
node configuration in this pre-boot environment. 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 wwclient
-------- --------
@@ -309,6 +316,36 @@ ignition
-------- --------
The **ignition** overlay defines partitions and file systems on local disks. 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 <node|profile> 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 debug
----- -----

View File

@@ -148,7 +148,13 @@ symlink.
IgnitionJson 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 abort
----- -----
@@ -174,10 +180,9 @@ nobackup
UniqueField UniqueField
----------- -----------
UniqueField returns a filtered version of a multi-line input string. input is Returns a filtered version of a multi-line input string. input is expected to be
expected to be a field-separated format with one record per line (terminated by a field-separated format with one record per line (terminated by `\n`). Order of
`\n`). Order of lines is preserved, with the first matching line taking lines is preserved, with the first matching line taking precedence.
precedence.
For example, the following template snippet has been used in the ``syncuser`` overlay For example, the following template snippet has been used in the ``syncuser`` overlay
to generate a combined ``/etc/passwd``. to generate a combined ``/etc/passwd``.
@@ -191,6 +196,24 @@ to generate a combined ``/etc/passwd``.
| UniqueField ":" 0 | trim | UniqueField ":" 0 | trim
}} }}
SystemdEscape
-------------
Escapes a string for use in a systemd unit file.
Escape rules are documented at `systemd.unit. <https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#String%20Escaping%20for%20Inclusion%20in%20Unit%20Names>`_
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. <https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#String%20Escaping%20for%20Inclusion%20in%20Unit%20Names>`_
Examples Examples
======== ========

View File

@@ -159,7 +159,7 @@ For example, the ``imgextract`` command can be `explicitly enabled`_.
support for ZLIB and GZIP archive image formats. support for ZLIB and GZIP archive image formats.
Configuring Warewulf (>= v4.5.0) Configuring Warewulf (>= v4.5.0)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In Warewulf v4.5.0, Warewulf can be configured to use these files using the In Warewulf v4.5.0, Warewulf can be configured to use these files using the
``tftp.ipxe`` and ``paths.ipxesource`` configuration parameters in ``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:
Booting with dracut Two-stage boot: dracut
=================== ======================
Some systems, typically due to limitations in their BIOS or EFI firmware, are 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, 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 With the ``warewulf-dracut`` package installed in the image, you can then build
an initramfs inside the image. an initramfs inside the image.
EL installation
---------------
.. code-block:: shell .. 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 --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 wwctl image exec rockylinux-9 -- /usr/bin/dracut --force --no-hostonly --add wwinit --regenerate-all
# SUSE
SUSE installation 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
.. code-block:: shell
zypper -y install warewulf-dracut
dracut --force --no-hostonly --add wwinit --regenerate-all
.. note:: .. 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 argument `wwinit.tmpfs.size`. (This parameter is passed to the `size` option
during tmpfs mount. See ``tmpfs(5)`` for more details.) during tmpfs mount. See ``tmpfs(5)`` for more details.)
Persistent installation .. _provision to disk:
========================
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
=================
*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:: .. 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:: .. note::
If the boot mode is perisiten the configured partion labeled `rootfs` will Warewulf doesn't install a bootloader to the disk or add UEFI entries. Nodes
be mounted as `/`. With any other boot method mount point confiured (here `/roofs`) still request an image and configuration from the Warewulf server on every
will be used. 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