Files
Xen-Orchestra-xen-orchestra…/@xen-orchestra/web/dev/v2v-mock/plugin.ts
Olivier Floch d10aa60358 feat(web): add the V2V start job
- useXoV2vStartJob posts the launch to the pool import route and returns
  its task id, without monitoring it: an import lasts hours
- useV2vLaunchForm: stop the source VMs and stop on error by default, two
  VMs at a time, concurrency an integer from 1 to 8 as the back end accepts
- Dev mock: the import route answers a task id, or the problems the back
  end would refuse the launch for
- New test helper: createV2vStartPayloadInput, the input of buildV2vStartPayload
2026-11-09 18:47:46 +01:00

169 lines
5.0 KiB
TypeScript

import type { XoV2vInventoryPayload, XoV2vStartPayload, XoV2vStatus, XoV2vVm } from '@vates/types'
import type { IncomingMessage, ServerResponse } from 'node:http'
import type { Plugin } from 'vite'
import { createMockInventory } from './fixtures.ts'
type V2vMockOptions = {
enabled: boolean
vecturaError?: string
}
function sendJson(response: ServerResponse, status: number, body: unknown) {
response.statusCode = status
response.setHeader('Content-Type', 'application/json')
response.end(JSON.stringify(body))
}
async function readJson<T>(request: IncomingMessage): Promise<T> {
const chunks: Buffer[] = []
for await (const chunk of request) {
chunks.push(chunk as Buffer)
}
return JSON.parse(Buffer.concat(chunks).toString('utf8')) as T
}
async function handleInventory(request: IncomingMessage, response: ServerResponse) {
const { connection } = await readJson<XoV2vInventoryPayload>(request)
if (connection.password === 'bad') {
sendJson(response, 422, { error: 'invalid credentials', data: { code: 'V2V_INVALID_CREDENTIALS' } })
return
}
if (connection.host === 'unreachable.lab') {
sendJson(response, 500, { error: `${connection.host}:${connection.port} is unreachable` })
return
}
// A real inventory takes a while: let the "connecting" state show
await new Promise(resolve => setTimeout(resolve, 800))
sendJson(response, 200, createMockInventory(connection.host))
}
type V2vStartProblem = { vm?: XoV2vVm['id']; field?: string; message: string }
const START_VM_UNSUPPORTED = 'starting the VM after its import is not supported yet'
const CLEANUP_UNSUPPORTED = 'cleaning up the VM is not supported yet'
// Mirrors `planMigration` of the REST API (`@xen-orchestra/rest-api/src/v2v/v2v.plan.mts`): every problem at once
function planMockMigration({ connection, defaults, vms }: XoV2vStartPayload): V2vStartProblem[] {
const problems: V2vStartProblem[] = []
const sourceVms = new Map(createMockInventory(connection.host).vms.map(vm => [vm.id, vm]))
const seenVmIds = new Set<XoV2vVm['id']>()
if (defaults.startVm) {
problems.push({ field: 'defaults.startVm', message: START_VM_UNSUPPORTED })
}
if (defaults.cleanup) {
problems.push({ field: 'defaults.cleanup', message: CLEANUP_UNSUPPORTED })
}
for (const vm of vms) {
const report = (message: string, field?: string) => problems.push({ vm: vm.id, field, message })
if (seenVmIds.has(vm.id)) {
report('the VM is listed more than once')
continue
}
seenVmIds.add(vm.id)
const sourceVm = sourceVms.get(vm.id)
if (sourceVm === undefined) {
report('no such VM on the source')
continue
}
if (vm.startVm) {
report(START_VM_UNSUPPORTED, 'startVm')
}
if (vm.cleanup) {
report(CLEANUP_UNSUPPORTED, 'cleanup')
}
if (vm.tags.length > 0) {
report('setting tags is not supported yet', 'tags')
}
if (!vm.keepMac) {
report('the MAC addresses are always kept for now', 'keepMac')
}
for (const disk of sourceVm.disks) {
if (disk.isRdm) {
report(`the disk ${disk.key} is a raw device mapping, which cannot be imported`, 'disks')
}
if (disk.isEncrypted) {
report(`the disk ${disk.key} is encrypted, which cannot be imported`, 'disks')
}
}
}
return problems
}
async function handleStart(request: IncomingMessage, response: ServerResponse) {
const problems = planMockMigration(await readJson<XoV2vStartPayload>(request))
if (problems.length > 0) {
// What the REST API error handler answers for `invalidParameters`
sendJson(response, 422, { error: 'the migration cannot be started', data: { errors: problems } })
return
}
// The task does not exist in XO: its link leads to an empty task panel
sendJson(response, 202, { taskId: `v2v-mock-task-${Date.now()}` })
}
const START_PATHNAME = /^\/rest\/v0\/pools\/[^/]+\/actions\/import_vms_from_vmware$/
// Stands in for the V2V REST routes until the back end ships them (dev server only, never built)
export function v2vMockPlugin({ enabled, vecturaError }: V2vMockOptions): Plugin {
return {
name: 'xo-v2v-mock',
apply: (_config, { command }) => enabled && command === 'serve',
configureServer(server) {
server.middlewares.use((request, response, next) => {
const { pathname } = new URL(request.url ?? '/', 'http://localhost')
if (pathname === '/rest/v0/v2v/status' && request.method === 'GET') {
const status: XoV2vStatus = {
vectura: vecturaError ? { status: 'error', error: vecturaError } : { status: 'success' },
}
sendJson(response, 200, status)
return
}
if (pathname === '/rest/v0/v2v/inventory' && request.method === 'POST') {
handleInventory(request, response).catch(next)
return
}
if (START_PATHNAME.test(pathname) && request.method === 'POST') {
handleStart(request, response).catch(next)
return
}
next()
})
},
}
}