diff --git a/CHANGELOG.md b/CHANGELOG.md index 70280717..93304050 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,6 +124,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support importing containers with symlinked `/bin/sh` #797 - Don't panic on malformed passwd #527 +- first container imported container is added to the default profile +- grub in combination can now be set as boot method with `warewulf.grubboot: true` in + `warewulf.conf`. For unknown nodes `grub.efi` and `shim.efi` will be extracted from + the host running warewulf. If node has container it will get these binaries from the + container image. + +- Added support for booting nodes with grub. Enable this behavior using + warewulf.grubboot: true in warewulf.conf. For unknown nodes, grub.efi + and shim.efi are extracted from the Warewulf host. If the booted node + has a container these binaries are extracted from the container image. ## [4.4.0] 2023-01-18 ### Added 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..916aff84 --- /dev/null +++ b/etc/grub/chainload.ww @@ -0,0 +1,18 @@ +# This file is autogenerated by warewulf +# Host: {{ .BuildHost }} +# Time: {{ .BuildTime }} +# Source: {{ .BuildSource }} +echo "================================================================================" +echo "Warewulf v4 now booting with grub" +echo +echo "Warewulf Controller: {{.Ipaddr}}" +echo "Chain loading specific grub.cfg" +uri="(http,{{.Ipaddr}}:{{.Warewulf.Port}})/efiboot/grub.cfg" +echo $uri +configfile $uri +echo "MESSAGE: This node is 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 diff --git a/etc/grub/grub.cfg.ww b/etc/grub/grub.cfg.ww new file mode 100644 index 00000000..79357fae --- /dev/null +++ b/etc/grub/grub.cfg.ww @@ -0,0 +1,29 @@ +echo "================================================================================" +echo "Warewulf v4 now booting with grub: {{.Fqdn}} ({{.Hwaddr}})" +echo "================================================================================" +uri="(http,{{.Ipaddr}}:9873)/provision/${net_default_mac}?assetkey=" +kernel="${uri}&stage=kernel" +container="${uri}&stage=container&compress=gz" +system="${uri}&stage=system&compress=gz" +runtime="${uri}&stage=runtime&compress=gz" +echo "Warewulf Controller: {{.Ipaddr}}" +{{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 +echo +boot +else +echo "MESSAGE: This node is 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 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 08c27b19..053d3a63 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: @@ -306,6 +307,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..b04a8a91 100644 --- a/internal/pkg/warewulfd/provision.go +++ b/internal/pkg/warewulfd/provision.go @@ -3,7 +3,9 @@ package warewulfd import ( "bytes" "errors" + "fmt" "net/http" + "os" "path" "strconv" "strings" @@ -17,7 +19,7 @@ import ( "github.com/hpcng/warewulf/internal/pkg/wwlog" ) -type iPxeTemplate struct { +type templateVars struct { Message string WaitTime string Hostname string @@ -32,13 +34,10 @@ type iPxeTemplate struct { KernelOverride string } -var status_stages = map[string]string{ - "ipxe": "IPXE", - "kernel": "KERNEL", - "kmods": "KMODS_OVERLAY", - "system": "SYSTEM_OVERLAY", - "runtime": "RUNTIME_OVERLAY"} - +/* +Handles all the http request for warewulfd, different stages are encoded +in the GET request. +*/ func ProvisionSend(w http.ResponseWriter, req *http.Request) { conf := warewulfconf.Get() @@ -59,8 +58,17 @@ 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 + var stage_file string = "" + updateSentDB := true // TODO: when module version is upgraded to go1.18, should be 'any' type var tmpl_data interface{} @@ -83,13 +91,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,8 +108,20 @@ 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 DBGetWWinit(node.Id.Get()) { + DBReset(node.Id.Get()) + } + if DBSize(node.Id.Get()) == 0 { + fd, err := os.Open(path.Join(path.Join(conf.Paths.Tftpdir, "warewulf", "grub.cfg"))) + if err != nil { + wwlog.Warn("no grub.cfg detected for potential tftp boot node: %s", node) + } else { + defer fd.Close() + DBAddImage(node.Id.Get(), "grub.cfg", fd) + } + + } if node.Kernel.Override.Defined() { stage_file = kernel.KernelImage(node.Kernel.Override.Get()) } else if node.ContainerName.Defined() { @@ -137,7 +157,6 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) { } else { context = rinfo.stage } - stage_file, err = getOverlayFile( node, context, @@ -154,6 +173,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) @@ -187,11 +264,12 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "text") w.Header().Set("Content-Length", strconv.Itoa(buf.Len())) + reader := bytes.NewReader(buf.Bytes()) + DBAddImage(node.Id.Get(), stage_file, reader) _, err = buf.WriteTo(w) if err != nil { wwlog.ErrorExc(err, "") } - wwlog.Send("%15s: %s", node.Id.Get(), stage_file) } else { @@ -210,7 +288,7 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusNotFound) } - err = sendFile(w, req, stage_file, node.Id.Get()) + err = sendFile(w, req, stage_file, node.Id.Get(), updateSentDB) if err != nil { wwlog.ErrorExc(err, "") return 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/sentDB.go b/internal/pkg/warewulfd/sentDB.go new file mode 100644 index 00000000..ee64e4c5 --- /dev/null +++ b/internal/pkg/warewulfd/sentDB.go @@ -0,0 +1,144 @@ +package warewulfd + +import ( + "crypto/sha256" + "fmt" + "io" + "path" + "sync" + + "github.com/hpcng/warewulf/internal/pkg/wwlog" +) + +/* +store the sent files name and its checksum +*/ +type SentFiles struct { + Files []File `json:"files:"` + sha256sum [32]byte + Sha256hex string `json:"sha256"` + wwinit bool +} + +type File struct { + FileName string `json:"file name"` + sha256sum [32]byte + Sha256hex string `json:"sha256"` +} + +/* +Database for the checksum of sent files, this +values can be used for measured boot in combination with +TPM devicces +*/ +var sentDB map[string]*SentFiles + +// mutex for locking the map +var mu sync.Mutex + +func init() { + sentDB = map[string]*SentFiles{} +} + +/* +Adds the image with the name to the database +*/ +func DBAddImage(node string, fileName string, content io.ReadSeeker) { + wwlog.Debug("adding file %s for node %s to sentDB", node, fileName) + hasher := sha256.New() + sent := File{ + FileName: path.Base(fileName), + } + if _, err := io.Copy(hasher, content); err != nil { + wwlog.SecWarn("couldn't create hash of %s for %s", fileName, node) + return + } + copy(sent.sha256sum[:], hasher.Sum(nil)) + sent.Sha256hex = fmt.Sprintf("%x", sent.sha256sum) + mu.Lock() + if _, ok := sentDB[node]; !ok { + sentDB[node] = new(SentFiles) + } + sentDB[node].Files = append(sentDB[node].Files, sent) + for i := 0; i < sha256.Size; i++ { + sentDB[node].sha256sum[i] = 1 + } + for _, sntFile := range sentDB[node].Files { + sentDB[node].sha256sum = sha256.Sum256(append(sentDB[node].sha256sum[:], sntFile.sha256sum[:]...)) + } + sentDB[node].Sha256hex = fmt.Sprintf("%x", sentDB[node].sha256sum) + mu.Unlock() + hasher.Reset() +} + +/* +Get the final sum of all the hashed files +*/ +func DBGetSum(node string) (ret [sha256.Size]byte) { + mu.Lock() + defer mu.Unlock() + if sentNode, ok := sentDB[node]; ok { + ret = sentNode.sha256sum + return + } + ret = [sha256.Size]byte{0} + return +} + +/* +Reset the database for a single node +*/ +func DBReset(node string) { + mu.Lock() + if _, ok := sentDB[node]; !ok { + sentDB[node] = new(SentFiles) + } + sentDB[node] = new(SentFiles) + mu.Unlock() +} + +/* +Reset the database +*/ +func DBResetAll() { + sentDB = make(map[string]*SentFiles) +} + +/* +Get the size of the DB +*/ +func DBSize(node string) int { + mu.Lock() + if _, ok := sentDB[node]; !ok { + sentDB[node] = new(SentFiles) + } + size := len(sentDB[node].Files) + mu.Unlock() + return size +} + +/* +Check if wwinit was sent +*/ +func DBGetWWinit(node string) bool { + mu.Lock() + if _, ok := sentDB[node]; !ok { + sentDB[node] = new(SentFiles) + } + ret := sentDB[node].wwinit + mu.Unlock() + return ret +} + +/* +Mark that wwinit was sent +*/ + +func DBWWinitSent(node string) { + mu.Lock() + if _, ok := sentDB[node]; !ok { + sentDB[node] = new(SentFiles) + } + sentDB[node].wwinit = true + mu.Unlock() +} diff --git a/internal/pkg/warewulfd/sentstatus.go b/internal/pkg/warewulfd/sentstatus.go new file mode 100644 index 00000000..36350367 --- /dev/null +++ b/internal/pkg/warewulfd/sentstatus.go @@ -0,0 +1,34 @@ +package warewulfd + +import ( + "encoding/json" + "net/http" + + "github.com/hpcng/warewulf/internal/pkg/wwlog" + "github.com/pkg/errors" +) + +func sentStatusJSON() ([]byte, error) { + wwlog.Debug("Request for node sent status data...") + + ret, err := json.MarshalIndent(sentDB, "", " ") + if err != nil { + return ret, errors.Wrap(err, "could not marshal JSON data from status structure") + } + + return ret, nil + +} + +func SentStatus(w http.ResponseWriter, req *http.Request) { + status, err := sentStatusJSON() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + _, err = w.Write(status) + if err != nil { + wwlog.Warn("Could not send sent status JSON: %s", err) + } +} diff --git a/internal/pkg/warewulfd/sentsums_test.go b/internal/pkg/warewulfd/sentsums_test.go new file mode 100644 index 00000000..41fcfe00 --- /dev/null +++ b/internal/pkg/warewulfd/sentsums_test.go @@ -0,0 +1,23 @@ +package warewulfd + +import ( + "bytes" + "testing" +) + +func Test_SumOne(t *testing.T) { + firstText := `Scalable. Flexible. Today, Warewulf unites the ecosystem with the ability to provision containers directly to the bare metal hardware at massive scale, simplistically while retaining massive flexibility.` + secondText := `Being open source for over two-decades, and pioneering the concept of stateless node management, Warewulf is among the most successful HPC cluster platforms in the industry with support from OpenHPC, contributors around the world, and usage from every industry.` + DBAddImage("n01", "firstText", bytes.NewReader([]byte(firstText))) + DBAddImage("n01", "secondText", bytes.NewReader([]byte(secondText))) + if sum_n02 := DBGetSum("n02"); sum_n02 != [32]byte{} { + t.Errorf("Sum of second entry must be zero") + } + if sum_n01 := DBGetSum("n01"); sum_n01 == [32]byte{} { + t.Errorf("Sum of entry must not be zero") + } + DBResetAll() + if sum_n01 := DBGetSum("n01"); sum_n01 != [32]byte{} { + t.Errorf("Sum after reset must be zero") + } +} diff --git a/internal/pkg/warewulfd/util.go b/internal/pkg/warewulfd/util.go index 3290a678..cefa1cab 100644 --- a/internal/pkg/warewulfd/util.go +++ b/internal/pkg/warewulfd/util.go @@ -1,8 +1,11 @@ package warewulfd import ( + "bufio" + "io" "net/http" "os" + "strings" "github.com/hpcng/warewulf/internal/pkg/node" nodepkg "github.com/hpcng/warewulf/internal/pkg/node" @@ -15,7 +18,7 @@ func sendFile( w http.ResponseWriter, req *http.Request, filename string, - sendto string) error { + sendto string, updateSentDB bool) error { fd, err := os.Open(filename) if err != nil { @@ -36,7 +39,14 @@ func sendFile( filename, stat.ModTime(), fd) - + // seek back + _, err = fd.Seek(0, io.SeekStart) + if err != nil { + wwlog.Warn("couldn't seek in file: %s", filename) + } + if updateSentDB { + DBAddImage(sendto, filename, fd) + } wwlog.Send("%15s: %s", sendto, filename) return nil @@ -70,3 +80,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..4c480ebe 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,27 @@ 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) + wwHandler.HandleFunc("/sentstatus", SentStatus) conf := warewulfconf.Get() daemonPort := conf.Warewulf.Port wwlog.Serv("Starting HTTPD REST service on port %d", daemonPort) - err = http.ListenAndServe(":"+strconv.Itoa(daemonPort), nil) + err = http.ListenAndServe(":"+strconv.Itoa(daemonPort), &slashFix{&wwHandler}) if err != nil { return errors.Wrap(err, "Could not start listening service") } 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 `_ +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 Container Management Kernel Management + Boot Management Node Configuration Node Profiles Warewulf 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