http-boot-config/config/main.go

388 lines
11 KiB
Go
Raw Permalink Normal View History

2023-08-13 14:20:55 +00:00
package main
2023-08-13 15:59:05 +00:00
import (
2023-08-22 20:39:23 +00:00
"bytes"
"context"
2023-08-22 20:39:23 +00:00
"encoding/binary"
"encoding/json"
2023-08-13 15:59:05 +00:00
"errors"
"flag"
"fmt"
2023-08-13 15:59:05 +00:00
"io/fs"
2023-08-22 20:39:23 +00:00
"io/ioutil"
"net"
2023-08-13 16:46:06 +00:00
"os"
2023-08-22 20:39:23 +00:00
"path"
2023-08-13 15:59:05 +00:00
2023-08-22 20:39:23 +00:00
"git.faercol.me/faercol/http-boot-config/config/efivar"
2023-08-13 15:59:05 +00:00
"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"
"github.com/google/uuid"
2023-08-13 15:59:05 +00:00
)
2023-08-13 16:46:06 +00:00
type action int
const (
actionList action = iota
actionGetRemote
actionEnroll
2023-08-22 20:39:23 +00:00
actionStatus
2023-08-13 16:46:06 +00:00
actionUnknown
)
2023-08-22 20:39:23 +00:00
const (
defaultEFIDest = "/EFI/httpboot/"
defaultEFIMount = "/boot"
defaultEFISrc = "httpboot.efi"
)
2023-08-13 15:59:05 +00:00
type cliArgs struct {
2023-08-22 20:39:23 +00:00
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
2023-08-13 16:46:06 +00:00
}
var defaultArgs cliArgs = cliArgs{
debug: false,
colour: true,
action: actionUnknown,
2023-08-13 15:59:05 +00:00
}
2023-08-13 16:46:06 +00:00
func parseArgs() (cliArgs, error) {
args := defaultArgs
var firstArg int
listFlagSet := flag.NewFlagSet("list", flag.ExitOnError)
2023-08-22 20:39:23 +00:00
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")
jsonFlag := getRemoteFlagSet.Bool("json", false, "Display the result in JSON format")
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")
2023-08-22 20:39:23 +00:00
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")
2023-08-13 15:59:05 +00:00
debugFlag := flag.Bool("debug", false, "Display debug logs")
noColourFlag := flag.Bool("no-colour", false, "Disable colour logs")
2023-08-13 16:46:06 +00:00
for i, v := range os.Args {
switch v {
case "list":
args.action = actionList
case "get-remote":
args.action = actionGetRemote
case "enroll":
args.action = actionEnroll
2023-08-22 20:39:23 +00:00
case "status":
args.action = actionStatus
2023-08-13 16:46:06 +00:00
default:
continue
}
firstArg = i + 1
}
2023-08-13 15:59:05 +00:00
2023-08-13 16:46:06 +00:00
switch args.action {
case actionList:
listFlagSet.Parse(os.Args[firstArg:])
2023-08-22 20:39:23 +00:00
case actionStatus:
statusFlagSet.Parse(os.Args[firstArg:])
args.remoteAddr = *hostStatusFlag
2023-08-13 16:46:06 +00:00
case actionGetRemote:
getRemoteFlagSet.Parse(os.Args[firstArg:])
parsedID, err := uuid.Parse(*uuidFlag)
if err != nil {
return args, fmt.Errorf("invalid format for uuid %q", *uuidFlag)
}
args.id = parsedID
args.remoteAddr = *hostGetRemoteFlag
args.prettyPrint = !*jsonFlag
case actionEnroll:
enrollFlagSet.Parse(os.Args[firstArg:])
args.remoteAddr = *hostEnrollFlag
args.name = *nameFlag
2023-08-22 20:39:23 +00:00
args.efiSrc = *efiSrcFlag
args.efiDest = *efiDestFlag
args.install = *installFlag
args.efiDisk = *efiDiskFlag
args.efiMountPoint = *efiMountPointFlag
args.ifaceName = *ifaceNameFlag
2023-08-13 16:46:06 +00:00
default:
flag.Parse()
return cliArgs{}, errors.New("missing an action")
2023-08-13 15:59:05 +00:00
}
2023-08-13 16:46:06 +00:00
flag.Parse()
args.debug = *debugFlag
args.colour = !*noColourFlag
return args, nil
2023-08-13 15:59:05 +00:00
}
2023-08-13 16:46:06 +00:00
func displayAppList(l *logger.SimpleLogger) {
l.Info("Checking EFI directory for available boot images...")
2023-08-22 20:39:23 +00:00
apps, err := prober.GetEFIApps()
2023-08-13 16:46:06 +00:00
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())
}
2023-08-13 15:59:05 +00:00
l.Info("Found the following EFI applications:")
for _, a := range apps {
prefix := " "
if a.Active {
prefix = "*"
}
2023-09-01 16:45:22 +00:00
l.Infof("\t- %s[%d] %s: %s", prefix, a.ID, a.Name, a.DevicePath)
2023-08-13 15:59:05 +00:00
}
}
2023-08-13 14:20:55 +00:00
func getRemoteConfig(l *logger.SimpleLogger, host string, id uuid.UUID, pretty bool) {
if pretty {
2023-08-19 08:54:18 +00:00
l.Info("Getting config from remote server...")
if err := remote.DisplayRemoteConfigPretty(context.Background(), host, id, l); err != nil {
l.Fatal(err.Error())
}
} else {
if err := remote.DisplayRemoteConfigJSON(context.Background(), host, id, l); err != nil {
l.Fatal(err.Error())
}
}
2023-08-13 16:46:06 +00:00
}
2023-08-13 14:20:55 +00:00
2023-08-22 20:39:23 +00:00
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")
2023-08-22 20:39:23 +00:00
clientConf, err := remote.Enroll(context.Background(), host, name, l)
if err != nil {
l.Fatal(err.Error())
}
2023-08-22 20:39:23 +00:00
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
}
2023-08-13 16:46:06 +00:00
func main() {
args, err := parseArgs()
2023-08-13 15:59:05 +00:00
if err != nil {
2023-08-13 16:46:06 +00:00
l := logger.New(true, false)
l.Fatalf("Invalid command: %s", err.Error())
2023-08-13 15:59:05 +00:00
}
2023-08-13 16:46:06 +00:00
l := logger.New(args.colour, args.debug)
fmt.Print("")
2023-08-13 14:20:55 +00:00
2023-08-13 16:46:06 +00:00
switch args.action {
case actionList:
displayAppList(l)
2023-08-22 20:39:23 +00:00
case actionStatus:
getStatus(l, args.remoteAddr)
2023-08-13 16:46:06 +00:00
case actionGetRemote:
getRemoteConfig(l, args.remoteAddr, args.id, args.prettyPrint)
case actionEnroll:
2023-08-22 20:39:23 +00:00
enroll(l, args.remoteAddr, args.name, args.efiSrc, args.efiDest, args.efiDisk, args.efiMountPoint, args.ifaceName, args.install)
2023-08-13 16:46:06 +00:00
default:
l.Fatal("Unknown action")
}
2023-08-13 14:20:55 +00:00
}