diff --git a/pkg/harvester-manager/l10n/en-us.yaml b/pkg/harvester-manager/l10n/en-us.yaml index 6506b3e03c4..7da28f81e5e 100644 --- a/pkg/harvester-manager/l10n/en-us.yaml +++ b/pkg/harvester-manager/l10n/en-us.yaml @@ -24,3 +24,5 @@ harvesterManager: title: VGPUs label: VGPU type placeholder: 'Please select a VGPU' + allocatable: allocatable + allocatableUnknown: missing allocation info diff --git a/pkg/harvester-manager/machine-config/harvester.vue b/pkg/harvester-manager/machine-config/harvester.vue index b312ad4d9cb..f24cc1c97fc 100644 --- a/pkg/harvester-manager/machine-config/harvester.vue +++ b/pkg/harvester-manager/machine-config/harvester.vue @@ -18,9 +18,10 @@ import YamlEditor from '@shell/components/YamlEditor'; import { Checkbox } from '@components/Form/Checkbox'; import { Banner } from '@components/Banner'; import { clone, get } from '@shell/utils/object'; +import { uniq, removeObject } from '@shell/utils/array'; + +import { _CREATE, _VIEW } from '@shell/config/query-params'; -import { _CREATE } from '@shell/config/query-params'; -import { removeObject } from '@shell/utils/array'; import { mapGetters } from 'vuex'; import { HCI, @@ -315,7 +316,7 @@ export default { this.networksObj = JSON.parse(this.value.networkInfo); this.networksHistoric = this.value.networkInfo; - this.getEnabledVGpuDevices(); + this.getAvailableVGpuDevices(); this.update(); } catch (e) { @@ -375,7 +376,7 @@ export default { if (this.value.vgpuInfo) { const vGPURequests = JSON.parse(this.value.vgpuInfo)?.vGPURequests; - vGpus = vGPURequests?.map((r) => r?.name).filter((r) => r) || []; + vGpus = vGPURequests?.map((r) => r?.deviceName).filter((f) => f) || []; } return { @@ -407,7 +408,8 @@ export default { networkDataIsBase64, vmAffinityIsBase64, SOURCE_TYPE, - vGpuEnabledDevices: {}, + vGpuDevices: {}, + vGpusInit: vGpus, vGpus, }; }, @@ -479,34 +481,15 @@ export default { }; }, - vGpusAllocatable() { - const allocatable = this.allNodeObjects.reduce((acc, node) => [ - ...acc, - ...Object.keys(node.status.allocatable || {}).filter((k) => k.startsWith(VGPU_PREFIX.NVIDIA)), - ], []); - - return allocatable.reduce((acc, v) => { - let available = 0; - - this.allNodeObjects.forEach((n) => { - if (n.status.allocatable[v]) { - available += Number(n.status.allocatable[v]); - } - }); - - if (available > 0) { - return { - ...acc, - [v]: available - }; - } - - return acc; - }, {}); - }, - vGpuOptions() { - return Object.keys(this.vGpuEnabledDevices).filter((x) => !this.vGpus.includes(x)); + const vGpuTypes = uniq([ + ...this.vGpusInit, + ...Object.values(this.vGpuDevices) + .filter((vGpu) => vGpu.enabled && !!vGpu.type && (vGpu.allocatable === null || vGpu.allocatable > 0)) + .map((vGpu) => vGpu.type), + ]); + + return vGpuTypes; } }, @@ -697,13 +680,31 @@ export default { validatorVGpus(errors) { const notAllocatable = this.vGpus - .map((id) => this.vGpuEnabledDevices[id]) - .filter((vGpu) => this.vGpusAllocatable[vGpu?.type] < this.machinePools[this.poolIndex]?.pool?.quantity); + .map((type) => { + const allocated = this.machinePools.reduce((acc, machinePool) => { + const vGPURequests = JSON.parse(machinePool?.config?.vgpuInfo || '')?.vGPURequests; + + const vGpuTypes = vGPURequests?.map((r) => r?.deviceName).filter((f) => f) || []; + + if (vGpuTypes.includes(type)) { + return acc + machinePool.pool.quantity; + } + + return acc; + }, 0); + + return { + vGpu: Object.values(this.vGpuDevices).filter((f) => f.type === type)?.[0], + allocated + }; + }) + .filter(({ vGpu, allocated }) => vGpu && vGpu.allocatable > 0 && vGpu.allocatable < allocated); - notAllocatable.forEach((vGpu) => { + notAllocatable.forEach(({ vGpu, allocated }) => { const message = this.$store.getters['i18n/t']('cluster.credential.harvester.vGpus.errors.notAllocatable', { - vGpus: vGpu?.type, - pool: this.machinePools[this.poolIndex]?.pool?.name || '', + vGpu: vGpu?.type, + allocated, + allocatable: vGpu?.allocatable }); errors.push(message); @@ -733,22 +734,45 @@ export default { } }, - async getEnabledVGpuDevices() { + async getAvailableVGpuDevices() { const clusterId = get(this.credential, 'decodedData.clusterId'); if (clusterId) { const url = `/k8s/clusters/${ clusterId }/v1`; - const res = await this.$store.dispatch('cluster/request', { url: `${ url }/${ HCI.VGPU_DEVICE }s` }); - - this.vGpuEnabledDevices = (res?.data || []) - .filter((v) => v.spec.enabled) - .reduce((acc, v) => ({ - ...acc, - [v.id]: { - type: VGPU_PREFIX.NVIDIA + v.spec.vGPUTypeName?.replace(' ', '_'), - id: v.id - }, - }), {}); + + const vGpus = await this.$store.dispatch('cluster/request', { url: `${ url }/${ HCI.VGPU_DEVICE }` }); + + let deviceCapacity = null; + + try { + const harvesterCluster = await this.$store.dispatch('cluster/request', { url: `${ url }/harvester/cluster/local` }); + + if (harvesterCluster?.links?.deviceCapacity) { + deviceCapacity = await this.$store.dispatch('cluster/request', { url: harvesterCluster?.links?.deviceCapacity }); + } + } catch (e) { + } + + this.vGpuDevices = (vGpus?.data || []) + .reduce((acc, v) => { + const type = v.spec.vGPUTypeName ? `${ VGPU_PREFIX.NVIDIA }${ v.spec.vGPUTypeName.replace(' ', '_') }` : ''; + + let allocatable = null; + + if (deviceCapacity) { + allocatable = deviceCapacity[type] ? Number(deviceCapacity[type]) : 0; + } + + return { + ...acc, + [v.id]: { + id: v.id, + enabled: v.spec.enabled, + allocatable, + type, + }, + }; + }, {}); } }, @@ -909,14 +933,14 @@ export default { }, updateVGpu() { - const vGPURequests = this.vGpus?.filter((name) => name).reduce((acc, name, i) => ([ - ...acc, - { - name, - deviceName: this.vGpuEnabledDevices[name]?.type, - } - ]) - , []); + const vGPURequests = this.vGpus?.filter((f) => f).map((deviceName) => ({ + /** + * 'provisioned' is a placeholder. + * The real vGpu name is assigned to the provisioned VM by the backend and saved in 'harvesterhci.io/deviceAllocationDetails' annotation. + */ + name: 'provisioned', + deviceName, + })) || []; this.value.vgpuInfo = vGPURequests.length > 0 ? JSON.stringify({ vGPURequests }) : ''; }, @@ -1117,9 +1141,25 @@ export default { }, vGpuOptionLabel(opt) { - const vGpu = this.vGpuEnabledDevices[opt]; + let label = opt.replace(VGPU_PREFIX.NVIDIA, ''); + + if (this.mode === _VIEW) { + return label; + } + + /** + * We get the allocatable label from the first vGpu profile found for each vGpu type. + * This is consistent as long as vGpu profiles with the same vGpu type, have the same allocable number. + */ + const vGpu = Object.values(this.vGpuDevices).filter((f) => f.type === opt)?.[0]; + + if (vGpu?.allocatable === null) { + label += ` (${ this.t('harvesterManager.vGpu.allocatableUnknown') })`; + } else if (vGpu?.allocatable > 0) { + label += ` (${ this.t('harvesterManager.vGpu.allocatable') }: ${ vGpu.allocatable })`; + } - return `${ vGpu?.type?.replace(VGPU_PREFIX.NVIDIA, '') } - ${ vGpu?.id } (allocatable: ${ this.vGpusAllocatable[vGpu?.type] })`; + return label; } } }; diff --git a/shell/assets/translations/en-us.yaml b/shell/assets/translations/en-us.yaml index 26aea5e2b19..332abb8ca90 100644 --- a/shell/assets/translations/en-us.yaml +++ b/shell/assets/translations/en-us.yaml @@ -1359,7 +1359,7 @@ cluster: macFormat: 'Invalid MAC address format.' vGpus: errors: - notAllocatable: '"VGPUs" not allocatable. There are not enough [{vGpus}] devices to be allocated to each node in machine pool [{pool}]' + notAllocatable: '[{vGpu}] vGPU device is not allocatable; required: {allocated}, allocatable: {allocatable}' volume: title: Volumes volume: Volume