Skip to content

Commit f10ea9a

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 8e4fe1c commit f10ea9a

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
@@ -3548,6 +3548,7 @@
35483548
"message.shutdown.triggered": "Shutdown has been triggered. This Management Server will not accept new jobs",
35493549
"message.maintenance.initiated": "Maintenance has been initiated. This Management Server will not accept new jobs",
35503550
"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",
3551+
"message.snapshot.desc": "Snapshot to create a ROOT disk from",
35513552
"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.",
35523553
"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.",
35533554
"message.specify.tag.key": "Please specify a tag key.",
@@ -3783,6 +3784,7 @@
37833784
"message.vnf.nic.move.down.fail": "Failed to move down this NIC",
37843785
"message.vnf.no.credentials": "No credentials found for the VNF appliance.",
37853786
"message.vnf.select.networks": "Please select the relevant network for each VNF NIC.",
3787+
"message.volume.desc": "Volume to use as a ROOT disk",
37863788
"message.volume.state.allocated": "The volume is allocated but has not been created yet.",
37873789
"message.volume.state.attaching": "The volume is attaching to a volume from Ready state.",
37883790
"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>
@@ -979,6 +985,8 @@ export default {
979985
},
980986
options: {
981987
guestOsCategories: [],
988+
volumes: {},
989+
snapshots: {},
982990
templates: {},
983991
isos: {},
984992
hypervisors: [],
@@ -1003,6 +1011,8 @@ export default {
10031011
loading: {
10041012
deploy: false,
10051013
guestOsCategories: false,
1014+
volumes: false,
1015+
snapshots: false,
10061016
templates: false,
10071017
isos: false,
10081018
hypervisors: false,
@@ -1381,6 +1391,12 @@ export default {
13811391
queryArchId () {
13821392
return this.$route.query.arch || null
13831393
},
1394+
querySnapshotId () {
1395+
return this.$route.query.snapshotid | null
1396+
},
1397+
queryVolumeId () {
1398+
return this.$route.query.volumeid || null
1399+
},
13841400
queryTemplateId () {
13851401
return this.$route.query.templateid || null
13861402
},
@@ -1923,6 +1939,8 @@ export default {
19231939
this.imageType = 'templateid'
19241940
this.form.templateid = value
19251941
this.form.isoid = null
1942+
this.form.volumeid = null
1943+
this.form.snapshotid = null
19261944
this.resetFromTemplateConfiguration()
19271945
let template = ''
19281946
for (const entry of Object.values(this.options.templates)) {
@@ -1958,6 +1976,8 @@ export default {
19581976
this.resetFromTemplateConfiguration()
19591977
this.form.isoid = value
19601978
this.form.templateid = null
1979+
this.form.volumeid = null
1980+
this.form.snapshotid = null
19611981
let iso = null
19621982
for (const entry of Object.values(this.options.isos)) {
19631983
iso = entry?.iso.find(option => option.id === value)
@@ -1970,6 +1990,46 @@ export default {
19701990
this.updateTemplateLinkedUserData(this.iso.userdataid)
19711991
this.userdataDefaultOverridePolicy = this.iso.userdatapolicy
19721992
}
1993+
} else if (name === 'volumeid') {
1994+
this.imageType = 'volumeid'
1995+
this.resetTemplateAssociatedResources()
1996+
this.resetFromTemplateConfiguration()
1997+
this.form.templateid = null
1998+
this.form.isoid = null
1999+
this.form.volumeid = value
2000+
this.form.snapshotid = null
2001+
let volume = null
2002+
for (const entry of Object.values(this.options.volumes)) {
2003+
volume = entry?.volume.find(option => option.id === value)
2004+
if (volume) {
2005+
this.volume = volume
2006+
break
2007+
}
2008+
}
2009+
if (volume) {
2010+
this.updateTemplateLinkedUserData(this.volume.userdataid)
2011+
this.userdataDefaultOverridePolicy = this.volume.userdatapolicy
2012+
}
2013+
} else if (name === 'snapshotid') {
2014+
this.imageType = 'snapshotid'
2015+
this.resetTemplateAssociatedResources()
2016+
this.resetFromTemplateConfiguration()
2017+
this.form.templateid = null
2018+
this.form.isoid = null
2019+
this.form.volumeid = null
2020+
this.form.snapshotid = value
2021+
let snapshot = null
2022+
for (const entry of Object.values(this.options.snapshots)) {
2023+
snapshot = entry?.snapshot.find(option => option.id === value)
2024+
if (snapshot) {
2025+
this.snapshot = snapshot
2026+
break
2027+
}
2028+
}
2029+
if (snapshot) {
2030+
this.updateTemplateLinkedUserData(this.snapshot.userdataid)
2031+
this.userdataDefaultOverridePolicy = this.snapshot.userdatapolicy
2032+
}
19732033
} else if (['cpuspeed', 'cpunumber', 'memory'].includes(name)) {
19742034
this.vm[name] = value
19752035
this.form[name] = value
@@ -2147,7 +2207,7 @@ export default {
21472207
if (this.loading.deploy) return
21482208
this.formRef.value.validate().then(async () => {
21492209
const values = toRaw(this.form)
2150-
if (!values.templateid && !values.isoid) {
2210+
if (!values.templateid && !values.isoid && !values.volumeid && !values.snapshotid) {
21512211
this.$notification.error({
21522212
message: this.$t('message.request.failed'),
21532213
description: this.$t('message.template.iso')
@@ -2203,6 +2263,10 @@ export default {
22032263
if (this.imageType === 'templateid') {
22042264
deployVmData.templateid = values.templateid
22052265
values.hypervisor = null
2266+
} else if (this.imageType === 'volumeid') {
2267+
deployVmData.volumeid = values.volumeid
2268+
} else if (this.imageType === 'snapshotid') {
2269+
deployVmData.snapshotid = values.snapshotid
22062270
} else {
22072271
deployVmData.templateid = values.isoid
22082272
}
@@ -2560,6 +2624,89 @@ export default {
25602624
})
25612625
})
25622626
},
2627+
fetchUnattachedVolumes (volumeFilter, params) {
2628+
const args = Object.assign({}, params)
2629+
if (this.isModernImageSelection && this.form.guestoscategoryid) {
2630+
args.oscategoryid = this.form.guestoscategoryid
2631+
}
2632+
if (args.keyword || (args.category && args.category !== volumeFilter)) {
2633+
args.page = 1
2634+
args.pageSize = args.pageSize || 10
2635+
}
2636+
args.zoneid = _.get(this.zone, 'id')
2637+
if (this.isZoneSelectedMultiArch) {
2638+
args.arch = this.selectedArchitecture
2639+
}
2640+
args.account = store.getters.project?.id ? null : this.owner.account
2641+
args.domainid = store.getters.project?.id ? null : this.owner.domainid
2642+
args.projectid = store.getters.project?.id || this.owner.projectid
2643+
args.volumefilter = volumeFilter
2644+
args.details = 'all'
2645+
args.showicon = 'true'
2646+
args.id = this.queryVolumeId
2647+
args.isvnf = false
2648+
2649+
delete args.category
2650+
delete args.public
2651+
delete args.featured
2652+
2653+
return new Promise((resolve, reject) => {
2654+
getAPI('listVolumes', args).then((response) => {
2655+
const listvolumesresponse = { count: 0, volume: [] }
2656+
response.listvolumesresponse.volume.forEach(volume => {
2657+
if (!volume.virtualmachineid && volume.state === 'Ready') {
2658+
listvolumesresponse.count += 1
2659+
listvolumesresponse.volume.push({ ...volume, displaytext: volume.name })
2660+
}
2661+
})
2662+
resolve({ listvolumesresponse })
2663+
}).catch((reason) => {
2664+
// ToDo: Handle errors
2665+
reject(reason)
2666+
})
2667+
})
2668+
},
2669+
fetchRootSnapshots (snapshotFilter, params) {
2670+
const args = Object.assign({}, params)
2671+
if (this.isModernImageSelection && this.form.guestoscategoryid) {
2672+
args.oscategoryid = this.form.guestoscategoryid
2673+
}
2674+
if (args.keyword || (args.category && args.category !== snapshotFilter)) {
2675+
args.page = 1
2676+
args.pageSize = args.pageSize || 10
2677+
}
2678+
args.zoneid = _.get(this.zone, 'id')
2679+
if (this.isZoneSelectedMultiArch) {
2680+
args.arch = this.selectedArchitecture
2681+
}
2682+
args.account = store.getters.project?.id ? null : this.owner.account
2683+
args.domainid = store.getters.project?.id ? null : this.owner.domainid
2684+
args.projectid = store.getters.project?.id || this.owner.projectid
2685+
args.snapshotfilter = snapshotFilter
2686+
args.details = 'all'
2687+
args.showicon = 'true'
2688+
args.isvnf = false
2689+
2690+
delete args.category
2691+
delete args.public
2692+
delete args.featured
2693+
2694+
return new Promise((resolve, reject) => {
2695+
getAPI('listSnapshots', args).then((response) => {
2696+
const listsnapshotsresponse = { count: 0, snapshot: [] }
2697+
response.listsnapshotsresponse.snapshot.forEach(snapshot => {
2698+
if (snapshot.volumetype === 'ROOT') {
2699+
listsnapshotsresponse.count += 1
2700+
listsnapshotsresponse.snapshot.push({ ...snapshot, displaytext: snapshot.name })
2701+
}
2702+
})
2703+
resolve({ listsnapshotsresponse })
2704+
}).catch((reason) => {
2705+
// ToDo: Handle errors
2706+
reject(reason)
2707+
})
2708+
})
2709+
},
25632710
fetchTemplates (templateFilter, params) {
25642711
const args = Object.assign({}, params)
25652712
if (this.isModernImageSelection && this.form.guestoscategoryid && !['-1', '0'].includes(this.form.guestoscategoryid)) {
@@ -2634,6 +2781,14 @@ export default {
26342781
this.fetchAllIsos(params)
26352782
return
26362783
}
2784+
if (this.imageType === 'volumeid') {
2785+
this.fetchAllVolumes(params)
2786+
return
2787+
}
2788+
if (this.imageType === 'snapshotid') {
2789+
this.fetchAllSnapshots(params)
2790+
return
2791+
}
26372792
this.fetchAllTemplates(params)
26382793
},
26392794
fetchAllTemplates (params) {
@@ -2680,6 +2835,50 @@ export default {
26802835
this.loading.isos = false
26812836
})
26822837
},
2838+
fetchAllVolumes (params) {
2839+
const promises = []
2840+
const volumes = {}
2841+
this.loading.volumes = true
2842+
this.imageSearchFilters = params
2843+
const volumeFilters = this.getImageFilters(params)
2844+
volumeFilters.forEach((filter) => {
2845+
volumes[filter] = { count: 0, iso: [] }
2846+
promises.push(this.fetchUnattachedVolumes(filter, params))
2847+
})
2848+
this.options.volumes = volumes
2849+
Promise.all(promises).then((response) => {
2850+
response.forEach((resItem, idx) => {
2851+
volumes[volumeFilters[idx]] = _.isEmpty(resItem.listvolumesresponse) ? { count: 0, volume: [] } : resItem.listvolumesresponse
2852+
this.options.volumes = { ...volumes }
2853+
})
2854+
}).catch((reason) => {
2855+
console.log(reason)
2856+
}).finally(() => {
2857+
this.loading.volumes = false
2858+
})
2859+
},
2860+
fetchAllSnapshots (params) {
2861+
const promises = []
2862+
const snapshots = {}
2863+
this.loading.snapshots = true
2864+
this.imageSearchFilters = params
2865+
const snapshotFilters = this.getImageFilters(params)
2866+
snapshotFilters.forEach((filter) => {
2867+
snapshots[filter] = { count: 0, iso: [] }
2868+
promises.push(this.fetchRootSnapshots(filter, params))
2869+
})
2870+
this.options.snapshots = snapshots
2871+
Promise.all(promises).then((response) => {
2872+
response.forEach((resItem, idx) => {
2873+
snapshots[snapshotFilters[idx]] = _.isEmpty(resItem.listsnapshotsresponse) ? { count: 0, snapshot: [] } : resItem.listsnapshotsresponse
2874+
this.options.snapshots = { ...snapshots }
2875+
})
2876+
}).catch((reason) => {
2877+
console.log(reason)
2878+
}).finally(() => {
2879+
this.loading.snapshots = false
2880+
})
2881+
},
26832882
filterOption (input, option) {
26842883
return option.label.toUpperCase().indexOf(input.toUpperCase()) >= 0
26852884
},

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)