mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-25 08:52:06 +00:00
Carry vSphere VM hardware config
Project vCenter VM hardware, CPU, memory, and boot configuration through the VMware resource facet and shared vSphere details so operators can inspect virtual hardware posture as read-only monitoring context.
This commit is contained in:
parent
add3984f25
commit
9f98f8fcb9
23 changed files with 1082 additions and 22 deletions
|
|
@ -736,7 +736,7 @@ a separate Docker-only or TrueNAS-local inventory path.
|
|||
27. Keep alert-side recovery drill-ins on that same shared route-helper contract. When alert investigation surfaces such as resource-incident panels expose recovery follow-up links for TrueNAS or future API-backed platforms, they must route through the canonical `frontend-modern/src/routing/resourceLinks.ts` recovery builder instead of freezing alert-local recovery URLs or introducing another provider-shaped recovery handoff vocabulary.
|
||||
28. Keep VMware onboarding runtime and recovery semantics separate on that same adjacent platform-connections contract. When `internal/api/router.go`, `internal/api/router_routes_registration.go`, or `internal/api/vmware_handlers.go` evolve VMware connection CRUD, poller-owned `poll` / `observed` summary payloads, saved-test refresh, or observed datastore/VM snapshot visibility, storage and recovery may consume the resulting shared context but must not treat those onboarding/runtime payloads as canonical recovery artifacts, restore capability, or recovery-local control transport.
|
||||
29. Keep VMware datastore projection on the shared unified-resource and storage-source contracts. When `frontend-modern/src/hooks/useUnifiedResources.ts` or shared `internal/api/router.go` wiring starts surfacing VMware-backed canonical `storage` resources, storage and recovery may expose those datastores through the owned `vmware-vsphere` source/platform vocabulary for inventory, capacity, and handoff flows only; they must not reinterpret that projection as VMware recovery support, restore semantics, or a provider-local protection surface.
|
||||
30. Keep VMware placement, guest-detail, VM snapshot-tree, VMware Tools, VM hardware Ethernet, and VM hardware disk enrichment descriptive on that same shared unified-resource contract. When `internal/vmware/provider.go`, `internal/unifiedresources/types.go`, and `frontend-modern/src/hooks/useUnifiedResources.ts` project datacenter, cluster, folder, runtime-host, datastore-attachment, guest-hostname, guest-IP, `vmware.currentSnapshotId`, `vmware.snapshotTree`, snapshot creation/state/quiesce/current markers, child snapshot metadata, `vmware.tools`, Tools run state, version status, version number/string, install type, upgrade policy, auto-update support, install-attempt count, guest reboot requests, `vmware.networkAdapters`, adapter MAC address/type, backing network id/name, backing type, connection state, start-connected / guest-control flags, `vmware.virtualDisks`, virtual disk label/type, IDE/SCSI/SATA/NVMe placement, VMDK path, backing type, datastore name, or capacity onto canonical VMware `agent` / `vm` / `storage` resources, storage and recovery may use that detail for labeling, navigation, and VM investigation context only; they must not promote those topology, guest, snapshot-tree, VMware Tools, vNIC, or virtual disk fields into recovery ownership, restore targeting, protection grouping, compliance scoring, or a VMware-local recovery taxonomy without a separately governed slice.
|
||||
30. Keep VMware placement, guest-detail, VM snapshot-tree, VM virtual-hardware configuration, VMware Tools, VM hardware Ethernet, and VM hardware disk enrichment descriptive on that same shared unified-resource contract. When `internal/vmware/provider.go`, `internal/unifiedresources/types.go`, and `frontend-modern/src/hooks/useUnifiedResources.ts` project datacenter, cluster, folder, runtime-host, datastore-attachment, guest-hostname, guest-IP, `vmware.currentSnapshotId`, `vmware.snapshotTree`, snapshot creation/state/quiesce/current markers, child snapshot metadata, `vmware.hardware`, virtual hardware version, hardware upgrade policy/version/status/error, boot type/order/retry/setup-mode flags, CPU cores-per-socket and hot-add/remove flags, memory hot-add settings, `vmware.tools`, Tools run state, version status, version number/string, install type, upgrade policy, auto-update support, install-attempt count, guest reboot requests, `vmware.networkAdapters`, adapter MAC address/type, backing network id/name, backing type, connection state, start-connected / guest-control flags, `vmware.virtualDisks`, virtual disk label/type, IDE/SCSI/SATA/NVMe placement, VMDK path, backing type, datastore name, or capacity onto canonical VMware `agent` / `vm` / `storage` resources, storage and recovery may use that detail for labeling, navigation, and VM investigation context only; they must not promote those topology, guest, snapshot-tree, virtual-hardware, VMware Tools, vNIC, or virtual disk fields into recovery ownership, restore targeting, protection grouping, compliance scoring, or a VMware-local recovery taxonomy without a separately governed slice.
|
||||
31. Keep VMware datastore classification neutral on the shared storage adapter contract. When `frontend-modern/src/features/storageBackups/resourceStorageMapping.ts`, `frontend-modern/src/features/storageBackups/resourceStoragePresentation.ts`, and `frontend-modern/src/features/storageBackups/storageAdapters.ts` evolve canonical storage-record mapping, VMware-backed datastores must continue to land on the shared storage route as inventory-only datastores with neutral protection fallback, not as backup repositories, backup targets, or recovery-protected resources.
|
||||
That same shared storage adapter boundary also owns canonical platform
|
||||
family vocabulary through the governed platform manifest.
|
||||
|
|
@ -3157,18 +3157,20 @@ Storage and recovery must not infer VMware restore support, recovery rollups,
|
|||
or VMware-local protection semantics from the presence of those datastores or
|
||||
VM snapshot-read context on the shared pages.
|
||||
That same shared adapter floor also now carries richer VMware placement,
|
||||
guest-detail, and VMware Tools metadata through the canonical `agent` / `vm` /
|
||||
`storage` resources that storage and recovery can inspect on shared pages.
|
||||
guest-detail, VM virtual-hardware, and VMware Tools metadata through the
|
||||
canonical `agent` / `vm` / `storage` resources that storage and recovery can
|
||||
inspect on shared pages.
|
||||
`internal/vmware/provider.go`, `internal/unifiedresources/types.go`, and
|
||||
`frontend-modern/src/hooks/useUnifiedResources.ts` may expose datacenter,
|
||||
cluster, folder, runtime-host, datastore-attachment, guest-hostname, and
|
||||
guest-IP detail plus VMware Tools run-state, version, policy, install-attempt,
|
||||
error, and guest-reboot context as inventory context, but those fields stay
|
||||
descriptive only. Storage and recovery must not treat topology labels,
|
||||
datastore attachments, guest identity, or VMware Tools posture as recovery
|
||||
ownership, restore targeting, protection grouping, or a new VMware-local
|
||||
storage/recovery taxonomy until a separately governed slice explicitly promotes
|
||||
them into recovery contracts.
|
||||
guest-IP detail plus VM virtual-hardware version, boot, CPU/memory hot-add,
|
||||
VMware Tools run-state, version, policy, install-attempt, error, and
|
||||
guest-reboot context as inventory context, but those fields stay descriptive
|
||||
only. Storage and recovery must not treat topology labels, datastore
|
||||
attachments, guest identity, virtual-hardware posture, or VMware Tools posture
|
||||
as recovery ownership, restore targeting, protection grouping, or a new
|
||||
VMware-local storage/recovery taxonomy until a separately governed slice
|
||||
explicitly promotes them into recovery contracts.
|
||||
That same storage/recovery surface now also owns physical-disk live I/O
|
||||
presentation through the canonical chart boundary. Storage disk drawers may
|
||||
show read, write, busy, and SMART history, but they must route every chart
|
||||
|
|
|
|||
|
|
@ -1151,9 +1151,10 @@ That same topology contract now also has a concrete projection seam.
|
|||
`internal/vmware/provider.go` must preserve VMware placement and identity
|
||||
detail on the shared `vmware` facet only: hosts may carry datacenter,
|
||||
compute-resource, cluster, folder, and attached-datastore metadata; VMs may
|
||||
carry runtime-host, folder, resource-pool, datastore, guest-identity, VMware
|
||||
Tools runtime status, and VM hardware Ethernet adapter plus VM hardware disk
|
||||
metadata plus canonical parentage to the owning ESXi `agent`; datastores may
|
||||
carry runtime-host, folder, resource-pool, datastore, guest-identity, VM
|
||||
virtual-hardware configuration, VMware Tools runtime status, and VM hardware
|
||||
Ethernet adapter plus VM hardware disk metadata plus canonical parentage to the
|
||||
owning ESXi `agent`; datastores may
|
||||
carry datacenter/folder placement plus shared storage-node and workload
|
||||
consumer metadata through `storage.nodes`, `storage.consumerCount`, and
|
||||
`storage.topConsumers`. VMs may also carry VI JSON snapshot-tree context under
|
||||
|
|
@ -1176,6 +1177,14 @@ request flag, requesting components, and request time. Those fields are
|
|||
operator-facing monitoring facts from vCenter's VM Tools API; they must not be
|
||||
promoted into lifecycle control, recovery status, workload identity, or a
|
||||
separate guest-runtime resource.
|
||||
VM virtual-hardware configuration belongs under `vmware.hardware`, including
|
||||
vCenter VM guest OS, instant-clone frozen flag, virtual hardware version,
|
||||
hardware upgrade policy/version/status/error, boot type, EFI legacy boot,
|
||||
network boot protocol, boot delay/retry/setup-mode flags, boot-device order,
|
||||
CPU cores-per-socket and CPU hot-add/remove flags, and memory hot-add
|
||||
increment/limit settings. Those fields are API-native read-only VM
|
||||
configuration facts; they must not become Pulse VM-control authority, workload
|
||||
identity aliases, recovery protection posture, or a separate hardware resource.
|
||||
Those enrichments must remain subordinate to shared `agent`, `vm`, and
|
||||
`storage` resources rather than becoming a VMware-only topology graph, recovery
|
||||
artifact, canonical identity alias, or separate provider detail drawer
|
||||
|
|
|
|||
|
|
@ -163,6 +163,67 @@ describe('toDiscoveryConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not synthesize service discovery for VMware VMs without a backend target', () => {
|
||||
const resource: Resource = {
|
||||
...baseResource(),
|
||||
id: 'vm-270aaf3d37cc7501',
|
||||
type: 'vm',
|
||||
name: 'warehouse-api-01',
|
||||
platformType: 'vmware-vsphere',
|
||||
platformScopes: ['vmware-vsphere'],
|
||||
sources: ['vmware'],
|
||||
parentName: 'redacted by policy',
|
||||
platformData: {
|
||||
sources: ['vmware'],
|
||||
vmware: {
|
||||
connectionName: 'Lab vCenter',
|
||||
clusterName: 'redacted by policy',
|
||||
runtimeHostName: 'esxi-01.lab.local',
|
||||
},
|
||||
},
|
||||
vmware: {
|
||||
connectionName: 'Lab vCenter',
|
||||
entityType: 'VirtualMachine',
|
||||
managedObjectId: 'vm-201',
|
||||
},
|
||||
};
|
||||
|
||||
expect(toDiscoveryConfig(resource)).toBeNull();
|
||||
});
|
||||
|
||||
it('still uses explicit backend discovery targets for VMware VMs', () => {
|
||||
const resource: Resource = {
|
||||
...baseResource(),
|
||||
id: 'vm-270aaf3d37cc7501',
|
||||
type: 'vm',
|
||||
name: 'warehouse-api-01',
|
||||
platformType: 'vmware-vsphere',
|
||||
platformScopes: ['vmware-vsphere'],
|
||||
sources: ['vmware'],
|
||||
discoveryTarget: {
|
||||
resourceType: 'vm',
|
||||
agentId: 'guest-agent-01',
|
||||
resourceId: 'vm-201',
|
||||
hostname: 'warehouse-api-01.internal',
|
||||
},
|
||||
vmware: {
|
||||
connectionName: 'Lab vCenter',
|
||||
entityType: 'VirtualMachine',
|
||||
managedObjectId: 'vm-201',
|
||||
},
|
||||
};
|
||||
|
||||
expect(toDiscoveryConfig(resource)).toEqual({
|
||||
resourceType: 'vm',
|
||||
agentId: 'guest-agent-01',
|
||||
resourceId: 'vm-201',
|
||||
hostname: 'warehouse-api-01.internal',
|
||||
metadataKind: 'guest',
|
||||
metadataId: 'vm-201',
|
||||
targetLabel: 'guest',
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers docker hostSourceId for app-container fallback agentId', () => {
|
||||
const resource: Resource = {
|
||||
...baseResource(),
|
||||
|
|
|
|||
|
|
@ -347,6 +347,33 @@ describe('ResourceDetailDrawer runtime and identity cards', () => {
|
|||
guestRebootComponents: ['drivers'],
|
||||
guestRebootRequestTime: '2026-03-30T18:20:00Z',
|
||||
},
|
||||
hardware: {
|
||||
guestOs: 'UBUNTU_64',
|
||||
instantCloneFrozen: false,
|
||||
version: 'VMX_20',
|
||||
upgradePolicy: 'AFTER_CLEAN_SHUTDOWN',
|
||||
upgradeVersion: 'VMX_21',
|
||||
upgradeStatus: 'PENDING',
|
||||
bootType: 'EFI',
|
||||
efiLegacyBoot: false,
|
||||
bootNetworkProtocol: 'IPV4',
|
||||
bootDelayMilliseconds: 5000,
|
||||
bootRetry: true,
|
||||
bootRetryDelayMilliseconds: 10000,
|
||||
enterSetupMode: false,
|
||||
bootDevices: [
|
||||
{ type: 'DISK', disks: ['2000'] },
|
||||
{ type: 'ETHERNET', nic: '4000' },
|
||||
],
|
||||
cpuCoresPerSocket: 2,
|
||||
cpuHotAddEnabled: true,
|
||||
cpuHotRemoveEnabled: false,
|
||||
memoryHotAddEnabled: true,
|
||||
memoryHotAddIncrementMib: 256,
|
||||
memoryHotAddLimitMib: 16384,
|
||||
},
|
||||
cpuCount: 4,
|
||||
memorySizeMib: 8192,
|
||||
activeAlarmCount: 1,
|
||||
activeAlarmSummary: 'Host fan degraded',
|
||||
recentTaskCount: 1,
|
||||
|
|
@ -426,6 +453,33 @@ describe('ResourceDetailDrawer runtime and identity cards', () => {
|
|||
guestRebootComponents: ['drivers'],
|
||||
guestRebootRequestTime: '2026-03-30T18:20:00Z',
|
||||
},
|
||||
hardware: {
|
||||
guestOs: 'UBUNTU_64',
|
||||
instantCloneFrozen: false,
|
||||
version: 'VMX_20',
|
||||
upgradePolicy: 'AFTER_CLEAN_SHUTDOWN',
|
||||
upgradeVersion: 'VMX_21',
|
||||
upgradeStatus: 'PENDING',
|
||||
bootType: 'EFI',
|
||||
efiLegacyBoot: false,
|
||||
bootNetworkProtocol: 'IPV4',
|
||||
bootDelayMilliseconds: 5000,
|
||||
bootRetry: true,
|
||||
bootRetryDelayMilliseconds: 10000,
|
||||
enterSetupMode: false,
|
||||
bootDevices: [
|
||||
{ type: 'DISK', disks: ['2000'] },
|
||||
{ type: 'ETHERNET', nic: '4000' },
|
||||
],
|
||||
cpuCoresPerSocket: 2,
|
||||
cpuHotAddEnabled: true,
|
||||
cpuHotRemoveEnabled: false,
|
||||
memoryHotAddEnabled: true,
|
||||
memoryHotAddIncrementMib: 256,
|
||||
memoryHotAddLimitMib: 16384,
|
||||
},
|
||||
cpuCount: 4,
|
||||
memorySizeMib: 8192,
|
||||
activeAlarmCount: 1,
|
||||
activeAlarmSummary: 'Host fan degraded',
|
||||
recentTaskCount: 1,
|
||||
|
|
@ -462,7 +516,7 @@ describe('ResourceDetailDrawer runtime and identity cards', () => {
|
|||
expect(getByTestId('resource-vmware-details-section')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(
|
||||
'Lab VC · Read-only vCenter context · 2 snapshots · 1 vNIC · 1 disk · Tools reboot requested · 1 alarm · 1 task',
|
||||
'Lab VC · Read-only vCenter context · 2 snapshots · 1 vNIC · 1 disk · Hardware pending · Tools reboot requested · 1 alarm · 1 task',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(queryByText('Compute Cluster')).toBeNull();
|
||||
|
|
@ -478,6 +532,7 @@ describe('ResourceDetailDrawer runtime and identity cards', () => {
|
|||
expect(within(section).getByText('State')).toBeInTheDocument();
|
||||
expect(within(section).getByText('Placement')).toBeInTheDocument();
|
||||
expect(within(section).getByText('Guest')).toBeInTheDocument();
|
||||
expect(within(section).getByText('Virtual hardware')).toBeInTheDocument();
|
||||
expect(within(section).getByText('VMware Tools')).toBeInTheDocument();
|
||||
expect(within(section).getByText('Virtual disks')).toBeInTheDocument();
|
||||
expect(within(section).getByText('Network')).toBeInTheDocument();
|
||||
|
|
@ -487,6 +542,14 @@ describe('ResourceDetailDrawer runtime and identity cards', () => {
|
|||
expect(within(section).getByText('Compute Cluster')).toBeInTheDocument();
|
||||
expect(within(section).getByText('esxi-01.lab.local')).toBeInTheDocument();
|
||||
expect(within(section).getByText('ubuntu64Guest')).toBeInTheDocument();
|
||||
expect(within(section).getByText('Hardware version')).toBeInTheDocument();
|
||||
expect(within(section).getByText('VMX 20')).toBeInTheDocument();
|
||||
expect(within(section).getByText('Upgrade status')).toBeInTheDocument();
|
||||
expect(within(section).getByText('Pending')).toBeInTheDocument();
|
||||
expect(within(section).getByText('CPU topology')).toBeInTheDocument();
|
||||
expect(within(section).getByText('4 vCPU · 2 cores/socket')).toBeInTheDocument();
|
||||
expect(within(section).getByText('Boot order')).toBeInTheDocument();
|
||||
expect(within(section).getByText('Disk 2000 -> Ethernet 4000')).toBeInTheDocument();
|
||||
expect(within(section).getByText('Run state')).toBeInTheDocument();
|
||||
expect(within(section).getByText('Running')).toBeInTheDocument();
|
||||
expect(within(section).getByText('Version status')).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -50,6 +50,33 @@ describe('resourceDetailDrawerVmwareModel', () => {
|
|||
guestRebootComponents: ['drivers'],
|
||||
guestRebootRequestTime: '2026-03-30T18:20:00Z',
|
||||
},
|
||||
hardware: {
|
||||
guestOs: 'UBUNTU_64',
|
||||
instantCloneFrozen: false,
|
||||
version: 'VMX_20',
|
||||
upgradePolicy: 'AFTER_CLEAN_SHUTDOWN',
|
||||
upgradeVersion: 'VMX_21',
|
||||
upgradeStatus: 'PENDING',
|
||||
bootType: 'EFI',
|
||||
efiLegacyBoot: false,
|
||||
bootNetworkProtocol: 'IPV4',
|
||||
bootDelayMilliseconds: 5000,
|
||||
bootRetry: true,
|
||||
bootRetryDelayMilliseconds: 10000,
|
||||
enterSetupMode: false,
|
||||
bootDevices: [
|
||||
{ type: 'DISK', disks: ['2000'] },
|
||||
{ type: 'ETHERNET', nic: '4000' },
|
||||
],
|
||||
cpuCoresPerSocket: 2,
|
||||
cpuHotAddEnabled: true,
|
||||
cpuHotRemoveEnabled: false,
|
||||
memoryHotAddEnabled: true,
|
||||
memoryHotAddIncrementMib: 256,
|
||||
memoryHotAddLimitMib: 16384,
|
||||
},
|
||||
cpuCount: 4,
|
||||
memorySizeMib: 8192,
|
||||
snapshotTree: [
|
||||
{
|
||||
snapshot: 'snapshot-201',
|
||||
|
|
@ -72,9 +99,37 @@ describe('resourceDetailDrawerVmwareModel', () => {
|
|||
};
|
||||
|
||||
expect(buildVMwareDetailsSummary('vm', vmware)).toBe(
|
||||
'Lab VC · Read-only vCenter context · 2 snapshots · 1 vNIC · 1 disk · Tools reboot requested',
|
||||
'Lab VC · Read-only vCenter context · 2 snapshots · 1 vNIC · 1 disk · Hardware pending · Tools reboot requested',
|
||||
);
|
||||
|
||||
const hardware = buildVMwareDetailSections('vm', vmware).find(
|
||||
(section) => section.id === 'hardware',
|
||||
);
|
||||
|
||||
expect(hardware?.rows).toEqual([
|
||||
{ label: 'Guest OS', value: 'Ubuntu 64' },
|
||||
{ label: 'Hardware version', value: 'VMX 20' },
|
||||
{ label: 'Upgrade status', value: 'Pending', tone: 'warning' },
|
||||
{ label: 'Upgrade policy', value: 'After Clean Shutdown' },
|
||||
{ label: 'Upgrade target', value: 'VMX 21' },
|
||||
{ label: 'Instant clone frozen', value: 'No', tone: 'default' },
|
||||
{ label: 'CPU topology', value: '4 vCPU · 2 cores/socket' },
|
||||
{ label: 'CPU hot-add', value: 'Yes' },
|
||||
{ label: 'CPU hot-remove', value: 'No' },
|
||||
{ label: 'Memory size', value: '8 GB' },
|
||||
{ label: 'Memory hot-add', value: 'Yes' },
|
||||
{ label: 'Memory hot-add increment', value: '256 MB' },
|
||||
{ label: 'Memory hot-add limit', value: '16 GB' },
|
||||
{ label: 'Boot type', value: 'EFI' },
|
||||
{ label: 'EFI legacy boot', value: 'No' },
|
||||
{ label: 'Boot network protocol', value: 'IPv4' },
|
||||
{ label: 'Boot delay', value: '5000 ms' },
|
||||
{ label: 'Boot retry', value: 'Yes' },
|
||||
{ label: 'Boot retry delay', value: '10000 ms' },
|
||||
{ label: 'Enter setup mode', value: 'No', tone: 'default' },
|
||||
{ label: 'Boot order', value: 'Disk 2000 -> Ethernet 4000' },
|
||||
]);
|
||||
|
||||
const tools = buildVMwareDetailSections('vm', vmware).find((section) => section.id === 'tools');
|
||||
|
||||
expect(tools?.rows).toEqual([
|
||||
|
|
|
|||
|
|
@ -43,12 +43,14 @@ type KubernetesPlatformData = {
|
|||
};
|
||||
|
||||
type PlatformData = {
|
||||
sources?: string[];
|
||||
agent?: {
|
||||
hostname?: string;
|
||||
};
|
||||
docker?: DockerPlatformData;
|
||||
kubernetes?: KubernetesPlatformData;
|
||||
proxmox?: ProxmoxPlatformData;
|
||||
vmware?: unknown;
|
||||
};
|
||||
|
||||
const asString = (value: unknown): string | undefined =>
|
||||
|
|
@ -57,6 +59,26 @@ const asString = (value: unknown): string | undefined =>
|
|||
const asNumber = (value: unknown): number | undefined =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
||||
|
||||
const isDiscoveryLookupValue = (value: unknown): value is string => {
|
||||
const candidate = asString(value);
|
||||
return Boolean(candidate && candidate.toLowerCase() !== 'redacted by policy');
|
||||
};
|
||||
|
||||
const hasSource = (resource: Resource, platformData: PlatformData | undefined, source: string) => {
|
||||
const normalizedSource = source.toLowerCase();
|
||||
return [
|
||||
...(Array.isArray(resource.sources) ? resource.sources : []),
|
||||
...(Array.isArray(platformData?.sources) ? platformData.sources : []),
|
||||
...(Array.isArray(resource.platformScopes) ? resource.platformScopes : []),
|
||||
].some((candidate) => asString(candidate)?.toLowerCase() === normalizedSource);
|
||||
};
|
||||
|
||||
const hasVMwareScope = (resource: Resource, platformData: PlatformData | undefined): boolean =>
|
||||
resource.platformType === 'vmware-vsphere' ||
|
||||
hasSource(resource, platformData, 'vmware') ||
|
||||
hasSource(resource, platformData, 'vmware-vsphere') ||
|
||||
Boolean(resource.vmware || platformData?.vmware);
|
||||
|
||||
const getPreferredHostLabel = (resource: Resource): string =>
|
||||
getPreferredResourceHostname(resource) ||
|
||||
getPreferredInfrastructureDisplayName(resource) ||
|
||||
|
|
@ -67,12 +89,15 @@ export const toDiscoveryConfig = (resource: Resource): DiscoveryConfig | null =>
|
|||
const explicitDiscoveryAgentId = asString(
|
||||
(explicitDiscoveryTarget as { agentId?: unknown } | undefined)?.agentId,
|
||||
);
|
||||
const explicitDiscoveryResourceId = asString(
|
||||
(explicitDiscoveryTarget as { resourceId?: unknown } | undefined)?.resourceId,
|
||||
);
|
||||
|
||||
if (
|
||||
explicitDiscoveryTarget &&
|
||||
explicitDiscoveryTarget.resourceType &&
|
||||
explicitDiscoveryAgentId &&
|
||||
explicitDiscoveryTarget.resourceId
|
||||
isDiscoveryLookupValue(explicitDiscoveryAgentId) &&
|
||||
isDiscoveryLookupValue(explicitDiscoveryResourceId)
|
||||
) {
|
||||
const explicitResourceType = canonicalDiscoveryResourceType(
|
||||
explicitDiscoveryTarget.resourceType,
|
||||
|
|
@ -104,10 +129,10 @@ export const toDiscoveryConfig = (resource: Resource): DiscoveryConfig | null =>
|
|||
return {
|
||||
resourceType,
|
||||
agentId: explicitDiscoveryAgentId,
|
||||
resourceId: explicitDiscoveryTarget.resourceId,
|
||||
resourceId: explicitDiscoveryResourceId,
|
||||
hostname,
|
||||
metadataKind: isHostDiscovery ? 'agent' : 'guest',
|
||||
metadataId: explicitDiscoveryTarget.resourceId,
|
||||
metadataId: explicitDiscoveryResourceId,
|
||||
targetLabel,
|
||||
};
|
||||
}
|
||||
|
|
@ -177,6 +202,9 @@ export const toDiscoveryConfig = (resource: Resource): DiscoveryConfig | null =>
|
|||
case 'pmg':
|
||||
case 'k8s-cluster':
|
||||
case 'k8s-node':
|
||||
if (!isDiscoveryLookupValue(agentLookupId)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
resourceType: 'agent',
|
||||
agentId: agentLookupId,
|
||||
|
|
@ -187,6 +215,12 @@ export const toDiscoveryConfig = (resource: Resource): DiscoveryConfig | null =>
|
|||
targetLabel: 'agent',
|
||||
};
|
||||
case 'vm':
|
||||
if (hasVMwareScope(resource, platformData) && !(proxmoxNodeName && vmidResourceId)) {
|
||||
return null;
|
||||
}
|
||||
if (!isDiscoveryLookupValue(workloadAgentId)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
resourceType: 'vm',
|
||||
agentId: workloadAgentId,
|
||||
|
|
@ -198,6 +232,9 @@ export const toDiscoveryConfig = (resource: Resource): DiscoveryConfig | null =>
|
|||
};
|
||||
case 'system-container':
|
||||
case 'oci-container':
|
||||
if (!isDiscoveryLookupValue(workloadAgentId)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
resourceType: 'system-container',
|
||||
agentId: workloadAgentId,
|
||||
|
|
@ -208,6 +245,9 @@ export const toDiscoveryConfig = (resource: Resource): DiscoveryConfig | null =>
|
|||
targetLabel: 'guest',
|
||||
};
|
||||
case 'app-container':
|
||||
if (!isDiscoveryLookupValue(workloadAgentId)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
resourceType: 'app-container',
|
||||
agentId: workloadAgentId,
|
||||
|
|
@ -220,6 +260,9 @@ export const toDiscoveryConfig = (resource: Resource): DiscoveryConfig | null =>
|
|||
case 'pod':
|
||||
case 'k8s-deployment':
|
||||
case 'k8s-service':
|
||||
if (!isDiscoveryLookupValue(workloadAgentId)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
resourceType: 'pod',
|
||||
agentId: workloadAgentId,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import type {
|
||||
ResourceType,
|
||||
ResourceVMwareBootDevice,
|
||||
ResourceVMwareHardware,
|
||||
ResourceVMwareMeta,
|
||||
ResourceVMwareNetworkAdapter,
|
||||
ResourceVMwareSnapshot,
|
||||
|
|
@ -16,7 +18,16 @@ export type ResourceDetailDrawerVMwareRow = {
|
|||
};
|
||||
|
||||
export type ResourceDetailDrawerVMwareSection = {
|
||||
id: 'state' | 'placement' | 'guest' | 'tools' | 'disks' | 'network' | 'signals' | 'snapshots';
|
||||
id:
|
||||
| 'state'
|
||||
| 'placement'
|
||||
| 'guest'
|
||||
| 'hardware'
|
||||
| 'tools'
|
||||
| 'disks'
|
||||
| 'network'
|
||||
| 'signals'
|
||||
| 'snapshots';
|
||||
label: string;
|
||||
rows: ResourceDetailDrawerVMwareRow[];
|
||||
};
|
||||
|
|
@ -39,6 +50,10 @@ const VMWARE_ENUM_ACRONYMS: Record<string, string> = {
|
|||
OS: 'OS',
|
||||
VM: 'VM',
|
||||
VMDK: 'VMDK',
|
||||
VMX: 'VMX',
|
||||
EFI: 'EFI',
|
||||
IPV4: 'IPv4',
|
||||
IPV6: 'IPv6',
|
||||
};
|
||||
|
||||
const formatEnumLabel = (value?: string | null): string => {
|
||||
|
|
@ -69,6 +84,16 @@ const formatCapacityBytes = (value?: number): string => {
|
|||
return `${size.toFixed(precision)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
const formatMiB = (value?: number): string => {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return '';
|
||||
return formatCapacityBytes(value * 1024 * 1024).replace(/\.0 ([A-Z]+)/, ' $1');
|
||||
};
|
||||
|
||||
const formatMilliseconds = (value?: number): string => {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return '';
|
||||
return `${value} ms`;
|
||||
};
|
||||
|
||||
const countSnapshotTree = (snapshots?: ResourceVMwareSnapshot[]): number =>
|
||||
(snapshots ?? []).reduce(
|
||||
(total, snapshot) => total + 1 + countSnapshotTree(snapshot.children),
|
||||
|
|
@ -178,6 +203,148 @@ const filterNonEmptyRows = (
|
|||
const getWarningTone = (hasWarning: boolean): ResourceDetailDrawerVMwareRowTone =>
|
||||
hasWarning ? 'warning' : 'default';
|
||||
|
||||
const hardwareSummary = (hardware?: ResourceVMwareHardware): string => {
|
||||
if (!hardware) return '';
|
||||
const upgradeStatus = asTrimmedString(hardware.upgradeStatus);
|
||||
if (upgradeStatus && !['NONE', 'OK'].includes(upgradeStatus.toUpperCase())) {
|
||||
return `Hardware ${formatEnumLabel(upgradeStatus).toLowerCase()}`;
|
||||
}
|
||||
return formatEnumLabel(hardware.version);
|
||||
};
|
||||
|
||||
const bootDeviceLabel = (device: ResourceVMwareBootDevice): string => {
|
||||
const type = formatEnumLabel(device.type);
|
||||
const details = [
|
||||
asTrimmedString(device.nic),
|
||||
...(device.disks ?? []).map((disk) => asTrimmedString(disk)),
|
||||
].filter(Boolean);
|
||||
return details.length > 0 ? `${type} ${details.join(', ')}` : type;
|
||||
};
|
||||
|
||||
const bootOrderLabel = (devices?: ResourceVMwareBootDevice[]): string =>
|
||||
(devices ?? []).map(bootDeviceLabel).filter(Boolean).join(' -> ');
|
||||
|
||||
const cpuTopologyLabel = (vmware: ResourceVMwareMeta): string => {
|
||||
const parts = [];
|
||||
if (
|
||||
typeof vmware.cpuCount === 'number' &&
|
||||
Number.isFinite(vmware.cpuCount) &&
|
||||
vmware.cpuCount > 0
|
||||
) {
|
||||
parts.push(`${vmware.cpuCount} vCPU`);
|
||||
}
|
||||
if (
|
||||
typeof vmware.hardware?.cpuCoresPerSocket === 'number' &&
|
||||
Number.isFinite(vmware.hardware.cpuCoresPerSocket) &&
|
||||
vmware.hardware.cpuCoresPerSocket > 0
|
||||
) {
|
||||
parts.push(`${vmware.hardware.cpuCoresPerSocket} cores/socket`);
|
||||
}
|
||||
return parts.join(' · ');
|
||||
};
|
||||
|
||||
const hardwareRows = (vmware?: ResourceVMwareMeta): ResourceDetailDrawerVMwareRow[] => {
|
||||
if (!vmware?.hardware) return [];
|
||||
const hardware = vmware.hardware;
|
||||
return filterNonEmptyRows([
|
||||
{
|
||||
label: 'Guest OS',
|
||||
value: formatEnumLabel(hardware.guestOs),
|
||||
},
|
||||
{
|
||||
label: 'Hardware version',
|
||||
value: formatEnumLabel(hardware.version),
|
||||
},
|
||||
{
|
||||
label: 'Upgrade status',
|
||||
value: formatEnumLabel(hardware.upgradeStatus),
|
||||
tone: getWarningTone(
|
||||
Boolean(asTrimmedString(hardware.upgradeStatus)) &&
|
||||
!['NONE', 'OK'].includes(asTrimmedString(hardware.upgradeStatus).toUpperCase()),
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Upgrade policy',
|
||||
value: formatEnumLabel(hardware.upgradePolicy),
|
||||
},
|
||||
{
|
||||
label: 'Upgrade target',
|
||||
value: formatEnumLabel(hardware.upgradeVersion),
|
||||
},
|
||||
{
|
||||
label: 'Upgrade error',
|
||||
value: asTrimmedString(hardware.upgradeErrorMessage),
|
||||
tone: 'warning',
|
||||
},
|
||||
{
|
||||
label: 'Instant clone frozen',
|
||||
value: formatBoolLabel(hardware.instantCloneFrozen),
|
||||
tone: getWarningTone(hardware.instantCloneFrozen === true),
|
||||
},
|
||||
{
|
||||
label: 'CPU topology',
|
||||
value: cpuTopologyLabel(vmware),
|
||||
},
|
||||
{
|
||||
label: 'CPU hot-add',
|
||||
value: formatBoolLabel(hardware.cpuHotAddEnabled),
|
||||
},
|
||||
{
|
||||
label: 'CPU hot-remove',
|
||||
value: formatBoolLabel(hardware.cpuHotRemoveEnabled),
|
||||
},
|
||||
{
|
||||
label: 'Memory size',
|
||||
value: formatMiB(vmware?.memorySizeMib),
|
||||
},
|
||||
{
|
||||
label: 'Memory hot-add',
|
||||
value: formatBoolLabel(hardware.memoryHotAddEnabled),
|
||||
},
|
||||
{
|
||||
label: 'Memory hot-add increment',
|
||||
value: formatMiB(hardware.memoryHotAddIncrementMib),
|
||||
},
|
||||
{
|
||||
label: 'Memory hot-add limit',
|
||||
value: formatMiB(hardware.memoryHotAddLimitMib),
|
||||
},
|
||||
{
|
||||
label: 'Boot type',
|
||||
value: formatEnumLabel(hardware.bootType),
|
||||
},
|
||||
{
|
||||
label: 'EFI legacy boot',
|
||||
value: formatBoolLabel(hardware.efiLegacyBoot),
|
||||
},
|
||||
{
|
||||
label: 'Boot network protocol',
|
||||
value: formatEnumLabel(hardware.bootNetworkProtocol),
|
||||
},
|
||||
{
|
||||
label: 'Boot delay',
|
||||
value: formatMilliseconds(hardware.bootDelayMilliseconds),
|
||||
},
|
||||
{
|
||||
label: 'Boot retry',
|
||||
value: formatBoolLabel(hardware.bootRetry),
|
||||
},
|
||||
{
|
||||
label: 'Boot retry delay',
|
||||
value: formatMilliseconds(hardware.bootRetryDelayMilliseconds),
|
||||
},
|
||||
{
|
||||
label: 'Enter setup mode',
|
||||
value: formatBoolLabel(hardware.enterSetupMode),
|
||||
tone: getWarningTone(hardware.enterSetupMode === true),
|
||||
},
|
||||
{
|
||||
label: 'Boot order',
|
||||
value: bootOrderLabel(hardware.bootDevices),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const toolsSummary = (tools?: ResourceVMwareTools): string => {
|
||||
if (!tools) return '';
|
||||
if (tools.guestRebootRequested) return 'Tools reboot requested';
|
||||
|
|
@ -372,6 +539,10 @@ export const buildVMwareDetailsSummary = (
|
|||
if (resourceType === 'vm' && virtualDiskCount > 0) {
|
||||
parts.push(formatCount(virtualDiskCount, 'disk'));
|
||||
}
|
||||
const hardware = resourceType === 'vm' ? hardwareSummary(vmware.hardware) : '';
|
||||
if (hardware) {
|
||||
parts.push(hardware);
|
||||
}
|
||||
const tools = resourceType === 'vm' ? toolsSummary(vmware.tools) : '';
|
||||
if (tools) {
|
||||
parts.push(tools);
|
||||
|
|
@ -503,6 +674,7 @@ export const buildVMwareDetailSections = (
|
|||
]);
|
||||
|
||||
const networkRows = resourceType === 'vm' ? networkAdapterRows(vmware.networkAdapters) : [];
|
||||
const vmwareHardwareRows = resourceType === 'vm' ? hardwareRows(vmware) : [];
|
||||
const vmwareToolsRows = resourceType === 'vm' ? toolsRows(vmware.tools) : [];
|
||||
const diskRows = resourceType === 'vm' ? virtualDiskRows(vmware.virtualDisks) : [];
|
||||
|
||||
|
|
@ -540,6 +712,7 @@ export const buildVMwareDetailSections = (
|
|||
{ id: 'state', label: 'State', rows: stateRows },
|
||||
{ id: 'placement', label: 'Placement', rows: placementRows },
|
||||
{ id: 'guest', label: 'Guest', rows: guestRows },
|
||||
{ id: 'hardware', label: 'Virtual hardware', rows: vmwareHardwareRows },
|
||||
{ id: 'tools', label: 'VMware Tools', rows: vmwareToolsRows },
|
||||
{ id: 'disks', label: 'Virtual disks', rows: diskRows },
|
||||
{ id: 'network', label: 'Network', rows: networkRows },
|
||||
|
|
|
|||
|
|
@ -139,6 +139,12 @@ describe('vmwarePageModel', () => {
|
|||
guestHostname: 'warehouse-api-01.internal',
|
||||
guestIpAddresses: ['10.42.10.21'],
|
||||
datastoreNames: ['nvme-primary'],
|
||||
hardware: {
|
||||
guestOs: 'UBUNTU_64',
|
||||
version: 'VMX_20',
|
||||
upgradeStatus: 'PENDING',
|
||||
bootDevices: [{ type: 'ETHERNET', nic: '4000' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
const attention = makeResource({
|
||||
|
|
@ -185,6 +191,11 @@ describe('vmwarePageModel', () => {
|
|||
(resource) => resource.id,
|
||||
),
|
||||
).toEqual(['vm-powered-off']);
|
||||
expect(
|
||||
filterVmwareVirtualMachines([poweredOn, attention, poweredOff], 'vmx 20', 'all').map(
|
||||
(resource) => resource.id,
|
||||
),
|
||||
).toEqual(['vm-powered-on']);
|
||||
});
|
||||
|
||||
it('builds and filters vSphere health signals from resource incidents', () => {
|
||||
|
|
|
|||
|
|
@ -132,6 +132,11 @@ const normalize = (value: unknown): string =>
|
|||
|
||||
const trimString = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
|
||||
|
||||
const enumSearchValue = (value: unknown): string => {
|
||||
const trimmed = trimString(value);
|
||||
return trimmed ? `${trimmed} ${trimmed.replace(/[_-]/g, ' ')}` : '';
|
||||
};
|
||||
|
||||
const normalizeToken = (value: unknown): string => normalize(value).replace(/[\s_-]/g, '');
|
||||
|
||||
const metadataString = (
|
||||
|
|
@ -739,6 +744,23 @@ const vmwareVirtualMachineSearchHaystack = (resource: Resource): string =>
|
|||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
[
|
||||
enumSearchValue(resource.vmware?.hardware?.guestOs),
|
||||
enumSearchValue(resource.vmware?.hardware?.version),
|
||||
enumSearchValue(resource.vmware?.hardware?.upgradePolicy),
|
||||
enumSearchValue(resource.vmware?.hardware?.upgradeVersion),
|
||||
enumSearchValue(resource.vmware?.hardware?.upgradeStatus),
|
||||
resource.vmware?.hardware?.upgradeErrorMessage,
|
||||
enumSearchValue(resource.vmware?.hardware?.bootType),
|
||||
enumSearchValue(resource.vmware?.hardware?.bootNetworkProtocol),
|
||||
resource.vmware?.hardware?.bootDevices
|
||||
?.map((device) =>
|
||||
[device.type, device.nic, device.disks?.join(' ')].filter(Boolean).join(' '),
|
||||
)
|
||||
.join(' '),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
resource.vmware?.activeAlarmSummary,
|
||||
resource.vmware?.recentTaskSummary,
|
||||
...(resource.tags ?? []),
|
||||
|
|
|
|||
|
|
@ -309,6 +309,31 @@ describe('Resource Helper Functions', () => {
|
|||
guestRebootComponents: ['drivers'],
|
||||
guestRebootRequestTime: '2026-03-30T18:20:00Z',
|
||||
},
|
||||
hardware: {
|
||||
guestOs: 'UBUNTU_64',
|
||||
instantCloneFrozen: false,
|
||||
version: 'VMX_20',
|
||||
upgradePolicy: 'AFTER_CLEAN_SHUTDOWN',
|
||||
upgradeVersion: 'VMX_21',
|
||||
upgradeStatus: 'PENDING',
|
||||
bootType: 'EFI',
|
||||
efiLegacyBoot: false,
|
||||
bootNetworkProtocol: 'IPV4',
|
||||
bootDelayMilliseconds: 5000,
|
||||
bootRetry: true,
|
||||
bootRetryDelayMilliseconds: 10000,
|
||||
enterSetupMode: false,
|
||||
bootDevices: [
|
||||
{ type: 'DISK', disks: ['2000'] },
|
||||
{ type: 'ETHERNET', nic: '4000' },
|
||||
],
|
||||
cpuCoresPerSocket: 2,
|
||||
cpuHotAddEnabled: true,
|
||||
cpuHotRemoveEnabled: false,
|
||||
memoryHotAddEnabled: true,
|
||||
memoryHotAddIncrementMib: 256,
|
||||
memoryHotAddLimitMib: 16384,
|
||||
},
|
||||
snapshotTree: [
|
||||
{
|
||||
snapshot: 'snapshot-101',
|
||||
|
|
@ -344,6 +369,10 @@ describe('Resource Helper Functions', () => {
|
|||
expect(vmware.tools?.versionStatus).toBe('CURRENT');
|
||||
expect(vmware.tools?.version).toBe('12.4.0');
|
||||
expect(vmware.tools?.guestRebootComponents).toEqual(['drivers']);
|
||||
expect(vmware.hardware?.version).toBe('VMX_20');
|
||||
expect(vmware.hardware?.upgradeStatus).toBe('PENDING');
|
||||
expect(vmware.hardware?.bootDevices?.[1]?.nic).toBe('4000');
|
||||
expect(vmware.hardware?.memoryHotAddLimitMib).toBe(16384);
|
||||
expect(vmware.snapshotTree?.[0]?.children?.[0]?.current).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -894,6 +894,36 @@ export interface ResourceVMwareTools {
|
|||
guestRebootRequestTime?: string | number;
|
||||
}
|
||||
|
||||
export interface ResourceVMwareBootDevice {
|
||||
type?: string;
|
||||
nic?: string;
|
||||
disks?: string[];
|
||||
}
|
||||
|
||||
export interface ResourceVMwareHardware {
|
||||
guestOs?: string;
|
||||
instantCloneFrozen?: boolean;
|
||||
version?: string;
|
||||
upgradePolicy?: string;
|
||||
upgradeVersion?: string;
|
||||
upgradeStatus?: string;
|
||||
upgradeErrorMessage?: string;
|
||||
bootType?: string;
|
||||
efiLegacyBoot?: boolean;
|
||||
bootNetworkProtocol?: string;
|
||||
bootDelayMilliseconds?: number;
|
||||
bootRetry?: boolean;
|
||||
bootRetryDelayMilliseconds?: number;
|
||||
enterSetupMode?: boolean;
|
||||
bootDevices?: ResourceVMwareBootDevice[];
|
||||
cpuCoresPerSocket?: number;
|
||||
cpuHotAddEnabled?: boolean;
|
||||
cpuHotRemoveEnabled?: boolean;
|
||||
memoryHotAddEnabled?: boolean;
|
||||
memoryHotAddIncrementMib?: number;
|
||||
memoryHotAddLimitMib?: number;
|
||||
}
|
||||
|
||||
export interface ResourceVMwareMeta {
|
||||
connectionId?: string;
|
||||
connectionName?: string;
|
||||
|
|
@ -940,6 +970,7 @@ export interface ResourceVMwareMeta {
|
|||
networkAdapters?: ResourceVMwareNetworkAdapter[];
|
||||
virtualDisks?: ResourceVMwareVirtualDisk[];
|
||||
tools?: ResourceVMwareTools;
|
||||
hardware?: ResourceVMwareHardware;
|
||||
}
|
||||
|
||||
export interface ResourceAvailabilityMeta {
|
||||
|
|
|
|||
|
|
@ -474,6 +474,10 @@ func TestRefreshCanonicalIdentityIgnoresVMwarePlacementDetailAliases(t *testing.
|
|||
installAttempts := int64(1)
|
||||
toolsVersionNumber := int64(12352)
|
||||
guestRebootRequested := true
|
||||
bootDelayMilliseconds := int64(5000)
|
||||
bootRetry := true
|
||||
coresPerSocket := int64(2)
|
||||
memoryHotAddLimitMiB := int64(16384)
|
||||
resource := Resource{
|
||||
ID: "vmware-vm-1",
|
||||
Type: ResourceTypeVM,
|
||||
|
|
@ -535,6 +539,21 @@ func TestRefreshCanonicalIdentityIgnoresVMwarePlacementDetailAliases(t *testing.
|
|||
GuestRebootRequested: &guestRebootRequested,
|
||||
GuestRebootComponents: []string{"drivers"},
|
||||
},
|
||||
Hardware: &VMwareVMHardwareData{
|
||||
GuestOS: "UBUNTU_64",
|
||||
Version: "VMX_20",
|
||||
UpgradePolicy: "AFTER_CLEAN_SHUTDOWN",
|
||||
UpgradeVersion: "VMX_21",
|
||||
UpgradeStatus: "PENDING",
|
||||
UpgradeErrorMessage: "Virtual hardware upgrade failed",
|
||||
BootType: "EFI",
|
||||
BootNetworkProtocol: "IPV4",
|
||||
BootDelayMilliseconds: &bootDelayMilliseconds,
|
||||
BootRetry: &bootRetry,
|
||||
BootDevices: []VMwareBootDeviceData{{Type: "DISK", Disks: []string{"2000"}}},
|
||||
CPUCoresPerSocket: &coresPerSocket,
|
||||
MemoryHotAddLimitMiB: &memoryHotAddLimitMiB,
|
||||
},
|
||||
SnapshotTree: []VMwareSnapshotData{{
|
||||
Snapshot: "snapshot-201",
|
||||
Name: "pre-upgrade",
|
||||
|
|
@ -615,6 +634,14 @@ func TestRefreshCanonicalIdentityIgnoresVMwarePlacementDetailAliases(t *testing.
|
|||
"OPEN_VM_TOOLS",
|
||||
"RUNNING",
|
||||
"drivers",
|
||||
"UBUNTU_64",
|
||||
"VMX_20",
|
||||
"AFTER_CLEAN_SHUTDOWN",
|
||||
"VMX_21",
|
||||
"PENDING",
|
||||
"Virtual hardware upgrade failed",
|
||||
"EFI",
|
||||
"IPV4",
|
||||
}
|
||||
for _, disallowed := range disallowedAliases {
|
||||
for _, alias := range resource.Canonical.Aliases {
|
||||
|
|
|
|||
|
|
@ -271,6 +271,7 @@ func cloneVMwareData(in *VMwareData) *VMwareData {
|
|||
out.NetworkAdapters = cloneVMwareNetworkAdapterDataSlice(in.NetworkAdapters)
|
||||
out.VirtualDisks = cloneVMwareVirtualDiskDataSlice(in.VirtualDisks)
|
||||
out.Tools = cloneVMwareToolsData(in.Tools)
|
||||
out.Hardware = cloneVMwareVMHardwareData(in.Hardware)
|
||||
return &out
|
||||
}
|
||||
|
||||
|
|
@ -332,6 +333,39 @@ func cloneVMwareToolsData(in *VMwareToolsData) *VMwareToolsData {
|
|||
return &out
|
||||
}
|
||||
|
||||
func cloneVMwareVMHardwareData(in *VMwareVMHardwareData) *VMwareVMHardwareData {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := *in
|
||||
out.InstantCloneFrozen = cloneBoolPtr(in.InstantCloneFrozen)
|
||||
out.EFILegacyBoot = cloneBoolPtr(in.EFILegacyBoot)
|
||||
out.BootDelayMilliseconds = cloneInt64Ptr(in.BootDelayMilliseconds)
|
||||
out.BootRetry = cloneBoolPtr(in.BootRetry)
|
||||
out.BootRetryDelayMilliseconds = cloneInt64Ptr(in.BootRetryDelayMilliseconds)
|
||||
out.EnterSetupMode = cloneBoolPtr(in.EnterSetupMode)
|
||||
out.BootDevices = cloneVMwareBootDeviceDataSlice(in.BootDevices)
|
||||
out.CPUCoresPerSocket = cloneInt64Ptr(in.CPUCoresPerSocket)
|
||||
out.CPUHotAddEnabled = cloneBoolPtr(in.CPUHotAddEnabled)
|
||||
out.CPUHotRemoveEnabled = cloneBoolPtr(in.CPUHotRemoveEnabled)
|
||||
out.MemoryHotAddEnabled = cloneBoolPtr(in.MemoryHotAddEnabled)
|
||||
out.MemoryHotAddIncrementMiB = cloneInt64Ptr(in.MemoryHotAddIncrementMiB)
|
||||
out.MemoryHotAddLimitMiB = cloneInt64Ptr(in.MemoryHotAddLimitMiB)
|
||||
return &out
|
||||
}
|
||||
|
||||
func cloneVMwareBootDeviceDataSlice(in []VMwareBootDeviceData) []VMwareBootDeviceData {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]VMwareBootDeviceData, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].Disks = cloneStringSlice(in[i].Disks)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func clonePMGRelayDomainMetaSlice(in []PMGRelayDomainMeta) []PMGRelayDomainMeta {
|
||||
if in == nil {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -159,6 +159,11 @@ func TestCloneResource_MutateVMwareDetailSlices(t *testing.T) {
|
|||
installAttempts := int64(1)
|
||||
toolsVersionNumber := int64(12352)
|
||||
guestRebootRequested := true
|
||||
instantCloneFrozen := false
|
||||
bootDelayMilliseconds := int64(5000)
|
||||
bootRetry := true
|
||||
coresPerSocket := int64(2)
|
||||
memoryHotAddLimitMiB := int64(16384)
|
||||
original := &Resource{
|
||||
ID: "vmware-vm-1",
|
||||
VMware: &VMwareData{
|
||||
|
|
@ -199,6 +204,17 @@ func TestCloneResource_MutateVMwareDetailSlices(t *testing.T) {
|
|||
GuestRebootRequested: &guestRebootRequested,
|
||||
GuestRebootComponents: []string{"tools"},
|
||||
},
|
||||
Hardware: &VMwareVMHardwareData{
|
||||
GuestOS: "UBUNTU_64",
|
||||
InstantCloneFrozen: &instantCloneFrozen,
|
||||
Version: "VMX_20",
|
||||
UpgradeStatus: "PENDING",
|
||||
BootDelayMilliseconds: &bootDelayMilliseconds,
|
||||
BootRetry: &bootRetry,
|
||||
BootDevices: []VMwareBootDeviceData{{Type: "DISK", Disks: []string{"2000"}}},
|
||||
CPUCoresPerSocket: &coresPerSocket,
|
||||
MemoryHotAddLimitMiB: &memoryHotAddLimitMiB,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -214,6 +230,10 @@ func TestCloneResource_MutateVMwareDetailSlices(t *testing.T) {
|
|||
*cloned.VMware.Tools.AutoUpdateSupported = false
|
||||
*cloned.VMware.Tools.VersionNumber = 1
|
||||
cloned.VMware.Tools.GuestRebootComponents[0] = "mutated"
|
||||
*cloned.VMware.Hardware.InstantCloneFrozen = true
|
||||
*cloned.VMware.Hardware.BootDelayMilliseconds = 1
|
||||
cloned.VMware.Hardware.BootDevices[0].Disks[0] = "mutated"
|
||||
*cloned.VMware.Hardware.MemoryHotAddLimitMiB = 1
|
||||
|
||||
if original.VMware.SnapshotTree[0].Name != "pre-upgrade" {
|
||||
t.Fatalf("mutating cloned VMware snapshot should not affect original: %+v", original.VMware.SnapshotTree)
|
||||
|
|
@ -248,6 +268,18 @@ func TestCloneResource_MutateVMwareDetailSlices(t *testing.T) {
|
|||
if original.VMware.Tools.GuestRebootComponents[0] != "tools" {
|
||||
t.Fatalf("mutating cloned VMware Tools reboot components should not affect original: %+v", original.VMware.Tools.GuestRebootComponents)
|
||||
}
|
||||
if original.VMware.Hardware.InstantCloneFrozen == nil || *original.VMware.Hardware.InstantCloneFrozen {
|
||||
t.Fatalf("mutating cloned VMware hardware frozen flag should not affect original: %+v", original.VMware.Hardware.InstantCloneFrozen)
|
||||
}
|
||||
if original.VMware.Hardware.BootDelayMilliseconds == nil || *original.VMware.Hardware.BootDelayMilliseconds != 5000 {
|
||||
t.Fatalf("mutating cloned VMware hardware boot delay should not affect original: %+v", original.VMware.Hardware.BootDelayMilliseconds)
|
||||
}
|
||||
if original.VMware.Hardware.BootDevices[0].Disks[0] != "2000" {
|
||||
t.Fatalf("mutating cloned VMware hardware boot devices should not affect original: %+v", original.VMware.Hardware.BootDevices)
|
||||
}
|
||||
if original.VMware.Hardware.MemoryHotAddLimitMiB == nil || *original.VMware.Hardware.MemoryHotAddLimitMiB != 16384 {
|
||||
t.Fatalf("mutating cloned VMware hardware memory limit should not affect original: %+v", original.VMware.Hardware.MemoryHotAddLimitMiB)
|
||||
}
|
||||
}
|
||||
|
||||
// --- cloneProxmoxData ---
|
||||
|
|
|
|||
|
|
@ -2008,11 +2008,14 @@ func TestCloneVMwareDataKeepsNestedRuntimeDetailsIsolated(t *testing.T) {
|
|||
"out.NetworkAdapters = cloneVMwareNetworkAdapterDataSlice(in.NetworkAdapters)",
|
||||
"out.VirtualDisks = cloneVMwareVirtualDiskDataSlice(in.VirtualDisks)",
|
||||
"out.Tools = cloneVMwareToolsData(in.Tools)",
|
||||
"out.Hardware = cloneVMwareVMHardwareData(in.Hardware)",
|
||||
"out[i].CreatedAt = cloneTimePtr(in[i].CreatedAt)",
|
||||
"out[i].Children = cloneVMwareSnapshotDataSlice(in[i].Children)",
|
||||
"out[i].PCISlotNumber = cloneInt64Ptr(in[i].PCISlotNumber)",
|
||||
"out[i].CapacityBytes = cloneInt64Ptr(in[i].CapacityBytes)",
|
||||
"out.GuestRebootComponents = cloneStringSlice(in.GuestRebootComponents)",
|
||||
"out.BootDevices = cloneVMwareBootDeviceDataSlice(in.BootDevices)",
|
||||
"out[i].Disks = cloneStringSlice(in[i].Disks)",
|
||||
}
|
||||
for _, snippet := range requiredSnippets {
|
||||
if !strings.Contains(source, snippet) {
|
||||
|
|
|
|||
|
|
@ -1832,6 +1832,24 @@ func mergeVMwareData(existing *VMwareData, incoming *VMwareData) *VMwareData {
|
|||
if incoming.SnapshotCount > 0 {
|
||||
merged.SnapshotCount = incoming.SnapshotCount
|
||||
}
|
||||
if incoming.CurrentSnapshotID != "" {
|
||||
merged.CurrentSnapshotID = incoming.CurrentSnapshotID
|
||||
}
|
||||
if len(incoming.SnapshotTree) > 0 {
|
||||
merged.SnapshotTree = cloneVMwareSnapshotDataSlice(incoming.SnapshotTree)
|
||||
}
|
||||
if len(incoming.NetworkAdapters) > 0 {
|
||||
merged.NetworkAdapters = cloneVMwareNetworkAdapterDataSlice(incoming.NetworkAdapters)
|
||||
}
|
||||
if len(incoming.VirtualDisks) > 0 {
|
||||
merged.VirtualDisks = cloneVMwareVirtualDiskDataSlice(incoming.VirtualDisks)
|
||||
}
|
||||
if incoming.Tools != nil {
|
||||
merged.Tools = cloneVMwareToolsData(incoming.Tools)
|
||||
}
|
||||
if incoming.Hardware != nil {
|
||||
merged.Hardware = cloneVMwareVMHardwareData(incoming.Hardware)
|
||||
}
|
||||
|
||||
return &merged
|
||||
}
|
||||
|
|
|
|||
|
|
@ -761,6 +761,7 @@ func TestMergeVMwareDataMergesSignalFieldsWithoutDroppingExistingIdentity(t *tes
|
|||
SnapshotCount: 1,
|
||||
}
|
||||
accessible := false
|
||||
bootRetry := true
|
||||
incoming := &VMwareData{
|
||||
ClusterName: "Cluster A",
|
||||
RuntimeHostName: "esxi-01.lab.local",
|
||||
|
|
@ -773,6 +774,19 @@ func TestMergeVMwareDataMergesSignalFieldsWithoutDroppingExistingIdentity(t *tes
|
|||
RecentTaskCount: 2,
|
||||
RecentTaskSummary: "vMotion task completed",
|
||||
SnapshotCount: 4,
|
||||
CurrentSnapshotID: "snapshot-202",
|
||||
SnapshotTree: []VMwareSnapshotData{{Snapshot: "snapshot-202", Name: "pre-maintenance"}},
|
||||
NetworkAdapters: []VMwareNetworkAdapterData{{NIC: "4000", NetworkName: "VM Network"}},
|
||||
VirtualDisks: []VMwareVirtualDiskData{{Disk: "2000", Label: "Hard disk 1"}},
|
||||
Tools: &VMwareToolsData{RunState: "RUNNING", VersionStatus: "CURRENT"},
|
||||
Hardware: &VMwareVMHardwareData{
|
||||
Version: "VMX_20",
|
||||
BootRetry: &bootRetry,
|
||||
BootDevices: []VMwareBootDeviceData{{
|
||||
Type: "DISK",
|
||||
Disks: []string{"2000"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
merged := mergeVMwareData(existing, incoming)
|
||||
|
|
@ -818,6 +832,24 @@ func TestMergeVMwareDataMergesSignalFieldsWithoutDroppingExistingIdentity(t *tes
|
|||
if got := merged.GuestIPAddresses; !reflect.DeepEqual(got, []string{"10.0.0.21"}) {
|
||||
t.Fatalf("guest ip addresses = %#v", got)
|
||||
}
|
||||
if got := merged.CurrentSnapshotID; got != "snapshot-202" {
|
||||
t.Fatalf("current snapshot id = %q, want snapshot-202", got)
|
||||
}
|
||||
if got := merged.SnapshotTree; len(got) != 1 || got[0].Name != "pre-maintenance" {
|
||||
t.Fatalf("snapshot tree = %#v", got)
|
||||
}
|
||||
if got := merged.NetworkAdapters; len(got) != 1 || got[0].NetworkName != "VM Network" {
|
||||
t.Fatalf("network adapters = %#v", got)
|
||||
}
|
||||
if got := merged.VirtualDisks; len(got) != 1 || got[0].Label != "Hard disk 1" {
|
||||
t.Fatalf("virtual disks = %#v", got)
|
||||
}
|
||||
if got := merged.Tools; got == nil || got.RunState != "RUNNING" {
|
||||
t.Fatalf("tools = %#v", got)
|
||||
}
|
||||
if got := merged.Hardware; got == nil || got.Version != "VMX_20" || got.BootDevices[0].Disks[0] != "2000" {
|
||||
t.Fatalf("hardware = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceRegistryClonesCarryPolicyMetadata(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1040,6 +1040,7 @@ type VMwareData struct {
|
|||
NetworkAdapters []VMwareNetworkAdapterData `json:"networkAdapters,omitempty"`
|
||||
VirtualDisks []VMwareVirtualDiskData `json:"virtualDisks,omitempty"`
|
||||
Tools *VMwareToolsData `json:"tools,omitempty"`
|
||||
Hardware *VMwareVMHardwareData `json:"hardware,omitempty"`
|
||||
}
|
||||
|
||||
// VMwareSnapshotData contains one node in the vSphere VM snapshot tree. It is
|
||||
|
|
@ -1118,6 +1119,40 @@ type VMwareToolsData struct {
|
|||
GuestRebootRequestTime string `json:"guestRebootRequestTime,omitempty"`
|
||||
}
|
||||
|
||||
// VMwareBootDeviceData contains one vCenter VM boot-device entry as read-only
|
||||
// virtual hardware context.
|
||||
type VMwareBootDeviceData struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
NIC string `json:"nic,omitempty"`
|
||||
Disks []string `json:"disks,omitempty"`
|
||||
}
|
||||
|
||||
// VMwareVMHardwareData contains vCenter VM virtual hardware, CPU, memory, and
|
||||
// boot configuration facts as read-only VM context.
|
||||
type VMwareVMHardwareData struct {
|
||||
GuestOS string `json:"guestOs,omitempty"`
|
||||
InstantCloneFrozen *bool `json:"instantCloneFrozen,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
UpgradePolicy string `json:"upgradePolicy,omitempty"`
|
||||
UpgradeVersion string `json:"upgradeVersion,omitempty"`
|
||||
UpgradeStatus string `json:"upgradeStatus,omitempty"`
|
||||
UpgradeErrorMessage string `json:"upgradeErrorMessage,omitempty"`
|
||||
BootType string `json:"bootType,omitempty"`
|
||||
EFILegacyBoot *bool `json:"efiLegacyBoot,omitempty"`
|
||||
BootNetworkProtocol string `json:"bootNetworkProtocol,omitempty"`
|
||||
BootDelayMilliseconds *int64 `json:"bootDelayMilliseconds,omitempty"`
|
||||
BootRetry *bool `json:"bootRetry,omitempty"`
|
||||
BootRetryDelayMilliseconds *int64 `json:"bootRetryDelayMilliseconds,omitempty"`
|
||||
EnterSetupMode *bool `json:"enterSetupMode,omitempty"`
|
||||
BootDevices []VMwareBootDeviceData `json:"bootDevices,omitempty"`
|
||||
CPUCoresPerSocket *int64 `json:"cpuCoresPerSocket,omitempty"`
|
||||
CPUHotAddEnabled *bool `json:"cpuHotAddEnabled,omitempty"`
|
||||
CPUHotRemoveEnabled *bool `json:"cpuHotRemoveEnabled,omitempty"`
|
||||
MemoryHotAddEnabled *bool `json:"memoryHotAddEnabled,omitempty"`
|
||||
MemoryHotAddIncrementMiB *int64 `json:"memoryHotAddIncrementMib,omitempty"`
|
||||
MemoryHotAddLimitMiB *int64 `json:"memoryHotAddLimitMib,omitempty"`
|
||||
}
|
||||
|
||||
// TrueNASData contains TrueNAS-specific metadata for TrueNAS resources.
|
||||
type TrueNASData struct {
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
|
|
|
|||
|
|
@ -75,6 +75,24 @@ func TestClientCollectInventoryEnrichesSignals(t *testing.T) {
|
|||
if vm.GuestHostname != "app-01.internal" || len(vm.GuestIPAddresses) != 1 || vm.GuestIPAddresses[0] != "10.0.0.21" {
|
||||
t.Fatalf("expected VM guest identity enrichment, got host=%q ips=%v", vm.GuestHostname, vm.GuestIPAddresses)
|
||||
}
|
||||
if vm.Hardware == nil {
|
||||
t.Fatalf("expected VM hardware enrichment")
|
||||
}
|
||||
if vm.Hardware.Version != "VMX_20" || vm.Hardware.UpgradePolicy != "AFTER_CLEAN_SHUTDOWN" || vm.Hardware.UpgradeStatus != "PENDING" {
|
||||
t.Fatalf("unexpected VM hardware upgrade context: %+v", vm.Hardware)
|
||||
}
|
||||
if vm.Hardware.GuestOS != "UBUNTU_64" {
|
||||
t.Fatalf("expected VM hardware guest OS, got %+v", vm.Hardware.GuestOS)
|
||||
}
|
||||
if vm.CPUCount != 6 || vm.Hardware.CPUCoresPerSocket == nil || *vm.Hardware.CPUCoresPerSocket != 3 {
|
||||
t.Fatalf("expected VM CPU topology from VM detail, got cpu=%d hardware=%+v", vm.CPUCount, vm.Hardware)
|
||||
}
|
||||
if vm.MemorySizeMiB != 12288 || vm.Hardware.MemoryHotAddLimitMiB == nil || *vm.Hardware.MemoryHotAddLimitMiB != 24576 {
|
||||
t.Fatalf("expected VM memory detail from VM detail, got memory=%d hardware=%+v", vm.MemorySizeMiB, vm.Hardware)
|
||||
}
|
||||
if len(vm.Hardware.BootDevices) != 2 || vm.Hardware.BootDevices[0].Disks[0] != "2000" || vm.Hardware.BootDevices[1].NIC != "4000" {
|
||||
t.Fatalf("expected VM boot device order, got %+v", vm.Hardware.BootDevices)
|
||||
}
|
||||
if vm.Tools == nil {
|
||||
t.Fatalf("expected VM tools enrichment")
|
||||
}
|
||||
|
|
@ -215,6 +233,9 @@ func TestClientCollectInventoryPreservesBaseInventoryWhenOptionalEnrichmentDegra
|
|||
if vm.GuestHostname != "" || len(vm.GuestIPAddresses) != 0 {
|
||||
t.Fatalf("expected guest identity to stay empty after degraded topology read, got host=%q ips=%v", vm.GuestHostname, vm.GuestIPAddresses)
|
||||
}
|
||||
if vm.Hardware == nil || vm.Hardware.Version != "VMX_20" {
|
||||
t.Fatalf("expected hardware enrichment to survive degraded guest read, got %+v", vm.Hardware)
|
||||
}
|
||||
if vm.Tools == nil || vm.Tools.RunState != "RUNNING" {
|
||||
t.Fatalf("expected tools enrichment to survive degraded guest read, got %+v", vm.Tools)
|
||||
}
|
||||
|
|
@ -387,10 +408,44 @@ func newVMwareTestServer(t *testing.T, cfg vmwareTestServerConfig) *httptest.Ser
|
|||
mux.HandleFunc("/api/vcenter/vm/vm-201", func(w http.ResponseWriter, r *http.Request) {
|
||||
requireAutomationSession(t, r)
|
||||
writeJSON(w, map[string]any{
|
||||
"guest_os": "UBUNTU_64",
|
||||
"power_state": "POWERED_ON",
|
||||
"instant_clone_frozen": false,
|
||||
"identity": map[string]any{
|
||||
"bios_uuid": "vm-bios-201",
|
||||
"instance_uuid": "vm-instance-201",
|
||||
},
|
||||
"hardware": map[string]any{
|
||||
"version": "VMX_20",
|
||||
"upgrade_policy": "AFTER_CLEAN_SHUTDOWN",
|
||||
"upgrade_version": "VMX_21",
|
||||
"upgrade_status": "PENDING",
|
||||
},
|
||||
"boot": map[string]any{
|
||||
"type": "EFI",
|
||||
"efi_legacy_boot": false,
|
||||
"network_protocol": "IPV4",
|
||||
"delay": 5000,
|
||||
"retry": true,
|
||||
"retry_delay": 10000,
|
||||
"enter_setup_mode": false,
|
||||
},
|
||||
"boot_devices": []map[string]any{
|
||||
{"type": "DISK", "disks": []string{"2000"}},
|
||||
{"type": "ETHERNET", "nic": "4000"},
|
||||
},
|
||||
"cpu": map[string]any{
|
||||
"count": 6,
|
||||
"cores_per_socket": 3,
|
||||
"hot_add_enabled": true,
|
||||
"hot_remove_enabled": false,
|
||||
},
|
||||
"memory": map[string]any{
|
||||
"size_mib": 12288,
|
||||
"hot_add_enabled": true,
|
||||
"hot_add_increment_size_mib": 256,
|
||||
"hot_add_limit_mib": 24576,
|
||||
},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/api/vcenter/vm/vm-201/guest/identity", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,54 @@ type vcenterVMIdentity struct {
|
|||
InstanceUUID string `json:"instance_uuid"`
|
||||
}
|
||||
|
||||
type vcenterVMHardwareInfo struct {
|
||||
Version string `json:"version"`
|
||||
UpgradePolicy string `json:"upgrade_policy"`
|
||||
UpgradeVersion string `json:"upgrade_version"`
|
||||
UpgradeStatus string `json:"upgrade_status"`
|
||||
UpgradeErrorMessage *vcenterVMLocalizableMessage `json:"upgrade_error"`
|
||||
}
|
||||
|
||||
type vcenterVMBootInfo struct {
|
||||
Type string `json:"type"`
|
||||
EFILegacyBoot *bool `json:"efi_legacy_boot"`
|
||||
NetworkProtocol string `json:"network_protocol"`
|
||||
Delay *int64 `json:"delay"`
|
||||
Retry *bool `json:"retry"`
|
||||
RetryDelay *int64 `json:"retry_delay"`
|
||||
EnterSetupMode *bool `json:"enter_setup_mode"`
|
||||
}
|
||||
|
||||
type vcenterVMBootDeviceInfo struct {
|
||||
Type string `json:"type"`
|
||||
NIC string `json:"nic"`
|
||||
Disks []string `json:"disks"`
|
||||
}
|
||||
|
||||
type vcenterVMCPUInfo struct {
|
||||
Count *int64 `json:"count"`
|
||||
CoresPerSocket *int64 `json:"cores_per_socket"`
|
||||
HotAddEnabled *bool `json:"hot_add_enabled"`
|
||||
HotRemoveEnabled *bool `json:"hot_remove_enabled"`
|
||||
}
|
||||
|
||||
type vcenterVMMemoryInfo struct {
|
||||
SizeMiB *int64 `json:"size_mib"`
|
||||
HotAddEnabled *bool `json:"hot_add_enabled"`
|
||||
HotAddIncrementSizeMiB *int64 `json:"hot_add_increment_size_mib"`
|
||||
HotAddLimitMiB *int64 `json:"hot_add_limit_mib"`
|
||||
}
|
||||
|
||||
type vcenterVMInfo struct {
|
||||
Identity *vcenterVMIdentity `json:"identity"`
|
||||
GuestOS string `json:"guest_os"`
|
||||
Identity *vcenterVMIdentity `json:"identity"`
|
||||
PowerState string `json:"power_state"`
|
||||
InstantCloneFrozen *bool `json:"instant_clone_frozen"`
|
||||
Hardware *vcenterVMHardwareInfo `json:"hardware"`
|
||||
Boot *vcenterVMBootInfo `json:"boot"`
|
||||
BootDevices []vcenterVMBootDeviceInfo `json:"boot_devices"`
|
||||
CPU *vcenterVMCPUInfo `json:"cpu"`
|
||||
Memory *vcenterVMMemoryInfo `json:"memory"`
|
||||
}
|
||||
|
||||
type vcenterVMGuestIdentity struct {
|
||||
|
|
@ -343,6 +389,13 @@ func (c *Client) enrichVMTopology(
|
|||
vm.InstanceUUID = strings.TrimSpace(vmInfo.Identity.InstanceUUID)
|
||||
vm.BIOSUUID = strings.TrimSpace(vmInfo.Identity.BIOSUUID)
|
||||
}
|
||||
if vmInfo != nil && vmInfo.CPU != nil && vmInfo.CPU.Count != nil && *vmInfo.CPU.Count > 0 {
|
||||
vm.CPUCount = int(*vmInfo.CPU.Count)
|
||||
}
|
||||
if vmInfo != nil && vmInfo.Memory != nil && vmInfo.Memory.SizeMiB != nil && *vmInfo.Memory.SizeMiB > 0 {
|
||||
vm.MemorySizeMiB = *vmInfo.Memory.SizeMiB
|
||||
}
|
||||
vm.Hardware = inventoryVMHardwareFromInfo(vmInfo)
|
||||
|
||||
guestIdentity, err := c.collectVMGuestIdentity(ctx, automationSessionID, vm.VM)
|
||||
if issue, ok := classifyInventoryEnrichmentIssue("topology", "vm", vm.VM, err); ok {
|
||||
|
|
@ -533,13 +586,94 @@ func (c *Client) collectVMAutomationInfo(
|
|||
return nil, nil
|
||||
}
|
||||
var payload vcenterVMInfo
|
||||
path := fmt.Sprintf("/api/vcenter/vm/%s", vmID)
|
||||
path := fmt.Sprintf("/api/vcenter/vm/%s", url.PathEscape(vmID))
|
||||
if err := c.getAutomationJSON(ctx, automationSessionID, path, "vm detail", &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &payload, nil
|
||||
}
|
||||
|
||||
func inventoryVMHardwareFromInfo(info *vcenterVMInfo) *InventoryVMHardware {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
hardware := &InventoryVMHardware{
|
||||
GuestOS: strings.TrimSpace(info.GuestOS),
|
||||
InstantCloneFrozen: cloneBoolPointer(info.InstantCloneFrozen),
|
||||
}
|
||||
if info.Hardware != nil {
|
||||
hardware.Version = strings.TrimSpace(info.Hardware.Version)
|
||||
hardware.UpgradePolicy = strings.TrimSpace(info.Hardware.UpgradePolicy)
|
||||
hardware.UpgradeVersion = strings.TrimSpace(info.Hardware.UpgradeVersion)
|
||||
hardware.UpgradeStatus = strings.TrimSpace(info.Hardware.UpgradeStatus)
|
||||
hardware.UpgradeErrorMessage = localizableMessageText(info.Hardware.UpgradeErrorMessage)
|
||||
}
|
||||
if info.Boot != nil {
|
||||
hardware.BootType = strings.TrimSpace(info.Boot.Type)
|
||||
hardware.EFILegacyBoot = cloneBoolPointer(info.Boot.EFILegacyBoot)
|
||||
hardware.BootNetworkProtocol = strings.TrimSpace(info.Boot.NetworkProtocol)
|
||||
hardware.BootDelayMilliseconds = cloneInt64Pointer(info.Boot.Delay)
|
||||
hardware.BootRetry = cloneBoolPointer(info.Boot.Retry)
|
||||
hardware.BootRetryDelayMilliseconds = cloneInt64Pointer(info.Boot.RetryDelay)
|
||||
hardware.EnterSetupMode = cloneBoolPointer(info.Boot.EnterSetupMode)
|
||||
}
|
||||
hardware.BootDevices = inventoryVMBootDevicesFromInfo(info.BootDevices)
|
||||
if info.CPU != nil {
|
||||
hardware.CPUCoresPerSocket = cloneInt64Pointer(info.CPU.CoresPerSocket)
|
||||
hardware.CPUHotAddEnabled = cloneBoolPointer(info.CPU.HotAddEnabled)
|
||||
hardware.CPUHotRemoveEnabled = cloneBoolPointer(info.CPU.HotRemoveEnabled)
|
||||
}
|
||||
if info.Memory != nil {
|
||||
hardware.MemoryHotAddEnabled = cloneBoolPointer(info.Memory.HotAddEnabled)
|
||||
hardware.MemoryHotAddIncrementMiB = cloneInt64Pointer(info.Memory.HotAddIncrementSizeMiB)
|
||||
hardware.MemoryHotAddLimitMiB = cloneInt64Pointer(info.Memory.HotAddLimitMiB)
|
||||
}
|
||||
if inventoryVMHardwareEmpty(hardware) {
|
||||
return nil
|
||||
}
|
||||
return hardware
|
||||
}
|
||||
|
||||
func inventoryVMBootDevicesFromInfo(devices []vcenterVMBootDeviceInfo) []InventoryVMBootDevice {
|
||||
if len(devices) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]InventoryVMBootDevice, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
out = append(out, InventoryVMBootDevice{
|
||||
Type: strings.TrimSpace(device.Type),
|
||||
NIC: strings.TrimSpace(device.NIC),
|
||||
Disks: cloneStringSlice(device.Disks),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func inventoryVMHardwareEmpty(hardware *InventoryVMHardware) bool {
|
||||
return hardware == nil ||
|
||||
(hardware.GuestOS == "" &&
|
||||
hardware.InstantCloneFrozen == nil &&
|
||||
hardware.Version == "" &&
|
||||
hardware.UpgradePolicy == "" &&
|
||||
hardware.UpgradeVersion == "" &&
|
||||
hardware.UpgradeStatus == "" &&
|
||||
hardware.UpgradeErrorMessage == "" &&
|
||||
hardware.BootType == "" &&
|
||||
hardware.EFILegacyBoot == nil &&
|
||||
hardware.BootNetworkProtocol == "" &&
|
||||
hardware.BootDelayMilliseconds == nil &&
|
||||
hardware.BootRetry == nil &&
|
||||
hardware.BootRetryDelayMilliseconds == nil &&
|
||||
hardware.EnterSetupMode == nil &&
|
||||
len(hardware.BootDevices) == 0 &&
|
||||
hardware.CPUCoresPerSocket == nil &&
|
||||
hardware.CPUHotAddEnabled == nil &&
|
||||
hardware.CPUHotRemoveEnabled == nil &&
|
||||
hardware.MemoryHotAddEnabled == nil &&
|
||||
hardware.MemoryHotAddIncrementMiB == nil &&
|
||||
hardware.MemoryHotAddLimitMiB == nil)
|
||||
}
|
||||
|
||||
func (c *Client) collectVMGuestIdentity(
|
||||
ctx context.Context,
|
||||
automationSessionID string,
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ func defaultFixturesPrimaryCluster(
|
|||
NetworkAdapters: fixtureVMNetworkAdapters("vm-201", "VM Network", true),
|
||||
VirtualDisks: fixtureVMVirtualDisks("vm-201", "nvme-primary", 128_000_000_000),
|
||||
Tools: fixtureVMTools("POWERED_ON", "green"),
|
||||
Hardware: fixtureVMHardware("UBUNTU_64", "VMX_20", true),
|
||||
SnapshotTree: []InventoryVMSnapshot{{
|
||||
Snapshot: "snapshot-201",
|
||||
Name: "pre-deploy-checkpoint",
|
||||
|
|
@ -270,6 +271,7 @@ func defaultFixturesPrimaryCluster(
|
|||
NetworkAdapters: fixtureVMNetworkAdapters("vm-202", "Database Network", true),
|
||||
VirtualDisks: fixtureVMVirtualDisks("vm-202", "nvme-primary", 256_000_000_000),
|
||||
Tools: fixtureVMTools("POWERED_ON", "yellow"),
|
||||
Hardware: fixtureVMHardware("RHEL_8_64", "VMX_19", true),
|
||||
SnapshotTree: []InventoryVMSnapshot{{
|
||||
Snapshot: "snapshot-211",
|
||||
Name: "baseline",
|
||||
|
|
@ -347,6 +349,7 @@ func defaultFixturesPrimaryCluster(
|
|||
NetworkAdapters: fixtureVMNetworkAdapters("vm-203", "Web Network", true),
|
||||
VirtualDisks: fixtureVMVirtualDisks("vm-203", "analytics-vsan", 96_000_000_000),
|
||||
Tools: fixtureVMTools("POWERED_ON", "green"),
|
||||
Hardware: fixtureVMHardware("UBUNTU_64", "VMX_20", true),
|
||||
Metrics: &InventoryMetrics{
|
||||
CPUPercent: float64Ptr(22.1),
|
||||
MemoryPercent: float64Ptr(51.2),
|
||||
|
|
@ -388,6 +391,7 @@ func defaultFixturesPrimaryCluster(
|
|||
NetworkAdapters: fixtureVMNetworkAdapters("vm-204", "Platform Network", true),
|
||||
VirtualDisks: fixtureVMVirtualDisks("vm-204", "analytics-vsan", 192_000_000_000),
|
||||
Tools: fixtureVMTools("POWERED_ON", "green"),
|
||||
Hardware: fixtureVMHardware("UBUNTU_64", "VMX_20", true),
|
||||
Metrics: &InventoryMetrics{
|
||||
CPUPercent: float64Ptr(27.8),
|
||||
MemoryPercent: float64Ptr(59.6),
|
||||
|
|
@ -429,6 +433,7 @@ func defaultFixturesPrimaryCluster(
|
|||
NetworkAdapters: fixtureVMNetworkAdapters("vm-205", "Utility Network", true),
|
||||
VirtualDisks: fixtureVMVirtualDisks("vm-205", "analytics-vsan", 80_000_000_000),
|
||||
Tools: fixtureVMTools("POWERED_ON", "green"),
|
||||
Hardware: fixtureVMHardware("WINDOWS_2019_64", "VMX_19", true),
|
||||
Metrics: &InventoryMetrics{
|
||||
CPUPercent: float64Ptr(16.8),
|
||||
MemoryPercent: float64Ptr(46.1),
|
||||
|
|
@ -468,6 +473,7 @@ func defaultFixturesPrimaryCluster(
|
|||
OverallStatus: "gray",
|
||||
VirtualDisks: fixtureVMVirtualDisks("vm-206", "archive-tier", 64_000_000_000),
|
||||
Tools: fixtureVMTools("POWERED_OFF", "gray"),
|
||||
Hardware: fixtureVMHardware("OTHER_64", "VMX_17", false),
|
||||
},
|
||||
},
|
||||
Datastores: []InventoryDatastore{
|
||||
|
|
@ -736,6 +742,7 @@ func appendEdgeClusterFixtures(
|
|||
NetworkAdapters: fixtureVMNetworkAdapters(v.ID, edgeVMNetworkName(v.Tier), v.PowerState == "POWERED_ON"),
|
||||
VirtualDisks: fixtureVMVirtualDisks(v.ID, firstNonEmptyTrimmed(dsNames...), int64(v.MemMiB)*1024*1024*4),
|
||||
Tools: fixtureVMTools(v.PowerState, v.Status),
|
||||
Hardware: fixtureVMHardware("UBUNTU_64", "VMX_20", v.PowerState == "POWERED_ON"),
|
||||
}
|
||||
if v.PowerState == "POWERED_ON" {
|
||||
vm.Metrics = &InventoryMetrics{
|
||||
|
|
@ -884,6 +891,42 @@ func fixtureVMTools(powerState, status string) *InventoryVMTools {
|
|||
}
|
||||
}
|
||||
|
||||
func fixtureVMHardware(guestOS, version string, hotAddEnabled bool) *InventoryVMHardware {
|
||||
instantCloneFrozen := false
|
||||
efiLegacyBoot := false
|
||||
bootDelayMilliseconds := int64(0)
|
||||
bootRetry := false
|
||||
bootRetryDelayMilliseconds := int64(10000)
|
||||
enterSetupMode := false
|
||||
coresPerSocket := int64(2)
|
||||
memoryHotAddIncrementMiB := int64(256)
|
||||
memoryHotAddLimitMiB := int64(16 * 1024)
|
||||
return &InventoryVMHardware{
|
||||
GuestOS: guestOS,
|
||||
InstantCloneFrozen: boolPointer(instantCloneFrozen),
|
||||
Version: version,
|
||||
UpgradePolicy: "NEVER",
|
||||
UpgradeStatus: "NONE",
|
||||
BootType: "EFI",
|
||||
EFILegacyBoot: &efiLegacyBoot,
|
||||
BootNetworkProtocol: "IPV4",
|
||||
BootDelayMilliseconds: &bootDelayMilliseconds,
|
||||
BootRetry: &bootRetry,
|
||||
BootRetryDelayMilliseconds: &bootRetryDelayMilliseconds,
|
||||
EnterSetupMode: &enterSetupMode,
|
||||
BootDevices: []InventoryVMBootDevice{{
|
||||
Type: "DISK",
|
||||
Disks: []string{"2000"},
|
||||
}},
|
||||
CPUCoresPerSocket: &coresPerSocket,
|
||||
CPUHotAddEnabled: boolPointer(hotAddEnabled),
|
||||
CPUHotRemoveEnabled: boolPointer(false),
|
||||
MemoryHotAddEnabled: boolPointer(hotAddEnabled),
|
||||
MemoryHotAddIncrementMiB: &memoryHotAddIncrementMiB,
|
||||
MemoryHotAddLimitMiB: &memoryHotAddLimitMiB,
|
||||
}
|
||||
}
|
||||
|
||||
func boolPointer(value bool) *bool {
|
||||
return &value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,40 @@ type InventoryVMTools struct {
|
|||
GuestRebootRequestTime string `json:"guest_reboot_request_time,omitempty"`
|
||||
}
|
||||
|
||||
// InventoryVMBootDevice preserves one vCenter VM boot-device entry as
|
||||
// read-only virtual hardware context.
|
||||
type InventoryVMBootDevice struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
NIC string `json:"nic,omitempty"`
|
||||
Disks []string `json:"disks,omitempty"`
|
||||
}
|
||||
|
||||
// InventoryVMHardware preserves vCenter VM hardware, CPU, memory, and boot
|
||||
// configuration as read-only VM context.
|
||||
type InventoryVMHardware struct {
|
||||
GuestOS string `json:"guest_os,omitempty"`
|
||||
InstantCloneFrozen *bool `json:"instant_clone_frozen,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
UpgradePolicy string `json:"upgrade_policy,omitempty"`
|
||||
UpgradeVersion string `json:"upgrade_version,omitempty"`
|
||||
UpgradeStatus string `json:"upgrade_status,omitempty"`
|
||||
UpgradeErrorMessage string `json:"upgrade_error_message,omitempty"`
|
||||
BootType string `json:"boot_type,omitempty"`
|
||||
EFILegacyBoot *bool `json:"efi_legacy_boot,omitempty"`
|
||||
BootNetworkProtocol string `json:"boot_network_protocol,omitempty"`
|
||||
BootDelayMilliseconds *int64 `json:"boot_delay_milliseconds,omitempty"`
|
||||
BootRetry *bool `json:"boot_retry,omitempty"`
|
||||
BootRetryDelayMilliseconds *int64 `json:"boot_retry_delay_milliseconds,omitempty"`
|
||||
EnterSetupMode *bool `json:"enter_setup_mode,omitempty"`
|
||||
BootDevices []InventoryVMBootDevice `json:"boot_devices,omitempty"`
|
||||
CPUCoresPerSocket *int64 `json:"cpu_cores_per_socket,omitempty"`
|
||||
CPUHotAddEnabled *bool `json:"cpu_hot_add_enabled,omitempty"`
|
||||
CPUHotRemoveEnabled *bool `json:"cpu_hot_remove_enabled,omitempty"`
|
||||
MemoryHotAddEnabled *bool `json:"memory_hot_add_enabled,omitempty"`
|
||||
MemoryHotAddIncrementMiB *int64 `json:"memory_hot_add_increment_mib,omitempty"`
|
||||
MemoryHotAddLimitMiB *int64 `json:"memory_hot_add_limit_mib,omitempty"`
|
||||
}
|
||||
|
||||
// InventoryMetrics captures the current runtime metric floor projected onto
|
||||
// canonical Pulse metrics for VMware-backed hosts and VMs.
|
||||
type InventoryMetrics struct {
|
||||
|
|
@ -201,6 +235,7 @@ type InventoryVM struct {
|
|||
NetworkAdapters []InventoryVMNetworkAdapter `json:"network_adapters,omitempty"`
|
||||
VirtualDisks []InventoryVMVirtualDisk `json:"virtual_disks,omitempty"`
|
||||
Tools *InventoryVMTools `json:"tools,omitempty"`
|
||||
Hardware *InventoryVMHardware `json:"hardware,omitempty"`
|
||||
Metrics *InventoryMetrics `json:"metrics,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -552,6 +587,7 @@ func vmwareRecordsFromSnapshot(snapshot *InventorySnapshot, now func() time.Time
|
|||
NetworkAdapters: vmwareNetworkAdaptersData(vm.NetworkAdapters),
|
||||
VirtualDisks: vmwareVirtualDisksData(vm.VirtualDisks),
|
||||
Tools: vmwareToolsData(vm.Tools),
|
||||
Hardware: vmwareVMHardwareData(vm.Hardware),
|
||||
},
|
||||
Tags: filterNonEmptyStrings(
|
||||
"vmware",
|
||||
|
|
@ -714,6 +750,7 @@ func cloneInventoryVMs(in []InventoryVM) []InventoryVM {
|
|||
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
|
||||
|
|
@ -824,6 +861,39 @@ func cloneInventoryVMTools(in *InventoryVMTools) *InventoryVMTools {
|
|||
return &out
|
||||
}
|
||||
|
||||
func cloneInventoryVMHardware(in *InventoryVMHardware) *InventoryVMHardware {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := *in
|
||||
out.InstantCloneFrozen = cloneBoolPointer(in.InstantCloneFrozen)
|
||||
out.EFILegacyBoot = cloneBoolPointer(in.EFILegacyBoot)
|
||||
out.BootDelayMilliseconds = cloneInt64Pointer(in.BootDelayMilliseconds)
|
||||
out.BootRetry = cloneBoolPointer(in.BootRetry)
|
||||
out.BootRetryDelayMilliseconds = cloneInt64Pointer(in.BootRetryDelayMilliseconds)
|
||||
out.EnterSetupMode = cloneBoolPointer(in.EnterSetupMode)
|
||||
out.BootDevices = cloneInventoryVMBootDevices(in.BootDevices)
|
||||
out.CPUCoresPerSocket = cloneInt64Pointer(in.CPUCoresPerSocket)
|
||||
out.CPUHotAddEnabled = cloneBoolPointer(in.CPUHotAddEnabled)
|
||||
out.CPUHotRemoveEnabled = cloneBoolPointer(in.CPUHotRemoveEnabled)
|
||||
out.MemoryHotAddEnabled = cloneBoolPointer(in.MemoryHotAddEnabled)
|
||||
out.MemoryHotAddIncrementMiB = cloneInt64Pointer(in.MemoryHotAddIncrementMiB)
|
||||
out.MemoryHotAddLimitMiB = cloneInt64Pointer(in.MemoryHotAddLimitMiB)
|
||||
return &out
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func cloneInventoryEnrichmentIssues(in []InventoryEnrichmentIssue) []InventoryEnrichmentIssue {
|
||||
if in == nil {
|
||||
return nil
|
||||
|
|
@ -1237,6 +1307,50 @@ func vmwareToolsData(tools *InventoryVMTools) *unifiedresources.VMwareToolsData
|
|||
}
|
||||
}
|
||||
|
||||
func vmwareVMHardwareData(hardware *InventoryVMHardware) *unifiedresources.VMwareVMHardwareData {
|
||||
if hardware == nil {
|
||||
return nil
|
||||
}
|
||||
return &unifiedresources.VMwareVMHardwareData{
|
||||
GuestOS: strings.TrimSpace(hardware.GuestOS),
|
||||
InstantCloneFrozen: cloneBoolPointer(hardware.InstantCloneFrozen),
|
||||
Version: strings.TrimSpace(hardware.Version),
|
||||
UpgradePolicy: strings.TrimSpace(hardware.UpgradePolicy),
|
||||
UpgradeVersion: strings.TrimSpace(hardware.UpgradeVersion),
|
||||
UpgradeStatus: strings.TrimSpace(hardware.UpgradeStatus),
|
||||
UpgradeErrorMessage: strings.TrimSpace(hardware.UpgradeErrorMessage),
|
||||
BootType: strings.TrimSpace(hardware.BootType),
|
||||
EFILegacyBoot: cloneBoolPointer(hardware.EFILegacyBoot),
|
||||
BootNetworkProtocol: strings.TrimSpace(hardware.BootNetworkProtocol),
|
||||
BootDelayMilliseconds: cloneInt64Pointer(hardware.BootDelayMilliseconds),
|
||||
BootRetry: cloneBoolPointer(hardware.BootRetry),
|
||||
BootRetryDelayMilliseconds: cloneInt64Pointer(hardware.BootRetryDelayMilliseconds),
|
||||
EnterSetupMode: cloneBoolPointer(hardware.EnterSetupMode),
|
||||
BootDevices: vmwareBootDevicesData(hardware.BootDevices),
|
||||
CPUCoresPerSocket: cloneInt64Pointer(hardware.CPUCoresPerSocket),
|
||||
CPUHotAddEnabled: cloneBoolPointer(hardware.CPUHotAddEnabled),
|
||||
CPUHotRemoveEnabled: cloneBoolPointer(hardware.CPUHotRemoveEnabled),
|
||||
MemoryHotAddEnabled: cloneBoolPointer(hardware.MemoryHotAddEnabled),
|
||||
MemoryHotAddIncrementMiB: cloneInt64Pointer(hardware.MemoryHotAddIncrementMiB),
|
||||
MemoryHotAddLimitMiB: cloneInt64Pointer(hardware.MemoryHotAddLimitMiB),
|
||||
}
|
||||
}
|
||||
|
||||
func vmwareBootDevicesData(devices []InventoryVMBootDevice) []unifiedresources.VMwareBootDeviceData {
|
||||
if len(devices) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]unifiedresources.VMwareBootDeviceData, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
out = append(out, unifiedresources.VMwareBootDeviceData{
|
||||
Type: strings.TrimSpace(device.Type),
|
||||
NIC: strings.TrimSpace(device.NIC),
|
||||
Disks: cloneStringSlice(device.Disks),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func vmwareNetworkAdapterMACAddresses(adapters []InventoryVMNetworkAdapter) []string {
|
||||
if len(adapters) == 0 {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -165,6 +165,31 @@ func TestProviderRecords_ProjectCanonicalVMwareResources(t *testing.T) {
|
|||
GuestRebootComponents: []string{"drivers"},
|
||||
GuestRebootRequestTime: "2026-03-30T18:20:00Z",
|
||||
},
|
||||
Hardware: &InventoryVMHardware{
|
||||
GuestOS: "UBUNTU_64",
|
||||
InstantCloneFrozen: boolPtr(false),
|
||||
Version: "VMX_20",
|
||||
UpgradePolicy: "AFTER_CLEAN_SHUTDOWN",
|
||||
UpgradeVersion: "VMX_21",
|
||||
UpgradeStatus: "PENDING",
|
||||
BootType: "EFI",
|
||||
EFILegacyBoot: boolPtr(false),
|
||||
BootNetworkProtocol: "IPV4",
|
||||
BootDelayMilliseconds: int64Ptr(5000),
|
||||
BootRetry: boolPtr(true),
|
||||
BootRetryDelayMilliseconds: int64Ptr(10000),
|
||||
EnterSetupMode: boolPtr(false),
|
||||
BootDevices: []InventoryVMBootDevice{
|
||||
{Type: "DISK", Disks: []string{"2000"}},
|
||||
{Type: "ETHERNET", NIC: "4000"},
|
||||
},
|
||||
CPUCoresPerSocket: int64Ptr(2),
|
||||
CPUHotAddEnabled: boolPtr(true),
|
||||
CPUHotRemoveEnabled: boolPtr(false),
|
||||
MemoryHotAddEnabled: boolPtr(true),
|
||||
MemoryHotAddIncrementMiB: int64Ptr(256),
|
||||
MemoryHotAddLimitMiB: int64Ptr(16384),
|
||||
},
|
||||
}},
|
||||
Datastores: []InventoryDatastore{{
|
||||
Datastore: "datastore-11",
|
||||
|
|
@ -295,6 +320,15 @@ func TestProviderRecords_ProjectCanonicalVMwareResources(t *testing.T) {
|
|||
if got := vmRecord.Resource.VMware.Tools.GuestRebootComponents; len(got) != 1 || got[0] != "drivers" {
|
||||
t.Fatalf("vm tools reboot components = %+v, want [drivers]", got)
|
||||
}
|
||||
if got := vmRecord.Resource.VMware.Hardware; got == nil || got.Version != "VMX_20" || got.UpgradeStatus != "PENDING" {
|
||||
t.Fatalf("vm hardware projection = %+v, want VMX_20 pending", got)
|
||||
}
|
||||
if got := vmRecord.Resource.VMware.Hardware.BootDevices; len(got) != 2 || got[0].Disks[0] != "2000" || got[1].NIC != "4000" {
|
||||
t.Fatalf("vm hardware boot devices = %+v, want disk and ethernet entries", got)
|
||||
}
|
||||
if got := vmRecord.Resource.VMware.Hardware.MemoryHotAddLimitMiB; got == nil || *got != 16384 {
|
||||
t.Fatalf("vm hardware memory hot-add limit = %+v, want 16384", got)
|
||||
}
|
||||
if got := vmRecord.Resource.ParentName; got != "esxi-01.lab.local" {
|
||||
t.Fatalf("vm parent name = %q, want esxi-01.lab.local", got)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue