Compare commits

..

No commits in common. "backup" and "main" have entirely different histories.
backup ... main

16 changed files with 411 additions and 829 deletions

View file

View file

@ -5,4 +5,4 @@ build:
go build -o build/
push-client:
scp build/config root@192.168.122.76:/usr/bin/efi-http-config
scp build/config root@192.168.122.2:/usr/bin/efi-http-config

View file

@ -1,60 +0,0 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"os"
"git.faercol.me/faercol/http-boot-config/config/config"
"github.com/spf13/cobra"
)
// configCmd represents the config command
var configCmd = &cobra.Command{
Use: "config",
Short: "Display the current configuration",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
displayConfig()
},
}
func displayConfig() {
conf, err := config.Get()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get boot client configuration: %s\n", err.Error())
os.Exit(1)
}
if !conf.Enrolled {
fmt.Println("Boot service has not been configured yet.")
os.Exit(0)
}
fmt.Printf("- Client ID: %s\n", conf.ClientID.String())
fmt.Println("- Network config:")
fmt.Printf(" - Multicast group: %s\n", conf.Multicast.Group.String())
fmt.Printf(" - Multicast port: %d\n", conf.Multicast.Port)
fmt.Printf(" - Multicast source addr: %s\n", conf.Multicast.SrcAddr.String())
}
func init() {
rootCmd.AddCommand(configCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// configCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// configCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View file

@ -1,60 +0,0 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"errors"
"fmt"
"os"
"git.faercol.me/faercol/http-boot-config/config/config"
"git.faercol.me/faercol/http-boot-config/config/discover"
"github.com/spf13/cobra"
)
// discoverCmd represents the discover command
var discoverCmd = &cobra.Command{
Use: "discover",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
discoverRemoteServer()
},
}
func discoverRemoteServer() {
fmt.Println("Trying to autodiscover remote boot server")
conf, _ := config.Get()
remote, err := discover.DiscoverServer(conf.Discovery)
if err != nil {
if errors.Is(err, discover.ErrNoServer) {
fmt.Println("No remote boot server found on the network.")
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Failed to discover server: %s\n", err.Error())
os.Exit(1)
}
fmt.Printf("Found remote boot server, server address is %s\n", remote)
}
func init() {
rootCmd.AddCommand(discoverCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// discoverCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// discoverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View file

@ -1,54 +0,0 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"os"
"git.faercol.me/faercol/http-boot-config/config/config"
"github.com/spf13/cobra"
)
// remoteCmd represents the remote command
var remoteCmd = &cobra.Command{
Use: "remote",
Short: "Get status of remote server",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
getRemoteStatus()
},
}
func getRemoteStatus() {
conf, err := config.Get()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get boot client configuration: %s\n", err.Error())
os.Exit(1)
}
if !conf.Enrolled {
fmt.Println("Boot service has not been configured yet.")
os.Exit(0)
}
}
func init() {
rootCmd.AddCommand(remoteCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// remoteCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// remoteCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View file

@ -1,51 +0,0 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "config",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.config.yaml)")
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View file

@ -1,40 +0,0 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// statusCmd represents the status command
var statusCmd = &cobra.Command{
Use: "status",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("status called")
},
}
func init() {
rootCmd.AddCommand(statusCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// statusCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// statusCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View file

@ -1,76 +0,0 @@
package config
import (
"encoding/binary"
"errors"
"fmt"
"net"
"time"
"git.faercol.me/faercol/http-boot-config/config/efivar"
"github.com/google/uuid"
)
const DefaultDiscoverAddr = "[ff02::1%enp6s0]:42"
const DefaultDiscoveryTimeout = 30 * time.Second
type MulticastConfig struct {
Group net.IP
SrcAddr net.IP
Port int
}
type Autodiscovery struct {
DiscoveryAddr string
Timeout time.Duration
}
type AppConfig struct {
Enrolled bool
Multicast MulticastConfig
ClientID uuid.UUID
Discovery Autodiscovery
}
func (a *AppConfig) getDataFromEFIVars() error {
srcAddr, err := efivar.GetVar(efivar.VendorID, efivar.VarBootIP)
if err != nil {
return err
}
a.Multicast.SrcAddr = net.IP(srcAddr)
mcastGroup, err := efivar.GetVar(efivar.VendorID, efivar.VarBootGroup)
if err != nil {
return err
}
a.Multicast.Group = net.IP(mcastGroup)
mcastPortRaw, err := efivar.GetVar(efivar.VendorID, efivar.VarBootPort)
if err != nil {
return err
}
mcastPort := binary.LittleEndian.Uint16(mcastPortRaw)
a.Multicast.Port = int(mcastPort)
clientID, err := efivar.GetVar(efivar.VendorID, efivar.VarBootUUID)
if err != nil {
return err
}
a.ClientID = uuid.UUID(clientID)
return nil
}
func Get() (AppConfig, error) {
conf := AppConfig{Enrolled: true, Discovery: Autodiscovery{Timeout: DefaultDiscoveryTimeout, DiscoveryAddr: DefaultDiscoverAddr}}
if err := conf.getDataFromEFIVars(); err != nil {
if errors.Is(err, efivar.ErrNotFound) {
conf.Enrolled = false
} else {
return conf, fmt.Errorf("failed to read EFI config: %w", err)
}
}
return conf, nil
}

View file

@ -1,59 +0,0 @@
package discover
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"time"
"git.faercol.me/faercol/http-boot-config/config/config"
)
var ErrNoServer = errors.New("no server found")
var discoveryMsg = []byte("BOOT_DISCOVER")
type discoveryPayload struct {
ManagementAddress string `json:"managementAddress"`
Version string `json:"version"`
}
func DiscoverServer(conf config.Autodiscovery) (string, error) {
raddr, err := net.ResolveUDPAddr("udp", conf.DiscoveryAddr)
if err != nil {
return "", fmt.Errorf("invalid discovery address: %w", err)
}
conn, err := net.ListenUDP("udp", nil)
if err != nil {
return "", fmt.Errorf("failed to dial UDP multicast group: %w", err)
}
defer conn.Close()
n, err := conn.WriteToUDP(discoveryMsg, raddr)
if err != nil {
return "", fmt.Errorf("failed to write to UDP connection: %w", err)
}
if n < len(discoveryMsg) {
return "", fmt.Errorf("failed to write the entire message (%d/%d)", n, len(discoveryMsg))
}
conn.SetReadDeadline(time.Now().Add(conf.Timeout))
resp := make([]byte, 2048)
if _, err := conn.Read(resp); err != nil {
if errors.Is(err, os.ErrDeadlineExceeded) {
return "", ErrNoServer
}
return "", fmt.Errorf("failed to read response from server: %w", err)
}
var payload discoveryPayload
if err := json.Unmarshal(bytes.Trim(resp, "\x00"), &payload); err != nil {
return "", fmt.Errorf("failed to parse response from server: %w", err)
}
return payload.ManagementAddress, nil
}

View file

@ -9,9 +9,6 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -1,17 +1,9 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View file

@ -1,11 +1,387 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package main
import "git.faercol.me/faercol/http-boot-config/config/cmd"
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() {
cmd.Execute()
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")
}
}

