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
IPs anonymisieren
Screenshot erstellen
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
}