forked from pool/warewulf4
- 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
1490 lines
48 KiB
Diff
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
|
|
|