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>
103 lines
2.4 KiB
Go
103 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
var stdinReader = bufio.NewReader(os.Stdin)
|
|
|
|
// prompt shows label with a "[current]" hint, reads one line, and returns
|
|
// current unchanged if the user just presses enter.
|
|
func prompt(label, current string) string {
|
|
if current != "" {
|
|
fmt.Printf("%s [%s]: ", label, current)
|
|
} else {
|
|
fmt.Printf("%s: ", label)
|
|
}
|
|
line, _ := stdinReader.ReadString('\n')
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
return current
|
|
}
|
|
return line
|
|
}
|
|
|
|
// interactiveEdit implements the bare `ipadm <hostname>` form: add the host
|
|
// if it doesn't exist yet, otherwise edit it in place. Returns the updated
|
|
// host list, or an error if validation fails.
|
|
func interactiveEdit(hosts []Host, name string, cfg netConfig) ([]Host, error) {
|
|
if err := validateHostname(name); err != nil {
|
|
return nil, err
|
|
}
|
|
idx := findHost(hosts, name)
|
|
|
|
var cur Host
|
|
if idx >= 0 {
|
|
cur = hosts[idx]
|
|
fmt.Printf("Editing existing host %q (press enter to keep a value)\n", name)
|
|
} else {
|
|
cur = Host{Name: name}
|
|
fmt.Printf("New host %q (press enter to accept defaults)\n", name)
|
|
}
|
|
|
|
// MAC decides which subnet the host belongs in (see netConfig), so it's
|
|
// asked first and used to suggest a matching free IP below.
|
|
for {
|
|
mac := prompt("MAC address (optional, '-' to clear)", macOrDash(cur.MAC))
|
|
if mac == "-" {
|
|
mac = ""
|
|
}
|
|
norm, err := normalizeMAC(mac)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "error:", err)
|
|
continue
|
|
}
|
|
cur.MAC = norm
|
|
break
|
|
}
|
|
|
|
cidr := cfg.cidrFor(cur.MAC)
|
|
suggested := cur.IP
|
|
if suggested == "" || validateIP(suggested, cidr, cfg.gatewayIP) != nil {
|
|
if freeIP, err := nextFreeIP(hosts, cidr, idx, cfg.gatewayIP); err == nil {
|
|
suggested = freeIP
|
|
}
|
|
}
|
|
|
|
for {
|
|
ip := prompt(fmt.Sprintf("IP address (%s)", cidr), suggested)
|
|
if err := validateIP(ip, cidr, cfg.gatewayIP); err != nil {
|
|
fmt.Fprintln(os.Stderr, "error:", err)
|
|
continue
|
|
}
|
|
cur.IP = ip
|
|
break
|
|
}
|
|
|
|
cur.Comment = prompt("Comment", cur.Comment)
|
|
|
|
if other := findByIP(hosts, cur.IP, idx); other >= 0 {
|
|
return nil, fmt.Errorf("IP %s is already assigned to host %q", cur.IP, hosts[other].Name)
|
|
}
|
|
if other := findByMAC(hosts, cur.MAC, idx); other >= 0 {
|
|
return nil, fmt.Errorf("MAC %s is already assigned to host %q", cur.MAC, hosts[other].Name)
|
|
}
|
|
|
|
if idx >= 0 {
|
|
hosts[idx] = cur
|
|
} else {
|
|
hosts = append(hosts, cur)
|
|
}
|
|
return hosts, nil
|
|
}
|
|
|
|
func macOrDash(mac string) string {
|
|
if mac == "" {
|
|
return ""
|
|
}
|
|
return mac
|
|
}
|