mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-20 11:46:11 +00:00
Some checks are pending
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
Canonical Governance / governance (push) Waiting to run
Patrol Qualification Regression / Catalog, scorer, and replay regression (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 8/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 4/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 5/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 6/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 7/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 1/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 2/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 3/8) (push) Waiting to run
Core E2E Tests / Agent registration lifecycle (push) Waiting to run
Core E2E Tests / E2E verdict (push) Blocked by required conditions
Unified Agent Native Verification / Linux ARM64 (push) Waiting to run
Unified Agent Native Verification / Linux x64 (push) Waiting to run
Unified Agent Native Verification / Windows x64 (push) Waiting to run
Unified Agent Native Verification / macOS ARM64 (push) Waiting to run
Unified Agent Native Verification / macOS Intel (push) Waiting to run
Unified Agent Native Verification / FreeBSD cross-build contract (push) Waiting to run
163 lines
6 KiB
TypeScript
163 lines
6 KiB
TypeScript
import type {
|
|
ClusterEndpoint,
|
|
ClusterEndpointOverridePayload,
|
|
ClusterNodeDisplayNameOverridePayload,
|
|
NodeConfig,
|
|
} from '@/types/nodes';
|
|
import type { SecurityStatus } from '@/types/config';
|
|
import type { NodeModalNodeType } from '@/utils/nodeModalPresentation';
|
|
|
|
export interface NodeModalProps {
|
|
isOpen: boolean;
|
|
resetKey?: number;
|
|
onClose: () => void;
|
|
nodeType: NodeModalNodeType;
|
|
editingNode?: NodeConfig;
|
|
prefillNode?: Partial<NodeConfig>;
|
|
onSave: (nodeData: Partial<NodeConfig>) => void;
|
|
showBackToDiscovery?: boolean;
|
|
onBackToDiscovery?: () => void;
|
|
securityStatus?: Partial<SecurityStatus>;
|
|
temperatureMonitoringEnabled?: boolean;
|
|
temperatureMonitoringLocked?: boolean;
|
|
savingTemperatureSetting?: boolean;
|
|
onToggleTemperatureMonitoring?: (enabled: boolean) => Promise<void> | void;
|
|
setupHandoffDisabled?: () => boolean;
|
|
setupHandoffDisabledReason?: string;
|
|
}
|
|
|
|
// Build the write-only clusterEndpointOverrides payload from the form's
|
|
// per-member address record, including only members whose value actually
|
|
// changed from the saved override. Returns undefined when nothing changed so
|
|
// the PUT stays a no-op for endpoints (partial-update semantics).
|
|
export const buildClusterEndpointOverridesPayload = (
|
|
endpoints: ClusterEndpoint[] | undefined,
|
|
overrides: Record<string, string>,
|
|
): ClusterEndpointOverridePayload[] | undefined => {
|
|
if (!endpoints?.length) return undefined;
|
|
const changed = endpoints
|
|
.filter((endpoint) => {
|
|
const value = overrides[endpoint.nodeName];
|
|
if (value === undefined) return false;
|
|
return value.trim() !== (endpoint.ipOverride ?? '');
|
|
})
|
|
.map((endpoint) => ({
|
|
nodeName: endpoint.nodeName,
|
|
ipOverride: (overrides[endpoint.nodeName] ?? '').trim(),
|
|
}));
|
|
return changed.length > 0 ? changed : undefined;
|
|
};
|
|
|
|
// Mirror a saved clusterEndpointOverrides payload onto locally cached
|
|
// endpoints so the editor reflects the save without a full nodes reload.
|
|
export const applyClusterEndpointOverridesLocally = (
|
|
endpoints: ClusterEndpoint[] | undefined,
|
|
overrides: ClusterEndpointOverridePayload[] | undefined,
|
|
): ClusterEndpoint[] | undefined => {
|
|
if (!endpoints?.length || !overrides?.length) return endpoints;
|
|
const byNodeName = new Map(overrides.map((override) => [override.nodeName, override.ipOverride]));
|
|
return endpoints.map((endpoint) => {
|
|
const value = byNodeName.get(endpoint.nodeName);
|
|
if (value === undefined) return endpoint;
|
|
return { ...endpoint, ipOverride: value || undefined };
|
|
});
|
|
};
|
|
|
|
export const buildClusterNodeDisplayNameOverridesPayload = (
|
|
endpoints: ClusterEndpoint[] | undefined,
|
|
displayNames: Record<string, string>,
|
|
): ClusterNodeDisplayNameOverridePayload[] | undefined => {
|
|
if (!endpoints?.length) return undefined;
|
|
const changed = endpoints.flatMap((endpoint) => {
|
|
const nodeIdentity = endpoint.nodeIdentity;
|
|
if (!nodeIdentity) return [];
|
|
const value = displayNames[nodeIdentity];
|
|
if (value === undefined || value.trim() === (endpoint.displayName ?? '')) return [];
|
|
return [{ nodeIdentity, displayName: value.trim() }];
|
|
});
|
|
return changed.length > 0 ? changed : undefined;
|
|
};
|
|
|
|
export const applyClusterNodeDisplayNamesLocally = (
|
|
endpoints: ClusterEndpoint[] | undefined,
|
|
overrides: ClusterNodeDisplayNameOverridePayload[] | undefined,
|
|
): ClusterEndpoint[] | undefined => {
|
|
if (!endpoints?.length || !overrides?.length) return endpoints;
|
|
const byIdentity = new Map(
|
|
overrides.map((override) => [override.nodeIdentity, override.displayName]),
|
|
);
|
|
return endpoints.map((endpoint) => {
|
|
if (!endpoint.nodeIdentity) return endpoint;
|
|
const value = byIdentity.get(endpoint.nodeIdentity);
|
|
if (value === undefined) return endpoint;
|
|
return { ...endpoint, displayName: value || undefined };
|
|
});
|
|
};
|
|
|
|
export const deriveNameFromHost = (host: string): string => {
|
|
let value = host.trim();
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
|
|
try {
|
|
const url = value.includes('://') ? new URL(value) : new URL(`https://${value}`);
|
|
value = url.hostname || value;
|
|
} catch {
|
|
value = value.replace(/^https?:\/\//, '');
|
|
}
|
|
|
|
value = value.replace(/\/.*$/, '').replace(/^\[(.*)\]$/, '$1');
|
|
value = value.replace(/\s+/g, '-');
|
|
|
|
return value;
|
|
};
|
|
|
|
export const PVE_MANUAL_PERMISSION_COMMAND = `# Apply monitoring permissions - use built-in PVEAuditor role
|
|
pveum aclmod / -user pulse-monitor@pve -role PVEAuditor
|
|
|
|
# Gather additional privileges for VM metrics
|
|
EXTRA_PRIVS=()
|
|
|
|
# Sys.Audit (Ceph, cluster status)
|
|
if pveum role list 2>/dev/null | grep -q "Sys.Audit"; then
|
|
EXTRA_PRIVS+=("Sys.Audit")
|
|
else
|
|
if pveum role add PulseTmpSysAudit -privs Sys.Audit 2>/dev/null; then
|
|
EXTRA_PRIVS+=("Sys.Audit")
|
|
pveum role delete PulseTmpSysAudit 2>/dev/null
|
|
fi
|
|
fi
|
|
|
|
# VM guest agent (PVE 9+) / monitor (PVE 8) privileges
|
|
HAS_GUEST_AGENT_AUDIT=false
|
|
if pveum role list 2>/dev/null | grep -q "VM.GuestAgent.Audit"; then
|
|
HAS_GUEST_AGENT_AUDIT=true
|
|
elif pveum role add PulseTmpGuestAudit -privs VM.GuestAgent.Audit 2>/dev/null; then
|
|
HAS_GUEST_AGENT_AUDIT=true
|
|
pveum role delete PulseTmpGuestAudit 2>/dev/null
|
|
fi
|
|
|
|
if [ "$HAS_GUEST_AGENT_AUDIT" = true ]; then
|
|
EXTRA_PRIVS+=("VM.GuestAgent.Audit")
|
|
|
|
if pveum role list 2>/dev/null | grep -q "VM.GuestAgent.FileRead"; then
|
|
EXTRA_PRIVS+=("VM.GuestAgent.FileRead")
|
|
elif pveum role add PulseTmpGuestFileRead -privs VM.GuestAgent.FileRead 2>/dev/null; then
|
|
EXTRA_PRIVS+=("VM.GuestAgent.FileRead")
|
|
pveum role delete PulseTmpGuestFileRead 2>/dev/null
|
|
fi
|
|
else
|
|
if pveum role list 2>/dev/null | grep -q "VM.Monitor"; then
|
|
EXTRA_PRIVS+=("VM.Monitor")
|
|
elif pveum role add PulseTmpVMMonitor -privs VM.Monitor 2>/dev/null; then
|
|
EXTRA_PRIVS+=("VM.Monitor")
|
|
pveum role delete PulseTmpVMMonitor 2>/dev/null
|
|
fi
|
|
fi
|
|
|
|
if [ \${#EXTRA_PRIVS[@]} -gt 0 ]; then
|
|
PRIV_STRING="$(IFS=,; echo "\${EXTRA_PRIVS[*]}")"
|
|
pveum role modify PulseMonitor -privs "$PRIV_STRING" 2>/dev/null || pveum role add PulseMonitor -privs "$PRIV_STRING" 2>/dev/null
|
|
pveum aclmod / -user pulse-monitor@pve -role PulseMonitor
|
|
fi`;
|