Compare commits
2 commits
1383272d2c
...
a173e1bf6d
Author | SHA1 | Date | |
---|---|---|---|
a173e1bf6d | |||
136a90db19 |
4 changed files with 223 additions and 22 deletions
|
@ -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
|
||||||
|
|
|
@ -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=
|
||||||
|
|
119
config/main.go
119
config/main.go
|
@ -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")
|
||||||
|
|
||||||
|
for i, v := range os.Args {
|
||||||
|
switch v {
|
||||||
|
case "list":
|
||||||
|
args.action = actionList
|
||||||
|
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()
|
flag.Parse()
|
||||||
|
return cliArgs{}, errors.New("missing an action")
|
||||||
return cliArgs{
|
}
|
||||||
debug: *debugFlag,
|
|
||||||
colour: !*noColourFlag,
|
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(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())
|
||||||
}
|
}
|
||||||
|
|
||||||
func displayAppList(apps []prober.EfiApp, l *logger.SimpleLogger) {
|
|
||||||
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 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() {
|
func main() {
|
||||||
args := parseArgs()
|
args, err := parseArgs()
|
||||||
l := logger.New(args.colour, args.debug)
|
|
||||||
|
|
||||||
l.Info("Checking EFI directory for available boot images...")
|
|
||||||
images, err := prober.GetEFIApps(l)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, fs.ErrPermission) {
|
l := logger.New(true, false)
|
||||||
l.Fatal("Permission error, try to run the command as sudo")
|
l.Fatalf("Invalid command: %s", err.Error())
|
||||||
}
|
|
||||||
l.Fatalf("Failed to check EFI directory: %s", err.Error())
|
|
||||||
}
|
}
|
||||||
|
l := logger.New(args.colour, args.debug)
|
||||||
|
fmt.Print("")
|
||||||
|
|
||||||
displayAppList(images, l)
|
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
111
config/remote/remote.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue