Merge pull request #182 from a110605/add_image_downloader

Add HarvesterImageDownloader for cdi image
This commit is contained in:
Andy Lee 2025-03-06 14:49:24 +08:00 committed by GitHub
commit a861450874
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 334 additions and 61 deletions

View File

@ -0,0 +1,166 @@
<script>
import { mapGetters } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI } from '../types';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import AppModal from '@shell/components/AppModal';
export default {
name: 'HarvesterImageDownloaderDialog',
emits: ['close'],
components: {
AsyncButton, Banner, Card, AppModal
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
return { errors: [], isOpen: false };
},
computed: {
...mapGetters({ t: 'i18n/t' }),
downloadImageInProgress() {
return this.$store.getters['harvester-common/isDownloadImageInProgress'];
},
image() {
return this.resources[0] || {};
},
imageName() {
return this.image?.name || '';
},
imageVirtualSize() {
return this.image?.virtualSize || this.image?.downSize || '';
}
},
methods: {
async cancelDownload() {
const url = this.image?.links?.downloadcancel;
if (url) {
await this.$store.dispatch('harvester/request', { url });
}
},
async close() {
if (this.downloadImageInProgress) {
this.$store.commit('harvester-common/setDownloadImageCancel', true);
this.$store.commit('harvester-common/setDownloadImageInProgress', false);
await this.cancelDownload();
}
this.$emit('close');
},
async startDownload(buttonCb) {
// clean the download image CRD first.
await this.cancelDownload();
this.$store.commit('harvester-common/setDownloadImageCancel', false);
this.$store.commit('harvester-common/setDownloadImageInProgress', false);
this.errors = [];
const name = this.image?.name || '';
const namespace = this.image?.namespace || '';
const imageCrd = {
apiVersion: 'harvesterhci.io/v1beta1',
type: HCI.VM_IMAGE_DOWNLOADER,
kind: 'VirtualMachineImageDownloader',
metadata: {
name,
namespace
},
spec: { imageName: name }
};
const inStore = this.$store.getters['currentProduct'].inStore;
const imageCreate = await this.$store.dispatch(`${ inStore }/create`, imageCrd);
try {
await imageCreate.save();
this.$store.commit('harvester-common/setDownloadImageId', `${ namespace }/${ name }`, { root: true });
this.$store.dispatch('harvester-common/downloadImageProgress', { root: true });
} catch (err) {
this.errors = exceptionToErrorsArray(err);
buttonCb(false);
}
}
},
};
</script>
<template>
<app-modal
class="image-downloader-modal"
name="image-download-dialog"
height="auto"
:width="600"
:click-to-close="false"
@close="close"
>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.downloadImage.title') }}
</template>
<template #body>
<Banner color="info">
{{ t('harvester.modal.downloadImage.banner', { size: imageVirtualSize }) }}
</Banner>
{{ t('harvester.modal.downloadImage.startMessage') }}
<br /><br />
</template>
<template #actions>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
>
{{ err }}
</Banner>
<div class="actions">
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
type="submit"
mode="download"
:disabled="downloadImageInProgress"
@click="startDownload"
/>
</div>
</div>
</template>
</Card>
</app-modal>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>

View File

@ -99,6 +99,11 @@ harvester:
tip: Please enter a virtual machine name!
success: 'Virtual machine { name } cloned successfully.'
failed: 'Failed clone virtual machine!'
downloadImage:
title: Download Image
banner: 'This action takes a while depending on the image size ({ size }). Please be patient.'
startMessage : 'The download process will auto start once the conversion is complete.'
download: Download
exportImage:
title: Export to Image
name: Name

View File

@ -73,7 +73,7 @@ export default class HciVmImage extends HarvesterResource {
disabled: !this.isReady,
},
{
action: 'download',
action: 'imageDownload',
enabled: this.links?.download,
icon: 'icon icon-download',
label: this.t('asyncButton.download.action'),
@ -394,7 +394,19 @@ export default class HciVmImage extends HarvesterResource {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
}
download() {
imageDownload(resources = this) {
// spec.backend is introduced in v1.5.0. If it's not set, it's an old image can be downloaded via link
if (this.spec?.backend === 'cdi') {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterImageDownloader'
});
} else {
this.downloadViaLink();
}
}
downloadViaLink() {
window.location.href = this.links.download;
}
}

View File

