feat: add vGPU MIGConfiguration page

Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
Andy Lee 2025-10-22 14:15:38 +08:00
parent 5d996c42dc
commit ce5928108e
No known key found for this signature in database
GPG Key ID: EC774C32160918ED
6 changed files with 370 additions and 1 deletions

View File

@ -457,6 +457,7 @@ export function init($plugin, store) {
HCI.PCI_DEVICE,
HCI.SR_IOVGPU_DEVICE,
HCI.VGPU_DEVICE,
HCI.MIG_CONFIGURATION,
HCI.USB_DEVICE,
HCI.ADD_ONS,
HCI.SECRET,
@ -849,6 +850,26 @@ export function init($plugin, store) {
]
});
virtualType({
labelKey: 'harvester.migconfiguration.label',
group: 'advanced',
weight: 12,
name: HCI.MIG_CONFIGURATION,
namespaced: false,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.MIG_CONFIGURATION }
},
exact: false,
ifHaveType: HCI.MIG_CONFIGURATION,
});
configureType(HCI.MIG_CONFIGURATION, {
isCreatable: false,
hiddenNamespaceGroupButton: true,
canYaml: false,
});
virtualType({
labelKey: 'harvester.usb.label',
group: 'advanced',

View File

@ -0,0 +1,129 @@
<script>
import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
import CruResource from '@shell/components/CruResource';
import { LabeledInput } from '@components/Form/LabeledInput';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import LabelValue from '@shell/components/LabelValue';
import CreateEditView from '@shell/mixins/create-edit-view';
export default {
name: 'HarvesterEditMIGConfiguration',
// emits: ['update:value'],
components: {
Tab,
Tabbed,
CruResource,
LabeledInput,
NameNsDescription,
LabelValue
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
value: {
type: Object,
required: true,
}
},
data() {
const { profileSpec } = this.value.spec;
return { profileSpec: profileSpec || [] };
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.updateBeforeSave);
}
},
methods: {
updateBeforeSave() {
// MIGConfiguration CRD don't have any namespace field,
// so we need to remove the namespace field before saving
delete this.value.metadata.namespace;
// enable the MIGConfiguration when saving
this.value.spec.enabled = true;
},
labelTitle(profile) {
return `${ profile.name }`;
},
available(profile) {
const count = this.value.status?.profileStatus?.find((p) => p.id === profile.id)?.available;
return count || 0;
},
updateRequested(neu, profile) {
if (neu === null || neu === '') return;
const newValue = Number(neu);
if (newValue < 0) {
profile.requested = 0;
} else if (newValue > this.available(profile)) {
profile.requested = this.available(profile);
} else {
profile.requested = newValue;
}
}
},
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
finish-button-mode="enable"
@finish="save"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
:mode="mode"
/>
<Tabbed
v-bind="$attrs"
class="mt-15"
:side-tabs="true"
>
<Tab
name="Profile Spec"
:label="t('harvester.migconfiguration.profileSpec')"
:weight="1"
class="bordered-table"
>
<div
v-for="(profile, index) in profileSpec"
:key="index"
>
<LabelValue
:value="labelTitle(profile)"
class="mb-10"
/>
<LabeledInput
v-model:value="profile.requested"
:min="0"
:max="available(profile)"
type="number"
class="mb-20"
:label="`${t('harvester.migconfiguration.requested')} (available : ${available(profile)})`"
@update:value="updateRequested($event, profile)"
/>
</div>
</Tab>
</Tabbed>
</CruResource>
</template>

View File

@ -272,6 +272,7 @@ harvester:
cronExpression: Cron Expression
retain: Retain
scheduleType: Type
profileCount: Profile Count
maxFailure: Max Failure
sourceVm: Source Virtual Machine
vmSchedule: Virtual Machine Schedule
@ -1664,6 +1665,12 @@ harvester:
middle: here
suffix: to enable it to manage your SR-IOV GPU devices.
migconfiguration:
label: vGPU MIG Configurations
infoBanner: To configure the MIG configuration, please disable it first and re-enable after editing the configuration.
profileSpec: Profile Specs
requested: Requested
vgpu:
label: vGPU Devices
noPermission: Please contact system administrator to add Harvester add-ons first.
@ -1937,6 +1944,12 @@ typeLabel:
other { vGPU Devices }
}
devices.harvesterhci.io.migconfiguration: |-
{count, plural,
one { vGPU MIG Configuration }
other { vGPU MIG Configurations }
}
harvesterhci.io.secret: |-
{count, plural,
one { Secret }

View File

@ -0,0 +1,105 @@
<script>
import { STATE, SIMPLE_NAME } from '@shell/config/table-headers';
import { allHash } from '@shell/utils/promise';
import Banner from '@components/Banner/Banner.vue';
import Loading from '@shell/components/Loading';
import ResourceTable from '@shell/components/ResourceTable';
import { HCI } from '../types';
export default {
name: 'ListMIGConfigurations',
inheritAttrs: false,
components: {
Banner,
Loading,
ResourceTable,
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.schema = this.$store.getters[`${ inStore }/schemaFor`](HCI.MIG_CONFIGURATION);
if (this.hasSchema) {
try {
const hash = await allHash({ migconfigs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.MIG_CONFIGURATION }) });
this.rows = hash.migconfigs;
} catch (e) {}
}
},
data() {
// const inStore = this.$store.getters['currentProduct'].inStore;
return {
rows: [],
schema: null,
};
},
computed: {
hasSchema() {
return !!this.schema;
},
rowsData() {
const inStore = this.$store.getters['currentProduct'].inStore;
const rows = this.$store.getters[`${ inStore }/all`](HCI.MIG_CONFIGURATION) || [];
return rows;
},
headers() {
const cols = [
STATE,
SIMPLE_NAME,
{
name: 'address',
label: 'Address',
value: 'spec.gpuAddress',
sort: ['spec.gpuAddress']
},
{
name: 'Profile Count',
label: 'Profile Count',
labelKey: 'harvester.tableHeaders.profileCount',
value: 'spec.profileSpec.length',
sort: ['spec.profileSpec.length'],
align: 'center'
},
{
name: 'status',
label: 'Status',
labelKey: 'tableHeaders.status',
sort: ['status.status'],
value: 'status.status',
},
];
return cols;
},
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else-if="hasSchema">
<Banner
color="info"
:label="t('harvester.migconfiguration.infoBanner')"
/>
<ResourceTable
v-bind="$attrs"
:groupable="false"
:namespaced="false"
:headers="headers"
:schema="schema"
:rows="rowsData"
key-field="_key"
/>
</div>
</template>

View File

@ -0,0 +1,100 @@
import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string';
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
// import { NODE } from '@shell/config/types';
// import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
// import { HCI } from '../types';
/**
* Class representing vGPU MIGConfiguration resource.
* @extends SteveModal
*/
export default class MIGCONFIGURATION extends SteveModel {
get _availableActions() {
let out = super._availableActions;
out = out.map((action) => {
if (action.action === 'goToEditYaml') {
return { ...action, enabled: true };
} else if (action.action === 'goToEdit') {
return { ...action, enabled: !this.spec.enabled };
} else {
return action;
}
});
out.push(
{
action: 'enableConfig',
enabled: !this.isEnabled,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableConfig',
enabled: this.isEnabled,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
},
);
return out;
}
get canYaml() {
return false;
}
get canDelete() {
return false;
}
get actualState() {
return this.isEnabled ? 'Enabled' : 'Disabled';
}
get stateDisplay() {
return this.actualState;
}
get stateColor() {
const state = this.actualState;
return colorForState(state);
}
get isEnabled() {
return this.spec.enabled;
}
async enableConfig() {
try {
this.spec.enabled = true;
await this.save();
} catch (err) {
this.$dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.name) }),
err,
}, { root: true });
}
}
async disableConfig() {
const { enabled: currentEnabled } = this.spec;
try {
this.spec.enabled = false;
await this.save();
} catch (err) {
this.spec.enabled = currentEnabled;
this.$dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.name) }),
err,
}, { root: true });
}
}
// cleanForSave(data, _forNew) {
// console.log("🚀 ~ MIGCONFIGURATION ~ cleanForSave ~ data:", data)
// return data;
// }
}

View File

@ -44,6 +44,7 @@ export const HCI = {
SR_IOVGPU_DEVICE: 'devices.harvesterhci.io.sriovgpudevice',
USB_DEVICE: 'devices.harvesterhci.io.usbdevice',
USB_CLAIM: 'devices.harvesterhci.io.usbdeviceclaim',
MIG_CONFIGURATION: 'devices.harvesterhci.io.migconfiguration',
VLAN_CONFIG: 'network.harvesterhci.io.vlanconfig',
VLAN_STATUS: 'network.harvesterhci.io.vlanstatus',
ADD_ONS: 'harvesterhci.io.addon',
@ -53,7 +54,7 @@ export const HCI = {
LB: 'loadbalancer.harvesterhci.io.loadbalancer',
IP_POOL: 'loadbalancer.harvesterhci.io.ippool',
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup'
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup',
};
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';