Add multi-server data model + legacy DB migration (step 0)

Adds Server.ID/Name, ServerInterface.Name, Client.ServerID, and a new
ServerSetting type as additive fields so existing single-server code
paths keep working unchanged. jsondb.Init() now detects a pre-existing
single-server db/server/ layout and mirrors it into new servers/,
server_settings/, server_hashes/ collections plus backfills ServerID
on existing clients, without touching/removing the legacy files yet.
This commit is contained in:
sysops
2026-07-11 22:35:45 +02:00
parent e5c0a5888f
commit 40d7862702
4 changed files with 142 additions and 0 deletions
+1
View File
@@ -7,6 +7,7 @@ import (
// Client model // Client model
type Client struct { type Client struct {
ID string `json:"id"` ID string `json:"id"`
ServerID string `json:"server_id,omitempty"`
PrivateKey string `json:"private_key"` PrivateKey string `json:"private_key"`
PublicKey string `json:"public_key"` PublicKey string `json:"public_key"`
PresharedKey string `json:"preshared_key"` PresharedKey string `json:"preshared_key"`
+3
View File
@@ -6,6 +6,8 @@ import (
// Server model // Server model
type Server struct { type Server struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
KeyPair *ServerKeypair KeyPair *ServerKeypair
Interface *ServerInterface Interface *ServerInterface
} }
@@ -19,6 +21,7 @@ type ServerKeypair struct {
// ServerInterface model // ServerInterface model
type ServerInterface struct { type ServerInterface struct {
Name string `json:"name,omitempty"` // OS interface name, e.g. wg0
Addresses []string `json:"addresses"` Addresses []string `json:"addresses"`
ListenPort int `json:"listen_port,string"` // ,string to get listen_port string input as int ListenPort int `json:"listen_port,string"` // ,string to get listen_port string input as int
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
+11
View File
@@ -15,3 +15,14 @@ type GlobalSetting struct {
ConfigFilePath string `json:"config_file_path"` ConfigFilePath string `json:"config_file_path"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
// ServerSetting model holds the per-server-interface subset of settings
// that used to live in the single global GlobalSetting. Introduced for the
// multi-server extension; not yet wired into store/handler layers.
type ServerSetting struct {
EndpointAddress string `json:"endpoint_address"`
FirewallMark string `json:"firewall_mark"`
Table string `json:"table"`
ConfigFilePath string `json:"config_file_path"`
UpdatedAt time.Time `json:"updated_at"`
}
+127
View File
@@ -4,9 +4,11 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"os" "os"
"path" "path"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/sdomino/scribble" "github.com/sdomino/scribble"
@@ -17,6 +19,10 @@ import (
"github.com/ngoduykhanh/wireguard-ui/util" "github.com/ngoduykhanh/wireguard-ui/util"
) )
// legacyDefaultServerID is the synthetic ID assigned to a pre-existing
// single-server installation when it is migrated to the multi-server layout.
const legacyDefaultServerID = "wg0"
type JsonDB struct { type JsonDB struct {
conn *scribble.Driver conn *scribble.Driver
dbPath string dbPath string
@@ -161,6 +167,11 @@ func (o *JsonDB) Init() error {
} }
} }
// migrate legacy single-server layout to the new multi-server layout
if err := o.migrateLegacyServer(); err != nil {
return err
}
// init cache // init cache
for _, i := range results { for _, i := range results {
user := model.User{} user := model.User{}
@@ -186,6 +197,122 @@ func (o *JsonDB) Init() error {
return nil return nil
} }
// migrateLegacyServer detects a pre-multi-server DB layout (single
// "server" collection, no "servers" registry yet) and migrates it into
// the new per-server layout ("servers/<id>.json", "server_settings/<id>.json",
// "server_hashes/<id>.json"), backfilling ServerID on every existing client.
// It is idempotent: it only runs once, guarded by the absence of the
// "servers" directory, and never deletes or modifies the legacy "server"
// collection - existing store methods keep reading/writing it directly
// until they are switched over to the new layout in a later step.
func (o *JsonDB) migrateLegacyServer() error {
legacyServerPath := path.Join(o.dbPath, "server")
legacyInterfacePath := path.Join(legacyServerPath, "interfaces.json")
newServersPath := path.Join(o.dbPath, "servers")
if _, err := os.Stat(legacyInterfacePath); os.IsNotExist(err) {
// nothing to migrate (fresh install)
return nil
}
if _, err := os.Stat(newServersPath); err == nil {
// already migrated
return nil
}
var legacyInterface model.ServerInterface
if err := o.conn.Read("server", "interfaces", &legacyInterface); err != nil {
return fmt.Errorf("migration: cannot read legacy server interface: %v", err)
}
var legacyKeyPair model.ServerKeypair
if err := o.conn.Read("server", "keypair", &legacyKeyPair); err != nil {
return fmt.Errorf("migration: cannot read legacy server keypair: %v", err)
}
var legacyGlobalSettings model.GlobalSetting
if err := o.conn.Read("server", "global_settings", &legacyGlobalSettings); err != nil {
return fmt.Errorf("migration: cannot read legacy global settings: %v", err)
}
var legacyHashes model.ClientServerHashes
hasHashes := true
if err := o.conn.Read("server", "hashes", &legacyHashes); err != nil {
hasHashes = false
}
serverID := legacyDefaultServerID
ifaceName := legacyDefaultServerID
if legacyGlobalSettings.ConfigFilePath != "" {
base := path.Base(legacyGlobalSettings.ConfigFilePath)
if strings.HasSuffix(base, ".conf") {
ifaceName = strings.TrimSuffix(base, ".conf")
}
}
legacyInterface.Name = ifaceName
server := model.Server{
ID: serverID,
Name: "Default Server",
KeyPair: &legacyKeyPair,
Interface: &legacyInterface,
}
if err := o.conn.Write("servers", serverID, server); err != nil {
return fmt.Errorf("migration: cannot write servers/%s.json: %v", serverID, err)
}
if err := util.ManagePerms(path.Join(newServersPath, serverID+".json")); err != nil {
return err
}
serverSetting := model.ServerSetting{
EndpointAddress: legacyGlobalSettings.EndpointAddress,
FirewallMark: legacyGlobalSettings.FirewallMark,
Table: legacyGlobalSettings.Table,
ConfigFilePath: legacyGlobalSettings.ConfigFilePath,
UpdatedAt: time.Now().UTC(),
}
if err := o.conn.Write("server_settings", serverID, serverSetting); err != nil {
return fmt.Errorf("migration: cannot write server_settings/%s.json: %v", serverID, err)
}
if err := util.ManagePerms(path.Join(o.dbPath, "server_settings", serverID+".json")); err != nil {
return err
}
if hasHashes {
if err := o.conn.Write("server_hashes", serverID, legacyHashes); err != nil {
return fmt.Errorf("migration: cannot write server_hashes/%s.json: %v", serverID, err)
}
if err := util.ManagePerms(path.Join(o.dbPath, "server_hashes", serverID+".json")); err != nil {
return err
}
}
// backfill ServerID on every existing client
clientRecords, err := o.conn.ReadAll("clients")
if err != nil && err != scribble.ErrMissingCollection {
return fmt.Errorf("migration: cannot read clients: %v", err)
}
for _, rec := range clientRecords {
var client model.Client
if err := json.Unmarshal(rec, &client); err != nil {
return fmt.Errorf("migration: cannot decode client json: %v", err)
}
if client.ServerID != "" {
continue
}
client.ServerID = serverID
if err := o.conn.Write("clients", client.ID, client); err != nil {
return fmt.Errorf("migration: cannot backfill server_id on client %s: %v", client.ID, err)
}
}
// NOTE: the legacy "server" directory is intentionally left in place
// (not renamed/removed) at this stage. Existing store methods (GetServer,
// GetGlobalSettings, SaveServerInterface, ...) still read/write it
// directly until they are switched over to the new per-server layout;
// removing it now would break the running app. The rename-to-backup
// step happens once those methods are migrated.
log.Printf("Migrated legacy single-server DB to multi-server layout (server id: %s); legacy db/server/ kept in place for now", serverID)
return nil
}
// GetUsers func to get all users from the database // GetUsers func to get all users from the database
func (o *JsonDB) GetUsers() ([]model.User, error) { func (o *JsonDB) GetUsers() ([]model.User, error) {
var users []model.User var users []model.User