backup
This commit is contained in:
parent
32bbd956f5
commit
3e4cbb70de
12 changed files with 804 additions and 382 deletions
0
config/LICENSE
Normal file
0
config/LICENSE
Normal file
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 (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
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/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
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
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=
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import "git.faercol.me/faercol/http-boot-config/config/cmd"
|
||||||
"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() {
|
func main() {
|
||||||
args, err := parseArgs()
|
cmd.Execute()
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
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")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue