Splits the LAN into four /24 zones: 10.0.0.0/24 static (no DHCP, DNS only), 10.0.1.0/24 fixed DHCP reservations by MAC, 10.0.2.0/24 dynamic DHCP pool, 10.0.3.0/24 spare/unused. ipadm now derives the required subnet from whether a host has a MAC, auto-assigns free IPs, and auto-migrates a host's IP when its MAC is added/removed. Migrated the existing archerc80 reservation from 10.0.0.2 to 10.0.1.2. Also fixes a latent bug found while doing this: dnsmasq's SIGHUP (`systemctl reload`) only re-reads /etc/hosts, not the conf-dir files ipadm writes to, so config changes were silently not applied on reload. ipadm now restarts dnsmasq instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
110 lines
2.6 KiB
Go
110 lines
2.6 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
const hostHeader = "# ipadm host database - managed with `ipadm`, do not edit while ipadm is running\n" +
|
|
"# name\tip\tmac\tcomment\n"
|
|
|
|
// One line per host, tab-separated: name\tip\tmac\tcomment
|
|
// MAC is stored as "-" when empty.
|
|
func parseHostLine(line string) (Host, error) {
|
|
parts := strings.SplitN(line, "\t", 4)
|
|
if len(parts) < 3 {
|
|
return Host{}, fmt.Errorf("invalid line (expected at least 3 tab-separated fields): %q", line)
|
|
}
|
|
mac := parts[2]
|
|
if mac == "-" {
|
|
mac = ""
|
|
}
|
|
comment := ""
|
|
if len(parts) == 4 {
|
|
comment = parts[3]
|
|
}
|
|
return Host{Name: parts[0], IP: parts[1], MAC: mac, Comment: comment}, nil
|
|
}
|
|
|
|
func formatHostLine(h Host) string {
|
|
mac := h.MAC
|
|
if mac == "" {
|
|
mac = "-"
|
|
}
|
|
return fmt.Sprintf("%s\t%s\t%s\t%s", h.Name, h.IP, mac, h.Comment)
|
|
}
|
|
|
|
func openHostStore(path string) (*LineStore[Host], error) {
|
|
return openLineStore(path, hostHeader, parseHostLine, formatHostLine)
|
|
}
|
|
|
|
func sortHosts(hosts []Host) {
|
|
sort.Slice(hosts, func(i, j int) bool {
|
|
return strings.ToLower(hosts[i].Name) < strings.ToLower(hosts[j].Name)
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// classifyHostArgs sorts the optional trailing arguments of `ipadm -a` (ip
|
|
// address and/or mac address, in any order, either of which may be omitted)
|
|
// into their fields. Returns an error on unrecognized or duplicate tokens.
|
|
func classifyHostArgs(args []string) (ip, mac string, err error) {
|
|
for _, a := range args {
|
|
if _, macErr := net.ParseMAC(a); macErr == nil {
|
|
if mac != "" {
|
|
return "", "", fmt.Errorf("MAC address given more than once: %q", a)
|
|
}
|
|
mac = a
|
|
continue
|
|
}
|
|
if parsed := net.ParseIP(a); parsed != nil && parsed.To4() != nil {
|
|
if ip != "" {
|
|
return "", "", fmt.Errorf("IP address given more than once: %q", a)
|
|
}
|
|
ip = a
|
|
continue
|
|
}
|
|
return "", "", fmt.Errorf("unrecognized argument %q (expected IP address or MAC address)", a)
|
|
}
|
|
return ip, mac, nil
|
|
}
|