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 `_ +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