SHA256
1
0
forked from pool/warewulf4
warewulf4/grub-boot.patch
Christian Goll b9cf0a703d Accepting request 1139432 from home:mslacken:pr
- added documentation for replacing dhcpd and tftp with dnsmasq
  as README.dnsmasq (jira#HPC-65)
- added following patches:
  * clean-warewulf-conf.patch
  * dnsmasq-template-move.patch

OBS-URL: https://build.opensuse.org/request/show/1139432
OBS-URL: https://build.opensuse.org/package/show/network:cluster/warewulf4?expand=0&rev=50
2024-01-17 13:10:53 +00:00

1490 lines
48 KiB
Diff

diff --git a/Makefile b/Makefile
index 4ef5a5bf..e85ac05b 100644
--- a/Makefile
+++ b/Makefile
@@ -94,8 +94,10 @@ install: build docs
install -d -m 0755 $(DESTDIR)$(WWCHROOTDIR)
install -d -m 0755 $(DESTDIR)$(WWPROVISIONDIR)
install -d -m 0755 $(DESTDIR)$(WWOVERLAYDIR)/wwinit/$(WWCLIENTDIR)
+ install -d -m 0755 $(DESTDIR)$(WWOVERLAYDIR)/host/$(TFTPDIR)/warewulf/
install -d -m 0755 $(DESTDIR)$(WWCONFIGDIR)/examples
install -d -m 0755 $(DESTDIR)$(WWCONFIGDIR)/ipxe
+ install -d -m 0755 $(DESTDIR)$(WWCONFIGDIR)/grub
install -d -m 0755 $(DESTDIR)$(BASHCOMPDIR)
install -d -m 0755 $(DESTDIR)$(MANDIR)/man1
install -d -m 0755 $(DESTDIR)$(MANDIR)/man5
@@ -112,6 +114,8 @@ install: build docs
test -f $(DESTDIR)$(DATADIR)/warewulf/defaults.conf || install -m 0644 etc/defaults.conf $(DESTDIR)$(DATADIR)/warewulf/defaults.conf
for f in etc/examples/*.ww; do install -m 0644 $$f $(DESTDIR)$(WWCONFIGDIR)/examples/; done
for f in etc/ipxe/*.ipxe; do install -m 0644 $$f $(DESTDIR)$(WWCONFIGDIR)/ipxe/; done
+ install -m 0644 etc/grub/grub.cfg.ww $(DESTDIR)$(WWCONFIGDIR)/grub/grub.cfg.ww
+ install -m 0644 etc/grub/chainload.ww $(DESTDIR)$(WWOVERLAYDIR)/host$(TFTPDIR)/warewulf/grub.cfg.ww
(cd overlays && find * -type f -exec install -D -m 0644 {} $(DESTDIR)$(WWOVERLAYDIR)/{} \;)
(cd overlays && find * -type d -exec mkdir -pv $(DESTDIR)$(WWOVERLAYDIR)/{} \;)
(cd overlays && find * -type l -exec cp -av {} $(DESTDIR)$(WWOVERLAYDIR)/{} \;)
diff --git a/etc/grub/chainload.ww b/etc/grub/chainload.ww
new file mode 100644
index 00000000..dc3c51d8
--- /dev/null
+++ b/etc/grub/chainload.ww
@@ -0,0 +1,28 @@
+# This file is autogenerated by warewulf
+# Host: {{ .BuildHost }}
+# Time: {{ .BuildTime }}
+# Source: {{ .BuildSource }}
+echo "================================================================================"
+echo "Warewulf v4 now iXPE booting with grub"
+echo "================================================================================"
+set timeout=2
+# Must chainload in order to get kernel args for specific node
+menuentry "Load specific configfile" {
+ conf="(http,{{.Ipaddr}}:{{.Warewulf.Port}})/efiboot/grub.cfg"
+ configfile $conf
+}
+menuentry "Chainload shim of container" {
+ shim="(http,{{.Ipaddr}}:{{.Warewulf.Port}})/efiboot/shim.efi"
+ chainloader ${shim}
+}
+menuentry "UEFI Firmware Settings" --id "uefi-firmware" {
+ fwsetup
+}
+menuentry "System restart" {
+ echo "System rebooting..."
+ reboot
+}
+menuentry "System shutdown" {
+ echo "System shutting down..."
+ halt
+}
diff --git a/etc/grub/grub.cfg.ww b/etc/grub/grub.cfg.ww
new file mode 100644
index 00000000..7efbbe0a
--- /dev/null
+++ b/etc/grub/grub.cfg.ww
@@ -0,0 +1,51 @@
+echo "================================================================================"
+echo "Warewulf v4 now http booting grub: {{.Fqdn}} ({{.Hwaddr}})"
+echo "================================================================================"
+echo
+echo "Warewulf Controller: {{.Ipaddr}}"
+echo
+sleep 1
+smbios --type1 --get-string 8 --set assetkey
+uri="(http,{{.Ipaddr}}:{{.Port}})/provision/${net_default_mac}?assetkey=${assetkey}"
+kernel="${uri}&stage=kernel"
+container="${uri}&stage=container&compress=gz"
+system="${uri}&stage=system&compress=gz"
+runtime="${uri}&stage=runtime&compress=gz"
+set default=ww4
+set timeout=5
+menuentry "Network boot node: {{.Id}}" --id ww4 {
+ {{if .KernelOverride }}
+ echo "Kernel: {{.KernelOverride}}"
+ {{else}}
+ echo "Kernel: {{.ContainerName}} (container default)"
+ {{end}}
+ echo "KernelArgs: {{.KernelArgs}}"
+ linux $kernel wwid=${net_default_mac} {{.KernelArgs}}
+ if [ x$? = x0 ] ; then
+ echo "Loading Container: {{.ContainerName}}"
+ initrd $container $system $runtime
+ boot
+ else
+ echo "MESSAGE: This node seems to be unconfigured. Please have your system administrator add a"
+ echo " configuration for this node with HW address: ${net_default_mac}"
+ echo ""
+ echo "Rebooting in 1 minute..."
+ sleep 60
+ reboot
+ fi
+}
+menuentry "Chainload specific configfile" {
+ conf="(http,{{.Ipaddr}}:{{.Port}})/efiboot/grub.cfg"
+ configfile $conf
+}
+menuentry "UEFI Firmware Settings" --id "uefi-firmware" {
+ fwsetup
+}
+menuentry "System restart" {
+ echo "System rebooting..."
+ reboot
+}
+menuentry "System shutdown" {
+ echo "System shutting down..."
+ halt
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index c9e07d29..4d304414 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
github.com/creasty/defaults v1.7.0
github.com/fatih/color v1.15.0
github.com/golang/glog v1.0.0
+ github.com/golang/protobuf v1.5.2
github.com/google/uuid v1.3.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2
github.com/manifoldco/promptui v0.9.0
@@ -56,7 +57,6 @@ require (
github.com/docker/go-units v0.4.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
- github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/gorilla/mux v1.7.4 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
diff --git a/internal/app/wwctl/container/delete/main.go b/internal/app/wwctl/container/delete/main.go
index 576ccef7..71e5a3d7 100644
--- a/internal/app/wwctl/container/delete/main.go
+++ b/internal/app/wwctl/container/delete/main.go
@@ -5,7 +5,8 @@ import (
"github.com/hpcng/warewulf/internal/pkg/api/container"
"github.com/hpcng/warewulf/internal/pkg/api/routes/wwapiv1"
- "github.com/hpcng/warewulf/internal/pkg/api/util"
+ apiutil "github.com/hpcng/warewulf/internal/pkg/api/util"
+
"github.com/spf13/cobra"
)
@@ -13,8 +14,9 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) {
cdp := &wwapiv1.ContainerDeleteParameter{
ContainerNames: args,
}
+
if !SetYes {
- yes := util.ConfirmationPrompt(fmt.Sprintf("Are you sure you want to delete container %s", args))
+ yes := apiutil.ConfirmationPrompt(fmt.Sprintf("Are you sure you want to delete container %s", args))
if !yes {
return
}
diff --git a/internal/app/wwctl/container/exec/main.go b/internal/app/wwctl/container/exec/main.go
index bc622de2..f34ada9f 100644
--- a/internal/app/wwctl/container/exec/main.go
+++ b/internal/app/wwctl/container/exec/main.go
@@ -133,7 +133,6 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
wwlog.Error("Could not build container %s: %s", containerName, err)
os.Exit(1)
}
-
return nil
}
func SetBinds(myBinds []string) {
diff --git a/internal/app/wwctl/container/imprt/main.go b/internal/app/wwctl/container/imprt/main.go
index 1ec28322..4114b45c 100644
--- a/internal/app/wwctl/container/imprt/main.go
+++ b/internal/app/wwctl/container/imprt/main.go
@@ -1,7 +1,9 @@
package imprt
import (
- "github.com/hpcng/warewulf/internal/pkg/api/container"
+ "github.com/hpcng/warewulf/internal/pkg/container"
+
+ apicontainer "github.com/hpcng/warewulf/internal/pkg/api/container"
"github.com/hpcng/warewulf/internal/pkg/api/routes/wwapiv1"
"github.com/spf13/cobra"
)
@@ -13,6 +15,9 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) {
if len(args) == 2 {
name = args[1]
}
+ if list, _ := container.ListSources(); len(list) == 0 {
+ SetDefault = true
+ }
cip := &wwapiv1.ContainerImportParameter{
Source: args[0],
@@ -23,6 +28,7 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) {
Default: SetDefault,
SyncUser: SyncUser,
}
- _, err = container.ContainerImport(cip)
+
+ _, err = apicontainer.ContainerImport(cip)
return
}
diff --git a/internal/app/wwctl/profile/delete/main.go b/internal/app/wwctl/profile/delete/main.go
index 71d3dabf..137ff4ae 100644
--- a/internal/app/wwctl/profile/delete/main.go
+++ b/internal/app/wwctl/profile/delete/main.go
@@ -5,6 +5,7 @@ import (
"os"
"github.com/hpcng/warewulf/internal/pkg/node"
+ "github.com/hpcng/warewulf/internal/pkg/util"
"github.com/hpcng/warewulf/internal/pkg/wwlog"
"github.com/manifoldco/promptui"
"github.com/pkg/errors"
@@ -13,6 +14,9 @@ import (
func CobraRunE(cmd *cobra.Command, args []string) error {
var count int
+ if util.InSlice(args, "default") {
+ return fmt.Errorf("can't delete the `default` profile ")
+ }
nodeDB, err := node.New()
if err != nil {
diff --git a/internal/pkg/api/profile/profile.go b/internal/pkg/api/profile/profile.go
index e6ca6c68..50e60afc 100644
--- a/internal/pkg/api/profile/profile.go
+++ b/internal/pkg/api/profile/profile.go
@@ -24,7 +24,8 @@ func ProfileSet(set *wwapiv1.ProfileSetParameter) (err error) {
if err != nil {
return errors.Wrap(err, "Could not open database")
}
- return apinode.DbSave(&nodeDB)
+ dbError := apinode.DbSave(&nodeDB)
+ return dbError
}
// ProfileSetParameterCheck does error checking on ProfileSetParameter.
diff --git a/internal/pkg/config/warewulf.go b/internal/pkg/config/warewulf.go
index e8f08ac5..9a0dc3f4 100644
--- a/internal/pkg/config/warewulf.go
+++ b/internal/pkg/config/warewulf.go
@@ -10,4 +10,5 @@ type WarewulfConf struct {
EnableHostOverlay bool `yaml:"host overlay" default:"true"`
Syslog bool `yaml:"syslog" default:"false"`
DataStore string `yaml:"datastore" default:"/var/lib/warewulf"`
+ GrubBoot bool `yaml:"grubboot" default:"false"`
}
diff --git a/internal/pkg/configure/tftp.go b/internal/pkg/configure/tftp.go
index 82e023f3..058ca059 100644
--- a/internal/pkg/configure/tftp.go
+++ b/internal/pkg/configure/tftp.go
@@ -1,12 +1,12 @@
package configure
import (
- "fmt"
"os"
"path"
warewulfconf "github.com/hpcng/warewulf/internal/pkg/config"
"github.com/hpcng/warewulf/internal/pkg/util"
+ "github.com/hpcng/warewulf/internal/pkg/warewulfd"
"github.com/hpcng/warewulf/internal/pkg/wwlog"
)
@@ -20,28 +20,34 @@ func TFTP() error {
return err
}
- fmt.Printf("Writing PXE files to: %s\n", tftpdir)
- copyCheck := make(map[string]bool)
- for _, f := range controller.TFTP.IpxeBinaries {
- if !path.IsAbs(f) {
- f = path.Join(controller.Paths.Ipxesource, f)
- }
- if copyCheck[f] {
- continue
- }
- copyCheck[f] = true
- err = util.SafeCopyFile(f, path.Join(tftpdir, path.Base(f)))
+ if controller.Warewulf.GrubBoot {
+ err := warewulfd.CopyShimGrub()
if err != nil {
- wwlog.Warn("ipxe binary could not be copied, booting may not work: %s", err)
+ wwlog.Warn("error when copying shim/grub binaries: %s", err)
+ }
+ } else {
+ wwlog.Info("Writing PXE files to: %s", tftpdir)
+ copyCheck := make(map[string]bool)
+ for _, f := range controller.TFTP.IpxeBinaries {
+ if !path.IsAbs(f) {
+ f = path.Join(controller.Paths.Ipxesource, f)
+ }
+ if copyCheck[f] {
+ continue
+ }
+ copyCheck[f] = true
+ err = util.SafeCopyFile(f, path.Join(tftpdir, path.Base(f)))
+ if err != nil {
+ wwlog.Warn("ipxe binary could not be copied, booting may not work: %s", err)
+ }
}
}
-
if !controller.TFTP.Enabled {
wwlog.Info("Warewulf does not auto start TFTP services due to disable by warewulf.conf")
os.Exit(0)
}
- fmt.Printf("Enabling and restarting the TFTP services\n")
+ wwlog.Info("Enabling and restarting the TFTP services")
err = util.SystemdStart(controller.TFTP.SystemdName)
if err != nil {
wwlog.Error("%s", err)
diff --git a/internal/pkg/container/shimgrub.go b/internal/pkg/container/shimgrub.go
new file mode 100644
index 00000000..a590690f
--- /dev/null
+++ b/internal/pkg/container/shimgrub.go
@@ -0,0 +1,94 @@
+package container
+
+import (
+ "os"
+ "path"
+ "path/filepath"
+
+ "github.com/hpcng/warewulf/internal/pkg/wwlog"
+)
+
+func shimDirs() []string {
+ return []string{
+ `/usr/share/efi/x86_64/`,
+ `/usr/lib64/efi/`,
+ `/boot/efi/EFI/*/`,
+ }
+}
+func shimNames() []string {
+ return []string{
+ `shim.efi`,
+ `shim-sles.efi`,
+ `shimx64.efi`,
+ `shim-susesigned.efi`,
+ }
+}
+
+func grubDirs() []string {
+ return []string{
+ `/usr/lib64/efi/`,
+ `/usr/share/grub2/*-efi/`,
+ `/usr/share/efi/*/`,
+ `/boot/efi/EFI/*/`,
+ }
+}
+func grubNames() []string {
+ return []string{
+ `grub-tpm.efi`,
+ `grub.efi`,
+ `grubx64.efi`,
+ `grubia32.efi`,
+ `grubaa64.efi`,
+ `grubarm.efi`,
+ }
+}
+
+/*
+find the path of the shim binary in container
+*/
+func ShimFind(container string) string {
+ var container_path string
+ if container != "" {
+ container_path = RootFsDir(container)
+ } else {
+ container_path = "/"
+ }
+ wwlog.Debug("Finding grub under paths: %s", container_path)
+ return BootLoaderFindPath(container_path, shimNames, shimDirs)
+}
+
+/*
+find a grub.efi in the used container
+*/
+func GrubFind(container string) string {
+ var container_path string
+ if container != "" {
+ container_path = RootFsDir(container)
+ } else {
+ container_path = "/"
+ }
+ wwlog.Debug("Finding grub under paths: %s", container_path)
+ return BootLoaderFindPath(container_path, grubNames, grubDirs)
+}
+
+/*
+find the path of the shim binary in container
+*/
+func BootLoaderFindPath(cpath string, names func() []string, paths func() []string) string {
+ for _, bdir := range paths() {
+ wwlog.Debug("Checking shim directory: %s", bdir)
+ for _, bname := range names() {
+ wwlog.Debug("Checking for bootloader name: %s", bname)
+ shimPaths, _ := filepath.Glob(path.Join(cpath, bdir, bname))
+ for _, shimPath := range shimPaths {
+ wwlog.Debug("Checking for bootloader path: %s", shimPath)
+ // Only succeeds if shimPath exists and, if a
+ // symlink, links to a path that also exists
+ if _, err := os.Stat(shimPath); err == nil {
+ return shimPath
+ }
+ }
+ }
+ }
+ return ""
+}
diff --git a/internal/pkg/node/constructors.go b/internal/pkg/node/constructors.go
index fe7eb87a..857f93cf 100644
--- a/internal/pkg/node/constructors.go
+++ b/internal/pkg/node/constructors.go
@@ -31,6 +31,7 @@ defaultnode:
init: /sbin/init
root: initramfs
ipxe template: default
+ boot method: ipxe
profiles:
- default
network devices:
@@ -305,6 +306,36 @@ func (config *NodeYaml) ListAllProfiles() []string {
return ret
}
+/*
+return a map where the key is the profile id
+*/
+func (config *NodeYaml) MapAllProfiles() (retMap map[string]*NodeInfo, err error) {
+ retMap = make(map[string]*NodeInfo)
+ profileList, err := config.FindAllProfiles()
+ if err != nil {
+ return
+ }
+ for _, pr := range profileList {
+ retMap[pr.Id.Get()] = &pr
+ }
+ return
+}
+
+/*
+return a map where the key is the node id
+*/
+func (config *NodeYaml) MapAllNodes() (retMap map[string]*NodeInfo, err error) {
+ retMap = make(map[string]*NodeInfo)
+ nodeList, err := config.FindAllNodes()
+ if err != nil {
+ return
+ }
+ for _, nd := range nodeList {
+ retMap[nd.Id.Get()] = &nd
+ }
+ return
+}
+
/*
FindDiscoverableNode returns the first discoverable node and an
interface to associate with the discovered interface. If the node has
diff --git a/internal/pkg/node/datastructure.go b/internal/pkg/node/datastructure.go
index 989c007a..34432454 100644
--- a/internal/pkg/node/datastructure.go
+++ b/internal/pkg/node/datastructure.go
@@ -154,6 +154,7 @@ type NodeInfo struct {
ClusterName Entry
ContainerName Entry
Ipxe Entry
+ Grub Entry
RuntimeOverlay Entry
SystemOverlay Entry
Root Entry
diff --git a/internal/pkg/node/util.go b/internal/pkg/node/util.go
index e90efee6..8e1273c3 100644
--- a/internal/pkg/node/util.go
+++ b/internal/pkg/node/util.go
@@ -6,6 +6,9 @@ import (
"strings"
)
+/*
+get node by its hardware/MAC address, return error otherwise
+*/
func (config *NodeYaml) FindByHwaddr(hwa string) (NodeInfo, error) {
if _, err := net.ParseMAC(hwa); err != nil {
return NodeInfo{}, errors.New("invalid hardware address: " + hwa)
@@ -26,6 +29,9 @@ func (config *NodeYaml) FindByHwaddr(hwa string) (NodeInfo, error) {
return ret, errors.New("No nodes found with HW Addr: " + hwa)
}
+/*
+get node by its ip address, return error otherwise
+*/
func (config *NodeYaml) FindByIpaddr(ipaddr string) (NodeInfo, error) {
if addr := net.ParseIP(ipaddr); addr == nil {
return NodeInfo{}, errors.New("invalid IP:" + ipaddr)
diff --git a/internal/pkg/overlay/datastructure.go b/internal/pkg/overlay/datastructure.go
index 15c9d3d5..0adc2f39 100644
--- a/internal/pkg/overlay/datastructure.go
+++ b/internal/pkg/overlay/datastructure.go
@@ -49,19 +49,15 @@ func InitStruct(nodeInfo *node.NodeInfo) TemplateStruct {
controller := warewulfconf.Get()
nodeDB, err := node.New()
if err != nil {
- wwlog.Error("%s", err)
- os.Exit(1)
+ wwlog.Warn("Problems opening nodes.conf: %s", err)
}
- allNodes, err := nodeDB.FindAllNodes()
+ tstruct.AllNodes, err = nodeDB.FindAllNodes()
if err != nil {
- wwlog.Error("%s", err)
- os.Exit(1)
+ wwlog.Warn("couldn't get all nodes: %s", err)
}
// init some convenience vars
tstruct.Id = nodeInfo.Id.Get()
tstruct.Hostname = nodeInfo.Id.Get()
- // Backwards compatibility for templates using "Keys"
- tstruct.AllNodes = allNodes
tstruct.Nfs = *controller.NFS
tstruct.Dhcp = *controller.DHCP
tstruct.Tftp = *controller.TFTP
diff --git a/internal/pkg/warewulfd/copyshim.go b/internal/pkg/warewulfd/copyshim.go
new file mode 100644
index 00000000..44ea45f4
--- /dev/null
+++ b/internal/pkg/warewulfd/copyshim.go
@@ -0,0 +1,45 @@
+package warewulfd
+
+import (
+ "fmt"
+ "os"
+ "path"
+
+ warewulfconf "github.com/hpcng/warewulf/internal/pkg/config"
+ "github.com/hpcng/warewulf/internal/pkg/wwlog"
+
+ "github.com/hpcng/warewulf/internal/pkg/container"
+ "github.com/hpcng/warewulf/internal/pkg/util"
+)
+
+/*
+Copies the default shim, which is the shim located on host
+to the tftp directory
+*/
+
+func CopyShimGrub() (err error) {
+ conf := warewulfconf.Get()
+ wwlog.Debug("copy shim and grub binaries from host")
+ shimPath := container.ShimFind("")
+ if shimPath == "" {
+ return fmt.Errorf("no shim found on the host os")
+ }
+ err = util.CopyFile(shimPath, path.Join(conf.Paths.Tftpdir, "warewulf", "shim.efi"))
+ if err != nil {
+ return err
+ }
+ _ = os.Chmod(path.Join(conf.Paths.Tftpdir, "warewulf", "shim.efi"), 0o755)
+ grubPath := container.GrubFind("")
+ if grubPath == "" {
+ return fmt.Errorf("no grub found on host os")
+ }
+ err = util.CopyFile(grubPath, path.Join(conf.Paths.Tftpdir, "warewulf", "grub.efi"))
+ if err != nil {
+ return err
+ }
+ _ = os.Chmod(path.Join(conf.Paths.Tftpdir, "warewulf", "grub.efi"), 0o755)
+ err = util.CopyFile(grubPath, path.Join(conf.Paths.Tftpdir, "warewulf", "grubx64.efi"))
+ _ = os.Chmod(path.Join(conf.Paths.Tftpdir, "warewulf", "grubx64.efi"), 0o755)
+
+ return
+}
diff --git a/internal/pkg/warewulfd/parser.go b/internal/pkg/warewulfd/parser.go
index 1e62dd0f..063d10b4 100644
--- a/internal/pkg/warewulfd/parser.go
+++ b/internal/pkg/warewulfd/parser.go
@@ -5,6 +5,7 @@ import (
"strconv"
"strings"
+ "github.com/hpcng/warewulf/internal/pkg/wwlog"
"github.com/pkg/errors"
)
@@ -16,6 +17,7 @@ type parserInfo struct {
uuid string
stage string
overlay string
+ efifile string
compress string
}
@@ -25,16 +27,22 @@ func parseReq(req *http.Request) (parserInfo, error) {
url := strings.Split(req.URL.Path, "?")[0]
path_parts := strings.Split(url, "/")
- if len(path_parts) != 3 {
+ if len(path_parts) < 3 {
return ret, errors.New("unknown path components in GET")
}
// handle when stage was passed in the url path /[stage]/hwaddr
stage := path_parts[1]
- hwaddr := path_parts[2]
- hwaddr = strings.ReplaceAll(hwaddr, "-", ":")
- hwaddr = strings.ToLower(hwaddr)
-
+ hwaddr := ""
+ if stage != "efiboot" {
+ hwaddr = path_parts[2]
+ hwaddr = strings.ReplaceAll(hwaddr, "-", ":")
+ hwaddr = strings.ToLower(hwaddr)
+ } else if len(path_parts) > 3 {
+ ret.efifile = strings.Join(path_parts[2:], "/")
+ } else {
+ ret.efifile = path_parts[2]
+ }
ret.hwaddr = hwaddr
ret.ipaddr = strings.Split(req.RemoteAddr, ":")[0]
ret.remoteport, _ = strconv.Atoi(strings.Split(req.RemoteAddr, ":")[1])
@@ -63,6 +71,8 @@ func parseReq(req *http.Request) (parserInfo, error) {
ret.stage = "system"
} else if stage == "overlay-runtime" {
ret.stage = "runtime"
+ } else if stage == "efiboot" {
+ ret.stage = "efiboot"
}
}
@@ -76,7 +86,11 @@ func parseReq(req *http.Request) (parserInfo, error) {
return ret, errors.New("no stage encoded in GET")
}
if ret.hwaddr == "" {
- return ret, errors.New("no hwaddr encoded in GET")
+ ret.hwaddr = ArpFind(ret.ipaddr)
+ wwlog.Verbose("node mac not encoded, arp cache got %s for %s", ret.hwaddr, ret.ipaddr)
+ if ret.hwaddr == "" {
+ return ret, errors.New("no hwaddr encoded in GET")
+ }
}
if ret.ipaddr == "" {
return ret, errors.New("could not obtain ipaddr from HTTP request")
diff --git a/internal/pkg/warewulfd/provision.go b/internal/pkg/warewulfd/provision.go
index f2851a26..bdca8a34 100644
--- a/internal/pkg/warewulfd/provision.go
+++ b/internal/pkg/warewulfd/provision.go
@@ -3,6 +3,7 @@ package warewulfd
import (
"bytes"
"errors"
+ "fmt"
"net/http"
"path"
"strconv"
@@ -17,7 +18,7 @@ import (
"github.com/hpcng/warewulf/internal/pkg/wwlog"
)
-type iPxeTemplate struct {
+type templateVars struct {
Message string
WaitTime string
Hostname string
@@ -32,20 +33,13 @@ type iPxeTemplate struct {
KernelOverride string
}
-var status_stages = map[string]string{
- "ipxe": "IPXE",
- "kernel": "KERNEL",
- "kmods": "KMODS_OVERLAY",
- "system": "SYSTEM_OVERLAY",
- "runtime": "RUNTIME_OVERLAY"}
-
func ProvisionSend(w http.ResponseWriter, req *http.Request) {
+ wwlog.Debug("Requested URL: %s", req.URL.String())
conf := warewulfconf.Get()
-
rinfo, err := parseReq(req)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
- wwlog.ErrorExc(err, "")
+ wwlog.ErrorExc(err, "Bad status")
return
}
@@ -59,6 +53,14 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) {
}
}
+ status_stages := map[string]string{
+ "efiboot": "EFI",
+ "ipxe": "IPXE",
+ "kernel": "KERNEL",
+ "kmods": "KMODS_OVERLAY",
+ "system": "SYSTEM_OVERLAY",
+ "runtime": "RUNTIME_OVERLAY"}
+
status_stage := status_stages[rinfo.stage]
var stage_file string
@@ -83,13 +85,13 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) {
wwlog.Error("%s (unknown/unconfigured node)", rinfo.hwaddr)
if rinfo.stage == "ipxe" {
stage_file = path.Join(conf.Paths.Sysconfdir, "/warewulf/ipxe/unconfigured.ipxe")
- tmpl_data = iPxeTemplate{
+ tmpl_data = templateVars{
Hwaddr: rinfo.hwaddr}
}
} else if rinfo.stage == "ipxe" {
stage_file = path.Join(conf.Paths.Sysconfdir, "warewulf/ipxe/"+node.Ipxe.Get()+".ipxe")
- tmpl_data = iPxeTemplate{
+ tmpl_data = templateVars{
Id: node.Id.Get(),
Cluster: node.ClusterName.Get(),
Fqdn: node.Id.Get(),
@@ -100,7 +102,6 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) {
ContainerName: node.ContainerName.Get(),
KernelArgs: node.Kernel.Args.Get(),
KernelOverride: node.Kernel.Override.Get()}
-
} else if rinfo.stage == "kernel" {
if node.Kernel.Override.Defined() {
stage_file = kernel.KernelImage(node.Kernel.Override.Get())
@@ -137,7 +138,6 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) {
} else {
context = rinfo.stage
}
-
stage_file, err = getOverlayFile(
node,
context,
@@ -154,6 +154,64 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) {
wwlog.ErrorExc(err, "")
return
}
+ } else if rinfo.stage == "efiboot" {
+ wwlog.Debug("requested method: %s", req.Method)
+ containerName := node.ContainerName.Get()
+ switch rinfo.efifile {
+ case "shim.efi":
+ stage_file = container.ShimFind(containerName)
+ if stage_file == "" {
+ wwlog.ErrorExc(fmt.Errorf("could't find shim.efi"), containerName)
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+ case "grub.efi", "grub-tpm.efi", "grubx64.efi", "grubia32.efi", "grubaa64.efi", "grubarm.efi":
+ stage_file = container.GrubFind(containerName)
+ if stage_file == "" {
+ wwlog.ErrorExc(fmt.Errorf("could't find grub.efi"), containerName)
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+ case "grub.cfg":
+ stage_file = path.Join(conf.Paths.Sysconfdir, "warewulf/grub/grub.cfg.ww")
+ tmpl_data = templateVars{
+ Id: node.Id.Get(),
+ Cluster: node.ClusterName.Get(),
+ Fqdn: node.Id.Get(),
+ Ipaddr: conf.Ipaddr,
+ Port: strconv.Itoa(conf.Warewulf.Port),
+ Hostname: node.Id.Get(),
+ Hwaddr: rinfo.hwaddr,
+ ContainerName: node.ContainerName.Get(),
+ KernelArgs: node.Kernel.Args.Get(),
+ KernelOverride: node.Kernel.Override.Get()}
+ if stage_file == "" {
+ wwlog.ErrorExc(fmt.Errorf("could't find grub.cfg template"), containerName)
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+ default:
+ wwlog.ErrorExc(fmt.Errorf("could't find efiboot file: %s", rinfo.efifile), "")
+ }
+ } else if rinfo.stage == "shim" {
+ if node.ContainerName.Defined() {
+ stage_file = container.ShimFind(node.ContainerName.Get())
+
+ if stage_file == "" {
+ wwlog.Error("No kernel found for container %s", node.ContainerName.Get())
+ }
+ } else {
+ wwlog.Warn("No container set for this %s", node.Id.Get())
+ }
+ } else if rinfo.stage == "grub" {
+ if node.ContainerName.Defined() {
+ stage_file = container.GrubFind(node.ContainerName.Get())
+ if stage_file == "" {
+ wwlog.Error("No grub found for container %s", node.ContainerName.Get())
+ }
+ } else {
+ wwlog.Warn("No conainer set for node %s", node.Id.Get())
+ }
}
wwlog.Serv("stage_file '%s'", stage_file)
diff --git a/internal/pkg/warewulfd/provision_test.go b/internal/pkg/warewulfd/provision_test.go
index 870e1de0..5ec8d72e 100644
--- a/internal/pkg/warewulfd/provision_test.go
+++ b/internal/pkg/warewulfd/provision_test.go
@@ -1,16 +1,18 @@
package warewulfd
import (
- "github.com/stretchr/testify/assert"
- "io/ioutil"
+ "io"
"net/http"
"net/http/httptest"
"os"
"path"
"testing"
+ "github.com/stretchr/testify/assert"
+
warewulfconf "github.com/hpcng/warewulf/internal/pkg/config"
"github.com/hpcng/warewulf/internal/pkg/node"
+ "github.com/hpcng/warewulf/internal/pkg/wwlog"
)
var provisionSendTests = []struct {
@@ -18,37 +20,80 @@ var provisionSendTests = []struct {
url string
body string
status int
+ ip string
}{
- {"system overlay", "/overlay-system/00:00:00:ff:ff:ff", "system overlay", 200},
- {"runtime overlay", "/overlay-runtime/00:00:00:ff:ff:ff", "runtime overlay", 200},
- {"fake overlay", "/overlay-system/00:00:00:ff:ff:ff?overlay=fake", "", 404},
- {"specific overlay", "/overlay-system/00:00:00:ff:ff:ff?overlay=o1", "specific overlay", 200},
+ {"system overlay", "/overlay-system/00:00:00:ff:ff:ff", "system overlay", 200, "10.10.10.10:9873"},
+ {"runtime overlay", "/overlay-runtime/00:00:00:ff:ff:ff", "runtime overlay", 200, "10.10.10.10:9873"},
+ {"fake overlay", "/overlay-system/00:00:00:ff:ff:ff?overlay=fake", "", 404, "10.10.10.10:9873:9873"},
+ {"specific overlay", "/overlay-system/00:00:00:ff:ff:ff?overlay=o1", "specific overlay", 200, "10.10.10.10:9873"},
+ {"find shim", "/efiboot/shim.efi", "", 200, "10.10.10.10:9873"},
+ {"find shim", "/efiboot/shim.efi", "", 404, "10.10.10.11:9873"},
+ {"find grub", "/efiboot/grub.efi", "", 200, "10.10.10.10:9873"},
+ {"find grub", "/efiboot/grub.efi", "", 404, "10.10.10.11:9873"},
}
func Test_ProvisionSend(t *testing.T) {
- file, err := os.CreateTemp(os.TempDir(), "ww-test-nodes.conf-*")
+ conf_file, err := os.CreateTemp(os.TempDir(), "ww-test-nodes.conf-*")
assert.NoError(t, err)
- defer file.Close()
+ defer conf_file.Close()
{
- _, err := file.WriteString(`WW_INTERNAL: 43
+ _, err := conf_file.WriteString(`WW_INTERNAL: 43
+nodeprofiles:
+ default:
+ container name: suse
nodes:
n1:
network devices:
default:
- hwaddr: 00:00:00:ff:ff:ff`)
+ hwaddr: 00:00:00:ff:ff:ff
+ n2:
+ network devices:
+ default:
+ hwaddr: 00:00:00:00:ff:ff
+ container name: none`)
+ assert.NoError(t, err)
+ }
+ assert.NoError(t, conf_file.Sync())
+ node.ConfigFile = conf_file.Name()
+
+ // create a arp file as for grub we look up the ip address through the arp cache
+ arp_file, err := os.CreateTemp(os.TempDir(), "ww-arp")
+ assert.NoError(t, err)
+ defer arp_file.Close()
+ {
+ _, err := arp_file.WriteString(`IP address HW type Flags HW address Mask Device
+10.10.10.10 0x1 0x2 00:00:00:ff:ff:ff * dummy
+10.10.10.11 0x1 0x2 00:00:00:00:ff:ff * dummy`)
assert.NoError(t, err)
}
- assert.NoError(t, file.Sync())
- node.ConfigFile = file.Name()
+ assert.NoError(t, arp_file.Sync())
+ SetArpFile(arp_file.Name())
+
+ conf := warewulfconf.Get()
+ containerDir, imageDirErr := os.MkdirTemp(os.TempDir(), "ww-test-container-*")
+ assert.NoError(t, imageDirErr)
+ defer os.RemoveAll(containerDir)
+ conf.Paths.WWChrootdir = containerDir
+ assert.NoError(t, os.MkdirAll(path.Join(containerDir, "suse/rootfs/usr/lib64/efi"), 0700))
+ {
+ _, err := os.Create(path.Join(containerDir, "suse/rootfs/usr/lib64/efi", "shim.efi"))
+ assert.NoError(t, err)
+ }
+ assert.NoError(t, os.MkdirAll(path.Join(containerDir, "suse/rootfs/usr/share/efi/x86_64/"), 0700))
+ {
+ _, err := os.Create(path.Join(containerDir, "suse/rootfs/usr/share/efi/x86_64/", "grub.efi"))
+ assert.NoError(t, err)
+ }
+
dbErr := LoadNodeDB()
assert.NoError(t, dbErr)
provisionDir, provisionDirErr := os.MkdirTemp(os.TempDir(), "ww-test-provision-*")
assert.NoError(t, provisionDirErr)
defer os.RemoveAll(provisionDir)
- conf := warewulfconf.Get()
conf.Paths.WWProvisiondir = provisionDir
conf.Warewulf.Secure = false
+ wwlog.SetLogLevel(wwlog.DEBUG)
assert.NoError(t, os.MkdirAll(path.Join(provisionDir, "overlays", "n1"), 0700))
assert.NoError(t, os.WriteFile(path.Join(provisionDir, "overlays", "n1", "__SYSTEM__.img"), []byte("system overlay"), 0600))
assert.NoError(t, os.WriteFile(path.Join(provisionDir, "overlays", "n1", "__RUNTIME__.img"), []byte("runtime overlay"), 0600))
@@ -57,12 +102,13 @@ nodes:
for _, tt := range provisionSendTests {
t.Run(tt.description, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tt.url, nil)
+ req.RemoteAddr = tt.ip
w := httptest.NewRecorder()
ProvisionSend(w, req)
res := w.Result()
defer res.Body.Close()
- data, readErr := ioutil.ReadAll(res.Body)
+ data, readErr := io.ReadAll(res.Body)
assert.NoError(t, readErr)
assert.Equal(t, tt.body, string(data))
assert.Equal(t, tt.status, res.StatusCode)
diff --git a/internal/pkg/warewulfd/util.go b/internal/pkg/warewulfd/util.go
index 3290a678..caaf22ae 100644
--- a/internal/pkg/warewulfd/util.go
+++ b/internal/pkg/warewulfd/util.go
@@ -1,8 +1,10 @@
package warewulfd
import (
+ "bufio"
"net/http"
"os"
+ "strings"
"github.com/hpcng/warewulf/internal/pkg/node"
nodepkg "github.com/hpcng/warewulf/internal/pkg/node"
@@ -38,7 +40,7 @@ func sendFile(
fd)
wwlog.Send("%15s: %s", sendto, filename)
-
+ req.Body.Close()
return nil
}
@@ -70,3 +72,34 @@ func getOverlayFile(
return
}
+
+var arpFile string
+
+func init() {
+ arpFile = "/proc/net/arp"
+}
+
+func SetArpFile(newName string) {
+ arpFile = newName
+}
+
+/*
+returns the mac address if it has an entry in the arp cache
+*/
+func ArpFind(ip string) (mac string) {
+ arpCache, err := os.Open(arpFile)
+ if err != nil {
+ return
+ }
+ defer arpCache.Close()
+
+ scanner := bufio.NewScanner(arpCache)
+ scanner.Scan()
+ for scanner.Scan() {
+ fields := strings.Fields(scanner.Text())
+ if strings.EqualFold(fields[0], ip) {
+ return fields[3]
+ }
+ }
+ return
+}
diff --git a/internal/pkg/warewulfd/warewulfd.go b/internal/pkg/warewulfd/warewulfd.go
index 796054b0..c9376634 100644
--- a/internal/pkg/warewulfd/warewulfd.go
+++ b/internal/pkg/warewulfd/warewulfd.go
@@ -5,6 +5,7 @@ import (
"os"
"os/signal"
"strconv"
+ "strings"
"syscall"
warewulfconf "github.com/hpcng/warewulf/internal/pkg/config"
@@ -14,6 +15,23 @@ import (
// TODO: https://github.com/danderson/netboot/blob/master/pixiecore/dhcp.go
// TODO: https://github.com/pin/tftp
+/*
+wrapper type for the server mux as shim requests http://efiboot//grub.efi
+which is filtered out by http to `301 Moved Permanently` what
+shim.efi can't handle. So filter out `//` before they hit go/http.
+Makes go/http more to behave like apache
+*/
+type slashFix struct {
+ mux http.Handler
+}
+
+/*
+Filter out the '//'
+*/
+func (h *slashFix) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ r.URL.Path = strings.Replace(r.URL.Path, "//", "/", -1)
+ h.mux.ServeHTTP(w, r)
+}
func RunServer() error {
err := DaemonInitLogging()
@@ -49,21 +67,36 @@ func RunServer() error {
wwlog.Error("Could not prepopulate node status DB: %s", err)
}
- http.HandleFunc("/provision/", ProvisionSend)
- http.HandleFunc("/ipxe/", ProvisionSend)
- http.HandleFunc("/kernel/", ProvisionSend)
- http.HandleFunc("/kmods/", ProvisionSend)
- http.HandleFunc("/container/", ProvisionSend)
- http.HandleFunc("/overlay-system/", ProvisionSend)
- http.HandleFunc("/overlay-runtime/", ProvisionSend)
- http.HandleFunc("/status", StatusSend)
+ if err != nil {
+ wwlog.Warn("couldn't copy default shim: %s", err)
+ }
+ var wwHandler http.ServeMux
+ wwHandler.HandleFunc("/provision/", ProvisionSend)
+ wwHandler.HandleFunc("/ipxe/", ProvisionSend)
+ wwHandler.HandleFunc("/efiboot/", ProvisionSend)
+ wwHandler.HandleFunc("/kernel/", ProvisionSend)
+ wwHandler.HandleFunc("/kmods/", ProvisionSend)
+ wwHandler.HandleFunc("/container/", ProvisionSend)
+ wwHandler.HandleFunc("/overlay-system/", ProvisionSend)
+ wwHandler.HandleFunc("/overlay-runtime/", ProvisionSend)
+ wwHandler.HandleFunc("/status", StatusSend)
conf := warewulfconf.Get()
daemonPort := conf.Warewulf.Port
- wwlog.Serv("Starting HTTPD REST service on port %d", daemonPort)
+ /*
+ wwlog.Serv("Starting HTTPD REST service on port %d", daemonPort)
+ s := &http.Server{
+ Addr: ":" + strconv.Itoa(daemonPort),
+ Handler: &slashFix{&wwHandler},
+ ReadTimeout: 10 * time.Second,
+ IdleTimeout: 10 * time.Second,
+ WriteTimeout: 10 * time.Second,
+ }
+ err = s.ListenAndServe()
+ */
+ err = http.ListenAndServe(":"+strconv.Itoa(daemonPort), &slashFix{&wwHandler})
- err = http.ListenAndServe(":"+strconv.Itoa(daemonPort), nil)
if err != nil {
return errors.Wrap(err, "Could not start listening service")
}
diff --git a/internal/pkg/wwlog/wwlog.go b/internal/pkg/wwlog/wwlog.go
index 98f67f2a..0b8ee5ca 100644
--- a/internal/pkg/wwlog/wwlog.go
+++ b/internal/pkg/wwlog/wwlog.go
@@ -36,9 +36,11 @@ var (
ERROR = SetLevelName(40, "ERROR")
SECWARN = SetLevelName(31, "SECWARN")
WARN = SetLevelName(30, "WARN")
+ ERROUT = SetLevelName(29, "ERROUT")
SEND = SetLevelName(27, "SEND")
RECV = SetLevelName(26, "RECV")
SERV = SetLevelName(25, "SERV")
+ OUT = SetLevelName(22, "OUT")
SECINFO = SetLevelName(21, "SECINFO")
INFO = SetLevelName(20, "INFO")
SECVERBOSE = SetLevelName(16, "SECVERBOSE")
@@ -52,6 +54,7 @@ var (
levelNames = []string{"NOTSET"}
logLevel = INFO
logErr io.Writer = os.Stderr
+ logOut io.Writer = os.Stdout
logFormatter LogFormatter = DefaultFormatter
)
@@ -153,11 +156,30 @@ func GetLogLevel() int {
Set the log output writer
By default they are set to output writer
*/
-func SetLogWriter(err io.Writer) {
- logErr = err
+func SetLogWriter(newOut io.Writer) {
+ logErr = newOut
+ logOut = newOut
}
-func GetLogWriter() io.Writer {
+/*
+Set the log ofr info only
+*/
+func SetLogWriterInfo(newOut io.Writer) {
+ logOut = newOut
+}
+
+/*
+Set the log ofr info only
+*/
+func SetLogWriterErr(newOut io.Writer) {
+ logOut = newOut
+}
+
+func GetLogWriterInfo() io.Writer {
+ return logOut
+}
+
+func GetLogWriterErr() io.Writer {
return logErr
}
@@ -196,8 +218,12 @@ func LogCaller(level int, skip int, err error, message string, a ...interface{})
}
message = logFormatter(logLevel, &rec)
+ if level == INFO || level == RECV || level == SEND || level == OUT {
+ fmt.Fprint(logOut, message)
+ } else {
+ fmt.Fprint(logErr, message)
- fmt.Fprint(logErr, message)
+ }
}
}
@@ -249,6 +275,10 @@ func Info(message string, a ...interface{}) {
LogCaller(INFO, 1, nil, message, a...)
}
+func Output(message string, a ...interface{}) {
+ LogCaller(OUT, 1, nil, message, a...)
+}
+
func InfoExc(err error, message string, a ...interface{}) {
LogCaller(INFO, 1, err, message, a...)
}
@@ -281,6 +311,10 @@ func SecWarn(message string, a ...interface{}) {
LogCaller(SECWARN, 1, nil, message, a...)
}
+func ErrOut(message string, a ...interface{}) {
+ LogCaller(ERROUT, 1, nil, message, a...)
+}
+
func Error(message string, a ...interface{}) {
LogCaller(ERROR, 1, nil, message, a...)
}
diff --git a/overlays/host/etc/dhcp/dhcpd.conf.ww b/overlays/host/etc/dhcp/dhcpd.conf.ww
index 4a9637f7..03fe54cb 100644
--- a/overlays/host/etc/dhcp/dhcpd.conf.ww
+++ b/overlays/host/etc/dhcp/dhcpd.conf.ww
@@ -1,4 +1,4 @@
-{{if .Dhcp.Enabled -}}
+{{if $.Dhcp.Enabled -}}
# This file is autogenerated by warewulf
# Host: {{.BuildHost}}
# Time: {{.BuildTime}}
@@ -15,8 +15,29 @@ option space ipxe;
option ipxe.no-pxedhcp code 176 = unsigned integer 8;
option ipxe.no-pxedhcp 1;
+option space PXE;
+option PXE.mtftp-ip code 1 = ip-address;
+option PXE.mtftp-cport code 2 = unsigned integer 16;
+option PXE.mtftp-sport code 3 = unsigned integer 16;
+option PXE.mtftp-tmout code 4 = unsigned integer 8;
+option PXE.mtftp-delay code 5 = unsigned integer 8;
+
option architecture-type code 93 = unsigned integer 16;
+{{- if $.Warewulf.GrubBoot }}
+if substring (option vendor-class-identifier, 0, 9) = "PXEClient" {
+ next-server {{ $.Ipaddr }};
+ if substring (option vendor-class-identifier, 15, 5) = "00000" {
+ # pure BIOS clients will get iPXE configuration
+ filename "http://{{$.Ipaddr}}:{{$.Warewulf.Port}}/ipxe/${mac:hexhyp}";
+ } else {
+ # EFI clients will get shim and grub instead
+ filename "warewulf/shim.efi";
+ }
+} elsif substring (option vendor-class-identifier, 0, 10) = "HTTPClient" {
+ filename "http://{{$.Ipaddr}}:{{$.Warewulf.Port}}/efiboot/shim.efi";
+}
+{{- else }}
if exists user-class and option user-class = "iPXE" {
filename "http://{{$.Ipaddr}}:{{$.Warewulf.Port}}/ipxe/${mac:hexhyp}";
} else {
@@ -24,39 +45,32 @@ if exists user-class and option user-class = "iPXE" {
if option architecture-type = {{ $type }} {
filename "/warewulf/{{ basename $name }}";
}
-{{ end }}
+{{- end }}{{/* range IpxeBinaries */}}
}
+{{- end }}{{/* BootMethod */}}
-{{if eq .Dhcp.Template "static" -}}
subnet {{$.Network}} netmask {{$.Netmask}} {
- next-server {{$.Ipaddr}};
+ max-lease-time 120;
+ range {{$.Dhcp.RangeStart}} {{$.Dhcp.RangeEnd}};
+ next-server {{.Ipaddr}};
}
-{{range $nodes := .AllNodes}}
+{{- range $nodes := $.AllNodes}}
{{- range $netname, $netdevs := $nodes.NetDevs}}
-host {{$nodes.Id.Get}}-{{if $netdevs.Device.Defined}}{{$netdevs.Device.Get}}{{else}}{{$netname}}{{end}} {
+host {{$nodes.Id.Get}}-{{$netdevs.Device.Get}}
+{
+ {{- if $netdevs.Primary.GetB}}
{{- if $netdevs.Hwaddr.Defined}}
hardware ethernet {{$netdevs.Hwaddr.Get}};
- {{- end}}
+ {{- end }}
{{- if $netdevs.Ipaddr.Defined}}
fixed-address {{$netdevs.Ipaddr.Get}};
- {{- end}}
- {{- if $netdevs.Primary.GetB}}
+ {{- end }}
option host-name "{{$nodes.Id.Get}}";
- {{else}}
- option host-name "{{$nodes.Id.Get}}-{{if $netdevs.Device.Defined}}{{$netdevs.Device.Get}}{{else}}{{$netname}}{{end}}";
- {{- end}}
-}
-{{end -}}
-{{end -}}
-
-{{else -}}
-subnet {{$.Network}} netmask {{$.Netmask}} {
- max-lease-time 120;
- range {{$.Dhcp.RangeStart}} {{$.Dhcp.RangeEnd}};
- next-server {{$.Ipaddr}};
+ {{- end }}
}
-{{end}}
+{{end -}}{{/* range NetDevs */}}
+{{end -}}{{/* range AllNodes */}}
{{- else}}
{{abort}}
-{{- end}}
+{{- end}}{{/* dhcp enabled */}}
diff --git a/overlays/host/etc/dhcpd.conf.ww b/overlays/host/etc/dhcpd.conf.ww
new file mode 120000
index 00000000..e5b73f7b
--- /dev/null
+++ b/overlays/host/etc/dhcpd.conf.ww
@@ -0,0 +1 @@
+dhcp/dhcpd.conf.ww
\ No newline at end of file
diff --git a/overlays/wwinit/warewulf/init.d/80-wwclient b/overlays/wwinit/warewulf/init.d/80-wwclient
new file mode 100644
index 00000000..38347110
--- /dev/null
+++ b/overlays/wwinit/warewulf/init.d/80-wwclient
@@ -0,0 +1,7 @@
+#!/bin/sh
+. /warewulf/config
+# Only start if the systemd is no available
+test -d /run/systemd/system && exit 0
+echo "Starting wwclient"
+nohup /warewulf/wwclient >/var/log/wwclient.log 2>&1 </dev/null &
+
diff --git a/userdocs/conf.py b/userdocs/conf.py
index b03f4737..92c28443 100644
--- a/userdocs/conf.py
+++ b/userdocs/conf.py
@@ -14,7 +14,7 @@ release = 'main'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
-extensions = ['sphinx.ext.autosectionlabel']
+extensions = ['sphinx.ext.autosectionlabel','sphinx.ext.graphviz']
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
diff --git a/userdocs/contents/boot-management.rst b/userdocs/contents/boot-management.rst
new file mode 100644
index 00000000..4ba87aa5
--- /dev/null
+++ b/userdocs/contents/boot-management.rst
@@ -0,0 +1,127 @@
+===============
+Boot Management
+===============
+
+Instead of the iPXE starter a combination of `shim and GRUB <https://www.suse.com/c/uefi-secure-boot-details/>`_
+can be used with the advantage that secure boot can be used. That means
+that only the signed kernel of a distribution can be booted. This can
+be a huge security benefit for some scenarios.
+
+In order to enable the grub boot method it has to be enabled in `warewulf.conf`.
+Nodes which are not known to warewulf will then booted with the shim/grub from
+the host on which warewulf is installed.
+
+
+Boot process
+============
+
+The boot process can be summarized with following diagram
+
+.. graphviz::
+
+ digraph foo {
+ node [shape=box];
+ subgraph boot {
+ "EFI" [label="EFI",row=boot];
+ "Shim" [label="Shim",row=boot];
+ "Grub" [label="Grub",row=boot];
+ "Kernel" [label="kernel",row=boot];
+ EFI -> Shim[label="Check for Microsoft signature"];
+ Shim -> Grub[label="Check for Distribution signature"];
+ Grub->Kernel[label="Check for Distribution or MOK signature"];
+ }
+ }
+
+If secure boot is enabled at every step a signature is checked and the boot process
+will fail if this check fails. Also at moment a Shim only includes the key
+of one Distribution, which means that every Distribution needs a separate
+`shim` and `grub` executable and warewulf extracts these binaries from
+the containers.
+
+For the case when the node is unknown to warewulf or
+can't be identified during the `tFTP`` boot phase, the shim/grub binaries of
+the host in which warewulf is running will be used.
+
+PXE/tFTP boot
+-------------
+
+The standard network boot process with `grub` and `iPXE` has following steps
+
+.. graphviz::
+
+ digraph G{
+ node [shape=box];
+ compound=true;
+ edge [label2node=true]
+ bios [shape=record label="{Bios | boots filename from nextboot per tFTP}"]
+ subgraph cluster0 {
+ label="iPXE boot"
+ iPXE;
+ ipxe_cfg [shape=record label="{ipxe.cfg|generated for indivdual node}"];
+ iPXE -> ipxe_cfg [label="http"];
+ }
+ bios->iPXE [lhead=cluster0,label="filename=iPXE.efi"];
+ bios->shim [lhead=cluster1,label="filename=shim.efi"];
+ subgraph cluster1{
+ label="Grub boot"
+ shim[shape=record label="{shim.efi|from ww4 host}"];
+ grub[shape=record label="{grubx64.efi | name hardcoded in shim.efi|from ww4 host}"]
+ shim->grub[label="tFTP"];
+ grubcfg[shape=record label="{grub.cfg|static under tFTP root}"];
+ grub->grubcfg[label="tFTP"];
+ }
+ kernel [shape=record label="{kernel|ramdisk (root fs)|wwinit overlay}|extracted from node container"];
+ grubcfg->kernel[ltail=cluster1,label="http"];
+ ipxe_cfg->kernel[ltail=cluster0,label="http"];
+ }
+
+As the tFTP server is independent of warewulf, the `shim` and `grub` EFI binaries
+for the tFTP server are copied from the host on which warewulf is running.
+This means that for secure boot the distributor e.g. SUSE of the container in
+the `default` profile must match the distributor of the container which then
+also must be signed by the SUSE key.
+
+http boot
+---------
+
+Modern EFI systems have the possibility to directly boot per http. The flow diagram
+is the following:
+
+.. graphviz::
+
+ digraph G{
+ node [shape=box];
+ efi [shape=record label="{EFI|boots from URI defined in filename}"];
+ shim [shape=record label="{shim.efi|replaces shim.efi with grubx64.efi in URI|extracted from node container}"];
+ grub [shape=record label="{grub.efi|checks for grub.cfg|extracted from node container}"]
+ kernel [shape=record label="{kernel|ramdisk (root fs)|wwinit overlay}|extracted from node container"];
+ efi->shim [label="http"];
+ shim->grub [label="http"];
+ grub->kernel [label="http"];
+ }
+
+The main difference is that the initial `shim.efi` and `grub.efi` are delivered by http with warewulf
+and are taken directly from the container assigned to the node. This means that secure boot will work
+for containers from different distributors.
+
+Install shim and efi
+--------------------
+
+The `shim.efi` and `grub.efi` must be installed via the package manager directly into the container.
+
+Install on SUSE systems
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: console
+
+ # wwctl container shell leap15.5
+ [leap15.5] Warewulf> zypper install grub2 shim
+
+
+Install on EL system
+^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: console
+
+ # wwctl container shell rocky9
+ [rocky9] Warewulf> dnf install shim-x64.x86_64 grub2-pc.x86_64
\ No newline at end of file
diff --git a/userdocs/contents/security.rst b/userdocs/contents/security.rst
index 1f585de1..b0bb42a8 100644
--- a/userdocs/contents/security.rst
+++ b/userdocs/contents/security.rst
@@ -55,7 +55,7 @@ when a user lands on a compute node, there is generally nothing
stopping them from spoofing a provision request and downloading the
provisioned raw materials for inspection.
-In Warewulf there are two ways to secure the provisioning process:
+In Warewulf there are ways multiple to secure the provisioning process:
#. The provisioning connections and transfers are not secure due to
not being able to manage a secure root of trust through a PXE
@@ -77,6 +77,11 @@ In Warewulf there are two ways to secure the provisioning process:
provision and communicate with requests from that system matching
that asset tag.
+#. When the nodes are booted via `shim` and `grub` Secure Boot can be
+ enabled. This means that the nodes only boot the kernel which is
+ provided by the distributor and also custom complied modules can't
+ be loaded.
+
Summary
=======
diff --git a/userdocs/index.rst b/userdocs/index.rst
index 71fb9da5..9d7a4b8c 100644
--- a/userdocs/index.rst
+++ b/userdocs/index.rst
@@ -18,6 +18,7 @@ Welcome to the Warewulf User Guide!
Warewulf Initialization <contents/initialization>
Container Management <contents/containers>
Kernel Management <contents/kernel>
+ Boot Management <contents/boot-management>
Node Configuration <contents/nodeconfig>
Node Profiles <contents/profiles>
Warewulf Overlays <contents/overlays>
diff --git a/warewulf.spec.in b/warewulf.spec.in
index 318703fb..e71a71a4 100644
--- a/warewulf.spec.in
+++ b/warewulf.spec.in
@@ -139,6 +139,7 @@ yq e '
%config(noreplace) %{_sysconfdir}/warewulf/wwapi*.conf
%config(noreplace) %{_sysconfdir}/warewulf/examples
%config(noreplace) %{_sysconfdir}/warewulf/ipxe
+%config(noreplace) %{_sysconfdir}/warewulf/grub
%config(noreplace) %attr(0640,-,-) %{_sysconfdir}/warewulf/nodes.conf
%{_sysconfdir}/bash_completion.d/wwctl