diff --git a/config/Makefile b/config/Makefile index 58373ce..4dfdb9f 100644 --- a/config/Makefile +++ b/config/Makefile @@ -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 diff --git a/config/efivar/efivar.go b/config/efivar/efivar.go new file mode 100644 index 0000000..8a1a6ec --- /dev/null +++ b/config/efivar/efivar.go @@ -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 +} diff --git a/config/efivar/efivar_test.go b/config/efivar/efivar_test.go new file mode 100644 index 0000000..23f2e45 --- /dev/null +++ b/config/efivar/efivar_test.go @@ -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) + }) +} diff --git a/config/main.go b/config/main.go index 92a3a4c..be50680 100644 --- a/config/main.go +++ b/config/main.go @@ -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") } diff --git a/config/prober/prober.go b/config/prober/prober.go index 91c0baf..e761c6d 100644 --- a/config/prober/prober.go +++ b/config/prober/prober.go @@ -8,12 +8,15 @@ import ( "regexp" "strconv" "strings" - - "git.faercol.me/faercol/http-boot-config/config/logger" ) -var efiBootmgrRegexp = regexp.MustCompile(`Boot(?P\d+)\* (?P[\w ]+)\t(HD\(.+,.+,(?P[0-9a-f-]+),.+,.+\))/File\((?P.+)\)`) +var efiBootmgrRegexp = regexp.MustCompile(`Boot(?P[0-9A-F]+)\* (?P.+)\t(.+\(.+,.+,(?P[0-9a-f-]+),.+,.+\))/File\((?P.+)\)`) 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 +} diff --git a/config/prober/prober_test.go b/config/prober/prober_test.go index bba5daf..e848fe0 100644 --- a/config/prober/prober_test.go +++ b/config/prober/prober_test.go @@ -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"} diff --git a/config/remote/enroll.go b/config/remote/enroll.go index cb0b378..22fd33b 100644 --- a/config/remote/enroll.go +++ b/config/remote/enroll.go @@ -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 } diff --git a/config/remote/remote.go b/config/remote/remote.go index 46fc981..5b69a6c 100644 --- a/config/remote/remote.go +++ b/config/remote/remote.go @@ -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) }