feat(xo-web/new-vm): possibility to destroy cloud config drive after first boot (#6486)
Fixes #6438
This commit is contained in:
parent
b63c4a0d4f
commit
2eb3b15930
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
|
||||||
await this.call('host.call_plugin', host.$ref, 'xscontainer', 'create_config_drive', {
|
// See https://github.com/xenserver/xscontainer/blob/master/src/scripts/xscontainer-pluginexample
|
||||||
vmuuid: vm.uuid,
|
const vdiUuid = (
|
||||||
sruuid: sr.uuid,
|
await this.call('host.call_plugin', host.$ref, 'xscontainer', 'create_config_drive', {
|
||||||
configuration: config,
|
vmuuid: vm.uuid,
|
||||||
})
|
sruuid: sr.uuid,
|
||||||
|
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)
|
||||||
|
@ -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)
|
||||||
},
|
},
|
||||||
|
@ -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',
|
||||||
|
@ -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' />
|
||||||
|
|
||||||
@ -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'>
|
||||||
|
|
||||||
|
{_('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>
|
||||||
|
Loading…
Reference in New Issue
Block a user