feat(xo6,rest-api): export vdi as vmdk (#10591)

This commit is contained in:
Florent BEAUCHAMP
2026-10-16 10:57:42 +02:00
committed by GitHub
parent a350c62cc3
commit cfa75ef652
10 changed files with 51 additions and 18 deletions

View File

@@ -663,6 +663,19 @@ export const SUPPORTED_VDI_FORMAT = {
export type SUPPORTED_VDI_FORMAT = (typeof SUPPORTED_VDI_FORMAT)[keyof typeof SUPPORTED_VDI_FORMAT]
/**
* Formats a VDI can be exported to by XO: the XAPI ones, plus the ones XO generates
* itself from the blocks of the disk.
*/
export const SUPPORTED_VDI_EXPORT_FORMAT = {
raw: 'raw',
vhd: 'vhd',
qcow2: 'qcow2',
vmdk: 'vmdk',
} as const
export type SUPPORTED_VDI_EXPORT_FORMAT = (typeof SUPPORTED_VDI_EXPORT_FORMAT)[keyof typeof SUPPORTED_VDI_EXPORT_FORMAT]
// ----- XAPI Stats
type XapiStatsResponse<T> = {

View File

@@ -42,6 +42,7 @@
"@xen-orchestra/backups": "^0.76.1",
"@xen-orchestra/log": "^0.7.2",
"@xen-orchestra/qcow2": "^1.4.0",
"@xen-orchestra/vmdk": "^0.1.0",
"@xen-orchestra/xapi": "^9.0.1",
"complex-matcher": "^1.1.1",
"fast-xml-parser": "^5.10.1",

View File

@@ -18,7 +18,7 @@ import { inject } from 'inversify'
import { provide } from 'inversify-binding-decorators'
import type { Readable } from 'node:stream'
import type { Request as ExRequest } from 'express'
import type { SUPPORTED_VDI_FORMAT, XoAlarm, XoMessage, XoTask, XoVdiSnapshot } from '@vates/types'
import type { SUPPORTED_VDI_EXPORT_FORMAT, XoAlarm, XoMessage, XoTask, XoVdiSnapshot } from '@vates/types'
import { acl } from '../middlewares/acl.middleware.mjs'
import { escapeUnsafeComplexMatcher } from '../helpers/utils.helper.mjs'
@@ -104,7 +104,7 @@ export class VdiSnapshotController extends XapiXoController<XoVdiSnapshot> {
async exportVdiSnapshotContent(
@Request() req: ExRequest,
@Path() id: string,
@Path() format: SUPPORTED_VDI_FORMAT
@Path() format: SUPPORTED_VDI_EXPORT_FORMAT
): Promise<Readable> {
const stream = await this.#vdiService.exportContent(id as XoVdiSnapshot['id'], 'VDI-snapshot', { format })

View File

@@ -21,7 +21,16 @@ import { inject } from 'inversify'
import { provide } from 'inversify-binding-decorators'
import type { Readable } from 'node:stream'
import { json, type Request as ExRequest } from 'express'
import type { SUPPORTED_VDI_FORMAT, Xapi, XoAlarm, XoMessage, XoSr, XoTask, XoVdi } from '@vates/types'
import type {
SUPPORTED_VDI_EXPORT_FORMAT,
SUPPORTED_VDI_FORMAT,
Xapi,
XoAlarm,
XoMessage,
XoSr,
XoTask,
XoVdi,
} from '@vates/types'
import { SUPPORTED_ACTIONS_BY_RESOURCE, type SupportedActions } from '@xen-orchestra/acl'
import { invalidParameters } from 'xo-common/api-errors.js'
@@ -132,7 +141,7 @@ export class VdiController extends XapiXoController<XoVdi> {
async exportVdiContent(
@Request() req: ExRequest,
@Path() id: string,
@Path() format: SUPPORTED_VDI_FORMAT
@Path() format: SUPPORTED_VDI_EXPORT_FORMAT
): Promise<Readable> {
const stream = await this.#vdiService.exportContent(id as XoVdi['id'], 'VDI', { format })

View File

@@ -1,7 +1,8 @@
import { createLogger } from '@xen-orchestra/log'
import { toQcow2Stream } from '@xen-orchestra/qcow2'
import { SUPPORTED_VDI_FORMAT } from '@vates/types'
import { SUPPORTED_VDI_EXPORT_FORMAT, SUPPORTED_VDI_FORMAT } from '@vates/types'
import { toVhdStream } from 'vhd-lib/disk-consumer/index.mjs'
import { toVmdkStream } from '@xen-orchestra/vmdk'
import { VHD_MAX_SIZE, XapiDiskSource } from '@xen-orchestra/xapi'
import type { Disk } from '@xen-orchestra/disk-transform'
import type { XoVdi, XoVdiSnapshot } from '@vates/types'
@@ -79,9 +80,9 @@ export class VdiService {
async exportContent<Vdi extends XoVdi | XoVdiSnapshot>(
id: Vdi['id'],
type: Vdi['type'],
{ format }: { format: SUPPORTED_VDI_FORMAT }
{ format }: { format: SUPPORTED_VDI_EXPORT_FORMAT }
): Promise<ExportedContentStream> {
if (format === SUPPORTED_VDI_FORMAT.vhd) {
if (format === SUPPORTED_VDI_EXPORT_FORMAT.vhd) {
const { size } = this.#restApi.getObject<Vdi>(id, type)
if (size > VHD_MAX_SIZE) {
throw new ApiError(`a VDI of ${size} bytes is too large to be exported as VHD`, 422, {
@@ -92,10 +93,18 @@ export class VdiService {
return this.#exportContentFromDiskSource<Vdi>(id, type, disk => toVhdStream(disk))
}
if (format === SUPPORTED_VDI_FORMAT.qcow2) {
if (format === SUPPORTED_VDI_EXPORT_FORMAT.qcow2) {
return this.#exportContentFromDiskSource<Vdi>(id, type, disk => toQcow2Stream(disk))
}
if (format === SUPPORTED_VDI_EXPORT_FORMAT.vmdk) {
// the `withLength` layout pads the grains so that the size is known before the
// export starts, at the cost of not shrinking the file to its compressed size
return this.#exportContentFromDiskSource<Vdi>(id, type, disk =>
toVmdkStream(disk, { diskName: `${id}.vmdk`, layout: 'withLength' })
)
}
return this.#exportContentAsRaw<Vdi>(id, type)
}
}

View File

@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import { Readable } from 'node:stream'
import { RandomAccessDisk } from '@xen-orchestra/disk-transform'
import { SUPPORTED_VDI_FORMAT } from '@vates/types'
import { SUPPORTED_VDI_EXPORT_FORMAT, SUPPORTED_VDI_FORMAT } from '@vates/types'
import { toVhdStream } from 'vhd-lib/disk-consumer/index.mjs'
import { VHD_MAX_SIZE } from '@xen-orchestra/xapi'
@@ -92,7 +92,7 @@ function createRestApi() {
}
describe('VdiService.exportContent', () => {
for (const format of Object.values(SUPPORTED_VDI_FORMAT)) {
for (const format of Object.values(SUPPORTED_VDI_EXPORT_FORMAT)) {
it(`announces, for the ${format} format, a length equal to the number of streamed bytes`, async () => {
const { restApi } = createRestApi()

View File

@@ -25,7 +25,7 @@ import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmB
import VtsSelect from '@core/components/select/VtsSelect.vue'
import UiDrawer from '@core/components/ui/drawer/UiDrawer.vue'
import { useFormSelect } from '@core/packages/form-select'
import { SUPPORTED_VDI_FORMAT } from '@vates/types'
import { SUPPORTED_VDI_EXPORT_FORMAT } from '@vates/types'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -36,10 +36,10 @@ const emit = defineEmits<{
const { t } = useI18n()
const exportFormat = ref<VdiExportFormat>(SUPPORTED_VDI_FORMAT.vhd)
const exportFormat = ref<VdiExportFormat>(SUPPORTED_VDI_EXPORT_FORMAT.vhd)
const options = Object.entries(SUPPORTED_VDI_FORMAT)
.filter(([, value]) => value !== SUPPORTED_VDI_FORMAT.qcow2)
const options = Object.entries(SUPPORTED_VDI_EXPORT_FORMAT)
.filter(([, value]) => value !== SUPPORTED_VDI_EXPORT_FORMAT.qcow2)
.map(([key, value]) => ({
value,
label: key,

View File

@@ -3,12 +3,12 @@ import type { FrontXoVdi } from '@/modules/vdi/remote-resources/use-xo-vdi-colle
import type { VdiExportFormat } from '@/shared/constants.ts'
import { useOverlay } from '@core/packages/overlay/use-overlay.ts'
import { toComputed } from '@core/utils/to-computed.util.ts'
import { SUPPORTED_VDI_FORMAT } from '@vates/types'
import { SUPPORTED_VDI_EXPORT_FORMAT } from '@vates/types'
import { type MaybeRefOrGetter, ref } from 'vue'
export function useVdiExport(rawVdi: MaybeRefOrGetter<FrontXoVdi>) {
const vdi = toComputed(rawVdi)
const selectedFormat = ref<VdiExportFormat>(SUPPORTED_VDI_FORMAT.vhd)
const selectedFormat = ref<VdiExportFormat>(SUPPORTED_VDI_EXPORT_FORMAT.vhd)
const { run, isRunning: isExportingVdi } = useXoVdiExportJob(vdi, selectedFormat)

View File

@@ -1,4 +1,4 @@
import { SUPPORTED_VDI_FORMAT } from '@vates/types'
import { SUPPORTED_VDI_EXPORT_FORMAT } from '@vates/types'
export const XOA_NAME = 'Xen Orchestra Appliance'
export const IS_XOA_BUILD = import.meta.env.VITE_XOA_BUILD === 'true'
@@ -35,7 +35,7 @@ export const VDI_SOURCE = {
URL: 'url',
} as const
export type VdiExportFormat = Exclude<SUPPORTED_VDI_FORMAT, 'qcow2'>
export type VdiExportFormat = Exclude<SUPPORTED_VDI_EXPORT_FORMAT, 'qcow2'>
export const VDI_PAGE_CONTEXT = {
VM: 'vm',

View File

@@ -51,6 +51,7 @@
- [XO5/Host/Patches] Warn when installing patches on a host while its pool master is not up to date (PR [#10523](https://github.com/vatesfr/xen-orchestra/pull/10523))
- [XO6] Add copy button for multiple information on Host/VM Dashboard and Pool/Host/VM System tabs (PR [#9976](https://github.com/vatesfr/xen-orchestra/pull/9976))
- [XO6] Export VDI as vmdk (PR [#10591](https://github.com/vatesfr/xen-orchestra/pull/10591))
### Bug fixes