From 639f4e60ea954e2fb487fee4817379fec1ce2759 Mon Sep 17 00:00:00 2001 From: Melora Hugues Date: Sun, 13 Aug 2023 22:34:31 +0200 Subject: [PATCH] Add the possibility to enroll a new client --- config/main.go | 25 ++++++++++- config/remote/enroll.go | 93 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 config/remote/enroll.go diff --git a/config/main.go b/config/main.go index abb1b53..d1748c8 100644 --- a/config/main.go +++ b/config/main.go @@ -19,6 +19,7 @@ type action int const ( actionList action = iota actionGetRemote + actionEnroll actionUnknown ) @@ -29,6 +30,7 @@ type cliArgs struct { id uuid.UUID remoteAddr string prettyPrint bool + name string } var defaultArgs cliArgs = cliArgs{ @@ -45,9 +47,13 @@ func parseArgs() (cliArgs, error) { 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") + 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") + debugFlag := flag.Bool("debug", false, "Display debug logs") noColourFlag := flag.Bool("no-colour", false, "Disable colour logs") @@ -57,6 +63,8 @@ func parseArgs() (cliArgs, error) { args.action = actionList case "get-remote": args.action = actionGetRemote + case "enroll": + args.action = actionEnroll default: continue } @@ -73,8 +81,12 @@ func parseArgs() (cliArgs, error) { return args, fmt.Errorf("invalid format for uuid %q", *uuidFlag) } args.id = parsedID - args.remoteAddr = *remoteFlag + args.remoteAddr = *hostGetRemoteFlag args.prettyPrint = !*jsonFlag + case actionEnroll: + enrollFlagSet.Parse(os.Args[firstArg:]) + args.remoteAddr = *hostEnrollFlag + args.name = *nameFlag default: flag.Parse() return cliArgs{}, errors.New("missing an action") @@ -119,6 +131,13 @@ func getRemoteConfig(l *logger.SimpleLogger, host string, id uuid.UUID, pretty b } } +func enroll(l *logger.SimpleLogger, host, name string) { + l.Info("Enrolling client") + if err := remote.Enroll(context.Background(), host, name, l); err != nil { + l.Fatal(err.Error()) + } +} + func main() { args, err := parseArgs() if err != nil { @@ -133,6 +152,8 @@ func main() { displayAppList(l) case actionGetRemote: getRemoteConfig(l, args.remoteAddr, args.id, args.prettyPrint) + case actionEnroll: + enroll(l, args.remoteAddr, args.name) default: l.Fatal("Unknown action") } diff --git a/config/remote/enroll.go b/config/remote/enroll.go new file mode 100644 index 0000000..9267b6f --- /dev/null +++ b/config/remote/enroll.go @@ -0,0 +1,93 @@ +package remote + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "git.faercol.me/faercol/http-boot-config/config/logger" + "git.faercol.me/faercol/http-boot-config/config/prober" + "github.com/google/uuid" +) + +const enrollURL = "/enroll" + +type enrollEFIOption struct { + Name string `json:"name"` + Path string `json:"path"` +} + +type enrollPayload struct { + Name string `json:"name"` + Options map[string]enrollEFIOption `json:"options"` + SelectedOption string `json:"selected_option"` +} + +type enrollRespPayload struct { + ID string `json:"ID"` +} + +func enrollToServer(ctx context.Context, host string, name string, apps []prober.EfiApp) (uuid.UUID, error) { + payload := enrollPayload{ + Name: name, + Options: make(map[string]enrollEFIOption), + } + for _, a := range apps { + appID := uuid.New() + payload.Options[appID.String()] = enrollEFIOption{Name: a.Name, Path: a.Path} + if a.Active { + payload.SelectedOption = appID.String() + } + } + + dat, err := json.Marshal(payload) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to serialize payload: %w", err) + } + + subCtx, cancel := context.WithTimeout(ctx, reqTimeout) + defer cancel() + req, err := http.NewRequestWithContext(subCtx, http.MethodPost, host+enrollURL, bytes.NewBuffer(dat)) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to initialize request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to do query to remote boot server: %w", err) + } + if resp.StatusCode != http.StatusOK { + return uuid.Nil, fmt.Errorf("unexpected returncode %d", resp.StatusCode) + } + + var respPayload enrollRespPayload + respDat, err := io.ReadAll(resp.Body) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to read response from server: %w", err) + } + if err := json.Unmarshal(respDat, &respPayload); err != nil { + return uuid.Nil, fmt.Errorf("failed to deserialize data from server: %w", err) + } + newID, err := uuid.Parse(respPayload.ID) + if err != nil { + return uuid.Nil, fmt.Errorf("invalid UUID %q from server: %w", respPayload.ID, err) + } + return newID, nil +} + +func Enroll(ctx context.Context, host, name string, l *logger.SimpleLogger) error { + apps, err := prober.GetEFIApps(l) + if err != nil { + return fmt.Errorf("failed to get list of available EFI applications: %w", err) + } + + newID, err := enrollToServer(ctx, host, name, apps) + if err != nil { + return fmt.Errorf("failed to enroll client to remote host: %w", err) + } + l.Infof("Successfully enrolled new client, ID is %q", newID.String()) + return nil +}