phase-b: add node uuid/alias/ip metadata APIs and node list enrichment
This commit is contained in:
59
internal/server/node_api.go
Normal file
59
internal/server/node_api.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *Server) HandleNodeMeta(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, `{"error":1,"message":"method not allowed"}`)
|
||||
return
|
||||
}
|
||||
if s.store == nil {
|
||||
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"store not ready"}`)
|
||||
return
|
||||
}
|
||||
ac, ok := s.ResolveTenantAccessToken(BearerToken(r))
|
||||
if !ok || ac.TenantID <= 0 {
|
||||
writeJSON(w, http.StatusUnauthorized, `{"error":1,"message":"unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(r.URL.Path, "/alias") {
|
||||
var req struct {
|
||||
NodeUUID string `json:"node_uuid"`
|
||||
Alias string `json:"alias"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.NodeUUID == "" {
|
||||
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"bad request"}`)
|
||||
return
|
||||
}
|
||||
if err := s.store.SetNodeAlias(ac.TenantID, req.NodeUUID, req.Alias); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"`+err.Error()+`"}`)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(r.URL.Path, "/ip") {
|
||||
var req struct {
|
||||
NodeUUID string `json:"node_uuid"`
|
||||
VirtualIP string `json:"virtual_ip"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.NodeUUID == "" || req.VirtualIP == "" {
|
||||
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"bad request"}`)
|
||||
return
|
||||
}
|
||||
if err := s.store.SetNodeVirtualIP(ac.TenantID, req.NodeUUID, req.VirtualIP); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"`+err.Error()+`"}`)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusNotFound, `{"error":1,"message":"not found"}`)
|
||||
}
|
||||
@@ -294,10 +294,14 @@ func (s *Server) HandleEnrollConsume(w http.ResponseWriter, r *http.Request) {
|
||||
Error int `json:"error"`
|
||||
Message string `json:"message"`
|
||||
NodeID int64 `json:"node_id"`
|
||||
NodeUUID string `json:"node_uuid"`
|
||||
NodeName string `json:"node_name"`
|
||||
Alias string `json:"alias"`
|
||||
VirtualIP string `json:"virtual_ip"`
|
||||
Secret string `json:"node_secret"`
|
||||
Tenant int64 `json:"tenant_id"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}{0, "ok", cred.NodeID, cred.Secret, cred.TenantID, cred.CreatedAt}
|
||||
}{0, "ok", cred.NodeID, cred.NodeUUID, cred.NodeName, cred.Alias, cred.VirtualIP, cred.Secret, cred.TenantID, cred.CreatedAt}
|
||||
b, _ := json.Marshal(resp)
|
||||
writeJSON(w, http.StatusOK, string(b))
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -49,7 +50,9 @@ type APIKey struct {
|
||||
|
||||
type NodeCredential struct {
|
||||
NodeID int64
|
||||
NodeUUID string
|
||||
NodeName string
|
||||
Alias string
|
||||
Secret string
|
||||
VirtualIP string
|
||||
TenantID int64
|
||||
@@ -134,7 +137,9 @@ func (s *Store) migrate() error {
|
||||
`CREATE TABLE IF NOT EXISTS nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
node_uuid TEXT,
|
||||
node_name TEXT NOT NULL,
|
||||
alias TEXT DEFAULT '',
|
||||
node_pubkey TEXT,
|
||||
node_secret_hash TEXT,
|
||||
virtual_ip TEXT,
|
||||
@@ -202,6 +207,15 @@ func (s *Store) migrate() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := s.DB.Exec(`ALTER TABLE nodes ADD COLUMN node_uuid TEXT`); err != nil && !strings.Contains(err.Error(), "duplicate column name") {
|
||||
return err
|
||||
}
|
||||
if _, err := s.DB.Exec(`ALTER TABLE nodes ADD COLUMN alias TEXT DEFAULT ''`); err != nil && !strings.Contains(err.Error(), "duplicate column name") {
|
||||
return err
|
||||
}
|
||||
if _, err := s.DB.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_nodes_node_uuid ON nodes(node_uuid)`); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -279,16 +293,59 @@ func (s *Store) CreateTenantWithUsers(name, adminPassword, operatorPassword stri
|
||||
return ten, admin, op, nil
|
||||
}
|
||||
|
||||
func (s *Store) AllocateNodeIP(tenantID int64) (string, error) {
|
||||
ten, err := s.GetTenantByID(tenantID)
|
||||
if err != nil || ten == nil {
|
||||
return "", errors.New("tenant not found")
|
||||
}
|
||||
ip, ipnet, err := net.ParseCIDR(ten.Subnet)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
base := ip.To4()
|
||||
if base == nil {
|
||||
return "", errors.New("only ipv4 subnet supported")
|
||||
}
|
||||
rows, err := s.DB.Query(`SELECT virtual_ip FROM nodes WHERE tenant_id=? AND virtual_ip IS NOT NULL AND virtual_ip<>''`, tenantID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
used := map[string]bool{}
|
||||
for rows.Next() {
|
||||
var v string
|
||||
if rows.Scan(&v) == nil && v != "" {
|
||||
used[v] = true
|
||||
}
|
||||
}
|
||||
for host := 2; host <= 254; host++ {
|
||||
cand := net.IPv4(base[0], base[1], base[2], byte(host)).String()
|
||||
if !ipnet.Contains(net.ParseIP(cand)) {
|
||||
continue
|
||||
}
|
||||
if used[cand] {
|
||||
continue
|
||||
}
|
||||
return cand, nil
|
||||
}
|
||||
return "", errors.New("no available ip")
|
||||
}
|
||||
|
||||
func (s *Store) CreateNodeCredential(tenantID int64, nodeName string) (*NodeCredential, error) {
|
||||
secret := randToken()
|
||||
h := hashTokenString(secret)
|
||||
now := time.Now().Unix()
|
||||
res, err := s.DB.Exec(`INSERT INTO nodes(tenant_id,node_name,node_secret_hash,status,last_seen,created_at) VALUES(?,?,?,1,?,?)`, tenantID, nodeName, h, now, now)
|
||||
nodeUUID := randUUID()
|
||||
vip, err := s.AllocateNodeIP(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := s.DB.Exec(`INSERT INTO nodes(tenant_id,node_uuid,node_name,alias,node_secret_hash,virtual_ip,status,last_seen,created_at) VALUES(?,?,?,?,?,?,1,?,?)`, tenantID, nodeUUID, nodeName, "", h, vip, now, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
return &NodeCredential{NodeID: id, NodeName: nodeName, Secret: secret, TenantID: tenantID, Status: 1, CreatedAt: now, LastSeen: &now}, nil
|
||||
return &NodeCredential{NodeID: id, NodeUUID: nodeUUID, NodeName: nodeName, Alias: "", Secret: secret, VirtualIP: vip, TenantID: tenantID, Status: 1, CreatedAt: now, LastSeen: &now}, nil
|
||||
}
|
||||
|
||||
func (s *Store) VerifyNodeSecret(nodeName, secret string) (*Tenant, error) {
|
||||
@@ -301,6 +358,66 @@ func (s *Store) VerifyNodeSecret(nodeName, secret string) (*Tenant, error) {
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetNodeCredentialByName(tenantID int64, nodeName string) (*NodeCredential, error) {
|
||||
row := s.DB.QueryRow(`SELECT id,node_uuid,node_name,alias,virtual_ip,tenant_id,status,last_seen,created_at FROM nodes WHERE tenant_id=? AND node_name=? ORDER BY id DESC LIMIT 1`, tenantID, nodeName)
|
||||
var n NodeCredential
|
||||
var seen sql.NullInt64
|
||||
if err := row.Scan(&n.NodeID, &n.NodeUUID, &n.NodeName, &n.Alias, &n.VirtualIP, &n.TenantID, &n.Status, &seen, &n.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if seen.Valid {
|
||||
v := seen.Int64
|
||||
n.LastSeen = &v
|
||||
}
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
func (s *Store) SetNodeAlias(tenantID int64, nodeUUID, alias string) error {
|
||||
alias = strings.TrimSpace(alias)
|
||||
res, err := s.DB.Exec(`UPDATE nodes SET alias=? WHERE tenant_id=? AND node_uuid=?`, alias, tenantID, nodeUUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return errors.New("node not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) SetNodeVirtualIP(tenantID int64, nodeUUID, ip string) error {
|
||||
ten, err := s.GetTenantByID(tenantID)
|
||||
if err != nil || ten == nil {
|
||||
return errors.New("tenant not found")
|
||||
}
|
||||
if net.ParseIP(ip) == nil {
|
||||
return errors.New("invalid ip")
|
||||
}
|
||||
_, ipnet, err := net.ParseCIDR(ten.Subnet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ipnet.Contains(net.ParseIP(ip)) {
|
||||
return errors.New("ip not in subnet")
|
||||
}
|
||||
var c int
|
||||
if err := s.DB.QueryRow(`SELECT COUNT(1) FROM nodes WHERE tenant_id=? AND virtual_ip=? AND node_uuid<>?`, tenantID, ip, nodeUUID).Scan(&c); err != nil {
|
||||
return err
|
||||
}
|
||||
if c > 0 {
|
||||
return errors.New("ip conflict")
|
||||
}
|
||||
res, err := s.DB.Exec(`UPDATE nodes SET virtual_ip=? WHERE tenant_id=? AND node_uuid=?`, ip, tenantID, nodeUUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return errors.New("node not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) GetTenantByToken(token uint64) (*Tenant, error) {
|
||||
h := hashToken(token)
|
||||
row := s.DB.QueryRow(`SELECT t.id,t.name,t.status,t.subnet,t.created_at FROM api_keys k JOIN tenants t ON k.tenant_id=t.id WHERE k.key_hash=? AND k.status=1 AND t.status=1`, h)
|
||||
@@ -636,5 +753,19 @@ func randToken() string {
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func randUUID() string {
|
||||
b := make([]byte, 16)
|
||||
_, _ = rand.Read(b)
|
||||
b[6] = (b[6] & 0x0f) | 0x40
|
||||
b[8] = (b[8] & 0x3f) | 0x80
|
||||
return fmt.Sprintf("%s-%s-%s-%s-%s",
|
||||
hex.EncodeToString(b[0:4]),
|
||||
hex.EncodeToString(b[4:6]),
|
||||
hex.EncodeToString(b[6:8]),
|
||||
hex.EncodeToString(b[8:10]),
|
||||
hex.EncodeToString(b[10:16]),
|
||||
)
|
||||
}
|
||||
|
||||
// helper to avoid unused import (net)
|
||||
var _ = net.IPv4len
|
||||
|
||||
Reference in New Issue
Block a user