feat(xo-web/new-vm): possibility to destroy cloud config drive after first boot (#6486)

Fixes #6438
This commit is contained in:
rajaa-b 2022-10-31 12:25:55 +01:00 committed by GitHub
parent b63c4a0d4f
commit 2eb3b15930
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 103 additions and 39 deletions

View File

@ -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)) - [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)) - [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 ### Bug fixes

View File

@ -48,6 +48,39 @@ const extract = (obj, prop) => {
return value 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 // TODO: Implement ACLs
export const create = defer(async function ($defer, params) { export const create = defer(async function ($defer, params) {
const { user } = this.apiContext const { user } = this.apiContext
@ -156,6 +189,30 @@ export const create = defer(async function ($defer, params) {
const vm = xapi.xo.addObject(xapiVm) 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) { if (resourceSet) {
await Promise.all([ await Promise.all([
params.share params.share
@ -173,7 +230,7 @@ export const create = defer(async function ($defer, params) {
} }
if (params.bootAfterCreate) { if (params.bootAfterCreate) {
ignoreErrors.call(xapi.startVm(vm._xapiId)) startVmAndDestroyCloudConfigVdi(xapi, xapiVm, cloudConfigVdiUuid, params)
} }
return vm.id return vm.id
@ -217,6 +274,11 @@ create.params = {
optional: true, optional: true,
}, },
destroyCloudConfigVdiAfterBoot: {
type: 'boolean',
optional: true,
},
installation: { installation: {
type: 'object', type: 'object',
optional: true, optional: true,

View File

@ -1287,12 +1287,17 @@ export default class Xapi extends XapiBase {
const host = this.pool.$master const host = this.pool.$master
const sr = this.getObject(srId) const sr = this.getObject(srId)
// 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', { await this.call('host.call_plugin', host.$ref, 'xscontainer', 'create_config_drive', {
vmuuid: vm.uuid, vmuuid: vm.uuid,
sruuid: sr.uuid, sruuid: sr.uuid,
configuration: config, configuration: config,
}) })
).replace(/^True/, '')
await this.registerDockerContainer(vmId) await this.registerDockerContainer(vmId)
return vdiUuid
} }
// Generic Config Drive // Generic Config Drive
@ -1343,6 +1348,8 @@ export default class Xapi extends XapiBase {
}) })
await this.VBD_create({ VDI: vdi.$ref, VM: vm.$ref }) await this.VBD_create({ VDI: vdi.$ref, VM: vm.$ref })
return vdi.uuid
} }
@decorateWith(deferrable) @decorateWith(deferrable)

View File

@ -11,7 +11,7 @@ import { defer as deferrable } from 'golike-defer'
import { ignoreErrors, pCatch } from 'promise-toolbox' import { ignoreErrors, pCatch } from 'promise-toolbox'
import { Ref } from 'xen-api' import { Ref } from 'xen-api'
import { forEach, parseSize } from '../../utils.mjs' import { parseSize } from '../../utils.mjs'
import { isVmHvm, isVmRunning, makeEditObject } from '../utils.mjs' import { isVmHvm, isVmRunning, makeEditObject } from '../utils.mjs'
@ -38,10 +38,6 @@ export default {
vifs = undefined, vifs = undefined,
existingVdis = undefined, existingVdis = undefined,
coreOs = false,
cloudConfig = undefined,
networkConfig = undefined,
vgpuType = undefined, vgpuType = undefined,
gpuGroup = undefined, gpuGroup = undefined,
@ -92,7 +88,7 @@ export default {
// installation. // installation.
await this.callAsync('VM.provision', vmRef) await this.callAsync('VM.provision', vmRef)
let vm = await this._getOrWaitObject(vmRef) const vm = await this._getOrWaitObject(vmRef)
// Set VMs params. // Set VMs params.
await this._editVm(vm, props, checkLimits) await this._editVm(vm, props, checkLimits)
@ -212,28 +208,6 @@ export default {
await this.createVgpu(vm, gpuGroup, vgpuType) 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 // wait for the record with all the VBDs and VIFs
return this.barrier(vm.$ref) return this.barrier(vm.$ref)
}, },

View File

@ -1553,6 +1553,7 @@ const messages = {
templateHasBiosStrings: 'The template already contains the BIOS strings', templateHasBiosStrings: 'The template already contains the BIOS strings',
secureBootLinkToDocumentationMessage: 'Click for more information about Guest UEFI Secure Boot.', secureBootLinkToDocumentationMessage: 'Click for more information about Guest UEFI Secure Boot.',
vmBootFirmwareIsUefi: 'The boot firmware is UEFI', vmBootFirmwareIsUefi: 'The boot firmware is UEFI',
destroyCloudConfigVdiAfterBoot: 'Destroy cloud config drive after first boot',
// ----- Self ----- // ----- Self -----
resourceSets: 'Resource sets', resourceSets: 'Resource sets',

View File

@ -305,6 +305,7 @@ export default class NewVm extends BaseComponent {
cpuCap: '', cpuCap: '',
cpusMax: '', cpusMax: '',
cpuWeight: '', cpuWeight: '',
destroyCloudConfigVdiAfterBoot: false,
existingDisks: {}, existingDisks: {},
fastClone: true, fastClone: true,
hvmBootFirmware: '', hvmBootFirmware: '',
@ -465,6 +466,7 @@ export default class NewVm extends BaseComponent {
bootAfterCreate: state.bootAfterCreate, bootAfterCreate: state.bootAfterCreate,
copyHostBiosStrings: copyHostBiosStrings:
state.hvmBootFirmware !== 'uefi' && !this._templateHasBiosStrings() && state.copyHostBiosStrings, state.hvmBootFirmware !== 'uefi' && !this._templateHasBiosStrings() && state.copyHostBiosStrings,
destroyCloudConfigVdiAfterBoot: state.destroyCloudConfigVdiAfterBoot,
secureBoot: state.secureBoot, secureBoot: state.secureBoot,
share: state.share, share: state.share,
cloudConfig, cloudConfig,
@ -1498,7 +1500,9 @@ export default class NewVm extends BaseComponent {
cpuCap, cpuCap,
cpusMax, cpusMax,
cpuWeight, cpuWeight,
destroyCloudConfigVdiAfterBoot,
hvmBootFirmware, hvmBootFirmware,
installMethod,
memoryDynamicMin, memoryDynamicMin,
memoryDynamicMax, memoryDynamicMax,
memoryStaticMax, memoryStaticMax,
@ -1538,8 +1542,8 @@ export default class NewVm extends BaseComponent {
</Button> </Button>
</SectionContent> </SectionContent>
{showAdvanced && [ {showAdvanced && [
<hr />, <hr key='hr' />,
<SectionContent> <SectionContent key='advanced'>
<Item> <Item>
<input checked={bootAfterCreate} onChange={this._linkState('bootAfterCreate')} type='checkbox' /> <input checked={bootAfterCreate} onChange={this._linkState('bootAfterCreate')} type='checkbox' />
&nbsp; &nbsp;
@ -1554,6 +1558,21 @@ export default class NewVm extends BaseComponent {
<Tags labels={tags} onChange={this._linkState('tags')} /> <Tags labels={tags} onChange={this._linkState('tags')} />
</Item> </Item>
</SectionContent>, </SectionContent>,
<SectionContent key='destroyCloudConfigVdi'>
<Item>
<input
checked={destroyCloudConfigVdiAfterBoot}
disabled={installMethod === 'noConfigDrive' || !bootAfterCreate}
id='destroyCloudConfigDisk'
onChange={this._toggleState('destroyCloudConfigVdiAfterBoot')}
type='checkbox'
/>
<label htmlFor='destroyCloudConfigDisk'>
&nbsp;
{_('destroyCloudConfigVdiAfterBoot')}
</label>
</Item>
</SectionContent>,
this._getResourceSet() !== undefined && ( this._getResourceSet() !== undefined && (
<SectionContent> <SectionContent>
<Item> <Item>
@ -1563,7 +1582,7 @@ export default class NewVm extends BaseComponent {
</Item> </Item>
</SectionContent> </SectionContent>
), ),
<SectionContent> <SectionContent key='newVmCpu'>
<Item label={_('newVmCpuWeightLabel')}> <Item label={_('newVmCpuWeightLabel')}>
<DebounceInput <DebounceInput
className='form-control' className='form-control'
@ -1598,7 +1617,7 @@ export default class NewVm extends BaseComponent {
/> />
</Item> </Item>
</SectionContent>, </SectionContent>,
<SectionContent> <SectionContent key='newVmDynamic'>
<Item label={_('newVmDynamicMinLabel')}> <Item label={_('newVmDynamicMinLabel')}>
<SizeInput <SizeInput
value={defined(memoryDynamicMin, null)} value={defined(memoryDynamicMin, null)}
@ -1621,7 +1640,7 @@ export default class NewVm extends BaseComponent {
/> />
</Item> </Item>
</SectionContent>, </SectionContent>,
<SectionContent> <SectionContent key='newVmMultipleVms'>
<Item label={_('newVmMultipleVms')}> <Item label={_('newVmMultipleVms')}>
<Toggle value={multipleVms} onChange={this._linkState('multipleVms')} /> <Toggle value={multipleVms} onChange={this._linkState('multipleVms')} />
</Item> </Item>