Pulse/internal/alerts/unified_incidents.go
rcourtman faefe6edc8 Remove 198 unreachable Go functions
Dead-code sweep. Functions flagged unreachable by golang.org/x/tools/cmd/deadcode
and confirmed unused across pulse, pulse-enterprise, pulse-pro and pulse-mobile by
adversarial cross-repo verification. Cross-module reachability was checked
explicitly (only pkg/ exported symbols are importable by other modules; internal/
packages and _test.go files are not). go build, go vet and test-compile all pass.
2026-06-03 12:29:37 +01:00

794 lines
27 KiB
Go

package alerts
import (
"strconv"
"strings"
"time"
alertspecs "github.com/rcourtman/pulse-go-rewrite/internal/alerts/specs"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
const unifiedIncidentAlertPrefix = "unified-incident-"
// SyncUnifiedResourceIncidents mirrors canonical unified-resource incidents into
// the live alert manager so provider-native storage incidents participate in the
// standard alert pipeline.
func (m *Manager) SyncUnifiedResourceIncidents(resources []unifiedresources.Resource) {
if m == nil {
return
}
desired := make(map[string]*Alert)
resourcesByID := make(map[string]unifiedresources.Resource, len(resources))
for _, resource := range resources {
canonical := resource
canonical.ID = unifiedresources.CanonicalResourceID(resource.ID)
canonical.Type = unifiedresources.CanonicalResourceType(resource.Type)
if canonical.ParentID != nil {
parentID := unifiedresources.CanonicalResourceID(strings.TrimSpace(*canonical.ParentID))
if parentID == "" {
canonical.ParentID = nil
} else {
canonical.ParentID = &parentID
}
}
resourcesByID[canonical.ID] = canonical
}
canonicalSpecs := alertspecs.BuildUnifiedResourceAlertSpecs(resources)
providerSpecsByResource := make(map[string][]alertspecs.ResourceAlertSpec, len(canonicalSpecs))
for _, spec := range canonicalSpecs {
if spec.Kind != alertspecs.AlertSpecKindProviderIncident {
continue
}
providerSpecsByResource[spec.ResourceID] = append(providerSpecsByResource[spec.ResourceID], spec)
}
m.mu.RLock()
enabled := m.config.Enabled
disableAllStorage := m.config.DisableAllStorage
disableAllKubernetes := m.config.DisableAllKubernetes
disableAllTrueNAS := m.config.DisableAllTrueNAS
disableAllVMware := m.config.DisableAllVMware
overrides := m.config.Overrides
m.mu.RUnlock()
if enabled {
now := time.Now()
for _, spec := range canonicalSpecs {
if spec.Kind != alertspecs.AlertSpecKindProviderIncident {
continue
}
resource, ok := resourcesByID[spec.ResourceID]
if !ok {
continue
}
if !resourceSupportsUnifiedIncidentAlerts(resource) {
continue
}
if alertType, ok := unifiedAlertResourceType(resource); ok {
if disableAllKubernetes && isUnifiedKubernetesAlertType(alertType) {
continue
}
if disableAllTrueNAS && isUnifiedTrueNASAlertType(alertType) {
continue
}
if disableAllVMware && isUnifiedVMwareAlertType(alertType) {
continue
}
}
if disableAllStorage && (resource.Type == unifiedresources.ResourceTypeStorage || resource.Type == unifiedresources.ResourceTypePhysicalDisk) {
continue
}
if override, exists := overrides[spec.ResourceID]; exists && override.Disabled {
continue
}
if shouldSuppressCanonicalUnifiedIncidentSpec(spec, providerSpecsByResource) {
continue
}
incident, ok := incidentForProviderSpec(resource, spec)
if !ok {
continue
}
level, ok := alertLevelFromCanonicalSeverity(spec.Severity)
if !ok {
continue
}
alert := unifiedIncidentAlert(resource, incident, level, now)
applyCanonicalIdentity(alert, spec.ID, string(spec.Kind))
if len(spec.SuppressionKeys) > 0 {
if alert.Metadata == nil {
alert.Metadata = map[string]interface{}{}
}
alert.Metadata["canonicalSuppressionKeys"] = append([]string(nil), spec.SuppressionKeys...)
}
desired[canonicalTrackingKeyForSpec(spec, alert.ID)] = alert
}
}
m.mu.Lock()
defer m.mu.Unlock()
for storageKey, alert := range m.activeAlerts {
if !strings.HasPrefix(storageKey, unifiedresources.CanonicalResourceID(alert.ResourceID)+canonicalStateSeparator+"alertspec:provider-incident:") {
continue
}
if _, keep := desired[storageKey]; keep {
continue
}
m.clearAlertNoLock(storageKey)
}
for storageKey, alert := range desired {
if existing, exists := m.getActiveAlertNoLock(storageKey); exists && existing != nil {
existing.LastSeen = alert.LastSeen
existing.Level = alert.Level
existing.ResourceID = alert.ResourceID
existing.ResourceName = alert.ResourceName
existing.Node = alert.Node
existing.NodeDisplayName = m.resolveNodeDisplayName(alert.Instance, alert.Node)
existing.Instance = alert.Instance
existing.Message = alert.Message
existing.Metadata = alert.Metadata
applyCanonicalIdentity(existing, alert.CanonicalSpecID, alert.CanonicalKind)
m.setActiveAlertNoLock(storageKey, existing)
continue
}
m.preserveAlertState(storageKey, alert)
m.setActiveAlertNoLock(storageKey, alert)
m.recentAlerts[canonicalTrackingKeyForAlert(alert)] = alert
m.historyManager.AddAlert(*alert)
m.dispatchAlert(alert, false)
}
}
func shouldSuppressCanonicalUnifiedIncidentSpec(spec alertspecs.ResourceAlertSpec, providerSpecsByResource map[string][]alertspecs.ResourceAlertSpec) bool {
if len(spec.SuppressionKeys) == 0 {
return false
}
switch spec.ResourceType {
case unifiedresources.ResourceTypeAgent:
for _, childID := range spec.ChildResourceIDs {
for _, childSpec := range providerSpecsByResource[childID] {
if childSpec.ResourceType != unifiedresources.ResourceTypeStorage {
continue
}
if hasSharedSuppressionKey(spec.SuppressionKeys, childSpec.SuppressionKeys) {
return true
}
}
}
case unifiedresources.ResourceTypePhysicalDisk:
for _, parentSpec := range providerSpecsByResource[spec.ParentResourceID] {
if parentSpec.ResourceType != unifiedresources.ResourceTypeStorage {
continue
}
if hasSharedSuppressionKey(spec.SuppressionKeys, parentSpec.SuppressionKeys) {
return true
}
}
case unifiedresources.ResourceTypeStorage:
for _, parentSpec := range providerSpecsByResource[spec.ParentResourceID] {
if parentSpec.ResourceType != unifiedresources.ResourceTypePBS {
continue
}
if hasSharedSuppressionKey(spec.SuppressionKeys, parentSpec.SuppressionKeys) {
return true
}
}
}
return false
}
func hasSharedSuppressionKey(left, right []string) bool {
if len(left) == 0 || len(right) == 0 {
return false
}
keys := make(map[string]struct{}, len(left))
for _, key := range left {
trimmed := strings.TrimSpace(key)
if trimmed == "" {
continue
}
keys[trimmed] = struct{}{}
}
for _, key := range right {
if _, ok := keys[strings.TrimSpace(key)]; ok {
return true
}
}
return false
}
func incidentForProviderSpec(resource unifiedresources.Resource, spec alertspecs.ResourceAlertSpec) (unifiedresources.ResourceIncident, bool) {
if spec.ProviderIncident == nil {
return unifiedresources.ResourceIncident{}, false
}
for _, incident := range resource.Incidents {
if strings.TrimSpace(incident.Provider) != strings.TrimSpace(spec.ProviderIncident.Provider) {
continue
}
if len(spec.ProviderIncident.Codes) > 0 && !containsCanonicalString(spec.ProviderIncident.Codes, incident.Code) {
continue
}
if len(spec.ProviderIncident.NativeIDs) > 0 && !containsCanonicalString(spec.ProviderIncident.NativeIDs, incident.NativeID) {
continue
}
return incident, true
}
return unifiedresources.ResourceIncident{}, false
}
func containsCanonicalString(values []string, target string) bool {
target = strings.TrimSpace(target)
for _, value := range values {
if strings.TrimSpace(value) == target {
return true
}
}
return false
}
func resourceSupportsUnifiedIncidentAlerts(resource unifiedresources.Resource) bool {
switch resource.Type {
case unifiedresources.ResourceTypeStorage, unifiedresources.ResourceTypePhysicalDisk:
return true
case unifiedresources.ResourceTypeAgent:
return len(resource.Incidents) > 0
case unifiedresources.ResourceTypeVM:
return len(resource.Incidents) > 0
case unifiedresources.ResourceTypePBS:
return len(resource.Incidents) > 0
case unifiedresources.ResourceTypeNetwork:
return len(resource.Incidents) > 0
case unifiedresources.ResourceTypeK8sCluster,
unifiedresources.ResourceTypeK8sNode,
unifiedresources.ResourceTypeK8sNamespace,
unifiedresources.ResourceTypeK8sDeployment,
unifiedresources.ResourceTypePod:
return len(resource.Incidents) > 0
case unifiedresources.ResourceTypeNetworkEndpoint:
return len(resource.Incidents) > 0
default:
return false
}
}
func alertLevelFromCanonicalSeverity(level alertspecs.AlertSeverity) (AlertLevel, bool) {
switch level {
case alertspecs.AlertSeverityCritical:
return AlertLevelCritical, true
case alertspecs.AlertSeverityWarning:
return AlertLevelWarning, true
default:
return "", false
}
}
func unifiedIncidentAlert(resource unifiedresources.Resource, incident unifiedresources.ResourceIncident, level AlertLevel, now time.Time) *Alert {
resourceName := unifiedIncidentResourceName(resource)
nodeName := unifiedIncidentNode(resource)
instanceName := unifiedIncidentInstance(resource)
alertType := unifiedIncidentAlertType(resource, incident)
alertID := unifiedIncidentAlertID(resource, incident)
message := unifiedIncidentMessage(resource, incident)
startTime := incident.StartedAt
if startTime.IsZero() {
startTime = now
}
return &Alert{
ID: alertID,
Type: alertType,
Level: level,
ResourceID: resource.ID,
ResourceName: resourceName,
Node: nodeName,
Instance: instanceName,
Message: message,
Value: 0,
Threshold: 0,
StartTime: startTime,
LastSeen: now,
Metadata: unifiedIncidentMetadata(resource, incident, alertType),
}
}
func unifiedIncidentAlertID(resource unifiedresources.Resource, incident unifiedresources.ResourceIncident) string {
keyParts := []string{resource.ID, incident.Provider, incident.NativeID, incident.Code}
return unifiedIncidentAlertPrefix + sanitizeAlertKey(strings.Join(keyParts, "-"))
}
func unifiedIncidentResourceName(resource unifiedresources.Resource) string {
if resource.Canonical != nil && strings.TrimSpace(resource.Canonical.DisplayName) != "" {
return strings.TrimSpace(resource.Canonical.DisplayName)
}
if strings.TrimSpace(resource.Name) != "" {
return strings.TrimSpace(resource.Name)
}
return string(resource.Type)
}
func unifiedIncidentNode(resource unifiedresources.Resource) string {
if strings.TrimSpace(resource.ParentName) != "" {
return strings.TrimSpace(resource.ParentName)
}
if resource.Canonical != nil && strings.TrimSpace(resource.Canonical.Hostname) != "" {
return strings.TrimSpace(resource.Canonical.Hostname)
}
if resource.VMware != nil {
if strings.TrimSpace(resource.VMware.RuntimeHostName) != "" {
return strings.TrimSpace(resource.VMware.RuntimeHostName)
}
if strings.TrimSpace(resource.VMware.ClusterName) != "" {
return strings.TrimSpace(resource.VMware.ClusterName)
}
if strings.TrimSpace(resource.VMware.DatacenterName) != "" {
return strings.TrimSpace(resource.VMware.DatacenterName)
}
if strings.TrimSpace(resource.VMware.VCenterHost) != "" {
return strings.TrimSpace(resource.VMware.VCenterHost)
}
}
return unifiedIncidentResourceName(resource)
}
func unifiedIncidentInstance(resource unifiedresources.Resource) string {
if resource.Proxmox != nil && strings.TrimSpace(resource.Proxmox.Instance) != "" {
return strings.TrimSpace(resource.Proxmox.Instance)
}
switch {
case resource.TrueNAS != nil:
return "TrueNAS"
case resource.PBS != nil:
return "PBS"
case resource.VMware != nil:
if strings.TrimSpace(resource.VMware.ConnectionName) != "" {
return strings.TrimSpace(resource.VMware.ConnectionName)
}
if strings.TrimSpace(resource.VMware.VCenterHost) != "" {
return strings.TrimSpace(resource.VMware.VCenterHost)
}
return "vSphere"
case resource.Availability != nil:
return "Availability"
case resource.Storage != nil && strings.TrimSpace(resource.Storage.Platform) != "":
platform := strings.TrimSpace(resource.Storage.Platform)
if platform == "" {
return ""
}
lower := strings.ToLower(platform)
return strings.ToUpper(lower[:1]) + lower[1:]
default:
return ""
}
}
func unifiedIncidentAlertType(resource unifiedresources.Resource, incident unifiedresources.ResourceIncident) string {
code := strings.ToLower(strings.TrimSpace(incident.Code))
switch resource.Type {
case unifiedresources.ResourceTypePhysicalDisk:
if strings.Contains(code, "wear") {
return "disk-wearout"
}
return "disk-health"
case unifiedresources.ResourceTypePBS:
return "backup-posture-incident"
case unifiedresources.ResourceTypeStorage:
if unifiedresources.IsBackupStorageResource(resource.Storage) {
return "backup-storage-incident"
}
if resource.Storage != nil && resource.Storage.IsZFS {
return "zfs-pool-state"
}
return "storage-incident"
default:
return "resource-incident"
}
}
func unifiedIncidentMessage(resource unifiedresources.Resource, incident unifiedresources.ResourceIncident) string {
if resource.Type == unifiedresources.ResourceTypePBS {
return unifiedIncidentPBSMessage(resource, incident)
}
base := strings.TrimSpace(incident.Summary)
if base == "" {
base = strings.TrimSpace(incident.Code)
}
if resource.Storage == nil || resource.Storage.ConsumerCount <= 0 {
return base
}
summary := unifiedIncidentConsumerSummary(resource.Storage)
if summary == "" {
return base
}
if base == "" {
return summary
}
return base + ". " + summary
}
func unifiedIncidentPBSMessage(resource unifiedresources.Resource, incident unifiedresources.ResourceIncident) string {
base := unifiedIncidentPBSBaseMessage(resource, incident)
summary := unifiedIncidentPBSDatastoreSummary(resource, incident.Code)
if summary == "" {
return base
}
if base == "" {
return summary
}
return base + ". " + summary
}
func unifiedIncidentPBSBaseMessage(resource unifiedresources.Resource, incident unifiedresources.ResourceIncident) string {
name := unifiedIncidentResourceName(resource)
switch strings.TrimSpace(incident.Code) {
case "capacity_runway_low":
return "Backup server " + name + " has datastore capacity risk"
case "pbs_datastore_error":
return "Backup server " + name + " has datastore errors"
case "pbs_datastore_state":
return "Backup server " + name + " has degraded datastore availability"
default:
base := strings.TrimSpace(incident.Summary)
if base != "" {
return base
}
return "Backup server " + name + " has degraded storage posture"
}
}
func unifiedIncidentPBSDatastoreSummary(resource unifiedresources.Resource, incidentCode string) string {
if resource.PBS != nil && strings.TrimSpace(resource.PBS.AffectedDatastoreSummary) != "" {
return strings.TrimSpace(resource.PBS.AffectedDatastoreSummary)
}
if resource.PBS == nil || len(resource.PBS.Datastores) == 0 {
return ""
}
names := affectedPBSDatastoreNames(resource.PBS.Datastores, incidentCode)
if len(names) == 0 {
return ""
}
label := "backup datastore"
if len(names) != 1 {
label = "backup datastores"
}
return "Affects " + intLabel(len(names)) + " " + label + ": " + strings.Join(names, ", ")
}
func affectedPBSDatastoreNames(datastores []unifiedresources.PBSDatastoreMeta, incidentCode string) []string {
names := make([]string, 0, len(datastores))
for _, datastore := range datastores {
if !pbsDatastoreMatchesIncident(datastore, incidentCode) {
continue
}
name := strings.TrimSpace(datastore.Name)
if name == "" {
continue
}
names = append(names, name)
}
return names
}
func pbsDatastoreMatchesIncident(datastore unifiedresources.PBSDatastoreMeta, incidentCode string) bool {
switch strings.TrimSpace(incidentCode) {
case "capacity_runway_low":
usage := datastore.UsagePercent
if usage <= 0 && datastore.Total > 0 {
usage = (float64(datastore.Used) / float64(datastore.Total)) * 100
}
return usage >= 90
case "pbs_datastore_error":
return strings.TrimSpace(datastore.Error) != ""
case "pbs_datastore_state":
switch strings.ToUpper(strings.TrimSpace(datastore.Status)) {
case "OFFLINE", "UNAVAILABLE", "ERROR", "FAILED", "DEGRADED", "WARN", "WARNING", "READ_ONLY":
return true
default:
return false
}
default:
return strings.TrimSpace(datastore.Error) != "" || pbsDatastoreMatchesIncident(datastore, "pbs_datastore_state") || pbsDatastoreMatchesIncident(datastore, "capacity_runway_low")
}
}
func unifiedIncidentConsumerSummary(storage *unifiedresources.StorageMeta) string {
if storage == nil || storage.ConsumerCount <= 0 {
return ""
}
if summary := strings.TrimSpace(storage.ConsumerImpactSummary); summary != "" {
return summary
}
return unifiedresources.StorageConsumerImpactSummary(storage)
}
func unifiedIncidentBackupConsumerSummary(storage *unifiedresources.StorageMeta) string {
if storage == nil || storage.ConsumerCount <= 0 {
return ""
}
names := make([]string, 0, len(storage.TopConsumers))
for _, consumer := range storage.TopConsumers {
name := strings.TrimSpace(consumer.Name)
if name == "" {
continue
}
names = append(names, name)
if len(names) == 3 {
break
}
}
workloadLabel := "protected workload"
if storage.ConsumerCount != 1 {
workloadLabel = "protected workloads"
}
if len(names) == 0 {
return "Puts backups for " + intLabel(storage.ConsumerCount) + " " + workloadLabel + " at risk"
}
if remaining := storage.ConsumerCount - len(names); remaining > 0 {
return "Puts backups for " + intLabel(storage.ConsumerCount) + " " + workloadLabel + " at risk: " + strings.Join(names, ", ") + ", and " + intLabel(remaining) + " more"
}
return "Puts backups for " + intLabel(storage.ConsumerCount) + " " + workloadLabel + " at risk: " + strings.Join(names, ", ")
}
func unifiedIncidentMetadata(resource unifiedresources.Resource, incident unifiedresources.ResourceIncident, alertType string) map[string]interface{} {
metadata := map[string]interface{}{
"metric": alertType,
"resourceType": string(resource.Type),
"incidentProvider": incident.Provider,
"incidentCode": incident.Code,
"incidentCategory": unifiedresources.IncidentCategoryForResource(&resource, incident),
"incidentLabel": unifiedresources.IncidentLabelForResource(&resource, incident, unifiedresources.IncidentCategoryForResource(&resource, incident)),
"incidentPriority": unifiedresources.IncidentPriorityForResource(&resource, incident, unifiedresources.IncidentCategoryForResource(&resource, incident)),
"incidentNativeID": incident.NativeID,
"incidentSource": incident.Source,
}
if urgency, action := unifiedresources.IncidentActionForResource(&resource, incident, unifiedresources.IncidentCategoryForResource(&resource, incident)); urgency != "" {
metadata["incidentUrgency"] = urgency
if action != "" {
metadata["incidentAction"] = action
}
}
if impact := unifiedresources.IncidentImpactSummaryForResource(&resource); impact != "" {
metadata["incidentImpactSummary"] = impact
}
if !incident.StartedAt.IsZero() {
metadata["incidentStartedAt"] = incident.StartedAt.UTC().Format(time.RFC3339)
}
if len(resource.Sources) > 0 {
sources := make([]string, 0, len(resource.Sources))
for _, source := range resource.Sources {
sources = append(sources, string(source))
}
metadata["resourceSources"] = sources
}
if resource.Storage != nil {
if platform := strings.TrimSpace(resource.Storage.Platform); platform != "" {
metadata["storagePlatform"] = platform
}
if topology := strings.TrimSpace(resource.Storage.Topology); topology != "" {
metadata["storageTopology"] = topology
}
if protection := strings.TrimSpace(resource.Storage.Protection); protection != "" {
metadata["storageProtection"] = protection
}
if resource.Storage.Risk != nil {
riskCodes, protectionReduced, rebuildInProgress, protectionSummary, rebuildSummary := storageRiskAlertSemantics(resource.Storage.Risk)
if len(riskCodes) > 0 {
metadata["storageRiskCodes"] = riskCodes
}
if protectionReduced {
metadata["protectionReduced"] = true
}
if rebuildInProgress {
metadata["rebuildInProgress"] = true
}
if protectionSummary != "" {
metadata["protectionSummary"] = protectionSummary
}
if rebuildSummary != "" {
metadata["rebuildSummary"] = rebuildSummary
}
}
if resource.Storage.ConsumerCount > 0 {
metadata["consumerCount"] = resource.Storage.ConsumerCount
metadata["consumerImpactSummary"] = unifiedIncidentConsumerSummary(resource.Storage)
}
if unifiedresources.IsBackupStorageResource(resource.Storage) {
metadata["backupTarget"] = true
if resource.Storage.ConsumerCount > 0 {
metadata["protectedWorkloadCount"] = resource.Storage.ConsumerCount
metadata["protectedWorkloadSummary"] = unifiedIncidentBackupConsumerSummary(resource.Storage)
}
}
if len(resource.Storage.ConsumerTypes) > 0 {
metadata["consumerTypes"] = append([]string(nil), resource.Storage.ConsumerTypes...)
if unifiedresources.IsBackupStorageResource(resource.Storage) {
metadata["protectedWorkloadTypes"] = append([]string(nil), resource.Storage.ConsumerTypes...)
}
}
if len(resource.Storage.TopConsumers) > 0 {
names := make([]string, 0, len(resource.Storage.TopConsumers))
topConsumers := make([]map[string]interface{}, 0, len(resource.Storage.TopConsumers))
for _, consumer := range resource.Storage.TopConsumers {
if name := strings.TrimSpace(consumer.Name); name != "" {
names = append(names, name)
}
topConsumers = append(topConsumers, map[string]interface{}{
"resourceId": consumer.ResourceID,
"resourceType": string(consumer.ResourceType),
"name": consumer.Name,
"diskCount": consumer.DiskCount,
})
}
if len(names) > 0 {
metadata["topConsumerNames"] = names
if unifiedresources.IsBackupStorageResource(resource.Storage) {
metadata["protectedWorkloadNames"] = append([]string(nil), names...)
}
}
metadata["topConsumers"] = topConsumers
}
}
if resource.PhysicalDisk != nil {
if devPath := strings.TrimSpace(resource.PhysicalDisk.DevPath); devPath != "" {
metadata["device"] = devPath
}
if model := strings.TrimSpace(resource.PhysicalDisk.Model); model != "" {
metadata["model"] = model
}
if serial := strings.TrimSpace(resource.PhysicalDisk.Serial); serial != "" {
metadata["serial"] = serial
}
if wwn := strings.TrimSpace(resource.PhysicalDisk.WWN); wwn != "" {
metadata["wwn"] = wwn
}
}
if resource.TrueNAS != nil {
metadata["storagePlatform"] = "truenas"
if hostname := strings.TrimSpace(resource.TrueNAS.Hostname); hostname != "" {
metadata["hostname"] = hostname
}
}
if resource.PBS != nil {
metadata["storagePlatform"] = "pbs"
metadata["backupServer"] = true
metadata["datastoreCount"] = resource.PBS.DatastoreCount
if hostname := strings.TrimSpace(resource.PBS.Hostname); hostname != "" {
metadata["hostname"] = hostname
}
if resource.PBS.StorageRisk != nil {
riskCodes, protectionReduced, rebuildInProgress, protectionSummary, rebuildSummary := storageRiskAlertSemantics(resource.PBS.StorageRisk)
if len(riskCodes) > 0 {
metadata["storageRiskCodes"] = riskCodes
}
if protectionReduced {
metadata["protectionReduced"] = true
}
if rebuildInProgress {
metadata["rebuildInProgress"] = true
}
if protectionSummary != "" {
metadata["protectionSummary"] = protectionSummary
}
if rebuildSummary != "" {
metadata["rebuildSummary"] = rebuildSummary
}
}
if names := affectedPBSDatastoreNames(resource.PBS.Datastores, incident.Code); len(names) > 0 {
metadata["affectedDatastoreCount"] = len(names)
metadata["affectedDatastores"] = append([]string(nil), names...)
metadata["backupServerPostureSummary"] = "Affects " + intLabel(len(names)) + " backup datastore"
if len(names) != 1 {
metadata["backupServerPostureSummary"] = "Affects " + intLabel(len(names)) + " backup datastores"
}
}
if resource.PBS.ProtectedWorkloadCount > 0 {
metadata["consumerCount"] = resource.PBS.ProtectedWorkloadCount
metadata["protectedWorkloadCount"] = resource.PBS.ProtectedWorkloadCount
metadata["consumerImpactSummary"] = unifiedIncidentPBSProtectedWorkloadSummary(resource.PBS)
metadata["protectedWorkloadSummary"] = unifiedIncidentPBSProtectedWorkloadSummary(resource.PBS)
}
if len(resource.PBS.ProtectedWorkloadTypes) > 0 {
metadata["consumerTypes"] = append([]string(nil), resource.PBS.ProtectedWorkloadTypes...)
metadata["protectedWorkloadTypes"] = append([]string(nil), resource.PBS.ProtectedWorkloadTypes...)
}
if len(resource.PBS.ProtectedWorkloadNames) > 0 {
metadata["topConsumerNames"] = append([]string(nil), resource.PBS.ProtectedWorkloadNames...)
metadata["protectedWorkloadNames"] = append([]string(nil), resource.PBS.ProtectedWorkloadNames...)
}
}
if resource.VMware != nil {
if connectionID := strings.TrimSpace(resource.VMware.ConnectionID); connectionID != "" {
metadata["vmwareConnectionId"] = connectionID
}
if connectionName := strings.TrimSpace(resource.VMware.ConnectionName); connectionName != "" {
metadata["vmwareConnectionName"] = connectionName
}
if vcenterHost := strings.TrimSpace(resource.VMware.VCenterHost); vcenterHost != "" {
metadata["vmwareVcenterHost"] = vcenterHost
}
if entityType := strings.TrimSpace(resource.VMware.EntityType); entityType != "" {
metadata["vmwareEntityType"] = entityType
}
if managedObjectID := strings.TrimSpace(resource.VMware.ManagedObjectID); managedObjectID != "" {
metadata["vmwareManagedObjectId"] = managedObjectID
}
if overallStatus := strings.TrimSpace(resource.VMware.OverallStatus); overallStatus != "" {
metadata["vmwareOverallStatus"] = overallStatus
}
if alarmCount := resource.VMware.ActiveAlarmCount; alarmCount > 0 {
metadata["vmwareActiveAlarmCount"] = alarmCount
}
if summary := strings.TrimSpace(resource.VMware.ActiveAlarmSummary); summary != "" {
metadata["vmwareActiveAlarmSummary"] = summary
}
if taskCount := resource.VMware.RecentTaskCount; taskCount > 0 {
metadata["vmwareRecentTaskCount"] = taskCount
}
if summary := strings.TrimSpace(resource.VMware.RecentTaskSummary); summary != "" {
metadata["vmwareRecentTaskSummary"] = summary
}
if snapshotCount := resource.VMware.SnapshotCount; snapshotCount > 0 {
metadata["vmwareSnapshotCount"] = snapshotCount
}
}
return metadata
}
func intLabel(value int) string {
return strconv.Itoa(value)
}
func unifiedIncidentPBSProtectedWorkloadSummary(pbs *unifiedresources.PBSData) string {
if pbs == nil || pbs.ProtectedWorkloadCount <= 0 {
return ""
}
if strings.TrimSpace(pbs.ProtectedWorkloadSummary) != "" {
return strings.TrimSpace(pbs.ProtectedWorkloadSummary)
}
workloadLabel := "protected workload"
if pbs.ProtectedWorkloadCount != 1 {
workloadLabel = "protected workloads"
}
if len(pbs.ProtectedWorkloadNames) == 0 {
return "Puts backups for " + intLabel(pbs.ProtectedWorkloadCount) + " " + workloadLabel + " at risk"
}
names := append([]string(nil), pbs.ProtectedWorkloadNames...)
if len(names) > 3 {
names = names[:3]
}
if remaining := pbs.ProtectedWorkloadCount - len(names); remaining > 0 {
return "Puts backups for " + intLabel(pbs.ProtectedWorkloadCount) + " " + workloadLabel + " at risk: " + strings.Join(names, ", ") + ", and " + intLabel(remaining) + " more"
}
return "Puts backups for " + intLabel(pbs.ProtectedWorkloadCount) + " " + workloadLabel + " at risk: " + strings.Join(names, ", ")
}
func storageRiskAlertSemantics(risk *unifiedresources.StorageRisk) ([]string, bool, bool, string, string) {
return unifiedresources.StorageRiskSemantics(risk)
}