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

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

View File

@@ -1,49 +1,12 @@
#!/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}"
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
if [ "${stage}" = "runtime" ]; then
localport="--local-port 1-1023"
fi
(
@@ -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

View File

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

View File

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

View File

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

View File

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

2
go.mod
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
// 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 ""

View File

@@ -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)
@@ -514,6 +510,8 @@ func RenderTemplateFile(fileName string, data TemplateStruct) (
return ""
},
"UniqueField": UniqueField,
"SystemdEscape": unit.UnitNameEscape,
"SystemdEscapePath": unit.UnitNamePathEscape,
}
// Merge sprig.FuncMap with our FuncMap

View File

@@ -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
tests := map[string]struct {
args []string
log string
json bool
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())
}
})
}
}

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
[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 }}

View File

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

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
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."

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

View File

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

View File

@@ -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
-------------
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.
This functionality is a technology preview and should be used with care. Pay
specific attention to ``wipeFilesytem`` (ignition) and ``overwrite`` (mkfs)
settings.
.. 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