diff --git a/bootserver/controllers/client/client.go b/bootserver/controllers/client/client.go index 90938bd..97a30b9 100644 --- a/bootserver/controllers/client/client.go +++ b/bootserver/controllers/client/client.go @@ -1,85 +1,78 @@ package client -// import ( -// "encoding/json" -// "fmt" -// "io" -// "net" -// "net/http" +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" -// "git.faercol.me/faercol/http-boot-server/bootserver/helpers" -// "git.faercol.me/faercol/http-boot-server/bootserver/services" -// "github.com/sirupsen/logrus" -// ) + "git.faercol.me/faercol/http-boot-server/bootserver/helpers" + "git.faercol.me/faercol/http-boot-server/bootserver/services" + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) -// const BootRoute = "/boot" +const SetBootRoute = "/config/boot" -// type BootController struct { -// clientService *services.ClientHandlerService -// l *logrus.Logger -// } +type setBootOptionPayload struct { + ClientID string `json:"client_id"` + OptionID string `json:"option_id"` +} -// func NewBootController(logger *logrus.Logger, service *services.ClientHandlerService) *BootController { -// return &BootController{ -// clientService: service, -// l: logger, -// } -// } +type BootController struct { + clientService *services.ClientHandlerService + l *logrus.Logger +} -// func (bc *BootController) getBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, []byte, error) { -// bootOption, err := bc.clientService.GetClientSelectedBootOption(clientIP) -// if err != nil { -// return http.StatusInternalServerError, nil, fmt.Errorf("failed to get boot option: %w", err) -// } +func NewBootController(logger *logrus.Logger, service *services.ClientHandlerService) *BootController { + return &BootController{ + clientService: service, + l: logger, + } +} -// dat, err := json.Marshal(bootOption) -// if err != nil { -// return http.StatusInternalServerError, nil, fmt.Errorf("failed to serialize body") -// } +func (bc *BootController) setBootOption(w http.ResponseWriter, r *http.Request) (int, []byte, error) { + dat, err := io.ReadAll(r.Body) + if err != nil { + return http.StatusInternalServerError, nil, fmt.Errorf("failed to read body: %w", err) + } + var payload setBootOptionPayload + if err := json.Unmarshal(dat, &payload); err != nil { + return http.StatusBadRequest, nil, fmt.Errorf("failed to parse body: %w", err) + } -// w.Header().Add("Content-Type", "application/json") -// return http.StatusOK, dat, nil -// } + clientID, err := uuid.Parse(payload.ClientID) + if err != nil { + return http.StatusBadRequest, []byte("bad client ID"), fmt.Errorf("invalid format for client ID: %w", err) + } + optionID, err := uuid.Parse(payload.OptionID) + if err != nil { + return http.StatusBadRequest, []byte("bad option ID"), fmt.Errorf("invalid format for option ID: %w", err) + } -// func (bc *BootController) setBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, error) { -// dat, err := io.ReadAll(r.Body) -// if err != nil { -// return http.StatusInternalServerError, fmt.Errorf("failed to read body: %w", err) -// } -// var option string -// if err := json.Unmarshal(dat, &option); err != nil { -// return http.StatusInternalServerError, fmt.Errorf("failed to parse body: %w", err) -// } + if err := bc.clientService.SetClientBootOption(clientID, optionID.String()); err != nil { + if errors.Is(err, services.ErrUnknownClient) { + return http.StatusNotFound, []byte("unknown client"), err + } + if errors.Is(err, services.ErrUnknownBootOption) { + return http.StatusNotFound, []byte("unknown boot option"), err + } + return http.StatusInternalServerError, nil, fmt.Errorf("failed to set boot option for client: %w", err) + } -// if err := bc.clientService.SetClientBootOption(clientIP, option); err != nil { -// return http.StatusInternalServerError, fmt.Errorf("failed to set boot option for client: %w", err) -// } + return http.StatusAccepted, nil, nil +} -// return http.StatusAccepted, nil -// } +func (bc *BootController) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + helpers.HandleResponse(w, r, http.StatusMethodNotAllowed, nil, bc.l) + return + } -// func (bc *BootController) ServeHTTP(w http.ResponseWriter, r *http.Request) { -// clientIP, _, err := net.SplitHostPort(r.RemoteAddr) -// if err != nil { -// bc.l.Errorf("Failed to read remote IP: %s", err.Error()) -// helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, bc.l) -// return -// } - -// var returncode int -// var content []byte - -// switch r.Method { -// case http.MethodGet: -// returncode, content, err = bc.getBootOption(clientIP, w, r) -// case http.MethodPut: -// returncode, err = bc.setBootOption(clientIP, w, r) -// default: -// returncode = http.StatusMethodNotAllowed -// } - -// if err != nil { -// bc.l.Errorf("An error occured while handling boot request: %q", err.Error()) -// } -// helpers.HandleResponse(w, r, returncode, content, bc.l) -// } + returncode, content, err := bc.setBootOption(w, r) + if err != nil { + bc.l.Errorf("Error setting boot option for client: %s", err.Error()) + } + helpers.HandleResponse(w, r, returncode, content, bc.l) +} diff --git a/bootserver/controllers/ui/ui.go b/bootserver/controllers/ui/ui.go index 7a25681..a9a2507 100644 --- a/bootserver/controllers/ui/ui.go +++ b/bootserver/controllers/ui/ui.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "path/filepath" + "sort" "git.faercol.me/faercol/http-boot-server/bootserver/helpers" "git.faercol.me/faercol/http-boot-server/bootserver/services" @@ -19,6 +20,7 @@ type templateBootOption struct { ID string Name string Path string + DiskID string Selected bool } @@ -65,8 +67,12 @@ func (uc *UIController) serveUI(w http.ResponseWriter, r *http.Request) (int, in Name: bo.Name, Path: bo.Path, ID: id, + DiskID: bo.DiskID, Selected: id == clt.SelectedOption, }) + sort.Slice(tplBO, func(i, j int) bool { + return tplBO[i].ID < tplBO[j].ID + }) } dat.Clients = append(dat.Clients, templateClient{ ID: clt.ID.String(), @@ -87,10 +93,16 @@ func (uc *UIController) serveUI(w http.ResponseWriter, r *http.Request) (int, in } func (uc *UIController) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.RequestURI != "/" { + uc.l.Errorf("Unhandled route %q", r.RequestURI) + helpers.HandleResponse(w, r, http.StatusNotFound, nil, uc.l) + return + } returncode, contentLength, err := uc.serveUI(w, r) if err != nil { uc.l.Errorf("Error serving UI: %s", err.Error()) helpers.HandleResponse(w, r, returncode, nil, uc.l) + } else { + helpers.AddToContext(r, returncode, contentLength) } - helpers.AddToContext(r, returncode, contentLength) } diff --git a/bootserver/server/server.go b/bootserver/server/server.go index f245a3d..27b275e 100644 --- a/bootserver/server/server.go +++ b/bootserver/server/server.go @@ -67,10 +67,11 @@ func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) { } service := services.NewClientHandlerService(appConf.DataFilepath, logger) controllers := map[string]http.Handler{ - client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service), logger), - client.ConfigRoute: middlewares.WithLogger(client.NewGetConfigController(logger, service, appConf), logger), - ui.StaticRoute: &ui.StaticController{}, - ui.UIRoute: middlewares.WithLogger(ui.NewUIController(logger, service), logger), + client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service), logger), + client.ConfigRoute: middlewares.WithLogger(client.NewGetConfigController(logger, service, appConf), logger), + client.SetBootRoute: middlewares.WithLogger(client.NewBootController(logger, service), logger), + ui.StaticRoute: &ui.StaticController{}, + ui.UIRoute: middlewares.WithLogger(ui.NewUIController(logger, service), logger), } m := http.NewServeMux() diff --git a/bootserver/services/services.go b/bootserver/services/services.go index 4eedb04..2293cc4 100644 --- a/bootserver/services/services.go +++ b/bootserver/services/services.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "sort" "time" "git.faercol.me/faercol/http-boot-server/bootserver/bootoption" @@ -111,6 +112,9 @@ func (chs *ClientHandlerService) GetAllClientConfig() ([]*bootoption.Client, err clt.ID = id clientList = append(clientList, clt) } + sort.Slice(clientList, func(i, j int) bool { + return clientList[i].ID.String() < clientList[j].ID.String() + }) return clientList, nil } diff --git a/bootserver/static/scripts/index.js b/bootserver/static/scripts/index.js new file mode 100644 index 0000000..96221e0 --- /dev/null +++ b/bootserver/static/scripts/index.js @@ -0,0 +1,20 @@ +function selectBootOption(clientID, bootID) { + const Http = new XMLHttpRequest; + var host = window.location.protocol + "//" + window.location.host; + const url = host + "/config/boot" + console.debug(`Sending request to ${url}`); + Http.open("PUT", url); + Http.setRequestHeader("Content-Type", "application/json"); + const body = JSON.stringify({ + client_id: clientID, + option_id: bootID, + }); + Http.onload = () => { + if (Http.readyState === 4 && Http.status === 202) { + location.reload(); + } else { + console.error(`Unexpected returncode ${Http.status}`) + } + }; + Http.send(body); +} diff --git a/bootserver/static/stylesheets/main.css b/bootserver/static/stylesheets/main.css index 729c094..7dc4799 100644 --- a/bootserver/static/stylesheets/main.css +++ b/bootserver/static/stylesheets/main.css @@ -1,24 +1,48 @@ +:root { + --page-background: #000000; + --background: #121212; + --surface: #181818; + --surface-active: #303030; + --surface-hover: #aaaaaa; + --primary: #3b80c9; + --primary-variant: #c9953b; + --secondary: #c94e3b; + --text-main: #ffffff; + --text-variant: #9B9B9B; +} + + body { - background-color: black; - color: white; + background-color: var(--page-background); + color: var(--text-main); +} + +.page-content { + width: 90%; + max-width: 1200px; + margin: auto; +} + +.container { + border-radius: 5px; + background-color: var(--surface); + padding: 5px; + border-radius: 5px 5px; + margin: 5px auto; } .main-container { - border-radius: 5px; - background-color: #121212; padding: 20px; } .title-container { - border-radius: 5px 5px 0px 0px; - padding: 5px; - background-color: #1A3A5D; + background-color: var(--primary); } .client-list-container { border-radius: 0px 0px 5px 5px; padding: 10px; - background-color: #232323; + background-color: var(--surface); } .client-container { @@ -36,7 +60,7 @@ body { .client-uuid { font-size: smaller; - color: #9B9B9B; + color: var(--text-variant); } } @@ -48,9 +72,16 @@ body { .client-boot-option { margin: 3px auto; padding: 8px 8px; + cursor: pointer; &.selected { - background-color: #3E4042; + background-color: var(--surface-active); + cursor: default; + } + + &:hover { + background-color: var(--surface-hover); + --text-variant: #000000; } } @@ -61,7 +92,7 @@ body { .boot-option-uuid { font-size: smaller; - color: #9B9B9B; + color: var(--text-variant); } .boot-option-path { diff --git a/bootserver/templates/index.html b/bootserver/templates/index.html index 3208884..f45d03f 100644 --- a/bootserver/templates/index.html +++ b/bootserver/templates/index.html @@ -5,43 +5,47 @@