Add data persistence
This commit is contained in:
parent
0b6bbce7d5
commit
3950a59241
6 changed files with 176 additions and 34 deletions
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
54
bootserver/filelock/filelock.go
Normal file
54
bootserver/filelock/filelock.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,109 @@
|
||||||
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, nil, 0o644); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to init data file: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (chs *ClientHandlerService) unload(conf map[uuid.UUID]*bootoption.Client) error {
|
||||||
|
dat, err := json.Marshal(conf)
|
||||||
|
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 {
|
||||||
|
return nil, fmt.Errorf("failed to read data file: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(dat, &conf); err != nil {
|
||||||
|
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 +112,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
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue