189 lines
5.2 KiB
Go
189 lines
5.2 KiB
Go
package watchdog
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
dhcpapi "git.g3e.fr/syonad/two/internal/api/dhcp"
|
|
"git.g3e.fr/syonad/two/internal/dhcp"
|
|
"git.g3e.fr/syonad/two/internal/dhcpbackend"
|
|
"git.g3e.fr/syonad/two/internal/state"
|
|
"git.g3e.fr/syonad/two/internal/watchdog/notify"
|
|
"git.g3e.fr/syonad/two/pkg/db/kv"
|
|
|
|
"github.com/dgraph-io/badger/v4"
|
|
)
|
|
|
|
type configFileReporter interface {
|
|
ConfigPath(s dhcpbackend.Subnet) string
|
|
}
|
|
|
|
type stateReporter interface {
|
|
State(s dhcpbackend.Subnet) (dhcpapi.State, string, error)
|
|
}
|
|
|
|
func expectedHosts(db *badger.DB, subnetName string) ([]dhcpapi.Host, error) {
|
|
pairs, err := kv.ListByPrefix(db, prefixVM)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing vms: %w", err)
|
|
}
|
|
|
|
hosts := make([]dhcpapi.Host, 0)
|
|
for _, vmName := range resourceNames(pairs, prefixVM) {
|
|
st, err := state.Get(db, prefixVM+vmName)
|
|
if err != nil || st != state.Running {
|
|
continue
|
|
}
|
|
|
|
vmHosts, err := expectedVMHosts(db, vmName, subnetName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hosts = append(hosts, vmHosts...)
|
|
}
|
|
|
|
dhcpapi.SortHosts(hosts)
|
|
return hosts, nil
|
|
}
|
|
|
|
func expectedVMHosts(db *badger.DB, vmName, subnetName string) ([]dhcpapi.Host, error) {
|
|
prefix := prefixVM + vmName + "/nic/"
|
|
entries, err := kv.ListByPrefix(db, prefix)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing nics of vm %s: %w", vmName, err)
|
|
}
|
|
|
|
indexes := make([]int, 0)
|
|
for key := range entries {
|
|
parts := strings.Split(strings.TrimPrefix(key, prefix), "/")
|
|
if len(parts) != 2 || parts[1] != "subnet" {
|
|
continue
|
|
}
|
|
idx, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid nic index %q for vm %s", parts[0], vmName)
|
|
}
|
|
indexes = append(indexes, idx)
|
|
}
|
|
sort.Ints(indexes)
|
|
|
|
hosts := make([]dhcpapi.Host, 0, len(indexes))
|
|
for _, idx := range indexes {
|
|
nic := fmt.Sprintf("%s%d/", prefix, idx)
|
|
if entries[nic+"subnet"] != subnetName {
|
|
continue
|
|
}
|
|
|
|
ip := entries[nic+"ip"]
|
|
if ip == "" {
|
|
return nil, fmt.Errorf("nic %d of vm %s has no ip", idx, vmName)
|
|
}
|
|
mac, err := dhcp.GetMACForIP(db, subnetName, ip)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get mac for ip %s: %w", ip, err)
|
|
}
|
|
|
|
hosts = append(hosts, dhcpapi.Host{
|
|
MAC: mac,
|
|
IP: ip,
|
|
VM: vmName,
|
|
DefaultRoute: entries[nic+"primary"] == "true",
|
|
})
|
|
}
|
|
return hosts, nil
|
|
}
|
|
|
|
func checkDHCP(db *badger.DB, name string, s dhcpbackend.Subnet, backend dhcpbackend.Backend, u unitChecker, n notify.Notifier) {
|
|
if backend == nil {
|
|
return
|
|
}
|
|
if r, ok := backend.(configFileReporter); ok {
|
|
checkDHCPConfigFile(name, r.ConfigPath(s), n)
|
|
}
|
|
if r, ok := backend.(stateReporter); ok {
|
|
checkDHCPState(db, name, s, r, n)
|
|
}
|
|
checkUnit(kindSubnet, name, backend.Unit(s), u, n)
|
|
}
|
|
|
|
func checkDHCPConfigFile(name, path string, n notify.Notifier) {
|
|
if _, err := os.Stat(path); err != nil {
|
|
n.Notify(kindSubnet, name, fmt.Sprintf("dnsmasq config missing (%s): %v", path, err))
|
|
}
|
|
}
|
|
|
|
func checkDHCPState(db *badger.DB, name string, s dhcpbackend.Subnet, r stateReporter, n notify.Notifier) {
|
|
served, _, err := r.State(s)
|
|
if err != nil {
|
|
n.Notify(kindSubnet, name, fmt.Sprintf("dhcp server unreachable: %v", err))
|
|
return
|
|
}
|
|
if served.Subnet == nil {
|
|
n.Notify(kindSubnet, name, "dhcp server has no subnet configuration: it serves nothing")
|
|
}
|
|
|
|
expected, err := expectedHosts(db, name)
|
|
if err != nil {
|
|
n.Notify(kindSubnet, name, fmt.Sprintf("expected dhcp reservations unreadable in database: %v", err))
|
|
return
|
|
}
|
|
|
|
expectedDigest, err := dhcpapi.Digest(dhcpapi.State{Hosts: expected})
|
|
if err != nil {
|
|
n.Notify(kindSubnet, name, fmt.Sprintf("expected dhcp reservations inconsistent in database: %v", err))
|
|
return
|
|
}
|
|
servedDigest, err := dhcpapi.Digest(dhcpapi.State{Hosts: served.Hosts})
|
|
if err != nil {
|
|
n.Notify(kindSubnet, name, fmt.Sprintf("dhcp reservations reported by the server are inconsistent: %v", err))
|
|
return
|
|
}
|
|
if expectedDigest == servedDigest {
|
|
return
|
|
}
|
|
|
|
for _, gap := range reservationGaps(expected, served.Hosts) {
|
|
n.Notify(kindSubnet, name, gap)
|
|
}
|
|
}
|
|
|
|
func reservationGaps(expected, served []dhcpapi.Host) []string {
|
|
index := func(hosts []dhcpapi.Host) map[string]dhcpapi.Host {
|
|
byMAC := make(map[string]dhcpapi.Host, len(hosts))
|
|
for _, h := range hosts {
|
|
if c, err := dhcpapi.CanonicalHost(h); err == nil {
|
|
byMAC[c.MAC] = c
|
|
} else {
|
|
byMAC[h.MAC] = h
|
|
}
|
|
}
|
|
return byMAC
|
|
}
|
|
|
|
want, got := index(expected), index(served)
|
|
|
|
gaps := make([]string, 0)
|
|
for mac, h := range want {
|
|
s, ok := got[mac]
|
|
if !ok {
|
|
gaps = append(gaps, fmt.Sprintf("dhcp reservation missing on the server: %s → %s (vm %s)", mac, h.IP, h.VM))
|
|
continue
|
|
}
|
|
if s.IP != h.IP {
|
|
gaps = append(gaps, fmt.Sprintf("dhcp reservation diverges for %s: server serves %s, database says %s (vm %s)", mac, s.IP, h.IP, h.VM))
|
|
}
|
|
if s.DefaultRoute != h.DefaultRoute {
|
|
gaps = append(gaps, fmt.Sprintf("dhcp default route diverges for %s: server says %t, database says %t (vm %s)", mac, s.DefaultRoute, h.DefaultRoute, h.VM))
|
|
}
|
|
}
|
|
for mac, h := range got {
|
|
if _, ok := want[mac]; !ok {
|
|
gaps = append(gaps, fmt.Sprintf("stale dhcp reservation on the server: %s → %s (vm %s)", mac, h.IP, h.VM))
|
|
}
|
|
}
|
|
sort.Strings(gaps)
|
|
return gaps
|
|
}
|