Compare commits

...

2 commits

Author SHA1 Message Date
a42d3e5643 Implemet multicast protocol
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-30 15:56:31 +02:00
3950a59241 Add data persistence 2023-07-30 11:20:45 +02:00
8 changed files with 249 additions and 44 deletions

View file

@ -4,14 +4,12 @@ import "github.com/google/uuid"
type EFIApp struct { type EFIApp struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"`
Path string `json:"path"` Path string `json:"path"`
} }
type Client struct { type Client struct {
ID uuid.UUID ID uuid.UUID
IP string `json:"ip"`
Name string `json:"name"` Name string `json:"name"`
Options []EFIApp `json:"options"` Options map[string]EFIApp `json:"options"`
SelectedOption string `json:"selected_option"` SelectedOption string `json:"selected_option"`
} }

View file

@ -42,6 +42,9 @@ type jsonConf struct {
Log struct { Log struct {
Level string `json:"level"` Level string `json:"level"`
} `json:"log"` } `json:"log"`
Storage struct {
Path string `json:"path"`
} `json:"storage"`
Server struct { Server struct {
Host string `json:"host"` Host string `json:"host"`
Port int `json:"port"` Port int `json:"port"`
@ -58,6 +61,7 @@ type jsonConf struct {
type AppConfig struct { type AppConfig struct {
LogLevel logrus.Level LogLevel logrus.Level
ServerMode ListeningMode ServerMode ListeningMode
DataFilepath string
Host string Host string
Port int Port int
SockPath string SockPath string
@ -93,12 +97,14 @@ func (ac *AppConfig) UnmarshalJSON(data []byte) error {
ac.UPDMcastGroup = jsonConf.BootProvider.McastGroup ac.UPDMcastGroup = jsonConf.BootProvider.McastGroup
ac.UDPIface = jsonConf.BootProvider.Iface ac.UDPIface = jsonConf.BootProvider.Iface
ac.UDPPort = jsonConf.BootProvider.Port ac.UDPPort = jsonConf.BootProvider.Port
ac.DataFilepath = jsonConf.Storage.Path
return nil return nil
} }
var defaultConfig AppConfig = AppConfig{ var defaultConfig AppConfig = AppConfig{
LogLevel: logrus.InfoLevel, LogLevel: logrus.InfoLevel,
ServerMode: ModeNet, ServerMode: ModeNet,
DataFilepath: "boot_options.json",
Host: "0.0.0.0", Host: "0.0.0.0",
Port: 5000, Port: 5000,
UPDMcastGroup: "ff02::abcd:1234", UPDMcastGroup: "ff02::abcd:1234",

View file

@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"git.faercol.me/faercol/http-boot-server/bootserver/bootoption" "git.faercol.me/faercol/http-boot-server/bootserver/bootoption"
@ -31,10 +30,6 @@ func (ec *EnrollController) enrollMachine(w http.ResponseWriter, r *http.Request
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
return http.StatusMethodNotAllowed, nil 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) dat, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
@ -45,10 +40,9 @@ func (ec *EnrollController) enrollMachine(w http.ResponseWriter, r *http.Request
if err := json.Unmarshal(dat, &client); err != nil { if err := json.Unmarshal(dat, &client); err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to parse body: %w", err) return http.StatusInternalServerError, fmt.Errorf("failed to parse body: %w", err)
} }
client.IP = clientIP
ec.clientService.AddClient(&client) ec.clientService.AddClient(&client)
ec.l.Infof("Added client %s", clientIP) ec.l.Infof("Added client")
return http.StatusAccepted, nil return http.StatusAccepted, nil
} }

View file

@ -0,0 +1,55 @@
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
}

View file

@ -10,6 +10,7 @@ import (
"git.faercol.me/faercol/http-boot-server/bootserver/config" "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/logger"
"git.faercol.me/faercol/http-boot-server/bootserver/server" "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" "git.faercol.me/faercol/http-boot-server/bootserver/udplistener"
) )
@ -42,6 +43,9 @@ func main() {
logger.Init(conf.LogLevel) logger.Init(conf.LogLevel)
logger.L.Infof("Initialized logger with level %v", 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") logger.L.Info("Initializing server")
s, err := server.New(conf, logger.L) s, err := server.New(conf, logger.L)
if err != nil { if err != nil {
@ -49,7 +53,7 @@ func main() {
} }
logger.L.Info("Initializing UDP listener") logger.L.Info("Initializing UDP listener")
listener, err := udplistener.New(conf.UDPIface, conf.UPDMcastGroup, conf.UDPPort, logger.L) listener, err := udplistener.New(conf, logger.L)
if err != nil { if err != nil {
logger.L.Fatalf("Failed to initialize UDP listener: %s", err.Error()) logger.L.Fatalf("Failed to initialize UDP listener: %s", err.Error())
} }

View file

@ -64,9 +64,8 @@ func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) {
default: default:
panic(fmt.Errorf("unexpected listening mode %v", appConf.ServerMode)) panic(fmt.Errorf("unexpected listening mode %v", appConf.ServerMode))
} }
service := services.NewClientHandlerService() service := services.NewClientHandlerService(appConf.DataFilepath, logger)
controllers := map[string]http.Handler{ controllers := map[string]http.Handler{
// client.BootRoute: middlewares.WithLogger(client.NewBootController(logger, service), logger),
client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service), logger), client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service), logger),
} }

View file

