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,9 +27,16 @@ 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
@ -31,6 +45,12 @@ type cliArgs struct {
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)
} }