mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-11-15 05:16:16 +08:00
* feat: move OS type, CPU model and system options to the VM Basics tab OS Type, CPU Model and the CPU/system checkboxes (CPU pinning, USB tablet, TPM, EFI, Secure Boot and their persistent-state options) are core machine settings, but they were buried at the bottom of Advanced Options. Show them on the Basics tab, next to CPU and memory. Install Guest Agent stays in Advanced Options because it changes the cloud config. It now sits right below the User Data editor (the data it modifies) through a new optional slot in the Cloud Config component. Only the placement changes; behaviour and validation are untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Alejandro Bonilla <abonilla@suse.com> * feat: follow the review on the VM Basics layout - Basics now reads OS Type and CPU Model, then CPU and memory, then the system checkboxes, the CPU Manager banner and the SSH key, in the order proposed in review. The checkboxes sit in one wrapper; USB Tablet drops its extra mt-20 and uses the plain `check` class like the others. - Advanced Options puts Maintenance Strategy on the same row as Run Strategy; Reserved Memory moves to its own row. - The moved labels leave harvester.virtualMachine.advancedOptions: TPM, EFI, Secure Boot and the persistent-state labels are now under harvester.virtualMachine.systemOptions (also used by the template version editor), and the CPU Manager banner text is under harvester.virtualMachine.cpuPinning.cpuManager. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Alejandro Bonilla <abonilla@suse.com> * feat: match the VM template form and address review on the VM Basics layout The VM template form still had OS Type, CPU Model and the CPU/system checkboxes under Advanced Options, so creating a VM and creating a template put them in different places. Use the same layout as the VM form: - Basics: OS Type and CPU Model, then CPU and memory, then the CPU Pinning, USB Tablet, TPM, EFI and Secure Boot checkboxes (plus the persistent-state options when they apply), then the SSH key. - Advanced Options: Run Strategy and Maintenance Strategy on one row, Reserved Memory on its own row, then Show more and Cloud Config. Install Guest Agent sits right below the User Data editor through the after-user-data slot. Review follow-ups on both forms: - OS Type shows an info icon in create mode warning that selecting an image with an OS label on the Volumes tab may change the OS Type selection, now that OS Type comes before the volumes. - The CPU Manager banner under CPU Pinning is info in create mode, where it only explains why the checkbox is disabled, and stays a warning in edit mode, where it is about the restart. Only placement and presentation change; behaviour and validation are untouched. Signed-off-by: Alejandro Bonilla <abonilla@suse.com> --------- Signed-off-by: Alejandro Bonilla <abonilla@suse.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
604 lines
17 KiB
Vue
604 lines
17 KiB
Vue
<script>
|
|
import { mapGetters } from 'vuex';
|
|
import Tabbed from '@shell/components/Tabbed';
|
|
import Tab from '@shell/components/Tabbed/Tab';
|
|
import { Checkbox } from '@components/Form/Checkbox';
|
|
import CruResource from '@shell/components/CruResource';
|
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
import { set } from '@shell/utils/object';
|
|
import { Banner } from '@components/Banner';
|
|
import KeyValue from '@shell/components/form/KeyValue';
|
|
import NodeScheduling from '@shell/components/form/NodeScheduling';
|
|
import PodAffinity from '@shell/components/form/PodAffinity';
|
|
import UnitInput from '@shell/components/form/UnitInput';
|
|
import { randomStr } from '@shell/utils/string';
|
|
import { _CONFIG, _EDIT, _VIEW } from '@shell/config/query-params';
|
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
import { AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook';
|
|
import { HCI } from '../types';
|
|
import VM_MIXIN from '../mixins/harvester-vm';
|
|
import Reserved from './kubevirt.io.virtualmachine/VirtualMachineReserved';
|
|
import Volume from './kubevirt.io.virtualmachine/VirtualMachineVolume';
|
|
import Network from './kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
|
import CpuMemory from './kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
|
|
import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel';
|
|
import CloudConfig from './kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
|
import SSHKey from './kubevirt.io.virtualmachine/VirtualMachineSSHKey';
|
|
import Filesystem from './kubevirt.io.virtualmachine/VirtualMachineFilesystem';
|
|
|
|
export default {
|
|
name: 'HarvesterEditVMTemplate',
|
|
|
|
emits: ['update:templateValue'],
|
|
|
|
components: {
|
|
Tab,
|
|
SSHKey,
|
|
Volume,
|
|
Tabbed,
|
|
Network,
|
|
Checkbox,
|
|
CpuMemory,
|
|
CpuModel,
|
|
CruResource,
|
|
CloudConfig,
|
|
LabeledSelect,
|
|
NameNsDescription,
|
|
NodeScheduling,
|
|
PodAffinity,
|
|
Reserved,
|
|
UnitInput,
|
|
Banner,
|
|
KeyValue,
|
|
Filesystem,
|
|
},
|
|
|
|
mixins: [CreateEditView, VM_MIXIN],
|
|
|
|
inheritAttrs: false,
|
|
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
},
|
|
|
|
data() {
|
|
if (this.mode === _EDIT) {
|
|
this.value.cleanForNew();
|
|
}
|
|
|
|
const templateId = this.value.templateId || this.$route.query.templateId;
|
|
|
|
return {
|
|
templateId,
|
|
templateValue: null,
|
|
templateSpec: null,
|
|
versionName: '',
|
|
description: '',
|
|
defaultVersion: null,
|
|
isDefaultVersion: false,
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters({ t: 'i18n/t' }),
|
|
isConfig() {
|
|
return this.$route.query?.as === _CONFIG || this.isView;
|
|
},
|
|
|
|
realTemplateMode() {
|
|
return this.templateId ? _VIEW : this.mode;
|
|
},
|
|
|
|
secretNamePrefix() {
|
|
return this.templateValue?.metadata?.name;
|
|
},
|
|
|
|
filesystemEnabled() {
|
|
return this.$store.getters['harvester-common/getFeatureEnabled']('supportFilesystem');
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
templateId: {
|
|
async handler(neu) {
|
|
const templates = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_TEMPLATE });
|
|
let templateValue = templates.find( (V) => V.id === neu) || null;
|
|
let templateSpec = templateValue?.spec;
|
|
|
|
if (!templateValue) {
|
|
templateSpec = {
|
|
description: '',
|
|
defaultVersionId: ''
|
|
};
|
|
|
|
templateValue = await this.$store.dispatch('harvester/create', {
|
|
metadata: {
|
|
name: '',
|
|
namespace: ''
|
|
},
|
|
spec: templateSpec,
|
|
type: HCI.VM_TEMPLATE
|
|
});
|
|
}
|
|
|
|
this.templateValue = templateValue;
|
|
this.templateSpec = templateSpec;
|
|
},
|
|
immediate: true
|
|
}
|
|
},
|
|
|
|
created() {
|
|
this.registerAfterHook(async() => {
|
|
if (this.isDefaultVersion) {
|
|
// Set the default version according to annotation:[HCI_ANNOTATIONS.TEMPLATE_VERSION_CUSTOM_NAME]
|
|
const versions = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_VERSION, opt: { force: true } });
|
|
|
|
const version = versions.find( (V) => V?.metadata?.annotations?.[HCI_ANNOTATIONS.TEMPLATE_VERSION_CUSTOM_NAME] === this.customName);
|
|
|
|
if (version) {
|
|
try {
|
|
this.templateValue.defaultVersionId = version.id;
|
|
|
|
const data = [{
|
|
op: 'replace', path: '/spec/defaultVersionId', value: version.id
|
|
}];
|
|
|
|
await this.templateValue.patch( data, { url: this.templateValue.linkFor('view') });
|
|
} catch (err) {
|
|
return Promise.reject(new Error(err.message));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
mounted() {
|
|
this.imageId = this.diskRows[0]?.image || '';
|
|
this['filesystemRows'] = this.getFilesystemRows(this.value.spec.vm);
|
|
},
|
|
|
|
methods: {
|
|
updateCpuModel(value) {
|
|
if (!this.spec?.template?.spec?.domain?.cpu) {
|
|
set(this.spec, 'template.spec.domain.cpu', {});
|
|
}
|
|
|
|
if (value && value !== '') {
|
|
set(this.spec.template.spec.domain.cpu, 'model', value);
|
|
} else {
|
|
delete this.spec.template.spec.domain.cpu.model;
|
|
}
|
|
},
|
|
|
|
async saveVMT(buttonCb) {
|
|
this.parseVM();
|
|
|
|
const templates = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_TEMPLATE });
|
|
const template = templates.find( (O) => O.metadata.name === this.templateValue.metadata.name);
|
|
|
|
try {
|
|
if (!this.templateId) {
|
|
if (this.templateValue?.metadata?.name) {
|
|
await this.templateValue.save();
|
|
} else {
|
|
this.errors = [this.t('validation.required', { key: this.t('harvester.vmTemplate.nameNsDescription.name') })];
|
|
buttonCb(false);
|
|
|
|
return;
|
|
}
|
|
} else {
|
|
template.save();
|
|
}
|
|
|
|
this.value.cleanForNew();
|
|
this.customName = randomStr(10);
|
|
this.value.metadata['annotations'] = {
|
|
...this.value.metadata.annotations,
|
|
[HCI_ANNOTATIONS.TEMPLATE_VERSION_CUSTOM_NAME]: this.customName
|
|
};
|
|
|
|
const name = this.templateValue.metadata.name || template.metadata.name;
|
|
const namespace = this.templateValue.metadata.namespace || template.metadata.namespace;
|
|
|
|
if (this.isCreate) {
|
|
this.value.metadata.namespace = namespace;
|
|
}
|
|
|
|
this.value.spec['templateId'] = `${ namespace }/${ name }`;
|
|
|
|
// inherit labels and annotations so the VM gets them when created from the template
|
|
this.value.spec.vm.metadata.labels = {
|
|
...this.value.spec.vm.metadata.labels,
|
|
...this.value.metadata.labels
|
|
};
|
|
this.value.spec.vm.metadata.annotations = {
|
|
...this.value.spec.vm.metadata.annotations,
|
|
...this.value.metadata.annotations
|
|
};
|
|
const res = await this.value.save();
|
|
|
|
await this.saveSecret(res);
|
|
await this.applyHooks(AFTER_SAVE_HOOKS);
|
|
this.done();
|
|
} catch (e) {
|
|
this.errors = [e];
|
|
buttonCb(false);
|
|
}
|
|
},
|
|
|
|
onTabChanged({ tab }) {
|
|
if (tab.name === 'advanced') {
|
|
this.$refs.yamlEditor?.refresh();
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<CruResource
|
|
v-if="templateSpec && spec"
|
|
:done-route="doneRoute"
|
|
:resource="value"
|
|
:can-yaml="false"
|
|
:mode="mode"
|
|
:errors="errors"
|
|
:apply-hooks="applyHooks"
|
|
@finish="saveVMT"
|
|
@error="e=>errors=e"
|
|
>
|
|
<NameNsDescription
|
|
:value="templateValue"
|
|
:mode="realTemplateMode"
|
|
name-label="harvester.vmTemplate.nameNsDescription.name"
|
|
:namespaced="true"
|
|
@update:value="$emit('update:templateValue', $event)"
|
|
/>
|
|
|
|
<Checkbox
|
|
v-if="templateId"
|
|
v-model:value="isDefaultVersion"
|
|
class="mb-20"
|
|
:label="t('tableHeaders.defaultVersion')"
|
|
type="checkbox"
|
|
:mode="mode"
|
|
/>
|
|
|
|
<Tabbed
|
|
:side-tabs="true"
|
|
@changed="onTabChanged"
|
|
>
|
|
<Tab
|
|
name="Basics"
|
|
:label="t('harvester.vmTemplate.tabs.basics')"
|
|
>
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<LabeledSelect
|
|
v-model:value="osType"
|
|
label-key="harvester.virtualMachine.osType"
|
|
:mode="mode"
|
|
:options="OS"
|
|
:tooltip="isCreate ? t('harvester.virtualMachine.osTypeTooltip') : null"
|
|
/>
|
|
</div>
|
|
<div class="col span-6">
|
|
<CpuModel
|
|
:value="spec.template.spec.domain.cpu?.model || ''"
|
|
:mode="mode"
|
|
@update:value="updateCpuModel"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<CpuMemory
|
|
class="mb-20"
|
|
:cpu="cpu"
|
|
:memory="memory"
|
|
:max-cpu="maxCpu"
|
|
:max-memory="maxMemory"
|
|
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
|
:disabled="isConfig"
|
|
@updateCpuMemory="updateCpuMemory"
|
|
/>
|
|
|
|
<div class="mb-20">
|
|
<Checkbox
|
|
v-if="value.cpuPinningFeatureEnabled"
|
|
v-model:value="cpuPinning"
|
|
class="check"
|
|
type="checkbox"
|
|
tooltip-key="harvester.virtualMachine.cpuPinning.tooltip"
|
|
label-key="harvester.virtualMachine.cpuPinning.label"
|
|
:mode="mode"
|
|
/>
|
|
|
|
<Checkbox
|
|
v-model:value="installUSBTablet"
|
|
class="check"
|
|
type="checkbox"
|
|
:label="t('harvester.virtualMachine.enableUsb')"
|
|
:mode="mode"
|
|
/>
|
|
|
|
<Checkbox
|
|
v-model:value="tpmEnabled"
|
|
class="check"
|
|
type="checkbox"
|
|
label-key="harvester.virtualMachine.systemOptions.tpm"
|
|
:mode="mode"
|
|
/>
|
|
|
|
<Checkbox
|
|
v-if="value.tpmPersistentStateFeatureEnabled && tpmEnabled"
|
|
v-model:value="tpmPersistentStateEnabled"
|
|
class="check"
|
|
type="checkbox"
|
|
:label="t('harvester.virtualMachine.systemOptions.tpmPersistentState')"
|
|
:mode="mode"
|
|
/>
|
|
|
|
<Checkbox
|
|
v-model:value="efiEnabled"
|
|
class="check"
|
|
type="checkbox"
|
|
:label="t('harvester.virtualMachine.systemOptions.efiEnabled')"
|
|
:mode="mode"
|
|
/>
|
|
|
|
<Checkbox
|
|
v-if="value.efiPersistentStateFeatureEnabled && efiEnabled"
|
|
v-model:value="efiPersistentStateEnabled"
|
|
class="check"
|
|
type="checkbox"
|
|
:label="t('harvester.virtualMachine.systemOptions.efiPersistentState')"
|
|
:mode="mode"
|
|
/>
|
|
|
|
<Checkbox
|
|
v-if="efiEnabled"
|
|
v-model:value="secureBoot"
|
|
class="check"
|
|
type="checkbox"
|
|
:label="t('harvester.virtualMachine.systemOptions.secureBoot')"
|
|
:mode="mode"
|
|
/>
|
|
</div>
|
|
|
|
<div class="mb-20">
|
|
<SSHKey
|
|
v-model:value="sshKey"
|
|
:create-namespace="true"
|
|
:namespace="templateValue.metadata.namespace"
|
|
:disable-create="isView"
|
|
:mode="mode"
|
|
@update:sshKey="updateSSHKey"
|
|
/>
|
|
</div>
|
|
</Tab>
|
|
|
|
<Tab
|
|
name="Volume"
|
|
:label="t('harvester.tab.volume')"
|
|
:weight="-1"
|
|
>
|
|
<Volume
|
|
v-model:value="diskRows"
|
|
v-model:block-multi-queue="blockMultiQueue"
|
|
v-model:io-threads-policy="ioThreadsPolicy"
|
|
v-model:io-thread-count="ioThreadCount"
|
|
:mode="mode"
|
|
:namespace="value.metadata.namespace"
|
|
:existing-volume-disabled="true"
|
|
/>
|
|
</Tab>
|
|
|
|
<Tab
|
|
name="Network"
|
|
:label="t('harvester.tab.network')"
|
|
:weight="-2"
|
|
>
|
|
<Network
|
|
v-model:value="networkRows"
|
|
v-model:network-boot-first="networkBootFirst"
|
|
:mode="mode"
|
|
/>
|
|
</Tab>
|
|
|
|
<Tab
|
|
name="nodeScheduling"
|
|
:label="t('workload.container.titles.nodeScheduling')"
|
|
:weight="-3"
|
|
>
|
|
<template #default>
|
|
<NodeScheduling
|
|
:mode="mode"
|
|
:value="spec.template.spec"
|
|
:nodes="nodesIdOptions"
|
|
/>
|
|
</template>
|
|
</Tab>
|
|
|
|
<Tab
|
|
:label="t('harvester.tab.vmScheduling')"
|
|
name="vmScheduling"
|
|
:weight="-4"
|
|
>
|
|
<template #default>
|
|
<PodAffinity
|
|
:mode="mode"
|
|
:value="spec.template.spec"
|
|
:nodes="nodes"
|
|
:all-namespaces-option-available="true"
|
|
:namespaces="filteredNamespaces"
|
|
:overwrite-labels="affinityLabels"
|
|
/>
|
|
</template>
|
|
</Tab>
|
|
|
|
<Tab
|
|
v-if="filesystemEnabled"
|
|
name="filesystem"
|
|
:label="t('harvester.tab.filesystem')"
|
|
:weight="-8"
|
|
>
|
|
<Filesystem
|
|
v-model:value="filesystemRows"
|
|
:mode="mode"
|
|
:namespace="templateValue.metadata.namespace"
|
|
/>
|
|
</Tab>
|
|
|
|
<Tab
|
|
name="labels"
|
|
:label="t('generic.labels')"
|
|
:weight="-9"
|
|
>
|
|
<Banner color="info">
|
|
<t k="harvester.virtualMachine.labels.banner" />
|
|
</Banner>
|
|
<KeyValue
|
|
key="labels"
|
|
:value="value.labels"
|
|
:add-label="t('labels.addLabel')"
|
|
:mode="mode"
|
|
:read-allowed="false"
|
|
:value-can-be-empty="true"
|
|
@update:value="value.setLabels($event)"
|
|
/>
|
|
</Tab>
|
|
|
|
<Tab
|
|
name="instanceLabel"
|
|
:label="t('harvester.tab.instanceLabel')"
|
|
:weight="-10"
|
|
>
|
|
<Banner color="info">
|
|
<t k="harvester.virtualMachine.instanceLabels.banner" />
|
|
</Banner>
|
|
<KeyValue
|
|
key="instance-labels"
|
|
:value="value.instanceLabels"
|
|
:protected-keys="value.systemLabels || []"
|
|
:toggle-filter="toggler"
|
|
:add-label="t('labels.addLabel')"
|
|
:mode="mode"
|
|
:read-allowed="false"
|
|
:value-can-be-empty="true"
|
|
@update:value="value.setInstanceLabels($event)"
|
|
/>
|
|
</Tab>
|
|
<Tab
|
|
name="annotations"
|
|
:label="t('harvester.tab.annotations')"
|
|
:weight="-11"
|
|
>
|
|
<Banner color="info">
|
|
<t k="harvester.virtualMachine.annotations.banner" />
|
|
</Banner>
|
|
<KeyValue
|
|
key="annotations"
|
|
:value="value.annotations"
|
|
:protected-keys="value.systemAnnotations || []"
|
|
:toggle-filter="true"
|
|
:add-label="t('labels.addAnnotation')"
|
|
:mode="mode"
|
|
:read-allowed="false"
|
|
:value-can-be-empty="true"
|
|
@update:value="value.setAnnotations($event)"
|
|
/>
|
|
</Tab>
|
|
<Tab
|
|
name="advanced"
|
|
:label="t('harvester.tab.advanced')"
|
|
:weight="-99"
|
|
>
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<LabeledSelect
|
|
v-model:value="runStrategy"
|
|
label-key="harvester.virtualMachine.runStrategy"
|
|
:options="runStrategies"
|
|
:mode="mode"
|
|
/>
|
|
</div>
|
|
<div class="col span-6">
|
|
<LabeledSelect
|
|
v-model:value="maintenanceStrategy"
|
|
label-key="harvester.virtualMachine.maintenanceStrategy.label"
|
|
:options="maintenanceStrategies"
|
|
:get-option-label="getMaintenanceStrategyOptionLabel"
|
|
:mode="mode"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<Reserved
|
|
:reserved-memory="reservedMemory"
|
|
:mode="mode"
|
|
@updateReserved="updateReserved"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-20">
|
|
<a
|
|
v-if="showAdvanced"
|
|
v-t="'harvester.generic.showMore'"
|
|
role="button"
|
|
@click="toggleAdvanced"
|
|
/>
|
|
<a
|
|
v-else
|
|
v-t="'harvester.generic.showMore'"
|
|
role="button"
|
|
@click="toggleAdvanced"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="showAdvanced">
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<UnitInput
|
|
v-model:value="terminationGracePeriodSeconds"
|
|
:suffix="terminationGracePeriodSeconds == 1 ? 'Second' : 'Seconds'"
|
|
:label="t('harvester.virtualMachine.terminationGracePeriodSeconds.label')"
|
|
:mode="mode"
|
|
@change="updateTerminationGracePeriodSeconds"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<CloudConfig
|
|
ref="yamlEditor"
|
|
:mode="mode"
|
|
:user-script="userScript"
|
|
:namespace="templateValue.metadata.namespace"
|
|
:network-script="networkScript"
|
|
@updateUserData="updateUserData"
|
|
@updateNetworkData="updateNetworkData"
|
|
>
|
|
<template #after-user-data>
|
|
<Checkbox
|
|
v-model:value="installAgent"
|
|
class="check mb-20"
|
|
type="checkbox"
|
|
label-key="harvester.virtualMachine.installAgent"
|
|
:mode="mode"
|
|
/>
|
|
</template>
|
|
</CloudConfig>
|
|
</Tab>
|
|
</Tabbed>
|
|
</CruResource>
|
|
</template>
|