feat(xo6): add possibility to export VM (#9989)

This commit is contained in:
Sandrine Barrucand
2026-07-15 14:42:36 +02:00
committed by GitHub
parent 2b7b6baf02
commit 1afffb8f75
8 changed files with 216 additions and 0 deletions

View File

@@ -776,6 +776,7 @@
"other": "Other",
"other-properties": "Other properties",
"other-settings": "Other settings",
"ova": "OVA",
"page-not-found": "This page is not to be found…",
"page-please-wait": "Hold tight, astronaut! The page is getting ready to take off.{newline}We're just aligning the thrusters and calibrating the systems. It won't be long now, stay tuned!",
"pagination:all": "@:all",
@@ -1201,6 +1202,7 @@
"xoa-password-confirm-different": "XOA password confirmation is different",
"xoa-ssh-account": "XOA SSH account",
"xoa-ssh-password-confirm-different": "SSH password confirmation is different",
"xva": "XVA",
"you-are-currently-on": "You are currently on: {0}",
"zstd": "ZSTD"
}

View File

@@ -776,6 +776,7 @@
"other": "Autre",
"other-properties": "Autres propriétés",
"other-settings": "Autres paramètres",
"ova": "OVA",
"page-not-found": "Cette page est introuvable…",
"page-please-wait": "Accrochez-vous, astronaute ! La page est sur le point de décoller.{newline}Nous alignons les propulseurs et calibrons les systèmes. Plus que quelques instants, restez à l'écoute !",
"pagination:all": "Tous",
@@ -1201,6 +1202,7 @@
"xoa-password-confirm-different": "La confirmation du mot de passe XOA est différente",
"xoa-ssh-account": "Compte SSH de la XOA",
"xoa-ssh-password-confirm-different": "La confirmation du mot de passe SSH est différente",
"xva": "XVA",
"you-are-currently-on": "Vous êtes actuellement sur : {0}",
"zstd": "ZSTD"
}

View File

@@ -14,12 +14,14 @@
{{ t('action:export') }}
</MenuItem>
<MenuSeparator />
<VmExportButton :vm />
<VmDeleteButton :vm />
</template>
<script lang="ts" setup>
import VmDeleteButton from '@/modules/vm/components/actions/delete/VmDeleteButton.vue'
import VmDuplicateButton from '@/modules/vm/components/actions/duplicate/VmDuplicateButton.vue'
import VmExportButton from '@/modules/vm/components/actions/export/VmExportButton.vue'
import VmSnapshotButton from '@/modules/vm/components/actions/snapshot/VmSnapshotButton.vue'
import VmPowerStateActions from '@/modules/vm/components/actions/VmPowerStateActions.vue'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'

View File

@@ -0,0 +1,20 @@
<template>
<MenuItem icon="action:download" @click="openDrawer()">
{{ t('action:export') }}
</MenuItem>
</template>
<script lang="ts" setup>
import { useVmExportDrawer } from '@/modules/vm/composables/use-vm-export-drawer.composable'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection'
import MenuItem from '@core/components/menu/MenuItem.vue'
import { useI18n } from 'vue-i18n'
const { vm } = defineProps<{
vm: FrontXoVm
}>()
const { t } = useI18n()
const { openDrawer } = useVmExportDrawer(() => vm)
</script>

View File