View file

@ -1,387 +0,0 @@
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", prefix, a.ID, a.Name, a.DevicePath)
}
}
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")
}
}

View file

@ -10,8 +10,7 @@ import (
"strings"
)
// 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>[0-9A-F]+)\* (?P<name>.+)\t(?P<device_path>.+\))`)
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},?)+)`)
@ -22,7 +21,8 @@ const execPath = "/usr/bin/efibootmgr"
type EfiApp struct {
ID int
Name string
DevicePath string
Path string
DiskID string
Active bool
}
@ -49,15 +49,20 @@ func efiAppFromBootMgrOutput(rawVal string, activeId int) (app EfiApp, ok bool)
if !ok {
return
}
devicePath, ok := result["device_path"]
filepath, ok := result["filepath"]
if !ok {
return
}
disk_id, ok := result["disk_id"]
if !ok {
return
}
return EfiApp{
ID: int(idInt),
Name: strings.TrimSpace(name),
DevicePath: strings.TrimSpace(devicePath),
Path: strings.TrimSpace(filepath),
Active: int(idInt) == activeId,
DiskID: disk_id,
}, true
}

View file

@ -10,7 +10,7 @@ func TestEfiFomBootMgr(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)` +
`57494e444f5753000100000088000000780000004200430044004f0042004a004500430054003d007b00390064006500610038003600320063002d0035006300640064002d00`)
expected := EfiApp{ID: 0, Name: "Windows Boot Manager", DevicePath: `HD(1,GPT,4ad714ba-6a01-4f76-90e3-1bb93a59b67e,0x800,0x32000)/File(\EFI\MICROSOFT\BOOT\BOOTMGFW.EFI)`, Active: true}
expected := EfiApp{ID: 0, Name: "Windows Boot Manager", Path: `\EFI\MICROSOFT\BOOT\BOOTMGFW.EFI`, Active: true, DiskID: "4ad714ba-6a01-4f76-90e3-1bb93a59b67e"}
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
assert.True(t, ok)
assert.Equal(t, expected, app)
@ -18,7 +18,7 @@ func TestEfiFomBootMgr(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)`
expected := EfiApp{ID: 1, Name: "Manjaro", DevicePath: `HD(1,GPT,16f06d01-50da-6544-86bd-f3457f980086,0x1000,0x96000)/File(\EFI\MANJARO\GRUBX64.EFI)`, Active: false}
expected := EfiApp{ID: 1, Name: "Manjaro", Path: `\EFI\MANJARO\GRUBX64.EFI`, Active: false, DiskID: "16f06d01-50da-6544-86bd-f3457f980086"}
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
assert.True(t, ok)
assert.Equal(t, expected, app)
@ -26,7 +26,7 @@ func TestEfiFomBootMgr(t *testing.T) {
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", DevicePath: `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)`, Active: false}
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)
@ -34,7 +34,7 @@ func TestEfiFomBootMgr(t *testing.T) {
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", DevicePath: `HD(1,GPT,c4540877-4592-494f-bd8a-2a23abb22c3f,0x800,0x100000)/File(\EFI\httpboot\httpboot.efi)`, Active: false}
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)
@ -42,18 +42,16 @@ func TestEfiFomBootMgr(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`
expected := EfiApp{ID: 2, Name: "UEFI OS", DevicePath: `HD(1,GPT,16f06d01-50da-6544-86bd-f3457f980086,0x1000,0x96000)/File(\EFI\BOOT\BOOTX64.EFI)`, Active: false}
expected := EfiApp{ID: 2, Name: "UEFI OS", Path: `\EFI\BOOT\BOOTX64.EFI`, Active: false, DiskID: "16f06d01-50da-6544-86bd-f3457f980086"}
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
assert.True(t, ok)
assert.Equal(t, expected, app)
})
t.Run("OK - CD", func(t *testing.T) {
t.Run("NOK - CD", func(t *testing.T) {
rawVal := `Boot0003* UEFI:CD/DVD Drive BBS(129,,0x0)`
expected := EfiApp{ID: 3, Name: "UEFI:CD/DVD Drive", DevicePath: `BBS(129,,0x0)`, Active: false}
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
assert.True(t, ok)
assert.Equal(t, expected, app)
_, ok := efiAppFromBootMgrOutput(rawVal, 0)
assert.False(t, ok)
})
t.Run("NOK - Other line", func(t *testing.T) {

View file

@ -18,7 +18,8 @@ const enrollURL = "/enroll"
type enrollEFIOption struct {
Name string `json:"name"`
DevicePath string `json:"device_path"`
Path string `json:"path"`
DiskID string `json:"disk_id"`
}
type enrollPayload struct {
@ -46,7 +47,7 @@ func enrollToServer(ctx context.Context, host string, name string, apps []prober
}
for _, a := range apps {
appID := uuid.New()
payload.Options[appID.String()] = enrollEFIOption{Name: a.Name, DevicePath: a.DevicePath}
payload.Options[appID.String()] = enrollEFIOption{Name: a.Name, Path: a.Path, DiskID: a.DiskID}
if a.Active {
payload.SelectedOption = appID.String()
}