Skip to content

Commit adccdf2

Browse files
authored
UI support for deploy a VM from volume/snapshot (#11164)
* UI support for deploy a virtual machine with existing volume or a disk snapshot
1 parent 5aa1518 commit adccdf2

File tree

3 files changed

+217
-6
lines changed

3 files changed

+217
-6
lines changed

ui/public/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3711,6 +3711,7 @@
37113711
"message.shared.network.unsupported.for.nsx": "Shared networks aren't supported for NSX enabled Zones",
37123712
"message.shutdown.triggered": "A shutdown has been triggered. CloudStack will not accept new jobs",
37133713
"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",
3714+
"message.snapshot.desc": "Snapshot to create a ROOT disk from",
37143715
"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.",
37153716
"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.",
37163717
"message.specify.tag.key": "Please specify a tag key.",
@@ -3888,7 +3889,7 @@
38883889
"message.template.arch": "Please select a Template architecture.",
38893890
"message.template.desc": "OS image that can be used to boot Instances.",
38903891
"message.template.import.vm.temporary": "If a temporary Template is used, the reset Instance operation will not work after importing it.",
3891-
"message.template.iso": "Please select a Template or ISO to continue.",
3892+
"message.template.iso": "Please select a Template, ISO, volume or a snapshot to continue.",
38923893
"message.template.type.change.warning": "WARNING: Changing the Template type to SYSTEM will disable further changes to the Template.",
38933894
"message.tooltip.reserved.system.netmask": "The Network prefix that defines the Pod subnet. Uses CIDR notation.",
38943895
"message.traffic.type.deleted": "Successfully deleted traffic type",
@@ -3963,6 +3964,7 @@
39633964
"message.vnf.nic.move.down.fail": "Failed to move down this NIC",
39643965
"message.vnf.no.credentials": "No credentials found for the VNF appliance.",
39653966
"message.vnf.select.networks": "Please select the relevant network for each VNF NIC.",
3967+
"message.volume.desc": "Volume to use as a ROOT disk",
39663968
"message.volume.state.allocated": "The volume is allocated but has not been created yet.",
39673969
"message.volume.state.attaching": "The volume is attaching to a volume from Ready state.",
39683970
"message.volume.state.copying": "The volume is being copied from the image store to primary storage, in case it's an uploaded volume.",

ui/src/views/compute/DeployVM.vue

Lines changed: 212 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,9 @@
133133
:guestOsCategories="options.guestOsCategories"
134134
:guestOsCategoriesLoading="loading.guestOsCategories"
135135
:selectedGuestOsCategoryId="form.guestoscategoryid"
136-
:imageItems="imageType === 'isoid' ? options.isos : options.templates"
137-
:imagesLoading="imageType === 'isoid' ? loading.isos : loading.templates"
138-
:diskSizeSelectionAllowed="imageType !== 'isoid'"
136+
:imageItems="imageType === 'isoid' ? options.isos : imageType === 'volumeid' ? options.volumes : imageType === 'snapshotid' ? options.snapshots : options.templates"
137+
:imagesLoading="imageType === 'isoid' ? loading.isos : imageType === 'volumeid' ? loading.volumes : imageType === 'snapshotid' ? loading.snapshots : loading.templates"
138+
:diskSizeSelectionAllowed="imageType !== 'isoid' && imageType !== 'volumeid' && imageType !== 'snapshotid'"
139139
:diskSizeSelectionDeployAsIsMessageVisible="template && template.deployasis"
140140
:rootDiskOverrideDisabled="rootDiskSizeFixed > 0 || (template && template.deployasis) || showOverrideDiskOfferingOption"
141141
:rootDiskOverrideChecked="form.rootdisksizeitem"
@@ -211,6 +211,12 @@
211211
<a-form-item class="form-item-hidden">
212212
<a-input v-model:value="form.isoid" />
213213
</a-form-item>
214+
<a-form-item class="form-item-hidden">
215+
<a-input v-model:value="form.volumeid" />
216+
</a-form-item>
217+
<a-form-item class="form-item-hidden">
218+
<a-input v-model:value="form.snapshotid" />
219+
</a-form-item>
214220
<a-form-item class="form-item-hidden">
215221
<a-input v-model:value="form.rootdisksize" />
216222
</a-form-item>
@@ -997,6 +1003,8 @@ export default {
9971003
},
9981004
options: {
9991005
guestOsCategories: [],
1006+
volumes: {},
1007+
snapshots: {},
10001008
templates: {},
10011009
isos: {},
10021010
hypervisors: [],
@@ -1020,6 +1028,8 @@ export default {
10201028
loading: {
10211029
deploy: false,
10221030
guestOsCategories: false,
1031+
volumes: false,
1032+
snapshots: false,
10231033
templates: false,
10241034
isos: false,
10251035
hypervisors: false,
@@ -1400,6 +1410,12 @@ export default {
14001410
queryArchId () {
14011411
return this.$route.query.arch || null
14021412
},
1413+
querySnapshotId () {
1414+
return this.$route.query.snapshotid || null
1415+
},
1416+
queryVolumeId () {
1417+
return this.$route.query.volumeid || null
1418+
},
14031419
queryTemplateId () {
14041420
return this.$route.query.templateid || null
14051421
},
@@ -1492,6 +1508,9 @@ export default {
14921508
return this.$config.showAllCategoryForModernImageSelection
14931509
},
14941510
guestOsCategoriesSelectionDisallowed () {
1511+
if (this.imageType === 'volumeid' || this.imageType === 'snapshotid') {
1512+
return true
1513+
}
14951514
return (!this.queryGuestOsCategoryId || this.options.guestOsCategories.length === 0) && (!!this.queryTemplateId || !!this.queryIsoId)
14961515
},
14971516
isTemplateHypervisorExternal () {
@@ -1955,6 +1974,8 @@ export default {
19551974
this.imageType = 'templateid'
19561975
this.form.templateid = value
19571976
this.form.isoid = null
1977+
this.form.volumeid = null
1978+
this.form.snapshotid = null
19581979
this.resetFromTemplateConfiguration()
19591980
let template = ''
19601981
for (const entry of Object.values(this.options.templates)) {
@@ -1991,6 +2012,8 @@ export default {
19912012
this.resetFromTemplateConfiguration()
19922013
this.form.isoid = value
19932014
this.form.templateid = null
2015+
this.form.volumeid = null
2016+
this.form.snapshotid = null
19942017
let iso = null
19952018
for (const entry of Object.values(this.options.isos)) {
19962019
iso = entry?.iso.find(option => option.id === value)
@@ -2003,13 +2026,59 @@ export default {
20032026
this.updateTemplateLinkedUserData(this.iso.userdataid)
20042027
this.userdataDefaultOverridePolicy = this.iso.userdatapolicy
20052028
}
2029+
} else if (name === 'volumeid') {
2030+
this.updateFieldValueForVolume(value)
2031+
} else if (name === 'snapshotid') {
2032+
this.updateFieldValueForSnapshot(value)
20062033
} else if (['cpuspeed', 'cpunumber', 'memory'].includes(name)) {
20072034
this.vm[name] = value
20082035
this.form[name] = value
20092036
} else {
20102037
this.form[name] = value
20112038
}
20122039
},
2040+
updateFieldValueForVolume (value) {
2041+
this.imageType = 'volumeid'
2042+
this.resetTemplateAssociatedResources()
2043+
this.resetFromTemplateConfiguration()
2044+
this.form.templateid = null
2045+
this.form.isoid = null
2046+
this.form.volumeid = value
2047+
this.form.snapshotid = null
2048+
let volume = null
2049+
for (const entry of Object.values(this.options.volumes)) {
2050+
volume = entry?.volume.find(option => option.id === value)
2051+
if (volume) {
2052+
this.volume = volume
2053+
break
2054+
}
2055+
}
2056+
if (volume) {
2057+
this.updateTemplateLinkedUserData(this.volume.userdataid)
2058+
this.userdataDefaultOverridePolicy = this.volume.userdatapolicy
2059+
}
2060+
},
2061+
updateFieldValueForSnapshot (value) {
2062+
this.imageType = 'snapshotid'
2063+
this.resetTemplateAssociatedResources()
2064+
this.resetFromTemplateConfiguration()
2065+
this.form.templateid = null
2066+
this.form.isoid = null
2067+
this.form.volumeid = null
2068+
this.form.snapshotid = value
2069+
let snapshot = null
2070+
for (const entry of Object.values(this.options.snapshots)) {
2071+
snapshot = entry?.snapshot.find(option => option.id === value)
2072+
if (snapshot) {
2073+
this.snapshot = snapshot
2074+
break
2075+
}
2076+
}
2077+
if (snapshot) {
2078+
this.updateTemplateLinkedUserData(this.snapshot.userdataid)
2079+
this.userdataDefaultOverridePolicy = this.snapshot.userdatapolicy
2080+
}
2081+
},
20132082
updateComputeOffering (id) {
20142083
this.form.computeofferingid = id
20152084
setTimeout(() => {
@@ -2171,7 +2240,7 @@ export default {
21712240
if (this.loading.deploy) return
21722241
this.formRef.value.validate().then(async () => {
21732242
const values = toRaw(this.form)
2174-
if (!values.templateid && !values.isoid) {
2243+
if (!values.templateid && !values.isoid && !values.volumeid && !values.snapshotid) {
21752244
this.$notification.error({
21762245
message: this.$t('message.request.failed'),
21772246
description: this.$t('message.template.iso')
@@ -2227,6 +2296,10 @@ export default {
22272296
if (this.imageType === 'templateid') {
22282297
deployVmData.templateid = values.templateid
22292298
values.hypervisor = null
2299+
} else if (this.imageType === 'volumeid') {
2300+
deployVmData.volumeid = values.volumeid
2301+
} else if (this.imageType === 'snapshotid') {
2302+
deployVmData.snapshotid = values.snapshotid
22302303
} else {
22312304
deployVmData.templateid = values.isoid
22322305
}
@@ -2599,6 +2672,88 @@ export default {
25992672
})
26002673
})
26012674
},
2675+
fetchUnattachedVolumes (volumeFilter, params) {
2676+
const args = Object.assign({}, params)
2677+
if (args.keyword || (args.category && args.category !== volumeFilter)) {
2678+
args.page = 1
2679+
args.pageSize = args.pageSize || 10
2680+
}
2681+
args.zoneid = _.get(this.zone, 'id')
2682+
if (this.isZoneSelectedMultiArch) {
2683+
args.arch = this.selectedArchitecture
2684+
}
2685+
args.account = store.getters.project?.id ? null : this.owner.account
2686+
args.domainid = store.getters.project?.id ? null : this.owner.domainid
2687+
args.projectid = store.getters.project?.id || this.owner.projectid
2688+
args.id = this.queryVolumeId
2689+
args.state = 'Ready'
2690+
const pageSize = args.pageSize ? args.pageSize : 10
2691+
const pageStart = (args.page ? args.page - 1 : 0) * pageSize
2692+
const pageEnd = pageSize * (pageStart + 1)
2693+
2694+
delete args.category
2695+
delete args.public
2696+
delete args.featured
2697+
delete args.page
2698+
delete args.pageSize
2699+
2700+
return new Promise((resolve, reject) => {
2701+
getAPI('listVolumes', args).then((response) => {
2702+
let count = 0
2703+
const volumes = []
2704+
response.listvolumesresponse.volume.forEach(volume => {
2705+
if (!volume.virtualmachineid) {
2706+
count += 1
2707+
volumes.push({ ...volume, displaytext: volume.name })
2708+
}
2709+
})
2710+
resolve({ listvolumesresponse: { count, volume: volumes.slice(pageStart, pageEnd) } })
2711+
}).catch((reason) => {
2712+
// ToDo: Handle errors
2713+
reject(reason)
2714+
})
2715+
})
2716+
},
2717+
fetchRootSnapshots (snapshotFilter, params) {
2718+
const args = Object.assign({}, params)
2719+
if (args.keyword || (args.category && args.category !== snapshotFilter)) {
2720+
args.page = 1
2721+
args.pageSize = args.pageSize || 10
2722+
}
2723+
args.zoneid = _.get(this.zone, 'id')
2724+
if (this.isZoneSelectedMultiArch) {
2725+
args.arch = this.selectedArchitecture
2726+
}
2727+
args.account = store.getters.project?.id ? null : this.owner.account
2728+
args.domainid = store.getters.project?.id ? null : this.owner.domainid
2729+
args.projectid = store.getters.project?.id || this.owner.projectid
2730+
const pageSize = args.pageSize ? args.pageSize : 10
2731+
const pageStart = (args.page ? args.page - 1 : 0) * pageSize
2732+
const pageEnd = pageSize * (pageStart + 1)
2733+
2734+
delete args.category
2735+
delete args.public
2736+
delete args.featured
2737+
delete args.page
2738+
delete args.pageSize
2739+
2740+
return new Promise((resolve, reject) => {
2741+
getAPI('listSnapshots', args).then((response) => {
2742+
let count = 0
2743+
const snapshots = []
2744+
response.listsnapshotsresponse.snapshot.forEach(snapshot => {
2745+
if (snapshot.volumetype === 'ROOT') {
2746+
count += 1
2747+
snapshots.push({ ...snapshot, displaytext: snapshot.name })
2748+
}
2749+
})
2750+
resolve({ listsnapshotsresponse: { count, snapshot: snapshots.slice(pageStart, pageEnd) } })
2751+
}).catch((reason) => {
2752+
// ToDo: Handle errors
2753+
reject(reason)
2754+
})
2755+
})
2756+
},
26022757
fetchTemplates (templateFilter, params) {
26032758
const args = Object.assign({}, params)
26042759
if (this.isModernImageSelection && this.form.guestoscategoryid && !['-1', '0'].includes(this.form.guestoscategoryid)) {
@@ -2678,6 +2833,14 @@ export default {
26782833
this.fetchAllIsos(params)
26792834
return
26802835
}
2836+
if (this.imageType === 'volumeid') {
2837+
this.fetchAllVolumes(params)
2838+
return
2839+
}
2840+
if (this.imageType === 'snapshotid') {
2841+
this.fetchAllSnapshots(params)
2842+
return
2843+
}
26812844
this.fetchAllTemplates(params)
26822845
},
26832846
fetchAllTemplates (params) {
@@ -2724,6 +2887,50 @@ export default {
27242887
this.loading.isos = false
27252888
})
27262889
},
2890+
fetchAllVolumes (params) {
2891+
const promises = []
2892+
const volumes = {}
2893+
this.loading.volumes = true
2894+
this.imageSearchFilters = params
2895+
const volumeFilters = this.getImageFilters(params)
2896+
volumeFilters.forEach((filter) => {
2897+
volumes[filter] = { count: 0, volume: [] }
2898+
promises.push(this.fetchUnattachedVolumes(filter, params))
2899+
})
2900+
this.options.volumes = volumes
2901+
Promise.all(promises).then((response) => {
2902+
response.forEach((resItem, idx) => {
2903+
volumes[volumeFilters[idx]] = _.isEmpty(resItem.listvolumesresponse) ? { count: 0, volume: [] } : resItem.listvolumesresponse
2904+
this.options.volumes = { ...volumes }
2905+
})
2906+
}).catch((reason) => {
2907+
console.log(reason)
2908+
}).finally(() => {
2909+
this.loading.volumes = false
2910+
})
2911+
},
2912+
fetchAllSnapshots (params) {
2913+
const promises = []
2914+
const snapshots = {}
2915+
this.loading.snapshots = true
2916+
this.imageSearchFilters = params
2917+
const snapshotFilters = this.getImageFilters(params)
2918+
snapshotFilters.forEach((filter) => {
2919+
snapshots[filter] = { count: 0, snapshot: [] }
2920+
promises.push(this.fetchRootSnapshots(filter, params))
2921+
})
2922+
this.options.snapshots = snapshots
2923+
Promise.all(promises).then((response) => {
2924+
response.forEach((resItem, idx) => {
2925+
snapshots[snapshotFilters[idx]] = _.isEmpty(resItem.listsnapshotsresponse) ? { count: 0, snapshot: [] } : resItem.listsnapshotsresponse
2926+
this.options.snapshots = { ...snapshots }
2927+
})
2928+
}).catch((reason) => {
2929+
console.log(reason)
2930+
}).finally(() => {
2931+
this.loading.snapshots = false
2932+
})
2933+
},
27272934
filterOption (input, option) {
27282935
return option.label.toUpperCase().indexOf(input.toUpperCase()) >= 0
27292936
},
@@ -2830,7 +3037,7 @@ export default {
28303037
}
28313038
},
28323039
updateImages () {
2833-
if (this.isModernImageSelection) {
3040+
if (this.isModernImageSelection && this.imageType !== 'snapshotid' && this.imageType !== 'volumeid') {
28343041
this.fetchGuestOsCategories()
28353042
return
28363043
}

ui/src/views/compute/wizard/OsBasedImageSelection.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
@change="emitChangeImageType()">
2626
<a-radio-button value="templateid">{{ $t('label.template') }}</a-radio-button>
2727
<a-radio-button value="isoid">{{ $t('label.iso') }}</a-radio-button>
28+
<a-radio-button value="volumeid">{{ $t('label.volume') }}</a-radio-button>
29+
<a-radio-button value="snapshotid">{{ $t('label.snapshot') }}</a-radio-button>
2830
</a-radio-group>
2931
<div style="margin-top: 5px; margin-bottom: 5px;">
3032
{{ $t('message.' + localSelectedImageType.replace('id', '') + '.desc') }}

0 commit comments

Comments
 (0)