@@ -0,0 +1,112 @@
<template>
<VtsDrawer dismissible @dismiss="emit('cancel')">
<template #title>
{{ t('action:export-n-vms', 1) }}
</template>
<template #content>
<div class="form">
<p class="section-title">{{ t('general-information') }}</p>
<!-- Type -->
<div class="field">
<UiLabel accent="neutral" required>{{ t('type') }}</UiLabel>
<div class="radio-group">
<UiRadioButton v-model="type" accent="brand" value="xva"> {{ t('xva') }} </UiRadioButton>
<UiRadioButton v-model="type" accent="brand" value="ova"> {{ t('ova') }} </UiRadioButton>
</div>
</div>
<!-- Compression: only available for XVA -->
<template v-if="type === 'xva'">
<p class="section-title">{{ t('options') }}</p>
<div class="field">
<UiLabel accent="neutral" required>{{ t('compression') }}</UiLabel>
<div class="radio-group">
<UiRadioButton v-model="compression" accent="brand" value="none">
{{ t('disabled') }}
</UiRadioButton>
<UiRadioButton v-model="compression" accent="brand" value="gzip"> {{ t('gzip') }}</UiRadioButton>
<UiRadioButton v-model="compression" accent="brand" value="zstd"> {{ t('zstd') }}</UiRadioButton>
</div>
</div>
</template>
</div>
</template>
<template #buttons>
<VtsDrawerCancelButton />
<VtsDrawerConfirmButton :on-click="handleConfirm">{{ t('action:export') }}</VtsDrawerConfirmButton>
</template>
</VtsDrawer>
</template>
<script lang="ts" setup>
import type { VmExportCompression, VmExportType } from '@/modules/vm/jobs/xo-vm-export.job'
import VtsDrawer from '@core/components/drawer/VtsDrawer.vue'
import VtsDrawerCancelButton from '@core/components/drawer/VtsDrawerCancelButton.vue'
import VtsDrawerConfirmButton from '@core/components/drawer/VtsDrawerConfirmButton.vue'
import UiLabel from '@core/components/ui/label/UiLabel.vue'
import UiRadioButton from '@core/components/ui/radio-button/UiRadioButton.vue'
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
export type VmExportFormValues = {
type: VmExportType
compression: VmExportCompression
}
const emit = defineEmits<{
confirm: [values: VmExportFormValues]
cancel: []
}>()
const { t } = useI18n()
const type = ref<VmExportType>('xva')
const compression = ref<VmExportCompression>('none')
watch(type, newType => {
if (newType === 'ova') {
compression.value = 'none'
}
})
function handleConfirm() {
emit('confirm', {
type: type.value,
compression: compression.value,
})
}
</script>
<style lang="postcss" scoped>
.form {
display: flex;
flex-direction: column;
gap: 1.6rem;
width: 100%;
text-align: left;
}
.section-title {
font-weight: 600;
color: var(--color-brand-txt-base);
border-bottom: 0.1rem solid var(--color-brand-txt-base);
padding-bottom: 0.8rem;
width: 100%;
}
.field {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.radio-group {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 1.6rem;
}
</style>

View File

@@ -0,0 +1,33 @@
import type { VmExportFormValues } from '@/modules/vm/components/drawer/VmExportDrawer.vue'
import { useXoVmExportJob, type VmExportCompression, type VmExportType } from '@/modules/vm/jobs/xo-vm-export.job'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection'
import { useDrawer } from '@core/packages/drawer/use-drawer'
import { toComputed } from '@core/utils/to-computed.util'
import { ref, type MaybeRefOrGetter } from 'vue'
export function useVmExportDrawer(rawVm: MaybeRefOrGetter<FrontXoVm>) {
const vm = toComputed(rawVm)
const exportType = ref<VmExportType>('xva')
const exportCompression = ref<VmExportCompression>('none')
const { run } = useXoVmExportJob(() => vm.value, exportType, exportCompression)
const openDrawer = useDrawer(() => ({
component: import('@/modules/vm/components/drawer/VmExportDrawer.vue'),
onConfirm: async (values: VmExportFormValues) => {
try {
exportType.value = values.type
exportCompression.value = values.compression
await run()
} catch (error) {
console.error('Error when exporting VM:', error)
}
},
}))
return {
openDrawer,
}
}

View File

@@ -0,0 +1,44 @@
import { xoVmArg } from '@/modules/vm/jobs/xo-vm-args'
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection'
import { BASE_URL } from '@/shared/utils/fetch.util'
import { defineJob, defineJobArg, JobError } from '@core/packages/job'
import { downloadFile } from '@core/utils/download-file.utils.ts'
import { useI18n } from 'vue-i18n'
export type VmExportType = 'xva' | 'ova'
export type VmExportCompression = 'none' | 'gzip' | 'zstd'
const xoVmExportTypeArg = defineJobArg<VmExportType>({
identify: type => type,
toArray: false,
})
const xoVmExportCompressionArg = defineJobArg<VmExportCompression>({
identify: compression => compression,
toArray: false,
})
export const useXoVmExportJob = defineJob('vm.export', [xoVmArg, xoVmExportTypeArg, xoVmExportCompressionArg], () => {
const { t } = useI18n()
return {
async run(vm: FrontXoVm, type: VmExportType, compression: VmExportCompression) {
const params = new URLSearchParams()
if (type === 'xva' && compression !== 'none') {
params.set('compress', compression)
}
const query = params.size > 0 ? `?${params.toString()}` : ''
const url = `${BASE_URL}/vms/${vm.id}.${type}${query}`
const fileName = `${vm.id}.${type}`
downloadFile(url, fileName)
},
validate(_isRunning: boolean, vm?: FrontXoVm) {
if (!vm) {
throw new JobError(t('job:vm-export:missing-vm'))
}
},
}
})

View File

@@ -25,6 +25,7 @@
- [RPU] Trace rolling pool updates/reboots to disk to allow diagnosis even after xo-server restarts (PR [#10078](https://github.com/vatesfr/xen-orchestra/pull/10078))
- [XO6/VM] Group VM power actions in a new "Change state" submenu and isolate the Delete action (PR [#10036](https://github.com/vatesfr/xen-orchestra/pull/10036))
- [OpenMetrics] Add an estimated per-VM power consumption metric (`xcp_vm_power_consumption_watts`), splitting each host's IPMI power across its running VMs proportionally to CPU load (PR [#10031](https://github.com/vatesfr/xen-orchestra/pull/10031))
- [XO6/VM] Add possibility to export a VM (PR [#9989](https://github.com/vatesfr/xen-orchestra/pull/9989))
### Bug fixes