add tons of shit to enroll

This commit is contained in:
Melora Hugues 2023-08-22 22:39:23 +02:00
parent 475a444129
commit 712df4c190
8 changed files with 519 additions and 44 deletions

View file

@ -3,3 +3,6 @@
build: build:
mkdir -p build/ mkdir -p build/
go build -o build/ go build -o build/
push-client:
scp build/config root@192.168.122.2:/usr/bin/efi-http-config

104
config/efivar/efivar.go Normal file
View file

@ -0,0 +1,104 @@
package efivar
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strconv"
"strings"
"github.com/google/uuid"
)
// const efiVarDir = "/sys/firmware/efi/efivars"
const execPath = "/usr/bin/efivar"
var VendorID = uuid.MustParse("638a24ce-a0ce-4908-8152-b2a4e8b2f29d")
const (
VarBootUUID = "HTTP_BOOT_UUID"
VarBootPort = "HTTP_BOOT_MCAST_PORT"
VarBootGroup = "HTTP_BOOT_MCAST_GROUP"
VarBootIP = "HTTP_BOOT_IP"
)
var ErrNotFound = errors.New("variable not found")
func toFullVarName(gid uuid.UUID, varName string) string {
return strings.Join([]string{gid.String(), varName}, "-")
}
func numSliceToBytes(in []string) ([]byte, error) {
res := []byte{}
for i, n := range in {
strippedN := strings.TrimSpace(n)
if len(strippedN) == 0 {
continue
}
intN, err := strconv.Atoi(strippedN)
if err != nil {
return nil, fmt.Errorf("invalid number %s at index %d", n, i)
}
if intN < 0 || intN > 255 {
return nil, fmt.Errorf("value %d at index %d out of range", intN, i)
}
res = append(res, byte(intN))
}
return res, nil
}
func GetVar(gid uuid.UUID, varName string) ([]byte, error) {
cmd := exec.Command(execPath, "-n", toFullVarName(gid, varName), "-d")
var outBuf, errBuf bytes.Buffer
cmd.Stdout = &outBuf
cmd.Stderr = &errBuf
err := cmd.Run()
var exitErr exec.ExitError
e := &exitErr
if err != nil && !errors.As(err, &e) {
return nil, err
}
if cmd.ProcessState.ExitCode() != 0 {
stderr := errBuf.String()
if strings.Contains(stderr, "No such file or directory") {
return nil, ErrNotFound
}
return nil, fmt.Errorf("error getting variable, returncode %d, stderr %q", cmd.ProcessState.ExitCode(), stderr)
}
nums := strings.Split(outBuf.String(), " ")
return numSliceToBytes(nums)
}
func SetVar(gid uuid.UUID, varName string, value []byte) error {
tmpVarFile, err := os.CreateTemp("/tmp", "efitmpvar")
if err != nil {
return fmt.Errorf("failed to create tmp file to write variable: %w", err)
}
defer tmpVarFile.Close()
defer os.Remove(tmpVarFile.Name())
if err := ioutil.WriteFile(tmpVarFile.Name(), value, 0o500); err != nil {
return fmt.Errorf("failed to write variable value to tmp file: %w", err)
}
cmd := exec.Command(execPath, "-n", toFullVarName(gid, varName), "-w", "-f", tmpVarFile.Name())
var errBuf bytes.Buffer
cmd.Stderr = &errBuf
err = cmd.Run()
var exitErr exec.ExitError
e := &exitErr
if err != nil && !errors.As(err, &e) {
return err
}
if cmd.ProcessState.ExitCode() != 0 {
stderr := errBuf.String()
return fmt.Errorf("error setting variable, returncode %d, stderr %q", cmd.ProcessState.ExitCode(), stderr)
}
return nil
}

View file

@ -0,0 +1,32 @@
package efivar
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNumSliceToBytes(t *testing.T) {
t.Run("OK", func(t *testing.T) {
expected := []byte{12, 53, 124, 84}
input := []string{"12", "53", "124", "84"}
res, err := numSliceToBytes(input)
require.NoError(t, err)
assert.Equal(t, expected, res)
})
t.Run("Err - invalid byte", func(t *testing.T) {
input := []string{"12", "11453", "124"}
res, err := numSliceToBytes(input)
require.ErrorContains(t, err, "value 11453 at index 1 out of range")
assert.Nil(t, res)
})
t.Run("Err - not numbers", func(t *testing.T) {
input := []string{"12", "toto", "124"}
res, err := numSliceToBytes(input)
require.ErrorContains(t, err, "invalid number toto at index 1")
assert.Nil(t, res)
})
}

