Add LXC config metadata for guest drawers (#596)

This commit is contained in:
rcourtman 2025-10-23 07:30:32 +00:00
parent f4ead79c82
commit be85459db2
7 changed files with 218 additions and 0 deletions

View file

@ -411,6 +411,9 @@ func (noopPVEClient) GetVMStatus(ctx context.Context, node string, vmid int) (*p
func (noopPVEClient) GetContainerStatus(ctx context.Context, node string, vmid int) (*proxmox.Container, error) {
return nil, nil
}
func (noopPVEClient) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
return nil, nil
}
func (noopPVEClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
return nil, nil
}

View file

@ -55,6 +55,7 @@ type PVEClientInterface interface {
GetContainerSnapshots(ctx context.Context, node string, vmid int) ([]proxmox.Snapshot, error)
GetVMStatus(ctx context.Context, node string, vmid int) (*proxmox.VMStatus, error)
GetContainerStatus(ctx context.Context, node string, vmid int) (*proxmox.Container, error)
GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error)
GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error)
IsClusterMember(ctx context.Context) (bool, error)
GetVMFSInfo(ctx context.Context, node string, vmid int) ([]proxmox.VMFileSystem, error)
@ -1864,6 +1865,7 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
return
}
rootDeviceHint := ""
addressSet := make(map[string]struct{})
addressOrder := make([]string, 0, 4)
@ -1931,6 +1933,31 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
}
}
configCtx, cancelConfig := context.WithTimeout(ctx, 5*time.Second)
configData, configErr := client.GetContainerConfig(configCtx, nodeName, container.VMID)
cancelConfig()
if configErr != nil {
log.Debug().
Err(configErr).
Str("instance", instanceName).
Str("node", nodeName).
Str("container", container.Name).
Int("vmid", container.VMID).
Msg("Container config metadata unavailable")
} else if len(configData) > 0 {
if hint := extractContainerRootDeviceFromConfig(configData); hint != "" {
rootDeviceHint = hint
}
for _, detail := range parseContainerConfigNetworks(configData) {
if len(detail.Addresses) > 0 {
for _, addr := range detail.Addresses {
addAddress(addr)
}
}
mergeContainerNetworkInterface(&networkIfaces, detail)
}
}
if len(networkIfaces) > 1 {
sort.SliceStable(networkIfaces, func(i, j int) bool {
left := strings.TrimSpace(networkIfaces[i].Name)
@ -1956,6 +1983,14 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
}
ensureContainerRootDiskEntry(container)
if rootDeviceHint != "" && len(container.Disks) > 0 {
for i := range container.Disks {
if container.Disks[i].Mountpoint == "/" && container.Disks[i].Device == "" {
container.Disks[i].Device = rootDeviceHint
}
}
}
}
func ensureContainerRootDiskEntry(container *models.Container) {
@ -2165,6 +2200,137 @@ func dedupeStringsPreserveOrder(values []string) []string {
return result
}
type containerNetworkDetails struct {
Name string
MAC string
Addresses []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 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
func (m *Monitor) GetConnectionStatuses() map[string]bool {
if mock.IsMockEnabled() {

View file

@ -77,6 +77,10 @@ func (s *stubPVEClient) GetContainerStatus(ctx context.Context, node string, vmi
return nil, nil
}
func (s *stubPVEClient) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
return nil, nil
}
func (s *stubPVEClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
return nil, nil
}

View file

@ -56,6 +56,10 @@ func (f fakeSnapshotClient) GetVMStatus(ctx context.Context, node string, vmid i
func (f fakeSnapshotClient) GetContainerStatus(ctx context.Context, node string, vmid int) (*proxmox.Container, error) {
return nil, nil
}
func (f fakeSnapshotClient) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
return nil, nil
}
func (f fakeSnapshotClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
return nil, nil
}

View file

@ -77,6 +77,10 @@ func (f *fakeStorageClient) GetContainerStatus(ctx context.Context, node string,
return nil, nil
}
func (f *fakeStorageClient) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
return nil, nil
}
func (f *fakeStorageClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
return nil, nil
}

View file

@ -785,6 +785,29 @@ type ContainerDiskUsage struct {
Used uint64 `json:"used,omitempty"`
}
// GetContainerConfig returns the configuration of a specific container
func (c *Client) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
resp, err := c.get(ctx, fmt.Sprintf("/nodes/%s/lxc/%d/config", node, vmid))
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result struct {
Data map[string]interface{} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
if result.Data == nil {
result.Data = make(map[string]interface{})
}
return result.Data, nil
}
// Storage represents a Proxmox VE storage
type Storage struct {
Storage string `json:"storage"`

View file

@ -1003,6 +1003,20 @@ func (cc *ClusterClient) GetContainerStatus(ctx context.Context, node string, vm
return result, err
}
// GetContainerConfig returns the configuration of a specific container
func (cc *ClusterClient) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
var result map[string]interface{}
err := cc.executeWithFailover(ctx, func(client *Client) error {
config, err := client.GetContainerConfig(ctx, node, vmid)
if err != nil {
return err
}
result = config
return nil
})
return result, err
}
// IsClusterMember checks if this node is part of a cluster
func (cc *ClusterClient) IsClusterMember(ctx context.Context) (bool, error) {
var result bool