mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-15 08:33:03 +00:00
5d4f51c027
Two TrueNAS systems that report the same hostname collapsed into one flapping resource (#1573, #1575): systemSourceID keyed the system by the snapshot-reported hostname, every child pool/dataset/app/VM/share/disk was scoped under it, and the client minted the system's machine key from the DMI serial with a hostname fallback, so serial-less systems sharing a hostname (and DR clones sharing a serial) also fully merged in the identity matcher. The system source ID now scopes to the connection ID the poller passes through NewLiveProviderForConnection; the hostname arm survives only for fixture snapshots, which carry no connection. The ingest identity drops the machine key entirely (DR clones share DMI serials, and vendor placeholder serials collide across unrelated machines), the client no longer falls back to the hostname for MachineID, and ingest skips identity-pin completion for SourceTrueNAS so a stale pre-fix pin or a same-named agent host's pin cannot lend the system a machine key and re-merge what connection scoping keeps apart. Agent.AgentID and the native metric history keys follow the source ID minus its system: prefix, so BuildMetricsTarget keeps resolving one series. Rows minted under the retired hostname-keyed derivation re-key once via record-declared succession: records name their old canonical IDs in IngestRecord.SupersededCanonicalIDs and IngestRecords applies the existing ApplyCanonicalIDSuccessions semantics (operator state and action audits re-key, the superseded pin drops, never while the old ID still belongs to a live resource, journal rows are never rewritten). Alert identities and persisted metric series under old child IDs are not re-keyed: active alerts re-arm under the new IDs and TrueNAS host charts are backed by native read-through history.
133 lines
5.3 KiB
Go
133 lines
5.3 KiB
Go
package unifiedresources
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
func normalizeSourceID(sourceID string) string {
|
|
return strings.TrimSpace(sourceID)
|
|
}
|
|
|
|
// SourceSpecificID returns the deterministic ID used for non-host resources when the registry
|
|
// does not have a canonical identity to key off of.
|
|
//
|
|
// This matches the ResourceRegistry's internal ID derivation:
|
|
// stable := fmt.Sprintf("%s:%s", source, sourceID)
|
|
// id := fmt.Sprintf("%s-%s", resourceType, hex(sha256(stable)[:8]))
|
|
func SourceSpecificID(resourceType ResourceType, source DataSource, sourceID string) string {
|
|
resourceType = CanonicalResourceType(resourceType)
|
|
stable := fmt.Sprintf("%s:%s", source, normalizeSourceID(sourceID))
|
|
hash := sha256.Sum256([]byte(stable))
|
|
return fmt.Sprintf("%s-%s", resourceType, hex.EncodeToString(hash[:8]))
|
|
}
|
|
|
|
// MachineIdentityCanonicalID returns the canonical ID the registry mints for
|
|
// a resource keyed by its machine identity, the strongest arm of the
|
|
// chooseNewID ladder. Providers use it to name superseded canonical IDs
|
|
// (IngestRecord.SupersededCanonicalIDs) when a derivation correction retires
|
|
// a machine key, so operator-owned rows follow the resource to its new ID.
|
|
func MachineIdentityCanonicalID(resourceType ResourceType, machineID string) string {
|
|
return buildHashID(resourceType, "machine:"+strings.TrimSpace(machineID))
|
|
}
|
|
|
|
// CanonicalResourceID returns the canonical v6 resource identifier.
|
|
func CanonicalResourceID(id string) string {
|
|
trimmed := strings.TrimSpace(id)
|
|
if trimmed == "" {
|
|
return ""
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
// ResourceIdentityPin is the durable identity→canonical-ID record persisted in
|
|
// the resource store (resource_identities table). It exists because canonical
|
|
// ID derivation keys off the strongest identity field known at mint time, and
|
|
// merged-source hosts (PVE node + pulse-agent) expose different identity
|
|
// subsets depending on which records are present when the registry rebuilds:
|
|
// the agent record knows the machine ID, the Proxmox node record only knows
|
|
// cluster+hostname. Pins let a rebuild that only sees the weak subset recover
|
|
// the strong keys, so the same physical host derives the same canonical ID in
|
|
// every boot, in every registry instance.
|
|
type ResourceIdentityPin struct {
|
|
CanonicalID string
|
|
ResourceType ResourceType
|
|
MachineID string
|
|
DMIUUID string
|
|
ClusterName string
|
|
// Hostname is the normalized full hostname (NormalizeFullHostname).
|
|
// Dotted hostnames are preserved so distinct machines that share a short
|
|
// name (cloud.rnd-lax1 vs cloud.gce-or1) keep distinct pins. Rows written
|
|
// before the fix for #1559 hold the collapsed short name; they heal in
|
|
// place on the host's next pin persist.
|
|
Hostname string
|
|
}
|
|
|
|
func (p ResourceIdentityPin) normalized() ResourceIdentityPin {
|
|
p.CanonicalID = CanonicalResourceID(p.CanonicalID)
|
|
p.ResourceType = CanonicalResourceType(p.ResourceType)
|
|
p.MachineID = strings.TrimSpace(p.MachineID)
|
|
p.DMIUUID = strings.TrimSpace(p.DMIUUID)
|
|
p.ClusterName = strings.TrimSpace(p.ClusterName)
|
|
p.Hostname = NormalizeFullHostname(p.Hostname)
|
|
return p
|
|
}
|
|
|
|
func (p ResourceIdentityPin) hasStrongKey() bool {
|
|
return p.MachineID != "" || p.DMIUUID != "" || (p.ClusterName != "" && p.Hostname != "")
|
|
}
|
|
|
|
// EraIDs returns every canonical ID this pin's identity keys derive under the
|
|
// historical chooseNewID ladder (machine > dmi > cluster+hostname > hostname).
|
|
// Change-journal rows written in boots that only knew a weaker key sit under
|
|
// the weaker key's hash; expanding a read to the full era set merges those
|
|
// journal eras without rewriting history.
|
|
func (p ResourceIdentityPin) EraIDs() []string {
|
|
p = p.normalized()
|
|
ids := make([]string, 0, 7)
|
|
if p.CanonicalID != "" {
|
|
ids = append(ids, p.CanonicalID)
|
|
}
|
|
if p.MachineID != "" {
|
|
ids = append(ids, buildHashID(p.ResourceType, "machine:"+p.MachineID))
|
|
}
|
|
if p.DMIUUID != "" {
|
|
ids = append(ids, buildHashID(p.ResourceType, "dmi:"+p.DMIUUID))
|
|
}
|
|
if p.Hostname != "" {
|
|
// The historical chooseNewID ladder hashed the short hostname; the
|
|
// pin now preserves the full dotted name, so derive eras for both so
|
|
// journal rows written under short-hostname IDs stay readable.
|
|
for _, hostname := range uniqueTrimmed(p.Hostname, NormalizeHostname(p.Hostname)) {
|
|
if p.ClusterName != "" {
|
|
ids = append(ids, buildHashID(p.ResourceType, fmt.Sprintf("cluster:%s:%s", p.ClusterName, hostname)))
|
|
}
|
|
ids = append(ids, buildHashID(p.ResourceType, "hostname:"+hostname))
|
|
}
|
|
}
|
|
return uniqueTrimmed(ids...)
|
|
}
|
|
|
|
// identityPinForResource derives the persistable pin for a canonical resource.
|
|
// Only host resources with at least one strong identity key are pinned; a
|
|
// hostname-only host has a single possible derivation and needs no pin.
|
|
func identityPinForResource(resource *Resource) (ResourceIdentityPin, bool) {
|
|
if resource == nil || CanonicalResourceType(resource.Type) != ResourceTypeAgent {
|
|
return ResourceIdentityPin{}, false
|
|
}
|
|
pin := ResourceIdentityPin{
|
|
CanonicalID: resource.ID,
|
|
ResourceType: ResourceTypeAgent,
|
|
MachineID: resource.Identity.MachineID,
|
|
DMIUUID: resource.Identity.DMIUUID,
|
|
ClusterName: resource.Identity.ClusterName,
|
|
Hostname: firstIdentityHostname(resource.Identity),
|
|
}.normalized()
|
|
if pin.CanonicalID == "" || !pin.hasStrongKey() {
|
|
return ResourceIdentityPin{}, false
|
|
}
|
|
return pin, true
|
|
}
|