Compare commits
No commits in common. "a42d3e56436dba0c466de8ec8232e940d5340737" and "0b6bbce7d594b3666c8ad8a7897ad207c52b6ad0" have entirely different histories.
a42d3e5643
...
0b6bbce7d5
8 changed files with 44 additions and 249 deletions
|
@ -4,12 +4,14 @@ import "github.com/google/uuid"
|
|||
|
||||
type EFIApp struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ID uuid.UUID
|
||||
IP string `json:"ip"`
|
||||
Name string `json:"name"`
|
||||
Options map[string]EFIApp `json:"options"`
|
||||
Options []EFIApp `json:"options"`
|
||||
SelectedOption string `json:"selected_option"`
|
||||
}
|
||||
|
|
|
@ -42,9 +42,6 @@ type jsonConf struct {
|
|||
Log struct {
|
||||
Level string `json:"level"`
|
||||
} `json:"log"`
|
||||
Storage struct {
|
||||
Path string `json:"path"`
|
||||
} `json:"storage"`
|
||||
Server struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
|
@ -61,7 +58,6 @@ type jsonConf struct {
|
|||
type AppConfig struct {
|
||||
LogLevel logrus.Level
|
||||
ServerMode ListeningMode
|
||||
DataFilepath string
|
||||
Host string
|
||||
Port int
|
||||
SockPath string
|
||||
|
@ -97,14 +93,12 @@ func (ac *AppConfig) UnmarshalJSON(data []byte) error {
|
|||
ac.UPDMcastGroup = jsonConf.BootProvider.McastGroup
|
||||
ac.UDPIface = jsonConf.BootProvider.Iface
|
||||
ac.UDPPort = jsonConf.BootProvider.Port
|
||||
ac.DataFilepath = jsonConf.Storage.Path
|
||||
return nil
|
||||
}
|
||||
|
||||
var defaultConfig AppConfig = AppConfig{
|
||||
LogLevel: logrus.InfoLevel,
|
||||
ServerMode: ModeNet,
|
||||
DataFilepath: "boot_options.json",
|
||||
Host: "0.0.0.0",
|
||||
Port: 5000,
|
||||
UPDMcastGroup: "ff02::abcd:1234",
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"git.faercol.me/faercol/http-boot-server/bootserver/bootoption"
|
||||
|
@ -30,6 +31,10 @@ func (ec *EnrollController) enrollMachine(w http.ResponseWriter, r *http.Request
|
|||
if r.Method != http.MethodPost {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to read remote IP: %w", err)
|
||||
}
|
||||
|
||||
dat, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
|
@ -40,9 +45,10 @@ func (ec *EnrollController) enrollMachine(w http.ResponseWriter, r *http.Request
|
|||
if err := json.Unmarshal(dat, &client); err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to parse body: %w", err)
|
||||
}
|
||||
client.IP = clientIP
|
||||
|
||||
ec.clientService.AddClient(&client)
|
||||
ec.l.Infof("Added client")
|
||||
ec.l.Infof("Added client %s", clientIP)
|
||||
return http.StatusAccepted, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
package filelock
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrLocked = errors.New("failed to get a lock in a timely manner")
|
||||
|
||||
type FileLock struct {
|
||||
lockPath string
|
||||
}
|
||||
|
||||
func New(filepath string) *FileLock {
|
||||
dir, filename := path.Split(filepath)
|
||||
lockPath := path.Join(dir, fmt.Sprintf(".%s.lock", filename))
|
||||
return &FileLock{
|
||||
lockPath: lockPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (fl *FileLock) lockFileExists() bool {
|
||||
if _, err := os.Stat(fl.lockPath); errors.Is(err, os.ErrNotExist) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (fl *FileLock) Lock(timeout time.Duration) error {
|
||||
start := time.Now().UTC()
|
||||
end := start.Add(timeout)
|
||||
|
||||
for {
|
||||
if !fl.lockFileExists() {
|
||||
if _, err := os.Create(fl.lockPath); err != nil {
|
||||
return fmt.Errorf("failed to create lock: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if time.Now().After(end) {
|
||||
return ErrLocked
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (fl *FileLock) Unlock() error {
|
||||
if err := os.Remove(fl.lockPath); err != nil {
|
||||
return fmt.Errorf("failed to remove lock: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -10,7 +10,6 @@ import (
|
|||
"git.faercol.me/faercol/http-boot-server/bootserver/config"
|
||||
"git.faercol.me/faercol/http-boot-server/bootserver/logger"
|
||||
"git.faercol.me/faercol/http-boot-server/bootserver/server"
|
||||
"git.faercol.me/faercol/http-boot-server/bootserver/services"
|
||||
"git.faercol.me/faercol/http-boot-server/bootserver/udplistener"
|
||||
)
|
||||
|
||||
|
@ -43,9 +42,6 @@ func main() {
|
|||
logger.Init(conf.LogLevel)
|
||||
logger.L.Infof("Initialized logger with level %v", conf.LogLevel)
|
||||
|
||||
logger.L.Info("Initializing data access service")
|
||||
services.NewClientHandlerService(conf.DataFilepath, logger.L).Init()
|
||||
|
||||
logger.L.Info("Initializing server")
|
||||
s, err := server.New(conf, logger.L)
|
||||
if err != nil {
|
||||
|
@ -53,7 +49,7 @@ func main() {
|
|||
}
|
||||
|
||||
logger.L.Info("Initializing UDP listener")
|
||||
listener, err := udplistener.New(conf, logger.L)
|
||||
listener, err := udplistener.New(conf.UDPIface, conf.UPDMcastGroup, conf.UDPPort, logger.L)
|
||||
if err != nil {
|
||||
logger.L.Fatalf("Failed to initialize UDP listener: %s", err.Error())
|
||||
}
|
||||
|
|
|
@ -64,8 +64,9 @@ func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) {
|
|||
default:
|
||||
panic(fmt.Errorf("unexpected listening mode %v", appConf.ServerMode))
|
||||
}
|
||||
service := services.NewClientHandlerService(appConf.DataFilepath, logger)
|
||||
service := services.NewClientHandlerService()
|
||||
controllers := map[string]http.Handler{
|
||||
// client.BootRoute: middlewares.WithLogger(client.NewBootController(logger, service), logger),
|
||||
client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service), logger),
|
||||
}
|
||||
|
||||
|
|
|
@ -1,112 +1,32 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.faercol.me/faercol/http-boot-server/bootserver/bootoption"
|
||||
"git.faercol.me/faercol/http-boot-server/bootserver/filelock"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var ErrUnknownClient = errors.New("unknown client")
|
||||
var ErrUnselectedBootOption = errors.New("unselected boot option")
|
||||
var ErrUnknownBootOption = errors.New("unknown boot option")
|
||||
|
||||
const defaultLockTimeout = 1 * time.Second
|
||||
|
||||
type ClientHandlerService struct {
|
||||
filepath string
|
||||
fileLock *filelock.FileLock
|
||||
lockTimeout time.Duration
|
||||
logger *logrus.Logger
|
||||
clients map[uuid.UUID]*bootoption.Client
|
||||
}
|
||||
|
||||
func NewClientHandlerService(filepath string, logger *logrus.Logger) *ClientHandlerService {
|
||||
func NewClientHandlerService() *ClientHandlerService {
|
||||
return &ClientHandlerService{
|
||||
filepath: filepath,
|
||||
fileLock: filelock.New(filepath),
|
||||
lockTimeout: defaultLockTimeout,
|
||||
logger: logger,
|
||||
clients: make(map[uuid.UUID]*bootoption.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (chs *ClientHandlerService) Init() {
|
||||
if _, err := os.Open(chs.filepath); errors.Is(err, os.ErrNotExist) {
|
||||
if err := os.WriteFile(chs.filepath, []byte("{}"), 0o644); err != nil {
|
||||
panic(fmt.Errorf("failed to init data file: %w", err))
|
||||
}
|
||||
}
|
||||
chs.fileLock.Unlock()
|
||||
}
|
||||
|
||||
func (chs *ClientHandlerService) unload(conf map[uuid.UUID]*bootoption.Client) error {
|
||||
dat, err := json.MarshalIndent(conf, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal data to JSON: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(chs.filepath, dat, 0o644); err != nil {
|
||||
return fmt.Errorf("failed to commit to the data file: %w", err)
|
||||
}
|
||||
if err := chs.fileLock.Unlock(); err != nil {
|
||||
return fmt.Errorf("failed to release the lock to data file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (chs *ClientHandlerService) load() (map[uuid.UUID]*bootoption.Client, error) {
|
||||
conf := make(map[uuid.UUID]*bootoption.Client)
|
||||
|
||||
if err := chs.fileLock.Lock(chs.lockTimeout); err != nil {
|
||||
return nil, fmt.Errorf("failed to obtain a lock to the data file: %w", err)
|
||||
}
|
||||
|
||||
dat, err := os.ReadFile(chs.filepath)
|
||||
if err != nil {
|
||||
defer chs.fileLock.Unlock()
|
||||
return nil, fmt.Errorf("failed to read data file: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(dat, &conf); err != nil {
|
||||
defer chs.fileLock.Unlock()
|
||||
return nil, fmt.Errorf("failed to parse data file: %w", err)
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func (chs *ClientHandlerService) unloadNoCommmit(conf map[uuid.UUID]*bootoption.Client) {
|
||||
if err := chs.unload(conf); err != nil {
|
||||
chs.logger.Errorf("Failed to unload config: %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (chs *ClientHandlerService) AddClient(client *bootoption.Client) (uuid.UUID, error) {
|
||||
clients, err := chs.load()
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("failed to load current config: %w", err)
|
||||
}
|
||||
|
||||
client.ID = uuid.New()
|
||||
clients[client.ID] = client
|
||||
|
||||
if err := chs.unload(clients); err != nil {
|
||||
return uuid.Nil, fmt.Errorf("failed to save current config: %w", err)
|
||||
}
|
||||
return client.ID, nil
|
||||
func (chs *ClientHandlerService) AddClient(client *bootoption.Client) {
|
||||
chs.clients[client.ID] = client
|
||||
}
|
||||
|
||||
func (chs *ClientHandlerService) GetClientSelectedBootOption(client uuid.UUID) (*bootoption.EFIApp, error) {
|
||||
clients, err := chs.load()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load current config: %w", err)
|
||||
}
|
||||
defer chs.unloadNoCommmit(clients)
|
||||
|
||||
clientDetails, ok := clients[client]
|
||||
clientDetails, ok := chs.clients[client]
|
||||
if !ok {
|
||||
return nil, ErrUnknownClient
|
||||
}
|
||||
|
@ -115,39 +35,25 @@ func (chs *ClientHandlerService) GetClientSelectedBootOption(client uuid.UUID) (
|
|||
return nil, ErrUnselectedBootOption
|
||||
}
|
||||
|
||||
if option, ok := clientDetails.Options[clientDetails.SelectedOption]; !ok {
|
||||
return nil, ErrUnknownBootOption
|
||||
} else {
|
||||
return &option, nil
|
||||
for _, o := range clientDetails.Options {
|
||||
if o.Name == clientDetails.SelectedOption {
|
||||
return &o, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrUnknownBootOption
|
||||
}
|
||||
|
||||
func (chs *ClientHandlerService) SetClientBootOption(client uuid.UUID, option string) error {
|
||||
var err error
|
||||
|
||||
clients, err := chs.load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load current config: %w", err)
|
||||
}
|
||||
|
||||
clientDetails, ok := clients[client]
|
||||
clientDetails, ok := chs.clients[client]
|
||||
if !ok {
|
||||
err = ErrUnknownClient
|
||||
} else {
|
||||
if _, ok := clientDetails.Options[option]; !ok {
|
||||
err = ErrUnknownBootOption
|
||||
} else {
|
||||
return ErrUnknownClient
|
||||
}
|
||||
|
||||
for _, o := range clientDetails.Options {
|
||||
if o.Name == option {
|
||||
clientDetails.SelectedOption = option
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
defer chs.unloadNoCommmit(clients)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := chs.unload(clients); err != nil {
|
||||
return fmt.Errorf("failed to save current config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ErrUnknownBootOption
|
||||
}
|
||||
|
|
|
@ -3,12 +3,10 @@ package udplistener
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"git.faercol.me/faercol/http-boot-server/bootserver/bootprotocol"
|
||||
"git.faercol.me/faercol/http-boot-server/bootserver/config"
|
||||
"git.faercol.me/faercol/http-boot-server/bootserver/services"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -30,22 +28,21 @@ type UDPListener struct {
|
|||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func New(conf *config.AppConfig, log *logrus.Logger) (*UDPListener, error) {
|
||||
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("[%s]:%d", conf.UPDMcastGroup, conf.UDPPort))
|
||||
func New(ifaceName, multicastGroup string, port int, log *logrus.Logger) (*UDPListener, error) {
|
||||
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("[%s]:%d", multicastGroup, port))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve UDP address: %w", err)
|
||||
}
|
||||
|
||||
iface, err := net.InterfaceByName(conf.UDPIface)
|
||||
iface, err := net.InterfaceByName(ifaceName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve interface name %s: %w", conf.UDPIface, err)
|
||||
return nil, fmt.Errorf("failed to resolve interface name %s: %w", ifaceName, err)
|
||||
}
|
||||
|
||||
return &UDPListener{
|
||||
addr: addr,
|
||||
iface: iface,
|
||||
ctx: context.TODO(),
|
||||
service: services.NewClientHandlerService(conf.DataFilepath, log),
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
@ -60,56 +57,6 @@ func (l *UDPListener) Init() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (l *UDPListener) handleBootRequest(msg bootprotocol.Message, subLogger logrus.FieldLogger) bootprotocol.Message {
|
||||
subLogger.Debugf("Processing message %q", msg.String())
|
||||
requestLogger := subLogger.WithField("clientID", msg.ID().String())
|
||||
requestLogger.Debug("Getting boot option for client")
|
||||
|
||||
bootOption, err := l.service.GetClientSelectedBootOption(msg.ID())
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrUnknownClient) || errors.Is(err, services.ErrUnselectedBootOption) {
|
||||
requestLogger.Warnf("Client is not configured, returning an error (original error is %q)", err.Error())
|
||||
return bootprotocol.Deny(msg.ID(), "client not configured")
|
||||
}
|
||||
if errors.Is(err, services.ErrUnknownBootOption) {
|
||||
requestLogger.Errorf("Invalid config for client: %s", err.Error())
|
||||
return bootprotocol.Deny(msg.ID(), "invalid client config")
|
||||
}
|
||||
requestLogger.Errorf("Failed to get config for client: %s", err.Error())
|
||||
return bootprotocol.Deny(msg.ID(), "unknown server error")
|
||||
}
|
||||
return bootprotocol.Accept(msg.ID(), bootOption.Path)
|
||||
}
|
||||
|
||||
func (l *UDPListener) handleClient(msg *udpMessage) error {
|
||||
clientLogger := l.log.WithField("clientIP", msg.sourceAddr.IP)
|
||||
|
||||
clientLogger.Debug("Handling request for client")
|
||||
response := l.handleBootRequest(msg.message, clientLogger)
|
||||
|
||||
clientLogger.Debug("Dialing client for answer")
|
||||
con, err := net.DialUDP("udp", nil, msg.sourceAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to dialed client: %w", err)
|
||||
}
|
||||
defer con.Close()
|
||||
|
||||
clientLogger.Debug("Sending response to client")
|
||||
dat, err := response.MarshalBinary()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal response to bytes, %w", err)
|
||||
}
|
||||
|
||||
n, err := con.Write(dat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send response to client, %w", err)
|
||||
}
|
||||
if n != len(dat) {
|
||||
return fmt.Errorf("failed to send the entire response to client (%d/%d)", n, len(dat))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *UDPListener) listen() (*udpMessage, error) {
|
||||
buffer := make([]byte, bufferLength)
|
||||
n, source, err := l.l.ReadFromUDP(buffer)
|
||||
|
@ -154,9 +101,7 @@ func (l *UDPListener) mainLoop() {
|
|||
case err := <-errChan:
|
||||
l.log.Error(err)
|
||||
case msg := <-msgChan:
|
||||
if err := l.handleClient(msg); err != nil {
|
||||
l.log.Errorf("Failed to handle message from client: %q", err.Error())
|
||||
}
|
||||
l.log.Infof("Request from %s: %q", msg.sourceAddr.String(), msg.message.String())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue