387 lines
11 KiB
Go
387 lines
11 KiB
Go
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"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type action int
|
|
|
|
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
|
|
efiDest string
|
|
efiMountPoint string
|
|
efiDisk string
|
|
efiSrc string
|
|
ifaceName string
|
|
install bool
|
|
}
|
|
|
|
var defaultArgs cliArgs = cliArgs{
|
|
debug: false,
|
|
colour: true,
|
|
action: actionUnknown,
|
|
}
|
|
|
|
func parseArgs() (cliArgs, error) {
|
|
args := defaultArgs
|
|
var firstArg int
|
|
|
|
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")
|
|
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")
|
|
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")
|
|
|
|
for i, v := range os.Args {
|
|
switch v {
|
|
case "list":
|
|
args.action = actionList
|
|
case "get-remote":
|
|
args.action = actionGetRemote
|
|
case "enroll":
|
|
args.action = actionEnroll
|
|
case "status":
|
|
args.action = actionStatus
|
|
default:
|
|
continue
|
|
}
|
|
firstArg = i + 1
|
|
}
|
|
|
|
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)
|
|
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
|
|
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")
|
|
}
|
|
|
|
flag.Parse()
|
|
args.debug = *debugFlag
|
|
args.colour = !*noColourFlag
|
|
return args, nil
|
|
}
|
|
|
|
func displayAppList(l *logger.SimpleLogger) {
|
|
l.Info("Checking EFI directory for available boot images...")
|
|
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())
|
|
}
|
|
|
|
l.Info("Found the following EFI applications:")
|
|
for _, a := range apps {
|
|
prefix := " "
|
|
if a.Active {
|
|
prefix = "*"
|
|
}
|
|
l.Infof("\t- %s[%d] %s: %s (disk id %s)", prefix, a.ID, a.Name, a.Path, a.DiskID)
|
|
}
|
|
}
|
|
|
|
func getRemoteConfig(l *logger.SimpleLogger, host string, id uuid.UUID, pretty bool) {
|
|
if pretty {
|
|
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())
|
|
}
|
|
}
|
|
}
|
|
|
|
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")
|
|
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() {
|
|
args, err := parseArgs()
|
|
if err != nil {
|
|
l := logger.New(true, false)
|
|
l.Fatalf("Invalid command: %s", err.Error())
|
|
}
|
|
l := logger.New(args.colour, args.debug)
|
|
fmt.Print("")
|
|
|
|
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, args.efiSrc, args.efiDest, args.efiDisk, args.efiMountPoint, args.ifaceName, args.install)
|
|
default:
|
|
l.Fatal("Unknown action")
|
|
}
|
|
}
|