166 lines
4.1 KiB
Go
166 lines
4.1 KiB
Go
package agent
|
||
|
||
import (
|
||
"bufio"
|
||
"context"
|
||
"net"
|
||
"os"
|
||
"slices"
|
||
"strings"
|
||
"time"
|
||
|
||
"sultan/pkg/docker"
|
||
"sultan/pkg/registry"
|
||
)
|
||
|
||
// osReleasePath: Quelle des Betriebssystems ohne Docker (systemd-Agent).
|
||
var osReleasePath = "/etc/os-release"
|
||
|
||
// resolver ist der Teil von *net.Resolver, den detectFQDN braucht.
|
||
type resolver interface {
|
||
LookupCNAME(ctx context.Context, host string) (string, error)
|
||
LookupAddr(ctx context.Context, addr string) ([]string, error)
|
||
}
|
||
|
||
// detectFQDN ermittelt den vollständigen Namen des Knotens für Inventar und
|
||
// Störfall (Spec 5.3): explizit (--fqdn) > Hostname mit Punkt > Hostname
|
||
// über die Suchdomain aufgelöst > Reverse-Lookup der Backend-Adresse >
|
||
// kurzer Hostname. Jede Abfrage ist kurz begrenzt; scheitert alles, bleibt
|
||
// der Hostname.
|
||
func detectFQDN(ctx context.Context, res resolver, explicit, hostname, backend string) string {
|
||
if n := normalizeName(explicit); n != "" {
|
||
return n
|
||
}
|
||
short := normalizeName(hostname)
|
||
if strings.Contains(short, ".") {
|
||
return short
|
||
}
|
||
lookup := func(f func(context.Context) string) string {
|
||
lctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||
defer cancel()
|
||
return f(lctx)
|
||
}
|
||
if short != "" {
|
||
if n := lookup(func(c context.Context) string {
|
||
cname, err := res.LookupCNAME(c, short)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
return normalizeName(cname)
|
||
}); strings.Contains(n, ".") {
|
||
return n
|
||
}
|
||
}
|
||
if net.ParseIP(backend) != nil {
|
||
if n := lookup(func(c context.Context) string {
|
||
names, err := res.LookupAddr(c, backend)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
for _, name := range names {
|
||
if n := normalizeName(name); strings.Contains(n, ".") {
|
||
return n
|
||
}
|
||
}
|
||
return ""
|
||
}); n != "" {
|
||
return n
|
||
}
|
||
}
|
||
return short
|
||
}
|
||
|
||
func normalizeName(s string) string {
|
||
return strings.ToLower(strings.TrimSuffix(strings.TrimSpace(s), "."))
|
||
}
|
||
|
||
// cgnat ist 100.64.0.0/10 – der Bereich, aus dem NetBird Adressen vergibt.
|
||
var cgnat = &net.IPNet{IP: net.IPv4(100, 64, 0, 0).To4(), Mask: net.CIDRMask(10, 32)}
|
||
|
||
// netbirdIP wählt die NetBird-Adresse: IPv4 auf wt0, sonst die erste
|
||
// IPv4-Adresse aus 100.64.0.0/10 (Interfaces alphabetisch).
|
||
func netbirdIP(addrs map[string][]net.IP) string {
|
||
for _, ip := range addrs["wt0"] {
|
||
if ip4 := ip.To4(); ip4 != nil {
|
||
return ip4.String()
|
||
}
|
||
}
|
||
names := make([]string, 0, len(addrs))
|
||
for name := range addrs {
|
||
names = append(names, name)
|
||
}
|
||
slices.Sort(names)
|
||
for _, name := range names {
|
||
for _, ip := range addrs[name] {
|
||
if ip4 := ip.To4(); ip4 != nil && cgnat.Contains(ip4) {
|
||
return ip4.String()
|
||
}
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// interfaceAddrs liest die Adressen aller Interfaces.
|
||
func interfaceAddrs() map[string][]net.IP {
|
||
ifaces, err := net.Interfaces()
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
out := map[string][]net.IP{}
|
||
for _, ifc := range ifaces {
|
||
addrs, err := ifc.Addrs()
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for _, a := range addrs {
|
||
if ipnet, ok := a.(*net.IPNet); ok {
|
||
out[ifc.Name] = append(out[ifc.Name], ipnet.IP)
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// osPrettyName liest PRETTY_NAME (ersatzweise NAME) aus einer os-release-Datei.
|
||
func osPrettyName(path string) string {
|
||
f, err := os.Open(path)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
defer f.Close()
|
||
var name string
|
||
sc := bufio.NewScanner(f)
|
||
for sc.Scan() {
|
||
key, value, ok := strings.Cut(strings.TrimSpace(sc.Text()), "=")
|
||
if !ok {
|
||
continue
|
||
}
|
||
value = strings.Trim(value, `"'`)
|
||
switch key {
|
||
case "PRETTY_NAME":
|
||
return value
|
||
case "NAME":
|
||
name = value
|
||
}
|
||
}
|
||
return name
|
||
}
|
||
|
||
// hostInfo sammelt die Angaben zum Knoten. Mit Docker kommt das
|
||
// Betriebssystem aus der Docker-API (das des Hosts, auch wenn der Agent in
|
||
// einem Container läuft); Fehler lassen Felder nur leer.
|
||
func (a *Agent) hostInfo(ctx context.Context, client *docker.Client) *registry.HostInfo {
|
||
h := ®istry.HostInfo{FQDN: a.fqdn, NetBirdIP: netbirdIP(interfaceAddrs())}
|
||
if client != nil {
|
||
info, err := client.Info(ctx)
|
||
if err != nil {
|
||
a.log.Debug("Docker-Info nicht lesbar", "err", err)
|
||
}
|
||
h.OS, h.DockerVersion = info.OperatingSystem, info.ServerVersion
|
||
}
|
||
if h.OS == "" {
|
||
h.OS = osPrettyName(osReleasePath)
|
||
}
|
||
return h
|
||
}
|