Compare commits
No commits in common. "712df4c19086712045e6fff2fd000a64baa3ccec" and "639f4e60ea954e2fb487fee4817379fec1ce2759" have entirely different histories.
712df4c190
...
639f4e60ea
8 changed files with 55 additions and 538 deletions
|
@ -3,6 +3,3 @@
|
||||||
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
|
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
239
config/main.go
239
config/main.go
|
@ -1,20 +1,13 @@
|
||||||
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"
|
||||||
|
@ -27,16 +20,9 @@ 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
|
||||||
|
@ -45,12 +31,6 @@ 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{
|
||||||
|
@ -65,9 +45,6 @@ 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")
|
||||||
|
@ -76,12 +53,6 @@ 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")
|
||||||
|
@ -94,8 +65,6 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -105,9 +74,6 @@ 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)
|
||||||
|
@ -121,12 +87,6 @@ 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")
|
||||||
|
@ -140,7 +100,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()
|
apps, err := prober.GetEFIApps(l)
|
||||||
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")
|
||||||
|
@ -154,13 +114,13 @@ func displayAppList(l *logger.SimpleLogger) {
|
||||||
if a.Active {
|
if a.Active {
|
||||||
prefix = "*"
|
prefix = "*"
|
||||||
}
|
}
|
||||||
l.Infof("\t- %s[%d] %s: %s (disk id %s)", prefix, a.ID, a.Name, a.Path, a.DiskID)
|
l.Infof("\t- %s[%d] %s: %s", prefix, a.ID, a.Name, a.Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRemoteConfig(l *logger.SimpleLogger, host string, id uuid.UUID, pretty bool) {
|
func getRemoteConfig(l *logger.SimpleLogger, host string, id uuid.UUID, pretty bool) {
|
||||||
if pretty {
|
|
||||||
l.Info("Getting config from remote server...")
|
l.Info("Getting config from remote server...")
|
||||||
|
if pretty {
|
||||||
if err := remote.DisplayRemoteConfigPretty(context.Background(), host, id, l); err != nil {
|
if err := remote.DisplayRemoteConfigPretty(context.Background(), host, id, l); err != nil {
|
||||||
l.Fatal(err.Error())
|
l.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -171,196 +131,11 @@ func getRemoteConfig(l *logger.SimpleLogger, host string, id uuid.UUID, pretty b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func enroll(l *logger.SimpleLogger, host, name, src, dest, destPart, mount, iface string, install bool) {
|
func enroll(l *logger.SimpleLogger, host, name string) {
|
||||||
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")
|
||||||
clientConf, err := remote.Enroll(context.Background(), host, name, l)
|
if err := remote.Enroll(context.Background(), host, name, l); err != nil {
|
||||||
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() {
|
||||||
|
@ -375,12 +150,10 @@ 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, args.efiSrc, args.efiDest, args.efiDisk, args.efiMountPoint, args.ifaceName, args.install)
|
enroll(l, args.remoteAddr, args.name)
|
||||||
default:
|
default:
|
||||||
l.Fatal("Unknown action")
|
l.Fatal("Unknown action")
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,21 +8,17 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/http-boot-config/config/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var efiBootmgrRegexp = regexp.MustCompile(`Boot(?P<id>[0-9A-F]+)\* (?P<name>.+)\t(.+\(.+,.+,(?P<disk_id>[0-9a-f-]+),.+,.+\))/File\((?P<filepath>.+)\)`)
|
var efiBootmgrRegexp = regexp.MustCompile(`Boot(?P<id>\d+)\* (?P<name>[\w ]+)\t(\w+\(.*\))/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
|
||||||
Name string
|
Name string
|
||||||
Path string
|
Path string
|
||||||
DiskID string
|
|
||||||
Active bool
|
Active bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +37,7 @@ func efiAppFromBootMgrOutput(rawVal string, activeId int) (app EfiApp, ok bool)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
idInt, err := strconv.ParseInt(id, 16, 32)
|
idInt, err := strconv.Atoi(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return app, false
|
return app, false
|
||||||
}
|
}
|
||||||
|
@ -53,16 +49,11 @@ func efiAppFromBootMgrOutput(rawVal string, activeId int) (app EfiApp, ok bool)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
disk_id, ok := result["disk_id"]
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return EfiApp{
|
return EfiApp{
|
||||||
ID: int(idInt),
|
ID: idInt,
|
||||||
Name: strings.TrimSpace(name),
|
Name: strings.TrimSpace(name),
|
||||||
Path: strings.TrimSpace(filepath),
|
Path: strings.TrimSpace(filepath),
|
||||||
Active: int(idInt) == activeId,
|
Active: idInt == activeId,
|
||||||
DiskID: disk_id,
|
|
||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +70,7 @@ func getActiveEFIApp(output string) (int, error) {
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetEFIApps() (apps []EfiApp, err error) {
|
func GetEFIApps(log *logger.SimpleLogger) (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
|
||||||
|
@ -99,94 +90,13 @@ func GetEFIApps() (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
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ func TestEfiFomBootMgr(t *testing.T) {
|
||||||
t.Run("OK - Windows", func(t *testing.T) {
|
t.Run("OK - Windows", func(t *testing.T) {
|
||||||
rawVal := (`Boot0000* Windows Boot Manager HD(1,GPT,4ad714ba-6a01-4f76-90e3-1bb93a59b67e,0x800,0x32000)/File(\EFI\MICROSOFT\BOOT\BOOTMGFW.EFI)` +
|
rawVal := (`Boot0000* Windows Boot Manager HD(1,GPT,4ad714ba-6a01-4f76-90e3-1bb93a59b67e,0x800,0x32000)/File(\EFI\MICROSOFT\BOOT\BOOTMGFW.EFI)` +
|
||||||
`57494e444f5753000100000088000000780000004200430044004f0042004a004500430054003d007b00390064006500610038003600320063002d0035006300640064002d00`)
|
`57494e444f5753000100000088000000780000004200430044004f0042004a004500430054003d007b00390064006500610038003600320063002d0035006300640064002d00`)
|
||||||
expected := EfiApp{ID: 0, Name: "Windows Boot Manager", Path: `\EFI\MICROSOFT\BOOT\BOOTMGFW.EFI`, Active: true, DiskID: "4ad714ba-6a01-4f76-90e3-1bb93a59b67e"}
|
expected := EfiApp{ID: 0, Name: "Windows Boot Manager", Path: `\EFI\MICROSOFT\BOOT\BOOTMGFW.EFI`, Active: true}
|
||||||
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
|
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Equal(t, expected, app)
|
assert.Equal(t, expected, app)
|
||||||
|
@ -18,23 +18,7 @@ func TestEfiFomBootMgr(t *testing.T) {
|
||||||
|
|
||||||
t.Run("OK - Manjaro", func(t *testing.T) {
|
t.Run("OK - Manjaro", func(t *testing.T) {
|
||||||
rawVal := `Boot0001* Manjaro HD(1,GPT,16f06d01-50da-6544-86bd-f3457f980086,0x1000,0x96000)/File(\EFI\MANJARO\GRUBX64.EFI)`
|
rawVal := `Boot0001* Manjaro HD(1,GPT,16f06d01-50da-6544-86bd-f3457f980086,0x1000,0x96000)/File(\EFI\MANJARO\GRUBX64.EFI)`
|
||||||
expected := EfiApp{ID: 1, Name: "Manjaro", Path: `\EFI\MANJARO\GRUBX64.EFI`, Active: false, DiskID: "16f06d01-50da-6544-86bd-f3457f980086"}
|
expected := EfiApp{ID: 1, Name: "Manjaro", Path: `\EFI\MANJARO\GRUBX64.EFI`, Active: false}
|
||||||
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
|
|
||||||
assert.True(t, ok)
|
|
||||||
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)
|
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Equal(t, expected, app)
|
assert.Equal(t, expected, app)
|
||||||
|
@ -42,7 +26,7 @@ func TestEfiFomBootMgr(t *testing.T) {
|
||||||
|
|
||||||
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}
|
||||||
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
|
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Equal(t, expected, app)
|
assert.Equal(t, expected, app)
|
||||||
|
|
|
@ -6,7 +6,6 @@ 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"
|
||||||
|
@ -19,7 +18,6 @@ const enrollURL = "/enroll"
|
||||||
type enrollEFIOption struct {
|
type enrollEFIOption struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
DiskID string `json:"disk_id"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type enrollPayload struct {
|
type enrollPayload struct {
|
||||||
|
@ -28,26 +26,18 @@ type enrollPayload struct {
|
||||||
SelectedOption string `json:"selected_option"`
|
SelectedOption string `json:"selected_option"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type enrollRespPayloadJSON struct {
|
type enrollRespPayload struct {
|
||||||
ID string `json:"ID"`
|
ID string `json:"ID"`
|
||||||
MulticastGroup string `json:"multicast_group"`
|
|
||||||
MulticastPort int `json:"multicast_port"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnrollConfig struct {
|
func enrollToServer(ctx context.Context, host string, name string, apps []prober.EfiApp) (uuid.UUID, error) {
|
||||||
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),
|
||||||
}
|
}
|
||||||
for _, a := range apps {
|
for _, a := range apps {
|
||||||
appID := uuid.New()
|
appID := uuid.New()
|
||||||
payload.Options[appID.String()] = enrollEFIOption{Name: a.Name, Path: a.Path, DiskID: a.DiskID}
|
payload.Options[appID.String()] = enrollEFIOption{Name: a.Name, Path: a.Path}
|
||||||
if a.Active {
|
if a.Active {
|
||||||
payload.SelectedOption = appID.String()
|
payload.SelectedOption = appID.String()
|
||||||
}
|
}
|
||||||
|
@ -55,49 +45,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 EnrollConfig{}, fmt.Errorf("failed to serialize payload: %w", err)
|
return uuid.Nil, 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 EnrollConfig{}, fmt.Errorf("failed to initialize request: %w", err)
|
return uuid.Nil, 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 EnrollConfig{}, fmt.Errorf("failed to do query to remote boot server: %w", err)
|
return uuid.Nil, fmt.Errorf("failed to do query to remote boot server: %w", err)
|
||||||
}
|
}
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return EnrollConfig{}, fmt.Errorf("unexpected returncode %d", resp.StatusCode)
|
return uuid.Nil, fmt.Errorf("unexpected returncode %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var respPayload enrollRespPayloadJSON
|
var respPayload enrollRespPayload
|
||||||
respDat, err := io.ReadAll(resp.Body)
|
respDat, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return EnrollConfig{}, fmt.Errorf("failed to read response from server: %w", err)
|
return uuid.Nil, 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 EnrollConfig{}, fmt.Errorf("failed to deserialize data from server: %w", err)
|
return uuid.Nil, 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 EnrollConfig{}, fmt.Errorf("invalid UUID %q from server: %w", respPayload.ID, err)
|
return uuid.Nil, fmt.Errorf("invalid UUID %q from server: %w", respPayload.ID, err)
|
||||||
}
|
}
|
||||||
return EnrollConfig{ID: newID, MulticastGroup: net.ParseIP(respPayload.MulticastGroup), MulticastPort: respPayload.MulticastPort}, nil
|
return newID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Enroll(ctx context.Context, host, name string, l *logger.SimpleLogger) (EnrollConfig, error) {
|
func Enroll(ctx context.Context, host, name string, l *logger.SimpleLogger) error {
|
||||||
apps, err := prober.GetEFIApps()
|
apps, err := prober.GetEFIApps(l)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return EnrollConfig{}, fmt.Errorf("failed to get list of available EFI applications: %w", err)
|
return fmt.Errorf("failed to get list of available EFI applications: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newConf, err := enrollToServer(ctx, host, name, apps)
|
newID, err := enrollToServer(ctx, host, name, apps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return EnrollConfig{}, fmt.Errorf("failed to enroll client to remote host: %w", err)
|
return fmt.Errorf("failed to enroll client to remote host: %w", err)
|
||||||
}
|
}
|
||||||
l.Infof("Successfully enrolled new client, ID is %q", newConf.ID.String())
|
l.Infof("Successfully enrolled new client, ID is %q", newID.String())
|
||||||
return newConf, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,14 +19,13 @@ 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"`
|
||||||
Options map[string]struct {
|
Options map[string]struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
DiskID string `json:"disk_id"`
|
|
||||||
} `json:"options"`
|
} `json:"options"`
|
||||||
SelectedOption string `json:"selected_option"`
|
SelectedOption string `json:"selected_option"`
|
||||||
} `json:"efi_config"`
|
} `json:"efi_config"`
|
||||||
|
@ -36,7 +35,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 +68,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 +84,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)
|
||||||
}
|
}
|
||||||
|
@ -104,7 +103,7 @@ func DisplayRemoteConfigPretty(ctx context.Context, host string, id uuid.UUID, l
|
||||||
}
|
}
|
||||||
l.Info(" * Available options are:")
|
l.Info(" * Available options are:")
|
||||||
for _, option := range parsedConf.EFIConfig.Options {
|
for _, option := range parsedConf.EFIConfig.Options {
|
||||||
l.Infof("\t - %s: %s (disk id %s)", option.Name, option.Path, option.DiskID)
|
l.Infof("\t - %s: %s", option.Name, option.Path)
|
||||||
}
|
}
|
||||||
l.Infof(" * Remote boot server is listening on %s:%d", parsedConf.NetConfig.MulticastGroup, parsedConf.NetConfig.Port)
|
l.Infof(" * Remote boot server is listening on %s:%d", parsedConf.NetConfig.MulticastGroup, parsedConf.NetConfig.Port)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue