diff --git a/boot-tar.go b/boot-tar.go new file mode 100644 index 0000000..16e5b05 --- /dev/null +++ b/boot-tar.go @@ -0,0 +1,176 @@ +package main + +import ( + "archive/tar" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" +) + +func rmTempFile(f *os.File) { + f.Close() + if err := os.Remove(f.Name()); err != nil { + log.Print("failed to remove ", f.Name(), ": ", err) + } +} + +func buildBootTar(out io.Writer, ctx *renderContext) (err error) { + grubCfg, err := ioutil.TempFile(os.TempDir(), "grub.cfg-") + if err != nil { + return + } + defer rmTempFile(grubCfg) + + _, err = grubCfg.WriteString(` +search --no-floppy --set=root --part-label boot + +insmod all_video +set timeout=3 + +set bootdev=PARTNAME=boot + +menuentry "Direktil" { + linux /current/vmlinuz direktil.boot=$bootdev + initrd /current/initrd +} +`) + if err != nil { + return + } + grubCfg.Close() + + // FIXME including in grub memdisk for now... + kernelPath, err := ctx.distFetch("kernels", ctx.Group.Kernel) + initrdPath, err := ctx.distFetch("initrd", ctx.Group.Initrd) + + grubMk := func(format string) ([]byte, error) { + grubOut, err := ioutil.TempFile(os.TempDir(), "grub.img-") + if err != nil { + return nil, err + } + defer rmTempFile(grubOut) + if err := grubOut.Close(); err != nil { + return nil, err + } + + cmd := exec.Command("grub-mkstandalone", + "--format="+format, + "--output="+grubOut.Name(), + "boot/grub/grub.cfg="+grubCfg.Name(), + "current/vmlinuz="+kernelPath, + "current/initrd="+initrdPath, + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return nil, err + } + return ioutil.ReadFile(grubOut.Name()) + } + + arch := tar.NewWriter(out) + defer arch.Close() + + archAdd := func(path string, ba []byte) (err error) { + err = arch.WriteHeader(&tar.Header{Name: path, Size: int64(len(ba))}) + if err != nil { + return + } + _, err = arch.Write(ba) + return + } + + ba, err := grubMk("x86_64-efi") + if err != nil { + return err + } + + err = archAdd("EFI/boot/bootx64.efi", ba) + if err != nil { + return + } + + if false { + // TODO + ba, err = grubMk("i386-pc") + if err != nil { + return err + } + + arch.WriteHeader(&tar.Header{ + Name: "grub.img", + Size: int64(len(ba)), + }) + arch.Write(ba) + } + + // config + cfgBytes, cfg, err := ctx.Config() + if err != nil { + return err + } + + archAdd("config.yaml", cfgBytes) + + // kernel and initrd + type distCopy struct { + Src []string + Dst string + } + + copies := []distCopy{ + // XXX {Src: []string{"kernels", ctx.Group.Kernel}, Dst: "current/vmlinuz"}, + // XXX {Src: []string{"initrd", ctx.Group.Initrd}, Dst: "current/initrd"}, + } + + // layers + for _, layer := range cfg.Layers { + layerVersion := ctx.Group.Versions[layer] + if layerVersion == "" { + return fmt.Errorf("layer %q not mapped to a version", layer) + } + + copies = append(copies, + distCopy{ + Src: []string{"layers", layer, layerVersion}, + Dst: filepath.Join("current", "layers", layer+".fs"), + }) + } + + for _, copy := range copies { + outPath, err := ctx.distFetch(copy.Src...) + if err != nil { + return err + } + + f, err := os.Open(outPath) + if err != nil { + return err + } + + defer f.Close() + + stat, err := f.Stat() + if err != nil { + return err + } + + if err = arch.WriteHeader(&tar.Header{ + Name: copy.Dst, + Size: stat.Size(), + }); err != nil { + return err + } + + _, err = io.Copy(arch, f) + if err != nil { + return err + } + } + + return nil +} diff --git a/cas-cleaner.go b/cas-cleaner.go index 507e4d9..a3be6ff 100644 --- a/cas-cleaner.go +++ b/cas-cleaner.go @@ -31,7 +31,10 @@ func cleanCAS() error { activeTags := make([]string, len(cfg.Hosts)) for i, host := range cfg.Hosts { - ctx := newRenderContext(host, cfg) + ctx, err := newRenderContext(host, cfg) + if err != nil { + return err + } tag, err := ctx.Tag() if err != nil { diff --git a/http.go b/http.go index c6ddf08..4dced7a 100644 --- a/http.go +++ b/http.go @@ -93,12 +93,7 @@ func serveHosts(w http.ResponseWriter, r *http.Request) { return } - hostNames := make([]string, len(cfg.Hosts)) - for i, host := range cfg.Hosts { - hostNames[i] = host.Name - } - - renderJSON(w, hostNames) + renderJSON(w, cfg.Hosts) } func serveHost(w http.ResponseWriter, r *http.Request) { @@ -137,7 +132,12 @@ func serveHost(w http.ResponseWriter, r *http.Request) { } func renderHost(w http.ResponseWriter, r *http.Request, what string, host *clustersconfig.Host, cfg *clustersconfig.Config) { - ctx := newRenderContext(host, cfg) + ctx, err := newRenderContext(host, cfg) + if err != nil { + log.Printf("host %s: %s: failed to render: %v", what, host.Name, err) + http.Error(w, "", http.StatusServiceUnavailable) + return + } switch what { case "ipxe": @@ -148,8 +148,6 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *clust w.Header().Set("Content-Type", "application/octet-stream") } - var err error - switch what { case "ipxe": err = renderIPXE(w, ctx) @@ -163,6 +161,9 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *clust case "boot.iso": err = renderCtx(w, r, ctx, "boot.iso", buildBootISO) + case "boot.tar": + err = renderCtx(w, r, ctx, "boot.tar", buildBootTar) + case "config": err = renderConfig(w, r, ctx) diff --git a/install-on-metal.sh b/install-on-metal.sh new file mode 100755 index 0000000..fa1111c --- /dev/null +++ b/install-on-metal.sh @@ -0,0 +1,37 @@ +#! /bin/sh + +if [ $# -ne 2 ]; then + echo "USAGE: $0 " +fi + +dev=$1 +tar_url=$2 + +: ${MP:=/mnt} + +set -ex + +[[ $dev =~ nvme ]] && + devp=${dev}p || + devp=${dev} + +vgdisplay storage || { + sgdisk --clear $dev + sgdisk \ + --new=0:4096:+2G --typecode=0:EF00 -c 0:boot \ + --new=0:0:+2M --typecode=0:EF02 -c 0:BIOS-BOOT \ + --new=0:0:0 --typecode=0:FFFF -c 0:data \ + --hybrid=1:2 \ + --print $dev + + mkfs.vfat -n DKLBOOT ${devp}1 + + pvcreate ${devp}3 + vgcreate storage ${devp}3 +} + +while umount $MP; do true; done + +mount -t vfat ${devp}1 $MP +curl $tar_url |tar xv -C $MP +umount $MP diff --git a/render-context.go b/render-context.go index 02dbd14..4cff7d9 100644 --- a/render-context.go +++ b/render-context.go @@ -30,9 +30,16 @@ type renderContext struct { clusterConfig *clustersconfig.Config } -func newRenderContext(host *clustersconfig.Host, cfg *clustersconfig.Config) *renderContext { - group := cfg.Group(host.Group) +func newRenderContext(host *clustersconfig.Host, cfg *clustersconfig.Config) (*renderContext, error) { cluster := cfg.Cluster(host.Cluster) + if cluster == nil { + return nil, fmt.Errorf("no cluster named %q", host.Cluster) + } + + group := cfg.Group(host.Group) + if group == nil { + return nil, fmt.Errorf("no group named %q", host.Group) + } vars := make(map[string]interface{}) @@ -55,7 +62,7 @@ func newRenderContext(host *clustersconfig.Host, cfg *clustersconfig.Config) *re StaticPodsTemplate: cfg.StaticPodsTemplate(group.StaticPods), clusterConfig: cfg, - } + }, nil } func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) {