mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-03-22 13:11:47 +00:00
feat: add cdrom hotplug volume (#703)
Signed-off-by: Tim Liou <tim.liou@suse.com>
This commit is contained in:
parent
c8a613874a
commit
3fdc9f03a3
@ -58,7 +58,9 @@ const FEATURE_FLAGS = {
|
|||||||
'resumeUpgradePausedNode',
|
'resumeUpgradePausedNode',
|
||||||
],
|
],
|
||||||
'v1.7.1': [],
|
'v1.7.1': [],
|
||||||
'v1.8.0': []
|
'v1.8.0': [
|
||||||
|
'hotplugCdRom',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateFeatureFlags = () => {
|
const generateFeatureFlags = () => {
|
||||||
|
|||||||
@ -39,6 +39,12 @@ export const VOLUME_TYPE = [{
|
|||||||
value: 'cd-rom'
|
value: 'cd-rom'
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
export const VOLUME_HOTPLUG_ACTION = {
|
||||||
|
INSERT_CDROM_IMAGE: 'INSERT_CDROM_IMAGE',
|
||||||
|
EJECT_CDROM_IMAGE: 'EJECT_CDROM_IMAGE',
|
||||||
|
DETACH_DISK: 'DETACH_DISK'
|
||||||
|
};
|
||||||
|
|
||||||
export const ACCESS_CREDENTIALS = {
|
export const ACCESS_CREDENTIALS = {
|
||||||
RESET_PWD: 'userPassword',
|
RESET_PWD: 'userPassword',
|
||||||
INJECT_SSH: 'sshPublicKey'
|
INJECT_SSH: 'sshPublicKey'
|
||||||
|
|||||||
@ -5,6 +5,10 @@ import { Card } from '@components/Card';
|
|||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
|
||||||
|
const VOLUME = 'volume';
|
||||||
|
const NETWORK = 'network';
|
||||||
|
const CDROM = 'cdrom';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterHotUnplug',
|
name: 'HarvesterHotUnplug',
|
||||||
|
|
||||||
@ -40,19 +44,37 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isVolume() {
|
isVolume() {
|
||||||
return this.modalData.type === 'volume';
|
return this.modalData.type === VOLUME;
|
||||||
},
|
},
|
||||||
|
|
||||||
titleKey() {
|
titleKey() {
|
||||||
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.title' : 'harvester.virtualMachine.hotUnplug.detachNIC.title';
|
const keys = {
|
||||||
|
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.title',
|
||||||
|
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.title',
|
||||||
|
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.title',
|
||||||
|
};
|
||||||
|
|
||||||
|
return keys[this.modalData.type];
|
||||||
},
|
},
|
||||||
|
|
||||||
actionLabelKey() {
|
actionLabelKey() {
|
||||||
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel' : 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabel';
|
const keys = {
|
||||||
|
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabels',
|
||||||
|
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.actionLabels',
|
||||||
|
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabels',
|
||||||
|
};
|
||||||
|
|
||||||
|
return keys[this.modalData.type];
|
||||||
},
|
},
|
||||||
|
|
||||||
successMessageKey() {
|
successMessageKey() {
|
||||||
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.success' : 'harvester.virtualMachine.hotUnplug.detachNIC.success';
|
const keys = {
|
||||||
|
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.success',
|
||||||
|
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.success',
|
||||||
|
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.success',
|
||||||
|
};
|
||||||
|
|
||||||
|
return keys[this.modalData.type];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -65,10 +87,12 @@ export default {
|
|||||||
try {
|
try {
|
||||||
let res;
|
let res;
|
||||||
|
|
||||||
if (this.isVolume) {
|
if (this.modalData.type === VOLUME) {
|
||||||
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
|
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
|
||||||
} else {
|
} else if (this.modalData.type === NETWORK) {
|
||||||
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
|
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
|
||||||
|
} else {
|
||||||
|
res = await this.actionResource.doAction('ejectCdRomVolume', { deviceName: this.name });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res._status === 200 || res._status === 204) {
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
|||||||
198
pkg/harvester/dialog/HarvesterInsertCdRomVolume.vue
Normal file
198
pkg/harvester/dialog/HarvesterInsertCdRomVolume.vue
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
<script>
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { mapState, mapGetters } from 'vuex';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterInsertCdRomVolume',
|
||||||
|
|
||||||
|
emits: ['close'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Card,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
try {
|
||||||
|
this.images = await this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = exceptionToErrorsArray(err);
|
||||||
|
this.images = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
imageName: '',
|
||||||
|
images: [],
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState('action-menu', ['modalData']),
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources?.[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
isFormValid() {
|
||||||
|
return this.imageName !== '';
|
||||||
|
},
|
||||||
|
|
||||||
|
deviceName() {
|
||||||
|
return this.modalData.name;
|
||||||
|
},
|
||||||
|
|
||||||
|
imagesOption() {
|
||||||
|
return this.images
|
||||||
|
.filter((image) => {
|
||||||
|
const labels = image.metadata?.labels || {};
|
||||||
|
const type = labels[HCI_ANNOTATIONS.IMAGE_SUFFIX];
|
||||||
|
|
||||||
|
return type === 'iso';
|
||||||
|
})
|
||||||
|
.map((image) => {
|
||||||
|
return ({
|
||||||
|
label: this.imageOptionLabel(image),
|
||||||
|
value: image.id,
|
||||||
|
disabled: image.isImportedImage
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.imageName = '';
|
||||||
|
this.errors = [];
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
imageOptionLabel(image) {
|
||||||
|
return `${ image.metadata.namespace }/${ image.spec.displayName }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
if (!this.actionResource) {
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
deviceName: this.deviceName,
|
||||||
|
imageName: this.imageName
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction('insertCdRomVolume', payload);
|
||||||
|
|
||||||
|
if ([200, 204].includes(res?._status)) {
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('generic.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.insertCdRomVolume.success', {
|
||||||
|
deviceName: this.deviceName,
|
||||||
|
imageName: this.imageName,
|
||||||
|
})
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
this.errors = exceptionToErrorsArray(res);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = exceptionToErrorsArray(err);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card
|
||||||
|
ref="modal"
|
||||||
|
name="modal"
|
||||||
|
:show-highlight-border="false"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<h4
|
||||||
|
v-clean-html="t('harvester.modal.insertCdRomVolume.title')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="deviceName"
|
||||||
|
:label="t('generic.name')"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="imageName"
|
||||||
|
class="mt-20"
|
||||||
|
:label="t('harvester.modal.insertCdRomVolume.image')"
|
||||||
|
:options="imagesOption"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
:label="err"
|
||||||
|
color="error"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-secondary mr-10"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="apply"
|
||||||
|
:disabled="!isFormValid"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -13,11 +13,12 @@ import { ucFirst, randomStr } from '@shell/utils/string';
|
|||||||
import { removeObject } from '@shell/utils/array';
|
import { removeObject } from '@shell/utils/array';
|
||||||
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
||||||
import { SOURCE_TYPE } from '../../../config/harvester-map';
|
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||||
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
||||||
|
import { EMPTY_IMAGE } from '../../../utils/vm';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
@ -117,6 +118,10 @@ export default {
|
|||||||
return this.mode === _CREATE;
|
return this.mode === _CREATE;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isHotplugCdRomFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('hotplugCdRom');
|
||||||
|
},
|
||||||
|
|
||||||
defaultStorageClass() {
|
defaultStorageClass() {
|
||||||
const defaultStorage = this.$store.getters['harvester/all'](STORAGE_CLASS).find((sc) => sc.isDefault);
|
const defaultStorage = this.$store.getters['harvester/all'](STORAGE_CLASS).find((sc) => sc.isDefault);
|
||||||
|
|
||||||
@ -146,7 +151,7 @@ export default {
|
|||||||
value: {
|
value: {
|
||||||
handler(neu) {
|
handler(neu) {
|
||||||
const rows = clone(neu).map((V) => {
|
const rows = clone(neu).map((V) => {
|
||||||
if (!this.isCreate && V.source !== SOURCE_TYPE.CONTAINER && !V.newCreateId) {
|
if (!this.isCreate && V.source !== SOURCE_TYPE.CONTAINER && !V.newCreateId && V.image !== EMPTY_IMAGE) {
|
||||||
V.to = {
|
V.to = {
|
||||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-namespace-id`,
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-namespace-id`,
|
||||||
params: {
|
params: {
|
||||||
@ -217,8 +222,48 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
unplugVolume(volume) {
|
canDoVolumeHotplugAction(volume) {
|
||||||
this.vm.unplugVolume(volume.name);
|
if (!this.isHotplugCdRomFeatureEnabled && volume.type === 'cd-rom') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (volume.hotpluggable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return volume.type === 'cd-rom' && volume.bus === 'sata' && volume.image === EMPTY_IMAGE;
|
||||||
|
},
|
||||||
|
|
||||||
|
getVolumeHotplugAction(volume) {
|
||||||
|
if (volume.type === 'cd-rom' && volume.bus === 'sata') {
|
||||||
|
if (volume.image === EMPTY_IMAGE) {
|
||||||
|
return VOLUME_HOTPLUG_ACTION.INSERT_CDROM_IMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return VOLUME_HOTPLUG_ACTION.EJECT_CDROM_IMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return VOLUME_HOTPLUG_ACTION.DETACH_DISK;
|
||||||
|
},
|
||||||
|
|
||||||
|
getVolumeHotplugActionLabel(volume) {
|
||||||
|
const labels = {
|
||||||
|
[VOLUME_HOTPLUG_ACTION.DETACH_DISK]: 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel',
|
||||||
|
[VOLUME_HOTPLUG_ACTION.INSERT_CDROM_IMAGE]: 'harvester.modal.insertCdRomVolume.actionLabel',
|
||||||
|
[VOLUME_HOTPLUG_ACTION.EJECT_CDROM_IMAGE]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.actionLabel',
|
||||||
|
};
|
||||||
|
|
||||||
|
return labels[this.getVolumeHotplugAction(volume)];
|
||||||
|
},
|
||||||
|
|
||||||
|
hotplugVolume(volume) {
|
||||||
|
const calls = {
|
||||||
|
[VOLUME_HOTPLUG_ACTION.DETACH_DISK]: () => this.vm.unplugVolume(volume.name),
|
||||||
|
[VOLUME_HOTPLUG_ACTION.INSERT_CDROM_IMAGE]: () => this.vm.insertCdRomVolume(volume.name),
|
||||||
|
[VOLUME_HOTPLUG_ACTION.EJECT_CDROM_IMAGE]: () => this.vm.ejectCdRomVolume(volume.name),
|
||||||
|
};
|
||||||
|
|
||||||
|
return calls[this.getVolumeHotplugAction(volume)]();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentFor(type) {
|
componentFor(type) {
|
||||||
@ -346,12 +391,12 @@ export default {
|
|||||||
<i class="icon icon-x" />
|
<i class="icon icon-x" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="volume.hotpluggable && isView"
|
v-if="canDoVolumeHotplugAction(volume) && isView"
|
||||||
type="button"
|
type="button"
|
||||||
class="role-link btn btn-sm remove"
|
class="role-link btn btn-sm remove"
|
||||||
@click="unplugVolume(volume)"
|
@click="hotplugVolume(volume)"
|
||||||
>
|
>
|
||||||
{{ t('harvester.virtualMachine.hotUnplug.detachVolume.actionLabel') }}
|
{{ t(getVolumeHotplugActionLabel(volume)) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { _VIEW } from '@shell/config/query-params';
|
|||||||
import LabelValue from '@shell/components/LabelValue';
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
import { ucFirst } from '@shell/utils/string';
|
import { ucFirst } from '@shell/utils/string';
|
||||||
import { GIBIBYTE } from '../../../../utils/unit';
|
import { GIBIBYTE } from '../../../../utils/unit';
|
||||||
|
import { EMPTY_IMAGE } from '../../../../utils/vm';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditVMImage',
|
name: 'HarvesterEditVMImage',
|
||||||
@ -96,8 +97,20 @@ export default {
|
|||||||
return this.mode === _VIEW;
|
return this.mode === _VIEW;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isExistingCdrom() {
|
||||||
|
return this.value.type === 'cd-rom' && !this.value.newCreateId;
|
||||||
|
},
|
||||||
|
|
||||||
|
isEmptyImage() {
|
||||||
|
return this.value.image === EMPTY_IMAGE;
|
||||||
|
},
|
||||||
|
|
||||||
|
isHotplugCdRomFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('hotplugCdRom');
|
||||||
|
},
|
||||||
|
|
||||||
imagesOption() {
|
imagesOption() {
|
||||||
return this.images
|
const images = this.images
|
||||||
.filter((image) => {
|
.filter((image) => {
|
||||||
if (!image.isReady) return false;
|
if (!image.isReady) return false;
|
||||||
|
|
||||||
@ -114,6 +127,19 @@ export default {
|
|||||||
value: image.id,
|
value: image.id,
|
||||||
disabled: image.isImportedImage
|
disabled: image.isImportedImage
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const options = [];
|
||||||
|
|
||||||
|
if (this.isHotplugCdRomFeatureEnabled) {
|
||||||
|
options.push({
|
||||||
|
label: this.t('harvester.virtualMachine.volume.emptyImage'),
|
||||||
|
value: EMPTY_IMAGE,
|
||||||
|
disabled: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
options.push(...images);
|
||||||
|
|
||||||
|
return options;
|
||||||
},
|
},
|
||||||
|
|
||||||
imageName() {
|
imageName() {
|
||||||
@ -179,6 +205,7 @@ export default {
|
|||||||
'value.type'(neu) {
|
'value.type'(neu) {
|
||||||
if (neu === 'cd-rom') {
|
if (neu === 'cd-rom') {
|
||||||
this.value['bus'] = 'sata';
|
this.value['bus'] = 'sata';
|
||||||
|
this.updateHotpluggable();
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -221,12 +248,48 @@ export default {
|
|||||||
|
|
||||||
return label;
|
return label;
|
||||||
},
|
},
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
this.value.hasDiskError = this.showDiskTooSmallError;
|
this.value.hasDiskError = this.showDiskTooSmallError;
|
||||||
this.$emit('update');
|
this.$emit('update');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateHotpluggable() {
|
||||||
|
if (this.value.type !== 'cd-rom') {
|
||||||
|
this.value['hotpluggable'] = false;
|
||||||
|
} else {
|
||||||
|
this.value['hotpluggable'] = (this.value.bus === 'sata');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onTypeChange() {
|
||||||
|
if (this.value.image === EMPTY_IMAGE && this.value.type !== 'cd-rom') {
|
||||||
|
this.value['image'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateHotpluggable();
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
onBusChange() {
|
||||||
|
if (this.value.image === EMPTY_IMAGE && this.value.bus !== 'sata') {
|
||||||
|
this.value['image'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateHotpluggable();
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
onImageChange() {
|
onImageChange() {
|
||||||
|
if (this.value.image === EMPTY_IMAGE) {
|
||||||
|
this.value['type'] = 'cd-rom';
|
||||||
|
this.value['bus'] = 'sata';
|
||||||
|
this.value['size'] = `0${ GIBIBYTE }`;
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const imageResource = this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
const imageResource = this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
||||||
const isIsoImage = /iso$/i.test(imageResource?.imageSuffix);
|
const isIsoImage = /iso$/i.test(imageResource?.imageSuffix);
|
||||||
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
|
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
|
||||||
@ -234,6 +297,7 @@ export default {
|
|||||||
if (isIsoImage) {
|
if (isIsoImage) {
|
||||||
this.value['type'] = 'cd-rom';
|
this.value['type'] = 'cd-rom';
|
||||||
this.value['bus'] = 'sata';
|
this.value['bus'] = 'sata';
|
||||||
|
this.updateHotpluggable();
|
||||||
} else {
|
} else {
|
||||||
this.value['type'] = 'disk';
|
this.value['type'] = 'disk';
|
||||||
this.value['bus'] = 'virtio';
|
this.value['bus'] = 'virtio';
|
||||||
@ -256,6 +320,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
checkImageExists(imageId) {
|
checkImageExists(imageId) {
|
||||||
|
if (imageId === EMPTY_IMAGE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!!imageId && this.imagesOption.length > 0 && !findBy(this.imagesOption, 'value', imageId)) {
|
if (!!imageId && this.imagesOption.length > 0 && !findBy(this.imagesOption, 'value', imageId)) {
|
||||||
this.$store.dispatch('growl/error', {
|
this.$store.dispatch('growl/error', {
|
||||||
title: this.$store.getters['i18n/t']('harvester.vmTemplate.tips.notExistImage.title', { name: imageId }),
|
title: this.$store.getters['i18n/t']('harvester.vmTemplate.tips.notExistImage.title', { name: imageId }),
|
||||||
@ -283,6 +351,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model:value="value.name"
|
v-model:value="value.name"
|
||||||
|
:disabled="!isCreate && isExistingCdrom"
|
||||||
:label="t('harvester.fields.name')"
|
:label="t('harvester.fields.name')"
|
||||||
required
|
required
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
@ -302,10 +371,11 @@ export default {
|
|||||||
>
|
>
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="value.type"
|
v-model:value="value.type"
|
||||||
|
:disabled="!isCreate && isExistingCdrom"
|
||||||
:label="t('harvester.fields.type')"
|
:label="t('harvester.fields.type')"
|
||||||
:options="VOLUME_TYPE"
|
:options="VOLUME_TYPE"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
@update:value="update"
|
@update:value="onTypeChange"
|
||||||
/>
|
/>
|
||||||
</InputOrDisplay>
|
</InputOrDisplay>
|
||||||
</div>
|
</div>
|
||||||
@ -323,7 +393,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="value.image"
|
v-model:value="value.image"
|
||||||
:disabled="idx === 0 && !isCreate && !value.newCreateId && isVirtualType"
|
:disabled="(idx === 0 || isExistingCdrom) && (!isCreate && !value.newCreateId && isVirtualType)"
|
||||||
:label="t('harvester.fields.image')"
|
:label="t('harvester.fields.image')"
|
||||||
:options="imagesOption"
|
:options="imagesOption"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
@ -351,7 +421,7 @@ export default {
|
|||||||
:label="t('harvester.fields.size')"
|
:label="t('harvester.fields.size')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:required="validateRequired"
|
:required="validateRequired"
|
||||||
:disabled="isResizeDisabled"
|
:disabled="isResizeDisabled || isEmptyImage || (!isCreate && isExistingCdrom)"
|
||||||
:suffix="GIBIBYTE"
|
:suffix="GIBIBYTE"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
@ -374,7 +444,8 @@ export default {
|
|||||||
:label="t('harvester.virtualMachine.volume.bus')"
|
:label="t('harvester.virtualMachine.volume.bus')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:options="InterfaceOption"
|
:options="InterfaceOption"
|
||||||
@update:value="update"
|
:disabled="!isCreate && isExistingCdrom"
|
||||||
|
@update:value="onBusChange"
|
||||||
/>
|
/>
|
||||||
</InputOrDisplay>
|
</InputOrDisplay>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -164,6 +164,11 @@ harvester:
|
|||||||
vmNetwork: Virtual Machine Network
|
vmNetwork: Virtual Machine Network
|
||||||
macAddress: MAC Address
|
macAddress: MAC Address
|
||||||
macAddressTooltip: If left blank, the MAC address will be automatically generated.
|
macAddressTooltip: If left blank, the MAC address will be automatically generated.
|
||||||
|
insertCdRomVolume:
|
||||||
|
success: '{ imageName } is inserted into device { deviceName }.'
|
||||||
|
title: Insert Image
|
||||||
|
image: Image
|
||||||
|
actionLabel: Insert Image
|
||||||
cpuMemoryHotplug:
|
cpuMemoryHotplug:
|
||||||
success: 'CPU and Memory are updated to the virtual machine { vm }.'
|
success: 'CPU and Memory are updated to the virtual machine { vm }.'
|
||||||
title: Edit CPU and Memory
|
title: Edit CPU and Memory
|
||||||
@ -631,6 +636,10 @@ harvester:
|
|||||||
title: 'Are you sure that you want to detach volume {name}?'
|
title: 'Are you sure that you want to detach volume {name}?'
|
||||||
actionLabel: Detach Volume
|
actionLabel: Detach Volume
|
||||||
success: 'Volume { name } is detached successfully.'
|
success: 'Volume { name } is detached successfully.'
|
||||||
|
ejectCdRomVolume:
|
||||||
|
title: 'Are you sure that you want to eject image from device {name}?'
|
||||||
|
actionLabel: Eject Image
|
||||||
|
success: 'Image from device { name } is ejected successfully.'
|
||||||
detachNIC:
|
detachNIC:
|
||||||
title: 'Are you sure that you want to detach network interface {name}?'
|
title: 'Are you sure that you want to detach network interface {name}?'
|
||||||
actionLabel: Detach Network Interface
|
actionLabel: Detach Network Interface
|
||||||
@ -737,6 +746,7 @@ harvester:
|
|||||||
unmount:
|
unmount:
|
||||||
title: Are you sure?
|
title: Are you sure?
|
||||||
message: Are you sure you want to unmount this volume?
|
message: Are you sure you want to unmount this volume?
|
||||||
|
emptyImage: No media
|
||||||
network:
|
network:
|
||||||
title: Network
|
title: Network
|
||||||
addNetwork: Add Network
|
addNetwork: Add Network
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
} from '../../config/harvester-map';
|
} from '../../config/harvester-map';
|
||||||
import { HCI_SETTING } from '../../config/settings';
|
import { HCI_SETTING } from '../../config/settings';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
import { parseVolumeClaimTemplates } from '../../utils/vm';
|
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
|
||||||
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
||||||
import { GIBIBYTE } from '../../utils/unit';
|
import { GIBIBYTE } from '../../utils/unit';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||||
@ -511,12 +511,15 @@ export default {
|
|||||||
|
|
||||||
const type = DISK?.cdrom ? CD_ROM : DISK?.disk ? HARD_DISK : '';
|
const type = DISK?.cdrom ? CD_ROM : DISK?.disk ? HARD_DISK : '';
|
||||||
|
|
||||||
if (volume?.containerDisk) { // SOURCE_TYPE.CONTAINER
|
if (type === CD_ROM && volume === undefined) {
|
||||||
|
// Empty CD_ROM
|
||||||
|
source = SOURCE_TYPE.IMAGE;
|
||||||
|
image = EMPTY_IMAGE;
|
||||||
|
size = `0${ GIBIBYTE }`;
|
||||||
|
} else if (volume.containerDisk) { // SOURCE_TYPE.CONTAINER
|
||||||
source = SOURCE_TYPE.CONTAINER;
|
source = SOURCE_TYPE.CONTAINER;
|
||||||
container = volume.containerDisk.image;
|
container = volume.containerDisk.image;
|
||||||
}
|
} else if (volume.persistentVolumeClaim && volume.persistentVolumeClaim?.claimName) {
|
||||||
|
|
||||||
if (volume.persistentVolumeClaim && volume.persistentVolumeClaim?.claimName) {
|
|
||||||
volumeName = volume.persistentVolumeClaim.claimName;
|
volumeName = volume.persistentVolumeClaim.claimName;
|
||||||
const DVT = _volumeClaimTemplates.find( (T) => T.metadata.name === volumeName);
|
const DVT = _volumeClaimTemplates.find( (T) => T.metadata.name === volumeName);
|
||||||
|
|
||||||
@ -704,6 +707,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
needVolume(R) {
|
||||||
|
if (R.image === EMPTY_IMAGE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
parseDiskRows(disk) {
|
parseDiskRows(disk) {
|
||||||
const disks = [];
|
const disks = [];
|
||||||
const volumes = [];
|
const volumes = [];
|
||||||
@ -711,18 +722,18 @@ export default {
|
|||||||
const volumeClaimTemplates = [];
|
const volumeClaimTemplates = [];
|
||||||
|
|
||||||
disk.forEach( (R, index) => {
|
disk.forEach( (R, index) => {
|
||||||
|
const _disk = this.parseDisk(R, index);
|
||||||
|
|
||||||
|
disks.push(_disk);
|
||||||
|
|
||||||
|
if (this.needVolume(R)) {
|
||||||
const prefixName = this.value.metadata?.name || '';
|
const prefixName = this.value.metadata?.name || '';
|
||||||
const dataVolumeName = this.parseDataVolumeName(R, prefixName);
|
const dataVolumeName = this.parseDataVolumeName(R, prefixName);
|
||||||
|
|
||||||
const _disk = this.parseDisk(R, index);
|
|
||||||
const _volume = this.parseVolume(R, dataVolumeName);
|
const _volume = this.parseVolume(R, dataVolumeName);
|
||||||
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
|
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
|
||||||
|
|
||||||
disks.push(_disk);
|
|
||||||
volumes.push(_volume);
|
volumes.push(_volume);
|
||||||
diskNameLabels.push(dataVolumeName);
|
diskNameLabels.push(dataVolumeName);
|
||||||
|
|
||||||
if (R.source !== SOURCE_TYPE.CONTAINER) {
|
|
||||||
volumeClaimTemplates.push(_dataVolumeTemplate);
|
volumeClaimTemplates.push(_dataVolumeTemplate);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -183,7 +183,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'ejectCDROM',
|
action: 'ejectCDROM',
|
||||||
enabled: !!this.actions?.ejectCdRom,
|
enabled: !this.hotplugCdRomEnabled && !!this.actions?.ejectCdRom,
|
||||||
icon: 'icon icon-delete',
|
icon: 'icon icon-delete',
|
||||||
label: this.t('harvester.action.ejectCDROM')
|
label: this.t('harvester.action.ejectCDROM')
|
||||||
},
|
},
|
||||||
@ -401,6 +401,17 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ejectCdRomVolume(diskName) {
|
||||||
|
const resources = this;
|
||||||
|
|
||||||
|
this.$dispatch('promptModal', {
|
||||||
|
resources,
|
||||||
|
name: diskName,
|
||||||
|
type: 'cdrom',
|
||||||
|
component: 'HarvesterHotUnplug',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
unplugNIC(networkName) {
|
unplugNIC(networkName) {
|
||||||
const resources = this;
|
const resources = this;
|
||||||
|
|
||||||
@ -523,6 +534,16 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
insertCdRomVolume(diskName) {
|
||||||
|
const resources = this;
|
||||||
|
|
||||||
|
this.$dispatch('promptModal', {
|
||||||
|
resources,
|
||||||
|
name: diskName,
|
||||||
|
component: 'HarvesterInsertCdRomVolume',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
addHotplugNic(resources = this) {
|
addHotplugNic(resources = this) {
|
||||||
this.$dispatch('promptModal', {
|
this.$dispatch('promptModal', {
|
||||||
resources,
|
resources,
|
||||||
@ -1267,6 +1288,10 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('hotplugNic');
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('hotplugNic');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hotplugCdRomEnabled() {
|
||||||
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('hotplugCdRom');
|
||||||
|
}
|
||||||
|
|
||||||
get isBackupTargetUnavailable() {
|
get isBackupTargetUnavailable() {
|
||||||
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
|
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
|
||||||
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
|
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
|
||||||
|
|||||||
@ -9,3 +9,5 @@ export function parseVolumeClaimTemplates(data) {
|
|||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EMPTY_IMAGE = 'EMPTY_IMAGE';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user