Compare commits
15 Commits
wip
...
99d8c7598e
| Author | SHA1 | Date | |
|---|---|---|---|
| 99d8c7598e | |||
| 66f391de19 | |||
| 07a9df2952 | |||
| c8ad4c20a0 | |||
| 6d26fd9fa6 | |||
| 1a4d908bef | |||
| 78aa9ba20d | |||
| b0e84f6aa8 | |||
| 7a6310c93e | |||
| e89b164581 | |||
| 1ad9785d07 | |||
| 06a87a6d07 | |||
| d37c4c2f13 | |||
| 629bb21f12 | |||
| 6d9499ebb1 |
+7
-5
@@ -1,6 +1,7 @@
|
|||||||
from novit.tech/direktil/dkl:bbea9b9 as dkl
|
from novit.tech/direktil/dkl:v1.2.0 as dkl
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
from golang:1.25.5-trixie as build
|
from golang:1.26.4-trixie as build
|
||||||
|
|
||||||
run apt-get update && apt-get install -y git
|
run apt-get update && apt-get install -y git
|
||||||
|
|
||||||
@@ -26,10 +27,11 @@ from debian:trixie
|
|||||||
entrypoint ["/bin/dkl-local-server"]
|
entrypoint ["/bin/dkl-local-server"]
|
||||||
|
|
||||||
env _uncache=1
|
env _uncache=1
|
||||||
run apt-get update \
|
run --mount=type=cache,id=debian-trixie-apt,target=/var/cache/apt \
|
||||||
&& yes |apt-get install -y genisoimage gdisk dosfstools util-linux udev binutils systemd \
|
apt update \
|
||||||
|
&& yes |apt install -y genisoimage gdisk dosfstools util-linux udev binutils systemd \
|
||||||
grub2 grub-pc-bin grub-efi-amd64-bin ca-certificates curl openssh-client qemu-utils wireguard-tools \
|
grub2 grub-pc-bin grub-efi-amd64-bin ca-certificates curl openssh-client qemu-utils wireguard-tools \
|
||||||
&& apt-get clean
|
erofs-utils erofsfuse cryptsetup systemd-boot-efi mtools
|
||||||
|
|
||||||
copy --from=dkl /bin/dkl /bin/dls /bin/
|
copy --from=dkl /bin/dkl /bin/dls /bin/
|
||||||
copy --from=build /src/dist/ /bin/
|
copy --from=build /src/dist/ /bin/
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -12,18 +13,16 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/pierrec/lz4"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildBootImg(out io.Writer, ctx *renderContext) (err error) {
|
func buildBootImg(out io.Writer, ctx *renderContext, uki bool, cmdline string) (err error) {
|
||||||
bootImg, err := os.CreateTemp(os.TempDir(), "boot.img-")
|
bootImg, err := os.CreateTemp(os.TempDir(), "boot.img-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rmTempFile(bootImg)
|
defer rmTempFile(bootImg)
|
||||||
|
|
||||||
err = setupBootImage(bootImg, ctx)
|
err = setupBootImage(bootImg, ctx, uki, cmdline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -34,21 +33,10 @@ func buildBootImg(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildBootImgLZ4(out io.Writer, ctx *renderContext) (err error) {
|
func buildBootImgGZ(out io.Writer, ctx *renderContext, uki bool, cmdline string) (err error) {
|
||||||
lz4Out := lz4.NewWriter(out)
|
|
||||||
|
|
||||||
if err = buildBootImg(lz4Out, ctx); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lz4Out.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildBootImgGZ(out io.Writer, ctx *renderContext) (err error) {
|
|
||||||
gzOut := gzip.NewWriter(out)
|
gzOut := gzip.NewWriter(out)
|
||||||
|
|
||||||
if err = buildBootImg(gzOut, ctx); err != nil {
|
if err = buildBootImg(gzOut, ctx, uki, cmdline); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +44,7 @@ func buildBootImgGZ(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildBootImgQemuConvert(out io.Writer, ctx *renderContext, format string) (err error) {
|
func buildBootImgQemuConvert(out io.Writer, ctx *renderContext, format string, uki bool, cmdline string) (err error) {
|
||||||
imgPath, err := func() (imgPath string, err error) {
|
imgPath, err := func() (imgPath string, err error) {
|
||||||
bootImg, err := os.CreateTemp(os.TempDir(), "boot.img-")
|
bootImg, err := os.CreateTemp(os.TempDir(), "boot.img-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -64,7 +52,7 @@ func buildBootImgQemuConvert(out io.Writer, ctx *renderContext, format string) (
|
|||||||
}
|
}
|
||||||
defer rmTempFile(bootImg)
|
defer rmTempFile(bootImg)
|
||||||
|
|
||||||
err = setupBootImage(bootImg, ctx)
|
err = setupBootImage(bootImg, ctx, uki, cmdline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -100,37 +88,20 @@ func buildBootImgQemuConvert(out io.Writer, ctx *renderContext, format string) (
|
|||||||
io.Copy(out, img)
|
io.Copy(out, img)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
func qemuImgBootImg(format string) func(out io.Writer, ctx *renderContext) (err error) {
|
func qemuImgBootImg(format string, uki bool, cmdline string) func(out io.Writer, ctx *renderContext) (err error) {
|
||||||
return func(out io.Writer, ctx *renderContext) (err error) {
|
return func(out io.Writer, ctx *renderContext) (err error) {
|
||||||
return buildBootImgQemuConvert(out, ctx, format)
|
return buildBootImgQemuConvert(out, ctx, format, uki, cmdline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var grubSupportVersion = flag.String("grub-support", "1.1.0", "GRUB support version")
|
func setupBootImage(bootImg *os.File, ctx *renderContext, uki bool, cmdline string) (err error) {
|
||||||
|
if uki {
|
||||||
func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
|
err = baseDiskUki(bootImg, 192)
|
||||||
path, err := distFetch("grub-support", *grubSupportVersion)
|
} else {
|
||||||
|
err = baseDiskGrub(bootImg)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
baseImage, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer baseImage.Close()
|
|
||||||
|
|
||||||
baseImageGz, err := gzip.NewReader(baseImage)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer baseImageGz.Close()
|
|
||||||
_, err = io.Copy(bootImg, baseImageGz)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Print("running losetup...")
|
log.Print("running losetup...")
|
||||||
@@ -163,6 +134,7 @@ func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
devp1 := dev + "p1"
|
devp1 := dev + "p1"
|
||||||
|
|
||||||
err = syscall.Mount(devp1, tempDir, "vfat", 0, "")
|
err = syscall.Mount(devp1, tempDir, "vfat", 0, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to mount %s to %s: %v", devp1, tempDir, err)
|
return fmt.Errorf("failed to mount %s to %s: %v", devp1, tempDir, err)
|
||||||
@@ -176,10 +148,10 @@ func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
|
|||||||
// add system elements
|
// add system elements
|
||||||
tarOut, tarIn := io.Pipe()
|
tarOut, tarIn := io.Pipe()
|
||||||
go func() {
|
go func() {
|
||||||
err2 := buildBootTar(tarIn, ctx)
|
e := buildBootTar(tarIn, ctx, uki, cmdline)
|
||||||
tarIn.Close()
|
tarIn.Close()
|
||||||
if err2 != nil {
|
if e != nil {
|
||||||
err = err2
|
err = e
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -217,6 +189,74 @@ func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func baseDiskUki(image *os.File, sizeMB int16) (err error) {
|
||||||
|
err = image.Truncate(0)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
size := int64(sizeMB) << 20
|
||||||
|
|
||||||
|
err = image.Truncate(size)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = image.Sync()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = run("sgdisk", "--zap", "--new=1:2048:", "--typecode=1:EF00", "--change-name=1:ESP", image.Name())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
espOffset = 2048 * 512
|
||||||
|
gptBackupSectors = 34 // real world data
|
||||||
|
)
|
||||||
|
espSectors := (size-espOffset)/512 - gptBackupSectors
|
||||||
|
|
||||||
|
cfg := fmt.Sprintf("drive d: file=%q offset=%d\n", image.Name(), espOffset)
|
||||||
|
err = mtool(cfg, "mformat", "-F", "-T", fmt.Sprint(espSectors), "d:")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var grubSupportVersion = flag.String("grub-support", "1.1.1", "GRUB support version")
|
||||||
|
|
||||||
|
func baseDiskGrub(image *os.File) (err error) {
|
||||||
|
path, err := distFetch("grub-support", *grubSupportVersion)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
baseImage, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer baseImage.Close()
|
||||||
|
|
||||||
|
baseImageGz, err := gzip.NewReader(baseImage)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer baseImageGz.Close()
|
||||||
|
_, err = io.Copy(image, baseImageGz)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func mtool(config string, program string, args ...string) (err error) {
|
||||||
|
cmd := exec.Command(program, args...)
|
||||||
|
cmd.Env = append(os.Environ(), "MTOOLSRC=/dev/stdin")
|
||||||
|
cmd.Stdin = bytes.NewBufferString(config)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
func run(program string, args ...string) (err error) {
|
func run(program string, args ...string) (err error) {
|
||||||
cmd := exec.Command(program, args...)
|
cmd := exec.Command(program, args...)
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func buildBootISO(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create a tag file
|
// create a tag file
|
||||||
bootstrapBytes, _, err := ctx.BootstrapConfig()
|
bootstrapBytes, err := ctx.BootstrapConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"debug/pe"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"novit.tech/direktil/local-server/pkg/utf16"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func rmTempFile(f *os.File) {
|
func rmTempFile(f *os.File) {
|
||||||
@@ -17,7 +19,124 @@ func rmTempFile(f *os.File) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildBootTar(out io.Writer, ctx *renderContext) (err error) {
|
func buildUki(out io.Writer, ctx *renderContext, uki bool, cmdline string) error {
|
||||||
|
if !uki {
|
||||||
|
return fmt.Errorf("buildUki only builds UKI")
|
||||||
|
}
|
||||||
|
|
||||||
|
const efiStubPath = "/usr/lib/systemd/boot/efi/linuxx64.efi.stub"
|
||||||
|
|
||||||
|
stub, err := pe.Open(efiStubPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("efi stub open: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hdr := stub.OptionalHeader.(*pe.OptionalHeader64)
|
||||||
|
align := uint64(hdr.SectionAlignment)
|
||||||
|
|
||||||
|
var offset uint64
|
||||||
|
for _, s := range stub.Sections {
|
||||||
|
end := uint64(s.VirtualAddress) + uint64(s.VirtualSize)
|
||||||
|
if end > offset {
|
||||||
|
offset = end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += hdr.ImageBase
|
||||||
|
|
||||||
|
stub.Close()
|
||||||
|
|
||||||
|
// kernel
|
||||||
|
kernelPath, err := distFetch("kernels", ctx.Host.Kernel)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fetch kernel: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// temp dir
|
||||||
|
tempDir, err := os.MkdirTemp(os.TempDir(), "uki-")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("mkdir temp: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// osrel
|
||||||
|
osrelPath := filepath.Join(tempDir, "osrel")
|
||||||
|
if err := os.WriteFile(osrelPath, fmt.Appendf(nil, "ID=direktil\nPRETTY_NAME='Direktil %s %s+0'\n", ctx.Host.Name,
|
||||||
|
time.Now().UTC().Format(time.DateTime)), 0o600); err != nil {
|
||||||
|
return fmt.Errorf("create osrel: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdline
|
||||||
|
cmdlinePath := filepath.Join(tempDir, "cmdline")
|
||||||
|
if err := os.WriteFile(cmdlinePath, append([]byte(cmdline), 0), 0o600); err != nil {
|
||||||
|
return fmt.Errorf("create cmdline: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initrd
|
||||||
|
initrdPath := filepath.Join(tempDir, "initrd")
|
||||||
|
if err := func() (err error) {
|
||||||
|
initrd, err := os.Create(initrdPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer initrd.Close()
|
||||||
|
return buildInitrd(initrd, ctx)
|
||||||
|
}(); err != nil {
|
||||||
|
return fmt.Errorf("create initrd: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assemble
|
||||||
|
|
||||||
|
args := make([]string, 0)
|
||||||
|
for _, i := range []struct {
|
||||||
|
section string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{"osrel", osrelPath},
|
||||||
|
{"cmdline", cmdlinePath},
|
||||||
|
{"initrd", initrdPath},
|
||||||
|
{"linux", kernelPath},
|
||||||
|
} {
|
||||||
|
offset += align - offset%align
|
||||||
|
args = append(args,
|
||||||
|
"--add-section", "."+i.section+"="+i.path, "--change-section-vma", fmt.Sprintf(".%s=0x%x", i.section, offset))
|
||||||
|
|
||||||
|
stat, err := os.Stat(i.path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stat %s: %w", i.section, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += uint64(stat.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
ukiPath := filepath.Join(tempDir, "uki")
|
||||||
|
args = append(args, efiStubPath, ukiPath)
|
||||||
|
|
||||||
|
if err := run("objcopy", args...); err != nil {
|
||||||
|
return fmt.Errorf("objcopy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// read
|
||||||
|
ukiBytes, err := os.ReadFile(ukiPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read uki: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
io.Copy(out, bytes.NewBuffer(ukiBytes))
|
||||||
|
|
||||||
|
// done
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBootTar(out io.Writer, ctx *renderContext, uki bool, cmdline string) (err error) {
|
||||||
|
if uki {
|
||||||
|
return buildUkiBootTar(out, ctx, cmdline)
|
||||||
|
} else {
|
||||||
|
return buildGrubBootTar(out, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildGrubBootTar(out io.Writer, ctx *renderContext) (err error) {
|
||||||
arch := tar.NewWriter(out)
|
arch := tar.NewWriter(out)
|
||||||
defer arch.Close()
|
defer arch.Close()
|
||||||
|
|
||||||
@@ -62,7 +181,7 @@ func buildBootTar(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildBootEFITar(out io.Writer, ctx *renderContext) (err error) {
|
func buildUkiBootTar(out io.Writer, ctx *renderContext, cmdline string) (err error) {
|
||||||
arch := tar.NewWriter(out)
|
arch := tar.NewWriter(out)
|
||||||
defer arch.Close()
|
defer arch.Close()
|
||||||
|
|
||||||
@@ -75,46 +194,14 @@ func buildBootEFITar(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
// UKI
|
||||||
prefix = "EFI/dkl/"
|
uki := new(bytes.Buffer)
|
||||||
efiPrefix = "\\EFI\\dkl\\"
|
err = buildUki(uki, ctx, true, cmdline)
|
||||||
)
|
|
||||||
|
|
||||||
// boot.csv
|
|
||||||
// -> annoyingly it's UTF-16...
|
|
||||||
bootCsvBytes := utf16.FromUTF8([]byte("" +
|
|
||||||
"current_kernel.efi,dkl current,initrd=" + efiPrefix + "current_initrd.img,Direktil current\n" +
|
|
||||||
"previous_kernel.efi,dkl previous,initrd=" + efiPrefix + "previous_initrd.img,Direktil previous\n"))
|
|
||||||
|
|
||||||
err = archAdd(prefix+"BOOT.CSV", []byte(bootCsvBytes))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// kernel
|
err = archAdd("EFI/BOOT/BOOTX64.EFI", uki.Bytes())
|
||||||
kernelPath, err := distFetch("kernels", ctx.Host.Kernel)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
kernelBytes, err := os.ReadFile(kernelPath)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = archAdd(prefix+"current_kernel.efi", kernelBytes)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// initrd
|
|
||||||
initrd := new(bytes.Buffer)
|
|
||||||
err = buildInitrd(initrd, ctx)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = archAdd(prefix+"current_initrd.img", initrd.Bytes())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+309
-42
@@ -4,40 +4,43 @@ import (
|
|||||||
"archive/tar"
|
"archive/tar"
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto"
|
"crypto"
|
||||||
"encoding/json"
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/cavaliergopher/cpio"
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
yaml "gopkg.in/yaml.v2"
|
|
||||||
|
|
||||||
|
"novit.tech/direktil/pkg/base64"
|
||||||
|
"novit.tech/direktil/pkg/config"
|
||||||
"novit.tech/direktil/pkg/cpiocat"
|
"novit.tech/direktil/pkg/cpiocat"
|
||||||
|
"novit.tech/direktil/pkg/localconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
func renderBootstrapConfig(w http.ResponseWriter, r *http.Request, ctx *renderContext, asJson bool) (err error) {
|
func renderBootstrapConfig(w http.ResponseWriter, ctx *renderContext) (err error) {
|
||||||
log.Printf("sending bootstrap config for %q", ctx.Host.Name)
|
log.Printf("sending bootstrap config for %q", ctx.Host.Name)
|
||||||
|
|
||||||
_, cfg, err := ctx.BootstrapConfig()
|
ba, err := ctx.BootstrapConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if asJson {
|
_, err = w.Write(ba)
|
||||||
err = json.NewEncoder(w).Encode(cfg)
|
return
|
||||||
} else {
|
|
||||||
err = yaml.NewEncoder(w).Encode(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
||||||
_, cfg, err := ctx.Config()
|
_, cfg, err := ctx.Config()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return fmt.Errorf("render config failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
zout, err := zstd.NewWriter(out, zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(12)))
|
zout, err := zstd.NewWriter(out, zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(12)))
|
||||||
@@ -50,10 +53,32 @@ func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
// initrd
|
// initrd
|
||||||
initrdPath, err := distFetch("initrd", ctx.Host.Initrd)
|
initrdPath, err := distFetch("initrd", ctx.Host.Initrd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return fmt.Errorf("fetch initrd failed: %w", err)
|
||||||
}
|
}
|
||||||
cat.AppendArchFile(initrdPath)
|
cat.AppendArchFile(initrdPath)
|
||||||
|
|
||||||
|
// copy config's udev rules (FIXME: too much logic for dls)
|
||||||
|
for _, file := range cfg.Files {
|
||||||
|
if !strings.HasPrefix(file.Path, "/etc/udev/rules.d/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
content := []byte(file.Content)
|
||||||
|
if c := file.Content64; c != "" {
|
||||||
|
content, err = base64.Decode(c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decode %s failed: %w", file.Path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := file.Mode
|
||||||
|
if mode == 0 {
|
||||||
|
mode = 0o644
|
||||||
|
}
|
||||||
|
|
||||||
|
cat.AppendBytes(content, file.Path, cpio.FileMode(mode))
|
||||||
|
}
|
||||||
|
|
||||||
// embedded layers (modules)
|
// embedded layers (modules)
|
||||||
for _, layer := range cfg.Layers {
|
for _, layer := range cfg.Layers {
|
||||||
switch layer {
|
switch layer {
|
||||||
@@ -64,7 +89,7 @@ func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
}
|
}
|
||||||
modulesPath, err := distFetch("layers", layer, layerVersion)
|
modulesPath, err := distFetch("layers", layer, layerVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("fetch layer %s failed: %w", layer, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cat.AppendFile(modulesPath, "modules.sqfs")
|
cat.AppendFile(modulesPath, "modules.sqfs")
|
||||||
@@ -72,9 +97,9 @@ func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// config
|
// config
|
||||||
cfgBytes, _, err := ctx.BootstrapConfig()
|
cfgBytes, err := ctx.BootstrapConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return fmt.Errorf("render bootstrap config failed: %w", err)
|
||||||
}
|
}
|
||||||
cat.AppendBytes(cfgBytes, "config.yaml", 0o600)
|
cat.AppendBytes(cfgBytes, "config.yaml", 0o600)
|
||||||
|
|
||||||
@@ -83,7 +108,7 @@ func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
cat.AppendDir("/etc/ssh", 0o700)
|
cat.AppendDir("/etc/ssh", 0o700)
|
||||||
|
|
||||||
// XXX do we want bootstrap-stage keys instead of the real host key?
|
// XXX do we want bootstrap-stage keys instead of the real host key?
|
||||||
for _, format := range []string{"rsa", "ecdsa", "ed25519"} {
|
for _, format := range sshKeyTypes {
|
||||||
keyPath := "/etc/ssh/ssh_host_" + format + "_key"
|
keyPath := "/etc/ssh/ssh_host_" + format + "_key"
|
||||||
cat.AppendBytes(cfg.FileContent(keyPath), keyPath, 0o600)
|
cat.AppendBytes(cfg.FileContent(keyPath), keyPath, 0o600)
|
||||||
}
|
}
|
||||||
@@ -107,15 +132,19 @@ func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSigner(clusterName string) (signer crypto.Signer, err error) {
|
||||||
|
ca, err := getUsableClusterCA(clusterName, "boot-signer")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return ca.ParseKey()
|
||||||
|
}
|
||||||
|
|
||||||
func buildBootstrap(out io.Writer, ctx *renderContext) (err error) {
|
func buildBootstrap(out io.Writer, ctx *renderContext) (err error) {
|
||||||
arch := tar.NewWriter(out)
|
arch := tar.NewWriter(out)
|
||||||
defer arch.Close()
|
defer arch.Close()
|
||||||
|
|
||||||
ca, err := getUsableClusterCA(ctx.Host.ClusterName, "boot-signer")
|
signer, err := getSigner(ctx.Host.ClusterName)
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
signer, err := ca.ParseKey()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -171,22 +200,8 @@ func buildBootstrap(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// layers
|
// layers
|
||||||
for _, layer := range cfg.Layers {
|
appendSignedLayer := func(layer, layerPath string) (err error) {
|
||||||
if layer == "modules" {
|
f, err := os.Open(layerPath)
|
||||||
continue // modules are in the initrd with boot v2
|
|
||||||
}
|
|
||||||
|
|
||||||
layerVersion := ctx.Host.Versions[layer]
|
|
||||||
if layerVersion == "" {
|
|
||||||
return fmt.Errorf("layer %q not mapped to a version", layer)
|
|
||||||
}
|
|
||||||
|
|
||||||
outPath, err := distFetch("layers", layer, layerVersion)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Open(outPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -202,7 +217,7 @@ func buildBootstrap(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
reader := io.TeeReader(f, h)
|
reader := io.TeeReader(f, h)
|
||||||
|
|
||||||
if err = arch.WriteHeader(&tar.Header{
|
if err = arch.WriteHeader(&tar.Header{
|
||||||
Name: layer + ".fs",
|
Name: layer,
|
||||||
Size: stat.Size(),
|
Size: stat.Size(),
|
||||||
Mode: 0o600,
|
Mode: 0o600,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -215,11 +230,263 @@ func buildBootstrap(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
digest := h.Sum(nil)
|
digest := h.Sum(nil)
|
||||||
err = sign(layer+".fs.sig", digest)
|
err = sign(layer+".sig", digest)
|
||||||
if err != nil {
|
|
||||||
return err
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allErofs := true
|
||||||
|
|
||||||
|
for _, layer := range cfg.Layers {
|
||||||
|
if layer == "modules" {
|
||||||
|
continue // modules are in the initrd with boot v2
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(ctx.Host.Versions[layer], ".erofs") {
|
||||||
|
allErofs = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allErofs {
|
||||||
|
layerPath, e := layersCombo(ctx.Host, cfg, signer)
|
||||||
|
if e != nil {
|
||||||
|
err = e
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = appendSignedLayer("merged", layerPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, layer := range cfg.Layers {
|
||||||
|
if layer == "modules" {
|
||||||
|
continue // modules are in the initrd with boot v2
|
||||||
|
}
|
||||||
|
|
||||||
|
layerPath, e := fetchHostLayer(ctx.Host, layer)
|
||||||
|
if e != nil {
|
||||||
|
err = e
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = appendSignedLayer(layer+".fs", layerPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func layersCombo(host *localconfig.Host, cfg *config.Config, signer crypto.Signer) (path string, err error) {
|
||||||
|
key := layersComboKey(host, cfg)
|
||||||
|
return opMutex(key, func() (path string, err error) {
|
||||||
|
return buildLayersCombo(host, cfg, signer)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func layersComboKey(host *localconfig.Host, cfg *config.Config) string {
|
||||||
|
key := new(strings.Builder)
|
||||||
|
key.WriteString("layers")
|
||||||
|
for _, layer := range cfg.Layers {
|
||||||
|
if layer == "modules" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key.WriteByte(':')
|
||||||
|
key.WriteString(layer)
|
||||||
|
if v, ok := host.Versions[layer]; ok {
|
||||||
|
key.WriteByte('@')
|
||||||
|
key.WriteString(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return key.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLayersCombo(host *localconfig.Host, cfg *config.Config, signer crypto.Signer) (path string, err error) {
|
||||||
|
path = filepath.Join(*dataDir, "cache")
|
||||||
|
if err = os.MkdirAll(path, 0o700); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key := layersComboKey(host, cfg)
|
||||||
|
path = filepath.Join(path, key) + ".fs"
|
||||||
|
|
||||||
|
if _, statErr := os.Stat(path); statErr == nil {
|
||||||
|
return // exists -> already done
|
||||||
|
}
|
||||||
|
|
||||||
|
workdir, err := os.MkdirTemp("/tmp", "layers")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.RemoveAll(workdir)
|
||||||
|
|
||||||
|
tmpTar := filepath.Join(workdir, "output.tar")
|
||||||
|
|
||||||
|
layers := slices.Clone(cfg.Layers)
|
||||||
|
slices.Reverse(layers)
|
||||||
|
|
||||||
|
cmdOut := new(bytes.Buffer)
|
||||||
|
|
||||||
|
run := func(prog string, arg ...string) bool {
|
||||||
|
cmdOut.Reset()
|
||||||
|
|
||||||
|
cmd := exec.Command(prog, arg...)
|
||||||
|
cmd.Stdout = cmdOut // os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if e := cmd.Run(); e != nil {
|
||||||
|
err = fmt.Errorf("%s %q failed: %w", cmd.Path, cmd.Args, e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, layer := range layers {
|
||||||
|
if layer == "modules" {
|
||||||
|
continue // modules are in the initrd with boot v2
|
||||||
|
}
|
||||||
|
|
||||||
|
layerFile, e := fetchHostLayer(host, layer)
|
||||||
|
if e != nil {
|
||||||
|
err = e
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mountPoint := filepath.Join(workdir, layer)
|
||||||
|
os.MkdirAll(mountPoint, 0700)
|
||||||
|
|
||||||
|
if e := exec.Command("erofsfuse", layerFile, mountPoint).Run(); e != nil {
|
||||||
|
err = fmt.Errorf("erofsfuse %s %s failed: %w", layerFile, mountPoint, e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := exec.Command("umount", mountPoint).Run(); err != nil {
|
||||||
|
log.Printf("umount %s failed: %v", mountPoint, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
mode := "--append"
|
||||||
|
if i == 0 {
|
||||||
|
mode = "--create"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !run("tar", mode, "-p", "-f", tmpTar, "-C", mountPoint, ".") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fsOut := filepath.Join(workdir, "output.fs")
|
||||||
|
if !run("mkfs.erofs", "-z", "lzma", "-C131072", "-Efragments,ztailpacking",
|
||||||
|
"-T0", "--all-time", "--ignore-mtime", "--tar=f", fsOut, tmpTar) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hashOut := filepath.Join(workdir, "output.hash")
|
||||||
|
|
||||||
|
if !run("veritysetup", "format", fsOut, hashOut) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootHash []byte
|
||||||
|
for line := range strings.SplitSeq(cmdOut.String(), "\n") {
|
||||||
|
v, ok := strings.CutPrefix(line, "Root hash:")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
|
||||||
|
b, e := hex.DecodeString(v)
|
||||||
|
if e != nil {
|
||||||
|
err = fmt.Errorf("invalid root hash: %w", e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rootHash = b
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rootHash) == 0 {
|
||||||
|
err = fmt.Errorf("root hash not found in output")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sigBytes, err := signer.Sign(nil, rootHash, crypto.SHA256)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("root hash signature failed: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outPath := path + ".tmp"
|
||||||
|
err = func() (err error) {
|
||||||
|
fsRd, e := os.Open(fsOut)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
defer fsRd.Close()
|
||||||
|
hashRd, e := os.Open(hashOut)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
defer hashRd.Close()
|
||||||
|
|
||||||
|
fsStat, e := fsRd.Stat()
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
hashStat, e := hashRd.Stat()
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.Create(outPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
append := func(sz uint64, rd io.Reader) {
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
szB := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint64(szB, sz)
|
||||||
|
_, err = out.Write(szB)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(out, rd)
|
||||||
|
}
|
||||||
|
|
||||||
|
append(uint64(len(sigBytes)), bytes.NewBuffer(sigBytes))
|
||||||
|
append(uint64(len(rootHash)), bytes.NewBuffer(rootHash))
|
||||||
|
append(uint64(fsStat.Size()), fsRd)
|
||||||
|
append(uint64(hashStat.Size()), hashRd)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = out.Close()
|
||||||
|
return
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("assembly failed: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Rename(outPath, path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchHostLayer(host *localconfig.Host, layer string) (path string, err error) {
|
||||||
|
layerVersion := host.Versions[layer]
|
||||||
|
if layerVersion == "" {
|
||||||
|
return "", fmt.Errorf("layer %q not mapped to a version", layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return distFetch("layers", layer, layerVersion)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func updateCache() {
|
||||||
|
log.Print("updating cache")
|
||||||
|
|
||||||
|
cfg, err := readConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("update cache failed: read config failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := 0
|
||||||
|
inUse := map[string]bool{}
|
||||||
|
|
||||||
|
fetch := func(path ...string) {
|
||||||
|
assetPath, err := distFetch(path...)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("update cache: dist fetch failed: %v", err)
|
||||||
|
errs += 1
|
||||||
|
}
|
||||||
|
inUse[assetPath] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch("grub-support", *grubSupportVersion)
|
||||||
|
|
||||||
|
for _, host := range cfg.Hosts {
|
||||||
|
ctx, err := newRenderContext(host, cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("update cache: %s: build context failed: %v", host.Name, err)
|
||||||
|
errs += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, cfg, err := ctx.Config()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("update cache: %s: render config failed: %v", host.Name, err)
|
||||||
|
errs += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := getSigner(host.ClusterName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("update cache: %s: get signer failed: %v", host.Name, err)
|
||||||
|
errs += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch("kernels", host.Kernel)
|
||||||
|
fetch("layers", "modules", host.Kernel)
|
||||||
|
fetch("initrd", host.Initrd)
|
||||||
|
|
||||||
|
allErofs := true
|
||||||
|
for layer, version := range host.Versions {
|
||||||
|
fetch("layers", layer, version)
|
||||||
|
|
||||||
|
if layer != "modules" && !strings.HasSuffix(version, ".erofs") {
|
||||||
|
allErofs = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allErofs {
|
||||||
|
path, err := layersCombo(host, cfg, signer)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("update cache: layer combo failed: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inUse[path] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs != 0 {
|
||||||
|
log.Printf("update cache: %d errors, not cleaning", errs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
distDir := filepath.Join(*dataDir, "dist")
|
||||||
|
cacheDir := filepath.Join(*dataDir, "cache")
|
||||||
|
|
||||||
|
existing := []string{}
|
||||||
|
|
||||||
|
for _, dir := range []string{distDir, cacheDir} {
|
||||||
|
fs.WalkDir(os.DirFS(dir), ".", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("update cache: walking %s failed: %v", dir, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !d.IsDir() {
|
||||||
|
existing = append(existing, filepath.Join(dir, path))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("update cache: %d/%d assets in use", len(inUse), len(existing))
|
||||||
|
|
||||||
|
for _, path := range existing {
|
||||||
|
if inUse[path] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Print("update cache: removing ", path)
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
log.Print("update cache: failed to remove: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -117,7 +117,12 @@ func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s = string(ca.Cert)
|
extra, err := caExtraCerts(cluster, name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s = string(ca.Cert) + extra
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -127,13 +132,18 @@ func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extra, err := caExtraCerts(cluster, name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
dir := "/etc/tls-ca/" + name
|
dir := "/etc/tls-ca/" + name
|
||||||
|
|
||||||
return asYaml([]config.FileDef{
|
return asYaml([]config.FileDef{
|
||||||
{
|
{
|
||||||
Path: path.Join(dir, "ca.crt"),
|
Path: path.Join(dir, "ca.crt"),
|
||||||
Mode: 0644,
|
Mode: 0644,
|
||||||
Content: string(ca.Cert),
|
Content: string(ca.Cert) + extra,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Path: path.Join(dir, "ca.key"),
|
Path: path.Join(dir, "ca.key"),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
func hash(values ...interface{}) string {
|
func hash(values ...any) string {
|
||||||
ba, err := json.Marshal(values)
|
ba, err := json.Marshal(values)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err) // should not happen
|
panic(err) // should not happen
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
yaml "gopkg.in/yaml.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func renderConfig(w http.ResponseWriter, r *http.Request, ctx *renderContext, asJson bool) (err error) {
|
func renderConfig(w http.ResponseWriter, _ *http.Request, ctx *renderContext, asJson bool) (err error) {
|
||||||
log.Printf("sending config for %q", ctx.Host.Name)
|
log.Printf("sending config for %q", ctx.Host.Name)
|
||||||
|
|
||||||
_, cfg, err := ctx.Config()
|
cfgBytes, cfg, err := ctx.Config()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -19,7 +17,7 @@ func renderConfig(w http.ResponseWriter, r *http.Request, ctx *renderContext, as
|
|||||||
if asJson {
|
if asJson {
|
||||||
err = json.NewEncoder(w).Encode(cfg)
|
err = json.NewEncoder(w).Encode(cfg)
|
||||||
} else {
|
} else {
|
||||||
err = yaml.NewEncoder(w).Encode(cfg)
|
_, err = w.Write(cfgBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ const (
|
|||||||
etcDir = "/etc/direktil"
|
etcDir = "/etc/direktil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "dev"
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
Date = "now"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
address = flag.String("address", ":7606", "HTTP listen address")
|
address = flag.String("address", ":7606", "HTTP listen address")
|
||||||
@@ -38,7 +41,7 @@ func main() {
|
|||||||
log.Fatal("no listen address given")
|
log.Fatal("no listen address given")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Print("Direktil local-server version ", Version)
|
log.Print("Direktil local-server version ", Version, " (", Date, ")")
|
||||||
wPublicState.Change(func(s *PublicState) { s.ServerVersion = Version })
|
wPublicState.Change(func(s *PublicState) { s.ServerVersion = Version })
|
||||||
|
|
||||||
computeUIHash()
|
computeUIHash()
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
var (
|
||||||
|
opMutexes = map[string]*sync.Mutex{}
|
||||||
|
opsLock sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func opMutex[T any](key string, op func() (T, error)) (T, error) {
|
||||||
|
lock := func() *sync.Mutex {
|
||||||
|
opsLock.Lock()
|
||||||
|
defer opsLock.Unlock()
|
||||||
|
|
||||||
|
lock, ok := opMutexes[key]
|
||||||
|
if !ok {
|
||||||
|
lock = new(sync.Mutex)
|
||||||
|
opMutexes[key] = lock
|
||||||
|
}
|
||||||
|
|
||||||
|
return lock
|
||||||
|
}()
|
||||||
|
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
|
return op()
|
||||||
|
}
|
||||||
@@ -26,14 +26,10 @@ import (
|
|||||||
|
|
||||||
"novit.tech/direktil/pkg/config"
|
"novit.tech/direktil/pkg/config"
|
||||||
"novit.tech/direktil/pkg/localconfig"
|
"novit.tech/direktil/pkg/localconfig"
|
||||||
|
|
||||||
bsconfig "novit.tech/direktil/pkg/bootstrapconfig"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdlineParam = restful.QueryParameter("cmdline", "Linux kernel cmdline addition")
|
var cmdlineParam = restful.QueryParameter("cmdline", "Linux kernel cmdline addition")
|
||||||
|
|
||||||
var b64 = base64.StdEncoding.WithPadding(base64.NoPadding)
|
|
||||||
|
|
||||||
type renderContext struct {
|
type renderContext struct {
|
||||||
Host *localconfig.Host
|
Host *localconfig.Host
|
||||||
SSLConfig *cfsslconfig.Config
|
SSLConfig *cfsslconfig.Config
|
||||||
@@ -114,19 +110,8 @@ func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *renderContext) BootstrapConfig() (ba []byte, cfg *bsconfig.Config, err error) {
|
func (ctx *renderContext) BootstrapConfig() (ba []byte, err error) {
|
||||||
ba, err = ctx.render(ctx.Host.BootstrapConfig)
|
return ctx.render(ctx.Host.BootstrapConfig)
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg = &bsconfig.Config{}
|
|
||||||
if err = yaml.Unmarshal(ba, cfg); err != nil {
|
|
||||||
log.Print("invalid bootstrap config yaml:\n", string(ba))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *renderContext) render(templateText string) (ba []byte, err error) {
|
func (ctx *renderContext) render(templateText string) (ba []byte, err error) {
|
||||||
@@ -190,7 +175,7 @@ func (ctx *renderContext) TemplateFuncs() map[string]any {
|
|||||||
|
|
||||||
for name, method := range map[string]any{
|
for name, method := range map[string]any{
|
||||||
"base64": func(input string) string {
|
"base64": func(input string) string {
|
||||||
return b64.EncodeToString([]byte(input))
|
return base64.StdEncoding.EncodeToString([]byte(input))
|
||||||
},
|
},
|
||||||
|
|
||||||
"host_ip": func() (s string) {
|
"host_ip": func() (s string) {
|
||||||
|
|||||||
@@ -137,7 +137,9 @@ func unlockSecretStore(name string, passphrase []byte) (err httperr.Error) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
go updateState()
|
go updateState()
|
||||||
go migrateSecrets()
|
|
||||||
|
migrateSecrets() // we can probably remove it now
|
||||||
|
go updateCache()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type State struct {
|
|||||||
Store struct {
|
Store struct {
|
||||||
DownloadToken string
|
DownloadToken string
|
||||||
KeyNames []string
|
KeyNames []string
|
||||||
|
Salt []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
Clusters []ClusterState
|
Clusters []ClusterState
|
||||||
@@ -157,6 +158,7 @@ func updateState() {
|
|||||||
wState.Change(func(v *State) {
|
wState.Change(func(v *State) {
|
||||||
v.HasConfig = true
|
v.HasConfig = true
|
||||||
v.Store.KeyNames = keyNames
|
v.Store.KeyNames = keyNames
|
||||||
|
v.Store.Salt = secStore.Salt[:]
|
||||||
v.Clusters = clusters
|
v.Clusters = clusters
|
||||||
v.Hosts = hosts
|
v.Hosts = hosts
|
||||||
v.HostTemplates = hostTemplates
|
v.HostTemplates = hostTemplates
|
||||||
|
|||||||
@@ -79,6 +79,17 @@ func getUsableClusterCA(cluster, name string) (ca CA, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func caExtraCerts(cluster, name string) (extra string, err error) {
|
||||||
|
cfg, err := readConfig()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cfg.ExtraCaCerts != nil {
|
||||||
|
extra = cfg.ExtraCaCerts[cluster+"/"+name]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var clusterCASignedKeys = newClusterSecretKV[KeyCert]("CA-signed-keys")
|
var clusterCASignedKeys = newClusterSecretKV[KeyCert]("CA-signed-keys")
|
||||||
|
|
||||||
func wsClusterCASignedKeys(req *restful.Request, resp *restful.Response) {
|
func wsClusterCASignedKeys(req *restful.Request, resp *restful.Response) {
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ func writeNewConfig(cfgBytes []byte) (err error) {
|
|||||||
err = os.Rename(out.Name(), cfgPath)
|
err = os.Rename(out.Name(), cfgPath)
|
||||||
|
|
||||||
updateState()
|
updateState()
|
||||||
|
go updateCache()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -262,14 +262,17 @@ func wsDownloadSet(req *restful.Request, resp *restful.Response) {
|
|||||||
|
|
||||||
for _, item := range set.Items {
|
for _, item := range set.Items {
|
||||||
names := make([]string, 0)
|
names := make([]string, 0)
|
||||||
|
section := ""
|
||||||
switch item.Kind {
|
switch item.Kind {
|
||||||
case "cluster":
|
case "cluster":
|
||||||
|
section = "Cluster"
|
||||||
for _, c := range cfg.Clusters {
|
for _, c := range cfg.Clusters {
|
||||||
if globMatch(item.Name, c.Name) {
|
if globMatch(item.Name, c.Name) {
|
||||||
names = append(names, c.Name)
|
names = append(names, c.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "host":
|
case "host":
|
||||||
|
section = "Host"
|
||||||
for _, h := range cfg.Hosts {
|
for _, h := range cfg.Hosts {
|
||||||
if globMatch(item.Name, h.Name) {
|
if globMatch(item.Name, h.Name) {
|
||||||
names = append(names, h.Name)
|
names = append(names, h.Name)
|
||||||
@@ -278,12 +281,22 @@ func wsDownloadSet(req *restful.Request, resp *restful.Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
fmt.Fprintf(buf, "<h2>%s %s</h2>", strings.Title(item.Kind), name)
|
buf.WriteString("<p>")
|
||||||
fmt.Fprintf(buf, "<p class=\"download-links\">\n")
|
fmt.Fprintf(buf, "<strong>%s %s:</strong>", section, name)
|
||||||
|
buf.WriteString(" <span class=\"download-links\">")
|
||||||
for _, asset := range item.Assets {
|
for _, asset := range item.Assets {
|
||||||
fmt.Fprintf(buf, " <a href=\"/public/download-set/%s/%s/%s?set=%s\" download>%s</a>\n", item.Kind, name, asset, setStr, asset)
|
for _, v := range hostAssetVariants(asset) {
|
||||||
|
url := fmt.Sprintf("/public/download-set/%s/%s/%s", item.Kind, name, v.url)
|
||||||
|
if strings.Contains(url, "?") {
|
||||||
|
url += "&"
|
||||||
|
} else {
|
||||||
|
url += "?"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(buf, `</p>`)
|
url += "set=" + setStr
|
||||||
|
fmt.Fprintf(buf, " <a href=\"%s\" download>%s</a>\n", url, v.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteString("</span></p>\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,11 +188,15 @@ func wsDownloadPage(req *restful.Request, resp *restful.Response) {
|
|||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
buf.WriteString(htmlHeader(fmt.Sprintf("Token assets: %s %s", spec.Kind, spec.Name)))
|
buf.WriteString(htmlHeader(fmt.Sprintf("Token assets: %s %s", spec.Kind, spec.Name)))
|
||||||
|
|
||||||
buf.WriteString("<ul>")
|
buf.WriteString("<table class=\"download-table\">")
|
||||||
for _, asset := range spec.Assets {
|
for _, asset := range spec.Assets {
|
||||||
fmt.Fprintf(buf, "<li><a href=\"%s\" download>%s</a></li>\n", asset, asset)
|
buf.WriteString("<tr>")
|
||||||
|
for _, v := range hostAssetVariants(asset) {
|
||||||
|
fmt.Fprintf(buf, "<td><a href=\"%s\" download>%s</a></td>", v.url, v.name)
|
||||||
}
|
}
|
||||||
buf.WriteString("</ul>")
|
buf.WriteString("</tr>\n")
|
||||||
|
}
|
||||||
|
buf.WriteString("</table>")
|
||||||
|
|
||||||
buf.WriteString(htmlFooter)
|
buf.WriteString(htmlFooter)
|
||||||
buf.WriteTo(resp)
|
buf.WriteTo(resp)
|
||||||
|
|||||||
+144
-50
@@ -1,10 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"bytes"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
restful "github.com/emicklei/go-restful"
|
restful "github.com/emicklei/go-restful"
|
||||||
|
|
||||||
@@ -13,10 +15,38 @@ import (
|
|||||||
"novit.tech/direktil/local-server/pkg/mime"
|
"novit.tech/direktil/local-server/pkg/mime"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var sshKeyTypes = []string{"rsa", "ecdsa", "ed25519"}
|
||||||
allowDetectedHost = flag.Bool("allow-detected-host", false, "Allow access to host assets from its IP (insecure but enables unattended netboot)")
|
|
||||||
trustXFF = flag.Bool("trust-xff", false, "Trust the X-Forwarded-For header")
|
type AssetVariant struct {
|
||||||
)
|
name string
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostAssetVariants(asset string) []AssetVariant {
|
||||||
|
switch asset {
|
||||||
|
case "uki":
|
||||||
|
return []AssetVariant{
|
||||||
|
{asset, asset},
|
||||||
|
{asset + " (serial)", asset + "?serial=0"},
|
||||||
|
}
|
||||||
|
case "boot.img",
|
||||||
|
"boot.img.gz",
|
||||||
|
"boot.qcow2",
|
||||||
|
"boot.qed",
|
||||||
|
"boot.vdi",
|
||||||
|
"boot.vmdk",
|
||||||
|
"boot.vpc",
|
||||||
|
"boot.iso",
|
||||||
|
"boot.tar":
|
||||||
|
return []AssetVariant{
|
||||||
|
{asset, asset},
|
||||||
|
{asset + " (UKI)", asset + "?uki"},
|
||||||
|
{asset + " (UKI+serial)", asset + "?uki&serial=0"},
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return []AssetVariant{{asset, asset}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type wsHost struct {
|
type wsHost struct {
|
||||||
hostDoc string
|
hostDoc string
|
||||||
@@ -51,9 +81,6 @@ func (ws wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBu
|
|||||||
b("boot.img.gz").
|
b("boot.img.gz").
|
||||||
Produces(mime.DISK + "+gzip").
|
Produces(mime.DISK + "+gzip").
|
||||||
Doc("Get the " + ws.hostDoc + "'s boot disk image, gzip compressed"),
|
Doc("Get the " + ws.hostDoc + "'s boot disk image, gzip compressed"),
|
||||||
b("boot.img.lz4").
|
|
||||||
Produces(mime.DISK + "+lz4").
|
|
||||||
Doc("Get the " + ws.hostDoc + "'s boot disk image, lz4 compressed"),
|
|
||||||
|
|
||||||
// - other formats
|
// - other formats
|
||||||
b("boot.qcow2").
|
b("boot.qcow2").
|
||||||
@@ -62,10 +89,10 @@ func (ws wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBu
|
|||||||
b("boot.qed").
|
b("boot.qed").
|
||||||
Produces(mime.DISK + "+qed").
|
Produces(mime.DISK + "+qed").
|
||||||
Doc("Get the " + ws.hostDoc + "'s boot disk image, QED (KVM)"),
|
Doc("Get the " + ws.hostDoc + "'s boot disk image, QED (KVM)"),
|
||||||
b("boot.vmdk").
|
b("boot.vdi").
|
||||||
Produces(mime.DISK + "+vdi").
|
Produces(mime.DISK + "+vdi").
|
||||||
Doc("Get the " + ws.hostDoc + "'s boot disk image, VDI (VirtualBox)"),
|
Doc("Get the " + ws.hostDoc + "'s boot disk image, VDI (VirtualBox)"),
|
||||||
b("boot.qcow2").
|
b("boot.vpc").
|
||||||
Produces(mime.DISK + "+vpc").
|
Produces(mime.DISK + "+vpc").
|
||||||
Doc("Get the " + ws.hostDoc + "'s boot disk image, VHD (Hyper-V)"),
|
Doc("Get the " + ws.hostDoc + "'s boot disk image, VHD (Hyper-V)"),
|
||||||
b("boot.vmdk").
|
b("boot.vmdk").
|
||||||
@@ -75,10 +102,7 @@ func (ws wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBu
|
|||||||
// metal/local HDD upgrades
|
// metal/local HDD upgrades
|
||||||
b("boot.tar").
|
b("boot.tar").
|
||||||
Produces(mime.TAR).
|
Produces(mime.TAR).
|
||||||
Doc("Get the " + ws.hostDoc + "'s /boot archive (ie: for metal upgrades)"),
|
Doc("Get the " + ws.hostDoc + "'s /boot archive"),
|
||||||
b("boot-efi.tar").
|
|
||||||
Produces(mime.TAR).
|
|
||||||
Doc("Get the " + ws.hostDoc + "'s /boot archive (ie: for metal upgrades)"),
|
|
||||||
|
|
||||||
// read-only ISO support
|
// read-only ISO support
|
||||||
b("boot.iso").
|
b("boot.iso").
|
||||||
@@ -98,6 +122,9 @@ func (ws wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBu
|
|||||||
b("initrd").
|
b("initrd").
|
||||||
Produces(mime.OCTET).
|
Produces(mime.OCTET).
|
||||||
Doc("Get the " + ws.hostDoc + "'s initial RAM disk (ie: for netboot)"),
|
Doc("Get the " + ws.hostDoc + "'s initial RAM disk (ie: for netboot)"),
|
||||||
|
b("uki").
|
||||||
|
Produces(mime.OCTET).
|
||||||
|
Doc("Get the " + ws.hostDoc + "'s Unified Kernel Image"),
|
||||||
|
|
||||||
// - bootstrap config
|
// - bootstrap config
|
||||||
b("bootstrap-config").
|
b("bootstrap-config").
|
||||||
@@ -109,6 +136,10 @@ func (ws wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBu
|
|||||||
b("bootstrap.tar").
|
b("bootstrap.tar").
|
||||||
Produces(mime.TAR).
|
Produces(mime.TAR).
|
||||||
Doc("Get the " + ws.hostDoc + "'s bootstrap seed archive"),
|
Doc("Get the " + ws.hostDoc + "'s bootstrap seed archive"),
|
||||||
|
|
||||||
|
// ssh
|
||||||
|
b("ssh.pub").Produces("text/plain").Doc("Get the " + ws.hostDoc + "'s ssh public keys"),
|
||||||
|
b("known_hosts").Produces("text/plain").Doc("Get the " + ws.hostDoc + "'s ssh known_hosts fragment"),
|
||||||
} {
|
} {
|
||||||
alterRB(rb)
|
alterRB(rb)
|
||||||
rws.Route(rb)
|
rws.Route(rb)
|
||||||
@@ -169,7 +200,59 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *local
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmdline := r.FormValue("cmdline")
|
||||||
|
|
||||||
|
if s := r.FormValue("serial"); s != "" {
|
||||||
|
if cmdline != "" {
|
||||||
|
cmdline += " "
|
||||||
|
}
|
||||||
|
cmdline += "console=tty0 console=ttyS" + s + ",115200"
|
||||||
|
}
|
||||||
|
|
||||||
|
_, uki := r.Form["uki"]
|
||||||
|
|
||||||
|
withUki := func(callback func(out io.Writer, ctx *renderContext, uki bool, cmdline string) (err error)) func(io.Writer, *renderContext) error {
|
||||||
|
return func(out io.Writer, ctx *renderContext) error {
|
||||||
|
return callback(out, ctx, uki, cmdline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch what {
|
switch what {
|
||||||
|
case "kernel":
|
||||||
|
err = renderKernel(w, r, ctx)
|
||||||
|
case "initrd":
|
||||||
|
err = renderCtx(w, r, ctx, what, buildInitrd)
|
||||||
|
case "uki":
|
||||||
|
uki = true
|
||||||
|
err = renderCtx(w, r, ctx, what, withUki(buildUki))
|
||||||
|
|
||||||
|
case "bootstrap.tar":
|
||||||
|
err = renderCtx(w, r, ctx, what, buildBootstrap)
|
||||||
|
|
||||||
|
case "boot.img":
|
||||||
|
err = renderCtx(w, r, ctx, what, withUki(buildBootImg))
|
||||||
|
case "boot.img.gz":
|
||||||
|
err = renderCtx(w, r, ctx, what, withUki(buildBootImgGZ))
|
||||||
|
case "boot.qcow2":
|
||||||
|
err = renderCtx(w, r, ctx, what, qemuImgBootImg("qcow2", uki, cmdline))
|
||||||
|
case "boot.qed":
|
||||||
|
err = renderCtx(w, r, ctx, what, qemuImgBootImg("qed", uki, cmdline))
|
||||||
|
case "boot.vdi":
|
||||||
|
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vdi", uki, cmdline))
|
||||||
|
case "boot.vmdk":
|
||||||
|
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vmdk", uki, cmdline))
|
||||||
|
case "boot.vpc":
|
||||||
|
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vpc", uki, cmdline))
|
||||||
|
case "boot.iso":
|
||||||
|
err = renderCtx(w, r, ctx, what, buildBootISO)
|
||||||
|
|
||||||
|
case "boot.tar":
|
||||||
|
err = renderCtx(w, r, ctx, what, withUki(buildBootTar))
|
||||||
|
|
||||||
|
// boot v2
|
||||||
|
case "bootstrap-config":
|
||||||
|
err = renderBootstrapConfig(w, ctx)
|
||||||
|
|
||||||
case "config":
|
case "config":
|
||||||
err = renderConfig(w, r, ctx, false)
|
err = renderConfig(w, r, ctx, false)
|
||||||
case "config.json":
|
case "config.json":
|
||||||
@@ -178,42 +261,10 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *local
|
|||||||
case "ipxe":
|
case "ipxe":
|
||||||
err = renderIPXE(w, ctx)
|
err = renderIPXE(w, ctx)
|
||||||
|
|
||||||
case "kernel":
|
case "ssh.pub":
|
||||||
err = renderKernel(w, r, ctx)
|
err = renderSshPub(w, ctx)
|
||||||
case "initrd":
|
case "known_hosts":
|
||||||
err = renderCtx(w, r, ctx, what, buildInitrd)
|
err = renderKnownHosts(w, ctx)
|
||||||
case "bootstrap.tar":
|
|
||||||
err = renderCtx(w, r, ctx, what, buildBootstrap)
|
|
||||||
|
|
||||||
case "boot.img":
|
|
||||||
err = renderCtx(w, r, ctx, what, buildBootImg)
|
|
||||||
case "boot.img.gz":
|
|
||||||
err = renderCtx(w, r, ctx, what, buildBootImgGZ)
|
|
||||||
case "boot.img.lz4":
|
|
||||||
err = renderCtx(w, r, ctx, what, buildBootImgLZ4)
|
|
||||||
case "boot.qcow2":
|
|
||||||
err = renderCtx(w, r, ctx, what, qemuImgBootImg("qcow2"))
|
|
||||||
case "boot.qed":
|
|
||||||
err = renderCtx(w, r, ctx, what, qemuImgBootImg("qed"))
|
|
||||||
case "boot.vdi":
|
|
||||||
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vdi"))
|
|
||||||
case "boot.vmdk":
|
|
||||||
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vmdk"))
|
|
||||||
case "boot.vpc":
|
|
||||||
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vpc"))
|
|
||||||
case "boot.iso":
|
|
||||||
err = renderCtx(w, r, ctx, what, buildBootISO)
|
|
||||||
|
|
||||||
case "boot.tar":
|
|
||||||
err = renderCtx(w, r, ctx, what, buildBootTar)
|
|
||||||
case "boot-efi.tar":
|
|
||||||
err = renderCtx(w, r, ctx, what, buildBootEFITar)
|
|
||||||
|
|
||||||
// boot v2
|
|
||||||
case "bootstrap-config":
|
|
||||||
err = renderBootstrapConfig(w, r, ctx, false)
|
|
||||||
case "bootstrap-config.json":
|
|
||||||
err = renderBootstrapConfig(w, r, ctx, true)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
@@ -224,3 +275,46 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *local
|
|||||||
http.Error(w, "", http.StatusServiceUnavailable)
|
http.Error(w, "", http.StatusServiceUnavailable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func renderSshPub(w http.ResponseWriter, ctx *renderContext) (err error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
for _, t := range sshKeyTypes {
|
||||||
|
k, err := ctx.sshHostKeyPair(t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buf.WriteString(k.Public)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write(buf.Bytes())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderKnownHosts(w http.ResponseWriter, ctx *renderContext) (err error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
names := append([]string{ctx.Host.Name},
|
||||||
|
ctx.Host.IPs...)
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
for _, t := range sshKeyTypes {
|
||||||
|
k, err := ctx.sshHostKeyPair(t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := strings.Fields(k.Public)
|
||||||
|
|
||||||
|
buf.WriteString(name)
|
||||||
|
buf.WriteByte(' ')
|
||||||
|
buf.WriteString(fields[0])
|
||||||
|
buf.WriteByte(' ')
|
||||||
|
buf.WriteString(fields[1])
|
||||||
|
buf.WriteByte('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Write(buf.Bytes())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ import (
|
|||||||
"novit.tech/direktil/local-server/secretstore"
|
"novit.tech/direktil/local-server/secretstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AddKeyReq struct {
|
||||||
|
NamedPassphrase `json:",inline"`
|
||||||
|
Hash []byte `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func wsStoreAddKey(req *restful.Request, resp *restful.Response) {
|
func wsStoreAddKey(req *restful.Request, resp *restful.Response) {
|
||||||
np := NamedPassphrase{}
|
np := AddKeyReq{}
|
||||||
|
|
||||||
err := req.ReadEntity(&np)
|
err := req.ReadEntity(&np)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -24,8 +29,13 @@ func wsStoreAddKey(req *restful.Request, resp *restful.Response) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(np.Passphrase) == 0 {
|
if len(np.Hash) == 0 && len(np.Passphrase) == 0 {
|
||||||
wsBadRequest(resp, "no passphrase given")
|
wsBadRequest(resp, "no hash or passphrase given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(np.Hash) != 0 && len(np.Hash) != 32 {
|
||||||
|
wsBadRequest(resp, "hash of a wrong length")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +46,14 @@ func wsStoreAddKey(req *restful.Request, resp *restful.Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(np.Hash) != 0 {
|
||||||
|
hash := [32]byte{}
|
||||||
|
copy(hash[:], np.Hash[:32])
|
||||||
|
secStore.AddRawKey(np.Name, hash)
|
||||||
|
} else {
|
||||||
secStore.AddKey(np.Name, np.Passphrase)
|
secStore.AddKey(np.Name, np.Passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
defer updateState()
|
defer updateState()
|
||||||
|
|
||||||
err = secStore.SaveTo(secKeysStorePath())
|
err = secStore.SaveTo(secKeysStorePath())
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ func wsError(resp *restful.Response, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func wsRender(resp *restful.Response, sslCfg *cfsslconfig.Config, tmplStr string, value interface{}) {
|
func wsRender(resp *restful.Response, sslCfg *cfsslconfig.Config, tmplStr string, value any) {
|
||||||
tmpl, err := template.New("wsRender").Funcs(templateFuncs(sslCfg)).Parse(tmplStr)
|
tmpl, err := template.New("wsRender").Funcs(templateFuncs(sslCfg)).Parse(tmplStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wsError(resp, err)
|
wsError(resp, err)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ require (
|
|||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
k8s.io/apimachinery v0.33.2
|
k8s.io/apimachinery v0.33.2
|
||||||
m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766
|
m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766
|
||||||
novit.tech/direktil/pkg v0.0.0-20260125193049-56f78e083a84
|
novit.tech/direktil/pkg v0.0.0-20260508111456-5a75785cfbbf
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/zmap/zlint/v3 => github.com/zmap/zlint/v3 v3.3.1
|
replace github.com/zmap/zlint/v3 => github.com/zmap/zlint/v3 v3.3.1
|
||||||
|
|||||||
@@ -346,5 +346,9 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8
|
|||||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||||
m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766 h1:JRzMBDbUwrTTGDJaJSH0ap4vRL0Q9CN1bG8a6n49eaQ=
|
m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766 h1:JRzMBDbUwrTTGDJaJSH0ap4vRL0Q9CN1bG8a6n49eaQ=
|
||||||
m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766/go.mod h1:BMv3aOSYpupuiiG3Ch3ND88aB5CfAks3YZuRLE8j1ls=
|
m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766/go.mod h1:BMv3aOSYpupuiiG3Ch3ND88aB5CfAks3YZuRLE8j1ls=
|
||||||
novit.tech/direktil/pkg v0.0.0-20260125193049-56f78e083a84 h1:eqLPaRpVth1WgdvprKKtc4CVF13dkxuKbo7bLzlYG6s=
|
novit.tech/direktil/pkg v0.0.0-20260210141740-4d5661fa8ecd h1:proGf8Cid9tzJzoRbqQHGGpZZKTpUDFwOREbjYrCbkM=
|
||||||
novit.tech/direktil/pkg v0.0.0-20260125193049-56f78e083a84/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10=
|
novit.tech/direktil/pkg v0.0.0-20260210141740-4d5661fa8ecd/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10=
|
||||||
|
novit.tech/direktil/pkg v0.0.0-20260221072850-b72bed72bb51 h1:NBcpvWcTBMzFos0pkuLsbVCQ+mHf8KqNOdVywMX6FFk=
|
||||||
|
novit.tech/direktil/pkg v0.0.0-20260221072850-b72bed72bb51/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10=
|
||||||
|
novit.tech/direktil/pkg v0.0.0-20260508111456-5a75785cfbbf h1:Qt4aaB1R/BjQGEAhhNoIhzQwzaycxPgxHiA9M7aEujk=
|
||||||
|
novit.tech/direktil/pkg v0.0.0-20260508111456-5a75785cfbbf/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10=
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
#! /bin/sh
|
#! /bin/sh
|
||||||
set -ex
|
set -ex
|
||||||
go build -o dist/ -trimpath -ldflags "-X main.Version=${GIT_TAG:-$(git describe --always --dirty)}" $*
|
go build -o dist/ -trimpath -ldflags "-X main.Version=${GIT_TAG:-$(git describe --always --dirty)} -X main.Date=$(date --utc +%Y%m%d_%H%M%S)" $*
|
||||||
|
|||||||
+1
-1
@@ -8,4 +8,4 @@ commit) tag=$GIT_TAG ;;
|
|||||||
"") tag=latest ;;
|
"") tag=latest ;;
|
||||||
*) tag=$1 ;;
|
*) tag=$1 ;;
|
||||||
esac
|
esac
|
||||||
docker build -t novit.tech/direktil/local-server:$tag . --build-arg GIT_TAG=$GIT_TAG
|
docker build --network=host -t novit.tech/direktil/local-server:$tag . --build-arg GIT_TAG=$GIT_TAG
|
||||||
|
|||||||
@@ -10,17 +10,17 @@ const Downloads = {
|
|||||||
host: [
|
host: [
|
||||||
"kernel",
|
"kernel",
|
||||||
"initrd",
|
"initrd",
|
||||||
|
"uki",
|
||||||
"bootstrap.tar",
|
"bootstrap.tar",
|
||||||
"boot.img.lz4",
|
|
||||||
"boot.img.gz",
|
"boot.img.gz",
|
||||||
|
"boot.img",
|
||||||
"boot.qcow2",
|
"boot.qcow2",
|
||||||
"boot.vmdk",
|
"boot.vmdk",
|
||||||
"boot.img",
|
|
||||||
"boot.iso",
|
"boot.iso",
|
||||||
"boot.tar",
|
"boot.tar",
|
||||||
"boot-efi.tar",
|
|
||||||
"config",
|
|
||||||
"bootstrap-config",
|
"bootstrap-config",
|
||||||
|
"config",
|
||||||
|
"config.json",
|
||||||
"ipxe",
|
"ipxe",
|
||||||
],
|
],
|
||||||
}[this.kind]
|
}[this.kind]
|
||||||
@@ -103,7 +103,12 @@ createApp({
|
|||||||
return {Name: this.forms.store.name, Passphrase: btoa(this.forms.store.pass1)}
|
return {Name: this.forms.store.name, Passphrase: btoa(this.forms.store.pass1)}
|
||||||
},
|
},
|
||||||
storeAddKey() {
|
storeAddKey() {
|
||||||
this.apiPost('/store/add-key', this.namedPassphrase(), (v) => {
|
const params = this.namedPassphrase();
|
||||||
|
if (this.forms.store.byHash) {
|
||||||
|
params.Hash = this.forms.store.hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.apiPost('/store/add-key', params, (v) => {
|
||||||
this.forms.store = {}
|
this.forms.store = {}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
+7
-2
@@ -10,18 +10,23 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.downloads, .download-links {
|
.downloads, .download-links, .download-table td {
|
||||||
& > * {
|
& > * {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 1ex;
|
margin-right: 1ex;
|
||||||
margin-bottom: 1ex;
|
margin-bottom: 1ex;
|
||||||
padding: 0.5ex;
|
padding: 0.5ex;
|
||||||
border: 1px solid;
|
border: 1px solid !important;
|
||||||
border-radius: 1ex;
|
border-radius: 1ex;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.download-table th,
|
||||||
|
.download-table td {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.downloads, .view-links {
|
.downloads, .view-links {
|
||||||
& > .selected {
|
& > .selected {
|
||||||
color: var(--link);
|
color: var(--link);
|
||||||
|
|||||||
+9
-3
@@ -10,8 +10,8 @@
|
|||||||
|
|
||||||
<style>@import url('/ui/style.css');@import url('/ui/app.css');</style>
|
<style>@import url('/ui/style.css');@import url('/ui/app.css');</style>
|
||||||
<script src="/ui/jsonpatch.min-942279a1c4209cc2.js" integrity="sha384-GcPrkRS12jtrElEkbJcrZ8asvvb9s3mc+CUq9UW/8bL4L0bpkmh9M+oFnWN6qLq2"></script>
|
<script src="/ui/jsonpatch.min-942279a1c4209cc2.js" integrity="sha384-GcPrkRS12jtrElEkbJcrZ8asvvb9s3mc+CUq9UW/8bL4L0bpkmh9M+oFnWN6qLq2"></script>
|
||||||
<script src="/ui/app-7f4645e84eaae7ce.js" defer integrity="sha384-31uiQnXVSPLDq61o+chfyKRkSdkmzv023M7KafOo+0xRw5AM70BUFyYO6yo0kPiZ" type="module"></script>
|
<script src="/ui/app-e7fb26679b9aa0f2.js" defer integrity="sha384-4oRQalb7IIBcqQzfDkeCj53qYOP6dLsTwqcjnm3EiBa92oNDD3chUw38W2gEC+3p" type="module"></script>
|
||||||
<script src="/ui/Downloads-c9374f19f52c46d.js" integrity="sha384-WQIkCSxlUkvu4jFIWwS3bESMaazGwwnBn9dyvB7nItz3L8BmusRMsJACzfJa7sC4"></script>
|
<script src="/ui/Downloads-29497c61f1fe9bf0.js" integrity="sha384-xwn49JflUBaZlQCHCn55Q9qSGqsw01Job+TXk53HLhvNhKAALN18+gNCdF0bUJW0"></script>
|
||||||
<script src="/ui/GetCopy-7e3c9678f9647d40.js" integrity="sha384-LzxUXylxE/t25HyTch8y2qvKcehvP2nqCo37swIBGEKZZUzHVJVQrS5UJDWNskTA"></script>
|
<script src="/ui/GetCopy-7e3c9678f9647d40.js" integrity="sha384-LzxUXylxE/t25HyTch8y2qvKcehvP2nqCo37swIBGEKZZUzHVJVQrS5UJDWNskTA"></script>
|
||||||
<script src="/ui/Cluster-703dcdca97841304.js" integrity="sha384-ifGpq/GB1nDfqczm5clTRtcfnc9UMkT+SptMyS3UG9Di3xoKORtOGGjmK5RnJx+v"></script>
|
<script src="/ui/Cluster-703dcdca97841304.js" integrity="sha384-ifGpq/GB1nDfqczm5clTRtcfnc9UMkT+SptMyS3UG9Di3xoKORtOGGjmK5RnJx+v"></script>
|
||||||
<script src="/ui/Host-61916516a854adff.js" integrity="sha384-/wh3KrC0sb4MT7ekO2U84rswxI42WSH/0jB4dbDaaGaGhX60xTEZHFsdQAf7UgTG"></script>
|
<script src="/ui/Host-61916516a854adff.js" integrity="sha384-/wh3KrC0sb4MT7ekO2U84rswxI42WSH/0jB4dbDaaGaGhX60xTEZHFsdQAf7UgTG"></script>
|
||||||
@@ -106,9 +106,15 @@
|
|||||||
<form @submit="storeAddKey" action="/store/add-key">
|
<form @submit="storeAddKey" action="/store/add-key">
|
||||||
<p>Add an unlock phrase:</p>
|
<p>Add an unlock phrase:</p>
|
||||||
<input type="text" v-model="forms.store.name" name="name" required placeholder="Name" /><br/>
|
<input type="text" v-model="forms.store.name" name="name" required placeholder="Name" /><br/>
|
||||||
|
<label><input type="checkbox" v-model="forms.store.byHash"> {{ forms.store.byHash ? "hash (base64)" : "passphrase"}}</label>
|
||||||
|
<div v-if="forms.store.byHash">
|
||||||
|
<input type="hash" v-model="forms.store.hash" name="passphrase" required placeholder="Hash" />
|
||||||
|
{{" "}}generate with <code>dls hash '{{state.Store.Salt}}'</code>
|
||||||
|
</div><div v-else>
|
||||||
<input type="password" v-model="forms.store.pass1" name="passphrase" autocomplete="new-password" required placeholder="Phrase" />
|
<input type="password" v-model="forms.store.pass1" name="passphrase" autocomplete="new-password" required placeholder="Phrase" />
|
||||||
<input type="password" v-model="forms.store.pass2" autocomplete="new-password" required placeholder="Phrase confirmation" />
|
<input type="password" v-model="forms.store.pass2" autocomplete="new-password" required placeholder="Phrase confirmation" />
|
||||||
<input type="submit" value="add unlock phrase" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
</div>
|
||||||
|
<input type="submit" value="add unlock phrase" :disabled="!((forms.store.pass1 && forms.store.pass1 == forms.store.pass2) || forms.store.hash)" />
|
||||||
</form>
|
</form>
|
||||||
<form @submit="storeDelKey" action="/store/delete-key">
|
<form @submit="storeDelKey" action="/store/delete-key">
|
||||||
<p>Remove an unlock phrase:</p>
|
<p>Remove an unlock phrase:</p>
|
||||||
|
|||||||
+3
-1
@@ -48,9 +48,11 @@ th, td {
|
|||||||
border-bottom: dotted 1pt;
|
border-bottom: dotted 1pt;
|
||||||
padding: 2pt 4pt;
|
padding: 2pt 4pt;
|
||||||
}
|
}
|
||||||
tr:first-child > th {
|
tr:first-child {
|
||||||
|
th, td {
|
||||||
border-top: dotted 1pt;
|
border-top: dotted 1pt;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
th, tr:last-child > td {
|
th, tr:last-child > td {
|
||||||
border-bottom: solid 1pt;
|
border-bottom: solid 1pt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,10 +211,13 @@ func (s *Store) Unlock(passphrase []byte) (ok bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) AddKey(name string, passphrase []byte) {
|
func (s *Store) AddKey(name string, passphrase []byte) {
|
||||||
key, hash := s.keyPairFromPassword(passphrase)
|
key, _ := s.keyPairFromPassword(passphrase)
|
||||||
memzero(passphrase)
|
memzero(passphrase)
|
||||||
|
s.AddRawKey(name, key)
|
||||||
|
}
|
||||||
|
|
||||||
defer memzero(key[:])
|
func (s *Store) AddRawKey(name string, key [32]byte) {
|
||||||
|
hash := sha512.Sum512(key[:])
|
||||||
|
|
||||||
k := KeyEntry{Name: name, Hash: hash}
|
k := KeyEntry{Name: name, Hash: hash}
|
||||||
|
|
||||||
@@ -222,6 +225,7 @@ func (s *Store) AddKey(name string, passphrase []byte) {
|
|||||||
copy(k.EncKey[:], encKey)
|
copy(k.EncKey[:], encKey)
|
||||||
|
|
||||||
s.Keys = append(s.Keys, k)
|
s.Keys = append(s.Keys, k)
|
||||||
|
memzero(key[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) keyPairFromPassword(password []byte) (key [32]byte, hash [64]byte) {
|
func (s *Store) keyPairFromPassword(password []byte) (key [32]byte, hash [64]byte) {
|
||||||
|
|||||||
+7
-2
@@ -10,18 +10,23 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.downloads, .download-links {
|
.downloads, .download-links, .download-table td {
|
||||||
& > * {
|
& > * {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 1ex;
|
margin-right: 1ex;
|
||||||
margin-bottom: 1ex;
|
margin-bottom: 1ex;
|
||||||
padding: 0.5ex;
|
padding: 0.5ex;
|
||||||
border: 1px solid;
|
border: 1px solid !important;
|
||||||
border-radius: 1ex;
|
border-radius: 1ex;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.download-table th,
|
||||||
|
.download-table td {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.downloads, .view-links {
|
.downloads, .view-links {
|
||||||
& > .selected {
|
& > .selected {
|
||||||
color: var(--link);
|
color: var(--link);
|
||||||
|
|||||||
+7
-1
@@ -106,9 +106,15 @@
|
|||||||
<form @submit="storeAddKey" action="/store/add-key">
|
<form @submit="storeAddKey" action="/store/add-key">
|
||||||
<p>Add an unlock phrase:</p>
|
<p>Add an unlock phrase:</p>
|
||||||
<input type="text" v-model="forms.store.name" name="name" required placeholder="Name" /><br/>
|
<input type="text" v-model="forms.store.name" name="name" required placeholder="Name" /><br/>
|
||||||
|
<label><input type="checkbox" v-model="forms.store.byHash"> {{ forms.store.byHash ? "hash (base64)" : "passphrase"}}</label>
|
||||||
|
<div v-if="forms.store.byHash">
|
||||||
|
<input type="hash" v-model="forms.store.hash" name="passphrase" required placeholder="Hash" />
|
||||||
|
{{" "}}generate with <code>dls hash '{{state.Store.Salt}}'</code>
|
||||||
|
</div><div v-else>
|
||||||
<input type="password" v-model="forms.store.pass1" name="passphrase" autocomplete="new-password" required placeholder="Phrase" />
|
<input type="password" v-model="forms.store.pass1" name="passphrase" autocomplete="new-password" required placeholder="Phrase" />
|
||||||
<input type="password" v-model="forms.store.pass2" autocomplete="new-password" required placeholder="Phrase confirmation" />
|
<input type="password" v-model="forms.store.pass2" autocomplete="new-password" required placeholder="Phrase confirmation" />
|
||||||
<input type="submit" value="add unlock phrase" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
</div>
|
||||||
|
<input type="submit" value="add unlock phrase" :disabled="!((forms.store.pass1 && forms.store.pass1 == forms.store.pass2) || forms.store.hash)" />
|
||||||
</form>
|
</form>
|
||||||
<form @submit="storeDelKey" action="/store/delete-key">
|
<form @submit="storeDelKey" action="/store/delete-key">
|
||||||
<p>Remove an unlock phrase:</p>
|
<p>Remove an unlock phrase:</p>
|
||||||
|
|||||||
+4
-4
@@ -10,17 +10,17 @@ const Downloads = {
|
|||||||
host: [
|
host: [
|
||||||
"kernel",
|
"kernel",
|
||||||
"initrd",
|
"initrd",
|
||||||
|
"uki",
|
||||||
"bootstrap.tar",
|
"bootstrap.tar",
|
||||||
"boot.img.lz4",
|
|
||||||
"boot.img.gz",
|
"boot.img.gz",
|
||||||
|
"boot.img",
|
||||||
"boot.qcow2",
|
"boot.qcow2",
|
||||||
"boot.vmdk",
|
"boot.vmdk",
|
||||||
"boot.img",
|
|
||||||
"boot.iso",
|
"boot.iso",
|
||||||
"boot.tar",
|
"boot.tar",
|
||||||
"boot-efi.tar",
|
|
||||||
"config",
|
|
||||||
"bootstrap-config",
|
"bootstrap-config",
|
||||||
|
"config",
|
||||||
|
"config.json",
|
||||||
"ipxe",
|
"ipxe",
|
||||||
],
|
],
|
||||||
}[this.kind]
|
}[this.kind]
|
||||||
|
|||||||
+6
-1
@@ -103,7 +103,12 @@ createApp({
|
|||||||
return {Name: this.forms.store.name, Passphrase: btoa(this.forms.store.pass1)}
|
return {Name: this.forms.store.name, Passphrase: btoa(this.forms.store.pass1)}
|
||||||
},
|
},
|
||||||
storeAddKey() {
|
storeAddKey() {
|
||||||
this.apiPost('/store/add-key', this.namedPassphrase(), (v) => {
|
const params = this.namedPassphrase();
|
||||||
|
if (this.forms.store.byHash) {
|
||||||
|
params.Hash = this.forms.store.hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.apiPost('/store/add-key', params, (v) => {
|
||||||
this.forms.store = {}
|
this.forms.store = {}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-1
@@ -48,9 +48,11 @@ th, td {
|
|||||||
border-bottom: dotted 1pt;
|
border-bottom: dotted 1pt;
|
||||||
padding: 2pt 4pt;
|
padding: 2pt 4pt;
|
||||||
}
|
}
|
||||||
tr:first-child > th {
|
tr:first-child {
|
||||||
|
th, td {
|
||||||
border-top: dotted 1pt;
|
border-top: dotted 1pt;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
th, tr:last-child > td {
|
th, tr:last-child > td {
|
||||||
border-bottom: solid 1pt;
|
border-bottom: solid 1pt;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user