Compare commits

...

2 commits

Author SHA1 Message Date
a173e1bf6d Allow getting the config from remote server 2023-08-13 21:30:44 +02:00
136a90db19 add basic command parser 2023-08-13 18:46:06 +02:00
4 changed files with 223 additions and 22 deletions

View file

@ -2,7 +2,10 @@ module git.faercol.me/faercol/http-boot-config/config
go 1.20 go 1.20
require github.com/stretchr/testify v1.8.4 require (
github.com/google/uuid v1.3.0
github.com/stretchr/testify v1.8.4
)
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect

View file

@ -1,5 +1,7 @@
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=

View file

@ -1,32 +1,101 @@
package main package main
import ( import (
"context"
"errors" "errors"
"flag" "flag"
"fmt"
"io/fs" "io/fs"
"os"
"git.faercol.me/faercol/http-boot-config/config/logger" "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/prober"
"git.faercol.me/faercol/http-boot-config/config/remote"
"github.com/google/uuid"
)
type action int
const (
actionList action = iota
actionGetRemote
actionUnknown
) )
type cliArgs struct { type cliArgs struct {
debug bool debug bool
colour bool colour bool
action action
id uuid.UUID
remoteAddr string
prettyPrint bool
} }
func parseArgs() cliArgs { 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)
getRemoteFlagSet := flag.NewFlagSet("get-remote", flag.ExitOnError)
uuidFlag := getRemoteFlagSet.String("uuid", "", "Client UUID")
remoteFlag := getRemoteFlagSet.String("remote-host", "http://localhost:5000", "Address for the remote boot server")
jsonFlag := getRemoteFlagSet.Bool("json", false, "Display the result in JSON format")
debugFlag := flag.Bool("debug", false, "Display debug logs") debugFlag := flag.Bool("debug", false, "Display debug logs")
noColourFlag := flag.Bool("no-colour", false, "Disable colour logs") noColourFlag := flag.Bool("no-colour", false, "Disable colour logs")
flag.Parse() for i, v := range os.Args {
switch v {
return cliArgs{ case "list":
debug: *debugFlag, args.action = actionList
colour: !*noColourFlag, case "get-remote":
args.action = actionGetRemote
default:
continue
}
firstArg = i + 1
} }
switch args.action {
case actionList:
listFlagSet.Parse(os.Args[firstArg:])
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 = *remoteFlag
args.prettyPrint = !*jsonFlag
default:
flag.Parse()
return cliArgs{}, errors.New("missing an action")
}
flag.Parse()
args.debug = *debugFlag
args.colour = !*noColourFlag
return args, nil
} }
func displayAppList(apps []prober.EfiApp, l *logger.SimpleLogger) { func displayAppList(l *logger.SimpleLogger) {
l.Info("Checking EFI directory for available boot images...")
apps, err := prober.GetEFIApps(l)
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:") l.Info("Found the following EFI applications:")
for _, a := range apps { for _, a := range apps {
prefix := " " prefix := " "
@ -37,18 +106,34 @@ func displayAppList(apps []prober.EfiApp, l *logger.SimpleLogger) {
} }
} }
func main() { func getRemoteConfig(l *logger.SimpleLogger, host string, id uuid.UUID, pretty bool) {
args := parseArgs() l.Info("Getting config from remote server...")
l := logger.New(args.colour, args.debug) if pretty {
if err := remote.DisplayRemoteConfigPretty(context.Background(), host, id, l); err != nil {
l.Info("Checking EFI directory for available boot images...") l.Fatal(err.Error())
images, err := prober.GetEFIApps(l) }
if err != nil { } else {
if errors.Is(err, fs.ErrPermission) { if err := remote.DisplayRemoteConfigJSON(context.Background(), host, id, l); err != nil {
l.Fatal("Permission error, try to run the command as sudo") l.Fatal(err.Error())
} }
l.Fatalf("Failed to check EFI directory: %s", err.Error())
} }
}
displayAppList(images, l)
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 actionGetRemote:
getRemoteConfig(l, args.remoteAddr, args.id, args.prettyPrint)
default:
l.Fatal("Unknown action")
}
} }

111
config/remote/remote.go Normal file
View file

@ -0,0 +1,111 @@
package remote
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"git.faercol.me/faercol/http-boot-config/config/logger"
"github.com/google/uuid"
)
var ErrNotEnrolled = errors.New("client not enrolled")
const getBootRoute = "/config"
const reqTimeout = 5 * time.Second
type clientConfig struct {
EFIConfig struct {
ID string `json:"ID"`
Name string `json:"name"`
Options map[string]struct {
Name string `json:"name"`
Path string `json:"path"`
} `json:"options"`
SelectedOption string `json:"selected_option"`
} `json:"efi_config"`
NetConfig struct {
MulticastGroup string `json:"multicast_group"`
Port int `json:"port"`
} `json:"net_config"`
}
func getRemoteConfig(ctx context.Context, host string, id uuid.UUID, l *logger.SimpleLogger) ([]byte, error) {
subCtx, cancel := context.WithTimeout(ctx, reqTimeout)
defer cancel()
req, err := http.NewRequestWithContext(subCtx, http.MethodGet, host+getBootRoute, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
q := req.URL.Query()
q.Add("id", id.String())
req.URL.RawQuery = q.Encode()
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to query server: %w", err)
}
switch resp.StatusCode {
case http.StatusBadRequest:
return nil, fmt.Errorf("invalid UUID")
case http.StatusNotFound:
return nil, ErrNotEnrolled
case http.StatusOK:
dat, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
return dat, nil
default:
return nil, fmt.Errorf("unexpected server error (%d)", resp.StatusCode)
}
}
func DisplayRemoteConfigJSON(ctx context.Context, host string, id uuid.UUID, l *logger.SimpleLogger) error {
dat, err := getRemoteConfig(ctx, host, id, l)
if err != nil {
return err
}
// We want to prettify the output for the user
buf := bytes.NewBuffer(nil)
if err := json.Indent(buf, dat, "", " "); err != nil {
return fmt.Errorf("failed to format result: %w", err)
}
l.Info(buf.String())
return nil
}
func DisplayRemoteConfigPretty(ctx context.Context, host string, id uuid.UUID, l *logger.SimpleLogger) error {
dat, err := getRemoteConfig(ctx, host, id, l)
if err != nil {
return err
}
var parsedConf clientConfig
if err := json.Unmarshal(dat, &parsedConf); err != nil {
return fmt.Errorf("invalid JSON format for result: %w", err)
}
l.Infof("Got the following config for client %s:", parsedConf.EFIConfig.Name)
selectedOption, ok := parsedConf.EFIConfig.Options[parsedConf.EFIConfig.SelectedOption]
if !ok {
l.Info(" * No selected boot option for client")
} else {
l.Infof(" * Client is set to boot to %s", selectedOption.Name)
}
l.Info(" * Available options are:")
for _, option := range parsedConf.EFIConfig.Options {
l.Infof("\t - %s: %s", option.Name, option.Path)
}
l.Infof(" * Remote boot server is listening on %s:%d", parsedConf.NetConfig.MulticastGroup, parsedConf.NetConfig.Port)
return nil
}