package main import ( "bufio" "fmt" "os" "sort" "strings" "syscall" ) // Host is one static host entry: hostname, IP, optional MAC, free-text comment. type Host struct { Name string IP string MAC string // "" if not set Comment string } // Store gives locked read-modify-write access to the flat-file host database. // One line per host, tab-separated: name\tip\tmac\tcomment // MAC is stored as "-" when empty. Lines starting with '#' and blank lines are ignored. type Store struct { path string file *os.File } func openStore(path string) (*Store, error) { if err := os.MkdirAll(dirOf(path), 0755); err != nil { return nil, fmt.Errorf("kann Verzeichnis für %s nicht anlegen: %w", path, err) } f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644) if err != nil { return nil, fmt.Errorf("kann %s nicht öffnen: %w", path, err) } if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { f.Close() return nil, fmt.Errorf("kann Lock auf %s nicht setzen: %w", path, err) } return &Store{path: path, file: f}, nil } func (s *Store) Close() error { syscall.Flock(int(s.file.Fd()), syscall.LOCK_UN) return s.file.Close() } func (s *Store) Load() ([]Host, error) { if _, err := s.file.Seek(0, 0); err != nil { return nil, err } var hosts []Host sc := bufio.NewScanner(s.file) lineNo := 0 for sc.Scan() { lineNo++ line := strings.TrimRight(sc.Text(), "\r\n") if line == "" || strings.HasPrefix(line, "#") { continue } parts := strings.SplitN(line, "\t", 4) if len(parts) < 3 { return nil, fmt.Errorf("%s:%d: ungültige Zeile (erwarte mind. 3 Tab-getrennte Felder): %q", s.path, lineNo, line) } mac := parts[2] if mac == "-" { mac = "" } comment := "" if len(parts) == 4 { comment = parts[3] } hosts = append(hosts, Host{Name: parts[0], IP: parts[1], MAC: mac, Comment: comment}) } if err := sc.Err(); err != nil { return nil, err } return hosts, nil } func (s *Store) Save(hosts []Host) error { sort.Slice(hosts, func(i, j int) bool { return strings.ToLower(hosts[i].Name) < strings.ToLower(hosts[j].Name) }) var b strings.Builder b.WriteString("# ipadm host database - managed with `ipadm`, do not edit while ipadm is running\n") b.WriteString("# name\tip\tmac\tcomment\n") for _, h := range hosts { mac := h.MAC if mac == "" { mac = "-" } fmt.Fprintf(&b, "%s\t%s\t%s\t%s\n", h.Name, h.IP, mac, h.Comment) } if err := s.file.Truncate(0); err != nil { return err } if _, err := s.file.Seek(0, 0); err != nil { return err } if _, err := s.file.WriteString(b.String()); err != nil { return err } return s.file.Sync() } func dirOf(path string) string { i := strings.LastIndexByte(path, '/') if i <= 0 { return "." } return path[:i] } func findHost(hosts []Host, name string) int { for i, h := range hosts { if strings.EqualFold(h.Name, name) { return i } } return -1 } func findByIP(hosts []Host, ip string, exceptIdx int) int { for i, h := range hosts { if i != exceptIdx && h.IP == ip { return i } } return -1 } func findByMAC(hosts []Host, mac string, exceptIdx int) int { if mac == "" { return -1 } for i, h := range hosts { if i != exceptIdx && h.MAC != "" && strings.EqualFold(h.MAC, mac) { return i } } return -1 }