package main import ( "encoding/csv" "encoding/json" "flag" "fmt" "log" "os" "os/exec" "regexp" "strings" ) // Globale Map zur Speicherung der IP-Anonymisierungen und der nächste Zähler var ipMap = make(map[string]string) var nextAnonID = 0 // Struktur für Disk-Device-Daten type Disk struct { Path string Pool string Source string } // Struktur für Container-Daten type Container struct { Name string IP string Profile string Proxies []Proxy Disks []Disk } // Struktur für Proxy-Device-Daten type Proxy struct { Name string Listen string Connect string } // Struktur für Netzwerk-Daten type Network struct { Name string IP string ACLs []string ACLRules map[string]string UsedBy []string Forwards []string } func main() { outFile := flag.String("out", "", "Pfad für die generierte HTML-Datei (Pflichtfeld)") flag.Parse() if *outFile == "" { log.Fatal("Fehler: Bitte das -out Flag angeben") } containers, err := getLXCContainers() if err != nil { log.Fatalf("Fehler beim Auslesen der LXC Container: %v", err) } networks, err := getLXDNetworks() if err != nil { log.Fatalf("Fehler beim Auslesen der LXD Netzwerke: %v", err) } collectAllIPs(containers, networks) // Konvertiere die Go-Map in ein JavaScript/JSON-Objekt jsMap, err := json.Marshal(ipMap) if err != nil { log.Fatalf("Fehler beim Erstellen der JSON-Map: %v", err) } jsMapString := string(jsMap) // 1. Map: Container-Namen auf das verbundene Netzwerk-Objekt abbilden. containerNetworkMap := make(map[string]Network) for _, n := range networks { for _, cNamePath := range n.UsedBy { // Pfad-Struktur: /1.0/instances/container-name parts := strings.Split(cNamePath, "/") if len(parts) >= 4 { containerName := parts[3] if _, exists := containerNetworkMap[containerName]; !exists { containerNetworkMap[containerName] = n } } } } // NEU: Map zur Speicherung eindeutiger Storage Blocks und ihrer Mermaid-ID // Schlüssel: "Pool|Source" storageBlocks := make(map[string]string) nextStorageID := 0 // NEU: Alle eindeutigen Storage-Blöcke sammeln und IDs zuweisen for _, c := range containers { for _, disk := range c.Disks { // Ein Storage Block ist eindeutig durch Pool und Source definiert key := fmt.Sprintf("%s|%s", disk.Pool, disk.Source) if _, exists := storageBlocks[key]; !exists { // Erstellt eine eindeutige Mermaid-ID storageID := fmt.Sprintf("Storage_Block_%d", nextStorageID) storageBlocks[key] = storageID nextStorageID++ } } } var html strings.Builder html.WriteString(fmt.Sprintf(` LXD Mermaid Übersicht
graph LR `, jsMapString)) // Netzwerke & ACLs (Rendering) for _, n := range networks { if len(n.UsedBy) == 0 && len(n.Forwards) == 0 { continue } netID := strings.ReplaceAll(n.Name, "-", "_") html.WriteString(fmt.Sprintf("%s[\"%s
%s\"]\n", netID, n.Name, n.IP)) // NAT-Färbung cmd := exec.Command("lxc", "network", "show", n.Name) out, err := cmd.Output() if err == nil && strings.Contains(string(out), "ipv4.nat: \"true\"") { html.WriteString(fmt.Sprintf("style %s fill:#ffcccc,stroke:#ff0000,stroke-width:2px\n", netID)) } else { html.WriteString(fmt.Sprintf("style %s fill:#e0e0ff,stroke:#333,stroke-width:1px\n", netID)) } // ACLs if len(n.ACLs) > 0 { for _, acl := range n.ACLs { if acl == "" { continue } aclID := netID + "_" + strings.ReplaceAll(acl, "-", "_") + "_acl" rules := n.ACLRules[acl] html.WriteString(fmt.Sprintf("%s[\"ACL: %s

%s\"]\n", aclID, acl, rules)) html.WriteString(fmt.Sprintf("style %s white-space: nowrap\n", aclID)) html.WriteString(fmt.Sprintf("%s --> %s\n", aclID, netID)) // ACL → Netzwerk for _, cName := range n.UsedBy { parts := strings.Split(cName, "/") if len(parts) < 4 { continue } containerName := parts[3] cID := strings.ReplaceAll(containerName, "-", "_") html.WriteString(fmt.Sprintf("%s --> %s\n", cID, aclID)) // Container → ACL } } } else { for _, cName := range n.UsedBy { parts := strings.Split(cName, "/") if len(parts) < 4 { continue } containerName := parts[3] cID := strings.ReplaceAll(containerName, "-", "_") html.WriteString(fmt.Sprintf("%s --> %s\n", cID, netID)) // Container → Netzwerk direkt } } // Forwards for _, f := range n.Forwards { fID := netID + "_forward_" + strings.ReplaceAll(f, ".", "_") html.WriteString(fmt.Sprintf("%s[\"Forward: %s\"]\n", fID, f)) html.WriteString(fmt.Sprintf("%s --> %s\n", fID, netID)) } } // NEU: Rendern der eindeutigen Storage-Blöcke (Grüne Box) for key, sID := range storageBlocks { parts := strings.Split(key, "|") pool := parts[0] source := parts[1] // Das Label des Storage Blocks enthält nun Pool und Source (den eigentlichen Speicher) storageLabel := fmt.Sprintf("Storage Pool: %s
Source: %s", pool, source) // Storage Block erstellen html.WriteString(fmt.Sprintf("%s[\"%s\"]\n", sID, storageLabel)) // Stil: Grüne Füllung für Speicher/Disks - DIES IST DER EINMALIGE GRÜNE BLOCK html.WriteString(fmt.Sprintf("style %s fill:#ccffcc,stroke:#00aa00,stroke-width:2px\n", sID)) html.WriteString("\n") } // Container-Blöcke, Proxies und Disks (Rendering) for _, c := range containers { cID := strings.ReplaceAll(c.Name, "-", "_") html.WriteString(fmt.Sprintf("%s[\"%s
%s\"]\n", cID, c.Name, c.IP)) html.WriteString("\n") // Zusätzliche Zeile zur besseren Trennung // Profil-Block if c.Profile != "" { pID := cID + "_profile" html.WriteString(fmt.Sprintf("%s[\"Profil: %s\"]\n", pID, c.Profile)) html.WriteString(fmt.Sprintf("%s --> %s\n", pID, cID)) // Profil → Container html.WriteString("\n") } // NEU: Disk-Devices (Mountpunkte) for i, disk := range c.Disks { // Eindeutige ID für den Mountpunkt im Container mountID := cID + fmt.Sprintf("_mount_%d", i) // Das Mount-Label zeigt nur den Mountpunkt (Path) mountLabel := fmt.Sprintf("Mount: %s", disk.Path) // Mount-Block erstellen (z.B. ein hellgraues Rechteck, NICHT grün) html.WriteString(fmt.Sprintf("%s[\"%s\"]\n", mountID, mountLabel)) html.WriteString(fmt.Sprintf("style %s fill:#f0f0f0,stroke:#666\n", mountID)) // Die zentrale Storage-Block ID (die grüne Box) key := fmt.Sprintf("%s|%s", disk.Pool, disk.Source) storageID := storageBlocks[key] // Verbindung 1: Mountpunkt → Container html.WriteString(fmt.Sprintf("%s --> %s\n", mountID, cID)) // Verbindung 2: Storage Block (Grüne Box) → Mountpunkt html.WriteString(fmt.Sprintf("%s --> %s\n", storageID, mountID)) html.WriteString("\n") // Fügt Leerzeile hinzu } // Proxy-Devices for _, proxy := range c.Proxies { prID := cID + "_" + strings.ReplaceAll(proxy.Name, "-", "_") html.WriteString(fmt.Sprintf("%s[\"Proxy: %s
%s → %s\"]\n", prID, proxy.Name, proxy.Listen, proxy.Connect)) // Unbedingte Rotfärbung des Proxy-Blocks html.WriteString(fmt.Sprintf("style %s fill:#ffcccc,stroke:#ff0000,stroke-width:2px\n", prID)) // Verbindung zum Netzwerk/ACL herstellen if connectedNet, found := containerNetworkMap[c.Name]; found { netID := strings.ReplaceAll(connectedNet.Name, "-", "_") if len(connectedNet.ACLs) > 0 { // Wenn ACLs vorhanden sind, zeige auf die ERSTE ACL des Netzwerks acl := connectedNet.ACLs[0] aclID := netID + "_" + strings.ReplaceAll(acl, "-", "_") + "_acl" // Proxy → ACL html.WriteString(fmt.Sprintf("%s --> %s\n", prID, aclID)) } else { // Wenn keine ACLs vorhanden, zeige direkt auf das Netzwerk // Proxy → Netzwerk html.WriteString(fmt.Sprintf("%s --> %s\n", prID, netID)) } } else { // Fallback: Wenn kein Netzwerk gefunden wird, zeige auf den Container html.WriteString(fmt.Sprintf("%s --> %s\n", prID, cID)) } html.WriteString("\n") // Fügt Leerzeile hinzu } } html.WriteString(`
`) if err := os.WriteFile(*outFile, []byte(html.String()), 0644); err != nil { log.Fatalf("Fehler beim Schreiben der Datei: %v", err) } fmt.Printf("Mermaid Diagramm erfolgreich generiert: %s\n", *outFile) } // getAndAnonymizeIP gibt die anonymisierte ID für eine IP zurück (oder generiert eine neue) func getAndAnonymizeIP(ip string) string { ip = strings.Split(ip, "/")[0] // Entferne CIDR-Suffix // Ignoriere leere Strings if ip == "" { return ip } if anonID, exists := ipMap[ip]; exists { return anonID } base := 65 // ASCII-Wert für 'A' numLetters := 26 // A-Z // Wir behandeln nextAnonID als eine Zahl im Basis-26-System. // Die Stellen sind die vier Teile der Fake-IP-Adresse. n := nextAnonID // Teil 4 (letzte Stelle, am schnellsten steigend) // Beispiel: 0 -> A, 1 -> B, ..., 26 -> A (mit Übertrag) char4Index := n % numLetters char4 := string(rune(base + char4Index)) n /= numLetters // Übertrag // Teil 3 char3Index := n % numLetters char3 := string(rune(base + char3Index)) n /= numLetters // Übertrag // Teil 2 char2Index := n % numLetters char2 := string(rune(base + char2Index)) n /= numLetters // Übertrag // Teil 1 (steigt am langsamsten) // Wenn n > 25, würden wir den 5. Teil benötigen. Wir begrenzen den Zähler n // effektiv auf das, was in die 4 Stellen passt, aber da n bei jedem Aufruf // wächst, wird die Zählung korrekt fortgesetzt. char1Index := n % numLetters char1 := string(rune(base + char1Index)) // Konstruiere die Fake-IP: A.A.A.A anonID := fmt.Sprintf("%s.%s.%s.%s", char1, char2, char3, char4) nextAnonID++ ipMap[ip] = anonID return anonID } // collectAllIPs sammelt alle IPs, bevor das Rendering beginnt, um eine vollständige ipMap zu gewährleisten func collectAllIPs(containers []Container, networks []Network) { // Suchmuster für IPs mit optionaler CIDR-Maske ipCIDRPattern := regexp.MustCompile(`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?`) // 1. Container IPs for _, c := range containers { //ip := strings.Split(c.IP, "/")[0] //getAndAnonymizeIP(ip) cleanedIPString := strings.ReplaceAll(c.IP, "
", " ") foundIPs := ipCIDRPattern.FindAllString(cleanedIPString, -1) for _, foundIP := range foundIPs { ip := strings.Split(foundIP, "/")[0] getAndAnonymizeIP(ip) } } // 2. Netzwerk IPs und Forwards for _, n := range networks { // Netzwerk-IP ip := strings.Split(n.IP, "/")[0] getAndAnonymizeIP(ip) // Forwards for _, fwd := range n.Forwards { ip := strings.Split(fwd, "/")[0] getAndAnonymizeIP(ip) } // ACLs for _, rules := range n.ACLRules { foundIPs := ipCIDRPattern.FindAllString(rules, -1) for _, foundIP := range foundIPs { ip := strings.Split(foundIP, "/")[0] getAndAnonymizeIP(ip) } } } // 3. Proxy IPs (Listen und Connect) for _, c := range containers { for _, p := range c.Proxies { // Listen IP parts := strings.Split(p.Listen, ":") if len(parts) > 1 { listenIP := strings.Split(parts[1], "/")[0] getAndAnonymizeIP(listenIP) } // Connect IP parts = strings.Split(p.Connect, ":") if len(parts) > 1 { connectIP := strings.Split(parts[1], "/")[0] getAndAnonymizeIP(connectIP) } } } } // getLXCContainers liest LXC Container, Profile, Proxies und NEU: Disks aus func getLXCContainers() ([]Container, error) { cmd := exec.Command("lxc", "list", "-f", "csv") out, err := cmd.Output() if err != nil { return nil, err } r := csv.NewReader(strings.NewReader(string(out))) records, err := r.ReadAll() if err != nil { return nil, err } var containers []Container for _, rec := range records { if len(rec) < 3 { continue } name := rec[0] //ip := strings.Split(rec[2], " ")[0] rawIP := strings.TrimSpace(rec[2]) ip := strings.ReplaceAll(rawIP, "\n", "
") // Profil per Shell-Befehl auslesen profileCmd := exec.Command("bash", "-c", fmt.Sprintf("lxc list %s -f json | jq -r '.[0].profiles[]'", name)) profileOut, err := profileCmd.Output() profile := "" if err == nil { profile = strings.TrimSpace(string(profileOut)) } // Proxies auslesen proxies, err := getContainerProxies(name) if err != nil { proxies = []Proxy{} } // NEU: Disks auslesen disks, err := getContainerDisks(name) if err != nil { disks = []Disk{} } containers = append(containers, Container{ Name: name, IP: ip, Profile: profile, Proxies: proxies, Disks: disks, // Hinzufügen der Disks }) } return containers, nil } // getContainerProxies liest Proxy-Devices eines Containers aus func getContainerProxies(name string) ([]Proxy, error) { cmd := exec.Command("bash", "-c", fmt.Sprintf( `lxc list %s -f json | jq -r '.[] | .expanded_devices | to_entries[] | select(.value.type=="proxy") | "\(.key)|\(.value.listen)|\(.value.connect)"'`, name)) out, err := cmd.Output() if err != nil { return nil, err } var proxies []Proxy lines := strings.Split(strings.TrimSpace(string(out)), "\n") for _, l := range lines { parts := strings.Split(l, "|") if len(parts) != 3 { continue } proxies = append(proxies, Proxy{ Name: parts[0], Listen: parts[1], Connect: parts[2], }) } return proxies, nil } // getContainerDisks liest Disk-Devices (ohne root) eines Containers aus func getContainerDisks(name string) ([]Disk, error) { // Nutzt jq, um alle Geräte vom Typ "disk" auszulesen, die NICHT "root" sind cmd := exec.Command("bash", "-c", fmt.Sprintf( `lxc list %s -f json | jq -r '.[0].expanded_devices | to_entries[] | select(.key != "root" and .value.type == "disk") | "\(.value.path)|\(.value.pool)|\(.value.source)"'`, name)) out, err := cmd.Output() if err != nil { return nil, err } var disks []Disk lines := strings.Split(strings.TrimSpace(string(out)), "\n") for _, l := range lines { if l == "" { continue } parts := strings.Split(l, "|") if len(parts) != 3 { // Erwartetes Format: /path|pool|source continue } disks = append(disks, Disk{ Path: parts[0], Pool: parts[1], Source: parts[2], }) } return disks, nil } func getLXDNetworks() ([]Network, error) { cmd := exec.Command("lxc", "network", "list", "-f", "csv") out, err := cmd.Output() if err != nil { return nil, err } r := csv.NewReader(strings.NewReader(string(out))) records, err := r.ReadAll() if err != nil { return nil, err } var networks []Network for _, rec := range records { if len(rec) < 8 { continue } status := strings.TrimSpace(rec[7]) if status != "CREATED" { continue } name := rec[0] ip := rec[3] acls, aclRules, usedBy, err := getNetworkDetails(name) if err != nil { return nil, err } // Forwards auslesen var forwards []string fwdCmd := exec.Command("lxc", "network", "forward", "list", "-f", "csv", name) fwdOut, err := fwdCmd.Output() if err == nil { lines := strings.Split(strings.TrimSpace(string(fwdOut)), "\n") for _, l := range lines { parts := strings.Split(l, ",") if len(parts) > 0 && parts[0] != "" { forwards = append(forwards, parts[0]) } } } networks = append(networks, Network{ Name: name, IP: ip, ACLs: acls, ACLRules: aclRules, UsedBy: usedBy, Forwards: forwards, }) } return networks, nil } func getNetworkDetails(name string) ([]string, map[string]string, []string, error) { cmd := exec.Command("lxc", "network", "show", name) out, err := cmd.Output() if err != nil { return nil, nil, nil, err } var used []string var acls []string aclRules := make(map[string]string) lines := strings.Split(string(out), "\n") inUsedBy := false inConfig := false for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "used_by:") { inUsedBy = true inConfig = false continue } if strings.HasPrefix(line, "config:") { inConfig = true inUsedBy = false continue } if inUsedBy { if line == "" || !strings.HasPrefix(line, "- ") { inUsedBy = false continue } path := strings.TrimPrefix(line, "- ") if strings.HasPrefix(path, "/1.0/instances/") { used = append(used, path) } } if inConfig && strings.HasPrefix(line, "security.acls:") { rawACL := strings.TrimPrefix(line, "security.acls:") for _, a := range strings.Split(rawACL, ",") { a = strings.TrimSpace(a) if a == "" { continue } acls = append(acls, a) rulesOut, err := exec.Command("lxc", "network", "acl", "show", a).Output() if err != nil { aclRules[a] = fmt.Sprintf("Fehler: %v", err) continue } var aclBlock strings.Builder var inACLBlock bool = false for _, l := range strings.Split(string(rulesOut), "\n") { l = strings.TrimSpace(l) if l == "egress:" || l == "ingress:" { inACLBlock = true } if strings.HasPrefix(l, "config:") { break } if inACLBlock { aclBlock.WriteString(l + "\n") } } var sb strings.Builder var inBlock string var ruleLine strings.Builder for _, l := range strings.Split(aclBlock.String(), "\n") { l = strings.TrimSpace(l) switch l { case "egress:": if ruleLine.Len() > 0 { sb.WriteString(ruleLine.String() + "
") ruleLine.Reset() } inBlock = "Egress" sb.WriteString("Egress:
") continue case "ingress:": if ruleLine.Len() > 0 { sb.WriteString(ruleLine.String() + "
") ruleLine.Reset() } inBlock = "Ingress" sb.WriteString("
Ingress:
") continue } if l == "" { if ruleLine.Len() > 0 { sb.WriteString(ruleLine.String() + "
") ruleLine.Reset() } continue } if inBlock != "" { if strings.HasPrefix(l, "- ") { l = strings.TrimPrefix(l, "- ") } l = strings.ReplaceAll(l, `"`, `'`) if strings.HasPrefix(l, "action:") { if ruleLine.Len() > 0 { sb.WriteString(ruleLine.String() + "
") ruleLine.Reset() } ruleLine.WriteString(l) } else { ruleLine.WriteString(" - " + l) } } } if ruleLine.Len() > 0 { sb.WriteString(ruleLine.String() + "
") } aclRules[a] = sb.String() } } } return acls, aclRules, used, nil }