mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Dedupe alerts PMG queue checks, vmware clones, licensing and proxmox client clones
Clears the dupl pairs outside internal/api: - internal/alerts/pmg.go: the total/deferred/hold per-node queue checks share evaluatePMGNodeQueueAlert; the historical short-circuit (a below-threshold clear or invalid spec skips the node's remaining checks) is preserved via the helper's skip-node return. - internal/vmware/provider.go: the cloneInventory* family rides generic cloneSliceWith / cloneShallowSlice helpers instead of fourteen copies of the same nil/make/loop scaffold. - internal/vmware/client.go + client_signals.go: the byte-identical Automation and VI/JSON fetchers delegate to one getSessionScopedJSON. - internal/vmware/fixtures.go: added to the dupl exclude list in .golangci.yml — literal mock fixture catalogs are the same category as the existing internal/mock/ exclusion. - pkg/licensing/license_server_client.go: Activate and ExchangeLegacyLicense share postActivationRequest (idempotent POST + shared activation response decode). - pkg/proxmox/client.go: LXC/VM RRD fetches share getGuestRRDData. - pkg/pulsecli/actions.go is intentionally left for the api-contracts slice. Full test suites pass for internal/alerts, internal/vmware, pkg/licensing, pkg/proxmox.
This commit is contained in:
parent
8439ce6e6b
commit
9f722a442a
7 changed files with 199 additions and 376 deletions
|
|
@ -19,6 +19,10 @@ issues:
|
|||
path: "_test\\.go$"
|
||||
- linters: [dupl]
|
||||
path: "internal/mock/"
|
||||
# Platform mock fixture catalogs outside internal/mock/ are the same
|
||||
# category: explicit literal fixture records that deliberately share shape.
|
||||
- linters: [dupl]
|
||||
path: "internal/vmware/fixtures\\.go$"
|
||||
linters-settings:
|
||||
gofmt:
|
||||
simplify: true
|
||||
|
|
|
|||
|
|
@ -398,6 +398,61 @@ func (m *Manager) checkPMGOldestMessage(pmg models.PMGInstance, defaults PMGThre
|
|||
}
|
||||
}
|
||||
|
||||
// evaluatePMGNodeQueueAlert runs one per-node PMG queue threshold check
|
||||
// (total / deferred / hold share this shape). messageNoun is the operator
|
||||
// phrasing ("total messages in queue", "deferred messages", "held messages")
|
||||
// and specLabel feeds the invalid-spec log line. It returns true when the
|
||||
// caller must skip the node's remaining checks — either the below-threshold
|
||||
// clear or an invalid spec — preserving the historical per-node
|
||||
// short-circuit semantics.
|
||||
func (m *Manager) evaluatePMGNodeQueueAlert(pmg models.PMGInstance, nodeName, metric, specLabel, messageNoun string, observed, scaledWarn, scaledCrit, median int) bool {
|
||||
if scaledWarn <= 0 && scaledCrit <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
alertID := fmt.Sprintf("%s-%s-%s", pmg.ID, nodeName, metric)
|
||||
if (scaledCrit <= 0 || observed < scaledCrit) && (scaledWarn <= 0 || observed < scaledWarn) {
|
||||
m.clearAlert(buildCanonicalStateID(pmg.ID, alertID))
|
||||
return true
|
||||
}
|
||||
|
||||
// Add outlier indicator to message if applicable
|
||||
isOutlier := isQueueOutlier(observed, median)
|
||||
outlierNote := ""
|
||||
if isOutlier {
|
||||
outlierNote = ", outlier"
|
||||
}
|
||||
|
||||
spec, err := buildCanonicalSeverityThresholdSpec(alertID, pmg.ID, pmg.Name, unifiedresources.ResourceTypePMG, metric, float64(scaledWarn), float64(scaledCrit), false)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("pmg", pmg.Name).Str("alertID", alertID).Msg("Skipping invalid canonical PMG node " + specLabel + " spec")
|
||||
return true
|
||||
}
|
||||
_, _ = m.evaluateCanonicalStatefulAlert(canonicalStatefulAlertParams{
|
||||
Spec: spec,
|
||||
Evidence: alertspecs.AlertEvidence{
|
||||
ObservedAt: time.Now(),
|
||||
SeverityThreshold: &alertspecs.SeverityThresholdEvidence{
|
||||
Metric: metric,
|
||||
Direction: alertspecs.ThresholdDirectionAbove,
|
||||
Observed: float64(observed),
|
||||
},
|
||||
},
|
||||
AlertID: alertID,
|
||||
AlertType: metric,
|
||||
ResourceID: pmg.ID,
|
||||
ResourceName: pmg.Name,
|
||||
Node: nodeName,
|
||||
Instance: pmg.Name,
|
||||
MessageBuilder: func(result alertspecs.EvaluationResult) (string, float64, float64) {
|
||||
currentThreshold := thresholdForCanonicalSeverity(result.State.Severity, float64(scaledWarn), float64(scaledCrit))
|
||||
return fmt.Sprintf("PMG node %s on %s has %d %s (threshold: %d%s)", nodeName, pmg.Name, observed, messageNoun, int(currentThreshold), outlierNote), float64(observed), currentThreshold
|
||||
},
|
||||
DispatchAsync: true,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// checkPMGNodeQueues checks individual PMG node queue health
|
||||
// Uses scaled thresholds (60% warn, 80% crit) and outlier detection
|
||||
func (m *Manager) checkPMGNodeQueues(pmg models.PMGInstance, defaults PMGThresholdConfig) {
|
||||
|
|
@ -439,137 +494,18 @@ func (m *Manager) checkPMGNodeQueues(pmg models.PMGInstance, defaults PMGThresho
|
|||
}
|
||||
|
||||
// Check total queue - always check thresholds
|
||||
if scaledQueueWarn > 0 || scaledQueueCrit > 0 {
|
||||
total := node.QueueStatus.Total
|
||||
alertID := fmt.Sprintf("%s-%s-queue-total", pmg.ID, node.Name)
|
||||
if (scaledQueueCrit <= 0 || total < scaledQueueCrit) && (scaledQueueWarn <= 0 || total < scaledQueueWarn) {
|
||||
m.clearAlert(buildCanonicalStateID(pmg.ID, alertID))
|
||||
continue
|
||||
}
|
||||
|
||||
isOutlier := isQueueOutlier(total, medianTotal)
|
||||
outlierNote := ""
|
||||
if isOutlier {
|
||||
outlierNote = ", outlier"
|
||||
}
|
||||
|
||||
spec, err := buildCanonicalSeverityThresholdSpec(alertID, pmg.ID, pmg.Name, unifiedresources.ResourceTypePMG, "queue-total", float64(scaledQueueWarn), float64(scaledQueueCrit), false)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("pmg", pmg.Name).Str("alertID", alertID).Msg("Skipping invalid canonical PMG node queue spec")
|
||||
continue
|
||||
}
|
||||
_, _ = m.evaluateCanonicalStatefulAlert(canonicalStatefulAlertParams{
|
||||
Spec: spec,
|
||||
Evidence: alertspecs.AlertEvidence{
|
||||
ObservedAt: time.Now(),
|
||||
SeverityThreshold: &alertspecs.SeverityThresholdEvidence{
|
||||
Metric: "queue-total",
|
||||
Direction: alertspecs.ThresholdDirectionAbove,
|
||||
Observed: float64(total),
|
||||
},
|
||||
},
|
||||
AlertID: alertID,
|
||||
AlertType: "queue-total",
|
||||
ResourceID: pmg.ID,
|
||||
ResourceName: pmg.Name,
|
||||
Node: node.Name,
|
||||
Instance: pmg.Name,
|
||||
MessageBuilder: func(result alertspecs.EvaluationResult) (string, float64, float64) {
|
||||
currentThreshold := thresholdForCanonicalSeverity(result.State.Severity, float64(scaledQueueWarn), float64(scaledQueueCrit))
|
||||
return fmt.Sprintf("PMG node %s on %s has %d total messages in queue (threshold: %d%s)", node.Name, pmg.Name, total, int(currentThreshold), outlierNote), float64(total), currentThreshold
|
||||
},
|
||||
DispatchAsync: true,
|
||||
})
|
||||
if m.evaluatePMGNodeQueueAlert(pmg, node.Name, "queue-total", "queue", "total messages in queue", node.QueueStatus.Total, scaledQueueWarn, scaledQueueCrit, medianTotal) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check deferred queue - always check thresholds
|
||||
if scaledDeferredWarn > 0 || scaledDeferredCrit > 0 {
|
||||
deferred := node.QueueStatus.Deferred
|
||||
alertID := fmt.Sprintf("%s-%s-queue-deferred", pmg.ID, node.Name)
|
||||
if (scaledDeferredCrit <= 0 || deferred < scaledDeferredCrit) && (scaledDeferredWarn <= 0 || deferred < scaledDeferredWarn) {
|
||||
m.clearAlert(buildCanonicalStateID(pmg.ID, alertID))
|
||||
continue
|
||||
}
|
||||
|
||||
// Add outlier indicator to message if applicable
|
||||
isOutlier := isQueueOutlier(deferred, medianDeferred)
|
||||
outlierNote := ""
|
||||
if isOutlier {
|
||||
outlierNote = ", outlier"
|
||||
}
|
||||
|
||||
spec, err := buildCanonicalSeverityThresholdSpec(alertID, pmg.ID, pmg.Name, unifiedresources.ResourceTypePMG, "queue-deferred", float64(scaledDeferredWarn), float64(scaledDeferredCrit), false)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("pmg", pmg.Name).Str("alertID", alertID).Msg("Skipping invalid canonical PMG node deferred queue spec")
|
||||
continue
|
||||
}
|
||||
_, _ = m.evaluateCanonicalStatefulAlert(canonicalStatefulAlertParams{
|
||||
Spec: spec,
|
||||
Evidence: alertspecs.AlertEvidence{
|
||||
ObservedAt: time.Now(),
|
||||
SeverityThreshold: &alertspecs.SeverityThresholdEvidence{
|
||||
Metric: "queue-deferred",
|
||||
Direction: alertspecs.ThresholdDirectionAbove,
|
||||
Observed: float64(deferred),
|
||||
},
|
||||
},
|
||||
AlertID: alertID,
|
||||
AlertType: "queue-deferred",
|
||||
ResourceID: pmg.ID,
|
||||
ResourceName: pmg.Name,
|
||||
Node: node.Name,
|
||||
Instance: pmg.Name,
|
||||
MessageBuilder: func(result alertspecs.EvaluationResult) (string, float64, float64) {
|
||||
currentThreshold := thresholdForCanonicalSeverity(result.State.Severity, float64(scaledDeferredWarn), float64(scaledDeferredCrit))
|
||||
return fmt.Sprintf("PMG node %s on %s has %d deferred messages (threshold: %d%s)", node.Name, pmg.Name, deferred, int(currentThreshold), outlierNote), float64(deferred), currentThreshold
|
||||
},
|
||||
DispatchAsync: true,
|
||||
})
|
||||
if m.evaluatePMGNodeQueueAlert(pmg, node.Name, "queue-deferred", "deferred queue", "deferred messages", node.QueueStatus.Deferred, scaledDeferredWarn, scaledDeferredCrit, medianDeferred) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check hold queue - always check thresholds
|
||||
if scaledHoldWarn > 0 || scaledHoldCrit > 0 {
|
||||
hold := node.QueueStatus.Hold
|
||||
alertID := fmt.Sprintf("%s-%s-queue-hold", pmg.ID, node.Name)
|
||||
if (scaledHoldCrit <= 0 || hold < scaledHoldCrit) && (scaledHoldWarn <= 0 || hold < scaledHoldWarn) {
|
||||
m.clearAlert(buildCanonicalStateID(pmg.ID, alertID))
|
||||
continue
|
||||
}
|
||||
|
||||
// Add outlier indicator to message if applicable
|
||||
isOutlier := isQueueOutlier(hold, medianHold)
|
||||
outlierNote := ""
|
||||
if isOutlier {
|
||||
outlierNote = ", outlier"
|
||||
}
|
||||
|
||||
spec, err := buildCanonicalSeverityThresholdSpec(alertID, pmg.ID, pmg.Name, unifiedresources.ResourceTypePMG, "queue-hold", float64(scaledHoldWarn), float64(scaledHoldCrit), false)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("pmg", pmg.Name).Str("alertID", alertID).Msg("Skipping invalid canonical PMG node hold queue spec")
|
||||
continue
|
||||
}
|
||||
_, _ = m.evaluateCanonicalStatefulAlert(canonicalStatefulAlertParams{
|
||||
Spec: spec,
|
||||
Evidence: alertspecs.AlertEvidence{
|
||||
ObservedAt: time.Now(),
|
||||
SeverityThreshold: &alertspecs.SeverityThresholdEvidence{
|
||||
Metric: "queue-hold",
|
||||
Direction: alertspecs.ThresholdDirectionAbove,
|
||||
Observed: float64(hold),
|
||||
},
|
||||
},
|
||||
AlertID: alertID,
|
||||
AlertType: "queue-hold",
|
||||
ResourceID: pmg.ID,
|
||||
ResourceName: pmg.Name,
|
||||
Node: node.Name,
|
||||
Instance: pmg.Name,
|
||||
MessageBuilder: func(result alertspecs.EvaluationResult) (string, float64, float64) {
|
||||
currentThreshold := thresholdForCanonicalSeverity(result.State.Severity, float64(scaledHoldWarn), float64(scaledHoldCrit))
|
||||
return fmt.Sprintf("PMG node %s on %s has %d held messages (threshold: %d%s)", node.Name, pmg.Name, hold, int(currentThreshold), outlierNote), float64(hold), currentThreshold
|
||||
},
|
||||
DispatchAsync: true,
|
||||
})
|
||||
if m.evaluatePMGNodeQueueAlert(pmg, node.Name, "queue-hold", "hold queue", "held messages", node.QueueStatus.Hold, scaledHoldWarn, scaledHoldCrit, medianHold) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check oldest message age per node
|
||||
|
|
|
|||
|
|
@ -327,7 +327,10 @@ func (c *Client) listAutomationResources(
|
|||
return c.getAutomationJSON(ctx, sessionID, path, label, target)
|
||||
}
|
||||
|
||||
func (c *Client) getAutomationJSON(
|
||||
// getSessionScopedJSON fetches a session-authenticated vCenter JSON endpoint
|
||||
// (shared by the Automation API and VI/JSON API paths) into target with the
|
||||
// inventory response size cap and shared error classification.
|
||||
func (c *Client) getSessionScopedJSON(
|
||||
ctx context.Context,
|
||||
sessionID string,
|
||||
path string,
|
||||
|
|
@ -360,6 +363,16 @@ func (c *Client) getAutomationJSON(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) getAutomationJSON(
|
||||
ctx context.Context,
|
||||
sessionID string,
|
||||
path string,
|
||||
label string,
|
||||
target any,
|
||||
) error {
|
||||
return c.getSessionScopedJSON(ctx, sessionID, path, label, target)
|
||||
}
|
||||
|
||||
func (c *Client) resolveVIJSONRelease(ctx context.Context) (string, viJSONServiceContentRefs, error) {
|
||||
var lastErr error
|
||||
for _, release := range supportedVIJSONReleases {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ package vmware
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -588,30 +585,7 @@ func (c *Client) resolveAlarmName(ctx context.Context, release, sessionID, alarm
|
|||
}
|
||||
|
||||
func (c *Client) getVIJSONJSON(ctx context.Context, sessionID, path, label string, target any) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL.String()+path, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build %s request: %w", label, err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("vmware-api-session-id", sessionID)
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return classifyTransportError(label, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, readErr := io.ReadAll(io.LimitReader(resp.Body, inventoryResponseLimitByte))
|
||||
if readErr != nil {
|
||||
return fmt.Errorf("read %s response: %w", label, readErr)
|
||||
}
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
default:
|
||||
return classifyReadStatusCode(label, resp.StatusCode)
|
||||
}
|
||||
if err := json.Unmarshal(body, target); err != nil {
|
||||
return &ConnectionError{Category: "endpoint", Message: fmt.Sprintf("VMware %s response was not valid JSON", label)}
|
||||
}
|
||||
return nil
|
||||
return c.getSessionScopedJSON(ctx, sessionID, path, label, target)
|
||||
}
|
||||
|
||||
func isVIJSONNotFound(err error) bool {
|
||||
|
|
|
|||
|
|
@ -828,171 +828,132 @@ func cloneInventorySnapshot(in *InventorySnapshot) *InventorySnapshot {
|
|||
return &out
|
||||
}
|
||||
|
||||
func cloneInventoryHosts(in []InventoryHost) []InventoryHost {
|
||||
// cloneSliceWith deep-copies a slice, applying fix to each copied element so
|
||||
// it can re-clone its pointer- and slice-typed fields.
|
||||
func cloneSliceWith[T any](in []T, fix func(*T)) []T {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryHost, len(in))
|
||||
out := make([]T, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].ClusterHAEnabled = cloneBoolPointer(in[i].ClusterHAEnabled)
|
||||
out[i].ClusterDRSEnabled = cloneBoolPointer(in[i].ClusterDRSEnabled)
|
||||
out[i].DatastoreIDs = cloneStringSlice(in[i].DatastoreIDs)
|
||||
out[i].DatastoreNames = cloneStringSlice(in[i].DatastoreNames)
|
||||
out[i].TriggeredAlarms = cloneInventoryAlarms(in[i].TriggeredAlarms)
|
||||
out[i].RecentTasks = cloneInventoryTasks(in[i].RecentTasks)
|
||||
out[i].RecentEvents = cloneInventoryEvents(in[i].RecentEvents)
|
||||
out[i].Metrics = cloneInventoryMetrics(in[i].Metrics)
|
||||
fix(&out[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// cloneShallowSlice copies a slice whose elements carry no shared references.
|
||||
func cloneShallowSlice[T any](in []T) []T {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]T, len(in))
|
||||
copy(out, in)
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneInventoryHosts(in []InventoryHost) []InventoryHost {
|
||||
return cloneSliceWith(in, func(item *InventoryHost) {
|
||||
item.ClusterHAEnabled = cloneBoolPointer(item.ClusterHAEnabled)
|
||||
item.ClusterDRSEnabled = cloneBoolPointer(item.ClusterDRSEnabled)
|
||||
item.DatastoreIDs = cloneStringSlice(item.DatastoreIDs)
|
||||
item.DatastoreNames = cloneStringSlice(item.DatastoreNames)
|
||||
item.TriggeredAlarms = cloneInventoryAlarms(item.TriggeredAlarms)
|
||||
item.RecentTasks = cloneInventoryTasks(item.RecentTasks)
|
||||
item.RecentEvents = cloneInventoryEvents(item.RecentEvents)
|
||||
item.Metrics = cloneInventoryMetrics(item.Metrics)
|
||||
})
|
||||
}
|
||||
|
||||
func cloneInventoryVMs(in []InventoryVM) []InventoryVM {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryVM, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].ClusterHAEnabled = cloneBoolPointer(in[i].ClusterHAEnabled)
|
||||
out[i].ClusterDRSEnabled = cloneBoolPointer(in[i].ClusterDRSEnabled)
|
||||
out[i].DatastoreIDs = cloneStringSlice(in[i].DatastoreIDs)
|
||||
out[i].DatastoreNames = cloneStringSlice(in[i].DatastoreNames)
|
||||
out[i].GuestIPAddresses = cloneStringSlice(in[i].GuestIPAddresses)
|
||||
out[i].TriggeredAlarms = cloneInventoryAlarms(in[i].TriggeredAlarms)
|
||||
out[i].RecentTasks = cloneInventoryTasks(in[i].RecentTasks)
|
||||
out[i].RecentEvents = cloneInventoryEvents(in[i].RecentEvents)
|
||||
out[i].SnapshotTree = cloneInventoryVMSnapshots(in[i].SnapshotTree)
|
||||
out[i].NetworkAdapters = cloneInventoryVMNetworkAdapters(in[i].NetworkAdapters)
|
||||
out[i].VirtualDisks = cloneInventoryVMVirtualDisks(in[i].VirtualDisks)
|
||||
out[i].Tools = cloneInventoryVMTools(in[i].Tools)
|
||||
out[i].Hardware = cloneInventoryVMHardware(in[i].Hardware)
|
||||
out[i].Metrics = cloneInventoryMetrics(in[i].Metrics)
|
||||
}
|
||||
return out
|
||||
return cloneSliceWith(in, func(item *InventoryVM) {
|
||||
item.ClusterHAEnabled = cloneBoolPointer(item.ClusterHAEnabled)
|
||||
item.ClusterDRSEnabled = cloneBoolPointer(item.ClusterDRSEnabled)
|
||||
item.DatastoreIDs = cloneStringSlice(item.DatastoreIDs)
|
||||
item.DatastoreNames = cloneStringSlice(item.DatastoreNames)
|
||||
item.GuestIPAddresses = cloneStringSlice(item.GuestIPAddresses)
|
||||
item.TriggeredAlarms = cloneInventoryAlarms(item.TriggeredAlarms)
|
||||
item.RecentTasks = cloneInventoryTasks(item.RecentTasks)
|
||||
item.RecentEvents = cloneInventoryEvents(item.RecentEvents)
|
||||
item.SnapshotTree = cloneInventoryVMSnapshots(item.SnapshotTree)
|
||||
item.NetworkAdapters = cloneInventoryVMNetworkAdapters(item.NetworkAdapters)
|
||||
item.VirtualDisks = cloneInventoryVMVirtualDisks(item.VirtualDisks)
|
||||
item.Tools = cloneInventoryVMTools(item.Tools)
|
||||
item.Hardware = cloneInventoryVMHardware(item.Hardware)
|
||||
item.Metrics = cloneInventoryMetrics(item.Metrics)
|
||||
})
|
||||
}
|
||||
|
||||
func cloneInventoryDatastores(in []InventoryDatastore) []InventoryDatastore {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryDatastore, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].HostIDs = cloneStringSlice(in[i].HostIDs)
|
||||
out[i].HostNames = cloneStringSlice(in[i].HostNames)
|
||||
out[i].VMIDs = cloneStringSlice(in[i].VMIDs)
|
||||
out[i].VMNames = cloneStringSlice(in[i].VMNames)
|
||||
out[i].Accessible = cloneBoolPointer(in[i].Accessible)
|
||||
out[i].MultipleHostAccess = cloneBoolPointer(in[i].MultipleHostAccess)
|
||||
out[i].TriggeredAlarms = cloneInventoryAlarms(in[i].TriggeredAlarms)
|
||||
out[i].RecentTasks = cloneInventoryTasks(in[i].RecentTasks)
|
||||
out[i].RecentEvents = cloneInventoryEvents(in[i].RecentEvents)
|
||||
}
|
||||
return out
|
||||
return cloneSliceWith(in, func(item *InventoryDatastore) {
|
||||
item.HostIDs = cloneStringSlice(item.HostIDs)
|
||||
item.HostNames = cloneStringSlice(item.HostNames)
|
||||
item.VMIDs = cloneStringSlice(item.VMIDs)
|
||||
item.VMNames = cloneStringSlice(item.VMNames)
|
||||
item.Accessible = cloneBoolPointer(item.Accessible)
|
||||
item.MultipleHostAccess = cloneBoolPointer(item.MultipleHostAccess)
|
||||
item.TriggeredAlarms = cloneInventoryAlarms(item.TriggeredAlarms)
|
||||
item.RecentTasks = cloneInventoryTasks(item.RecentTasks)
|
||||
item.RecentEvents = cloneInventoryEvents(item.RecentEvents)
|
||||
})
|
||||
}
|
||||
|
||||
func cloneInventoryClusters(in []InventoryCluster) []InventoryCluster {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryCluster, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].HAEnabled = cloneBoolPointer(in[i].HAEnabled)
|
||||
out[i].DRSEnabled = cloneBoolPointer(in[i].DRSEnabled)
|
||||
}
|
||||
return out
|
||||
return cloneSliceWith(in, func(item *InventoryCluster) {
|
||||
item.HAEnabled = cloneBoolPointer(item.HAEnabled)
|
||||
item.DRSEnabled = cloneBoolPointer(item.DRSEnabled)
|
||||
})
|
||||
}
|
||||
|
||||
func cloneInventoryNetworks(in []InventoryNetwork) []InventoryNetwork {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryNetwork, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].HostIDs = cloneStringSlice(in[i].HostIDs)
|
||||
out[i].HostNames = cloneStringSlice(in[i].HostNames)
|
||||
out[i].VMIDs = cloneStringSlice(in[i].VMIDs)
|
||||
out[i].VMNames = cloneStringSlice(in[i].VMNames)
|
||||
out[i].TriggeredAlarms = cloneInventoryAlarms(in[i].TriggeredAlarms)
|
||||
out[i].RecentTasks = cloneInventoryTasks(in[i].RecentTasks)
|
||||
out[i].RecentEvents = cloneInventoryEvents(in[i].RecentEvents)
|
||||
}
|
||||
return out
|
||||
return cloneSliceWith(in, func(item *InventoryNetwork) {
|
||||
item.HostIDs = cloneStringSlice(item.HostIDs)
|
||||
item.HostNames = cloneStringSlice(item.HostNames)
|
||||
item.VMIDs = cloneStringSlice(item.VMIDs)
|
||||
item.VMNames = cloneStringSlice(item.VMNames)
|
||||
item.TriggeredAlarms = cloneInventoryAlarms(item.TriggeredAlarms)
|
||||
item.RecentTasks = cloneInventoryTasks(item.RecentTasks)
|
||||
item.RecentEvents = cloneInventoryEvents(item.RecentEvents)
|
||||
})
|
||||
}
|
||||
|
||||
func cloneInventoryAlarms(in []InventoryAlarm) []InventoryAlarm {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryAlarm, len(in))
|
||||
copy(out, in)
|
||||
return out
|
||||
return cloneShallowSlice(in)
|
||||
}
|
||||
|
||||
func cloneInventoryTasks(in []InventoryTask) []InventoryTask {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryTask, len(in))
|
||||
copy(out, in)
|
||||
return out
|
||||
return cloneShallowSlice(in)
|
||||
}
|
||||
|
||||
func cloneInventoryEvents(in []InventoryEvent) []InventoryEvent {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryEvent, len(in))
|
||||
copy(out, in)
|
||||
return out
|
||||
return cloneShallowSlice(in)
|
||||
}
|
||||
|
||||
func cloneInventoryVMSnapshots(in []InventoryVMSnapshot) []InventoryVMSnapshot {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryVMSnapshot, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].CreatedAt = cloneTimePointer(in[i].CreatedAt)
|
||||
out[i].Children = cloneInventoryVMSnapshots(in[i].Children)
|
||||
}
|
||||
return out
|
||||
return cloneSliceWith(in, func(item *InventoryVMSnapshot) {
|
||||
item.CreatedAt = cloneTimePointer(item.CreatedAt)
|
||||
item.Children = cloneInventoryVMSnapshots(item.Children)
|
||||
})
|
||||
}
|
||||
|
||||
func cloneInventoryVMNetworkAdapters(in []InventoryVMNetworkAdapter) []InventoryVMNetworkAdapter {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryVMNetworkAdapter, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].PCISlotNumber = cloneInt64Pointer(in[i].PCISlotNumber)
|
||||
}
|
||||
return out
|
||||
return cloneSliceWith(in, func(item *InventoryVMNetworkAdapter) {
|
||||
item.PCISlotNumber = cloneInt64Pointer(item.PCISlotNumber)
|
||||
})
|
||||
}
|
||||
|
||||
func cloneInventoryVMVirtualDisks(in []InventoryVMVirtualDisk) []InventoryVMVirtualDisk {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryVMVirtualDisk, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].IDEPrimary = cloneBoolPointer(in[i].IDEPrimary)
|
||||
out[i].IDEMaster = cloneBoolPointer(in[i].IDEMaster)
|
||||
out[i].SCSIBus = cloneInt64Pointer(in[i].SCSIBus)
|
||||
out[i].SCSIUnit = cloneInt64Pointer(in[i].SCSIUnit)
|
||||
out[i].SATABus = cloneInt64Pointer(in[i].SATABus)
|
||||
out[i].SATAUnit = cloneInt64Pointer(in[i].SATAUnit)
|
||||
out[i].NVMEBus = cloneInt64Pointer(in[i].NVMEBus)
|
||||
out[i].NVMEUnit = cloneInt64Pointer(in[i].NVMEUnit)
|
||||
out[i].CapacityBytes = cloneInt64Pointer(in[i].CapacityBytes)
|
||||
}
|
||||
return out
|
||||
return cloneSliceWith(in, func(item *InventoryVMVirtualDisk) {
|
||||
item.IDEPrimary = cloneBoolPointer(item.IDEPrimary)
|
||||
item.IDEMaster = cloneBoolPointer(item.IDEMaster)
|
||||
item.SCSIBus = cloneInt64Pointer(item.SCSIBus)
|
||||
item.SCSIUnit = cloneInt64Pointer(item.SCSIUnit)
|
||||
item.SATABus = cloneInt64Pointer(item.SATABus)
|
||||
item.SATAUnit = cloneInt64Pointer(item.SATAUnit)
|
||||
item.NVMEBus = cloneInt64Pointer(item.NVMEBus)
|
||||
item.NVMEUnit = cloneInt64Pointer(item.NVMEUnit)
|
||||
item.CapacityBytes = cloneInt64Pointer(item.CapacityBytes)
|
||||
})
|
||||
}
|
||||
|
||||
func cloneInventoryVMTools(in *InventoryVMTools) *InventoryVMTools {
|
||||
|
|
@ -1030,24 +991,13 @@ func cloneInventoryVMHardware(in *InventoryVMHardware) *InventoryVMHardware {
|
|||
}
|
||||
|
||||
func cloneInventoryVMBootDevices(in []InventoryVMBootDevice) []InventoryVMBootDevice {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryVMBootDevice, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].Disks = cloneStringSlice(in[i].Disks)
|
||||
}
|
||||
return out
|
||||
return cloneSliceWith(in, func(item *InventoryVMBootDevice) {
|
||||
item.Disks = cloneStringSlice(item.Disks)
|
||||
})
|
||||
}
|
||||
|
||||
func cloneInventoryEnrichmentIssues(in []InventoryEnrichmentIssue) []InventoryEnrichmentIssue {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryEnrichmentIssue, len(in))
|
||||
copy(out, in)
|
||||
return out
|
||||
return cloneShallowSlice(in)
|
||||
}
|
||||
|
||||
func inventoryEnrichmentIssueSortKey(issue InventoryEnrichmentIssue) string {
|
||||
|
|
@ -1731,12 +1681,7 @@ func cloneBoolPointer(in *bool) *bool {
|
|||
}
|
||||
|
||||
func cloneStringSlice(in []string) []string {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, len(in))
|
||||
copy(out, in)
|
||||
return out
|
||||
return cloneShallowSlice(in)
|
||||
}
|
||||
|
||||
func firstNonEmptyTrimmed(values ...string) string {
|
||||
|
|
|
|||
|
|
@ -106,27 +106,29 @@ func (c *LicenseServerClient) ready() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Activate calls the license server to create an installation and receive a relay grant.
|
||||
func (c *LicenseServerClient) Activate(ctx context.Context, req ActivateInstallationRequest) (*ActivateInstallationResponse, error) {
|
||||
// postActivationRequest POSTs an idempotent JSON request to the license
|
||||
// server and decodes the shared activation response shape. label feeds error
|
||||
// wrapping ("activate", "exchange").
|
||||
func (c *LicenseServerClient) postActivationRequest(ctx context.Context, path, label string, req any) (*ActivateInstallationResponse, error) {
|
||||
if err := c.ready(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal activate request: %w", err)
|
||||
return nil, fmt.Errorf("marshal %s request: %w", label, err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/activate", bytes.NewReader(body))
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create activate request: %w", err)
|
||||
return nil, fmt.Errorf("create %s request: %w", label, err)
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Idempotency-Key", generateIdempotencyKey())
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("activate request failed: %w", err)
|
||||
return nil, fmt.Errorf("%s request failed: %w", label, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
|
@ -136,46 +138,21 @@ func (c *LicenseServerClient) Activate(ctx context.Context, req ActivateInstalla
|
|||
|
||||
var result ActivateInstallationResponse
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("decode activate response: %w", err)
|
||||
return nil, fmt.Errorf("decode %s response: %w", label, err)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Activate calls the license server to create an installation and receive a relay grant.
|
||||
func (c *LicenseServerClient) Activate(ctx context.Context, req ActivateInstallationRequest) (*ActivateInstallationResponse, error) {
|
||||
return c.postActivationRequest(ctx, "/v1/activate", "activate", req)
|
||||
}
|
||||
|
||||
// ExchangeLegacyLicense converts a legacy v5 JWT-style license into a v6 activation.
|
||||
// The response shape matches normal activation so the runtime can persist activation
|
||||
// state and start using grant refresh immediately.
|
||||
func (c *LicenseServerClient) ExchangeLegacyLicense(ctx context.Context, req ExchangeLegacyLicenseRequest) (*ActivateInstallationResponse, error) {
|
||||
if err := c.ready(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal exchange request: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/licenses/exchange", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create exchange request: %w", err)
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Idempotency-Key", generateIdempotencyKey())
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("exchange request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
return nil, c.parseError(resp)
|
||||
}
|
||||
|
||||
var result ActivateInstallationResponse
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("decode exchange response: %w", err)
|
||||
}
|
||||
return &result, nil
|
||||
return c.postActivationRequest(ctx, "/v1/licenses/exchange", "exchange", req)
|
||||
}
|
||||
|
||||
// RefreshGrant calls the license server to refresh a relay grant.
|
||||
|
|
|
|||
|
|
@ -889,8 +889,9 @@ func (c *Client) GetNodeRRDData(ctx context.Context, node, timeframe, cf string,
|
|||
return result.Data, nil
|
||||
}
|
||||
|
||||
// GetLXCRRDData retrieves RRD metrics for an LXC container.
|
||||
func (c *Client) GetLXCRRDData(ctx context.Context, node string, vmid int, timeframe, cf string, ds []string) ([]GuestRRDPoint, error) {
|
||||
// getGuestRRDData retrieves RRD metrics for a guest. guestPath is the PVE API
|
||||
// guest segment ("lxc", "qemu").
|
||||
func (c *Client) getGuestRRDData(ctx context.Context, node, guestPath string, vmid int, timeframe, cf string) ([]GuestRRDPoint, error) {
|
||||
if timeframe == "" {
|
||||
timeframe = "hour"
|
||||
}
|
||||
|
|
@ -904,7 +905,7 @@ func (c *Client) GetLXCRRDData(ctx context.Context, node string, vmid int, timef
|
|||
// Note: the "ds" parameter is not sent because older PVE versions
|
||||
// (including 9.x) reject it as an unknown property.
|
||||
|
||||
path := fmt.Sprintf("/nodes/%s/lxc/%d/rrddata", url.PathEscape(node), vmid)
|
||||
path := fmt.Sprintf("/nodes/%s/%s/%d/rrddata", url.PathEscape(node), guestPath, vmid)
|
||||
if query := params.Encode(); query != "" {
|
||||
path = fmt.Sprintf("%s?%s", path, query)
|
||||
}
|
||||
|
|
@ -926,41 +927,14 @@ func (c *Client) GetLXCRRDData(ctx context.Context, node string, vmid int, timef
|
|||
return result.Data, nil
|
||||
}
|
||||
|
||||
// GetLXCRRDData retrieves RRD metrics for an LXC container.
|
||||
func (c *Client) GetLXCRRDData(ctx context.Context, node string, vmid int, timeframe, cf string, ds []string) ([]GuestRRDPoint, error) {
|
||||
return c.getGuestRRDData(ctx, node, "lxc", vmid, timeframe, cf)
|
||||
}
|
||||
|
||||
// GetVMRRDData retrieves RRD metrics for a QEMU VM.
|
||||
func (c *Client) GetVMRRDData(ctx context.Context, node string, vmid int, timeframe, cf string, ds []string) ([]GuestRRDPoint, error) {
|
||||
if timeframe == "" {
|
||||
timeframe = "hour"
|
||||
}
|
||||
if cf == "" {
|
||||
cf = "AVERAGE"
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("timeframe", timeframe)
|
||||
params.Set("cf", cf)
|
||||
// Note: the "ds" parameter is not sent because older PVE versions
|
||||
// (including 9.x) reject it as an unknown property.
|
||||
|
||||
path := fmt.Sprintf("/nodes/%s/qemu/%d/rrddata", url.PathEscape(node), vmid)
|
||||
if query := params.Encode(); query != "" {
|
||||
path = fmt.Sprintf("%s?%s", path, query)
|
||||
}
|
||||
|
||||
resp, err := c.get(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Data []GuestRRDPoint `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.Data, nil
|
||||
return c.getGuestRRDData(ctx, node, "qemu", vmid, timeframe, cf)
|
||||
}
|
||||
|
||||
// VM represents a Proxmox VE virtual machine
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue