mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-03-21 20:51:45 +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',
|
||||
],
|
||||
'v1.7.1': [],
|
||||
'v1.8.0': []
|
||||
'v1.8.0': [
|
||||
'hotplugCdRom',
|
||||
],
|
||||
};
|
||||
|
||||
const generateFeatureFlags = () => {
|
||||
|
||||
@ -39,6 +39,12 @@ export const VOLUME_TYPE = [{
|
||||
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 = {
|
||||
RESET_PWD: 'userPassword',
|
||||
INJECT_SSH: 'sshPublicKey'
|
||||
|
||||
@ -5,6 +5,10 @@ import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
|
||||
const VOLUME = 'volume';
|
||||
const NETWORK = 'network';
|
||||
const CDROM = 'cdrom';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterHotUnplug',
|
||||
|
||||
@ -40,19 +44,37 @@ export default {
|
||||
},
|
||||
|
||||
isVolume() {
|
||||
return this.modalData.type === 'volume';
|
||||
return this.modalData.type === VOLUME;
|
||||
},
|
||||
|
||||
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() {
|
||||
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() {
|
||||
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 {
|
||||
let res;
|
||||
|
||||
if (this.isVolume) {
|
||||
if (this.modalData.type === VOLUME) {
|
||||
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 });
|
||||
} else {
|
||||
res = await this.actionResource.doAction('ejectCdRomVolume', { deviceName: this.name });
|
||||
}
|
||||
|
||||
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 { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
||||
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 { HCI } from '../../../types';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
||||
import { EMPTY_IMAGE } from '../../../utils/vm';
|
||||
|
||||
export default {
|
||||
emits: ['update:value'],
|
||||
@ -117,6 +118,10 @@ export default {
|
||||
return this.mode === _CREATE;
|
||||
},
|
||||
|
||||
isHotplugCdRomFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('hotplugCdRom');
|
||||
},
|
||||
|
||||
defaultStorageClass() {
|
||||
const defaultStorage = this.$store.getters['harvester/all'](STORAGE_CLASS).find((sc) => sc.isDefault);
|
||||
|
||||
@ -146,7 +151,7 @@ export default {
|
||||
value: {
|
||||
handler(neu) {
|
||||
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 = {
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-namespace-id`,
|
||||
params: {
|
||||
@ -217,8 +222,48 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
unplugVolume(volume) {
|
||||
this.vm.unplugVolume(volume.name);
|
||||
canDoVolumeHotplugAction(volume) {
|
||||
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) {
|
||||
@ -346,12 +391,12 @@ export default {
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
<button
|
||||
v-if="volume.hotpluggable && isView"
|
||||
v-if="canDoVolumeHotplugAction(volume) && isView"
|
||||
type="button"
|
||||
class="role-link btn btn-sm remove"
|
||||
@click="unplugVolume(volume)"
|
||||
@click="hotplugVolume(volume)"
|
||||
>
|
||||
{{ t('harvester.virtualMachine.hotUnplug.detachVolume.actionLabel') }}
|
||||
{{ t(getVolumeHotplugActionLabel(volume)) }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -14,6 +14,7 @@ import { _VIEW } from '@shell/config/query-params';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import { ucFirst } from '@shell/utils/string';
|
||||
import { GIBIBYTE } from '../../../../utils/unit';
|
||||
import { EMPTY_IMAGE } from '../../../../utils/vm';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditVMImage',
|
||||
@ -96,8 +97,20 @@ export default {
|
||||
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() {
|
||||
return this.images
|
||||
const images = this.images
|
||||
.filter((image) => {
|
||||
if (!image.isReady) return false;
|
||||
|
||||
@ -114,6 +127,19 @@ export default {
|
||||
value: image.id,
|
||||
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() {
|
||||
@ -179,6 +205,7 @@ export default {
|
||||
'value.type'(neu) {
|
||||
if (neu === 'cd-rom') {
|
||||
this.value['bus'] = 'sata';
|
||||
this.updateHotpluggable();
|
||||
this.update();
|
||||
}
|
||||
},
|
||||
@ -221,12 +248,48 @@ export default {
|
||||
|
||||
return label;
|
||||
},
|
||||
|
||||
update() {
|
||||
this.value.hasDiskError = this.showDiskTooSmallError;
|
||||
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() {
|
||||
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 isIsoImage = /iso$/i.test(imageResource?.imageSuffix);
|
||||
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
|
||||
@ -234,6 +297,7 @@ export default {
|
||||
if (isIsoImage) {
|
||||
this.value['type'] = 'cd-rom';
|
||||
this.value['bus'] = 'sata';
|
||||
this.updateHotpluggable();
|
||||
} else {
|
||||
this.value['type'] = 'disk';
|
||||
this.value['bus'] = 'virtio';
|
||||
@ -256,6 +320,10 @@ export default {
|
||||
},
|
||||
|
||||
checkImageExists(imageId) {
|
||||
if (imageId === EMPTY_IMAGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!imageId && this.imagesOption.length > 0 && !findBy(this.imagesOption, 'value', imageId)) {
|
||||
this.$store.dispatch('growl/error', {
|
||||
title: this.$store.getters['i18n/t']('harvester.vmTemplate.tips.notExistImage.title', { name: imageId }),
|
||||
@ -283,6 +351,7 @@ export default {
|
||||
>
|
||||
<LabeledInput
|
||||
v-model:value="value.name"
|
||||
:disabled="!isCreate && isExistingCdrom"
|
||||
:label="t('harvester.fields.name')"
|
||||
required
|
||||
:mode="mode"
|
||||
@ -302,10 +371,11 @@ export default {
|
||||
>
|
||||
<LabeledSelect
|
||||
v-model:value="value.type"
|
||||
:disabled="!isCreate && isExistingCdrom"
|
||||
:label="t('harvester.fields.type')"
|
||||
:options="VOLUME_TYPE"
|
||||
:mode="mode"
|
||||
@update:value="update"
|
||||
@update:value="onTypeChange"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
@ -323,7 +393,7 @@ export default {
|
||||
>
|
||||
<LabeledSelect
|
||||
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')"
|
||||
:options="imagesOption"
|
||||
:mode="mode"
|
||||
@ -351,7 +421,7 @@ export default {
|
||||
:label="t('harvester.fields.size')"
|
||||
:mode="mode"
|
||||
:required="validateRequired"
|
||||
:disabled="isResizeDisabled"
|
||||
:disabled="isResizeDisabled || isEmptyImage || (!isCreate && isExistingCdrom)"
|
||||
:suffix="GIBIBYTE"
|
||||
@update:value="update"
|
||||
/>
|
||||
@ -374,7 +444,8 @@ export default {
|
||||
:label="t('harvester.virtualMachine.volume.bus')"
|
||||
:mode="mode"
|
||||
:options="InterfaceOption"
|
||||
@update:value="update"
|
||||
:disabled="!isCreate && isExistingCdrom"
|
||||
@update:value="onBusChange"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
|
||||
@ -164,6 +164,11 @@ harvester:
|
||||
vmNetwork: Virtual Machine Network
|
||||
macAddress: MAC Address
|
||||
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:
|
||||
success: 'CPU and Memory are updated to the virtual machine { vm }.'
|
||||
title: Edit CPU and Memory
|
||||
@ -190,7 +195,7 @@ harvester:
|
||||
title: Restart Virtual Machine
|
||||
tip: Restart the virtual machine for configuration changes to take effect.
|
||||
cancel: Save
|
||||
|
||||
|
||||
notification:
|
||||
title:
|
||||
succeed: Succeed
|
||||
@ -631,6 +636,10 @@ harvester:
|
||||
title: 'Are you sure that you want to detach volume {name}?'
|
||||
actionLabel: Detach Volume
|
||||
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:
|
||||
title: 'Are you sure that you want to detach network interface {name}?'
|
||||
actionLabel: Detach Network Interface
|
||||
@ -737,6 +746,7 @@ harvester:
|
||||
unmount:
|
||||
title: Are you sure?
|
||||
message: Are you sure you want to unmount this volume?
|
||||
emptyImage: No media
|
||||
network:
|
||||
title: Network
|
||||
addNetwork: Add Network
|
||||
@ -1008,7 +1018,7 @@ harvester:
|
||||
createTitle: Create Schedule
|
||||
createButtonText: Create Schedule
|
||||
scheduleType: Virtual Machine Schedule Type
|
||||
cron:
|
||||
cron:
|
||||
label: Cron Schedule
|
||||
editButton: Edit
|
||||
detail:
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
} from '../../config/harvester-map';
|
||||
import { HCI_SETTING } from '../../config/settings';
|
||||
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 { GIBIBYTE } from '../../utils/unit';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
@ -511,12 +511,15 @@ export default {
|
||||
|
||||
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;
|
||||
container = volume.containerDisk.image;
|
||||
}
|
||||
|
||||
if (volume.persistentVolumeClaim && volume.persistentVolumeClaim?.claimName) {
|
||||
} else if (volume.persistentVolumeClaim && volume.persistentVolumeClaim?.claimName) {
|
||||
volumeName = volume.persistentVolumeClaim.claimName;
|
||||
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) {
|
||||
const disks = [];
|
||||
const volumes = [];
|
||||
@ -711,18 +722,18 @@ export default {
|
||||
const volumeClaimTemplates = [];
|
||||
|
||||
disk.forEach( (R, index) => {
|
||||
const prefixName = this.value.metadata?.name || '';
|
||||
const dataVolumeName = this.parseDataVolumeName(R, prefixName);
|
||||
|
||||
const _disk = this.parseDisk(R, index);
|
||||
const _volume = this.parseVolume(R, dataVolumeName);
|
||||
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
|
||||
|
||||
disks.push(_disk);
|
||||
volumes.push(_volume);
|
||||
diskNameLabels.push(dataVolumeName);
|
||||
|
||||
if (R.source !== SOURCE_TYPE.CONTAINER) {
|
||||
if (this.needVolume(R)) {
|
||||
const prefixName = this.value.metadata?.name || '';
|
||||
const dataVolumeName = this.parseDataVolumeName(R, prefixName);
|
||||
const _volume = this.parseVolume(R, dataVolumeName);
|
||||
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
|
||||
|
||||
volumes.push(_volume);
|
||||
diskNameLabels.push(dataVolumeName);
|
||||
volumeClaimTemplates.push(_dataVolumeTemplate);
|
||||
}
|
||||
});
|
||||
|
||||
@ -183,7 +183,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
},
|
||||
{
|
||||
action: 'ejectCDROM',
|
||||
enabled: !!this.actions?.ejectCdRom,
|
||||
enabled: !this.hotplugCdRomEnabled && !!this.actions?.ejectCdRom,
|
||||
icon: 'icon icon-delete',
|
||||
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) {
|
||||
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) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
@ -1267,6 +1288,10 @@ export default class VirtVm extends HarvesterResource {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('hotplugNic');
|
||||
}
|
||||
|
||||
get hotplugCdRomEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('hotplugCdRom');
|
||||
}
|
||||
|
||||
get isBackupTargetUnavailable() {
|
||||
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
|
||||
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
|
||||
|
||||
@ -9,3 +9,5 @@ export function parseVolumeClaimTemplates(data) {
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export const EMPTY_IMAGE = 'EMPTY_IMAGE';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user