diff --git a/config/LICENSE b/config/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/config/cmd/config.go b/config/cmd/config.go new file mode 100644 index 0000000..ba29a85 --- /dev/null +++ b/config/cmd/config.go @@ -0,0 +1,60 @@ +/* +Copyright © 2024 NAME HERE +*/ +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") +} diff --git a/config/cmd/discover.go b/config/cmd/discover.go new file mode 100644 index 0000000..1f4c389 --- /dev/null +++ b/config/cmd/discover.go @@ -0,0 +1,60 @@ +/* +Copyright © 2024 NAME HERE +*/ +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") +} diff --git a/config/cmd/remote.go b/config/cmd/remote.go new file mode 100644 index 0000000..372f028 --- /dev/null +++ b/config/cmd/remote.go @@ -0,0 +1,54 @@ +/* +Copyright © 2024 NAME HERE +*/ +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") +} diff --git a/config/cmd/root.go b/config/cmd/root.go new file mode 100644 index 0000000..7915a6f --- /dev/null +++ b/config/cmd/root.go @@ -0,0 +1,51 @@ +/* +Copyright © 2024 NAME HERE + +*/ +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") +} + + diff --git a/config/cmd/status.go b/config/cmd/status.go new file mode 100644 index 0000000..2af70f0 --- /dev/null +++ b/config/cmd/status.go @@ -0,0 +1,40 @@ +/* +Copyright © 2024 NAME HERE + +*/ +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") +} diff --git a/config/config/config.go b/config/config/config.go new file mode 100644 index 0000000..8656b02 --- /dev/null +++ b/config/config/config.go @@ -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 +} diff --git a/config/discover/discover.go b/config/discover/discover.go new file mode 100644 index 0000000..221d474 --- /dev/null +++ b/config/discover/discover.go @@ -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 +} diff --git a/config/go.mod b/config/go.mod index e6e3c73..44d9183 100644 --- a/config/go.mod +++ b/config/go.mod @@ -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 ) diff --git a/config/go.sum b/config/go.sum index 8d8b455..928a297 100644 --- a/config/go.sum +++ b/config/go.sum @@ -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= diff --git a/config/main.go b/config/main.go index 62ef547..a359380 100644 --- a/config/main.go +++ b/config/main.go @@ -1,387 +1,11 @@ +/* +Copyright © 2024 NAME HERE + +*/ 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 -} +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() } diff --git a/config/main.old b/config/main.old new file mode 100644 index 0000000..62ef547 --- /dev/null +++ b/config/main.old @@ -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") + } +}