@ -5,16 +5,33 @@ import { featureEnabled, getVersion } from '../utils/feature-flags';
const state = function() {
return {
latestBundleId: '',
bundlePending: false,
showBundleModal: false,
bundlePercentage: 0,
uploadingImages: [],
uploadingImageError: {},
// support bundle
latestBundleId: '',
bundlePending: false,
showBundleModal: false,
bundlePercentage: 0,
uploadingImages: [],
uploadingImageError: {},
// download cdi image
downloadImageId: '',
downloadImageInProgress: false,
isDownloadImageCancel: false,
};
};
const mutations = {
setDownloadImageId(state, id) {
state.downloadImageId = id;
},
setDownloadImageCancel(state, value) {
state.isDownloadImageCancel = value;
},
setDownloadImageInProgress(state, value) {
state.downloadImageInProgress = value;
},
setLatestBundleId(state, bundleId) {
state.latestBundleId = bundleId;
},
@ -51,6 +68,14 @@ const getters = {
return state.latestBundleId;
},
isDownloadImageCancel(state) {
return state.isDownloadImageCancel;
},
isDownloadImageInProgress(state) {
return state.downloadImageInProgress;
},
isBundlePending(state) {
return state.bundlePending;
},
@ -98,6 +123,70 @@ const getters = {
};
const actions = {
async downloadImageProgress({
state, dispatch, commit, rootGetters
}) {
const parse = Parse(window.history.href);
const id = state.downloadImageId; // id is image_ns / image_name
let imageCrd = await dispatch(
'harvester/find',
{ type: HCI.VM_IMAGE_DOWNLOADER, id },
{ root: true }
);
await commit('setDownloadImageInProgress', true);
let count = 0;
const timer = setInterval(async() => {
count = count + 1;
if (count % 3 === 0) {
// ws maybe disconnect, force to get the latest status
imageCrd = await dispatch(
'harvester/find',
{
type: HCI.VM_IMAGE_DOWNLOADER,
id,
opt: { force: true }
},
{ root: true }
);
}
// If is cancel, clear the timer
if (state.isDownloadImageCancel === true) {
clearInterval(timer);
return;
}
// converting image status becomes ready
if (imageCrd?.status?.status === 'Ready') {
imageCrd = rootGetters['harvester/byId'](HCI.VM_IMAGE_DOWNLOADER, id);
setTimeout(() => {
commit('setDownloadImageInProgress', false);
dispatch('promptModal'); // bring undefined data will close the promptModal
}, 600);
if (rootGetters['isMultiCluster']) {
const clusterId = rootGetters['clusterId'];
const prefix = `/k8s/clusters/${ clusterId }`;
window.location.href = `${ parse.origin }${ prefix }/v1/harvester/${ HCI.IMAGE }/${ id }/download`;
} else {
const link = `${ parse.origin }/v1/harvester/${ HCI.IMAGE }/${ id }/download`;
window.location.href = link;
}
clearInterval(timer);
}
}, 1000);
},
async bundleProgress({
state, dispatch, commit, rootGetters
}) {
@ -117,7 +206,7 @@ const actions = {
const timer = setInterval(async() => {
count = count + 1;
if (count % 3 === 0) {
// ws mayby disconnect
// ws maybe disconnect
bundleCrd = await dispatch(
'harvester/find',
{

View File

@ -1,56 +1,57 @@
export const HCI = {
VM: 'kubevirt.io.virtualmachine',
VMI: 'kubevirt.io.virtualmachineinstance',
VMIM: 'kubevirt.io.virtualmachineinstancemigration',
VM_TEMPLATE: 'harvesterhci.io.virtualmachinetemplate',
VM_VERSION: 'harvesterhci.io.virtualmachinetemplateversion',
IMAGE: 'harvesterhci.io.virtualmachineimage',
SSH: 'harvesterhci.io.keypair',
VOLUME: 'harvesterhci.io.volume',
USER: 'harvesterhci.io.user',
SETTING: 'harvesterhci.io.setting',
UPGRADE: 'harvesterhci.io.upgrade',
UPGRADE_LOG: 'harvesterhci.io.upgradelog',
SCHEDULE_VM_BACKUP: 'harvesterhci.io.schedulevmbackup',
BACKUP: 'harvesterhci.io.virtualmachinebackup',
RESTORE: 'harvesterhci.io.virtualmachinerestore',
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
CLUSTER: 'harvesterhci.io.management.cluster',
DASHBOARD: 'harvesterhci.io.dashboard',
BLOCK_DEVICE: 'harvesterhci.io.blockdevice',
CLOUD_TEMPLATE: 'harvesterhci.io.cloudtemplate',
HOST: 'harvesterhci.io.host',
VERSION: 'harvesterhci.io.version',
SNAPSHOT: 'harvesterhci.io.volumesnapshot',
VM_SNAPSHOT: 'harvesterhci.io.vmsnapshot',
ALERTMANAGERCONFIG: 'harvesterhci.io.monitoring.alertmanagerconfig',
CLUSTER_FLOW: 'harvesterhci.io.logging.clusterflow',
CLUSTER_OUTPUT: 'harvesterhci.io.logging.clusteroutput',
FLOW: 'harvesterhci.io.logging.flow',
OUTPUT: 'harvesterhci.io.logging.output',
STORAGE: 'harvesterhci.io.storage',
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
KSTUNED: 'node.harvesterhci.io.ksmtuned',
PCI_DEVICE: 'devices.harvesterhci.io.pcidevice',
PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim',
SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice',
VGPU_DEVICE: 'devices.harvesterhci.io.vgpudevice',
SR_IOVGPU_DEVICE: 'devices.harvesterhci.io.sriovgpudevice',
USB_DEVICE: 'devices.harvesterhci.io.usbdevice',
USB_CLAIM: 'devices.harvesterhci.io.usbdeviceclaim',
VLAN_CONFIG: 'network.harvesterhci.io.vlanconfig',
VLAN_STATUS: 'network.harvesterhci.io.vlanstatus',
ADD_ONS: 'harvesterhci.io.addon',
LINK_MONITOR: 'network.harvesterhci.io.linkmonitor',
SECRET: 'harvesterhci.io.secret',
INVENTORY: 'metal.harvesterhci.io.inventory',
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'
VM: 'kubevirt.io.virtualmachine',
VMI: 'kubevirt.io.virtualmachineinstance',
VMIM: 'kubevirt.io.virtualmachineinstancemigration',
VM_TEMPLATE: 'harvesterhci.io.virtualmachinetemplate',
VM_VERSION: 'harvesterhci.io.virtualmachinetemplateversion',
IMAGE: 'harvesterhci.io.virtualmachineimage',
SSH: 'harvesterhci.io.keypair',
VOLUME: 'harvesterhci.io.volume',
USER: 'harvesterhci.io.user',
SETTING: 'harvesterhci.io.setting',
UPGRADE: 'harvesterhci.io.upgrade',
UPGRADE_LOG: 'harvesterhci.io.upgradelog',
SCHEDULE_VM_BACKUP: 'harvesterhci.io.schedulevmbackup',
BACKUP: 'harvesterhci.io.virtualmachinebackup',
RESTORE: 'harvesterhci.io.virtualmachinerestore',
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
VM_IMAGE_DOWNLOADER: 'harvesterhci.io.virtualmachineimagedownloader',
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
CLUSTER: 'harvesterhci.io.management.cluster',
DASHBOARD: 'harvesterhci.io.dashboard',
BLOCK_DEVICE: 'harvesterhci.io.blockdevice',
CLOUD_TEMPLATE: 'harvesterhci.io.cloudtemplate',
HOST: 'harvesterhci.io.host',
VERSION: 'harvesterhci.io.version',
SNAPSHOT: 'harvesterhci.io.volumesnapshot',
VM_SNAPSHOT: 'harvesterhci.io.vmsnapshot',
ALERTMANAGERCONFIG: 'harvesterhci.io.monitoring.alertmanagerconfig',
CLUSTER_FLOW: 'harvesterhci.io.logging.clusterflow',
CLUSTER_OUTPUT: 'harvesterhci.io.logging.clusteroutput',
FLOW: 'harvesterhci.io.logging.flow',
OUTPUT: 'harvesterhci.io.logging.output',
STORAGE: 'harvesterhci.io.storage',
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
KSTUNED: 'node.harvesterhci.io.ksmtuned',
PCI_DEVICE: 'devices.harvesterhci.io.pcidevice',
PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim',
SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice',
VGPU_DEVICE: 'devices.harvesterhci.io.vgpudevice',
SR_IOVGPU_DEVICE: 'devices.harvesterhci.io.sriovgpudevice',
USB_DEVICE: 'devices.harvesterhci.io.usbdevice',
USB_CLAIM: 'devices.harvesterhci.io.usbdeviceclaim',
VLAN_CONFIG: 'network.harvesterhci.io.vlanconfig',
VLAN_STATUS: 'network.harvesterhci.io.vlanstatus',
ADD_ONS: 'harvesterhci.io.addon',
LINK_MONITOR: 'network.harvesterhci.io.linkmonitor',
SECRET: 'harvesterhci.io.secret',
INVENTORY: 'metal.harvesterhci.io.inventory',
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'
};
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';