Skip to content

Commit 2df82d8

Browse files
authored
ui: changes in migrate vm storage and migrate volume form (apache#5145)
Better forms in UI for migrating VMs and volumes. - Show option to migrate with storage while live migrating a VM - For VM storage migration (stopped VM), allow migrating volumes to specific primary storages - Show primary storage details in migrate volume form Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
1 parent 14f3b24 commit 2df82d8

File tree

11 files changed

+1045
-281
lines changed

11 files changed

+1045
-281
lines changed

server/src/main/java/com/cloud/api/query/QueryManagerImpl.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2565,13 +2565,21 @@ private Pair<List<StoragePoolJoinVO>, Integer> searchForStoragePoolsInternal(Lis
25652565
sc.setParameters("dataCenterId", zoneId);
25662566
}
25672567
if (pod != null) {
2568-
sc.setParameters("podId", pod);
2568+
SearchCriteria<StoragePoolJoinVO> ssc = _poolJoinDao.createSearchCriteria();
2569+
ssc.addOr("podId", Op.EQ, pod);
2570+
ssc.addOr("podId", Op.NULL);
2571+
2572+
sc.addAnd("podId", SearchCriteria.Op.SC, ssc);
25692573
}
25702574
if (address != null) {
25712575
sc.setParameters("hostAddress", address);
25722576
}
25732577
if (cluster != null) {
2574-
sc.setParameters("clusterId", cluster);
2578+
SearchCriteria<StoragePoolJoinVO> ssc = _poolJoinDao.createSearchCriteria();
2579+
ssc.addOr("clusterId", Op.EQ, cluster);
2580+
ssc.addOr("clusterId", Op.NULL);
2581+
2582+
sc.addAnd("clusterId", SearchCriteria.Op.SC, ssc);
25752583
}
25762584
if (scopeType != null) {
25772585
sc.setParameters("scope", scopeType.toString());

ui/public/locales/en.json

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@
470470
"label.asyncbackup": "Async Backup",
471471
"label.author.email": "Author e-mail",
472472
"label.author.name": "Author name",
473+
"label.auto.assign": "Automatically assign",
473474
"label.auto.assign.diskoffering.disk.size": "Automatically assign offering matching the disk size",
474475
"label.auto.assign.random.ip": "Automatically assign a random IP address",
475476
"label.autoscale": "AutoScale",
@@ -550,6 +551,7 @@
550551
"label.certificate.upload.failed": "Certificate Upload Failed",
551552
"label.certificate.upload.failed.description": "Failed to update SSL Certificate. Failed to pass certificate validation check",
552553
"label.certificateid": "Certificate ID",
554+
"label.change": "Change",
553555
"label.change.affinity": "Change Affinity",
554556
"label.change.ip.addess": "Change IP Address",
555557
"label.change.ipaddress": "Change IP address for NIC",
@@ -818,6 +820,7 @@
818820
"label.disksize": "Disk Size (in GB)",
819821
"label.disksizeallocated": "Disk Allocated",
820822
"label.disksizeallocatedgb": "Allocated",
823+
"label.disksizefree": "Disk Free",
821824
"label.disksizetotal": "Disk Total",
822825
"label.disksizetotalgb": "Total",
823826
"label.disksizeunallocatedgb": "Unallocated",
@@ -1425,6 +1428,8 @@
14251428
"label.migrate.instance.to": "Migrate instance to",
14261429
"label.migrate.instance.to.host": "Migrate instance to another host",
14271430
"label.migrate.instance.to.ps": "Migrate instance to another primary storage",
1431+
"label.migrate.instance.single.storage": "Migrate all volume(s) of the instance to a single primary storage",
1432+
"label.migrate.instance.specific.storages": "Migrate volume(s) of the instance to specific primary storages",
14281433
"label.migrate.lb.vm": "Migrate LB VM",
14291434
"label.migrate.lb.vm.to.ps": "Migrate LB VM to another primary storage",
14301435
"label.migrate.router.to": "Migrate Router to",
@@ -1434,6 +1439,7 @@
14341439
"label.migrate.volume": "Migrate Volume",
14351440
"label.migrate.volume.newdiskoffering.desc": "This option allows administrators to replace the old disk offering, using one that better suits the new placement of the volume.",
14361441
"label.migrate.volume.to.primary.storage": "Migrate volume to another primary storage",
1442+
"label.migrate.with.storage": "Migrate with storage",
14371443
"label.migrating": "Migrating",
14381444
"label.migrating.data": "Migrating Data",
14391445
"label.min.balance": "Min Balance",
@@ -1977,6 +1983,7 @@
19771983
"label.select.offering": "Select offering",
19781984
"label.select.project": "Select Project",
19791985
"label.select.projects": "Select Projects",
1986+
"label.select.ps": "Select Primary Storage",
19801987
"label.select.region": "Select region",
19811988
"label.select.tier": "Select Tier",
19821989
"label.select.vm.for.static.nat": "Select VM for static NAT",
@@ -3068,17 +3075,20 @@
30683075
"message.lock.account": "Please confirm that you want to lock this account. By locking the account, all users for this account will no longer be able to manage their cloud resources. Existing resources can still be accessed.",
30693076
"message.login.failed": "Login Failed",
30703077
"message.migrate.instance.confirm": "Please confirm the host you wish to migrate the virtual instance to.",
3078+
"message.migrate.instance.host.auto.assign": "Host for the instance will be automatically chosen based on the suitability within the same cluster",
30713079
"message.migrate.instance.select.host": "Please select a host for migration",
3072-
"message.migrate.instance.to.host": "Please confirm that you want to migrate instance to another host.",
3073-
"message.migrate.instance.to.ps": "Please confirm that you want to migrate instance to another primary storage.",
3080+
"message.migrate.instance.to.host": "Please confirm that you want to migrate this instance to another host. When migration is between hosts of different clusters volume(s) of the instance may get migrated to suitable storage pools.",
3081+
"message.migrate.instance.to.ps": "Please confirm that you want to migrate this instance to another primary storage.",
30743082
"message.migrate.lb.vm.to.ps": "Please confirm that you want to migrate LB VM to another primary storage.",
30753083
"message.migrate.router.confirm": "Please confirm the host you wish to migrate the router to:",
30763084
"message.migrate.router.to.ps": "Please confirm that you want to migrate router to another primary storage.",
30773085
"message.migrate.system.vm.to.ps": "Please confirm that you want to migrate system VM to another primary storage.",
30783086
"message.migrate.systemvm.confirm": "Please confirm the host you wish to migrate the system VM to:",
3079-
"message.migrate.volume": "Please confirm that you want to migrate volume to another primary storage.",
3087+
"message.migrate.volume": "Please confirm that you want to migrate this volume to another primary storage.",
30803088
"message.migrate.volume.failed": "Migrating volume failed",
3089+
"message.migrate.volume.pool.auto.assign": "Primary storage for the volume will be automatically chosen based on the suitability and VM destination",
30813090
"message.migrate.volume.processing": "Migrating volume...",
3091+
"message.migrate.with.storage": "Specify storage pool for volumes of the instance.",
30823092
"message.migrating.failed": "Migration failed",
30833093
"message.migrating.processing": "Migration in progress for",
30843094
"message.migrating.vm.to.host.failed": "Failed to migrate VM to host",
@@ -3144,6 +3154,7 @@
31443154
"message.pod.dedicated": "Pod Dedicated",
31453155
"message.pod.dedication.released": "Pod dedication released",
31463156
"message.portable.ip.delete.confirm": "Please confirm you want to delete Portable IP Range",
3157+
"message.primary.storage.invalid.state": "Primary storage is not in Up state",
31473158
"message.processing.complete": "Processing complete!",
31483159
"message.project.invite.sent": "Invite sent to user; they will be added to the project once they accept the invitation",
31493160
"message.protocol.description": "For XenServer, choose NFS, iSCSI, or PreSetup. For KVM, choose NFS, SharedMountPoint, RDB, CLVM or Gluster. For vSphere, choose NFS, PreSetup (VMFS or iSCSI or FiberChannel or vSAN or vVols) or DatastoreCluster. For Hyper-V, choose SMB/CIFS. For LXC, choose NFS or SharedMountPoint. For OVM, choose NFS or ocfs2.",
@@ -3419,6 +3430,7 @@
34193430
"message.volume.state.uploaderror": "Volume upload encountered some error",
34203431
"message.volume.state.uploadinprogress": "Volume upload is in progress",
34213432
"message.volume.state.uploadop": "The volume upload operation is in progress or in short the volume is on secondary storage",
3433+
"message.volume.state.primary.storage.suitability": "The suitability of a primary storage for a volume depends on the disk offering of the volume and on the virtual machine allocations if the volume is attached to a virtual machine.",
34223434
"message.waiting.for.builtin.templates.to.load": "Waiting for builtin templates to load...",
34233435
"message.warn.filetype": "jpg, jpeg, png, bmp and svg are the only supported image formats",
34243436
"message.xstools61plus.update.failed": "Failed to update Original XS Version is 6.1+ field. Error:",
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
<template>
19+
<div>
20+
<a-table
21+
class="top-spaced"
22+
size="small"
23+
style="max-height: 250px; overflow-y: auto"
24+
:loading="volumesLoading"
25+
:columns="volumeColumns"
26+
:dataSource="volumes"
27+
:pagination="false"
28+
:rowKey="record => record.id">
29+
<div slot="size" slot-scope="record">
30+
<span v-if="record.size">
31+
{{ $bytesToHumanReadableSize(record.size) }}
32+
</span>
33+
</div>
34+
<template slot="selectedstorage" slot-scope="record">
35+
<span>{{ record.selectedstoragename || '' }}</span>
36+
</template>
37+
<template slot="select" slot-scope="record">
38+
<div style="display: flex; justify-content: flex-end;"><a-button @click="openVolumeStoragePoolSelector(record)">{{ record.selectedstorageid ? $t('label.change') : $t('label.select') }}</a-button></div>
39+
</template>
40+
</a-table>
41+
42+
<a-modal
43+
:visible="!(!selectedVolumeForStoragePoolSelection.id)"
44+
:title="$t('label.select.ps')"
45+
:closable="true"
46+
:maskClosable="false"
47+
:footer="null"
48+
:cancelText="$t('label.cancel')"
49+
@cancel="closeVolumeStoragePoolSelector()"
50+
centered
51+
width="auto">
52+
<volume-storage-pool-select-form
53+
:resource="selectedVolumeForStoragePoolSelection"
54+
:clusterId="storagePoolsClusterId"
55+
:autoAssignAllowed="storagePoolsClusterId != null"
56+
:isOpen="!(!selectedVolumeForStoragePoolSelection.id)"
57+
@close-action="closeVolumeStoragePoolSelector()"
58+
@select="handleVolumeStoragePoolSelection" />
59+
</a-modal>
60+
</div>
61+
</template>
62+
63+
<script>
64+
import { api } from '@/api'
65+
import VolumeStoragePoolSelectForm from '@/components/view/VolumeStoragePoolSelectForm'
66+
67+
export default {
68+
name: 'InstanceVolumesStoragePoolSelectListView',
69+
components: {
70+
VolumeStoragePoolSelectForm
71+
},
72+
props: {
73+
resource: {
74+
type: Object,
75+
required: true
76+
},
77+
clusterId: {
78+
type: String,
79+
required: false,
80+
default: null
81+
}
82+
},
83+
data () {
84+
return {
85+
volumes: [],
86+
volumesLoading: false,
87+
volumeColumns: [
88+
{
89+
title: this.$t('label.volumeid'),
90+
dataIndex: 'name'
91+
},
92+
{
93+
title: this.$t('label.type'),
94+
dataIndex: 'type'
95+
},
96+
{
97+
title: this.$t('label.size'),
98+
scopedSlots: { customRender: 'size' }
99+
},
100+
{
101+
title: this.$t('label.storage'),
102+
scopedSlots: { customRender: 'selectedstorage' }
103+
},
104+
{
105+
title: '',
106+
scopedSlots: { customRender: 'select' }
107+
}
108+
],
109+
selectedVolumeForStoragePoolSelection: {},
110+
selectedClusterId: null,
111+
volumesWithClusterStoragePool: []
112+
}
113+
},
114+
beforeCreate () {
115+
this.form = this.$form.createForm(this)
116+
this.apiParams = {}
117+
if (this.$route.meta.name === 'vm') {
118+
this.apiConfig = this.$store.getters.apis.migrateVirtualMachineWithVolume || {}
119+
this.apiConfig.params.forEach(param => {
120+
this.apiParams[param.name] = param
121+
})
122+
this.apiConfig = this.$store.getters.apis.migrateVirtualMachine || {}
123+
this.apiConfig.params.forEach(param => {
124+
if (!(param.name in this.apiParams)) {
125+
this.apiParams[param.name] = param
126+
}
127+
})
128+
} else {
129+
this.apiConfig = this.$store.getters.apis.migrateSystemVm || {}
130+
this.apiConfig.params.forEach(param => {
131+
if (!(param.name in this.apiParams)) {
132+
this.apiParams[param.name] = param
133+
}
134+
})
135+
}
136+
},
137+
created () {
138+
this.fetchVolumes()
139+
},
140+
computed: {
141+
isSelectedVolumeOnlyClusterStoragePoolVolume () {
142+
if (this.volumesWithClusterStoragePool.length !== 1) {
143+
return false
144+
}
145+
for (const volume of this.volumesWithClusterStoragePool) {
146+
if (volume.id === this.selectedVolumeForStoragePoolSelection.id) {
147+
return true
148+
}
149+
}
150+
return false
151+
},
152+
storagePoolsClusterId () {
153+
if (this.clusterId) {
154+
return this.clusterId
155+
}
156+
return this.isSelectedVolumeOnlyClusterStoragePoolVolume ? null : this.selectedClusterId
157+
}
158+
},
159+
methods: {
160+
fetchVolumes () {
161+
this.volumesLoading = true
162+
this.volumes = []
163+
api('listVolumes', {
164+
listAll: true,
165+
virtualmachineid: this.resource.id
166+
}).then(response => {
167+
var volumes = response.listvolumesresponse.volume
168+
if (volumes && volumes.length > 0) {
169+
volumes.sort((a, b) => {
170+
return b.type.localeCompare(a.type)
171+
})
172+
this.volumes = volumes
173+
}
174+
}).finally(() => {
175+
this.resetSelection()
176+
this.volumesLoading = false
177+
})
178+
},
179+
resetSelection () {
180+
var volumes = this.volumes
181+
this.volumes = []
182+
for (var volume of volumes) {
183+
if (this.clusterId) {
184+
volume.selectedstorageid = -1
185+
volume.selectedstoragename = this.$t('label.auto.assign')
186+
} else {
187+
volume.selectedstorageid = null
188+
volume.selectedstoragename = ''
189+
}
190+
delete volume.selectedstorageclusterid
191+
}
192+
this.volumes = volumes
193+
this.updateVolumeToStoragePoolSelection()
194+
},
195+
openVolumeStoragePoolSelector (volume) {
196+
this.selectedVolumeForStoragePoolSelection = volume
197+
},
198+
closeVolumeStoragePoolSelector () {
199+
this.selectedVolumeForStoragePoolSelection = {}
200+
},
201+
handleVolumeStoragePoolSelection (volumeId, storagePool) {
202+
for (const volume of this.volumes) {
203+
if (volume.id === volumeId) {
204+
volume.selectedstorageid = storagePool.id
205+
volume.selectedstoragename = storagePool.name
206+
volume.selectedstorageclusterid = storagePool.clusterid
207+
break
208+
}
209+
}
210+
this.updateVolumeToStoragePoolSelection()
211+
},
212+
updateVolumeToStoragePoolSelection () {
213+
var clusterId = null
214+
this.volumeToPoolSelection = []
215+
this.volumesWithClusterStoragePool = []
216+
for (const volume of this.volumes) {
217+
if (volume.selectedstorageid && volume.selectedstorageid !== -1) {
218+
this.volumeToPoolSelection.push({ volume: volume.id, pool: volume.selectedstorageid })
219+
}
220+
if (!this.clusterId && volume.selectedstorageclusterid) {
221+
clusterId = volume.selectedstorageclusterid
222+
this.volumesWithClusterStoragePool.push(volume)
223+
}
224+
}
225+
if (!this.clusterId) {
226+
this.selectedClusterId = clusterId
227+
for (const volume of this.volumes) {
228+
if (this.selectedClusterId == null && volume.selectedstorageid === -1) {
229+
volume.selectedstorageid = null
230+
volume.selectedstoragename = ''
231+
}
232+
if (this.selectedClusterId && volume.selectedstorageid == null) {
233+
volume.selectedstorageid = -1
234+
volume.selectedstoragename = this.$t('label.auto.assign')
235+
}
236+
}
237+
}
238+
this.$emit('select', this.volumeToPoolSelection)
239+
}
240+
}
241+
}
242+
</script>
243+
244+
<style scoped lang="less">
245+
.top-spaced {
246+
margin-top: 20px;
247+
}
248+
</style>

0 commit comments

Comments
 (0)