@ -1,32 +1,112 @@
package services package services
import ( import (
"encoding/json"
"errors" "errors"
"fmt"
"os"
"time"
"git.faercol.me/faercol/http-boot-server/bootserver/bootoption" "git.faercol.me/faercol/http-boot-server/bootserver/bootoption"
"git.faercol.me/faercol/http-boot-server/bootserver/filelock"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sirupsen/logrus"
) )
var ErrUnknownClient = errors.New("unknown client") var ErrUnknownClient = errors.New("unknown client")
var ErrUnselectedBootOption = errors.New("unselected boot option") var ErrUnselectedBootOption = errors.New("unselected boot option")
var ErrUnknownBootOption = errors.New("unknown boot option") var ErrUnknownBootOption = errors.New("unknown boot option")
const defaultLockTimeout = 1 * time.Second
type ClientHandlerService struct { type ClientHandlerService struct {
clients map[uuid.UUID]*bootoption.Client filepath string
fileLock *filelock.FileLock
lockTimeout time.Duration
logger *logrus.Logger
} }
func NewClientHandlerService() *ClientHandlerService { func NewClientHandlerService(filepath string, logger *logrus.Logger) *ClientHandlerService {
return &ClientHandlerService{ return &ClientHandlerService{
clients: make(map[uuid.UUID]*bootoption.Client), filepath: filepath,
fileLock: filelock.New(filepath),
lockTimeout: defaultLockTimeout,
logger: logger,
} }
} }
func (chs *ClientHandlerService) AddClient(client *bootoption.Client) { func (chs *ClientHandlerService) Init() {
chs.clients[client.ID] = client 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) GetClientSelectedBootOption(client uuid.UUID) (*bootoption.EFIApp, error) { func (chs *ClientHandlerService) GetClientSelectedBootOption(client uuid.UUID) (*bootoption.EFIApp, error) {
clientDetails, ok := chs.clients[client] 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]
if !ok { if !ok {
return nil, ErrUnknownClient return nil, ErrUnknownClient
} }
@ -35,25 +115,39 @@ func (chs *ClientHandlerService) GetClientSelectedBootOption(client uuid.UUID) (
return nil, ErrUnselectedBootOption return nil, ErrUnselectedBootOption
} }
for _, o := range clientDetails.Options { if option, ok := clientDetails.Options[clientDetails.SelectedOption]; !ok {
if o.Name == clientDetails.SelectedOption {
return &o, nil
}
}
return nil, ErrUnknownBootOption return nil, ErrUnknownBootOption
} else {
return &option, nil
}
} }
func (chs *ClientHandlerService) SetClientBootOption(client uuid.UUID, option string) error { func (chs *ClientHandlerService) SetClientBootOption(client uuid.UUID, option string) error {
clientDetails, ok := chs.clients[client] var err error
if !ok {
return ErrUnknownClient clients, err := chs.load()
if err != nil {
return fmt.Errorf("failed to load current config: %w", err)
} }
for _, o := range clientDetails.Options { clientDetails, ok := clients[client]
if o.Name == option { if !ok {
err = ErrUnknownClient
} else {
if _, ok := clientDetails.Options[option]; !ok {
err = ErrUnknownBootOption
} else {
clientDetails.SelectedOption = 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 nil
}
}
return ErrUnknownBootOption
} }

View file

@ -3,10 +3,12 @@ package udplistener
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"git.faercol.me/faercol/http-boot-server/bootserver/bootprotocol" "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" "git.faercol.me/faercol/http-boot-server/bootserver/services"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -28,21 +30,22 @@ type UDPListener struct {
cancel context.CancelFunc cancel context.CancelFunc
} }
func New(ifaceName, multicastGroup string, port int, log *logrus.Logger) (*UDPListener, error) { func New(conf *config.AppConfig, log *logrus.Logger) (*UDPListener, error) {
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("[%s]:%d", multicastGroup, port)) addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("[%s]:%d", conf.UPDMcastGroup, conf.UDPPort))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to resolve UDP address: %w", err) return nil, fmt.Errorf("failed to resolve UDP address: %w", err)
} }
iface, err := net.InterfaceByName(ifaceName) iface, err := net.InterfaceByName(conf.UDPIface)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to resolve interface name %s: %w", ifaceName, err) return nil, fmt.Errorf("failed to resolve interface name %s: %w", conf.UDPIface, err)
} }
return &UDPListener{ return &UDPListener{
addr: addr, addr: addr,
iface: iface, iface: iface,
ctx: context.TODO(), ctx: context.TODO(),
service: services.NewClientHandlerService(conf.DataFilepath, log),
log: log, log: log,
}, nil }, nil
} }
@ -57,6 +60,56 @@ func (l *UDPListener) Init() error {
return nil 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) { func (l *UDPListener) listen() (*udpMessage, error) {
buffer := make([]byte, bufferLength) buffer := make([]byte, bufferLength)
n, source, err := l.l.ReadFromUDP(buffer) n, source, err := l.l.ReadFromUDP(buffer)
@ -101,7 +154,9 @@ func (l *UDPListener) mainLoop() {
case err := <-errChan: case err := <-errChan:
l.log.Error(err) l.log.Error(err)
case msg := <-msgChan: case msg := <-msgChan:
l.log.Infof("Request from %s: %q", msg.sourceAddr.String(), msg.message.String()) if err := l.handleClient(msg); err != nil {
l.log.Errorf("Failed to handle message from client: %q", err.Error())
}
} }
} }