Accountant: Inventar, where, Checkmk, Abrechnung

This commit is contained in:
br committed 2026-10-18 17:08:58 +02:00
1 parent 9f16148cde
commit dd93277750
59 files changed
+7522 -113

No files matched your search

+165
View File
@@ -0,0 +1,165 @@
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 := &registry.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
}