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:
mkdir -p 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
import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"flag"
"fmt"
"io/fs"
"io/ioutil"
"net"
"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/prober"
"git.faercol.me/faercol/http-boot-config/config/remote"
@ -20,17 +27,30 @@ const (
actionList action = iota
actionGetRemote
actionEnroll
actionStatus
actionUnknown
)
const (
defaultEFIDest = "/EFI/httpboot/"
defaultEFIMount = "/boot"
defaultEFISrc = "httpboot.efi"
)
type cliArgs struct {
debug bool
colour bool
action action
id uuid.UUID
remoteAddr string
prettyPrint bool
name string
debug bool
colour bool
action action
id uuid.UUID
remoteAddr string
prettyPrint bool
name string
efiDest string
efiMountPoint string
efiDisk string
efiSrc string
ifaceName string
install bool
}
var defaultArgs cliArgs = cliArgs{
@ -45,6 +65,9 @@ func parseArgs() (cliArgs, error) {
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)
uuidFlag := getRemoteFlagSet.String("uuid", "", "Client UUID")
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)
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")
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")
noColourFlag := flag.Bool("no-colour", false, "Disable colour logs")
@ -65,6 +94,8 @@ func parseArgs() (cliArgs, error) {
args.action = actionGetRemote
case "enroll":
args.action = actionEnroll
case "status":
args.action = actionStatus
default:
continue
}
@ -74,6 +105,9 @@ func parseArgs() (cliArgs, error) {
switch args.action {
case actionList:
listFlagSet.Parse(os.Args[firstArg:])
case actionStatus:
statusFlagSet.Parse(os.Args[firstArg:])
args.remoteAddr = *hostStatusFlag
case actionGetRemote:
getRemoteFlagSet.Parse(os.Args[firstArg:])
parsedID, err := uuid.Parse(*uuidFlag)
@ -87,6 +121,12 @@ func parseArgs() (cliArgs, error) {
enrollFlagSet.Parse(os.Args[firstArg:])
args.remoteAddr = *hostEnrollFlag
args.name = *nameFlag
args.efiSrc = *efiSrcFlag
args.efiDest = *efiDestFlag
args.install = *installFlag
args.efiDisk = *efiDiskFlag
args.efiMountPoint = *efiMountPointFlag
args.ifaceName = *ifaceNameFlag
default:
flag.Parse()
return cliArgs{}, errors.New("missing an action")
@ -100,7 +140,7 @@ func parseArgs() (cliArgs, error) {
func displayAppList(l *logger.SimpleLogger) {
l.Info("Checking EFI directory for available boot images...")
apps, err := prober.GetEFIApps(l)
apps, err := prober.GetEFIApps()
if err != nil {
if errors.Is(err, fs.ErrPermission) {
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")
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())
}
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() {
@ -150,10 +375,12 @@ func main() {
switch args.action {
case actionList:
displayAppList(l)
case actionStatus:
getStatus(l, args.remoteAddr)
case actionGetRemote:
getRemoteConfig(l, args.remoteAddr, args.id, args.prettyPrint)
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:
l.Fatal("Unknown action")
}

View file

@ -8,12 +8,15 @@ import (
"regexp"
"strconv"
"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 bootOrderRegexp = regexp.MustCompile(`BootOrder: ((?:[0-9A-F]{4},?)+)`)
const HTTPBootName = "httpboot.efi"
const HTTPBootLabel = "HTTP_BOOT"
const execPath = "/usr/bin/efibootmgr"
type EfiApp struct {
ID int
@ -38,7 +41,7 @@ func efiAppFromBootMgrOutput(rawVal string, activeId int) (app EfiApp, ok bool)
if !ok {
return
}
idInt, err := strconv.Atoi(id)
idInt, err := strconv.ParseInt(id, 16, 32)
if err != nil {
return app, false
}
@ -55,10 +58,10 @@ func efiAppFromBootMgrOutput(rawVal string, activeId int) (app EfiApp, ok bool)
return
}
return EfiApp{
ID: idInt,
ID: int(idInt),
Name: strings.TrimSpace(name),
Path: strings.TrimSpace(filepath),
Active: idInt == activeId,
Active: int(idInt) == activeId,
DiskID: disk_id,
}, true
}
@ -76,7 +79,7 @@ func getActiveEFIApp(output string) (int, error) {
return id, nil
}
func GetEFIApps(log *logger.SimpleLogger) (apps []EfiApp, err error) {
func GetEFIApps() (apps []EfiApp, err error) {
cmd := exec.Command("/usr/bin/efibootmgr")
var out bytes.Buffer
cmd.Stdout = &out
@ -96,13 +99,94 @@ func GetEFIApps(log *logger.SimpleLogger) (apps []EfiApp, err error) {
}
for _, l := range strings.Split(outStr, "\n") {
log.Debugf("Parsing line %q", l)
app, ok := efiAppFromBootMgrOutput(l, activeBootID)
if !ok {
log.Debug("Line is not a valid EFI application")
continue
}
apps = append(apps, app)
}
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)
})
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) {
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"}

View file

@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"git.faercol.me/faercol/http-boot-config/config/logger"
@ -27,11 +28,19 @@ type enrollPayload struct {
SelectedOption string `json:"selected_option"`
}
type enrollRespPayload struct {
ID string `json:"ID"`
type enrollRespPayloadJSON struct {
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{
Name: name,
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)
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)
defer cancel()
req, err := http.NewRequestWithContext(subCtx, http.MethodPost, host+enrollURL, bytes.NewBuffer(dat))
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)
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 {
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)
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 {
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)
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 {
apps, err := prober.GetEFIApps(l)
func Enroll(ctx context.Context, host, name string, l *logger.SimpleLogger) (EnrollConfig, error) {
apps, err := prober.GetEFIApps()
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 {
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())
return nil
l.Infof("Successfully enrolled new client, ID is %q", newConf.ID.String())
return newConf, nil
}

View file

@ -19,7 +19,7 @@ var ErrNotEnrolled = errors.New("client not enrolled")
const getBootRoute = "/config"
const reqTimeout = 5 * time.Second
type clientConfig struct {
type ClientConfig struct {
EFIConfig struct {
ID string `json:"ID"`
Name string `json:"name"`
@ -36,7 +36,7 @@ type clientConfig struct {
} `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)
defer cancel()
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 {
dat, err := getRemoteConfig(ctx, host, id, l)
dat, err := GetRemoteConfig(ctx, host, id, l)
if err != nil {
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 {
dat, err := getRemoteConfig(ctx, host, id, l)
dat, err := GetRemoteConfig(ctx, host, id, l)
if err != nil {
return err
}
var parsedConf clientConfig
var parsedConf ClientConfig
if err := json.Unmarshal(dat, &parsedConf); err != nil {
return fmt.Errorf("invalid JSON format for result: %w", err)
}