mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 19:57:09 +00:00
Address shared storage node metadata (#504)
This commit is contained in:
@@ -100,30 +100,81 @@ const Storage: Component = () => {
|
||||
}
|
||||
|
||||
if (!storageMap.has(key)) {
|
||||
const backendNodes = (s.nodes ?? []).filter((node): node is string => Boolean(node));
|
||||
const backendNodeIds = ((s as { nodeIds?: string[] }).nodeIds ?? []).filter(
|
||||
(id): id is string => Boolean(id),
|
||||
);
|
||||
|
||||
const rawNodes = backendNodes.length > 0 ? backendNodes : [s.node].filter(Boolean);
|
||||
const uniqueNodes = Array.from(new Set(rawNodes));
|
||||
const normalizedInitialNodes =
|
||||
uniqueNodes.length > 1 ? uniqueNodes.filter((node) => node !== 'cluster') : uniqueNodes;
|
||||
const nodesForStorage =
|
||||
normalizedInitialNodes.length > 0 ? normalizedInitialNodes : uniqueNodes;
|
||||
const initialNodeIds = Array.from(
|
||||
new Set(backendNodeIds.length > 0 ? backendNodeIds : [nodeId].filter(Boolean)),
|
||||
);
|
||||
|
||||
const initialPBSNames =
|
||||
s.type === 'pbs'
|
||||
? Array.from(new Set((s.pbsNames ?? [s.name]).filter((name): name is string => Boolean(name))))
|
||||
: undefined;
|
||||
|
||||
// First occurrence - store it with node list
|
||||
storageMap.set(key, {
|
||||
...s,
|
||||
name: s.type === 'pbs' ? 'PBS Storage' : s.name, // Generic name for PBS
|
||||
nodes: [s.node],
|
||||
nodeIds: [nodeId],
|
||||
nodeCount: 1,
|
||||
pbsNames: s.type === 'pbs' ? [s.name] : undefined, // Track individual PBS names
|
||||
nodes: nodesForStorage,
|
||||
nodeIds: initialNodeIds,
|
||||
nodeCount: nodesForStorage.length,
|
||||
pbsNames: initialPBSNames, // Track individual PBS names
|
||||
});
|
||||
} else {
|
||||
// Duplicate - just add to node list
|
||||
const existing = storageMap.get(key);
|
||||
if (!existing.nodes.includes(s.node)) {
|
||||
existing.nodes.push(s.node);
|
||||
existing.nodeCount = existing.nodes.length;
|
||||
}
|
||||
|
||||
const backendNodes = (s.nodes ?? []).filter((node): node is string => Boolean(node));
|
||||
const rawCandidateNodes =
|
||||
backendNodes.length > 0 ? backendNodes : [s.node].filter(Boolean);
|
||||
const candidateNodes =
|
||||
rawCandidateNodes.length > 1
|
||||
? rawCandidateNodes.filter((node) => node !== 'cluster')
|
||||
: rawCandidateNodes;
|
||||
candidateNodes.forEach((node) => {
|
||||
if (!existing.nodes.includes(node)) {
|
||||
existing.nodes.push(node);
|
||||
}
|
||||
});
|
||||
existing.nodeCount = existing.nodes.length;
|
||||
|
||||
const backendNodeIds = ((s as { nodeIds?: string[] }).nodeIds ?? []).filter(
|
||||
(id): id is string => Boolean(id),
|
||||
);
|
||||
const candidateNodeIds =
|
||||
backendNodeIds.length > 0 ? backendNodeIds : [nodeId].filter(Boolean);
|
||||
|
||||
if (!existing.nodeIds) {
|
||||
existing.nodeIds = [nodeId];
|
||||
} else if (!existing.nodeIds.includes(nodeId)) {
|
||||
existing.nodeIds.push(nodeId);
|
||||
existing.nodeIds = [];
|
||||
}
|
||||
// For PBS, collect all namespace names
|
||||
if (s.type === 'pbs' && existing.pbsNames && !existing.pbsNames.includes(s.name)) {
|
||||
existing.pbsNames.push(s.name);
|
||||
candidateNodeIds.forEach((id) => {
|
||||
if (!existing.nodeIds!.includes(id)) {
|
||||
existing.nodeIds!.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
// For PBS, collect all namespace names (merging backend-provided data)
|
||||
if (s.type === 'pbs') {
|
||||
if (!existing.pbsNames) {
|
||||
existing.pbsNames = [];
|
||||
}
|
||||
const incomingPBSNames = (s.pbsNames ?? [s.name]).filter(
|
||||
(name): name is string => Boolean(name),
|
||||
);
|
||||
incomingPBSNames.forEach((name) => {
|
||||
if (!existing.pbsNames!.includes(name)) {
|
||||
existing.pbsNames!.push(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -124,6 +124,7 @@ export interface Storage {
|
||||
active: boolean;
|
||||
// Added for deduplication in storage view
|
||||
nodes?: string[];
|
||||
nodeIds?: string[];
|
||||
nodeCount?: number;
|
||||
pbsNames?: string[];
|
||||
// ZFS pool status
|
||||
|
||||
@@ -177,22 +177,25 @@ func (c Container) ToFrontend() ContainerFrontend {
|
||||
// ToFrontend converts Storage to StorageFrontend
|
||||
func (s Storage) ToFrontend() StorageFrontend {
|
||||
return StorageFrontend{
|
||||
ID: s.ID,
|
||||
Storage: s.Name,
|
||||
Name: s.Name,
|
||||
Node: s.Node,
|
||||
Instance: s.Instance,
|
||||
Type: s.Type,
|
||||
Status: s.Status,
|
||||
Total: s.Total,
|
||||
Used: s.Used,
|
||||
Avail: s.Free,
|
||||
Free: s.Free,
|
||||
Usage: s.Usage,
|
||||
Content: s.Content,
|
||||
Shared: s.Shared,
|
||||
Enabled: s.Enabled,
|
||||
Active: s.Active,
|
||||
ID: s.ID,
|
||||
Storage: s.Name,
|
||||
Name: s.Name,
|
||||
Node: s.Node,
|
||||
Instance: s.Instance,
|
||||
Nodes: s.Nodes,
|
||||
NodeIDs: s.NodeIDs,
|
||||
NodeCount: s.NodeCount,
|
||||
Type: s.Type,
|
||||
Status: s.Status,
|
||||
Total: s.Total,
|
||||
Used: s.Used,
|
||||
Avail: s.Free,
|
||||
Free: s.Free,
|
||||
Usage: s.Usage,
|
||||
Content: s.Content,
|
||||
Shared: s.Shared,
|
||||
Enabled: s.Enabled,
|
||||
Active: s.Active,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-15
@@ -133,21 +133,24 @@ type Container struct {
|
||||
|
||||
// Storage represents a storage resource
|
||||
type Storage struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Node string `json:"node"`
|
||||
Instance string `json:"instance"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
Free int64 `json:"free"`
|
||||
Usage float64 `json:"usage"`
|
||||
Content string `json:"content"`
|
||||
Shared bool `json:"shared"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Active bool `json:"active"`
|
||||
ZFSPool *ZFSPool `json:"zfsPool,omitempty"` // ZFS pool details if this is ZFS storage
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Node string `json:"node"`
|
||||
Instance string `json:"instance"`
|
||||
Nodes []string `json:"nodes,omitempty"`
|
||||
NodeIDs []string `json:"nodeIds,omitempty"`
|
||||
NodeCount int `json:"nodeCount,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
Free int64 `json:"free"`
|
||||
Usage float64 `json:"usage"`
|
||||
Content string `json:"content"`
|
||||
Shared bool `json:"shared"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Active bool `json:"active"`
|
||||
ZFSPool *ZFSPool `json:"zfsPool,omitempty"` // ZFS pool details if this is ZFS storage
|
||||
}
|
||||
|
||||
// ZFSPool represents a ZFS pool with health and error information
|
||||
|
||||
@@ -93,22 +93,25 @@ type ContainerFrontend struct {
|
||||
|
||||
// StorageFrontend represents Storage with frontend-friendly field names
|
||||
type StorageFrontend struct {
|
||||
ID string `json:"id"`
|
||||
Storage string `json:"storage"` // Maps to Name
|
||||
Name string `json:"name"`
|
||||
Node string `json:"node"`
|
||||
Instance string `json:"instance"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
Avail int64 `json:"avail"` // Maps to Free
|
||||
Free int64 `json:"free"`
|
||||
Usage float64 `json:"usage"`
|
||||
Content string `json:"content"`
|
||||
Shared bool `json:"shared"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Active bool `json:"active"`
|
||||
ID string `json:"id"`
|
||||
Storage string `json:"storage"` // Maps to Name
|
||||
Name string `json:"name"`
|
||||
Node string `json:"node"`
|
||||
Instance string `json:"instance"`
|
||||
Nodes []string `json:"nodes,omitempty"`
|
||||
NodeIDs []string `json:"nodeIds,omitempty"`
|
||||
NodeCount int `json:"nodeCount,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
Avail int64 `json:"avail"` // Maps to Free
|
||||
Free int64 `json:"free"`
|
||||
Usage float64 `json:"usage"`
|
||||
Content string `json:"content"`
|
||||
Shared bool `json:"shared"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
// StateFrontend represents the state with frontend-friendly field names
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -966,7 +967,21 @@ func (m *Monitor) pollStorageWithNodesOptimized(ctx context.Context, instanceNam
|
||||
|
||||
// Collect results from all nodes
|
||||
var allStorage []models.Storage
|
||||
sharedStorageMap := make(map[string]models.Storage) // Map to keep best shared storage entry
|
||||
type sharedStorageAggregation struct {
|
||||
storage models.Storage
|
||||
nodes map[string]struct{}
|
||||
nodeIDs map[string]struct{}
|
||||
}
|
||||
sharedStorageMap := make(map[string]*sharedStorageAggregation) // Map to keep shared storage entries with node affiliations
|
||||
|
||||
toSortedSlice := func(set map[string]struct{}) []string {
|
||||
slice := make([]string, 0, len(set))
|
||||
for value := range set {
|
||||
slice = append(slice, value)
|
||||
}
|
||||
sort.Strings(slice)
|
||||
return slice
|
||||
}
|
||||
successfulNodes := 0
|
||||
failedNodes := 0
|
||||
|
||||
@@ -978,16 +993,33 @@ func (m *Monitor) pollStorageWithNodesOptimized(ctx context.Context, instanceNam
|
||||
polledNodes[result.node] = true // Mark this node as successfully polled
|
||||
for _, storage := range result.storage {
|
||||
if storage.Shared {
|
||||
// For shared storage, use just the storage name as key
|
||||
// This ensures consistent deduplication regardless of which node reports first
|
||||
// For shared storage, aggregate by storage name so we can retain the reporting nodes
|
||||
key := storage.Name
|
||||
nodeIdentifier := fmt.Sprintf("%s-%s", storage.Instance, storage.Node)
|
||||
|
||||
// Keep the entry with the most complete data (highest usage)
|
||||
// or the first one if all are equal
|
||||
if existing, exists := sharedStorageMap[key]; !exists || storage.Used > existing.Used {
|
||||
// Update the Node field to indicate it's shared across cluster
|
||||
storage.Node = "cluster"
|
||||
sharedStorageMap[key] = storage
|
||||
if entry, exists := sharedStorageMap[key]; exists {
|
||||
entry.nodes[storage.Node] = struct{}{}
|
||||
entry.nodeIDs[nodeIdentifier] = struct{}{}
|
||||
|
||||
// Prefer the entry with the most up-to-date utilization data
|
||||
if storage.Used > entry.storage.Used || (storage.Total > entry.storage.Total && storage.Used == entry.storage.Used) {
|
||||
entry.storage.Total = storage.Total
|
||||
entry.storage.Used = storage.Used
|
||||
entry.storage.Free = storage.Free
|
||||
entry.storage.Usage = storage.Usage
|
||||
entry.storage.ZFSPool = storage.ZFSPool
|
||||
entry.storage.Status = storage.Status
|
||||
entry.storage.Enabled = storage.Enabled
|
||||
entry.storage.Active = storage.Active
|
||||
entry.storage.Content = storage.Content
|
||||
entry.storage.Type = storage.Type
|
||||
}
|
||||
} else {
|
||||
sharedStorageMap[key] = &sharedStorageAggregation{
|
||||
storage: storage,
|
||||
nodes: map[string]struct{}{storage.Node: {}},
|
||||
nodeIDs: map[string]struct{}{nodeIdentifier: {}},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-shared storage goes directly to results
|
||||
@@ -998,8 +1030,12 @@ func (m *Monitor) pollStorageWithNodesOptimized(ctx context.Context, instanceNam
|
||||
}
|
||||
|
||||
// Add deduplicated shared storage to results
|
||||
for _, storage := range sharedStorageMap {
|
||||
allStorage = append(allStorage, storage)
|
||||
for _, entry := range sharedStorageMap {
|
||||
entry.storage.Node = "cluster"
|
||||
entry.storage.Nodes = toSortedSlice(entry.nodes)
|
||||
entry.storage.NodeIDs = toSortedSlice(entry.nodeIDs)
|
||||
entry.storage.NodeCount = len(entry.storage.Nodes)
|
||||
allStorage = append(allStorage, entry.storage)
|
||||
}
|
||||
|
||||
// Preserve existing storage data for nodes that weren't polled (offline or error)
|
||||
|
||||
Reference in New Issue
Block a user