mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Merge pull request #2198 from rcourtman/maintainer/20260923T115045Z
Keep PBS History on its host target when the connection uses an IP or alias
This commit is contained in:
@@ -29,7 +29,11 @@ const pbs = {
|
||||
memory: { current: 20.3, total: 8000, used: 1624, free: 6376 },
|
||||
pbs: {
|
||||
instanceId: 'proxback',
|
||||
hostname: 'proxback-vm',
|
||||
// The connection is configured by an address the agent never reports, so
|
||||
// the reported node name below is the only machine identity that links the
|
||||
// PBS service row to its host agent (#1723).
|
||||
hostname: '10.0.0.5',
|
||||
nodeName: 'proxback-vm',
|
||||
version: '3.2.1',
|
||||
connectionHealth: 'healthy',
|
||||
datastores: [
|
||||
@@ -42,7 +46,12 @@ const pbs = {
|
||||
discoveryTarget: { resourceType: 'agent', resourceId: 'pbs-1' },
|
||||
platformData: {
|
||||
sources: ['pbs'],
|
||||
pbs: { instanceId: 'proxback', hostname: 'proxback-vm', datastoreCount: 2 },
|
||||
pbs: {
|
||||
instanceId: 'proxback',
|
||||
hostname: '10.0.0.5',
|
||||
nodeName: 'proxback-vm',
|
||||
datastoreCount: 2,
|
||||
},
|
||||
},
|
||||
} as unknown as Resource;
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "92edc056627a88bbdb11a8f6d6f95aacd061607a",
|
||||
"verified_at": "2026-09-23T09:22:16Z",
|
||||
"base_sha": "4f11deddc728a69f937dcb04777c9e44276c3f95",
|
||||
"verified_at": "2026-09-23T11:26:00Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx"
|
||||
"frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx",
|
||||
"frontend-modern/src/types/resource.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "b19198cb055e5f0f107c2d6749d6def719bd84310de585e3b6a349fa22fac3c1"
|
||||
"frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "f6b50e71870ca4a3e0dcfa3e47413542e8444296bc68481c60e15d1a5d921f9f",
|
||||
"frontend-modern/src/types/resource.ts": "db2509b73b2ed550ea1890aaef34e2a3dfe7d500aa193586562f9e20b95283ab"
|
||||
},
|
||||
"routes": [
|
||||
"/browser-tests/pbs-host-history-correlation.html?topology=guest&order=stable (Proxmox Backups server drawer)",
|
||||
@@ -28,17 +30,18 @@
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"PBS connection configured by 10.0.0.5 and linked to the host agent only by the reported pbs.nodeName, across pbs-only, side-by-side and PVE-guest topologies",
|
||||
"Drawer History tab selected with a populated correlated host series",
|
||||
"Timer-driven snapshot replacing the datastore inventory in stable and reversed order",
|
||||
"Snapshot that briefly omits the correlated host row while the PBS server row remains",
|
||||
"Snapshot that restores the correlated host row on the same selection"
|
||||
],
|
||||
"interactions": [
|
||||
"Expand the Proxmox Backups server row, switch between datastore rows and select History; assert the chart paths render against the host metrics target",
|
||||
"Expand the Proxmox Backups server row, switch between datastore rows and select History; assert the chart paths render against the host metrics target even though the configured endpoint is not a machine-identity token",
|
||||
"Apply timer-driven stable and reordered snapshots; assert the drawer is not remounted, the History selection is retained, the chart series survive and the Discovery/Metrics Target rows stay byte-identical",
|
||||
"Apply a snapshot that omits the correlated host row; assert the Identity rows and History chart stay on the retained host target instead of falling back to the PBS service key",
|
||||
"Restore the correlated host row; assert the same target is confirmed, and repeat the complete matrix at 1280x900 and 390x844"
|
||||
],
|
||||
"command": "pulse-worker-browser scripts/check-pbs-host-history-correlation.cjs",
|
||||
"notes": "Chromium 141.0.7390.37 / Playwright 1.56.1. Production ProxmoxBackupServersTable mounted through the real browser fixture with synthetic API responses and an intercepted metrics-history request. Proves the retained host correlation, not backend persistence or installed release behaviour. The same check fails on the pre-fix source: omitting the host row flips the drawer's Discovery and Metrics Target rows to agent:pbs-1 and drops the host series."
|
||||
"notes": "Chromium 141.0.7390.37 / Playwright 1.56.1. Production ProxmoxBackupServersTable mounted through the real browser fixture with synthetic API responses and an intercepted metrics-history request. Proves the reported-node-name host correlation and its retention across a transient host-row omission, not backend persistence or installed release behaviour. The recorded history requests target agent/agent-proxback and vm/proxmox:100 and never the PBS service target agent/pbs-1. The same check fails on the pre-fix source: the drawer falls back to the PBS service key when the endpoint is not a machine-identity token, and omitting the host row flips the Discovery and Metrics Target rows to agent:pbs-1."
|
||||
}
|
||||
|
||||
@@ -110,6 +110,12 @@ const identityValues = (resource: Resource): Array<string | undefined> => [
|
||||
...(resource.identity?.ips ?? []),
|
||||
resource.agent?.hostname,
|
||||
resource.pbs?.hostname,
|
||||
// The node hostname PBS reports about itself is machine identity. The
|
||||
// connection may be configured by an IP or DNS alias the agent never
|
||||
// reports, so without this the host correlation can drop out between
|
||||
// snapshots and the drawer falls back to the service history target
|
||||
// (#1723).
|
||||
resource.pbs?.nodeName,
|
||||
resource.pbs?.instanceId,
|
||||
resource.platformId,
|
||||
resource.name,
|
||||
|
||||
+52
@@ -224,6 +224,58 @@ describe('ProxmoxBackupServersTable details', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it('correlates a PBS connection configured by IP to its agent via the reported node name', () => {
|
||||
const pbs = makePbsResource();
|
||||
pbs.sources = ['pbs'];
|
||||
pbs.agent = undefined;
|
||||
pbs.name = 'backup-connection';
|
||||
pbs.displayName = 'Backup connection';
|
||||
pbs.platformId = 'pbs-1';
|
||||
pbs.pbs = {
|
||||
...pbs.pbs!,
|
||||
instanceId: 'pbs-1',
|
||||
hostname: '10.0.0.5',
|
||||
nodeName: 'pbs-one.local',
|
||||
};
|
||||
pbs.metricsTarget = { resourceType: 'agent', resourceId: 'pbs-1' };
|
||||
pbs.platformData = {
|
||||
sources: ['pbs'],
|
||||
pbs: {
|
||||
instanceId: 'pbs-1',
|
||||
hostname: '10.0.0.5',
|
||||
nodeName: 'pbs-one.local',
|
||||
datastoreCount: 1,
|
||||
},
|
||||
};
|
||||
const agent = {
|
||||
id: 'agent-host-1',
|
||||
type: 'agent',
|
||||
name: 'pbs-one.local',
|
||||
displayName: 'PBS host',
|
||||
platformId: 'agent-host-1',
|
||||
platformType: 'proxmox-pbs',
|
||||
sourceType: 'hybrid',
|
||||
sources: ['agent', 'pbs'],
|
||||
status: 'online',
|
||||
lastSeen: pbs.lastSeen + 1_000,
|
||||
agent: { agentId: 'agent-pbs-1', hostname: 'pbs-one.local', osName: 'Debian GNU/Linux' },
|
||||
identity: { hostname: 'pbs-one.local', ips: ['10.9.9.9'] },
|
||||
metricsTarget: { resourceType: 'agent', resourceId: 'agent-pbs-1' },
|
||||
platformData: {
|
||||
sources: ['agent', 'pbs'],
|
||||
agent: { agentId: 'agent-pbs-1', hostname: 'pbs-one.local' },
|
||||
},
|
||||
} as Resource;
|
||||
|
||||
render(() => <ProxmoxBackupServersTable servers={[pbs, agent]} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Expand details for backup-connection' }));
|
||||
|
||||
const detail = screen.getByTestId('pbs-resource-detail');
|
||||
expect(detail).toHaveAttribute('data-agent-id', 'agent-pbs-1');
|
||||
expect(detail).toHaveAttribute('data-metrics-resource-id', 'agent-pbs-1');
|
||||
expect(detail).toHaveAttribute('data-metrics-resource-type', 'agent');
|
||||
});
|
||||
|
||||
it('reuses the PVE guest when the same agent is listed as both guest and standalone host', () => {
|
||||
const pbs = makePbsResource();
|
||||
pbs.sources = ['pbs'];
|
||||
|
||||
@@ -472,6 +472,7 @@ export interface ResourcePBSDatastore {
|
||||
export interface ResourcePBSMeta {
|
||||
instanceId?: string;
|
||||
hostname?: string;
|
||||
nodeName?: string;
|
||||
version?: string;
|
||||
uptimeSeconds?: number;
|
||||
datastoreCount?: number;
|
||||
|
||||
@@ -1507,6 +1507,7 @@ func resourceFromPBSInstance(instance models.PBSInstance) (Resource, ResourceIde
|
||||
PBS: &PBSData{
|
||||
InstanceID: instance.ID,
|
||||
Hostname: extractHostname(instance.Host),
|
||||
NodeName: strings.TrimSpace(instance.NodeName),
|
||||
HostURL: instance.Host,
|
||||
GuestURL: instance.GuestURL,
|
||||
Version: instance.Version,
|
||||
|
||||
@@ -98,6 +98,13 @@ func pbsInstanceCorroboratesHost(instance models.PBSInstance, host models.Host)
|
||||
if hostName == "" {
|
||||
return false
|
||||
}
|
||||
// The node hostname the PBS API reports about itself is machine identity.
|
||||
// It is the strongest evidence when the connection is configured by IP or
|
||||
// a DNS alias the agent never reports, which the connection label and the
|
||||
// configured endpoint cannot corroborate on their own (#1723).
|
||||
if nodeName := NormalizeHostname(instance.NodeName); nodeName != "" && nodeName == hostName {
|
||||
return true
|
||||
}
|
||||
if instanceName := NormalizeHostname(instance.Name); instanceName != "" && instanceName == hostName {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -309,6 +309,67 @@ func TestIngestSnapshotSkipsAmbiguousPBSHostAgentAssociation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A PBS connection is often configured by IP or a DNS alias the agent never
|
||||
// reports. The node hostname PBS reports about itself is then the only machine
|
||||
// identity linking the host agent to the connection (#1723).
|
||||
func TestIngestSnapshotAssociatesPBSHostAgentByReportedNodeName(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
registry := NewRegistry(nil)
|
||||
registry.IngestSnapshot(models.StateSnapshot{
|
||||
Hosts: []models.Host{
|
||||
{
|
||||
ID: "agent-pbs-1",
|
||||
Hostname: "pbs-one.local",
|
||||
MachineID: "machine-pbs-1",
|
||||
ReportIP: "10.9.9.9",
|
||||
Status: "online",
|
||||
LastSeen: now,
|
||||
Sensors: models.HostSensorSummary{
|
||||
SMART: []models.HostDiskSMART{{Device: "/dev/sda", Serial: "PBS-DISK-1"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
PBSInstances: []models.PBSInstance{
|
||||
{
|
||||
ID: "pbs-1",
|
||||
Name: "backup-connection", // connection label, not the machine
|
||||
Host: "https://10.0.0.5:8007", // endpoint the agent never reports
|
||||
// Machine identity the PBS node reports about itself.
|
||||
NodeName: "pbs-one.local",
|
||||
Status: "online",
|
||||
LastSeen: now,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
var pbsResource *Resource
|
||||
var hostResource *Resource
|
||||
for _, resource := range registry.List() {
|
||||
switch resource.Type {
|
||||
case ResourceTypePBS:
|
||||
copy := resource
|
||||
pbsResource = ©
|
||||
case ResourceTypeAgent:
|
||||
if resource.Agent != nil && resource.Agent.AgentID == "agent-pbs-1" {
|
||||
copy := resource
|
||||
hostResource = ©
|
||||
}
|
||||
}
|
||||
}
|
||||
if pbsResource == nil || pbsResource.PBS == nil {
|
||||
t.Fatalf("expected PBS resource with payload, got %+v", pbsResource)
|
||||
}
|
||||
if pbsResource.PBS.NodeName != "pbs-one.local" {
|
||||
t.Fatalf("PBS nodeName = %q, want reported machine hostname", pbsResource.PBS.NodeName)
|
||||
}
|
||||
if hostResource == nil {
|
||||
t.Fatal("expected host agent resource")
|
||||
}
|
||||
if !containsDataSource(hostResource.Sources, SourcePBS) {
|
||||
t.Fatalf("PBS host sources = %v, want PBS membership via reported node name", hostResource.Sources)
|
||||
}
|
||||
}
|
||||
|
||||
func containsPlatformScope(values []string, expected string) bool {
|
||||
for _, value := range values {
|
||||
if value == expected {
|
||||
|
||||
@@ -1248,6 +1248,7 @@ type DockerData struct {
|
||||
type PBSData struct {
|
||||
InstanceID string `json:"instanceId,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
NodeName string `json:"nodeName,omitempty"`
|
||||
HostURL string `json:"hostUrl,omitempty"`
|
||||
GuestURL string `json:"guestUrl,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user