mirror of https://github.com/apache/cloudstack.git
UI support for deploy a VM from volume/snapshot (#11164)
* UI support for deploy a virtual machine with existing volume or a disk snapshot
This commit is contained in:
parent
5aa15187b6
commit
adccdf2c7b
|
|
@ -3711,6 +3711,7 @@
|
|||
"message.shared.network.unsupported.for.nsx": "Shared networks aren't supported for NSX enabled Zones",
|
||||
"message.shutdown.triggered": "A shutdown has been triggered. CloudStack will not accept new jobs",
|
||||
"message.snapshot.additional.zones": "Snapshots will always be created in its native Zone - %x, here you can select additional zone(s) where it will be copied to at creation time",
|
||||
"message.snapshot.desc": "Snapshot to create a ROOT disk from",
|
||||
"message.sourcenatip.change.warning": "WARNING: Changing the sourcenat IP address of the network will cause connectivity downtime for the Instances with NICs in the Network.",
|
||||
"message.sourcenatip.change.inhibited": "Changing the sourcenat to this IP of the Network to this address is inhibited as firewall rules are defined for it. This can include port forwarding or load balancing rules.\n - If this is an Isolated Network, please use updateNetwork/click the edit button.\n - If this is a VPC, first clear all other rules for this address.",
|
||||
"message.specify.tag.key": "Please specify a tag key.",
|
||||
|
|
@ -3888,7 +3889,7 @@
|
|||
"message.template.arch": "Please select a Template architecture.",
|
||||
"message.template.desc": "OS image that can be used to boot Instances.",
|
||||
"message.template.import.vm.temporary": "If a temporary Template is used, the reset Instance operation will not work after importing it.",
|
||||
"message.template.iso": "Please select a Template or ISO to continue.",
|
||||
"message.template.iso": "Please select a Template, ISO, volume or a snapshot to continue.",
|
||||
"message.template.type.change.warning": "WARNING: Changing the Template type to SYSTEM will disable further changes to the Template.",
|
||||
"message.tooltip.reserved.system.netmask": "The Network prefix that defines the Pod subnet. Uses CIDR notation.",
|
||||
"message.traffic.type.deleted": "Successfully deleted traffic type",
|
||||
|
|
@ -3963,6 +3964,7 @@
|
|||
"message.vnf.nic.move.down.fail": "Failed to move down this NIC",
|
||||
"message.vnf.no.credentials": "No credentials found for the VNF appliance.",
|
||||
"message.vnf.select.networks": "Please select the relevant network for each VNF NIC.",
|
||||
"message.volume.desc": "Volume to use as a ROOT disk",
|
||||
"message.volume.state.allocated": "The volume is allocated but has not been created yet.",
|
||||
"message.volume.state.attaching": "The volume is attaching to a volume from Ready state.",
|
||||
"message.volume.state.copying": "The volume is being copied from the image store to primary storage, in case it's an uploaded volume.",
|
||||
|
|
|
|||
|
|
@ -133,9 +133,9 @@
|
|||
:guestOsCategories="options.guestOsCategories"
|
||||
:guestOsCategoriesLoading="loading.guestOsCategories"
|
||||
:selectedGuestOsCategoryId="form.guestoscategoryid"
|
||||
:imageItems="imageType === 'isoid' ? options.isos : options.templates"
|
||||
:imagesLoading="imageType === 'isoid' ? loading.isos : loading.templates"
|
||||
:diskSizeSelectionAllowed="imageType !== 'isoid'"
|
||||
:imageItems="imageType === 'isoid' ? options.isos : imageType === 'volumeid' ? options.volumes : imageType === 'snapshotid' ? options.snapshots : options.templates"
|
||||
:imagesLoading="imageType === 'isoid' ? loading.isos : imageType === 'volumeid' ? loading.volumes : imageType === 'snapshotid' ? loading.snapshots : loading.templates"
|
||||
:diskSizeSelectionAllowed="imageType !== 'isoid' && imageType !== 'volumeid' && imageType !== 'snapshotid'"
|
||||
:diskSizeSelectionDeployAsIsMessageVisible="template && template.deployasis"
|
||||
:rootDiskOverrideDisabled="rootDiskSizeFixed > 0 || (template && template.deployasis) || showOverrideDiskOfferingOption"
|
||||
:rootDiskOverrideChecked="form.rootdisksizeitem"
|
||||
|
|
@ -211,6 +211,12 @@
|
|||
<a-form-item class="form-item-hidden">
|
||||
<a-input v-model:value="form.isoid" />
|
||||
</a-form-item>
|
||||
<a-form-item class="form-item-hidden">
|
||||
<a-input v-model:value="form.volumeid" />
|
||||
</a-form-item>
|
||||
<a-form-item class="form-item-hidden">
|
||||
<a-input v-model:value="form.snapshotid" />
|
||||
</a-form-item>
|
||||
<a-form-item class="form-item-hidden">
|
||||
<a-input v-model:value="form.rootdisksize" />
|
||||
</a-form-item>
|
||||
|
|
@ -997,6 +1003,8 @@ export default {
|
|||
},
|
||||
options: {
|
||||
guestOsCategories: [],
|
||||
volumes: {},
|
||||
snapshots: {},
|
||||
templates: {},
|
||||
isos: {},
|
||||
hypervisors: [],
|
||||
|
|
@ -1020,6 +1028,8 @@ export default {
|
|||
loading: {
|
||||
deploy: false,
|
||||
guestOsCategories: false,
|
||||
volumes: false,
|
||||
snapshots: false,
|
||||
templates: false,
|
||||
isos: false,
|
||||
hypervisors: false,
|
||||
|
|
@ -1400,6 +1410,12 @@ export default {
|
|||
queryArchId () {
|
||||
return this.$route.query.arch || null
|
||||
},
|
||||
querySnapshotId () {
|
||||
return this.$route.query.snapshotid || null
|
||||
},
|
||||
queryVolumeId () {
|
||||
return this.$route.query.volumeid || null
|
||||
},
|
||||
queryTemplateId () {
|
||||
return this.$route.query.templateid || null
|
||||
},
|
||||
|
|
@ -1492,6 +1508,9 @@ export default {
|
|||
return this.$config.showAllCategoryForModernImageSelection
|
||||
},
|
||||
guestOsCategoriesSelectionDisallowed () {
|
||||
if (this.imageType === 'volumeid' || this.imageType === 'snapshotid') {
|
||||
return true
|
||||
}
|
||||
return (!this.queryGuestOsCategoryId || this.options.guestOsCategories.length === 0) && (!!this.queryTemplateId || !!this.queryIsoId)
|
||||
},
|
||||
isTemplateHypervisorExternal () {
|
||||
|
|
@ -1955,6 +1974,8 @@ export default {
|
|||
this.imageType = 'templateid'
|
||||
this.form.templateid = value
|
||||
this.form.isoid = null
|
||||
this.form.volumeid = null
|
||||
this.form.snapshotid = null
|
||||
this.resetFromTemplateConfiguration()
|
||||
let template = ''
|
||||
for (const entry of Object.values(this.options.templates)) {
|
||||
|
|
@ -1991,6 +2012,8 @@ export default {
|
|||
this.resetFromTemplateConfiguration()
|
||||
this.form.isoid = value
|
||||
this.form.templateid = null
|
||||
this.form.volumeid = null
|
||||
this.form.snapshotid = null
|
||||
let iso = null
|
||||
for (const entry of Object.values(this.options.isos)) {
|
||||
iso = entry?.iso.find(option => option.id === value)
|
||||
|
|
@ -2003,6 +2026,10 @@ export default {
|
|||
this.updateTemplateLinkedUserData(this.iso.userdataid)
|
||||
this.userdataDefaultOverridePolicy = this.iso.userdatapolicy
|
||||
}
|
||||
} else if (name === 'volumeid') {
|
||||
this.updateFieldValueForVolume(value)
|
||||
} else if (name === 'snapshotid') {
|
||||
this.updateFieldValueForSnapshot(value)
|
||||
} else if (['cpuspeed', 'cpunumber', 'memory'].includes(name)) {
|
||||
this.vm[name] = value
|
||||
this.form[name] = value
|
||||
|
|
@ -2010,6 +2037,48 @@ export default {
|
|||
this.form[name] = value
|
||||
}
|
||||
},
|
||||
updateFieldValueForVolume (value) {
|
||||
this.imageType = 'volumeid'
|
||||
this.resetTemplateAssociatedResources()
|
||||
this.resetFromTemplateConfiguration()
|
||||
this.form.templateid = null
|
||||
this.form.isoid = null
|
||||
this.form.volumeid = value
|
||||
this.form.snapshotid = null
|
||||
let volume = null
|
||||
for (const entry of Object.values(this.options.volumes)) {
|
||||
volume = entry?.volume.find(option => option.id === value)
|
||||
if (volume) {
|
||||
this.volume = volume
|
||||
break
|
||||
}
|
||||
}
|
||||
if (volume) {
|
||||
this.updateTemplateLinkedUserData(this.volume.userdataid)
|
||||
this.userdataDefaultOverridePolicy = this.volume.userdatapolicy
|
||||
}
|
||||
},
|
||||
updateFieldValueForSnapshot (value) {
|
||||
this.imageType = 'snapshotid'
|
||||
this.resetTemplateAssociatedResources()
|
||||
this.resetFromTemplateConfiguration()
|
||||
this.form.templateid = null
|
||||
this.form.isoid = null
|
||||
this.form.volumeid = null
|
||||
this.form.snapshotid = value
|
||||
let snapshot = null
|
||||
for (const entry of Object.values(this.options.snapshots)) {
|
||||
snapshot = entry?.snapshot.find(option => option.id === value)
|
||||
if (snapshot) {
|
||||
this.snapshot = snapshot
|
||||
break
|
||||
}
|
||||
}
|
||||
if (snapshot) {
|
||||
this.updateTemplateLinkedUserData(this.snapshot.userdataid)
|
||||
this.userdataDefaultOverridePolicy = this.snapshot.userdatapolicy
|
||||
}
|
||||
},
|
||||
updateComputeOffering (id) {
|
||||
this.form.computeofferingid = id
|
||||
setTimeout(() => {
|
||||
|
|
@ -2171,7 +2240,7 @@ export default {
|
|||
if (this.loading.deploy) return
|
||||
this.formRef.value.validate().then(async () => {
|
||||
const values = toRaw(this.form)
|
||||
if (!values.templateid && !values.isoid) {
|
||||
if (!values.templateid && !values.isoid && !values.volumeid && !values.snapshotid) {
|
||||
this.$notification.error({
|
||||
message: this.$t('message.request.failed'),
|
||||
description: this.$t('message.template.iso')
|
||||
|
|
@ -2227,6 +2296,10 @@ export default {
|
|||
if (this.imageType === 'templateid') {
|
||||
deployVmData.templateid = values.templateid
|
||||
values.hypervisor = null
|
||||
} else if (this.imageType === 'volumeid') {
|
||||
deployVmData.volumeid = values.volumeid
|
||||
} else if (this.imageType === 'snapshotid') {
|
||||
deployVmData.snapshotid = values.snapshotid
|
||||
} else {
|
||||
deployVmData.templateid = values.isoid
|
||||
}
|
||||
|
|
@ -2599,6 +2672,88 @@ export default {
|
|||
})
|
||||
})
|
||||
},
|
||||
fetchUnattachedVolumes (volumeFilter, params) {
|
||||
const args = Object.assign({}, params)
|
||||
if (args.keyword || (args.category && args.category !== volumeFilter)) {
|
||||
args.page = 1
|
||||
args.pageSize = args.pageSize || 10
|
||||
}
|
||||
args.zoneid = _.get(this.zone, 'id')
|
||||
if (this.isZoneSelectedMultiArch) {
|
||||
args.arch = this.selectedArchitecture
|
||||
}
|
||||
args.account = store.getters.project?.id ? null : this.owner.account
|
||||
args.domainid = store.getters.project?.id ? null : this.owner.domainid
|
||||
args.projectid = store.getters.project?.id || this.owner.projectid
|
||||
args.id = this.queryVolumeId
|
||||
args.state = 'Ready'
|
||||
const pageSize = args.pageSize ? args.pageSize : 10
|
||||
const pageStart = (args.page ? args.page - 1 : 0) * pageSize
|
||||
const pageEnd = pageSize * (pageStart + 1)
|
||||
|
||||
delete args.category
|
||||
delete args.public
|
||||
delete args.featured
|
||||
delete args.page
|
||||
delete args.pageSize
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getAPI('listVolumes', args).then((response) => {
|
||||
let count = 0
|
||||
const volumes = []
|
||||
response.listvolumesresponse.volume.forEach(volume => {
|
||||
if (!volume.virtualmachineid) {
|
||||
count += 1
|
||||
volumes.push({ ...volume, displaytext: volume.name })
|
||||
}
|
||||
})
|
||||
resolve({ listvolumesresponse: { count, volume: volumes.slice(pageStart, pageEnd) } })
|
||||
}).catch((reason) => {
|
||||
// ToDo: Handle errors
|
||||
reject(reason)
|
||||
})
|
||||
})
|
||||
},
|
||||
fetchRootSnapshots (snapshotFilter, params) {
|
||||
const args = Object.assign({}, params)
|
||||
if (args.keyword || (args.category && args.category !== snapshotFilter)) {
|
||||
args.page = 1
|
||||
args.pageSize = args.pageSize || 10
|
||||
}
|
||||
args.zoneid = _.get(this.zone, 'id')
|
||||
if (this.isZoneSelectedMultiArch) {
|
||||
args.arch = this.selectedArchitecture
|
||||
}
|
||||
args.account = store.getters.project?.id ? null : this.owner.account
|
||||
args.domainid = store.getters.project?.id ? null : this.owner.domainid
|
||||
args.projectid = store.getters.project?.id || this.owner.projectid
|
||||
const pageSize = args.pageSize ? args.pageSize : 10
|
||||
const pageStart = (args.page ? args.page - 1 : 0) * pageSize
|
||||
const pageEnd = pageSize * (pageStart + 1)
|
||||
|
||||
delete args.category
|
||||
delete args.public
|
||||
delete args.featured
|
||||
delete args.page
|
||||
delete args.pageSize
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getAPI('listSnapshots', args).then((response) => {
|
||||
let count = 0
|
||||
const snapshots = []
|
||||
response.listsnapshotsresponse.snapshot.forEach(snapshot => {
|
||||
if (snapshot.volumetype === 'ROOT') {
|
||||
count += 1
|
||||
snapshots.push({ ...snapshot, displaytext: snapshot.name })
|
||||
}
|
||||
})
|
||||
resolve({ listsnapshotsresponse: { count, snapshot: snapshots.slice(pageStart, pageEnd) } })
|
||||
}).catch((reason) => {
|
||||
// ToDo: Handle errors
|
||||
reject(reason)
|
||||
})
|
||||
})
|
||||
},
|
||||
fetchTemplates (templateFilter, params) {
|
||||
const args = Object.assign({}, params)
|
||||
if (this.isModernImageSelection && this.form.guestoscategoryid && !['-1', '0'].includes(this.form.guestoscategoryid)) {
|
||||
|
|
@ -2678,6 +2833,14 @@ export default {
|
|||
this.fetchAllIsos(params)
|
||||
return
|
||||
}
|
||||
if (this.imageType === 'volumeid') {
|
||||
this.fetchAllVolumes(params)
|
||||
return
|
||||
}
|
||||
if (this.imageType === 'snapshotid') {
|
||||
this.fetchAllSnapshots(params)
|
||||
return
|
||||
}
|
||||
this.fetchAllTemplates(params)
|
||||
},
|
||||
fetchAllTemplates (params) {
|
||||
|
|
@ -2724,6 +2887,50 @@ export default {
|
|||
this.loading.isos = false
|
||||
})
|
||||
},
|
||||
fetchAllVolumes (params) {
|
||||
const promises = []
|
||||
const volumes = {}
|
||||
this.loading.volumes = true
|
||||
this.imageSearchFilters = params
|
||||
const volumeFilters = this.getImageFilters(params)
|
||||
volumeFilters.forEach((filter) => {
|
||||
volumes[filter] = { count: 0, volume: [] }
|
||||
promises.push(this.fetchUnattachedVolumes(filter, params))
|
||||
})
|
||||
this.options.volumes = volumes
|
||||
Promise.all(promises).then((response) => {
|
||||
response.forEach((resItem, idx) => {
|
||||
volumes[volumeFilters[idx]] = _.isEmpty(resItem.listvolumesresponse) ? { count: 0, volume: [] } : resItem.listvolumesresponse
|
||||
this.options.volumes = { ...volumes }
|
||||
})
|
||||
}).catch((reason) => {
|
||||
console.log(reason)
|
||||
}).finally(() => {
|
||||
this.loading.volumes = false
|
||||
})
|
||||
},
|
||||
fetchAllSnapshots (params) {
|
||||
const promises = []
|
||||
const snapshots = {}
|
||||
this.loading.snapshots = true
|
||||
this.imageSearchFilters = params
|
||||
const snapshotFilters = this.getImageFilters(params)
|
||||
snapshotFilters.forEach((filter) => {
|
||||
snapshots[filter] = { count: 0, snapshot: [] }
|
||||
promises.push(this.fetchRootSnapshots(filter, params))
|
||||
})
|
||||
this.options.snapshots = snapshots
|
||||
Promise.all(promises).then((response) => {
|
||||
response.forEach((resItem, idx) => {
|
||||
snapshots[snapshotFilters[idx]] = _.isEmpty(resItem.listsnapshotsresponse) ? { count: 0, snapshot: [] } : resItem.listsnapshotsresponse
|
||||
this.options.snapshots = { ...snapshots }
|
||||
})
|
||||
}).catch((reason) => {
|
||||
console.log(reason)
|
||||
}).finally(() => {
|
||||
this.loading.snapshots = false
|
||||
})
|
||||
},
|
||||
filterOption (input, option) {
|
||||
return option.label.toUpperCase().indexOf(input.toUpperCase()) >= 0
|
||||
},
|
||||
|
|
@ -2830,7 +3037,7 @@ export default {
|
|||
}
|
||||
},
|
||||
updateImages () {
|
||||
if (this.isModernImageSelection) {
|
||||
if (this.isModernImageSelection && this.imageType !== 'snapshotid' && this.imageType !== 'volumeid') {
|
||||
this.fetchGuestOsCategories()
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@
|
|||
@change="emitChangeImageType()">
|
||||
<a-radio-button value="templateid">{{ $t('label.template') }}</a-radio-button>
|
||||
<a-radio-button value="isoid">{{ $t('label.iso') }}</a-radio-button>
|
||||
<a-radio-button value="volumeid">{{ $t('label.volume') }}</a-radio-button>
|
||||
<a-radio-button value="snapshotid">{{ $t('label.snapshot') }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
<div style="margin-top: 5px; margin-bottom: 5px;">
|
||||
{{ $t('message.' + localSelectedImageType.replace('id', '') + '.desc') }}
|
||||
|
|
|
|||
Loading…
Reference in New Issue