Compare commits
2 commits
Author | SHA1 | Date | |
---|---|---|---|
3e4cbb70de | |||
32bbd956f5 |
16 changed files with 829 additions and 411 deletions
0
config/LICENSE
Normal file
0
config/LICENSE
Normal file
|
@ -5,4 +5,4 @@ build:
|
|||
go build -o build/
|
||||
|
||||
push-client:
|
||||
scp build/config root@192.168.122.2:/usr/bin/efi-http-config
|
||||
scp build/config root@192.168.122.76:/usr/bin/efi-http-config
|
||||
|
|
60
config/cmd/config.go
Normal file
60
config/cmd/config.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
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")
|
||||
}
|
60
config/cmd/discover.go
Normal file
60
config/cmd/discover.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
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")
|
||||
}
|
54
config/cmd/remote.go
Normal file
54
config/cmd/remote.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
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")
|
||||
}
|
51
config/cmd/root.go
Normal file
51
config/cmd/root.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
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")
|
||||
}
|
||||
|
||||
|
40
config/cmd/status.go
Normal file
40
config/cmd/status.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
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")
|
||||
}
|
76
config/config/config.go
Normal file
76
config/config/config.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
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
|
||||
}
|
59
config/discover/discover.go
Normal file
59
config/discover/discover.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
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
|
||||
}
|
|
@ -9,6 +9,9 @@ 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
|
||||
)
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
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=
|
||||
|
|
388
config/main.go
388
config/main.go
|
@ -1,387 +1,11 @@
|
|||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
|
||||
*/
|
||||
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
|
||||
}
|
||||
import "git.faercol.me/faercol/http-boot-config/config/cmd"
|
||||
|
||||
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")
|
||||
}
|
||||
cmd.Execute()
|
||||
}
|
||||
|
|
387
config/main.old
Normal file
387
config/main.old
Normal file
|
@ -0,0 +1,387 @@
|
|||
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")
|
||||
}
|
||||
}
|
|
@ -10,7 +10,8 @@ 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<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 activeRegexp = regexp.MustCompile(`BootCurrent: (\d+)`)
|
||||
var bootOrderRegexp = regexp.MustCompile(`BootOrder: ((?:[0-9A-F]{4},?)+)`)
|
||||
|
||||
|
@ -19,11 +20,10 @@ const HTTPBootLabel = "HTTP_BOOT"
|
|||
const execPath = "/usr/bin/efibootmgr"
|
||||
|
||||
type EfiApp struct {
|
||||
ID int
|
||||
Name string
|
||||
Path string
|
||||
DiskID string
|
||||
Active bool
|
||||
ID int
|
||||
Name string
|
||||
DevicePath string
|
||||
Active bool
|
||||
}
|
||||
|
||||
func efiAppFromBootMgrOutput(rawVal string, activeId int) (app EfiApp, ok bool) {
|
||||
|
@ -49,20 +49,15 @@ func efiAppFromBootMgrOutput(rawVal string, activeId int) (app EfiApp, ok bool)
|
|||
if !ok {
|
||||
return
|
||||
}
|
||||
filepath, ok := result["filepath"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
disk_id, ok := result["disk_id"]
|
||||
devicePath, ok := result["device_path"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
return EfiApp{
|
||||
ID: int(idInt),
|
||||
Name: strings.TrimSpace(name),
|
||||
Path: strings.TrimSpace(filepath),
|
||||
Active: int(idInt) == activeId,
|
||||
DiskID: disk_id,
|
||||
ID: int(idInt),
|
||||
Name: strings.TrimSpace(name),
|
||||
DevicePath: strings.TrimSpace(devicePath),
|
||||
Active: int(idInt) == activeId,
|
||||
}, true
|
||||
}
|
||||
|
||||
|
|
|
@ -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", Path: `\EFI\MICROSOFT\BOOT\BOOTMGFW.EFI`, Active: true, DiskID: "4ad714ba-6a01-4f76-90e3-1bb93a59b67e"}
|
||||
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}
|
||||
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", Path: `\EFI\MANJARO\GRUBX64.EFI`, Active: false, DiskID: "16f06d01-50da-6544-86bd-f3457f980086"}
|
||||
expected := EfiApp{ID: 1, Name: "Manjaro", DevicePath: `HD(1,GPT,16f06d01-50da-6544-86bd-f3457f980086,0x1000,0x96000)/File(\EFI\MANJARO\GRUBX64.EFI)`, Active: false}
|
||||
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", Path: `\EFI\systemd\systemd-bootx64.efi`, Active: false, DiskID: "c4540877-4592-494f-bd8a-2a23abb22c3f"}
|
||||
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}
|
||||
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", Path: `\EFI\httpboot\httpboot.efi`, Active: false, DiskID: "c4540877-4592-494f-bd8a-2a23abb22c3f"}
|
||||
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}
|
||||
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, expected, app)
|
||||
|
@ -42,16 +42,18 @@ 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", Path: `\EFI\BOOT\BOOTX64.EFI`, Active: false, DiskID: "16f06d01-50da-6544-86bd-f3457f980086"}
|
||||
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}
|
||||
app, ok := efiAppFromBootMgrOutput(rawVal, 0)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, expected, app)
|
||||
})
|
||||
|
||||
t.Run("NOK - CD", func(t *testing.T) {
|
||||
t.Run("OK - CD", func(t *testing.T) {
|
||||
rawVal := `Boot0003* UEFI:CD/DVD Drive BBS(129,,0x0)`
|
||||
_, ok := efiAppFromBootMgrOutput(rawVal, 0)
|
||||
assert.False(t, ok)
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("NOK - Other line", func(t *testing.T) {
|
||||
|
|
|
@ -17,9 +17,8 @@ import (
|
|||
const enrollURL = "/enroll"
|
||||
|
||||
type enrollEFIOption struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
DiskID string `json:"disk_id"`
|
||||
Name string `json:"name"`
|
||||
DevicePath string `json:"device_path"`
|
||||
}
|
||||
|
||||
type enrollPayload struct {
|
||||
|
@ -47,7 +46,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, Path: a.Path, DiskID: a.DiskID}
|
||||
payload.Options[appID.String()] = enrollEFIOption{Name: a.Name, DevicePath: a.DevicePath}
|
||||
if a.Active {
|
||||
payload.SelectedOption = appID.String()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue