mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
feat(xo6,rest-api): export vdi as vmdk (#10591)
This commit is contained in:
committed by
GitHub
parent
a350c62cc3
commit
cfa75ef652
@@ -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> = {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user