View file

@ -1,13 +1,20 @@
package main package main
import ( import (
"bytes"
"context" "context"
"encoding/binary"
"encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"io/fs" "io/fs"
"io/ioutil"
"net"
"os" "os"
"path"
"git.faercol.me/faercol/http-boot-config/config/efivar"
"git.faercol.me/faercol/http-boot-config/config/logger" "git.faercol.me/faercol/http-boot-config/config/logger"
"git.faercol.me/faercol/http-boot-config/config/prober" "git.faercol.me/faercol/http-boot-config/config/prober"
"git.faercol.me/faercol/http-boot-config/config/remote" "git.faercol.me/faercol/http-boot-config/config/remote"
@ -20,17 +27,30 @@ const (
actionList action = iota actionList action = iota
actionGetRemote actionGetRemote
actionEnroll actionEnroll
actionStatus
actionUnknown actionUnknown
) )
const (
defaultEFIDest = "/EFI/httpboot/"
defaultEFIMount = "/boot"
defaultEFISrc = "httpboot.efi"
)
type cliArgs struct { type cliArgs struct {
debug bool debug bool
colour bool colour bool
action action action action
id uuid.UUID id uuid.UUID
remoteAddr string remoteAddr string
prettyPrint bool prettyPrint bool
name string name string
efiDest string
efiMountPoint string
efiDisk string
efiSrc string
ifaceName string
install bool
} }
var defaultArgs cliArgs = cliArgs{ var defaultArgs cliArgs = cliArgs{
@ -45,6 +65,9 @@ func parseArgs() (cliArgs, error) {
listFlagSet := flag.NewFlagSet("list", flag.ExitOnError) listFlagSet := flag.NewFlagSet("list", flag.ExitOnError)
statusFlagSet := flag.NewFlagSet("status", flag.ExitOnError)
hostStatusFlag := statusFlagSet.String("remote-host", "http://localhost:5000", "Address for the remote boot server")
getRemoteFlagSet := flag.NewFlagSet("get-remote", flag.ExitOnError) getRemoteFlagSet := flag.NewFlagSet("get-remote", flag.ExitOnError)
uuidFlag := getRemoteFlagSet.String("uuid", "", "Client UUID") uuidFlag := getRemoteFlagSet.String("uuid", "", "Client UUID")
hostGetRemoteFlag := getRemoteFlagSet.String("remote-host", "http://localhost:5000", "Address for the remote boot server") hostGetRemoteFlag := getRemoteFlagSet.String("remote-host", "http://localhost:5000", "Address for the remote boot server")
@ -53,6 +76,12 @@ func parseArgs() (cliArgs, error) {
enrollFlagSet := flag.NewFlagSet("enroll", flag.ExitOnError) enrollFlagSet := flag.NewFlagSet("enroll", flag.ExitOnError)
hostEnrollFlag := enrollFlagSet.String("remote-host", "http://localhost:5000", "Address for the remote boot server") hostEnrollFlag := enrollFlagSet.String("remote-host", "http://localhost:5000", "Address for the remote boot server")
nameFlag := enrollFlagSet.String("name", "default", "Name for the client on the remote boot server") nameFlag := enrollFlagSet.String("name", "default", "Name for the client on the remote boot server")
efiSrcFlag := enrollFlagSet.String("src", defaultEFISrc, "HTTP EFI app to install")
efiDestFlag := enrollFlagSet.String("dest", defaultEFIDest, "Directory in which to store the EFI app")
efiMountPointFlag := enrollFlagSet.String("mountpoint", defaultEFIMount, "EFI partition mountpoint")
installFlag := enrollFlagSet.Bool("install", false, "Install the EFI app in the system")
efiDiskFlag := enrollFlagSet.String("disk", "/dev/sda1", "Partition in which to install the EFI app")
ifaceNameFlag := enrollFlagSet.String("iface", "eth0", "Iface name to use for the boot loader")
debugFlag := flag.Bool("debug", false, "Display debug logs") debugFlag := flag.Bool("debug", false, "Display debug logs")
noColourFlag := flag.Bool("no-colour", false, "Disable colour logs") noColourFlag := flag.Bool("no-colour", false, "Disable colour logs")
@ -65,6 +94,8 @@ func parseArgs() (cliArgs, error) {
args.action = actionGetRemote args.action = actionGetRemote
case "enroll": case "enroll":
args.action = actionEnroll args.action = actionEnroll
case "status":
args.action = actionStatus
default: default:
continue continue
} }
@ -74,6 +105,9 @@ func parseArgs() (cliArgs, error) {
switch args.action { switch args.action {
case actionList: case actionList:
listFlagSet.Parse(os.Args[firstArg:]) listFlagSet.Parse(os.Args[firstArg:])
case actionStatus:
statusFlagSet.Parse(os.Args[firstArg:])
args.remoteAddr = *hostStatusFlag
case actionGetRemote: case actionGetRemote:
getRemoteFlagSet.Parse(os.Args[firstArg:]) getRemoteFlagSet.Parse(os.Args[firstArg:])
parsedID, err := uuid.Parse(*uuidFlag) parsedID, err := uuid.Parse(*uuidFlag)
@ -87,6 +121,12 @@ func parseArgs() (cliArgs, error) {
enrollFlagSet.Parse(os.Args[firstArg:]) enrollFlagSet.Parse(os.Args[firstArg:])
args.remoteAddr = *hostEnrollFlag args.remoteAddr = *hostEnrollFlag
args.name = *nameFlag args.name = *nameFlag
args.efiSrc = *efiSrcFlag
args.efiDest = *efiDestFlag
args.install = *installFlag
args.efiDisk = *efiDiskFlag
args.efiMountPoint = *efiMountPointFlag
args.ifaceName = *ifaceNameFlag
default: default:
flag.Parse() flag.Parse()
return cliArgs{}, errors.New("missing an action") return cliArgs{}, errors.New("missing an action")
@ -100,7 +140,7 @@ func parseArgs() (cliArgs, error) {
func displayAppList(l *logger.SimpleLogger) { func displayAppList(l *logger.SimpleLogger) {
l.Info("Checking EFI directory for available boot images...") l.Info("Checking EFI directory for available boot images...")
apps, err := prober.GetEFIApps(l) apps, err := prober.GetEFIApps()
if err != nil { if err != nil {
if errors.Is(err, fs.ErrPermission) { if errors.Is(err, fs.ErrPermission) {
l.Fatal("Permission error, try to run the command as sudo") l.Fatal("Permission error, try to run the command as sudo")
@ -131,11 +171,196 @@ func getRemoteConfig(l *logger.SimpleLogger, host string, id uuid.UUID, pretty b
} }
} }
func enroll(l *logger.SimpleLogger, host, name string) { func enroll(l *logger.SimpleLogger, host, name, src, dest, destPart, mount, iface string, install bool) {
if install {
l.Debug("Installing boot application")
if err := installApp(l, src, dest, destPart, mount); err != nil {
l.Fatalf("Failed to install boot application: %s", err.Error())
}
}
l.Info("Enrolling client") l.Info("Enrolling client")
if err := remote.Enroll(context.Background(), host, name, l); err != nil { clientConf, err := remote.Enroll(context.Background(), host, name, l)
if err != nil {
l.Fatal(err.Error()) l.Fatal(err.Error())
} }
if install {
l.Debug("Setting EFI vars")
if err := installVars(l, clientConf.ID, clientConf.MulticastPort, clientConf.MulticastGroup, iface); err != nil {
l.Fatalf("Failed to install efi vars: %s", err.Error())
}
}
}
func getEFIConf() (clientID uuid.UUID, port int, group net.IP, ip net.IP, err error) {
idRaw, err := efivar.GetVar(efivar.VendorID, efivar.VarBootUUID)
if err != nil {
return
}
clientID, err = uuid.ParseBytes(idRaw)
if err != nil {
err = fmt.Errorf("invalid uuid %q: %w", string(idRaw), err)
return
}
portRaw, err := efivar.GetVar(efivar.VendorID, efivar.VarBootPort)
if err != nil {
return
}
if len(portRaw) == 1 {
portRaw = append(portRaw, 0)
}
port = int(binary.LittleEndian.Uint16(portRaw))
groupRaw, err := efivar.GetVar(efivar.VendorID, efivar.VarBootGroup)
if err != nil {
return
}
group = net.IP(groupRaw)
ipRaw, err := efivar.GetVar(efivar.VendorID, efivar.VarBootIP)
if err != nil {
return
}
ip = net.IP(ipRaw)
return
}
func getStatus(l *logger.SimpleLogger, host string) {
l.Debug("Checking EFI vars")
clientID, port, group, ip, err := getEFIConf()
if err != nil {
if errors.Is(err, efivar.ErrNotFound) {
l.Info("Boot client is not configured")
return
}
l.Fatalf("Failed to get EFI configuration: %s", err.Error())
}
l.Infof("Registered client ID %s", clientID.String())
l.Infof("Registered multicast group %s:%d", group.String(), port)
l.Infof("HTTP_BOOT client using local ip address %s", ip.String())
l.Debug("Getting remote config")
remoteRaw, err := remote.GetRemoteConfig(context.Background(), host, clientID, l)
if err != nil {
l.Fatalf("Failed to get remote config: %s", err.Error())
}
var remoteConf remote.ClientConfig
if err := json.Unmarshal(remoteRaw, &remoteConf); err != nil {
l.Fatalf("Invalid remote config: %s", err.Error())
}
l.Infof("Remote HTTP Boot configured and set to boot to %s", remoteConf.EFIConfig.Options[remoteConf.EFIConfig.SelectedOption].Name)
l.Debug("Checking selected EFI image")
apps, err := prober.GetEFIApps()
if err != nil {
if errors.Is(err, fs.ErrPermission) {
l.Fatal("Permission error, try to run the command as sudo")
}
l.Fatalf("Failed to check EFI directory: %s", err.Error())
}
installed := false
for _, a := range apps {
if a.Name == prober.HTTPBootLabel {
installed = true
l.Info("HTTP_BOOT efi application found in the EFI partitions")
}
}
if !installed {
l.Warning("HTTP_BOOT efi application is not installed")
return
}
httpNext, err := prober.IsHTTPBootNext()
if err != nil {
l.Fatalf("Failed to check EFI app selected for next boot: %s", err.Error())
}
if httpNext {
l.Info("HTTP_BOOT set for next boot")
} else {
l.Warning("HTTP_BOOT is not set for next boot")
}
}
func getIPAddr(ifaceName string) (net.IP, error) {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return nil, fmt.Errorf("failed to get iface: %w", err)
}
addrs, err := iface.Addrs()
if err != nil {
return nil, fmt.Errorf("failed to get addresses for iface: %w", err)
}
for _, a := range addrs {
parsedIP, _, err := net.ParseCIDR(a.String())
if err != nil {
return nil, fmt.Errorf("invalid addr %s: %w", a.String(), err)
}
if parsedIP.IsLinkLocalUnicast() && parsedIP.To4() == nil {
return parsedIP, nil
}
}
return nil, errors.New("no link-local ipv6 found")
}
func installApp(l *logger.SimpleLogger, srcPath, destDir, destPart, mount string) error {
destFullDir := path.Join(mount, destDir)
l.Debug("Creating dest directory")
if err := os.MkdirAll(destFullDir, 0o755); err != nil {
return fmt.Errorf("failed to create dest directory: %w", err)
}
l.Debugf("Opening source file %s for copy", srcPath)
input, err := ioutil.ReadFile(srcPath)
if err != nil {
return fmt.Errorf("failed to open source file for copy: %w", err)
}
dest := path.Join(destFullDir, path.Base(srcPath))
l.Debugf("Copying efi image to %s", dest)
if err := ioutil.WriteFile(dest, input, 0o755); err != nil {
return fmt.Errorf("failed to copy efi image: %w", err)
}
l.Debug("Deleting source file after copy")
if err := os.Remove(srcPath); err != nil {
return fmt.Errorf("failed to remove source file after copy: %w", err)
}
l.Debug("Installing app in efibootmgr")
if err := prober.Install(path.Join(destDir, path.Base(srcPath)), destPart); err != nil {
return fmt.Errorf("failed to install EFI app in efibootmgr: %w", err)
}
return nil
}
func installVars(l *logger.SimpleLogger, clientID uuid.UUID, port int, group net.IP, ifaceName string) error {
l.Debug("Setting multicast port")
buf := new(bytes.Buffer)
if err := binary.Write(buf, binary.LittleEndian, uint16(port)); err != nil {
return fmt.Errorf("invalid port format: %w", err)
}
if err := efivar.SetVar(efivar.VendorID, efivar.VarBootPort, buf.Bytes()); err != nil {
return fmt.Errorf("failed to set port number: %w", err)
}
l.Debug("Setting multicast group")
if err := efivar.SetVar(efivar.VendorID, efivar.VarBootGroup, group); err != nil {
return fmt.Errorf("failed to set multicast group: %w", err)
}
l.Debug("Setting UUID")
if err := efivar.SetVar(efivar.VendorID, efivar.VarBootUUID, []byte(clientID.String())); err != nil {
return fmt.Errorf("failed to set client UUID: %w", err)
}
l.Debug("Setting link local IPv6")
ipAddr, err := getIPAddr(ifaceName)
if err != nil {
return fmt.Errorf("failed to get ipv6 link local address: %w", err)
}
if err := efivar.SetVar(efivar.VendorID, efivar.VarBootIP, ipAddr); err != nil {
return fmt.Errorf("failed to set ip address: %w", err)
}
return nil
} }
func main() { func main() {
@ -150,10 +375,12 @@ func main() {
switch args.action { switch args.action {
case actionList: case actionList:
displayAppList(l) displayAppList(l)
case actionStatus:
getStatus(l, args.remoteAddr)
case actionGetRemote: case actionGetRemote:
getRemoteConfig(l, args.remoteAddr, args.id, args.prettyPrint) getRemoteConfig(l, args.remoteAddr, args.id, args.prettyPrint)
case actionEnroll: case actionEnroll:
enroll(l, args.remoteAddr, args.name) enroll(l, args.remoteAddr, args.name, args.efiSrc, args.efiDest, args.efiDisk, args.efiMountPoint, args.ifaceName, args.install)
default: default:
l.Fatal("Unknown action") l.Fatal("Unknown action")
} }

View file

@ -8,12 +8,15 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"git.faercol.me/faercol/http-boot-config/config/logger"
) )
var efiBootmgrRegexp = regexp.MustCompile(`Boot(?P<id>\d+)\* (?P<name>[\w ]+)\t(HD\(.+,.+,(?P<disk_id>[0-9a-f-]+),.+,.+\))/File\((?P<filepath>.+)\)`) var efiBootmgrRegexp = regexp.MustCompile(`Boot(?P<id>[0-9A-F]+)\* (?P<name>.+)\t(.+\(.+,.+,(?P<disk_id>[0-9a-f-]+),.+,.+\))/File\((?P<filepath>.+)\)`)
var activeRegexp = regexp.MustCompile(`BootCurrent: (\d+)`) var activeRegexp = regexp.MustCompile(`BootCurrent: (\d+)`)
var bootOrderRegexp = regexp.MustCompile(`BootOrder: ((?:[0-9A-F]{4},?)+)`)
const HTTPBootName = "httpboot.efi"
const HTTPBootLabel = "HTTP_BOOT"
const execPath = "/usr/bin/efibootmgr"
type EfiApp struct { type EfiApp struct {
ID int ID int
@ -38,7 +41,7 @@ func efiAppFromBootMgrOutput(rawVal string, activeId int) (app EfiApp, ok bool)
if !ok { if !ok {
return return
} }
idInt, err := strconv.Atoi(id) idInt, err := strconv.ParseInt(id, 16, 32)
if err != nil { if err != nil {
return app, false return app, false
} }
@ -55,10 +58,10 @@ func efiAppFromBootMgrOutput(rawVal string, activeId int) (app EfiApp, ok bool)
return return
} }
return EfiApp{ return EfiApp{
ID: idInt, ID: int(idInt),
Name: strings.TrimSpace(name), Name: strings.TrimSpace(name),
Path: strings.TrimSpace(filepath), Path: strings.TrimSpace(filepath),
Active: idInt == activeId, Active: int(idInt) == activeId,
DiskID: disk_id, DiskID: disk_id,
}, true }, true
} }
@ -76,7 +79,7 @@ func getActiveEFIApp(output string) (int, error) {
return id, nil return id, nil
} }
func GetEFIApps(log *logger.SimpleLogger) (apps []EfiApp, err error) { func GetEFIApps() (apps []EfiApp, err error) {
cmd := exec.Command("/usr/bin/efibootmgr") cmd := exec.Command("/usr/bin/efibootmgr")
var out bytes.Buffer var out bytes.Buffer
cmd.Stdout = &out cmd.Stdout = &out
@ -96,13 +99,94 @@ func GetEFIApps(log *logger.SimpleLogger) (apps []EfiApp, err error) {
} }
for _, l := range strings.Split(outStr, "\n") { for _, l := range strings.Split(outStr, "\n") {
log.Debugf("Parsing line %q", l)
app, ok := efiAppFromBootMgrOutput(l, activeBootID) app, ok := efiAppFromBootMgrOutput(l, activeBootID)
if !ok { if !ok {
log.Debug("Line is not a valid EFI application")
continue continue
} }
apps = append(apps, app) apps = append(apps, app)
} }
return return
} }
func Installed() (bool, error) {
apps, err := GetEFIApps()
if err != nil {
return false, fmt.Errorf("failed to get list of EFI applications: %w", err)
}
for _, a := range apps {
if a.Name == HTTPBootLabel {
return true, nil
}
}
return false, nil
}
func getHTTPBoot() (EfiApp, error) {
apps, err := GetEFIApps()
if err != nil {
return EfiApp{}, fmt.Errorf("failed to get installed EFI applications: %w", err)
}
for _, a := range apps {
if a.Name == HTTPBootLabel {
return a, nil
}
}
return EfiApp{}, errors.New("HTTP_BOOT not found")
}
func IsHTTPBootNext() (present bool, err error) {
httpBootApp, err := getHTTPBoot()
if err != nil {
return false, fmt.Errorf("failed to get HTTP_BOOT config: %w", err)
}
cmd := exec.Command("/usr/bin/efibootmgr")
var out bytes.Buffer
cmd.Stdout = &out
err = cmd.Run()
if err != nil {
return
}
if cmd.ProcessState.ExitCode() != 0 {
return false, fmt.Errorf("error running efibootmgr, returncode %d", cmd.ProcessState.ExitCode())
}
outStr := out.String()
match := bootOrderRegexp.FindStringSubmatch(outStr)
if len(match) == 0 {
return false, errors.New("no boot order found")
}
orderedBootOptionsStr := strings.Split(match[1], ",")
var orderedBootOptionsInt []int
for _, o := range orderedBootOptionsStr {
if oInt, convEerr := strconv.ParseInt(o, 16, 32); err != nil {
return false, fmt.Errorf("invalid value for boot option: %w", convEerr)
} else {
orderedBootOptionsInt = append(orderedBootOptionsInt, int(oInt))
}
}
return orderedBootOptionsInt[0] == httpBootApp.ID, nil
}
func Install(path, device string) (err error) {
partNum := device[len(device)-1:]
diskDev := device[:len(device)-1]
cmd := exec.Command(execPath, "--create", "--label", HTTPBootLabel, "--loader", path, "--part", partNum, "--disk", diskDev)
var errBuf bytes.Buffer
cmd.Stderr = &errBuf
err = cmd.Run()
var exitErr exec.ExitError
e := &exitErr
if err != nil && !errors.As(err, &e) {
return err
}
if cmd.ProcessState.ExitCode() != 0 {
stderr := errBuf.String()
return fmt.Errorf("error setting variable, returncode %d, stderr %q", cmd.ProcessState.ExitCode(), stderr)
}
return nil
}

View file

@ -24,6 +24,22 @@ func TestEfiFomBootMgr(t *testing.T) {
assert.Equal(t, expected, app) assert.Equal(t, expected, app)
}) })
t.Run("OK - SystemdBoot VM", func(t *testing.T) {
rawVal := `Boot0001* SYSTEMD_BOOT PciRoot(0x0)/Pci(0x1f,0x2)/Sata(0,65535,0)/HD(1,GPT,c4540877-4592-494f-bd8a-2a23abb22c3f,0x800,0x100000)/File(\EFI\systemd\systemd-bootx64.efi)`
expected := EfiApp{ID: 1, Name: "SYSTEMD_BOOT", Path: `\EFI\systemd\systemd-bootx64.efi`, Active: false, DiskID: "c4540877-4592-494f-bd8a-2a23abb22c3f"}
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
assert.True(t, ok)
assert.Equal(t, expected, app)
})
t.Run("OK - HTTP_BOOT hexa", func(t *testing.T) {
rawVal := `Boot000A* httpboot.efi HD(1,GPT,c4540877-4592-494f-bd8a-2a23abb22c3f,0x800,0x100000)/File(\EFI\httpboot\httpboot.efi)`
expected := EfiApp{ID: 10, Name: "httpboot.efi", Path: `\EFI\httpboot\httpboot.efi`, Active: false, DiskID: "c4540877-4592-494f-bd8a-2a23abb22c3f"}
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
assert.True(t, ok)
assert.Equal(t, expected, app)
})
t.Run("OK - EFI Base", func(t *testing.T) { t.Run("OK - EFI Base", func(t *testing.T) {
rawVal := `Boot0002* UEFI OS HD(1,GPT,16f06d01-50da-6544-86bd-f3457f980086,0x1000,0x96000)/File(\EFI\BOOT\BOOTX64.EFI)0000424f` rawVal := `Boot0002* UEFI OS HD(1,GPT,16f06d01-50da-6544-86bd-f3457f980086,0x1000,0x96000)/File(\EFI\BOOT\BOOTX64.EFI)0000424f`
expected := EfiApp{ID: 2, Name: "UEFI OS", Path: `\EFI\BOOT\BOOTX64.EFI`, Active: false, DiskID: "16f06d01-50da-6544-86bd-f3457f980086"} expected := EfiApp{ID: 2, Name: "UEFI OS", Path: `\EFI\BOOT\BOOTX64.EFI`, Active: false, DiskID: "16f06d01-50da-6544-86bd-f3457f980086"}

View file

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"git.faercol.me/faercol/http-boot-config/config/logger" "git.faercol.me/faercol/http-boot-config/config/logger"
@ -27,11 +28,19 @@ type enrollPayload struct {
SelectedOption string `json:"selected_option"` SelectedOption string `json:"selected_option"`
} }
type enrollRespPayload struct { type enrollRespPayloadJSON struct {
ID string `json:"ID"` ID string `json:"ID"`
MulticastGroup string `json:"multicast_group"`
MulticastPort int `json:"multicast_port"`
} }
func enrollToServer(ctx context.Context, host string, name string, apps []prober.EfiApp) (uuid.UUID, error) { type EnrollConfig struct {
ID uuid.UUID
MulticastGroup net.IP
MulticastPort int
}
func enrollToServer(ctx context.Context, host string, name string, apps []prober.EfiApp) (EnrollConfig, error) {
payload := enrollPayload{ payload := enrollPayload{
Name: name, Name: name,
Options: make(map[string]enrollEFIOption), Options: make(map[string]enrollEFIOption),
@ -46,49 +55,49 @@ func enrollToServer(ctx context.Context, host string, name string, apps []prober
dat, err := json.Marshal(payload) dat, err := json.Marshal(payload)
if err != nil { if err != nil {
return uuid.Nil, fmt.Errorf("failed to serialize payload: %w", err) return EnrollConfig{}, fmt.Errorf("failed to serialize payload: %w", err)
} }
subCtx, cancel := context.WithTimeout(ctx, reqTimeout) subCtx, cancel := context.WithTimeout(ctx, reqTimeout)
defer cancel() defer cancel()
req, err := http.NewRequestWithContext(subCtx, http.MethodPost, host+enrollURL, bytes.NewBuffer(dat)) req, err := http.NewRequestWithContext(subCtx, http.MethodPost, host+enrollURL, bytes.NewBuffer(dat))
if err != nil { if err != nil {
return uuid.Nil, fmt.Errorf("failed to initialize request: %w", err) return EnrollConfig{}, fmt.Errorf("failed to initialize request: %w", err)
} }
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return uuid.Nil, fmt.Errorf("failed to do query to remote boot server: %w", err) return EnrollConfig{}, fmt.Errorf("failed to do query to remote boot server: %w", err)
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return uuid.Nil, fmt.Errorf("unexpected returncode %d", resp.StatusCode) return EnrollConfig{}, fmt.Errorf("unexpected returncode %d", resp.StatusCode)
} }
var respPayload enrollRespPayload var respPayload enrollRespPayloadJSON
respDat, err := io.ReadAll(resp.Body) respDat, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return uuid.Nil, fmt.Errorf("failed to read response from server: %w", err) return EnrollConfig{}, fmt.Errorf("failed to read response from server: %w", err)
} }
if err := json.Unmarshal(respDat, &respPayload); err != nil { if err := json.Unmarshal(respDat, &respPayload); err != nil {
return uuid.Nil, fmt.Errorf("failed to deserialize data from server: %w", err) return EnrollConfig{}, fmt.Errorf("failed to deserialize data from server: %w", err)
} }
newID, err := uuid.Parse(respPayload.ID) newID, err := uuid.Parse(respPayload.ID)
if err != nil { if err != nil {
return uuid.Nil, fmt.Errorf("invalid UUID %q from server: %w", respPayload.ID, err) return EnrollConfig{}, fmt.Errorf("invalid UUID %q from server: %w", respPayload.ID, err)
} }
return newID, nil return EnrollConfig{ID: newID, MulticastGroup: net.ParseIP(respPayload.MulticastGroup), MulticastPort: respPayload.MulticastPort}, nil
} }
func Enroll(ctx context.Context, host, name string, l *logger.SimpleLogger) error { func Enroll(ctx context.Context, host, name string, l *logger.SimpleLogger) (EnrollConfig, error) {
apps, err := prober.GetEFIApps(l) apps, err := prober.GetEFIApps()
if err != nil { if err != nil {
return fmt.Errorf("failed to get list of available EFI applications: %w", err) return EnrollConfig{}, fmt.Errorf("failed to get list of available EFI applications: %w", err)
} }
newID, err := enrollToServer(ctx, host, name, apps) newConf, err := enrollToServer(ctx, host, name, apps)
if err != nil { if err != nil {
return fmt.Errorf("failed to enroll client to remote host: %w", err) return EnrollConfig{}, fmt.Errorf("failed to enroll client to remote host: %w", err)
} }
l.Infof("Successfully enrolled new client, ID is %q", newID.String()) l.Infof("Successfully enrolled new client, ID is %q", newConf.ID.String())
return nil return newConf, nil
} }

View file

@ -19,7 +19,7 @@ var ErrNotEnrolled = errors.New("client not enrolled")
const getBootRoute = "/config" const getBootRoute = "/config"
const reqTimeout = 5 * time.Second const reqTimeout = 5 * time.Second
type clientConfig struct { type ClientConfig struct {
EFIConfig struct { EFIConfig struct {
ID string `json:"ID"` ID string `json:"ID"`
Name string `json:"name"` Name string `json:"name"`
@ -36,7 +36,7 @@ type clientConfig struct {
} `json:"net_config"` } `json:"net_config"`
} }
func getRemoteConfig(ctx context.Context, host string, id uuid.UUID, l *logger.SimpleLogger) ([]byte, error) { func GetRemoteConfig(ctx context.Context, host string, id uuid.UUID, l *logger.SimpleLogger) ([]byte, error) {
subCtx, cancel := context.WithTimeout(ctx, reqTimeout) subCtx, cancel := context.WithTimeout(ctx, reqTimeout)
defer cancel() defer cancel()
req, err := http.NewRequestWithContext(subCtx, http.MethodGet, host+getBootRoute, nil) req, err := http.NewRequestWithContext(subCtx, http.MethodGet, host+getBootRoute, nil)
@ -69,7 +69,7 @@ func getRemoteConfig(ctx context.Context, host string, id uuid.UUID, l *logger.S
} }
func DisplayRemoteConfigJSON(ctx context.Context, host string, id uuid.UUID, l *logger.SimpleLogger) error { func DisplayRemoteConfigJSON(ctx context.Context, host string, id uuid.UUID, l *logger.SimpleLogger) error {
dat, err := getRemoteConfig(ctx, host, id, l) dat, err := GetRemoteConfig(ctx, host, id, l)
if err != nil { if err != nil {
return err return err
} }
@ -85,12 +85,12 @@ func DisplayRemoteConfigJSON(ctx context.Context, host string, id uuid.UUID, l *
} }
func DisplayRemoteConfigPretty(ctx context.Context, host string, id uuid.UUID, l *logger.SimpleLogger) error { func DisplayRemoteConfigPretty(ctx context.Context, host string, id uuid.UUID, l *logger.SimpleLogger) error {
dat, err := getRemoteConfig(ctx, host, id, l) dat, err := GetRemoteConfig(ctx, host, id, l)
if err != nil { if err != nil {
return err return err
} }
var parsedConf clientConfig var parsedConf ClientConfig
if err := json.Unmarshal(dat, &parsedConf); err != nil { if err := json.Unmarshal(dat, &parsedConf); err != nil {
return fmt.Errorf("invalid JSON format for result: %w", err) return fmt.Errorf("invalid JSON format for result: %w", err)
} }