diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 1b33c72cb..56b82116f 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -9,6 +9,7 @@ - [Delta Backup] Use [NBD](https://en.wikipedia.org/wiki/Network_block_device) to download disks (PR [#6461](https://github.com/vatesfr/xen-orchestra/pull/6461)) - [License] Possibility to bind XCP-ng license to hosts at pool level (PR [#6453](https://github.com/vatesfr/xen-orchestra/pull/6453)) +- [New VM] Ability to destroy the cloud configuration disk after the first boot [#6438](https://github.com/vatesfr/xen-orchestra/issues/6438) (PR [#6486](https://github.com/vatesfr/xen-orchestra/pull/6486)) ### Bug fixes diff --git a/packages/xo-server/src/api/vm.mjs b/packages/xo-server/src/api/vm.mjs index c5c02ef38..8c04265d9 100644 --- a/packages/xo-server/src/api/vm.mjs +++ b/packages/xo-server/src/api/vm.mjs @@ -48,6 +48,39 @@ const extract = (obj, prop) => { return value } +const startVmAndDestroyCloudConfigVdi = async (xapi, vm, vdiUuid, params) => { + try { + const timeLimit = Date.now() + 10 * 60 * 1000 + await xapi.startVm(vm.uuid) + + if (params.destroyCloudConfigVdiAfterBoot && vdiUuid !== undefined) { + // wait for the 'Running' event to be really stored in local xapi object cache + await xapi.waitObjectState(vm.uuid, vm => vm.power_state === 'Running', { timeout: timeLimit - Date.now() }) + + // wait for the guest tool version to be defined + await xapi + .waitObjectState( + xapi.getObjectByRef(vm.$ref).guest_metrics, + gm => gm?.PV_drivers_version?.major !== undefined, + { timeout: timeLimit - Date.now() } + ) + .catch(error => { + log.warn('startVmAndDestroyCloudConfigVdi: failed to wait guest metrics, consider VM as started', { + error, + vm: { uuid: vm.uuid }, + }) + }) + + // destroy cloud config drive + const vdi = xapi.getObjectByUuid(vdiUuid) + await vdi.$VBDs[0].$unplug() + await vdi.$destroy() + } + } catch (error) { + log.warn('startVmAndDestroyCloudConfigVdi', { error, vdi: { uuid: vdiUuid }, vm: { uuid: vm.uuid } }) + } +} + // TODO: Implement ACLs export const create = defer(async function ($defer, params) { const { user } = this.apiContext @@ -156,6 +189,30 @@ export const create = defer(async function ($defer, params) { const vm = xapi.xo.addObject(xapiVm) + // create cloud config drive + let cloudConfigVdiUuid + if (params.cloudConfig != null) { + // Find the SR of the first VDI. + let srId + forEach(vm.$VBDs, vbdId => { + const vbd = this.getObject(vbdId) + const vdiId = vbd.VDI + if (!vbd.is_cd_drive && vdiId !== undefined) { + srId = this.getObject(vdiId).$SR + return false + } + }) + + try { + cloudConfigVdiUuid = params.coreOs + ? await xapi.createCoreOsCloudInitConfigDrive(vm.id, srId, params.cloudConfig) + : await xapi.createCloudInitConfigDrive(vm.id, srId, params.cloudConfig, params.networkConfig) + } catch (error) { + log.warn('vm.create', { vmId: vm.id, srId, error }) + throw error + } + } + if (resourceSet) { await Promise.all([ params.share @@ -173,7 +230,7 @@ export const create = defer(async function ($defer, params) { } if (params.bootAfterCreate) { - ignoreErrors.call(xapi.startVm(vm._xapiId)) + startVmAndDestroyCloudConfigVdi(xapi, xapiVm, cloudConfigVdiUuid, params) } return vm.id @@ -217,6 +274,11 @@ create.params = { optional: true, }, + destroyCloudConfigVdiAfterBoot: { + type: 'boolean', + optional: true, + }, + installation: { type: 'object', optional: true, diff --git a/packages/xo-server/src/xapi/index.mjs b/packages/xo-server/src/xapi/index.mjs index e09e990ce..d2ce0d759 100644 --- a/packages/xo-server/src/xapi/index.mjs +++ b/packages/xo-server/src/xapi/index.mjs @@ -1287,12 +1287,17 @@ export default class Xapi extends XapiBase { const host = this.pool.$master const sr = this.getObject(srId) - await this.call('host.call_plugin', host.$ref, 'xscontainer', 'create_config_drive', { - vmuuid: vm.uuid, - sruuid: sr.uuid, - configuration: config, - }) + // See https://github.com/xenserver/xscontainer/blob/master/src/scripts/xscontainer-pluginexample + const vdiUuid = ( + await this.call('host.call_plugin', host.$ref, 'xscontainer', 'create_config_drive', { + vmuuid: vm.uuid, + sruuid: sr.uuid, + configuration: config, + }) + ).replace(/^True/, '') await this.registerDockerContainer(vmId) + + return vdiUuid } // Generic Config Drive @@ -1343,6 +1348,8 @@ export default class Xapi extends XapiBase { }) await this.VBD_create({ VDI: vdi.$ref, VM: vm.$ref }) + + return vdi.uuid } @decorateWith(deferrable) diff --git a/packages/xo-server/src/xapi/mixins/vm.mjs b/packages/xo-server/src/xapi/mixins/vm.mjs index 4bc688ca0..0301d6434 100644 --- a/packages/xo-server/src/xapi/mixins/vm.mjs +++ b/packages/xo-server/src/xapi/mixins/vm.mjs @@ -11,7 +11,7 @@ import { defer as deferrable } from 'golike-defer' import { ignoreErrors, pCatch } from 'promise-toolbox' import { Ref } from 'xen-api' -import { forEach, parseSize } from '../../utils.mjs' +import { parseSize } from '../../utils.mjs' import { isVmHvm, isVmRunning, makeEditObject } from '../utils.mjs' @@ -38,10 +38,6 @@ export default { vifs = undefined, existingVdis = undefined, - coreOs = false, - cloudConfig = undefined, - networkConfig = undefined, - vgpuType = undefined, gpuGroup = undefined, @@ -92,7 +88,7 @@ export default { // installation. await this.callAsync('VM.provision', vmRef) - let vm = await this._getOrWaitObject(vmRef) + const vm = await this._getOrWaitObject(vmRef) // Set VMs params. await this._editVm(vm, props, checkLimits) @@ -212,28 +208,6 @@ export default { await this.createVgpu(vm, gpuGroup, vgpuType) } - if (cloudConfig != null) { - // Refresh the record. - await this.barrier(vm.$ref) - vm = this.getObjectByRef(vm.$ref) - - // Find the SR of the first VDI. - let srRef - forEach(vm.$VBDs, vbd => { - let vdi - if (vbd.type === 'Disk' && (vdi = vbd.$VDI)) { - srRef = vdi.SR - return false - } - }) - - if (coreOs) { - await this.createCoreOsCloudInitConfigDrive(vm.$id, srRef, cloudConfig) - } else { - await this.createCloudInitConfigDrive(vm.$id, srRef, cloudConfig, networkConfig) - } - } - // wait for the record with all the VBDs and VIFs return this.barrier(vm.$ref) }, diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js index 4816913c6..1db16dd28 100644 --- a/packages/xo-web/src/common/intl/messages.js +++ b/packages/xo-web/src/common/intl/messages.js @@ -1553,6 +1553,7 @@ const messages = { templateHasBiosStrings: 'The template already contains the BIOS strings', secureBootLinkToDocumentationMessage: 'Click for more information about Guest UEFI Secure Boot.', vmBootFirmwareIsUefi: 'The boot firmware is UEFI', + destroyCloudConfigVdiAfterBoot: 'Destroy cloud config drive after first boot', // ----- Self ----- resourceSets: 'Resource sets', diff --git a/packages/xo-web/src/xo-app/new-vm/index.js b/packages/xo-web/src/xo-app/new-vm/index.js index a8d303044..428c44a18 100644 --- a/packages/xo-web/src/xo-app/new-vm/index.js +++ b/packages/xo-web/src/xo-app/new-vm/index.js @@ -305,6 +305,7 @@ export default class NewVm extends BaseComponent { cpuCap: '', cpusMax: '', cpuWeight: '', + destroyCloudConfigVdiAfterBoot: false, existingDisks: {}, fastClone: true, hvmBootFirmware: '', @@ -465,6 +466,7 @@ export default class NewVm extends BaseComponent { bootAfterCreate: state.bootAfterCreate, copyHostBiosStrings: state.hvmBootFirmware !== 'uefi' && !this._templateHasBiosStrings() && state.copyHostBiosStrings, + destroyCloudConfigVdiAfterBoot: state.destroyCloudConfigVdiAfterBoot, secureBoot: state.secureBoot, share: state.share, cloudConfig, @@ -1498,7 +1500,9 @@ export default class NewVm extends BaseComponent { cpuCap, cpusMax, cpuWeight, + destroyCloudConfigVdiAfterBoot, hvmBootFirmware, + installMethod, memoryDynamicMin, memoryDynamicMax, memoryStaticMax, @@ -1538,8 +1542,8 @@ export default class NewVm extends BaseComponent { {showAdvanced && [ -