From a173e1bf6d16faae86240f8d7e353c3a8e9cbb45 Mon Sep 17 00:00:00 2001 From: Melora Hugues Date: Sun, 13 Aug 2023 21:30:44 +0200 Subject: [PATCH] Allow getting the config from remote server --- config/go.mod | 5 +- config/go.sum | 2 + config/main.go | 40 ++++++++++++--- config/remote/remote.go | 111 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 config/remote/remote.go diff --git a/config/go.mod b/config/go.mod index bafeacd..e6e3c73 100644 --- a/config/go.mod +++ b/config/go.mod @@ -2,7 +2,10 @@ module git.faercol.me/faercol/http-boot-config/config 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 ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/config/go.sum b/config/go.sum index fa4b6e6..8d8b455 100644 --- a/config/go.sum +++ b/config/go.sum @@ -1,5 +1,7 @@ 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= diff --git a/config/main.go b/config/main.go index 0fe9e05..abb1b53 100644 --- a/config/main.go +++ b/config/main.go @@ -1,13 +1,17 @@ package main import ( + "context" "errors" "flag" + "fmt" "io/fs" "os" "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 @@ -19,9 +23,12 @@ const ( ) type cliArgs struct { - debug bool - colour bool - action action + debug bool + colour bool + action action + id uuid.UUID + remoteAddr string + prettyPrint bool } var defaultArgs cliArgs = cliArgs{ @@ -34,9 +41,13 @@ func parseArgs() (cliArgs, error) { args := defaultArgs var firstArg int - getRemoteFlagSet := flag.NewFlagSet("get-remote", flag.ExitOnError) 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") noColourFlag := flag.Bool("no-colour", false, "Disable colour logs") @@ -57,6 +68,13 @@ func parseArgs() (cliArgs, error) { 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") @@ -88,8 +106,17 @@ func displayAppList(l *logger.SimpleLogger) { } } -func getRemoteConfig(l *logger.SimpleLogger) { +func getRemoteConfig(l *logger.SimpleLogger, host string, id uuid.UUID, pretty bool) { l.Info("Getting config from remote server...") + if pretty { + 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 main() { @@ -99,12 +126,13 @@ func main() { 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) + getRemoteConfig(l, args.remoteAddr, args.id, args.prettyPrint) default: l.Fatal("Unknown action") } diff --git a/config/remote/remote.go b/config/remote/remote.go new file mode 100644 index 0000000..a22d196 --- /dev/null +++ b/config/remote/remote.go @@ -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 +}