| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- package network
- import (
- "encoding/json"
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "sync"
- "time"
- )
- var defaultMetadataPath = "/etc/proxmox-router/metadata.json"
- var defaultRoles = []RoleTag{
- {ID: "wan", Name: "WAN", Color: "amber", Description: "Public internet uplink interfaces", Builtin: true},
- {ID: "lan", Name: "LAN", Color: "blue", Description: "Local area network", Builtin: true},
- {ID: "dmz", Name: "DMZ", Color: "purple", Description: "Demilitarized zone / Public services", Builtin: true},
- {ID: "guest", Name: "GUEST", Color: "cyan", Description: "Isolated guest network", Builtin: true},
- {ID: "iot", Name: "IOT", Color: "emerald", Description: "Smart devices & sensors", Builtin: true},
- {ID: "mgmt", Name: "MGMT", Color: "rose", Description: "Management & OOB network", Builtin: true},
- }
- // MetadataManager manages user annotations for interfaces.
- type MetadataManager struct {
- mu sync.RWMutex
- filePath string
- store MetadataStore
- }
- func NewMetadataManager(customPath string) *MetadataManager {
- p := defaultMetadataPath
- if customPath != "" {
- p = customPath
- }
- mm := &MetadataManager{
- filePath: p,
- store: MetadataStore{
- Interfaces: make(map[string]InterfaceMetadata),
- },
- }
- mm.load()
- return mm
- }
- func (mm *MetadataManager) load() {
- mm.mu.Lock()
- defer mm.mu.Unlock()
- data, err := os.ReadFile(mm.filePath)
- if err == nil {
- _ = json.Unmarshal(data, &mm.store)
- }
- if mm.store.Interfaces == nil {
- mm.store.Interfaces = make(map[string]InterfaceMetadata)
- }
- if len(mm.store.Roles) == 0 {
- mm.store.Roles = make([]RoleTag, len(defaultRoles))
- copy(mm.store.Roles, defaultRoles)
- _ = mm.saveLocked()
- }
- }
- func (mm *MetadataManager) save() error {
- mm.mu.Lock()
- defer mm.mu.Unlock()
- return mm.saveLocked()
- }
- func (mm *MetadataManager) saveLocked() error {
- dir := filepath.Dir(mm.filePath)
- if err := os.MkdirAll(dir, 0755); err != nil {
- return err
- }
- data, err := json.MarshalIndent(mm.store, "", " ")
- if err != nil {
- return err
- }
- return os.WriteFile(mm.filePath, data, 0644)
- }
- // GetRolesWithUsage returns all role tags with assigned interface details.
- func (mm *MetadataManager) GetRolesWithUsage() []RoleTagWithUsage {
- mm.mu.RLock()
- defer mm.mu.RUnlock()
- roles := mm.store.Roles
- if len(roles) == 0 {
- roles = defaultRoles
- }
- result := make([]RoleTagWithUsage, len(roles))
- for i, r := range roles {
- ru := RoleTagWithUsage{
- RoleTag: r,
- Interfaces: make([]string, 0),
- }
- for ifaceName, meta := range mm.store.Interfaces {
- if strings.EqualFold(meta.Role, r.Name) || strings.EqualFold(meta.Role, r.ID) {
- ru.Interfaces = append(ru.Interfaces, ifaceName)
- }
- }
- ru.AssignedCount = len(ru.Interfaces)
- result[i] = ru
- }
- return result
- }
- // GetRoles returns all role tags.
- func (mm *MetadataManager) GetRoles() []RoleTag {
- mm.mu.RLock()
- defer mm.mu.RUnlock()
- if len(mm.store.Roles) == 0 {
- return defaultRoles
- }
- return append([]RoleTag(nil), mm.store.Roles...)
- }
- // GetRole returns a specific role tag by ID.
- func (mm *MetadataManager) GetRole(id string) (*RoleTag, error) {
- mm.mu.RLock()
- defer mm.mu.RUnlock()
- for _, r := range mm.store.Roles {
- if strings.EqualFold(r.ID, id) || strings.EqualFold(r.Name, id) {
- rCopy := r
- return &rCopy, nil
- }
- }
- return nil, fmt.Errorf("role tag '%s' not found", id)
- }
- // SaveRole creates or updates a role tag.
- func (mm *MetadataManager) SaveRole(role RoleTag) (*RoleTag, error) {
- mm.mu.Lock()
- defer mm.mu.Unlock()
- name := strings.TrimSpace(role.Name)
- if name == "" {
- return nil, fmt.Errorf("role name cannot be empty")
- }
- color := strings.ToLower(strings.TrimSpace(role.Color))
- if color == "" {
- color = "blue"
- }
- role.Color = color
- role.Name = name
- id := strings.TrimSpace(role.ID)
- if id == "" {
- slug := strings.ToLower(strings.ReplaceAll(name, " ", "-"))
- id = slug
- for _, existing := range mm.store.Roles {
- if existing.ID == id {
- id = fmt.Sprintf("%s-%d", slug, time.Now().Unix()%10000)
- break
- }
- }
- }
- role.ID = id
- found := false
- for i, existing := range mm.store.Roles {
- if strings.EqualFold(existing.ID, id) {
- oldName := existing.Name
- role.Builtin = existing.Builtin
- mm.store.Roles[i] = role
- found = true
- // If name changed, migrate assigned interfaces
- if oldName != "" && !strings.EqualFold(oldName, name) {
- for ifName, meta := range mm.store.Interfaces {
- if strings.EqualFold(meta.Role, oldName) {
- meta.Role = name
- mm.store.Interfaces[ifName] = meta
- }
- }
- }
- break
- }
- }
- if !found {
- mm.store.Roles = append(mm.store.Roles, role)
- }
- if err := mm.saveLocked(); err != nil {
- return nil, err
- }
- return &role, nil
- }
- // DeleteRole removes a role tag.
- func (mm *MetadataManager) DeleteRole(id string) error {
- mm.mu.Lock()
- defer mm.mu.Unlock()
- idx := -1
- var deletedRole RoleTag
- for i, r := range mm.store.Roles {
- if strings.EqualFold(r.ID, id) {
- idx = i
- deletedRole = r
- break
- }
- }
- if idx == -1 {
- return fmt.Errorf("role tag '%s' not found", id)
- }
- mm.store.Roles = append(mm.store.Roles[:idx], mm.store.Roles[idx+1:]...)
- // Reset any interfaces that used this role to "LAN" or empty
- for ifName, meta := range mm.store.Interfaces {
- if strings.EqualFold(meta.Role, deletedRole.Name) || strings.EqualFold(meta.Role, deletedRole.ID) {
- meta.Role = "LAN"
- mm.store.Interfaces[ifName] = meta
- }
- }
- return mm.saveLocked()
- }
- func (mm *MetadataManager) Get(name string) InterfaceMetadata {
- mm.mu.RLock()
- defer mm.mu.RUnlock()
- return mm.store.Interfaces[name]
- }
- func (mm *MetadataManager) Set(name string, role, desc string) error {
- mm.mu.Lock()
- defer mm.mu.Unlock()
- meta := mm.store.Interfaces[name]
- meta.Role = role
- meta.Description = desc
- mm.store.Interfaces[name] = meta
- return mm.saveLocked()
- }
- func (mm *MetadataManager) AddIP(name, cidr string) error {
- mm.mu.Lock()
- defer mm.mu.Unlock()
- meta := mm.store.Interfaces[name]
- for _, ip := range meta.IPAddresses {
- if ip == cidr {
- return nil
- }
- }
- meta.IPAddresses = append(meta.IPAddresses, cidr)
- meta.AutoUp = true
- mm.store.Interfaces[name] = meta
- return mm.saveLocked()
- }
- func (mm *MetadataManager) RemoveIP(name, cidr string) error {
- mm.mu.Lock()
- defer mm.mu.Unlock()
- meta := mm.store.Interfaces[name]
- var updated []string
- for _, ip := range meta.IPAddresses {
- if ip != cidr {
- updated = append(updated, ip)
- }
- }
- meta.IPAddresses = updated
- mm.store.Interfaces[name] = meta
- return mm.saveLocked()
- }
- func (mm *MetadataManager) GetAllWithIPs() map[string][]string {
- mm.mu.RLock()
- defer mm.mu.RUnlock()
- res := make(map[string][]string)
- for name, meta := range mm.store.Interfaces {
- if len(meta.IPAddresses) > 0 {
- res[name] = append([]string(nil), meta.IPAddresses...)
- }
- }
- return res
- }
- func (mm *MetadataManager) Delete(name string) error {
- mm.mu.Lock()
- defer mm.mu.Unlock()
- delete(mm.store.Interfaces, name)
- return mm.saveLocked()
- }
- // Rename migrates interface metadata when an interface is renamed.
- func (mm *MetadataManager) Rename(oldName, newName string) error {
- mm.mu.Lock()
- defer mm.mu.Unlock()
- if meta, exists := mm.store.Interfaces[oldName]; exists {
- delete(mm.store.Interfaces, oldName)
- mm.store.Interfaces[newName] = meta
- return mm.saveLocked()
- }
- return nil
- }
|