Skip to content

Commit ee01c9b

Browse files
committed
UI support for deploy VM fro volume/snapshot
ui support to deploy a virtual machine with existing volume or a disk snapshot
1 parent 76cfcb4 commit ee01c9b

File tree

3 files changed

+207
-4
lines changed

3 files changed

+207
-4
lines changed

ui/public/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3697,6 +3697,7 @@
36973697
"message.shared.network.unsupported.for.nsx": "Shared networks aren't supported for NSX enabled Zones",
36983698
"message.shutdown.triggered": "A shutdown has been triggered. CloudStack will not accept new jobs",
36993699
"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",
3700+
"message.snapshot.desc": "Snapshot to create a ROOT disk from",
37003701
"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.",
37013702
"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.",
37023703
"message.specify.tag.key": "Please specify a tag key.",
@@ -3949,6 +3950,7 @@
39493950
"message.vnf.nic.move.down.fail": "Failed to move down this NIC",
39503951
"message.vnf.no.credentials": "No credentials found for the VNF appliance.",
39513952
"message.vnf.select.networks": "Please select the relevant network for each VNF NIC.",
3953+
"message.volume.desc": "Volume to use as a ROOT disk",
39523954
"message.volume.state.allocated": "The volume is allocated but has not been created yet.",
39533955
"message.volume.state.attaching": "The volume is attaching to a volume from Ready state.",
39543956
"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: 203 additions & 4 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
},
@@ -1953,6 +1969,8 @@ export default {
19531969
this.imageType = 'templateid'
19541970
this.form.templateid = value
19551971
this.form.isoid = null
1972+
this.form.volumeid = null
1973+
this.form.snapshotid = null
19561974
this.resetFromTemplateConfiguration()
19571975
let template = ''
19581976
for (const entry of Object.values(this.options.templates)) {
@@ -1989,6 +2007,8 @@ export default {
19892007
this.resetFromTemplateConfiguration()
19902008
this.form.isoid = value
19912009
this.form.templateid = null
2010+
this.form.volumeid = null
2011+
this.form.snapshotid = null
19922012
let iso = null
19932013
for (const entry of Object.values(this.options.isos)) {
19942014
iso = entry?.iso.find(option => option.id === value)
@@ -2001,6 +2021,46 @@ export default {
20012021
this.updateTemplateLinkedUserData(this.iso.userdataid)
20022022
this.userdataDefaultOverridePolicy = this.iso.userdatapolicy
20032023
}
2024+
} else if (name === 'volumeid') {
2025+
this.imageType = 'volumeid'
2026+
this.resetTemplateAssociatedResources()
2027+
this.resetFromTemplateConfiguration()
2028+
this.form.templateid = null
2029+
this.form.isoid = null
2030+
this.form.volumeid = value
2031+
this.form.snapshotid = null
2032+
let volume = null
2033+
for (const entry of Object.values(this.options.volumes)) {
2034+
volume = entry?.volume.find(option => option.id === value)
2035+
if (volume) {
2036+
this.volume = volume
2037+
break
2038+
}
2039+
}
2040+
if (volume) {
2041+
this.updateTemplateLinkedUserData(this.volume.userdataid)
2042+
this.userdataDefaultOverridePolicy = this.volume.userdatapolicy
2043+
}
2044+
} else if (name === 'snapshotid') {
2045+
this.imageType = 'snapshotid'
2046+
this.resetTemplateAssociatedResources()
2047+
this.resetFromTemplateConfiguration()
2048+
this.form.templateid = null
2049+
this.form.isoid = null
2050+
this.form.volumeid = null
2051+
this.form.snapshotid = value
2052+
let snapshot = null
2053+
for (const entry of Object.values(this.options.snapshots)) {
2054+
snapshot = entry?.snapshot.find(option => option.id === value)
2055+
if (snapshot) {
2056+
this.snapshot = snapshot
2057+
break
2058+
}
2059+
}
2060+
if (snapshot) {
2061+
this.updateTemplateLinkedUserData(this.snapshot.userdataid)
2062+
this.userdataDefaultOverridePolicy = this.snapshot.userdatapolicy
2063+
}
20042064
} else if (['cpuspeed', 'cpunumber', 'memory'].includes(name)) {
20052065
this.vm[name] = value
20062066
this.form[name] = value
@@ -2169,7 +2229,7 @@ export default {
21692229
if (this.loading.deploy) return
21702230
this.formRef.value.validate().then(async () => {
21712231
const values = toRaw(this.form)
2172-
if (!values.templateid && !values.isoid) {
2232+
if (!values.templateid && !values.isoid && !values.volumeid && !values.snapshotid) {
21732233
this.$notification.error({
21742234
message: this.$t('message.request.failed'),
21752235
description: this.$t('message.template.iso')
@@ -2225,6 +2285,10 @@ export default {
22252285
if (this.imageType === 'templateid') {
22262286
deployVmData.templateid = values.templateid
22272287
values.hypervisor = null
2288+
} else if (this.imageType === 'volumeid') {
2289+
deployVmData.volumeid = values.volumeid
2290+
} else if (this.imageType === 'snapshotid') {
2291+
deployVmData.snapshotid = values.snapshotid
22282292
} else {
22292293
deployVmData.templateid = values.isoid
22302294
}
@@ -2597,6 +2661,89 @@ export default {
25972661
})
25982662
})
25992663
},
2664+
fetchUnattachedVolumes (volumeFilter, params) {
2665+
const args = Object.assign({}, params)
2666+
if (this.isModernImageSelection && this.form.guestoscategoryid) {
2667+
args.oscategoryid = this.form.guestoscategoryid
2668+
}
2669+
if (args.keyword || (args.category && args.category !== volumeFilter)) {
2670+
args.page = 1
2671+
args.pageSize = args.pageSize || 10
2672+
}
2673+
args.zoneid = _.get(this.zone, 'id')
2674+
if (this.isZoneSelectedMultiArch) {
2675+
args.arch = this.selectedArchitecture
2676+
}
2677+
args.account = store.getters.project?.id ? null : this.owner.account
2678+
args.domainid = store.getters.project?.id ? null : this.owner.domainid
2679+
args.projectid = store.getters.project?.id || this.owner.projectid
2680+
args.volumefilter = volumeFilter
2681+
args.details = 'all'
2682+
args.showicon = 'true'
2683+
args.id = this.queryVolumeId
2684+
args.isvnf = false
2685+
2686+
delete args.category
2687+
delete args.public
2688+
delete args.featured
2689+
2690+
return new Promise((resolve, reject) => {
2691+
getAPI('listVolumes', args).then((response) => {
2692+
const listvolumesresponse = { count: 0, volume: [] }
2693+
response.listvolumesresponse.volume.forEach(volume => {
2694+
if (!volume.virtualmachineid && volume.state === 'Ready') {
2695+
listvolumesresponse.count += 1
2696+
listvolumesresponse.volume.push({ ...volume, displaytext: volume.name })
2697+
}
2698+
})
2699+
resolve({ listvolumesresponse })
2700+
}).catch((reason) => {
2701+
// ToDo: Handle errors
2702+
reject(reason)
2703+
})
2704+
})
2705+
},
2706+
fetchRootSnapshots (snapshotFilter, params) {
2707+
const args = Object.assign({}, params)
2708+
if (this.isModernImageSelection && this.form.guestoscategoryid) {
2709+
args.oscategoryid = this.form.guestoscategoryid
2710+
}
2711+
if (args.keyword || (args.category && args.category !== snapshotFilter)) {
2712+
args.page = 1
2713+
args.pageSize = args.pageSize || 10
2714+
}
2715+
args.zoneid = _.get(this.zone, 'id')
2716+
if (this.isZoneSelectedMultiArch) {
2717+
args.arch = this.selectedArchitecture
2718+
}
2719+
args.account = store.getters.project?.id ? null : this.owner.account
2720+
args.domainid = store.getters.project?.id ? null : this.owner.domainid
2721+
args.projectid = store.getters.project?.id || this.owner.projectid
2722+
args.snapshotfilter = snapshotFilter
2723+
args.details = 'all'
2724+
args.showicon = 'true'
2725+
args.isvnf = false
2726+
2727+
delete args.category
2728+
delete args.public
2729+
delete args.featured
2730+
2731+
return new Promise((resolve, reject) => {
2732+
getAPI('listSnapshots', args).then((response) => {
2733+
const listsnapshotsresponse = { count: 0, snapshot: [] }
2734+
response.listsnapshotsresponse.snapshot.forEach(snapshot => {
2735+
if (snapshot.volumetype === 'ROOT') {
2736+
listsnapshotsresponse.count += 1
2737+
listsnapshotsresponse.snapshot.push({ ...snapshot, displaytext: snapshot.name })
2738+
}
2739+
})
2740+
resolve({ listsnapshotsresponse })
2741+
}).catch((reason) => {
2742+
// ToDo: Handle errors
2743+
reject(reason)
2744+
})
2745+
})
2746+
},
26002747
fetchTemplates (templateFilter, params) {
26012748
const args = Object.assign({}, params)
26022749
if (this.isModernImageSelection && this.form.guestoscategoryid && !['-1', '0'].includes(this.form.guestoscategoryid)) {
@@ -2674,6 +2821,14 @@ export default {
26742821
this.fetchAllIsos(params)
26752822
return
26762823
}
2824+
if (this.imageType === 'volumeid') {
2825+
this.fetchAllVolumes(params)
2826+
return
2827+
}
2828+
if (this.imageType === 'snapshotid') {
2829+
this.fetchAllSnapshots(params)
2830+
return
2831+
}
26772832
this.fetchAllTemplates(params)
26782833
},
26792834
fetchAllTemplates (params) {
@@ -2720,6 +2875,50 @@ export default {
27202875
this.loading.isos = false
27212876
})
27222877
},
2878+
fetchAllVolumes (params) {
2879+
const promises = []
2880+
const volumes = {}
2881+
this.loading.volumes = true
2882+
this.imageSearchFilters = params
2883+
const volumeFilters = this.getImageFilters(params)
2884+
volumeFilters.forEach((filter) => {
2885+
volumes[filter] = { count: 0, iso: [] }
2886+
promises.push(this.fetchUnattachedVolumes(filter, params))
2887+
})
2888+
this.options.volumes = volumes
2889+
Promise.all(promises).then((response) => {
2890+
response.forEach((resItem, idx) => {
2891+
volumes[volumeFilters[idx]] = _.isEmpty(resItem.listvolumesresponse) ? { count: 0, volume: [] } : resItem.listvolumesresponse
2892+
this.options.volumes = { ...volumes }
2893+
})
2894+
}).catch((reason) => {
2895+
console.log(reason)
2896+
}).finally(() => {
2897+
this.loading.volumes = false
2898+
})
2899+
},
2900+
fetchAllSnapshots (params) {
2901+
const promises = []
2902+
const snapshots = {}
2903+
this.loading.snapshots = true
2904+
this.imageSearchFilters = params
2905+
const snapshotFilters = this.getImageFilters(params)
2906+
snapshotFilters.forEach((filter) => {
2907+
snapshots[filter] = { count: 0, iso: [] }
2908+
promises.push(this.fetchRootSnapshots(filter, params))
2909+
})
2910+
this.options.snapshots = snapshots
2911+
Promise.all(promises).then((response) => {
2912+
response.forEach((resItem, idx) => {
2913+
snapshots[snapshotFilters[idx]] = _.isEmpty(resItem.listsnapshotsresponse) ? { count: 0, snapshot: [] } : resItem.listsnapshotsresponse
2914+
this.options.snapshots = { ...snapshots }
2915+
})
2916+
}).catch((reason) => {
2917+
console.log(reason)
2918+
}).finally(() => {
2919+
this.loading.snapshots = false
2920+
})
2921+
},
27232922
filterOption (input, option) {
27242923
return option.label.toUpperCase().indexOf(input.toUpperCase()) >= 0
27252924
},

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)