refactor: Extract container parsing functions to separate file
Move 13 related container parsing functions from monitor.go (9359 lines) to container_parsing.go (451 lines). Reduces monitor.go by 427 lines. Functions extracted: - ensureContainerRootDiskEntry - convertContainerDiskInfo - sanitizeRootFSDevice - parseContainerRawIPs - collectIPsFromInterface - sanitizeGuestAddressStrings - dedupeStringsPreserveOrder - containerNetworkDetails struct - containerMountMetadata struct - parseContainerConfigNetworks - parseContainerMountMetadata - mergeContainerNetworkInterface - extractContainerRootDeviceFromConfig
This commit is contained in:
parent
82c7925895
commit
0b2485b26b
2 changed files with 451 additions and 427 deletions
451
internal/monitoring/container_parsing.go
Normal file
451
internal/monitoring/container_parsing.go
Normal file
|
|
@ -0,0 +1,451 @@
|
||||||
|
package monitoring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
|
||||||
|
)
|
||||||
|
|
||||||
|
// containerNetworkDetails holds parsed network interface information from container config.
|
||||||
|
type containerNetworkDetails struct {
|
||||||
|
Name string
|
||||||
|
MAC string
|
||||||
|
Addresses []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// containerMountMetadata holds parsed mount point information from container config.
|
||||||
|
type containerMountMetadata struct {
|
||||||
|
Key string
|
||||||
|
Mountpoint string
|
||||||
|
Source string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureContainerRootDiskEntry adds a root disk entry to a container if none exists.
|
||||||
|
func ensureContainerRootDiskEntry(container *models.Container) {
|
||||||
|
if container == nil || len(container.Disks) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
total := container.Disk.Total
|
||||||
|
used := container.Disk.Used
|
||||||
|
if total > 0 && used > total {
|
||||||
|
used = total
|
||||||
|
}
|
||||||
|
|
||||||
|
free := total - used
|
||||||
|
if free < 0 {
|
||||||
|
free = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
usage := container.Disk.Usage
|
||||||
|
if total > 0 && usage <= 0 {
|
||||||
|
usage = safePercentage(float64(used), float64(total))
|
||||||
|
}
|
||||||
|
|
||||||
|
container.Disks = []models.Disk{
|
||||||
|
{
|
||||||
|
Total: total,
|
||||||
|
Used: used,
|
||||||
|
Free: free,
|
||||||
|
Usage: usage,
|
||||||
|
Mountpoint: "/",
|
||||||
|
Type: "rootfs",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertContainerDiskInfo converts Proxmox container disk info to the models format.
|
||||||
|
func convertContainerDiskInfo(status *proxmox.Container, metadata map[string]containerMountMetadata) []models.Disk {
|
||||||
|
if status == nil || len(status.DiskInfo) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
disks := make([]models.Disk, 0, len(status.DiskInfo))
|
||||||
|
for name, info := range status.DiskInfo {
|
||||||
|
total := clampToInt64(info.Total)
|
||||||
|
used := clampToInt64(info.Used)
|
||||||
|
if total > 0 && used > total {
|
||||||
|
used = total
|
||||||
|
}
|
||||||
|
free := total - used
|
||||||
|
if free < 0 {
|
||||||
|
free = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
disk := models.Disk{
|
||||||
|
Total: total,
|
||||||
|
Used: used,
|
||||||
|
Free: free,
|
||||||
|
}
|
||||||
|
|
||||||
|
if total > 0 {
|
||||||
|
disk.Usage = safePercentage(float64(used), float64(total))
|
||||||
|
}
|
||||||
|
|
||||||
|
label := strings.TrimSpace(name)
|
||||||
|
lowerLabel := strings.ToLower(label)
|
||||||
|
mountpoint := ""
|
||||||
|
device := ""
|
||||||
|
|
||||||
|
if metadata != nil {
|
||||||
|
if meta, ok := metadata[lowerLabel]; ok {
|
||||||
|
mountpoint = strings.TrimSpace(meta.Mountpoint)
|
||||||
|
device = strings.TrimSpace(meta.Source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(label, "rootfs") || label == "" {
|
||||||
|
if mountpoint == "" {
|
||||||
|
mountpoint = "/"
|
||||||
|
}
|
||||||
|
disk.Type = "rootfs"
|
||||||
|
if device == "" {
|
||||||
|
device = sanitizeRootFSDevice(status.RootFS)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if mountpoint == "" {
|
||||||
|
mountpoint = label
|
||||||
|
}
|
||||||
|
if lowerLabel != "" {
|
||||||
|
disk.Type = lowerLabel
|
||||||
|
} else {
|
||||||
|
disk.Type = "disk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disk.Mountpoint = mountpoint
|
||||||
|
if disk.Device == "" && device != "" {
|
||||||
|
disk.Device = device
|
||||||
|
}
|
||||||
|
|
||||||
|
disks = append(disks, disk)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(disks) > 1 {
|
||||||
|
sort.SliceStable(disks, func(i, j int) bool {
|
||||||
|
return disks[i].Mountpoint < disks[j].Mountpoint
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return disks
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeRootFSDevice extracts the device path from a rootfs config string.
|
||||||
|
func sanitizeRootFSDevice(root string) string {
|
||||||
|
root = strings.TrimSpace(root)
|
||||||
|
if root == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if idx := strings.Index(root, ","); idx != -1 {
|
||||||
|
root = root[:idx]
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseContainerRawIPs extracts IP addresses from raw JSON data.
|
||||||
|
func parseContainerRawIPs(raw json.RawMessage) []string {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var data interface{}
|
||||||
|
if err := json.Unmarshal(raw, &data); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return collectIPsFromInterface(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectIPsFromInterface recursively extracts IP addresses from various data types.
|
||||||
|
func collectIPsFromInterface(value interface{}) []string {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case nil:
|
||||||
|
return nil
|
||||||
|
case string:
|
||||||
|
return sanitizeGuestAddressStrings(v)
|
||||||
|
case []interface{}:
|
||||||
|
results := make([]string, 0, len(v))
|
||||||
|
for _, item := range v {
|
||||||
|
results = append(results, collectIPsFromInterface(item)...)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
case []string:
|
||||||
|
results := make([]string, 0, len(v))
|
||||||
|
for _, item := range v {
|
||||||
|
results = append(results, sanitizeGuestAddressStrings(item)...)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
case map[string]interface{}:
|
||||||
|
results := make([]string, 0)
|
||||||
|
for _, key := range []string{"ip", "ip6", "ipv4", "ipv6", "address", "value"} {
|
||||||
|
if val, ok := v[key]; ok {
|
||||||
|
results = append(results, collectIPsFromInterface(val)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
case json.Number:
|
||||||
|
return sanitizeGuestAddressStrings(v.String())
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeGuestAddressStrings cleans and validates IP address strings.
|
||||||
|
func sanitizeGuestAddressStrings(value string) []string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lower := strings.ToLower(value)
|
||||||
|
switch lower {
|
||||||
|
case "dhcp", "manual", "static", "auto", "none", "n/a", "unknown", "0.0.0.0", "::", "::1":
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.FieldsFunc(value, func(r rune) bool {
|
||||||
|
return unicode.IsSpace(r) || r == ',' || r == ';'
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(parts) > 1 {
|
||||||
|
results := make([]string, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
results = append(results, sanitizeGuestAddressStrings(part)...)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx := strings.Index(value, "/"); idx > 0 {
|
||||||
|
value = strings.TrimSpace(value[:idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
lower = strings.ToLower(value)
|
||||||
|
|
||||||
|
if idx := strings.Index(value, "%"); idx > 0 {
|
||||||
|
value = strings.TrimSpace(value[:idx])
|
||||||
|
lower = strings.ToLower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(value, "127.") || strings.HasPrefix(lower, "0.0.0.0") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(lower, "fe80") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(lower, "::1") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dedupeStringsPreserveOrder removes duplicates from a string slice while preserving order.
|
||||||
|
func dedupeStringsPreserveOrder(values []string) []string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(values))
|
||||||
|
result := make([]string, 0, len(values))
|
||||||
|
for _, v := range values {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[v]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[v] = struct{}{}
|
||||||
|
result = append(result, v)
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseContainerConfigNetworks extracts network interface details from container config.
|
||||||
|
func parseContainerConfigNetworks(config map[string]interface{}) []containerNetworkDetails {
|
||||||
|
if len(config) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(config))
|
||||||
|
for key := range config {
|
||||||
|
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(key)), "net") {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
results := make([]containerNetworkDetails, 0, len(keys))
|
||||||
|
for _, key := range keys {
|
||||||
|
raw := fmt.Sprint(config[key])
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
detail := containerNetworkDetails{}
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
for _, part := range parts {
|
||||||
|
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
|
||||||
|
if len(kv) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
k := strings.ToLower(strings.TrimSpace(kv[0]))
|
||||||
|
value := strings.TrimSpace(kv[1])
|
||||||
|
switch k {
|
||||||
|
case "name":
|
||||||
|
detail.Name = value
|
||||||
|
case "hwaddr", "mac", "macaddr":
|
||||||
|
detail.MAC = strings.ToUpper(value)
|
||||||
|
case "ip", "ip6", "ips", "ip6addr", "ip6prefix":
|
||||||
|
detail.Addresses = append(detail.Addresses, sanitizeGuestAddressStrings(value)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if detail.Name == "" {
|
||||||
|
detail.Name = strings.TrimSpace(key)
|
||||||
|
}
|
||||||
|
if len(detail.Addresses) > 0 {
|
||||||
|
detail.Addresses = dedupeStringsPreserveOrder(detail.Addresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
if detail.Name != "" || detail.MAC != "" || len(detail.Addresses) > 0 {
|
||||||
|
results = append(results, detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseContainerMountMetadata extracts mount point metadata from container config.
|
||||||
|
func parseContainerMountMetadata(config map[string]interface{}) map[string]containerMountMetadata {
|
||||||
|
if len(config) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make(map[string]containerMountMetadata)
|
||||||
|
for rawKey, rawValue := range config {
|
||||||
|
key := strings.ToLower(strings.TrimSpace(rawKey))
|
||||||
|
if key != "rootfs" && !strings.HasPrefix(key, "mp") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value := strings.TrimSpace(fmt.Sprint(rawValue))
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := containerMountMetadata{
|
||||||
|
Key: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(value, ",")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
meta.Source = strings.TrimSpace(parts[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, part := range parts[1:] {
|
||||||
|
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
|
||||||
|
if len(kv) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
k := strings.ToLower(strings.TrimSpace(kv[0]))
|
||||||
|
v := strings.TrimSpace(kv[1])
|
||||||
|
switch k {
|
||||||
|
case "mp", "mountpoint":
|
||||||
|
meta.Mountpoint = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if meta.Mountpoint == "" && key == "rootfs" {
|
||||||
|
meta.Mountpoint = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
results[key] = meta
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeContainerNetworkInterface merges network interface details into the target slice.
|
||||||
|
func mergeContainerNetworkInterface(target *[]models.GuestNetworkInterface, detail containerNetworkDetails) {
|
||||||
|
if target == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(detail.Addresses) > 0 {
|
||||||
|
detail.Addresses = dedupeStringsPreserveOrder(detail.Addresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
findMatch := func() int {
|
||||||
|
for i := range *target {
|
||||||
|
if detail.Name != "" && (*target)[i].Name != "" && strings.EqualFold((*target)[i].Name, detail.Name) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
if detail.MAC != "" && (*target)[i].MAC != "" && strings.EqualFold((*target)[i].MAC, detail.MAC) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx := findMatch(); idx >= 0 {
|
||||||
|
if detail.Name != "" && (*target)[idx].Name == "" {
|
||||||
|
(*target)[idx].Name = detail.Name
|
||||||
|
}
|
||||||
|
if detail.MAC != "" && (*target)[idx].MAC == "" {
|
||||||
|
(*target)[idx].MAC = detail.MAC
|
||||||
|
}
|
||||||
|
if len(detail.Addresses) > 0 {
|
||||||
|
combined := append((*target)[idx].Addresses, detail.Addresses...)
|
||||||
|
(*target)[idx].Addresses = dedupeStringsPreserveOrder(combined)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newIface := models.GuestNetworkInterface{
|
||||||
|
Name: detail.Name,
|
||||||
|
MAC: detail.MAC,
|
||||||
|
}
|
||||||
|
if len(detail.Addresses) > 0 {
|
||||||
|
newIface.Addresses = dedupeStringsPreserveOrder(detail.Addresses)
|
||||||
|
}
|
||||||
|
*target = append(*target, newIface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractContainerRootDeviceFromConfig extracts the root device path from container config.
|
||||||
|
func extractContainerRootDeviceFromConfig(config map[string]interface{}) string {
|
||||||
|
if len(config) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
raw, ok := config["rootfs"]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
value := strings.TrimSpace(fmt.Sprint(raw))
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(value, ",")
|
||||||
|
device := strings.TrimSpace(parts[0])
|
||||||
|
return device
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||||
|
|
@ -2951,432 +2950,6 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureContainerRootDiskEntry(container *models.Container) {
|
|
||||||
if container == nil || len(container.Disks) > 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
total := container.Disk.Total
|
|
||||||
used := container.Disk.Used
|
|
||||||
if total > 0 && used > total {
|
|
||||||
used = total
|
|
||||||
}
|
|
||||||
|
|
||||||
free := total - used
|
|
||||||
if free < 0 {
|
|
||||||
free = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
usage := container.Disk.Usage
|
|
||||||
if total > 0 && usage <= 0 {
|
|
||||||
usage = safePercentage(float64(used), float64(total))
|
|
||||||
}
|
|
||||||
|
|
||||||
container.Disks = []models.Disk{
|
|
||||||
{
|
|
||||||
Total: total,
|
|
||||||
Used: used,
|
|
||||||
Free: free,
|
|
||||||
Usage: usage,
|
|
||||||
Mountpoint: "/",
|
|
||||||
Type: "rootfs",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertContainerDiskInfo(status *proxmox.Container, metadata map[string]containerMountMetadata) []models.Disk {
|
|
||||||
if status == nil || len(status.DiskInfo) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
disks := make([]models.Disk, 0, len(status.DiskInfo))
|
|
||||||
for name, info := range status.DiskInfo {
|
|
||||||
total := clampToInt64(info.Total)
|
|
||||||
used := clampToInt64(info.Used)
|
|
||||||
if total > 0 && used > total {
|
|
||||||
used = total
|
|
||||||
}
|
|
||||||
free := total - used
|
|
||||||
if free < 0 {
|
|
||||||
free = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
disk := models.Disk{
|
|
||||||
Total: total,
|
|
||||||
Used: used,
|
|
||||||
Free: free,
|
|
||||||
}
|
|
||||||
|
|
||||||
if total > 0 {
|
|
||||||
disk.Usage = safePercentage(float64(used), float64(total))
|
|
||||||
}
|
|
||||||
|
|
||||||
label := strings.TrimSpace(name)
|
|
||||||
lowerLabel := strings.ToLower(label)
|
|
||||||
mountpoint := ""
|
|
||||||
device := ""
|
|
||||||
|
|
||||||
if metadata != nil {
|
|
||||||
if meta, ok := metadata[lowerLabel]; ok {
|
|
||||||
mountpoint = strings.TrimSpace(meta.Mountpoint)
|
|
||||||
device = strings.TrimSpace(meta.Source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.EqualFold(label, "rootfs") || label == "" {
|
|
||||||
if mountpoint == "" {
|
|
||||||
mountpoint = "/"
|
|
||||||
}
|
|
||||||
disk.Type = "rootfs"
|
|
||||||
if device == "" {
|
|
||||||
device = sanitizeRootFSDevice(status.RootFS)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if mountpoint == "" {
|
|
||||||
mountpoint = label
|
|
||||||
}
|
|
||||||
if lowerLabel != "" {
|
|
||||||
disk.Type = lowerLabel
|
|
||||||
} else {
|
|
||||||
disk.Type = "disk"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disk.Mountpoint = mountpoint
|
|
||||||
if disk.Device == "" && device != "" {
|
|
||||||
disk.Device = device
|
|
||||||
}
|
|
||||||
|
|
||||||
disks = append(disks, disk)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(disks) > 1 {
|
|
||||||
sort.SliceStable(disks, func(i, j int) bool {
|
|
||||||
return disks[i].Mountpoint < disks[j].Mountpoint
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return disks
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeRootFSDevice(root string) string {
|
|
||||||
root = strings.TrimSpace(root)
|
|
||||||
if root == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if idx := strings.Index(root, ","); idx != -1 {
|
|
||||||
root = root[:idx]
|
|
||||||
}
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseContainerRawIPs(raw json.RawMessage) []string {
|
|
||||||
if len(raw) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var data interface{}
|
|
||||||
if err := json.Unmarshal(raw, &data); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return collectIPsFromInterface(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectIPsFromInterface(value interface{}) []string {
|
|
||||||
switch v := value.(type) {
|
|
||||||
case nil:
|
|
||||||
return nil
|
|
||||||
case string:
|
|
||||||
return sanitizeGuestAddressStrings(v)
|
|
||||||
case []interface{}:
|
|
||||||
results := make([]string, 0, len(v))
|
|
||||||
for _, item := range v {
|
|
||||||
results = append(results, collectIPsFromInterface(item)...)
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
case []string:
|
|
||||||
results := make([]string, 0, len(v))
|
|
||||||
for _, item := range v {
|
|
||||||
results = append(results, sanitizeGuestAddressStrings(item)...)
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
case map[string]interface{}:
|
|
||||||
results := make([]string, 0)
|
|
||||||
for _, key := range []string{"ip", "ip6", "ipv4", "ipv6", "address", "value"} {
|
|
||||||
if val, ok := v[key]; ok {
|
|
||||||
results = append(results, collectIPsFromInterface(val)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
case json.Number:
|
|
||||||
return sanitizeGuestAddressStrings(v.String())
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeGuestAddressStrings(value string) []string {
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
if value == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lower := strings.ToLower(value)
|
|
||||||
switch lower {
|
|
||||||
case "dhcp", "manual", "static", "auto", "none", "n/a", "unknown", "0.0.0.0", "::", "::1":
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.FieldsFunc(value, func(r rune) bool {
|
|
||||||
return unicode.IsSpace(r) || r == ',' || r == ';'
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(parts) > 1 {
|
|
||||||
results := make([]string, 0, len(parts))
|
|
||||||
for _, part := range parts {
|
|
||||||
results = append(results, sanitizeGuestAddressStrings(part)...)
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
if idx := strings.Index(value, "/"); idx > 0 {
|
|
||||||
value = strings.TrimSpace(value[:idx])
|
|
||||||
}
|
|
||||||
|
|
||||||
lower = strings.ToLower(value)
|
|
||||||
|
|
||||||
if idx := strings.Index(value, "%"); idx > 0 {
|
|
||||||
value = strings.TrimSpace(value[:idx])
|
|
||||||
lower = strings.ToLower(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(value, "127.") || strings.HasPrefix(lower, "0.0.0.0") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(lower, "fe80") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(lower, "::1") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dedupeStringsPreserveOrder(values []string) []string {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
seen := make(map[string]struct{}, len(values))
|
|
||||||
result := make([]string, 0, len(values))
|
|
||||||
for _, v := range values {
|
|
||||||
v = strings.TrimSpace(v)
|
|
||||||
if v == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[v]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[v] = struct{}{}
|
|
||||||
result = append(result, v)
|
|
||||||
}
|
|
||||||
if len(result) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
type containerNetworkDetails struct {
|
|
||||||
Name string
|
|
||||||
MAC string
|
|
||||||
Addresses []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type containerMountMetadata struct {
|
|
||||||
Key string
|
|
||||||
Mountpoint string
|
|
||||||
Source string
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseContainerConfigNetworks(config map[string]interface{}) []containerNetworkDetails {
|
|
||||||
if len(config) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := make([]string, 0, len(config))
|
|
||||||
for key := range config {
|
|
||||||
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(key)), "net") {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(keys) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
results := make([]containerNetworkDetails, 0, len(keys))
|
|
||||||
for _, key := range keys {
|
|
||||||
raw := fmt.Sprint(config[key])
|
|
||||||
raw = strings.TrimSpace(raw)
|
|
||||||
if raw == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
detail := containerNetworkDetails{}
|
|
||||||
parts := strings.Split(raw, ",")
|
|
||||||
for _, part := range parts {
|
|
||||||
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
|
|
||||||
if len(kv) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
k := strings.ToLower(strings.TrimSpace(kv[0]))
|
|
||||||
value := strings.TrimSpace(kv[1])
|
|
||||||
switch k {
|
|
||||||
case "name":
|
|
||||||
detail.Name = value
|
|
||||||
case "hwaddr", "mac", "macaddr":
|
|
||||||
detail.MAC = strings.ToUpper(value)
|
|
||||||
case "ip", "ip6", "ips", "ip6addr", "ip6prefix":
|
|
||||||
detail.Addresses = append(detail.Addresses, sanitizeGuestAddressStrings(value)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if detail.Name == "" {
|
|
||||||
detail.Name = strings.TrimSpace(key)
|
|
||||||
}
|
|
||||||
if len(detail.Addresses) > 0 {
|
|
||||||
detail.Addresses = dedupeStringsPreserveOrder(detail.Addresses)
|
|
||||||
}
|
|
||||||
|
|
||||||
if detail.Name != "" || detail.MAC != "" || len(detail.Addresses) > 0 {
|
|
||||||
results = append(results, detail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseContainerMountMetadata(config map[string]interface{}) map[string]containerMountMetadata {
|
|
||||||
if len(config) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
results := make(map[string]containerMountMetadata)
|
|
||||||
for rawKey, rawValue := range config {
|
|
||||||
key := strings.ToLower(strings.TrimSpace(rawKey))
|
|
||||||
if key != "rootfs" && !strings.HasPrefix(key, "mp") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
value := strings.TrimSpace(fmt.Sprint(rawValue))
|
|
||||||
if value == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
meta := containerMountMetadata{
|
|
||||||
Key: key,
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(value, ",")
|
|
||||||
if len(parts) > 0 {
|
|
||||||
meta.Source = strings.TrimSpace(parts[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, part := range parts[1:] {
|
|
||||||
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
|
|
||||||
if len(kv) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
k := strings.ToLower(strings.TrimSpace(kv[0]))
|
|
||||||
v := strings.TrimSpace(kv[1])
|
|
||||||
switch k {
|
|
||||||
case "mp", "mountpoint":
|
|
||||||
meta.Mountpoint = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if meta.Mountpoint == "" && key == "rootfs" {
|
|
||||||
meta.Mountpoint = "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
results[key] = meta
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeContainerNetworkInterface(target *[]models.GuestNetworkInterface, detail containerNetworkDetails) {
|
|
||||||
if target == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(detail.Addresses) > 0 {
|
|
||||||
detail.Addresses = dedupeStringsPreserveOrder(detail.Addresses)
|
|
||||||
}
|
|
||||||
|
|
||||||
findMatch := func() int {
|
|
||||||
for i := range *target {
|
|
||||||
if detail.Name != "" && (*target)[i].Name != "" && strings.EqualFold((*target)[i].Name, detail.Name) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
if detail.MAC != "" && (*target)[i].MAC != "" && strings.EqualFold((*target)[i].MAC, detail.MAC) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
if idx := findMatch(); idx >= 0 {
|
|
||||||
if detail.Name != "" && (*target)[idx].Name == "" {
|
|
||||||
(*target)[idx].Name = detail.Name
|
|
||||||
}
|
|
||||||
if detail.MAC != "" && (*target)[idx].MAC == "" {
|
|
||||||
(*target)[idx].MAC = detail.MAC
|
|
||||||
}
|
|
||||||
if len(detail.Addresses) > 0 {
|
|
||||||
combined := append((*target)[idx].Addresses, detail.Addresses...)
|
|
||||||
(*target)[idx].Addresses = dedupeStringsPreserveOrder(combined)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newIface := models.GuestNetworkInterface{
|
|
||||||
Name: detail.Name,
|
|
||||||
MAC: detail.MAC,
|
|
||||||
}
|
|
||||||
if len(detail.Addresses) > 0 {
|
|
||||||
newIface.Addresses = dedupeStringsPreserveOrder(detail.Addresses)
|
|
||||||
}
|
|
||||||
*target = append(*target, newIface)
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractContainerRootDeviceFromConfig(config map[string]interface{}) string {
|
|
||||||
if len(config) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
raw, ok := config["rootfs"]
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
value := strings.TrimSpace(fmt.Sprint(raw))
|
|
||||||
if value == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(value, ",")
|
|
||||||
device := strings.TrimSpace(parts[0])
|
|
||||||
return device
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConnectionStatuses returns the current connection status for all nodes
|
// GetConnectionStatuses returns the current connection status for all nodes
|
||||||
func (m *Monitor) GetConnectionStatuses() map[string]bool {
|
func (m *Monitor) GetConnectionStatuses() map[string]bool {
|
||||||
if mock.IsMockEnabled() {
|
if mock.IsMockEnabled() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue