package network import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "sync" "time" ) var defaultMetadataPath = "/etc/debtik/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() } // SetMAC associates a physical MAC address with an interface name. func (mm *MetadataManager) SetMAC(name, mac string) error { mm.mu.Lock() defer mm.mu.Unlock() mac = strings.ToLower(strings.TrimSpace(mac)) meta := mm.store.Interfaces[name] if meta.MAC == mac { return nil } meta.MAC = mac mm.store.Interfaces[name] = meta return mm.saveLocked() } // GetAllMACs returns a map of target interface name to its recorded MAC address. func (mm *MetadataManager) GetAllMACs() map[string]string { mm.mu.RLock() defer mm.mu.RUnlock() res := make(map[string]string) for name, meta := range mm.store.Interfaces { if meta.MAC != "" { res[name] = strings.ToLower(meta.MAC) } } return res } 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 }