Stabilize guest threshold overrides across node moves (#1334)
Some checks are pending
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
Core E2E Tests / Playwright Core E2E (push) Waiting to run

This commit is contained in:
rcourtman 2026-03-31 23:18:19 +01:00
parent 70a15e0801
commit 31753e5536
6 changed files with 332 additions and 21 deletions

View file

@ -486,9 +486,12 @@ export const extractTriggerValues = (
export const normalizeRawOverrideConfigKeys = (
rawOverrides: Record<string, RawOverrideConfig>,
storage: Storage[] = [],
guests: Array<Pick<VM, 'id' | 'instance' | 'node' | 'vmid'> | Pick<Container, 'id' | 'instance' | 'node' | 'vmid'>> = [],
): Record<string, RawOverrideConfig> => {
const normalized: Record<string, RawOverrideConfig> = {};
const sharedStorageLegacyKeyMap = new Map<string, string>();
const guestLegacyKeyMap = new Map<string, string>();
const guestLegacyMatchers: Array<{ canonicalID: string; instance: string; vmid: number }> = [];
storage.forEach((entry) => {
if (!entry.shared || !entry.id || !entry.name) {
@ -504,6 +507,21 @@ export const normalizeRawOverrideConfigKeys = (
});
});
guests.forEach((guest) => {
const instance = guest.instance?.trim() || guest.node?.trim() || '';
const node = guest.node?.trim() || '';
const canonicalID = guest.id || `${instance}:${node}:${guest.vmid}`;
if (!instance || !node || !guest.vmid || !canonicalID) {
return;
}
guestLegacyKeyMap.set(`${instance}-${guest.vmid}`, canonicalID);
if (instance !== node) {
guestLegacyKeyMap.set(`${instance}-${node}-${guest.vmid}`, canonicalID);
guestLegacyMatchers.push({ canonicalID, instance, vmid: guest.vmid });
}
});
for (const [rawKey, value] of Object.entries(rawOverrides || {})) {
let key = rawKey;
@ -522,6 +540,18 @@ export const normalizeRawOverrideConfigKeys = (
key = sharedStorageKey;
}
const guestKey = guestLegacyKeyMap.get(key);
if (guestKey) {
key = guestKey;
} else {
const matchedGuest = guestLegacyMatchers.find(({ instance, vmid }) =>
key.startsWith(`${instance}-`) && key.endsWith(`-${vmid}`),
);
if (matchedGuest) {
key = matchedGuest.canonicalID;
}
}
normalized[key] = value;
}
@ -689,7 +719,11 @@ export function Alerts() {
createEffect(() => {
const currentRawOverrides = rawOverridesConfig();
const normalized = normalizeRawOverrideConfigKeys(currentRawOverrides, state.storage || []);
const normalized = normalizeRawOverrideConfigKeys(
currentRawOverrides,
state.storage || [],
[...(state.vms || []), ...(state.containers || [])],
);
if (JSON.stringify(normalized) !== JSON.stringify(currentRawOverrides)) {
setRawOverridesConfig(normalized);
}
@ -1304,7 +1338,13 @@ export function Alerts() {
setDisableAllPMGOffline(config.disableAllPMGOffline ?? false);
setDisableAllDockerHostsOffline(config.disableAllDockerHostsOffline ?? false);
setRawOverridesConfig(normalizeRawOverrideConfigKeys(config.overrides || {}, state.storage || []));
setRawOverridesConfig(
normalizeRawOverrideConfigKeys(
config.overrides || {},
state.storage || [],
[...(state.vms || []), ...(state.containers || [])],
),
);
if (config.schedule) {
if (config.schedule.quietHours) {

View file

@ -227,4 +227,38 @@ describe('threshold helper utilities', () => {
},
});
});
it('maps stable clustered guest override keys onto the current canonical guest id', () => {
expect(normalizeRawOverrideConfigKeys({
'Main-101': {
cpu: { trigger: 95, clear: 90 },
},
}, [], [{
id: 'Main:node2:101',
instance: 'Main',
node: 'node2',
vmid: 101,
} as any])).toEqual({
'Main:node2:101': {
cpu: { trigger: 95, clear: 90 },
},
});
});
it('maps legacy clustered instance-node-vmid guest override keys onto the current canonical guest id', () => {
expect(normalizeRawOverrideConfigKeys({
'Main-node1-101': {
memory: { trigger: 96, clear: 91 },
},
}, [], [{
id: 'Main:node2:101',
instance: 'Main',
node: 'node2',
vmid: 101,
} as any])).toEqual({
'Main:node2:101': {
memory: { trigger: 96, clear: 91 },
},
});
});
});