add tons of shit to enroll
This commit is contained in:
parent
475a444129
commit
712df4c190
8 changed files with 519 additions and 44 deletions
|
@ -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
104
config/efivar/efivar.go
Normal 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
|
||||
}
|
32
config/efivar/efivar_test.go
Normal file
32
config/efivar/efivar_test.go
Normal 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)
|
||||
})
|
||||
}
|
249
config/main.go
249
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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue