Skip to content

Commit dd60131

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

File tree

3 files changed

+211
-30
lines changed

3 files changed

+211
-30
lines changed

ui/public/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3842,6 +3842,7 @@
38423842
"message.shutdown.triggered": "A shutdown has been triggered. This ABLESTACK Mold Server will not accept new jobs",
38433843
"message.maintenance.initiated": "Maintenance has been initiated. This ABLESTACK Mold Server will not accept new jobs",
38443844
"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",
3845+
"message.snapshot.desc": "Snapshot to create a ROOT disk from",
38453846
"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.",
38463847
"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.",
38473848
"message.specify.tag.key": "Please specify a tag key.",
@@ -4022,7 +4023,7 @@
40224023
"message.template.arch": "Please select a Template architecture.",
40234024
"message.template.desc": "OS image that can be used to boot Instances.",
40244025
"message.template.import.vm.temporary": "If a temporary Template is used, the reset Instance operation will not work after importing it.",
4025-
"message.template.iso": "Please select a Template or ISO to continue.",
4026+
"message.template.iso": "Please select a Template, ISO, volume or a snapshot to continue.",
40264027
"message.template.type.change.warning": "WARNING: Changing the Template type to SYSTEM will disable further changes to the Template.",
40274028
"message.tier.required": "Tier is required.",
40284029
"message.tooltip.dns.1": "Name of a DNS server for use by VMs in the zone. The public IP addresses for the zone must have a route to this server.",
@@ -4105,6 +4106,7 @@
41054106
"message.vnf.nic.move.down.fail": "Failed to move down this NIC",
41064107
"message.vnf.no.credentials": "No credentials found for the VNF appliance.",
41074108
"message.vnf.select.networks": "Please select the relevant network for each VNF NIC.",
4109+
"message.volume.desc": "Volume to use as a ROOT disk",
41084110
"message.volume.state.allocated": "The volume is allocated but has not been created yet.",
41094111
"message.volume.state.attaching": "The volume is attaching to a volume from Ready state.",
41104112
"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: 206 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,9 @@
155155
:guestOsCategories="options.guestOsCategories"
156156
:guestOsCategoriesLoading="loading.guestOsCategories"
157157
:selectedGuestOsCategoryId="form.guestoscategoryid"
158-
:imageItems="imageType === 'isoid' ? options.isos : options.templates"
159-
:imagesLoading="imageType === 'isoid' ? loading.isos : loading.templates"
160-
:diskSizeSelectionAllowed="imageType !== 'isoid'"
158+
:imageItems="imageType === 'isoid' ? options.isos : imageType === 'volumeid' ? options.volumes : imageType === 'snapshotid' ? options.snapshots : options.templates"
159+
:imagesLoading="imageType === 'isoid' ? loading.isos : imageType === 'volumeid' ? loading.volumes : imageType === 'snapshotid' ? loading.snapshots : loading.templates"
160+
:diskSizeSelectionAllowed="imageType !== 'isoid' && imageType !== 'volumeid' && imageType !== 'snapshotid'"
161161
:diskSizeSelectionDeployAsIsMessageVisible="template && template.deployasis"
162162
:rootDiskOverrideDisabled="rootDiskSizeFixed > 0 || (template && template.deployasis) || showOverrideDiskOfferingOption"
163163
:rootDiskOverrideChecked="form.rootdisksizeitem"
@@ -248,6 +248,12 @@
248248
<a-form-item class="form-item-hidden">
249249
<a-input v-model:value="form.isoid" />
250250
</a-form-item>
251+
<a-form-item class="form-item-hidden">
252+
<a-input v-model:value="form.volumeid" />
253+
</a-form-item>
254+
<a-form-item class="form-item-hidden">
255+
<a-input v-model:value="form.snapshotid" />
256+
</a-form-item>
251257
<a-form-item class="form-item-hidden">
252258
<a-input v-model:value="form.rootdisksize" />
253259
</a-form-item>
@@ -1068,6 +1074,8 @@ export default {
10681074
},
10691075
options: {
10701076
guestOsCategories: [],
1077+
volumes: {},
1078+
snapshots: {},
10711079
templates: {},
10721080
isos: {},
10731081
hypervisors: [],
@@ -1095,6 +1103,8 @@ export default {
10951103
loading: {
10961104
deploy: false,
10971105
guestOsCategories: false,
1106+
volumes: false,
1107+
snapshots: false,
10981108
templates: false,
10991109
isos: false,
11001110
hypervisors: false,
@@ -1478,6 +1488,12 @@ export default {
14781488
queryArchId () {
14791489
return this.$route.query.arch || null
14801490
},
1491+
querySnapshotId () {
1492+
return this.$route.query.snapshotid || null
1493+
},
1494+
queryVolumeId () {
1495+
return this.$route.query.volumeid || null
1496+
},
14811497
queryTemplateId () {
14821498
return this.$route.query.templateid || null
14831499
},
@@ -1601,6 +1617,9 @@ export default {
16011617
return this.$config.showAllCategoryForModernImageSelection
16021618
},
16031619
guestOsCategoriesSelectionDisallowed () {
1620+
if (this.imageType === 'volumeid' || this.imageType === 'snapshotid') {
1621+
return true
1622+
}
16041623
return (!this.queryGuestOsCategoryId || this.options.guestOsCategories.length === 0) && (!!this.queryTemplateId || !!this.queryIsoId)
16051624
},
16061625
isTemplateHypervisorExternal () {
@@ -2087,7 +2106,9 @@ export default {
20872106
this.imageType = 'templateid'
20882107
this.form.templateid = value
20892108
this.form.isoid = null
2090-
this.form.volumeId = null
2109+
this.form.volumeid = null
2110+
this.form.snapshotid = null
2111+
this.resetFromTemplateConfiguration()
20912112
let template = ''
20922113
for (const entry of Object.values(this.options.templates)) {
20932114
template = entry?.template.find(option => option.id === value) || null
@@ -2128,6 +2149,8 @@ export default {
21282149
this.resetFromTemplateConfiguration()
21292150
this.form.isoid = value
21302151
this.form.templateid = null
2152+
this.form.volumeid = null
2153+
this.form.snapshotid = null
21312154
let iso = null
21322155
for (const entry of Object.values(this.options.isos)) {
21332156
iso = entry?.iso.find(option => option.id === value)
@@ -2140,24 +2163,59 @@ export default {
21402163
this.updateTemplateLinkedUserData(this.iso.userdataid)
21412164
this.userdataDefaultOverridePolicy = this.iso.userdatapolicy
21422165
}
2143-
} else if (name === 'volumeId') {
2144-
this.templateConfigurations = []
2145-
this.selectedTemplateConfiguration = {}
2146-
this.templateNics = []
2147-
this.templateLicenses = []
2148-
this.templateProperties = {}
2149-
this.tabKey = 'volumeId'
2150-
this.resetFromTemplateConfiguration()
2151-
this.form.volumeId = value
2152-
this.form.templateid = null
2153-
this.form.isoid = null
2166+
} else if (name === 'volumeid') {
2167+
this.updateFieldValueForVolume(value)
2168+
} else if (name === 'snapshotid') {
2169+
this.updateFieldValueForSnapshot(value)
21542170
} else if (['cpuspeed', 'cpunumber', 'memory'].includes(name)) {
21552171
this.vm[name] = value
21562172
this.form[name] = value
21572173
} else {
21582174
this.form[name] = value
21592175
}
21602176
},
2177+
updateFieldValueForVolume (value) {
2178+
this.imageType = 'volumeid'
2179+
this.resetTemplateAssociatedResources()
2180+
this.resetFromTemplateConfiguration()
2181+
this.form.templateid = null
2182+
this.form.isoid = null
2183+
this.form.volumeid = value
2184+
this.form.snapshotid = null
2185+
let volume = null
2186+
for (const entry of Object.values(this.options.volumes)) {
2187+
volume = entry?.volume.find(option => option.id === value)
2188+
if (volume) {
2189+
this.volume = volume
2190+
break
2191+
}
2192+
}
2193+
if (volume) {
2194+
this.updateTemplateLinkedUserData(this.volume.userdataid)
2195+
this.userdataDefaultOverridePolicy = this.volume.userdatapolicy
2196+
}
2197+
},
2198+
updateFieldValueForSnapshot (value) {
2199+
this.imageType = 'snapshotid'
2200+
this.resetTemplateAssociatedResources()
2201+
this.resetFromTemplateConfiguration()
2202+
this.form.templateid = null
2203+
this.form.isoid = null
2204+
this.form.volumeid = null
2205+
this.form.snapshotid = value
2206+
let snapshot = null
2207+
for (const entry of Object.values(this.options.snapshots)) {
2208+
snapshot = entry?.snapshot.find(option => option.id === value)
2209+
if (snapshot) {
2210+
this.snapshot = snapshot
2211+
break
2212+
}
2213+
}
2214+
if (snapshot) {
2215+
this.updateTemplateLinkedUserData(this.snapshot.userdataid)
2216+
this.userdataDefaultOverridePolicy = this.snapshot.userdatapolicy
2217+
}
2218+
},
21612219
updateComputeOffering (id, kvdoEnable) {
21622220
this.form.computeofferingid = id
21632221
this.form.computeOfferingKvdoEnable = kvdoEnable
@@ -2327,7 +2385,7 @@ export default {
23272385
if (this.loading.deploy) return
23282386
this.formRef.value.validate().then(async () => {
23292387
const values = toRaw(this.form)
2330-
if (!values.templateid && !values.isoid && !values.volumeId) {
2388+
if (!values.templateid && !values.isoid && !values.volumeid && !values.snapshotid) {
23312389
this.$notification.error({
23322390
message: this.$t('message.request.failed'),
23332391
description: this.$t('message.template.iso')
@@ -2392,11 +2450,13 @@ export default {
23922450
this.loading.deploy = false
23932451
return
23942452
}
2395-
} else if (this.tabKey === 'isoid') {
2453+
} else if (this.imageType === 'volumeid') {
2454+
deployVmData.volumeid = values.volumeid
2455+
} else if (this.imageType === 'snapshotid') {
2456+
deployVmData.snapshotid = values.snapshotid
2457+
} else {
23962458
deployVmData.templateid = values.isoid
2397-
} else if (this.tabKey === 'volumeId') {
2398-
deployVmData.volumeId = values.volumeId
2399-
}
2459+
}
24002460
24012461
if (this.showRootDiskSizeChanger && values.rootdisksize && values.rootdisksize > 0) {
24022462
deployVmData.rootdisksize = values.rootdisksize
@@ -2830,6 +2890,88 @@ export default {
28302890
})
28312891
})
28322892
},
2893+
fetchUnattachedVolumes (volumeFilter, params) {
2894+
const args = Object.assign({}, params)
2895+
if (args.keyword || (args.category && args.category !== volumeFilter)) {
2896+
args.page = 1
2897+
args.pageSize = args.pageSize || 10
2898+
}
2899+
args.zoneid = _.get(this.zone, 'id')
2900+
if (this.isZoneSelectedMultiArch) {
2901+
args.arch = this.selectedArchitecture
2902+
}
2903+
args.account = store.getters.project?.id ? null : this.owner.account
2904+
args.domainid = store.getters.project?.id ? null : this.owner.domainid
2905+
args.projectid = store.getters.project?.id || this.owner.projectid
2906+
args.id = this.queryVolumeId
2907+
args.state = 'Ready'
2908+
const pageSize = args.pageSize ? args.pageSize : 10
2909+
const pageStart = (args.page ? args.page - 1 : 0) * pageSize
2910+
const pageEnd = pageSize * (pageStart + 1)
2911+
2912+
delete args.category
2913+
delete args.public
2914+
delete args.featured
2915+
delete args.page
2916+
delete args.pageSize
2917+
2918+
return new Promise((resolve, reject) => {
2919+
getAPI('listVolumes', args).then((response) => {
2920+
let count = 0
2921+
const volumes = []
2922+
response.listvolumesresponse.volume.forEach(volume => {
2923+
if (!volume.virtualmachineid) {
2924+
count += 1
2925+
volumes.push({ ...volume, displaytext: volume.name })
2926+
}
2927+
})
2928+
resolve({ listvolumesresponse: { count, volume: volumes.slice(pageStart, pageEnd) } })
2929+
}).catch((reason) => {
2930+
// ToDo: Handle errors
2931+
reject(reason)
2932+
})
2933+
})
2934+
},
2935+
fetchRootSnapshots (snapshotFilter, params) {
2936+
const args = Object.assign({}, params)
2937+
if (args.keyword || (args.category && args.category !== snapshotFilter)) {
2938+
args.page = 1
2939+
args.pageSize = args.pageSize || 10
2940+
}
2941+
args.zoneid = _.get(this.zone, 'id')
2942+
if (this.isZoneSelectedMultiArch) {
2943+
args.arch = this.selectedArchitecture
2944+
}
2945+
args.account = store.getters.project?.id ? null : this.owner.account
2946+
args.domainid = store.getters.project?.id ? null : this.owner.domainid
2947+
args.projectid = store.getters.project?.id || this.owner.projectid
2948+
const pageSize = args.pageSize ? args.pageSize : 10
2949+
const pageStart = (args.page ? args.page - 1 : 0) * pageSize
2950+
const pageEnd = pageSize * (pageStart + 1)
2951+
2952+
delete args.category
2953+
delete args.public
2954+
delete args.featured
2955+
delete args.page
2956+
delete args.pageSize
2957+
2958+
return new Promise((resolve, reject) => {
2959+
getAPI('listSnapshots', args).then((response) => {
2960+
let count = 0
2961+
const snapshots = []
2962+
response.listsnapshotsresponse.snapshot.forEach(snapshot => {
2963+
if (snapshot.volumetype === 'ROOT') {
2964+
count += 1
2965+
snapshots.push({ ...snapshot, displaytext: snapshot.name })
2966+
}
2967+
})
2968+
resolve({ listsnapshotsresponse: { count, snapshot: snapshots.slice(pageStart, pageEnd) } })
2969+
}).catch((reason) => {
2970+
// ToDo: Handle errors
2971+
reject(reason)
2972+
})
2973+
})
2974+
},
28332975
fetchTemplates (templateFilter, params) {
28342976
const args = Object.assign({}, params)
28352977
if (this.isModernImageSelection && this.form.guestoscategoryid && !['-1', '0'].includes(this.form.guestoscategoryid)) {
@@ -2926,6 +3068,14 @@ export default {
29263068
this.fetchAllIsos(params)
29273069
return
29283070
}
3071+
if (this.imageType === 'volumeid') {
3072+
this.fetchAllVolumes(params)
3073+
return
3074+
}
3075+
if (this.imageType === 'snapshotid') {
3076+
this.fetchAllSnapshots(params)
3077+
return
3078+
}
29293079
this.fetchAllTemplates(params)
29303080
},
29313081
fetchAllTemplates (params) {
@@ -2972,21 +3122,48 @@ export default {
29723122
this.loading.isos = false
29733123
})
29743124
},
2975-
fetchAllRbdImage (params) {
3125+
fetchAllVolumes (params) {
3126+
const promises = []
3127+
const volumes = {}
3128+
this.loading.volumes = true
3129+
this.imageSearchFilters = params
3130+
const volumeFilters = this.getImageFilters(params)
3131+
volumeFilters.forEach((filter) => {
3132+
volumes[filter] = { count: 0, volume: [] }
3133+
promises.push(this.fetchUnattachedVolumes(filter, params))
3134+
})
3135+
this.options.volumes = volumes
3136+
Promise.all(promises).then((response) => {
3137+
response.forEach((resItem, idx) => {
3138+
volumes[volumeFilters[idx]] = _.isEmpty(resItem.listvolumesresponse) ? { count: 0, volume: [] } : resItem.listvolumesresponse
3139+
this.options.volumes = { ...volumes }
3140+
})
3141+
}).catch((reason) => {
3142+
console.log(reason)
3143+
}).finally(() => {
3144+
this.loading.volumes = false
3145+
})
3146+
},
3147+
fetchAllSnapshots (params) {
29763148
const promises = []
2977-
const volume = {}
2978-
this.loading.volume = true
2979-
promises.push(this.fetchRbdImage(params))
2980-
this.options.volume = volume
3149+
const snapshots = {}
3150+
this.loading.snapshots = true
3151+
this.imageSearchFilters = params
3152+
const snapshotFilters = this.getImageFilters(params)
3153+
snapshotFilters.forEach((filter) => {
3154+
snapshots[filter] = { count: 0, snapshot: [] }
3155+
promises.push(this.fetchRootSnapshots(filter, params))
3156+
})
3157+
this.options.snapshots = snapshots
29813158
Promise.all(promises).then((response) => {
29823159
response.forEach((resItem, idx) => {
2983-
this.options.volume = resItem.listvolumesresponse.volume
2984-
this.rowCount.volume = resItem.listvolumesresponse.count
3160+
snapshots[snapshotFilters[idx]] = _.isEmpty(resItem.listsnapshotsresponse) ? { count: 0, snapshot: [] } : resItem.listsnapshotsresponse
3161+
this.options.snapshots = { ...snapshots }
29853162
})
29863163
}).catch((reason) => {
29873164
console.log(reason)
29883165
}).finally(() => {
2989-
this.loading.volume = false
3166+
this.loading.snapshots = false
29903167
})
29913168
},
29923169
filterOption (input, option) {
@@ -3121,7 +3298,7 @@ export default {
31213298
}
31223299
},
31233300
updateImages () {
3124-
if (this.isModernImageSelection) {
3301+
if (this.isModernImageSelection && this.imageType !== 'snapshotid' && this.imageType !== 'volumeid') {
31253302
this.fetchGuestOsCategories()
31263303
return
31273304
}

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)