From db3a3fee0532610831235d7f29f5dac9e101f6c3 Mon Sep 17 00:00:00 2001 From: Jonathon Anderson Date: Mon, 30 Jun 2025 23:24:20 -0600 Subject: [PATCH] Additional overlays to support provision-to-disk without ignition - sfdisk - partitions the disk - mkfs - formats partitions - mkswap - formats partitions for swap - systemd.mount - explicit creation of systemd mount units - systemd.swap - explicit creation of systemd swap units Signed-off-by: Jonathon Anderson --- CHANGELOG.md | 4 +- internal/pkg/overlay/overlay.go | 40 ++- internal/pkg/overlay/overlay_test.go | 16 + overlays/mkfs/internal/mkfs_test.go | 216 ++++++++++++ .../rootfs/warewulf/wwinit.d/20-mkfs.sh.ww | 69 ++++ overlays/mkswap/internal/mkswap_test.go | 314 ++++++++++++++++++ .../rootfs/warewulf/wwinit.d/20-mkswap.sh.ww | 69 ++++ overlays/sfdisk/internal/sfdisk_test.go | 286 ++++++++++++++++ .../sfdisk/rootfs/warewulf/sfdisk/disks.ww | 52 +++ .../rootfs/warewulf/wwinit.d/10-sfdisk.sh.ww | 51 +++ .../internal/systemd_mount_test.go | 118 +++++++ .../rootfs/etc/systemd/system/disk.mount.ww | 24 ++ .../local-fs.target.wants/disk.mount.ww | 11 + .../internal/systemd_swap_test.go | 98 ++++++ .../rootfs/etc/systemd/system/disk.swap.ww | 19 ++ .../system/local-fs.target.wants/disk.swap.ww | 11 + userdocs/nodes/disks.rst | 3 +- userdocs/overlays/overlays.rst | 99 ++++++ userdocs/overlays/templates.rst | 13 + userdocs/server/bootloaders.rst | 46 +++ warewulf.spec.in | 5 + 21 files changed, 1547 insertions(+), 17 deletions(-) create mode 100644 overlays/mkfs/internal/mkfs_test.go create mode 100644 overlays/mkfs/rootfs/warewulf/wwinit.d/20-mkfs.sh.ww create mode 100644 overlays/mkswap/internal/mkswap_test.go create mode 100644 overlays/mkswap/rootfs/warewulf/wwinit.d/20-mkswap.sh.ww create mode 100644 overlays/sfdisk/internal/sfdisk_test.go create mode 100644 overlays/sfdisk/rootfs/warewulf/sfdisk/disks.ww create mode 100644 overlays/sfdisk/rootfs/warewulf/wwinit.d/10-sfdisk.sh.ww create mode 100644 overlays/systemd.mount/internal/systemd_mount_test.go create mode 100644 overlays/systemd.mount/rootfs/etc/systemd/system/disk.mount.ww create mode 100644 overlays/systemd.mount/rootfs/etc/systemd/system/local-fs.target.wants/disk.mount.ww create mode 100644 overlays/systemd.swap/internal/systemd_swap_test.go create mode 100644 overlays/systemd.swap/rootfs/etc/systemd/system/disk.swap.ww create mode 100644 overlays/systemd.swap/rootfs/etc/systemd/system/local-fs.target.wants/disk.swap.ww diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d98da7a..6a68939c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added userdocs for `wwctl node import` from yaml/csv. - Added uid, gid, and permissions to OverlayFile in REST API. #1925 - (preview) Support provisioning to local storage with `wwctl set --root=/path/to/disk`. #1894 -- (preview) Support disk provisioning during dracut boot stage with Ignition. #1894 +- (preview) Support disk provisioning during dracut boot stage with Ignition, sfdisk, mkfs, and mkswap. #1894 +- Support creating multiple symlinks from a single overlay template. #1894 +- Added systemd.mount and systemd.swap overlays. #1894 - Support configuring Ignition with resources. #1894 - Added `wwctl set --parttype`. #1894 diff --git a/internal/pkg/overlay/overlay.go b/internal/pkg/overlay/overlay.go index ea75bae9..90dac978 100644 --- a/internal/pkg/overlay/overlay.go +++ b/internal/pkg/overlay/overlay.go @@ -368,17 +368,24 @@ func BuildOverlayIndir(nodeData node.Node, allNodes []node.Node, overlayNames [] // search for magic file name comment fileScanner := bufio.NewScanner(bytes.NewReader(buffer.Bytes())) fileScanner.Split(ScanLines) - foundFileComment := false + writingToNamedFile := false + isLink := false for fileScanner.Scan() { line := fileScanner.Text() filenameFromTemplate := regFile.FindAllStringSubmatch(line, -1) - softlinkFromTemplate := regLink.FindAllStringSubmatch(line, -1) - if len(softlinkFromTemplate) != 0 { - wwlog.Debug("Creating soft link %s -> %s", outputPath, softlinkFromTemplate[0][1]) - return os.Symlink(softlinkFromTemplate[0][1], outputPath) + targetFromTemplate := regLink.FindAllStringSubmatch(line, -1) + if len(targetFromTemplate) != 0 { + target := targetFromTemplate[0][1] + wwlog.Debug("Creating soft link %s -> %s", outputPath, target) + err := os.Symlink(target, outputPath) + if err != nil { + return fmt.Errorf("could not create symlink from template: %w", err) + } else { + isLink = true + } } else if len(filenameFromTemplate) != 0 { wwlog.Debug("Writing file %s", filenameFromTemplate[0][1]) - if foundFileComment { + if writingToNamedFile && !isLink { err = CarefulWriteBuffer(outputPath, fileBuffer, backupFile, info.Mode()) if err != nil { return fmt.Errorf("could not write file from template: %w", err) @@ -390,22 +397,25 @@ func BuildOverlayIndir(nodeData node.Node, allNodes []node.Node, overlayNames [] fileBuffer.Reset() } outputPath = path.Join(path.Dir(originalOutputPath), filenameFromTemplate[0][1]) - foundFileComment = true + writingToNamedFile = true + isLink = false } else { if _, err = fileBuffer.WriteString(line); err != nil { return fmt.Errorf("could not write to template buffer: %w", err) } } } - err = CarefulWriteBuffer(outputPath, fileBuffer, backupFile, info.Mode()) - if err != nil { - return fmt.Errorf("could not write file from template: %w", err) + if !isLink { + err = CarefulWriteBuffer(outputPath, fileBuffer, backupFile, info.Mode()) + if err != nil { + return fmt.Errorf("could not write file from template: %w", err) + } + err = util.CopyUIDGID(walkPath, outputPath) + if err != nil { + return fmt.Errorf("failed setting permissions on template output file: %w", err) + } + wwlog.Debug("Wrote template file into overlay: %s", outputPath) } - err = util.CopyUIDGID(walkPath, outputPath) - if err != nil { - return fmt.Errorf("failed setting permissions on template output file: %w", err) - } - wwlog.Debug("Wrote template file into overlay: %s", outputPath) } else if info.Mode()&os.ModeSymlink == os.ModeSymlink { wwlog.Debug("Found symlink %s", walkPath) diff --git a/internal/pkg/overlay/overlay_test.go b/internal/pkg/overlay/overlay_test.go index b3c010b8..6f8d44d8 100644 --- a/internal/pkg/overlay/overlay_test.go +++ b/internal/pkg/overlay/overlay_test.go @@ -253,6 +253,22 @@ T3 "test-link": "/test-target", }, }, + "multiple symlinks from template": { + overlays: []string{"o1"}, + overlayFiles: map[string]string{ + "/var/lib/warewulf/overlays/o1/rootfs/test-link.ww": ` +{{ file "test-link1"}} +{{ softlink "/test-target1" }} +{{ file "test-link2"}} +{{ softlink "/test-target2" }} +`, + }, + outputDir: "/image", + outputSymlinks: map[string]string{ + "test-link1": "/test-target1", + "test-link2": "/test-target2", + }, + }, "expansion of nodes": { overlays: []string{"o1"}, overlayFiles: map[string]string{ diff --git a/overlays/mkfs/internal/mkfs_test.go b/overlays/mkfs/internal/mkfs_test.go new file mode 100644 index 00000000..e1edae87 --- /dev/null +++ b/overlays/mkfs/internal/mkfs_test.go @@ -0,0 +1,216 @@ +package mkfs + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/app/wwctl/overlay/show" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func Test_mkfsOverlay(t *testing.T) { + tests := map[string]struct { + args []string + nodesConf string + output string + }{ + "mkfs:20-mkfs.sh.ww (empty)": { + args: []string{"--quiet=true", "--render=node1", "mkfs", "warewulf/wwinit.d/20-mkfs.sh.ww"}, + nodesConf: ` +nodes: + node1: {}`, + output: `#!/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 + +already_formatted() { + if ! command -v wipefs >/dev/null ; then + info "warewulf: wipefs not found, cannot check if device is already formatted" + return 0 + fi + + if wipefs -n "${1}" &>/dev/null; then + info "warewulf: ${1} already formatted" + return 0 + fi + + return 1 +} + +if command -v mkfs >/dev/null ; then : +else + info "warewulf: mkfs not found" +fi +`, + }, + + "mkfs:20-mkfs.sh.ww (resource)": { + args: []string{"--quiet=true", "--render=node1", "mkfs", "warewulf/wwinit.d/20-mkfs.sh.ww"}, + nodesConf: ` +nodes: + node1: + resources: + mkfs: + - device: /dev/disk/by-partlabel/rootfs + type: ext4 + - device: /dev/disk/by-partlabel/scratch + type: ext4 + overwrite: true`, + output: `#!/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 + +already_formatted() { + if ! command -v wipefs >/dev/null ; then + info "warewulf: wipefs not found, cannot check if device is already formatted" + return 0 + fi + + if wipefs -n "${1}" &>/dev/null; then + info "warewulf: ${1} already formatted" + return 0 + fi + + return 1 +} + +if command -v mkfs >/dev/null ; then : + if false || ! already_formatted /dev/disk/by-partlabel/rootfs; then + info "warewulf: mkfs: formatting /dev/disk/by-partlabel/rootfs" + mkfs --type=ext4 /dev/disk/by-partlabel/rootfs || die "warewulf: mkfs: failed to format /dev/disk/by-partlabel/rootfs" + else + info "warewulf: mkfs: skipping /dev/disk/by-partlabel/rootfs" + continue + fi + if true || ! already_formatted /dev/disk/by-partlabel/scratch; then + info "warewulf: mkfs: formatting /dev/disk/by-partlabel/scratch" + mkfs --type=ext4 /dev/disk/by-partlabel/scratch || die "warewulf: mkfs: failed to format /dev/disk/by-partlabel/scratch" + else + info "warewulf: mkfs: skipping /dev/disk/by-partlabel/scratch" + continue + fi +else + info "warewulf: mkfs not found" +fi +`, + }, + + "mkfs:20-mkfs.sh.ww (native)": { + args: []string{"--quiet=true", "--render=node1", "mkfs", "warewulf/wwinit.d/20-mkfs.sh.ww"}, + nodesConf: ` +nodes: + node1: + filesystems: + /dev/disk/by-partlabel/rootfs: + format: ext4 + path: / + /dev/disk/by-partlabel/scratch: + format: ext4 + path: /scratch + wipe_filesystem: true + /dev/disk/by-partlabel/swap: + format: swap + path: swap`, + output: `#!/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 + +already_formatted() { + if ! command -v wipefs >/dev/null ; then + info "warewulf: wipefs not found, cannot check if device is already formatted" + return 0 + fi + + if wipefs -n "${1}" &>/dev/null; then + info "warewulf: ${1} already formatted" + return 0 + fi + + return 1 +} + +if command -v mkfs >/dev/null ; then : + if false || ! already_formatted /dev/disk/by-partlabel/rootfs; then + info "warewulf: mkfs: formatting /dev/disk/by-partlabel/rootfs" + mkfs --type=ext4 /dev/disk/by-partlabel/rootfs || die "warewulf: mkfs: failed to format /dev/disk/by-partlabel/rootfs" + else + info "warewulf: mkfs: skipping /dev/disk/by-partlabel/rootfs" + continue + fi + if true || ! already_formatted /dev/disk/by-partlabel/scratch; then + info "warewulf: mkfs: formatting /dev/disk/by-partlabel/scratch" + mkfs --type=ext4 /dev/disk/by-partlabel/scratch || die "warewulf: mkfs: failed to format /dev/disk/by-partlabel/scratch" + else + info "warewulf: mkfs: skipping /dev/disk/by-partlabel/scratch" + continue + fi +else + info "warewulf: mkfs not found" +fi +`, + }, + } + + 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/mkfs/rootfs/warewulf/wwinit.d/20-mkfs.sh.ww", "../rootfs/warewulf/wwinit.d/20-mkfs.sh.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()) + assert.Equal(t, tt.output, logbuf.String()) + }) + } +} diff --git a/overlays/mkfs/rootfs/warewulf/wwinit.d/20-mkfs.sh.ww b/overlays/mkfs/rootfs/warewulf/wwinit.d/20-mkfs.sh.ww new file mode 100644 index 00000000..4a4777b4 --- /dev/null +++ b/overlays/mkfs/rootfs/warewulf/wwinit.d/20-mkfs.sh.ww @@ -0,0 +1,69 @@ +#!/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 + +already_formatted() { + if ! command -v wipefs >/dev/null ; then + info "warewulf: wipefs not found, cannot check if device is already formatted" + return 0 + fi + + if wipefs -n "${1}" &>/dev/null; then + info "warewulf: ${1} already formatted" + return 0 + fi + + return 1 +} + +if command -v mkfs >/dev/null ; then : +{{- $filesystems := list }} +{{- if .FileSystems }} +{{- range $fs := .ThisNode.FileSystemList }} +{{- if ne $fs.Format "swap" }} +{{- $fsDict := dict "device" $fs.Id "overwrite" $fs.WipeFileSystem }} +{{- if $fs.Format }} +{{- $_ := set $fsDict "type" $fs.Format }} +{{- end }} +{{- if $fs.Options }} +{{- $_ := set $fsDict "options" ($fs.Options | join " ") }} +{{- end }} +{{- if $fs.Label }} +{{- $_ := set $fsDict "label" $fs.Label }} +{{- end }} +{{- if $fs.Uuid }} +{{- $_ := set $fsDict "uuid" $fs.Uuid }} +{{- end }} +{{- $filesystems = append $filesystems $fsDict }} +{{- end }} +{{- end }} +{{- else }} +{{- $filesystems = index .Resources "mkfs" }} +{{- end }} +{{- range $i, $fs := $filesystems }} +{{- if and $fs.type $fs.device }} + if {{if $fs.overwrite}}true{{else}}false{{end}} || ! already_formatted {{ $fs.device }}; then + info "warewulf: mkfs: formatting {{ $fs.device }}" + mkfs {{ not (empty $fs.type) | ternary (print "--type=" $fs.type) "" }} {{ not (empty $fs.label) | ternary (print "-L " $fs.label) "" }} {{ not (empty $fs.uuid) | ternary (print "-U " $fs.uuid) "" }} {{ default "" $fs.options }} {{ $fs.device }} {{ default "" $fs.size }} || die "warewulf: mkfs: failed to format {{ $fs.device }}" + else + info "warewulf: mkfs: skipping {{ $fs.device }}" + continue + fi +{{- end }} +{{- end }} +else + info "warewulf: mkfs not found" +fi diff --git a/overlays/mkswap/internal/mkswap_test.go b/overlays/mkswap/internal/mkswap_test.go new file mode 100644 index 00000000..f84351da --- /dev/null +++ b/overlays/mkswap/internal/mkswap_test.go @@ -0,0 +1,314 @@ +package mkswap + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/app/wwctl/overlay/show" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func Test_mkswapOverlay(t *testing.T) { + tests := map[string]struct { + args []string + nodesConf string + output string + }{ + "mkswap:20-mkswap.sh.ww (empty)": { + args: []string{"--quiet=true", "--render=node1", "mkswap", "warewulf/wwinit.d/20-mkswap.sh.ww"}, + nodesConf: ` +nodes: + node1: {}`, + output: `#!/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 + +already_formatted() { + if ! command -v wipefs >/dev/null ; then + info "warewulf: wipefs not found, cannot check if device is already formatted" + return 0 + fi + + if wipefs -n "${1}" &>/dev/null; then + info "warewulf: ${1} already formatted" + return 0 + fi + + return 1 +} + +if command -v mkswap >/dev/null; then : +else + info "warewulf: mkswap not found" +fi +`, + }, + + "mkswap:20-mkswap.sh.ww (resource)": { + args: []string{"--quiet=true", "--render=node1", "mkswap", "warewulf/wwinit.d/20-mkswap.sh.ww"}, + nodesConf: ` +nodes: + node1: + resources: + mkswap: + - device: /dev/disk/by-partlabel/swap`, + output: `#!/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 + +already_formatted() { + if ! command -v wipefs >/dev/null ; then + info "warewulf: wipefs not found, cannot check if device is already formatted" + return 0 + fi + + if wipefs -n "${1}" &>/dev/null; then + info "warewulf: ${1} already formatted" + return 0 + fi + + return 1 +} + +if command -v mkswap >/dev/null; then : + if false || ! already_formatted /dev/disk/by-partlabel/swap; then + info "warewulf: mkswap: formatting /dev/disk/by-partlabel/swap" + mkswap /dev/disk/by-partlabel/swap || die "warewulf: mkswap: failed to format /dev/disk/by-partlabel/swap" + else + info "warewulf: mkswap: skipping /dev/disk/by-partlabel/swap" + continue + fi +else + info "warewulf: mkswap not found" +fi +`, + }, + + "mkswap:20-mkswap.sh.ww (resource overwrite)": { + args: []string{"--quiet=true", "--render=node1", "mkswap", "warewulf/wwinit.d/20-mkswap.sh.ww"}, + nodesConf: ` +nodes: + node1: + resources: + mkswap: + - device: /dev/disk/by-partlabel/swap + overwrite: true`, + output: `#!/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 + +already_formatted() { + if ! command -v wipefs >/dev/null ; then + info "warewulf: wipefs not found, cannot check if device is already formatted" + return 0 + fi + + if wipefs -n "${1}" &>/dev/null; then + info "warewulf: ${1} already formatted" + return 0 + fi + + return 1 +} + +if command -v mkswap >/dev/null; then : + if true || ! already_formatted /dev/disk/by-partlabel/swap; then + info "warewulf: mkswap: formatting /dev/disk/by-partlabel/swap" + mkswap /dev/disk/by-partlabel/swap || die "warewulf: mkswap: failed to format /dev/disk/by-partlabel/swap" + else + info "warewulf: mkswap: skipping /dev/disk/by-partlabel/swap" + continue + fi +else + info "warewulf: mkswap not found" +fi +`, + }, + + "mkswap:20-mkswap.sh.ww (native)": { + args: []string{"--quiet=true", "--render=node1", "mkswap", "warewulf/wwinit.d/20-mkswap.sh.ww"}, + nodesConf: ` +nodes: + node1: + filesystems: + /dev/disk/by-partlabel/rootfs: + format: ext4 + path: / + /dev/disk/by-partlabel/scratch: + format: ext4 + path: /scratch + wipe_filesystem: true + /dev/disk/by-partlabel/swap: + format: swap + path: swap`, + output: `#!/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 + +already_formatted() { + if ! command -v wipefs >/dev/null ; then + info "warewulf: wipefs not found, cannot check if device is already formatted" + return 0 + fi + + if wipefs -n "${1}" &>/dev/null; then + info "warewulf: ${1} already formatted" + return 0 + fi + + return 1 +} + +if command -v mkswap >/dev/null; then : + if false || ! already_formatted /dev/disk/by-partlabel/swap; then + info "warewulf: mkswap: formatting /dev/disk/by-partlabel/swap" + mkswap /dev/disk/by-partlabel/swap || die "warewulf: mkswap: failed to format /dev/disk/by-partlabel/swap" + else + info "warewulf: mkswap: skipping /dev/disk/by-partlabel/swap" + continue + fi +else + info "warewulf: mkswap not found" +fi +`, + }, + + "mkswap:20-mkswap.sh.ww (native overwrite)": { + args: []string{"--quiet=true", "--render=node1", "mkswap", "warewulf/wwinit.d/20-mkswap.sh.ww"}, + nodesConf: ` +nodes: + node1: + filesystems: + /dev/disk/by-partlabel/rootfs: + format: ext4 + path: / + /dev/disk/by-partlabel/scratch: + format: ext4 + path: /scratch + wipe_filesystem: true + /dev/disk/by-partlabel/swap: + format: swap + path: swap + wipe_filesystem: true`, + output: `#!/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 + +already_formatted() { + if ! command -v wipefs >/dev/null ; then + info "warewulf: wipefs not found, cannot check if device is already formatted" + return 0 + fi + + if wipefs -n "${1}" &>/dev/null; then + info "warewulf: ${1} already formatted" + return 0 + fi + + return 1 +} + +if command -v mkswap >/dev/null; then : + if true || ! already_formatted /dev/disk/by-partlabel/swap; then + info "warewulf: mkswap: formatting /dev/disk/by-partlabel/swap" + mkswap /dev/disk/by-partlabel/swap || die "warewulf: mkswap: failed to format /dev/disk/by-partlabel/swap" + else + info "warewulf: mkswap: skipping /dev/disk/by-partlabel/swap" + continue + fi +else + info "warewulf: mkswap not found" +fi +`, + }, + } + + 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/mkswap/rootfs/warewulf/wwinit.d/20-mkswap.sh.ww", "../rootfs/warewulf/wwinit.d/20-mkswap.sh.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()) + assert.Equal(t, tt.output, logbuf.String()) + }) + } +} diff --git a/overlays/mkswap/rootfs/warewulf/wwinit.d/20-mkswap.sh.ww b/overlays/mkswap/rootfs/warewulf/wwinit.d/20-mkswap.sh.ww new file mode 100644 index 00000000..3996910d --- /dev/null +++ b/overlays/mkswap/rootfs/warewulf/wwinit.d/20-mkswap.sh.ww @@ -0,0 +1,69 @@ +#!/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 + +already_formatted() { + if ! command -v wipefs >/dev/null ; then + info "warewulf: wipefs not found, cannot check if device is already formatted" + return 0 + fi + + if wipefs -n "${1}" &>/dev/null; then + info "warewulf: ${1} already formatted" + return 0 + fi + + return 1 +} + +if command -v mkswap >/dev/null; then : +{{- $filesystems := list }} +{{- if .FileSystems }} +{{- range $fs := .ThisNode.FileSystemList }} +{{- if eq $fs.Format "swap" }} +{{- $fsDict := dict "device" $fs.Id "overwrite" $fs.WipeFileSystem }} +{{- if $fs.Format }} +{{- $_ := set $fsDict "type" $fs.Format }} +{{- end }} +{{- if $fs.Options }} +{{- $_ := set $fsDict "options" ($fs.Options | join " ") }} +{{- end }} +{{- if $fs.Label }} +{{- $_ := set $fsDict "label" $fs.Label }} +{{- end }} +{{- if $fs.Uuid }} +{{- $_ := set $fsDict "uuid" $fs.Uuid }} +{{- end }} +{{- $filesystems = append $filesystems $fsDict }} +{{- end }} +{{- end }} +{{- else }} +{{- $filesystems = index .Resources "mkswap" }} +{{- end }} +{{- range $fs := $filesystems }} +{{- if $fs.device }} + if {{if $fs.overwrite}}true{{else}}false{{end}} || ! already_formatted {{ $fs.device }}; then + info "warewulf: mkswap: formatting {{ $fs.device }}" + mkswap {{ not (empty $fs.label) | ternary (print "--label=" $fs.label) "" }} {{ not (empty $fs.uuid) | ternary (print "--uuid=" $fs.uuid) "" }} {{ $fs.device }} {{ default "" $fs.size }} || die "warewulf: mkswap: failed to format {{ $fs.device }}" + else + info "warewulf: mkswap: skipping {{ $fs.device }}" + continue + fi +{{- end }} +{{- end }} +else + info "warewulf: mkswap not found" +fi diff --git a/overlays/sfdisk/internal/sfdisk_test.go b/overlays/sfdisk/internal/sfdisk_test.go new file mode 100644 index 00000000..c8fbdc3e --- /dev/null +++ b/overlays/sfdisk/internal/sfdisk_test.go @@ -0,0 +1,286 @@ +package sfdisk + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/app/wwctl/overlay/show" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func Test_sfdiskOverlay(t *testing.T) { + tests := map[string]struct { + args []string + nodesConf string + output string + }{ + "sfdisk:disks.ww (empty)": { + args: []string{"--quiet=false", "--render=node1", "sfdisk", "warewulf/sfdisk/disks.ww"}, + nodesConf: ` +nodes: + node1: {}`, + output: `backupFile: true +writeFile: true +Filename: warewulf/sfdisk/disks + +`, + }, + "sfdisk:disks.ww (resource)": { + args: []string{"--quiet=false", "--render=node1", "sfdisk", "warewulf/sfdisk/disks.ww"}, + nodesConf: ` +nodes: + node1: + resources: + sfdisk: + - device: /dev/sda + first-lba: 34 + label: gpt + last-lba: 20971486 + partitions: + - device: /dev/sda1 + name: sfdisk-rootfs + size: 4194304 + start: 2048 + type: linux + - device: /dev/sda2 + name: sfdisk-scratch + size: 1048576 + start: 4196352 + type: linux + - device: /dev/sda3 + name: sfdisk-swap + size: 2097152 + start: 5244928 + type: linux + sector-size: 512`, + output: `backupFile: true +writeFile: true +Filename: device-0 + +label: gpt +first-lba: 34 +last-lba: 20971486 +sector-size: 512 +/dev/sda1 : start=2048 size=4194304 name=sfdisk-rootfs type=linux +/dev/sda2 : start=4196352 size=1048576 name=sfdisk-scratch type=linux +/dev/sda3 : start=5244928 size=2097152 name=sfdisk-swap type=linux +`, + }, + "sfdisk:disks.ww (native)": { + args: []string{"--quiet=false", "--render=node1", "sfdisk", "warewulf/sfdisk/disks.ww"}, + nodesConf: ` +nodes: + node1: + disks: + /dev/sda: + wipe_table: true + partitions: + rootfs: + number: "1" + start_mib: "2" + size_mib: "4096" + should_exist: true + type_guid: linux + scratch: + number: "2" + start_mib: "4098" + size_mib: "10240" + should_exist: true + type_guid: linux + swap: + number: "3" + start_mib: "5122" + size_mib: "2048" + should_exist: true + type_guid: swap`, + output: `backupFile: true +writeFile: true +Filename: device-0 + +start=2MiB size=4096MiB name=rootfs type=linux +start=4098MiB size=10240MiB name=scratch type=linux +start=5122MiB size=2048MiB name=swap type=swap +`, + }, + "sfdisk:10-sfdisk.sh.ww (empty)": { + args: []string{"--quiet", "--render=node1", "sfdisk", "warewulf/wwinit.d/10-sfdisk.sh.ww"}, + nodesConf: ` +nodes: + node1: {}`, + output: `#!/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 sfdisk >/dev/null ; then + info "warewulf: sfdisk not found, skipping partitioning" +else : +fi +`, + }, + "sfdisk:10-sfdisk.sh.ww (resource)": { + args: []string{"--quiet", "--render=node1", "sfdisk", "warewulf/wwinit.d/10-sfdisk.sh.ww"}, + nodesConf: ` +nodes: + node1: + resources: + sfdisk: + - device: /dev/sda + first-lba: 34 + label: gpt + last-lba: 20971486 + partitions: + - device: /dev/sda1 + name: sfdisk-rootfs + size: 4194304 + start: 2048 + type: linux + - device: /dev/sda2 + name: sfdisk-scratch + size: 1048576 + start: 4196352 + type: linux + - device: /dev/sda3 + name: sfdisk-swap + size: 2097152 + start: 5244928 + type: linux + sector-size: 512`, + output: `#!/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 sfdisk >/dev/null ; then + info "warewulf: sfdisk not found, skipping partitioning" +else : + info "warewulf: sfdisk: partitioning /dev/sda" + sfdisk --wipe "auto" "/dev/sda" < "${PREFIX}/warewulf/sfdisk/device-0" || die "warewulf: sfdisk: failed to partition /dev/sda" + + if command -v blockdev >/dev/null ; then + info "warewulf: blockdev: re-reading partition table" + blockdev --rereadpt /dev/sda + fi + if command -v udevadm >/dev/null ; then + info "warewulf: udevadm: triggering udev events for block devices" + udevadm trigger --subsystem-match=block --action=add + udevadm settle + fi +fi +`, + }, + "sfdisk:10-sfdisk.sh.ww (native)": { + args: []string{"--quiet", "--render=node1", "sfdisk", "warewulf/wwinit.d/10-sfdisk.sh.ww"}, + nodesConf: ` +nodes: + node1: + disks: + /dev/sda: + partitions: + rootfs: + number: "1" + size_mib: "4096" + should_exist: true + scratch: + number: "2" + size_mib: "10240" + should_exist: true + swap: + number: "3" + size_mib: "2048" + should_exist: true + /dev/sdb: + wipe_table: true`, + output: `#!/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 sfdisk >/dev/null ; then + info "warewulf: sfdisk not found, skipping partitioning" +else : + info "warewulf: sfdisk: partitioning /dev/sda" + sfdisk --wipe "auto" "/dev/sda" < "${PREFIX}/warewulf/sfdisk/device-0" || die "warewulf: sfdisk: failed to partition /dev/sda" + + if command -v blockdev >/dev/null ; then + info "warewulf: blockdev: re-reading partition table" + blockdev --rereadpt /dev/sda + fi + info "warewulf: sfdisk: partitioning /dev/sdb" + sfdisk --wipe "always" "/dev/sdb" < "${PREFIX}/warewulf/sfdisk/device-1" || die "warewulf: sfdisk: failed to partition /dev/sdb" + + if command -v blockdev >/dev/null ; then + info "warewulf: blockdev: re-reading partition table" + blockdev --rereadpt /dev/sdb + fi + if command -v udevadm >/dev/null ; then + info "warewulf: udevadm: triggering udev events for block devices" + udevadm trigger --subsystem-match=block --action=add + udevadm settle + fi +fi +`, + }, + } + + 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/sfdisk/rootfs/warewulf/sfdisk/disks.ww", "../rootfs/warewulf/sfdisk/disks.ww") + env.ImportFile("var/lib/warewulf/overlays/sfdisk/rootfs/warewulf/wwinit.d/10-sfdisk.sh.ww", "../rootfs/warewulf/wwinit.d/10-sfdisk.sh.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()) + assert.Equal(t, tt.output, logbuf.String()) + }) + } +} diff --git a/overlays/sfdisk/rootfs/warewulf/sfdisk/disks.ww b/overlays/sfdisk/rootfs/warewulf/sfdisk/disks.ww new file mode 100644 index 00000000..759f54e8 --- /dev/null +++ b/overlays/sfdisk/rootfs/warewulf/sfdisk/disks.ww @@ -0,0 +1,52 @@ +{{- $disks := list }} +{{- if .Disks }} +{{- $diskNames := list }} +{{- range $disk := .ThisNode.DiskList }} +{{- $partitions := list }} +{{- range $partition := $disk.PartitionList }} +{{- if $partition.ShouldExist }} +{{- $partitionDict := dict "name" $partition.Id }} +{{- if $partition.StartMiB }} +{{- $_ := set $partitionDict "start" (printf "%s%s" $partition.StartMiB "MiB") }} +{{- end }} +{{- if $partition.SizeMiB }} +{{- $_ := set $partitionDict "size" (printf "%s%s" $partition.SizeMiB "MiB") }} +{{- end }} +{{- if $partition.TypeGuid }} +{{- $_ := set $partitionDict "type" $partition.TypeGuid }} +{{- end }} +{{- if $partition.Guid }} +{{- $_ := set $partitionDict "uuid" $partition.Guid }} +{{- end }} +{{- $partitions = append $partitions $partitionDict }} +{{- end }} +{{- end }} +{{- $disks = append $disks (dict "partitions" $partitions) }} +{{- end }} +{{- else }} +{{- $disks = index .Resources "sfdisk" }} +{{- end }} +{{- range $i, $device := $disks }} +{{ file (print "device-" $i) }} +{{- range $header := list "unit" "label" "label-id" "first-lba" "last-lba" "table-length" "grain" "sector-size" }} +{{- if index $device $header }} +{{ $header }}: {{ index $device $header }} +{{- end }} +{{- end }} +{{- range $partition := $device.partitions }} +{{- $fields := list }} +{{- range $field := list "start" "size" "attrs" "uuid" "name" "type" }} +{{- if index $partition $field }} +{{- $fields = append $fields (print $field "=" (index $partition $field)) }} +{{- end }} +{{- if $partition.bootable }} +{{- $fields = append $fields "bootable" }} +{{- end }} +{{- end }} +{{- if $partition.device }} +{{ $partition.device }} : {{ $fields | join " " }} +{{- else }} +{{ $fields | join " " }} +{{- end }} +{{- end }} +{{- end }} diff --git a/overlays/sfdisk/rootfs/warewulf/wwinit.d/10-sfdisk.sh.ww b/overlays/sfdisk/rootfs/warewulf/wwinit.d/10-sfdisk.sh.ww new file mode 100644 index 00000000..67d7d211 --- /dev/null +++ b/overlays/sfdisk/rootfs/warewulf/wwinit.d/10-sfdisk.sh.ww @@ -0,0 +1,51 @@ +#!/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 sfdisk >/dev/null ; then + info "warewulf: sfdisk not found, skipping partitioning" +else : +{{- $disks := list }} +{{- if .Disks }} +{{- range $disk := .ThisNode.DiskList }} +{{- $diskDict := dict "device" $disk.Id }} +{{- if $disk.WipeTable }} +{{- $_ := set $diskDict "wipe" "always" }} +{{- end }} +{{- $disks = append $disks $diskDict }} +{{- end }} +{{- else }} +{{- $disks = index .Resources "sfdisk" }} +{{- end }} +{{- range $i, $device := $disks }} +{{- if $device.device }} + info "warewulf: sfdisk: partitioning {{ $device.device }}" + sfdisk --wipe "{{ default "auto" $device.wipe }}" "{{ $device.device }}" < "${PREFIX}/warewulf/sfdisk/device-{{ $i }}" || die "warewulf: sfdisk: failed to partition {{ $device.device }}" + + if command -v blockdev >/dev/null ; then + info "warewulf: blockdev: re-reading partition table" + blockdev --rereadpt {{ $device.device }} + fi +{{- end }} +{{- end }} +{{- if $disks }} + if command -v udevadm >/dev/null ; then + info "warewulf: udevadm: triggering udev events for block devices" + udevadm trigger --subsystem-match=block --action=add + udevadm settle + fi +{{- end }} +fi diff --git a/overlays/systemd.mount/internal/systemd_mount_test.go b/overlays/systemd.mount/internal/systemd_mount_test.go new file mode 100644 index 00000000..a8e79226 --- /dev/null +++ b/overlays/systemd.mount/internal/systemd_mount_test.go @@ -0,0 +1,118 @@ +package systemd_mount + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/app/wwctl/overlay/show" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func Test_systemdMountOverlay(t *testing.T) { + tests := map[string]struct { + args []string + nodesConf string + output string + }{ + "systemd.mount:disk.mount.ww": { + args: []string{"--quiet=false", "--render=node1", "systemd.mount", "etc/systemd/system/disk.mount.ww"}, + nodesConf: ` +nodes: + node1: + filesystems: + /dev/disk/by-partlabel/rootfs: + format: ext4 + path: / + /dev/disk/by-partlabel/scratch: + format: ext4 + path: /scratch + wipe_filesystem: true + /dev/disk/by-partlabel/swap: + format: swap + path: swap`, + output: `backupFile: true +writeFile: true +Filename: -.mount + + +[Unit] +Before=local-fs.target + +[Mount] +Where=/ +What=/dev/disk/by-partlabel/rootfs +Type=ext4 + +[Install] +RequiredBy=local-fs.target +backupFile: true +writeFile: true +Filename: scratch.mount + +[Unit] +Before=local-fs.target + +[Mount] +Where=/scratch +What=/dev/disk/by-partlabel/scratch +Type=ext4 + +[Install] +RequiredBy=local-fs.target +`, + }, + + "systemd.mount:local-fs.target.wants/disk.mount.ww": { + args: []string{"--quiet=false", "--render=node1", "systemd.mount", "etc/systemd/system/local-fs.target.wants/disk.mount.ww"}, + nodesConf: ` +nodes: + node1: + filesystems: + /dev/disk/by-partlabel/rootfs: + format: ext4 + path: / + /dev/disk/by-partlabel/scratch: + format: ext4 + path: /scratch + wipe_filesystem: true + /dev/disk/by-partlabel/swap: + format: swap + path: swap`, + output: `backupFile: true +writeFile: true +Filename: -.mount + +{{ /* softlink "/etc/systemd/system/-.mount" */ }} +backupFile: true +writeFile: true +Filename: scratch.mount +{{ /* softlink "/etc/systemd/system/scratch.mount" */ }} +`, + }, + } + + 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/systemd.mount/rootfs/etc/systemd/system/disk.mount.ww", "../rootfs/etc/systemd/system/disk.mount.ww") + env.ImportFile("var/lib/warewulf/overlays/systemd.mount/rootfs/etc/systemd/system/local-fs.target.wants/disk.mount.ww", "../rootfs/etc/systemd/system/local-fs.target.wants/disk.mount.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()) + assert.Equal(t, tt.output, logbuf.String()) + }) + } +} diff --git a/overlays/systemd.mount/rootfs/etc/systemd/system/disk.mount.ww b/overlays/systemd.mount/rootfs/etc/systemd/system/disk.mount.ww new file mode 100644 index 00000000..61698f7f --- /dev/null +++ b/overlays/systemd.mount/rootfs/etc/systemd/system/disk.mount.ww @@ -0,0 +1,24 @@ +{{- $abort := true }} +{{- range $fs := .ThisNode.FileSystemList }} +{{- if and $fs.Path (ne $fs.Format "swap") }} +{{- $abort = false }} +{{ file (print ($fs.Path | SystemdEscapePath) ".mount") }} + +[Unit] +Before=local-fs.target + +[Mount] +Where={{ $fs.Path }} +What={{ $fs.Id }} +Type={{ $fs.Format }} +{{- if $fs.MountOptions }} +Options={{ $fs.MountOptions }} +{{- end }} + +[Install] +RequiredBy=local-fs.target +{{- end }} +{{- end }} +{{- if $abort }} +{{ abort }} +{{- end }} diff --git a/overlays/systemd.mount/rootfs/etc/systemd/system/local-fs.target.wants/disk.mount.ww b/overlays/systemd.mount/rootfs/etc/systemd/system/local-fs.target.wants/disk.mount.ww new file mode 100644 index 00000000..d80ae239 --- /dev/null +++ b/overlays/systemd.mount/rootfs/etc/systemd/system/local-fs.target.wants/disk.mount.ww @@ -0,0 +1,11 @@ +{{- $abort := true }} +{{- range $fs := .ThisNode.FileSystemList }} +{{- if and $fs.Path (ne $fs.Format "swap") }} +{{- $abort = false }} +{{ file (print ($fs.Path | SystemdEscapePath) ".mount") }} +{{ softlink (print "/etc/systemd/system/" ($fs.Path | SystemdEscapePath) ".mount") }} +{{- end }} +{{- end }} +{{- if $abort }} +{{ abort }} +{{- end }} diff --git a/overlays/systemd.swap/internal/systemd_swap_test.go b/overlays/systemd.swap/internal/systemd_swap_test.go new file mode 100644 index 00000000..2f361ec9 --- /dev/null +++ b/overlays/systemd.swap/internal/systemd_swap_test.go @@ -0,0 +1,98 @@ +package systemd_swap + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/app/wwctl/overlay/show" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func Test_systemdSwapOverlay(t *testing.T) { + tests := map[string]struct { + args []string + nodesConf string + output string + }{ + "systemd.swap:disk.swap.ww": { + args: []string{"--quiet=false", "--render=node1", "systemd.swap", "etc/systemd/system/disk.swap.ww"}, + nodesConf: ` +nodes: + node1: + filesystems: + /dev/disk/by-partlabel/rootfs: + format: ext4 + path: / + /dev/disk/by-partlabel/scratch: + format: ext4 + path: /scratch + wipe_filesystem: true + /dev/disk/by-partlabel/swap: + format: swap + path: swap`, + output: `backupFile: true +writeFile: true +Filename: dev-disk-by\x2dpartlabel-swap.swap + + +[Unit] +Before=swap.target + +[Swap] +What=/dev/disk/by-partlabel/swap + +[Install] +RequiredBy=swap.target +`, + }, + + "systemd.swap:local-fs.target.wants/disk.swap.ww": { + args: []string{"--quiet=false", "--render=node1", "systemd.swap", "etc/systemd/system/local-fs.target.wants/disk.swap.ww"}, + nodesConf: ` +nodes: + node1: + filesystems: + /dev/disk/by-partlabel/rootfs: + format: ext4 + path: / + /dev/disk/by-partlabel/scratch: + format: ext4 + path: /scratch + wipe_filesystem: true + /dev/disk/by-partlabel/swap: + format: swap + path: swap`, + output: `backupFile: true +writeFile: true +Filename: dev-disk-by\x2dpartlabel-swap.swap + +{{ /* softlink "/etc/systemd/system/dev-disk-by\x2dpartlabel-swap.swap" */ }} +`, + }, + } + + 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/systemd.swap/rootfs/etc/systemd/system/disk.swap.ww", "../rootfs/etc/systemd/system/disk.swap.ww") + env.ImportFile("var/lib/warewulf/overlays/systemd.swap/rootfs/etc/systemd/system/local-fs.target.wants/disk.swap.ww", "../rootfs/etc/systemd/system/local-fs.target.wants/disk.swap.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()) + assert.Equal(t, tt.output, logbuf.String()) + }) + } +} diff --git a/overlays/systemd.swap/rootfs/etc/systemd/system/disk.swap.ww b/overlays/systemd.swap/rootfs/etc/systemd/system/disk.swap.ww new file mode 100644 index 00000000..51c5495b --- /dev/null +++ b/overlays/systemd.swap/rootfs/etc/systemd/system/disk.swap.ww @@ -0,0 +1,19 @@ +{{- $abort := true }} +{{- range $fs := .ThisNode.FileSystemList }} +{{- if eq $fs.Format "swap" }} +{{- $abort = false }} +{{ file (print ($fs.Id | SystemdEscapePath) ".swap") }} + +[Unit] +Before=swap.target + +[Swap] +What={{ $fs.Id }} + +[Install] +RequiredBy=swap.target +{{- end }} +{{- end }} +{{- if $abort }} +{{ abort }} +{{- end }} diff --git a/overlays/systemd.swap/rootfs/etc/systemd/system/local-fs.target.wants/disk.swap.ww b/overlays/systemd.swap/rootfs/etc/systemd/system/local-fs.target.wants/disk.swap.ww new file mode 100644 index 00000000..12f60fe7 --- /dev/null +++ b/overlays/systemd.swap/rootfs/etc/systemd/system/local-fs.target.wants/disk.swap.ww @@ -0,0 +1,11 @@ +{{- $abort := true }} +{{- range $fs := .ThisNode.FileSystemList }} +{{- if eq $fs.Format "swap" }} +{{- $abort = false }} +{{ file (print ($fs.Id | SystemdEscapePath) ".swap") }} +{{ softlink (print "/etc/systemd/system/" ($fs.Id | SystemdEscapePath) ".swap") }} +{{- end }} +{{- end }} +{{- if $abort }} +{{ abort }} +{{- end }} diff --git a/userdocs/nodes/disks.rst b/userdocs/nodes/disks.rst index 53742c29..353c2a44 100644 --- a/userdocs/nodes/disks.rst +++ b/userdocs/nodes/disks.rst @@ -4,7 +4,8 @@ Provisioning disks As a tech preview, Warewulf provides structures to define disks, partitions, and file systems. These structures can generate a configuration for `Ignition`_ to -provision partitions and file systems dynamically on cluster nodes. +provision partitions and file systems dynamically on cluster nodes, or with +sfdisk, mkfs, and mkswap during boot. .. _Ignition: https://coreos.github.io/ignition/ diff --git a/userdocs/overlays/overlays.rst b/userdocs/overlays/overlays.rst index 422ddcd7..73306f2b 100644 --- a/userdocs/overlays/overlays.rst +++ b/userdocs/overlays/overlays.rst @@ -371,6 +371,105 @@ specified with a ``localtime`` tag. wwctl profile set default --tagadd="localtime=UTC" +sfdisk +------ + +The **sfdisk** overlay partitions block devices during wwinit. Configuration may +be provided using native disk and partition configuration or via an ``sfdisk`` +resource. + +Multiple devices can be partitioned, with each device provided as an item in +``sfdisk`` list. + +.. code-block:: yaml + + sfdisk: + - device: /dev/sda + label: gpt + partitions: + - device: /dev/sda1 + name: sfdisk-rootfs + size: 4194304 + - device: /dev/sda2 + name: sfdisk-scratch + size: 1048576 + - device: /dev/sda3 + name: sfdisk-swap + size: 2097152 + +All headers and named partition fields supported by the ``sfdisk`` input format +are supported in the ``sfdisk`` resource. + +If any disk/partition configuration is provided for a node with explicit +arguments to ``wwctl set``, the ``sfdisk`` resource is ignored. + +To use the sfdisk overlay, include sfdisk in the Dracut image. Optionally also +include blockdev and/or udevadm, to allow the partition table to be re-scanned. + +.. code-block:: shell + + wwctl image exec rockylinux-8 -- /usr/bin/dracut --force --no-hostonly --add wwinit --install sfdisk --install blockdev --install udevadm --regenerate-all + +For more information, see the :ref:`provision to disk` section. + +mkfs +---- + +The **mkfs** overlay formats block devices during wwinit. Configuration may be +provided using native filesystem fields or via an ``mkfs`` resource. + +.. code-block:: yaml + + mkfs: + - device: /dev/sda1 # the device to format + type: xfs # what type of file system to create + options: -b 1024 # additional options to pass to mkfs + overwrite: false # whether to overwrite an existing format + size: 0 # defaults to the full device + +If any filesystem configuration is provided for a node with explicit arguments +to ``wwctl set``, the ``mkfs`` resource is ignored. + +To use the mkfs overlay, include mkfs and any necessary file-system-specific +sub-commands in the Dracut image. Optionally also include wipefs to detect +existing file systems. + +.. code-block:: shell + + wwctl image exec rockylinux-9 -- /usr/bin/dracut --force --no-hostonly --add wwinit --install mkfs --install mkfs.ext4 --install wipefs --regenerate-all + +For more information, see the :ref:`provision to disk` section. + +mkswap +------ + +The **mkswap** overlay formats block devices during wwinit. Configuration may be +provided using native filesystem fields or via a ``mkswap`` resource. + +.. code-block:: yaml + + mkswap: + - device: /dev/sda2 # the device to format + overwrite: false # whether to overwrite an existing format + label: swap # the label to set for the swap device + +If any filesystem configuration is provided for a node with explicit arguments +to ``wwctl set``, the ``mkswap`` resource is ignored. + +To use the mkswap overlay, include mkswap in the Dracut image. Optionally also +include wipefs to detect existing file systems. + +.. code-block:: shell + + wwctl image exec rockylinux-9 -- /usr/bin/dracut --force --no-hostonly --add wwinit --install mkswap --regenerate-all + +systemd mounts +-------------- + +Two overlays, **systemd.mount** and **systemd.swap**, configure mounted and swap +storage based on the configuration of native file system fields. They are often +paired with the ``mkfs`` and ``mkswap`` overlays. + host ---- diff --git a/userdocs/overlays/templates.rst b/userdocs/overlays/templates.rst index 9d7a806b..683e8fc5 100644 --- a/userdocs/overlays/templates.rst +++ b/userdocs/overlays/templates.rst @@ -95,6 +95,16 @@ referenced symlink. {{ ImportLink "/etc/localtime" }} +When paired with ``file``, ``ImportLink`` can create multiple symlinks from a +single template. + +.. code-block:: + + {{ file "/tmp/test-link1"}} + {{ softlink "/tmp/test-target1" }} + {{ file "/tmp/test-link2"}} + {{ softlink "/tmp/test-target2" }} + basename -------- @@ -133,6 +143,9 @@ Causes the processed template file to become a symlink to the referenced target. {{ printf "%s/%s" "/usr/share/zoneinfo" .Tags.localtime | softlink }} +When paired with ``file``, ``softlink`` can create multiple symlinks from a +single template. + .. _readlink: readlink diff --git a/userdocs/server/bootloaders.rst b/userdocs/server/bootloaders.rst index c9bf9b2f..2a81e6f3 100644 --- a/userdocs/server/bootloaders.rst +++ b/userdocs/server/bootloaders.rst @@ -454,6 +454,52 @@ Ignition must be included in the Dracut image. The necessary file system may alternatively be prepared out-of-band. +With sfdisk and mkfs +-------------------- + +Systems that do not have access to Ignition (e.g., Rocky Linux 8) can provision +the root file system using a combination of ``sfdisk`` and ``mkfs``. To use +them, include ``sfdisk`` and ``mkfs`` in your system overlay. The ``sfdisk`` and +``mkfs`` overlays provision disk and file systems during the first stage of a +two-stage boot. This allows the root file system to be provisioned before the +image is loaded. + +Configure the ``sfdisk`` and ``mkfs`` overlays using resources: + +.. 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, some +additional commands must be included in the Dracut image, depending on which +functionality is used: + +- **sfdisk:** writes the partition table + + - **blockdev:** used to re-read the partition table after writing + + - **udevadm:** used to trigger udev events after writing the partition table + +- **mkfs:** formats file systems (may also require file-system-specific commands like mkfs.ext4) + + - **mkfs.ext4**, **mkfs.btrfs**, etc: used by mkfs to format specific file systems + + - **wipefs:** used to determine if a file system already exists + +.. code-block:: shell + + wwctl image exec rockylinux-8 -- /usr/bin/dracut --force --no-hostonly \ + --add wwinit \ + --install sfdisk \ + --install blockdev \ + --install udevadm \ + --install mkfs \ + --install mkfs.ext4 \ + --install wipefs \ + --regenerate-all Configuring the root device --------------------------- diff --git a/warewulf.spec.in b/warewulf.spec.in index ddc0e313..ff0ee945 100644 --- a/warewulf.spec.in +++ b/warewulf.spec.in @@ -190,6 +190,11 @@ getent group %{wwgroup} >/dev/null || groupadd -r %{wwgroup} %{_datadir}/warewulf/overlays/wwclient/rootfs/* %{_datadir}/warewulf/overlays/wwinit/rootfs/* %{_datadir}/warewulf/overlays/localtime/rootfs/* +%{_datadir}/warewulf/overlays/sfdisk/rootfs/* +%{_datadir}/warewulf/overlays/mkfs/rootfs/* +%{_datadir}/warewulf/overlays/mkswap/rootfs/* +%{_datadir}/warewulf/overlays/systemd.mount/rootfs/* +%{_datadir}/warewulf/overlays/systemd.swap/rootfs/* %{_bindir}/wwctl %{_prefix}/lib/firewalld/services/warewulf.xml