Add ipadm: Go-Tool zur Verwaltung statischer DHCP/DNS-Hosts

This commit is contained in:
2026-08-18 11:17:16 +02:00
parent dc1d490e26
commit 49bad1fec1
8 changed files with 818 additions and 0 deletions
+143
View File
@@ -0,0 +1,143 @@
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
}