You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ro...@apache.org on 2021/11/30 11:38:15 UTC

[cloudstack] branch main updated: ui: changes in migrate vm storage and migrate volume form (#5145)

This is an automated email from the ASF dual-hosted git repository.

rohit pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 2df82d8  ui: changes in migrate vm storage and migrate volume form (#5145)
2df82d8 is described below

commit 2df82d818851e579cf0aec687059101d0f7ff136
Author: Abhishek Kumar <ab...@gmail.com>
AuthorDate: Tue Nov 30 17:07:48 2021 +0530

    ui: changes in migrate vm storage and migrate volume form (#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 <ab...@gmail.com>
---
 .../java/com/cloud/api/query/QueryManagerImpl.java |  12 +-
 ui/public/locales/en.json                          |  18 +-
 .../InstanceVolumesStoragePoolSelectListView.vue   | 248 +++++++++++++++++++
 ui/src/components/view/StoragePoolSelectView.vue   | 274 +++++++++++++++++++++
 .../view/VolumeStoragePoolSelectForm.vue           | 129 ++++++++++
 ui/src/main.js                                     |   3 +-
 ui/src/utils/plugins.js                            |  27 ++
 ui/src/views/compute/MigrateVMStorage.vue          | 252 ++++++++++---------
 ui/src/views/compute/MigrateWizard.vue             | 176 +++++++++----
 ui/src/views/storage/MigrateVolume.vue             | 184 ++++++--------
 ui/tests/common/index.js                           |   3 +-
 11 files changed, 1045 insertions(+), 281 deletions(-)

diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
index 0a0f528..d12da63 100644
--- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
+++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
@@ -2565,13 +2565,21 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
             sc.setParameters("dataCenterId", zoneId);
         }
         if (pod != null) {
-            sc.setParameters("podId", pod);
+            SearchCriteria<StoragePoolJoinVO> ssc = _poolJoinDao.createSearchCriteria();
+            ssc.addOr("podId", Op.EQ, pod);
+            ssc.addOr("podId", Op.NULL);
+
+            sc.addAnd("podId", SearchCriteria.Op.SC, ssc);
         }
         if (address != null) {
             sc.setParameters("hostAddress", address);
         }
         if (cluster != null) {
-            sc.setParameters("clusterId", cluster);
+            SearchCriteria<StoragePoolJoinVO> ssc = _poolJoinDao.createSearchCriteria();
+            ssc.addOr("clusterId", Op.EQ, cluster);
+            ssc.addOr("clusterId", Op.NULL);
+
+            sc.addAnd("clusterId", SearchCriteria.Op.SC, ssc);
         }
         if (scopeType != null) {
             sc.setParameters("scope", scopeType.toString());
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 372cd31..4111e0a 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -470,6 +470,7 @@
 "label.asyncbackup": "Async Backup",
 "label.author.email": "Author e-mail",
 "label.author.name": "Author name",
+"label.auto.assign": "Automatically assign",
 "label.auto.assign.diskoffering.disk.size": "Automatically assign offering matching the disk size",
 "label.auto.assign.random.ip": "Automatically assign a random IP address",
 "label.autoscale": "AutoScale",
@@ -550,6 +551,7 @@
 "label.certificate.upload.failed": "Certificate Upload Failed",
 "label.certificate.upload.failed.description": "Failed to update SSL Certificate. Failed to pass certificate validation check",
 "label.certificateid": "Certificate ID",
+"label.change": "Change",
 "label.change.affinity": "Change Affinity",
 "label.change.ip.addess": "Change IP Address",
 "label.change.ipaddress": "Change IP address for NIC",
@@ -818,6 +820,7 @@
 "label.disksize": "Disk Size (in GB)",
 "label.disksizeallocated": "Disk Allocated",
 "label.disksizeallocatedgb": "Allocated",
+"label.disksizefree": "Disk Free",
 "label.disksizetotal": "Disk Total",
 "label.disksizetotalgb": "Total",
 "label.disksizeunallocatedgb": "Unallocated",
@@ -1425,6 +1428,8 @@
 "label.migrate.instance.to": "Migrate instance to",
 "label.migrate.instance.to.host": "Migrate instance to another host",
 "label.migrate.instance.to.ps": "Migrate instance to another primary storage",
+"label.migrate.instance.single.storage": "Migrate all volume(s) of the instance to a single primary storage",
+"label.migrate.instance.specific.storages": "Migrate volume(s) of the instance to specific primary storages",
 "label.migrate.lb.vm": "Migrate LB VM",
 "label.migrate.lb.vm.to.ps": "Migrate LB VM to another primary storage",
 "label.migrate.router.to": "Migrate Router to",
@@ -1434,6 +1439,7 @@
 "label.migrate.volume": "Migrate Volume",
 "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.",
 "label.migrate.volume.to.primary.storage": "Migrate volume to another primary storage",
+"label.migrate.with.storage": "Migrate with storage",
 "label.migrating": "Migrating",
 "label.migrating.data": "Migrating Data",
 "label.min.balance": "Min Balance",
@@ -1977,6 +1983,7 @@
 "label.select.offering": "Select offering",
 "label.select.project": "Select Project",
 "label.select.projects": "Select Projects",
+"label.select.ps": "Select Primary Storage",
 "label.select.region": "Select region",
 "label.select.tier": "Select Tier",
 "label.select.vm.for.static.nat": "Select VM for static NAT",
@@ -3068,17 +3075,20 @@
 "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.",
 "message.login.failed": "Login Failed",
 "message.migrate.instance.confirm": "Please confirm the host you wish to migrate the virtual instance to.",
+"message.migrate.instance.host.auto.assign": "Host for the instance will be automatically chosen based on the suitability within the same cluster",
 "message.migrate.instance.select.host": "Please select a host for migration",
-"message.migrate.instance.to.host": "Please confirm that you want to migrate instance to another host.",
-"message.migrate.instance.to.ps": "Please confirm that you want to migrate instance to another primary storage.",
+"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.",
+"message.migrate.instance.to.ps": "Please confirm that you want to migrate this instance to another primary storage.",
 "message.migrate.lb.vm.to.ps": "Please confirm that you want to migrate LB VM to another primary storage.",
 "message.migrate.router.confirm": "Please confirm the host you wish to migrate the router to:",
 "message.migrate.router.to.ps": "Please confirm that you want to migrate router to another primary storage.",
 "message.migrate.system.vm.to.ps": "Please confirm that you want to migrate system VM to another primary storage.",
 "message.migrate.systemvm.confirm": "Please confirm the host you wish to migrate the system VM to:",
-"message.migrate.volume": "Please confirm that you want to migrate volume to another primary storage.",
+"message.migrate.volume": "Please confirm that you want to migrate this volume to another primary storage.",
 "message.migrate.volume.failed": "Migrating volume failed",
+"message.migrate.volume.pool.auto.assign": "Primary storage for the volume will be automatically chosen based on the suitability and VM destination",
 "message.migrate.volume.processing": "Migrating volume...",
+"message.migrate.with.storage": "Specify storage pool for volumes of the instance.",
 "message.migrating.failed": "Migration failed",
 "message.migrating.processing": "Migration in progress for",
 "message.migrating.vm.to.host.failed": "Failed to migrate VM to host",
@@ -3144,6 +3154,7 @@
 "message.pod.dedicated": "Pod Dedicated",
 "message.pod.dedication.released": "Pod dedication released",
 "message.portable.ip.delete.confirm": "Please confirm you want to delete Portable IP Range",
+"message.primary.storage.invalid.state": "Primary storage is not in Up state",
 "message.processing.complete": "Processing complete!",
 "message.project.invite.sent": "Invite sent to user; they will be added to the project once they accept the invitation",
 "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 @@
 "message.volume.state.uploaderror": "Volume upload encountered some error",
 "message.volume.state.uploadinprogress": "Volume upload is in progress",
 "message.volume.state.uploadop": "The volume upload operation is in progress or in short the volume is on secondary storage",
+"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.",
 "message.waiting.for.builtin.templates.to.load": "Waiting for builtin templates to load...",
 "message.warn.filetype": "jpg, jpeg, png, bmp and svg are the only supported image formats",
 "message.xstools61plus.update.failed": "Failed to update Original XS Version is 6.1+ field. Error:",
diff --git a/ui/src/components/view/InstanceVolumesStoragePoolSelectListView.vue b/ui/src/components/view/InstanceVolumesStoragePoolSelectListView.vue
new file mode 100644
index 0000000..a0d482c
--- /dev/null
+++ b/ui/src/components/view/InstanceVolumesStoragePoolSelectListView.vue
@@ -0,0 +1,248 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <div>
+    <a-table
+      class="top-spaced"
+      size="small"
+      style="max-height: 250px; overflow-y: auto"
+      :loading="volumesLoading"
+      :columns="volumeColumns"
+      :dataSource="volumes"
+      :pagination="false"
+      :rowKey="record => record.id">
+      <div slot="size" slot-scope="record">
+        <span v-if="record.size">
+          {{ $bytesToHumanReadableSize(record.size) }}
+        </span>
+      </div>
+      <template slot="selectedstorage" slot-scope="record">
+        <span>{{ record.selectedstoragename || '' }}</span>
+      </template>
+      <template slot="select" slot-scope="record">
+        <div style="display: flex; justify-content: flex-end;"><a-button @click="openVolumeStoragePoolSelector(record)">{{ record.selectedstorageid ? $t('label.change') : $t('label.select') }}</a-button></div>
+      </template>
+    </a-table>
+
+    <a-modal
+      :visible="!(!selectedVolumeForStoragePoolSelection.id)"
+      :title="$t('label.select.ps')"
+      :closable="true"
+      :maskClosable="false"
+      :footer="null"
+      :cancelText="$t('label.cancel')"
+      @cancel="closeVolumeStoragePoolSelector()"
+      centered
+      width="auto">
+      <volume-storage-pool-select-form
+        :resource="selectedVolumeForStoragePoolSelection"
+        :clusterId="storagePoolsClusterId"
+        :autoAssignAllowed="storagePoolsClusterId != null"
+        :isOpen="!(!selectedVolumeForStoragePoolSelection.id)"
+        @close-action="closeVolumeStoragePoolSelector()"
+        @select="handleVolumeStoragePoolSelection" />
+    </a-modal>
+  </div>
+</template>
+
+<script>
+import { api } from '@/api'
+import VolumeStoragePoolSelectForm from '@/components/view/VolumeStoragePoolSelectForm'
+
+export default {
+  name: 'InstanceVolumesStoragePoolSelectListView',
+  components: {
+    VolumeStoragePoolSelectForm
+  },
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    },
+    clusterId: {
+      type: String,
+      required: false,
+      default: null
+    }
+  },
+  data () {
+    return {
+      volumes: [],
+      volumesLoading: false,
+      volumeColumns: [
+        {
+          title: this.$t('label.volumeid'),
+          dataIndex: 'name'
+        },
+        {
+          title: this.$t('label.type'),
+          dataIndex: 'type'
+        },
+        {
+          title: this.$t('label.size'),
+          scopedSlots: { customRender: 'size' }
+        },
+        {
+          title: this.$t('label.storage'),
+          scopedSlots: { customRender: 'selectedstorage' }
+        },
+        {
+          title: '',
+          scopedSlots: { customRender: 'select' }
+        }
+      ],
+      selectedVolumeForStoragePoolSelection: {},
+      selectedClusterId: null,
+      volumesWithClusterStoragePool: []
+    }
+  },
+  beforeCreate () {
+    this.form = this.$form.createForm(this)
+    this.apiParams = {}
+    if (this.$route.meta.name === 'vm') {
+      this.apiConfig = this.$store.getters.apis.migrateVirtualMachineWithVolume || {}
+      this.apiConfig.params.forEach(param => {
+        this.apiParams[param.name] = param
+      })
+      this.apiConfig = this.$store.getters.apis.migrateVirtualMachine || {}
+      this.apiConfig.params.forEach(param => {
+        if (!(param.name in this.apiParams)) {
+          this.apiParams[param.name] = param
+        }
+      })
+    } else {
+      this.apiConfig = this.$store.getters.apis.migrateSystemVm || {}
+      this.apiConfig.params.forEach(param => {
+        if (!(param.name in this.apiParams)) {
+          this.apiParams[param.name] = param
+        }
+      })
+    }
+  },
+  created () {
+    this.fetchVolumes()
+  },
+  computed: {
+    isSelectedVolumeOnlyClusterStoragePoolVolume () {
+      if (this.volumesWithClusterStoragePool.length !== 1) {
+        return false
+      }
+      for (const volume of this.volumesWithClusterStoragePool) {
+        if (volume.id === this.selectedVolumeForStoragePoolSelection.id) {
+          return true
+        }
+      }
+      return false
+    },
+    storagePoolsClusterId () {
+      if (this.clusterId) {
+        return this.clusterId
+      }
+      return this.isSelectedVolumeOnlyClusterStoragePoolVolume ? null : this.selectedClusterId
+    }
+  },
+  methods: {
+    fetchVolumes () {
+      this.volumesLoading = true
+      this.volumes = []
+      api('listVolumes', {
+        listAll: true,
+        virtualmachineid: this.resource.id
+      }).then(response => {
+        var volumes = response.listvolumesresponse.volume
+        if (volumes && volumes.length > 0) {
+          volumes.sort((a, b) => {
+            return b.type.localeCompare(a.type)
+          })
+          this.volumes = volumes
+        }
+      }).finally(() => {
+        this.resetSelection()
+        this.volumesLoading = false
+      })
+    },
+    resetSelection () {
+      var volumes = this.volumes
+      this.volumes = []
+      for (var volume of volumes) {
+        if (this.clusterId) {
+          volume.selectedstorageid = -1
+          volume.selectedstoragename = this.$t('label.auto.assign')
+        } else {
+          volume.selectedstorageid = null
+          volume.selectedstoragename = ''
+        }
+        delete volume.selectedstorageclusterid
+      }
+      this.volumes = volumes
+      this.updateVolumeToStoragePoolSelection()
+    },
+    openVolumeStoragePoolSelector (volume) {
+      this.selectedVolumeForStoragePoolSelection = volume
+    },
+    closeVolumeStoragePoolSelector () {
+      this.selectedVolumeForStoragePoolSelection = {}
+    },
+    handleVolumeStoragePoolSelection (volumeId, storagePool) {
+      for (const volume of this.volumes) {
+        if (volume.id === volumeId) {
+          volume.selectedstorageid = storagePool.id
+          volume.selectedstoragename = storagePool.name
+          volume.selectedstorageclusterid = storagePool.clusterid
+          break
+        }
+      }
+      this.updateVolumeToStoragePoolSelection()
+    },
+    updateVolumeToStoragePoolSelection () {
+      var clusterId = null
+      this.volumeToPoolSelection = []
+      this.volumesWithClusterStoragePool = []
+      for (const volume of this.volumes) {
+        if (volume.selectedstorageid && volume.selectedstorageid !== -1) {
+          this.volumeToPoolSelection.push({ volume: volume.id, pool: volume.selectedstorageid })
+        }
+        if (!this.clusterId && volume.selectedstorageclusterid) {
+          clusterId = volume.selectedstorageclusterid
+          this.volumesWithClusterStoragePool.push(volume)
+        }
+      }
+      if (!this.clusterId) {
+        this.selectedClusterId = clusterId
+        for (const volume of this.volumes) {
+          if (this.selectedClusterId == null && volume.selectedstorageid === -1) {
+            volume.selectedstorageid = null
+            volume.selectedstoragename = ''
+          }
+          if (this.selectedClusterId && volume.selectedstorageid == null) {
+            volume.selectedstorageid = -1
+            volume.selectedstoragename = this.$t('label.auto.assign')
+          }
+        }
+      }
+      this.$emit('select', this.volumeToPoolSelection)
+    }
+  }
+}
+</script>
+
+<style scoped lang="less">
+  .top-spaced {
+    margin-top: 20px;
+  }
+</style>
diff --git a/ui/src/components/view/StoragePoolSelectView.vue b/ui/src/components/view/StoragePoolSelectView.vue
new file mode 100644
index 0000000..d0dcf25
--- /dev/null
+++ b/ui/src/components/view/StoragePoolSelectView.vue
@@ -0,0 +1,274 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <div>
+    <a-input-search
+      class="top-spaced"
+      :placeholder="$t('label.search')"
+      v-model="searchQuery"
+      style="margin-bottom: 10px;"
+      @search="fetchStoragePools"
+      autoFocus />
+    <a-table
+      size="small"
+      style="overflow-y: auto"
+      :loading="loading"
+      :columns="columns"
+      :dataSource="storagePools"
+      :pagination="false"
+      :rowKey="record => record.id">
+      <span slot="suitabilityCustomTitle">
+        {{ $t('label.suitability') }}
+        <a-tooltip :title="$t('message.volume.state.primary.storage.suitability')" placement="top">
+          <a-icon type="info-circle" class="table-tooltip-icon" />
+        </a-tooltip>
+      </span>
+      <div slot="name" slot-scope="record">
+        {{ record.name }}
+        <a-tooltip v-if="record.name === $t('label.auto.assign')" :title="$t('message.migrate.volume.pool.auto.assign')" placement="top">
+          <a-icon type="info-circle" class="table-tooltip-icon" />
+        </a-tooltip>
+      </div>
+      <div slot="suitability" slot-scope="record">
+        <a-icon
+          class="host-item__suitability-icon"
+          type="check-circle"
+          theme="twoTone"
+          twoToneColor="#52c41a"
+          v-if="record.suitableformigration" />
+        <a-icon
+          class="host-item__suitability-icon"
+          type="close-circle"
+          theme="twoTone"
+          twoToneColor="#f5222d"
+          v-else />
+      </div>
+      <div slot="disksizetotal" slot-scope="record">
+        <span v-if="record.disksizetotal">{{ $bytesToHumanReadableSize(record.disksizetotal) }}</span>
+      </div>
+      <div slot="disksizeused" slot-scope="record">
+        <span v-if="record.disksizeused">{{ $bytesToHumanReadableSize(record.disksizeused) }}</span>
+      </div>
+      <div slot="disksizefree" slot-scope="record">
+        <span v-if="record.disksizetotal && record.disksizeused">{{ $bytesToHumanReadableSize(record.disksizetotal * 1 - record.disksizeused * 1) }}</span>
+      </div>
+      <template slot="select" slot-scope="record">
+        <a-tooltip placement="top" :title="record.state !== 'Up' ? $t('message.primary.storage.invalid.state') : ''">
+          <a-radio
+            :disabled="record.id !== -1 && record.state !== 'Up'"
+            @click="updateSelection(record)"
+            :checked="selectedStoragePool != null && record.id === selectedStoragePool.id">
+          </a-radio>
+        </a-tooltip>
+      </template>
+    </a-table>
+    <a-pagination
+      class="top-spaced"
+      size="small"
+      :current="page"
+      :pageSize="pageSize"
+      :total="totalCount"
+      :showTotal="total => `${$t('label.total')} ${total} ${$t('label.items')}`"
+      :pageSizeOptions="['10', '20', '40', '80', '100']"
+      @change="handleChangePage"
+      @showSizeChange="handleChangePageSize"
+      showSizeChanger>
+      <template slot="buildOptionText" slot-scope="props">
+        <span>{{ props.value }} / {{ $t('label.page') }}</span>
+      </template>
+    </a-pagination>
+  </div>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+  name: 'VolumeStoragePoolSelector',
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    },
+    clusterId: {
+      type: String,
+      required: false,
+      default: null
+    },
+    suitabilityEnabled: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    autoAssignAllowed: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    isOpen: {
+      type: Boolean,
+      required: false
+    }
+  },
+  data () {
+    return {
+      loading: false,
+      storagePools: [],
+      searchQuery: '',
+      totalCount: 0,
+      page: 1,
+      pageSize: 10,
+      selectedStoragePool: null,
+      columns: [
+        {
+          title: this.$t('label.storageid'),
+          scopedSlots: { customRender: 'name' }
+        },
+        {
+          title: this.$t('label.clusterid'),
+          dataIndex: 'clustername'
+        },
+        {
+          title: this.$t('label.podid'),
+          dataIndex: 'podname'
+        },
+        {
+          title: this.$t('label.disksizetotal'),
+          scopedSlots: { customRender: 'disksizetotal' }
+        },
+        {
+          title: this.$t('label.disksizeused'),
+          scopedSlots: { customRender: 'disksizeused' }
+        },
+        {
+          title: this.$t('label.disksizefree'),
+          scopedSlots: { customRender: 'disksizefree' }
+        },
+        {
+          title: this.$t('label.select'),
+          scopedSlots: { customRender: 'select' }
+        }
+      ]
+    }
+  },
+  created () {
+    if (this.suitabilityEnabled) {
+      this.columns.splice(1, 0, { slots: { title: 'suitabilityCustomTitle' }, scopedSlots: { customRender: 'suitability' } }
+      )
+    }
+    this.preselectStoragePool()
+    this.fetchStoragePools()
+  },
+  watch: {
+    searchQuery (newValue, oldValue) {
+      if (newValue !== oldValue) {
+        this.page = 1
+      }
+    }
+  },
+  methods: {
+    fetchStoragePools () {
+      this.loading = true
+      if (this.suitabilityEnabled) {
+        api('findStoragePoolsForMigration', {
+          id: this.resource.id,
+          keyword: this.searchQuery,
+          page: this.page,
+          pagesize: this.pageSize
+        }).then(response => {
+          this.storagePools = response.findstoragepoolsformigrationresponse.storagepool || []
+          this.totalCount = response.findstoragepoolsformigrationresponse.count
+        }).catch(error => {
+          this.$notifyError(error)
+        }).finally(() => {
+          this.handleStoragePoolsFetchComplete()
+        })
+      } else {
+        var params = {
+          zoneid: this.resource.zoneid,
+          keyword: this.searchQuery,
+          page: this.page,
+          pagesize: this.pageSize
+        }
+        if (this.clusterId) {
+          params.clusterid = this.clusterId
+        }
+        api('listStoragePools', params).then(response => {
+          this.storagePools = response.liststoragepoolsresponse.storagepool || []
+          this.totalCount = response.liststoragepoolsresponse.count
+        }).catch(error => {
+          this.$notifyError(error)
+        }).finally(() => {
+          this.handleStoragePoolsFetchComplete()
+        })
+      }
+    },
+    handleStoragePoolsFetchComplete () {
+      this.$emit('storagePoolsUpdated', this.storagePools)
+      this.addAutoAssignOption()
+      this.loading = false
+    },
+    addAutoAssignOption () {
+      if (this.autoAssignAllowed && this.page === 1) {
+        this.storagePools.unshift({ id: -1, name: this.$t('label.auto.assign'), clustername: '', podname: '' })
+      }
+    },
+    handleChangePage (page, pageSize) {
+      this.page = page
+      this.pageSize = pageSize
+      this.fetchStoragePools()
+    },
+    handleChangePageSize (currentPage, pageSize) {
+      this.page = currentPage
+      this.pageSize = pageSize
+      this.fetchStoragePools()
+    },
+    preselectStoragePool () {
+      if (this.resource && 'selectedstorageid' in this.resource) {
+        this.selectedStoragePool = { id: this.resource.selectedstorageid }
+      }
+    },
+    clearView () {
+      this.storagePools = []
+      this.searchQuery = ''
+      this.totalCount = 0
+      this.page = 1
+      this.pageSize = 10
+      this.selectedStoragePool = null
+    },
+    reset () {
+      this.clearView()
+      this.preselectStoragePool()
+      this.fetchStoragePools()
+    },
+    updateSelection (storagePool) {
+      this.selectedStoragePool = storagePool
+      this.$emit('select', this.selectedStoragePool)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+  .top-spaced {
+    margin-top: 20px;
+  }
+  .table-tooltip-icon {
+    color: rgba(0,0,0,.45);
+  }
+</style>
diff --git a/ui/src/components/view/VolumeStoragePoolSelectForm.vue b/ui/src/components/view/VolumeStoragePoolSelectForm.vue
new file mode 100644
index 0000000..eea416f
--- /dev/null
+++ b/ui/src/components/view/VolumeStoragePoolSelectForm.vue
@@ -0,0 +1,129 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <div class="form" v-ctrl-enter="handleKeyboardSubmit">
+    <storage-pool-select-view
+      ref="selectionView"
+      :resource="resource"
+      :clusterId="clusterId"
+      :suitabilityEnabled="suitabilityEnabled"
+      :autoAssignAllowed="autoAssignAllowed"
+      @select="handleSelect" />
+
+    <a-divider />
+
+    <div class="actions">
+      <a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
+      <a-button type="primary" ref="submit" :disabled="!selectedStoragePool" @click="submitForm">{{ $t('label.ok') }}</a-button>
+    </div>
+
+  </div>
+</template>
+
+<script>
+import StoragePoolSelectView from '@/components/view/StoragePoolSelectView'
+
+export default {
+  name: 'VolumeStoragePoolSelectionForm',
+  components: {
+    StoragePoolSelectView
+  },
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    },
+    clusterId: {
+      type: String,
+      required: false,
+      default: null
+    },
+    suitabilityEnabled: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    autoAssignAllowed: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    isOpen: {
+      type: Boolean,
+      required: false
+    }
+  },
+  data () {
+    return {
+      selectedStoragePool: null
+    }
+  },
+  watch: {
+    isOpen (newValue) {
+      if (newValue) {
+        setTimeout(() => {
+          this.$refs.selectionView.reset()
+        }, 50)
+      }
+    }
+  },
+  methods: {
+    handleSelect (storagePool) {
+      this.selectedStoragePool = storagePool
+    },
+    closeModal () {
+      this.$emit('close-action')
+    },
+    handleKeyboardSubmit () {
+      if (this.selectedStoragePool != null) {
+        this.submitForm()
+      }
+    },
+    submitForm () {
+      this.$emit('select', this.resource.id, this.selectedStoragePool)
+      this.closeModal()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+  .form {
+    width: 80vw;
+
+    @media (min-width: 900px) {
+      width: 850px;
+    }
+  }
+
+  .top-spaced {
+    margin-top: 20px;
+  }
+
+  .actions {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 20px;
+
+    button {
+      &:not(:last-child) {
+        margin-right: 10px;
+      }
+    }
+  }
+</style>
diff --git a/ui/src/main.js b/ui/src/main.js
index 0635b99..7cd5d21 100644
--- a/ui/src/main.js
+++ b/ui/src/main.js
@@ -26,7 +26,7 @@ import './core/lazy_use'
 import './core/ext'
 import './permission' // permission control
 import './utils/filter' // global filter
-import { pollJobPlugin, notifierPlugin, toLocaleDatePlugin, configUtilPlugin, apiMetaUtilPlugin, showIconPlugin, resourceTypePlugin } from './utils/plugins'
+import { pollJobPlugin, notifierPlugin, toLocaleDatePlugin, configUtilPlugin, apiMetaUtilPlugin, showIconPlugin, resourceTypePlugin, fileSizeUtilPlugin } from './utils/plugins'
 import { VueAxios } from './utils/request'
 import './utils/directives'
 
@@ -60,3 +60,4 @@ fetch('config.json').then(response => response.json()).then(config => {
 
 Vue.use(configUtilPlugin)
 Vue.use(apiMetaUtilPlugin)
+Vue.use(fileSizeUtilPlugin)
diff --git a/ui/src/utils/plugins.js b/ui/src/utils/plugins.js
index 99db8f7..6cf48ad 100644
--- a/ui/src/utils/plugins.js
+++ b/ui/src/utils/plugins.js
@@ -299,3 +299,30 @@ export const apiMetaUtilPlugin = {
     }
   }
 }
+
+const KB = 1024
+const MB = 1024 * KB
+const GB = 1024 * MB
+const TB = 1024 * GB
+
+export const fileSizeUtilPlugin = {
+  install (Vue) {
+    Vue.prototype.$bytesToHumanReadableSize = function (bytes) {
+      if (bytes == null) {
+        return ''
+      }
+      if (bytes < KB && bytes >= 0) {
+        return bytes + ' bytes'
+      }
+      if (bytes < MB) {
+        return (bytes / KB).toFixed(2) + ' KB'
+      } else if (bytes < GB) {
+        return (bytes / MB).toFixed(2) + ' MB'
+      } else if (bytes < TB) {
+        return (bytes / GB).toFixed(2) + ' GB'
+      } else {
+        return (bytes / TB).toFixed(2) + ' TB'
+      }
+    }
+  }
+}
diff --git a/ui/src/views/compute/MigrateVMStorage.vue b/ui/src/views/compute/MigrateVMStorage.vue
index c994b56..4acc7d6 100644
--- a/ui/src/views/compute/MigrateVMStorage.vue
+++ b/ui/src/views/compute/MigrateVMStorage.vue
@@ -16,47 +16,53 @@
 // under the License.
 
 <template>
-  <div class="form-layout">
-    <a-spin :spinning="loading">
-      <a-form
-        :form="form"
-        @submit="handleSubmit"
-        layout="vertical">
-        <a-form-item>
-          <tooltip-label slot="label" :title="$t('label.storageid')" :tooltip="apiParams.storageid ? apiParams.storageid.description : ''"/>
-          <a-select
-            :loading="loading"
-            v-decorator="['storageid', {
-              rules: [{ required: true, message: `${this.$t('message.error.required.input')}` }]
-            }]"
-            showSearch
-            optionFilterProp="children"
-            :filterOption="(input, option) => {
-              return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
-            }" >
-            <a-select-option v-for="storagePool in storagePools" :key="storagePool.id">
-              {{ storagePool.name || storagePool.id }}
-            </a-select-option>
-          </a-select>
-        </a-form-item>
+  <div class="form-layout" v-ctrl-enter="handleKeyboardSubmit">
+    <a-alert type="warning">
+      <span slot="message" v-html="$t('message.migrate.instance.to.ps')" />
+    </a-alert>
+    <a-radio-group
+      v-if="migrateVmWithVolumeAllowed"
+      :defaultValue="migrateMode"
+      @change="e => { handleMigrateModeChange(e.target.value) }">
+      <a-radio class="radio-style" :value="1">
+        {{ $t('label.migrate.instance.single.storage') }}
+      </a-radio>
+      <a-radio class="radio-style" :value="2">
+        {{ $t('label.migrate.instance.specific.storages') }}
+      </a-radio>
+    </a-radio-group>
+    <div v-if="migrateMode == 1">
+      <storage-pool-select-view
+        ref="storagePoolSelection"
+        :resource="resource"
+        @select="handleStoragePoolChange" />
+    </div>
+    <instance-volumes-storage-pool-select-list-view
+      v-else
+      :resource="resource"
+      @select="handleVolumeToPoolChange" />
 
-        <div :span="24" class="action-button">
-          <a-button @click="closeAction">{{ this.$t('label.cancel') }}</a-button>
-          <a-button :loading="loading" type="primary" @click="handleSubmit">{{ this.$t('label.ok') }}</a-button>
-        </div>
-      </a-form>
-    </a-spin>
+    <a-divider />
+
+    <div class="actions">
+      <a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
+      <a-button type="primary" :disabled="!formSubmitAllowed" @click="submitForm">{{ $t('label.ok') }}</a-button>
+    </div>
   </div>
 </template>
 
 <script>
 import { api } from '@/api'
 import TooltipLabel from '@/components/widgets/TooltipLabel'
+import StoragePoolSelectView from '@/components/view/StoragePoolSelectView'
+import InstanceVolumesStoragePoolSelectListView from '@/components/view/InstanceVolumesStoragePoolSelectListView'
 
 export default {
   name: 'MigrateVMStorage',
   components: {
-    TooltipLabel
+    TooltipLabel,
+    StoragePoolSelectView,
+    InstanceVolumesStoragePoolSelectListView
   },
   props: {
     resource: {
@@ -66,50 +72,44 @@ export default {
   },
   data () {
     return {
-      loading: false,
-      storagePools: []
+      migrateMode: 1,
+      selectedPool: {},
+      volumeToPoolSelection: []
     }
   },
   beforeCreate () {
     this.form = this.$form.createForm(this)
-    this.apiParams = {}
-    if (this.$route.meta.name === 'vm') {
-      this.apiConfig = this.$store.getters.apis.migrateVirtualMachineWithVolume || {}
-      this.apiConfig.params.forEach(param => {
-        this.apiParams[param.name] = param
-      })
-      this.apiConfig = this.$store.getters.apis.migrateVirtualMachine || {}
-      this.apiConfig.params.forEach(param => {
-        if (!(param.name in this.apiParams)) {
-          this.apiParams[param.name] = param
-        }
-      })
-    } else {
-      this.apiConfig = this.$store.getters.apis.migrateSystemVm || {}
-      this.apiConfig.params.forEach(param => {
-        if (!(param.name in this.apiParams)) {
-          this.apiParams[param.name] = param
+    this.migrateVmWithVolumeApiParams = this.$getApiParams('migrateVirtualMachineWithVolume')
+  },
+  computed: {
+    migrateVmWithVolumeAllowed () {
+      return this.$route.meta.name === 'vm' && this.migrateVmWithVolumeApiParams.hostid && this.migrateVmWithVolumeApiParams.hostid.required === false
+    },
+    formSubmitAllowed () {
+      return this.migrateMode === 2 ? this.volumeToPoolSelection.length > 0 : this.selectedPool.id
+    },
+    isSelectedVolumeOnlyClusterStoragePoolVolume () {
+      if (this.volumesWithClusterStoragePool.length !== 1) {
+        return false
+      }
+      for (const volume of this.volumesWithClusterStoragePool) {
+        if (volume.id === this.selectedVolumeForStoragePoolSelection.id) {
+          return true
         }
-      })
+      }
+      return false
     }
   },
-  created () {
-  },
-  mounted () {
-    this.fetchData()
-  },
   methods: {
     fetchData () {
-      this.loading = true
-      api('listStoragePools', {
-        zoneid: this.resource.zoneid
-      }).then(response => {
-        if (this.arrayHasItems(response.liststoragepoolsresponse.storagepool)) {
-          this.storagePools = response.liststoragepoolsresponse.storagepool
-        }
-      }).finally(() => {
-        this.loading = false
-      })
+      if (this.migrateMode === 2) {
+        this.fetchVolumes()
+      }
+    },
+    handleMigrateModeChange (value) {
+      this.migrateMode = value
+      this.selectedPool = {}
+      this.volumeToPoolSelection = []
     },
     isValidValueForKey (obj, key) {
       return key in obj && obj[key] != null
@@ -120,89 +120,70 @@ export default {
     isObjectEmpty (obj) {
       return !(obj !== null && obj !== undefined && Object.keys(obj).length > 0 && obj.constructor === Object)
     },
-    handleSubmit (e) {
-      e.preventDefault()
-      this.form.validateFieldsAndScroll((err, values) => {
-        if (err) {
-          return
-        }
-        this.loading = true
-        var isUserVm = true
-        if (this.$route.meta.name !== 'vm') {
-          isUserVm = false
-        }
-        var migrateApi = isUserVm ? 'migrateVirtualMachine' : 'migrateSystemVm'
-        if (isUserVm && this.apiParams.hostid && this.apiParams.hostid.required === false) {
-          migrateApi = 'migrateVirtualMachineWithVolume'
-          var rootVolume = null
-          api('listVolumes', {
-            listAll: true,
-            virtualmachineid: this.resource.id
-          }).then(response => {
-            var volumes = response.listvolumesresponse.volume
-            if (volumes && volumes.length > 0) {
-              volumes = volumes.filter(item => item.type === 'ROOT')
-              if (volumes && volumes.length > 0) {
-                rootVolume = volumes[0]
-              }
-              if (rootVolume == null) {
-                this.$message.error('Failed to find ROOT volume for the VM ' + this.resource.id)
-                this.closeAction()
-              }
-              this.migrateVm(migrateApi, values.storageid, rootVolume.id)
-            }
-          })
-          return
-        }
-        this.migrateVm(migrateApi, values.storageid, null)
-      })
+    handleStoragePoolChange (storagePool) {
+      this.selectedPool = storagePool
+    },
+    handleVolumeToPoolChange (volumeToPool) {
+      this.volumeToPoolSelection = volumeToPool
+    },
+    handleKeyboardSubmit () {
+      if (this.formSubmitAllowed) {
+        this.submitForm()
+      }
+    },
+    submitForm () {
+      var isUserVm = true
+      if (this.$route.meta.name !== 'vm') {
+        isUserVm = false
+      }
+      var migrateApi = isUserVm ? 'migrateVirtualMachine' : 'migrateSystemVm'
+      if (isUserVm && this.migrateMode === 2) {
+        migrateApi = 'migrateVirtualMachineWithVolume'
+        this.migrateVm(migrateApi, null, this.volumeToPoolSelection)
+        return
+      }
+      this.migrateVm(migrateApi, this.selectedPool.id, null)
     },
-    migrateVm (migrateApi, storageId, rootVolumeId) {
+    migrateVm (migrateApi, storageId, volumeToPool) {
       var params = {
-        virtualmachineid: this.resource.id,
-        storageid: storageId
+        virtualmachineid: this.resource.id
       }
-      if (rootVolumeId !== null) {
-        params = {
-          virtualmachineid: this.resource.id,
-          'migrateto[0].volume': rootVolumeId,
-          'migrateto[0].pool': storageId
+      if (this.migrateMode === 2) {
+        for (var i = 0; i < volumeToPool.length; i++) {
+          const mapping = volumeToPool[i]
+          params['migrateto[' + i + '].volume'] = mapping.volume
+          params['migrateto[' + i + '].pool'] = mapping.pool
         }
+      } else {
+        params.storageid = storageId
       }
       api(migrateApi, params).then(response => {
-        var jobId = ''
-        if (migrateApi === 'migrateVirtualMachineWithVolume') {
-          jobId = response.migratevirtualmachinewithvolumeresponse.jobid
-        } else if (migrateApi === 'migrateSystemVm') {
-          jobId = response.migratesystemvmresponse.jobid
-        } else {
-          jobId = response.migratevirtualmachineresponse.jobid
-        }
+        const jobId = response[migrateApi.toLowerCase() + 'response'].jobid
         this.$pollJob({
           title: `${this.$t('label.migrating')} ${this.resource.name}`,
           description: this.resource.name,
           jobId: jobId,
           successMessage: `${this.$t('message.success.migrating')} ${this.resource.name}`,
           successMethod: () => {
-            this.$parent.$parent.close()
+            this.closeModal()
           },
           errorMessage: this.$t('message.migrating.failed'),
           errorMethod: () => {
-            this.$parent.$parent.close()
+            this.closeModal()
           },
           loadingMessage: `${this.$t('message.migrating.processing')} ${this.resource.name}`,
           catchMessage: this.$t('error.fetching.async.job.result'),
           catchMethod: () => {
-            this.$parent.$parent.close()
+            this.closeModal()
           }
         })
-        this.$parent.$parent.close()
+        this.closeModal()
       }).catch(error => {
         console.error(error)
         this.$message.error(`${this.$t('message.migrating.vm.to.storage.failed')} ${storageId}`)
       })
     },
-    closeAction () {
+    closeModal () {
       this.$emit('close-action')
     }
   }
@@ -211,18 +192,33 @@ export default {
 
 <style scoped lang="less">
   .form-layout {
-    width: 60vw;
+    width: 80vw;
 
-    @media (min-width: 500px) {
-      width: 450px;
+    @media (min-width: 900px) {
+      width: 850px;
     }
   }
 
-  .action-button {
-    text-align: right;
+  .top-spaced {
+    margin-top: 20px;
+  }
+
+  .radio-style {
+    display: block;
+    margin-left: 10px;
+    height: 40px;
+    line-height: 40px;
+  }
+
+  .actions {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 20px;
 
     button {
-      margin-right: 5px;
+      &:not(:last-child) {
+        margin-right: 10px;
+      }
     }
   }
 </style>
diff --git a/ui/src/views/compute/MigrateWizard.vue b/ui/src/views/compute/MigrateWizard.vue
index 3107998..45eff22 100644
--- a/ui/src/views/compute/MigrateWizard.vue
+++ b/ui/src/views/compute/MigrateWizard.vue
@@ -16,14 +16,18 @@
 // under the License.
 
 <template>
-  <div class="form" v-ctrl-enter="submitForm">
+  <div class="form" v-ctrl-enter="handleKeyboardSubmit">
+    <a-alert type="warning">
+      <span slot="message" v-html="$t('message.migrate.instance.to.host')" />
+    </a-alert>
     <a-input-search
+      class="top-spaced"
       :placeholder="$t('label.search')"
       v-model="searchQuery"
-      style="margin-bottom: 10px;"
       @search="fetchData"
       autoFocus />
     <a-table
+      class="top-spaced"
       size="small"
       style="overflow-y: auto"
       :loading="loading"
@@ -31,6 +35,12 @@
       :dataSource="hosts"
       :pagination="false"
       :rowKey="record => record.id">
+      <div slot="name" slot-scope="record">
+        {{ record.name }}
+        <a-tooltip v-if="record.name === $t('label.auto.assign')" :title="$t('message.migrate.instance.host.auto.assign')" placement="top">
+          <a-icon type="info-circle" class="table-tooltip-icon" />
+        </a-tooltip>
+      </div>
       <div slot="suitability" slot-scope="record">
         <a-icon
           class="host-item__suitability-icon"
@@ -47,7 +57,7 @@
       </div>
       <div slot="memused" slot-scope="record">
         <span v-if="record.memoryused">
-          {{ record.memoryused | byteToGigabyte }} GB
+          {{ $bytesToHumanReadableSize(record.memoryused) }}
         </span>
       </div>
       <div slot="memoryallocatedpercentage" slot-scope="record">
@@ -65,7 +75,7 @@
       <template slot="select" slot-scope="record">
         <a-radio
           class="host-item__radio"
-          @click="selectedHost = record"
+          @click="handleSelectedHostChange(record)"
           :checked="record.id === selectedHost.id"
           :disabled="!record.suitableformigration"></a-radio>
       </template>
@@ -86,10 +96,27 @@
       </template>
     </a-pagination>
 
-    <div style="margin-top: 20px; display: flex; justify-content:flex-end;">
-      <a-button type="primary" ref="submit" :disabled="!selectedHost.id" @click="submitForm">
-        {{ $t('label.ok') }}
-      </a-button>
+    <a-form-item
+      v-if="isUserVm"
+      class="top-spaced">
+      <tooltip-label slot="label" :title="$t('label.migrate.with.storage')" :tooltip="$t('message.migrate.with.storage')"/>
+      <a-switch
+        v-model="migrateWithStorage"
+        :disabled="!selectedHost || !selectedHost.id || selectedHost.id === -1" />
+    </a-form-item>
+    <instance-volumes-storage-pool-select-list-view
+      ref="volumeToPoolSelect"
+      v-if="migrateWithStorage"
+      class="top-spaced"
+      :resource="resource"
+      :clusterId="selectedHost.id ? selectedHost.clusterid : null"
+      @select="handleVolumeToPoolChange" />
+
+    <a-divider />
+
+    <div class="actions">
+      <a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
+      <a-button type="primary" ref="submit" :disabled="!selectedHost.id" @click="submitForm">{{ $t('label.ok') }}</a-button>
     </div>
   </div>
 
@@ -97,9 +124,15 @@
 
 <script>
 import { api } from '@/api'
+import TooltipLabel from '@/components/widgets/TooltipLabel'
+import InstanceVolumesStoragePoolSelectListView from '@/components/view/InstanceVolumesStoragePoolSelectListView'
 
 export default {
   name: 'VMMigrateWizard',
+  components: {
+    TooltipLabel,
+    InstanceVolumesStoragePoolSelectListView
+  },
   props: {
     resource: {
       type: Object,
@@ -117,8 +150,8 @@ export default {
       pageSize: 10,
       columns: [
         {
-          title: this.$t('label.name'),
-          dataIndex: 'name'
+          title: this.$t('label.hostid'),
+          scopedSlots: { customRender: 'name' }
         },
         {
           title: this.$t('label.suitability'),
@@ -152,13 +185,30 @@ export default {
           title: this.$t('label.select'),
           scopedSlots: { customRender: 'select' }
         }
-      ]
+      ],
+      migrateWithStorage: false,
+      volumeToPoolSelection: []
     }
   },
   created () {
     this.fetchData()
   },
+  computed: {
+    isUserVm () {
+      return this.$route.meta.name === 'vm'
+    }
+  },
+  watch: {
+    searchQuery (newValue, oldValue) {
+      if (newValue !== oldValue) {
+        this.page = 1
+      }
+    }
+  },
   methods: {
+    arrayHasItems (array) {
+      return array !== null && array !== undefined && Array.isArray(array) && array.length > 0
+    },
     fetchData () {
       this.loading = true
       api('findHostsForMigration', {
@@ -173,7 +223,7 @@ export default {
         })
         for (const key in this.hosts) {
           if (this.hosts[key].suitableformigration && !this.hosts[key].requiresstoragemigration) {
-            this.hosts.unshift({ id: -1, name: this.$t('label.migrate.auto.select'), suitableformigration: true, requiresstoragemigration: false })
+            this.hosts.unshift({ id: -1, name: this.$t('label.auto.assign'), suitableformigration: true, requiresstoragemigration: false })
             break
           }
         }
@@ -184,41 +234,77 @@ export default {
         this.loading = false
       })
     },
+    handleChangePage (page, pageSize) {
+      this.page = page
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    handleChangePageSize (currentPage, pageSize) {
+      this.page = currentPage
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    handleSelectedHostChange (host) {
+      if (host.id === -1) {
+        this.migrateWithStorage = false
+      }
+      this.selectedHost = host
+      this.selectedVolumeForStoragePoolSelection = {}
+      this.volumeToPoolSelection = []
+      if (this.migrateWithStorage) {
+        this.$refs.volumeToPoolSelect.resetSelection()
+      }
+    },
+    handleVolumeToPoolChange (volumeToPool) {
+      this.volumeToPoolSelection = volumeToPool
+    },
+    handleKeyboardSubmit () {
+      if (this.selectedHost.id) {
+        this.submitForm()
+      }
+    },
+    closeModal () {
+      this.$emit('close-action')
+    },
     submitForm () {
       if (this.loading) return
       this.loading = true
-      var isUserVm = true
-      if (this.$route.meta.name !== 'vm') {
-        isUserVm = false
-      }
-      var migrateApi = isUserVm
-        ? this.selectedHost.requiresStorageMotion ? 'migrateVirtualMachineWithVolume' : 'migrateVirtualMachine'
+      const migrateApi = this.isUserVm
+        ? (this.selectedHost.requiresStorageMotion || this.volumeToPoolSelection.length > 0)
+          ? 'migrateVirtualMachineWithVolume'
+          : 'migrateVirtualMachine'
         : 'migrateSystemVm'
-      var migrateParams = this.selectedHost.id === -1 ? { autoselect: true, virtualmachineid: this.resource.id }
+      var params = this.selectedHost.id === -1
+        ? { autoselect: true, virtualmachineid: this.resource.id }
         : { hostid: this.selectedHost.id, virtualmachineid: this.resource.id }
-      api(migrateApi, migrateParams).then(response => {
-        const jobid = isUserVm
-          ? this.selectedHost.requiresStorageMotion ? response.migratevirtualmachinewithvolumeresponse.jobid : response.migratevirtualmachineresponse.jobid
-          : response.migratesystemvmresponse.jobid
+      if (this.migrateWithStorage) {
+        for (var i = 0; i < this.volumeToPoolSelection.length; i++) {
+          const mapping = this.volumeToPoolSelection[i]
+          params['migrateto[' + i + '].volume'] = mapping.volume
+          params['migrateto[' + i + '].pool'] = mapping.pool
+        }
+      }
+      api(migrateApi, params).then(response => {
+        const jobId = response[migrateApi.toLowerCase() + 'response'].jobid
         this.$pollJob({
-          jobId: jobid,
+          jobId: jobId,
           title: `${this.$t('label.migrating')} ${this.resource.name}`,
           description: this.resource.name,
           successMessage: `${this.$t('message.success.migrating')} ${this.resource.name}`,
           successMethod: () => {
-            this.$emit('close-action')
+            this.closeModal()
           },
           errorMessage: this.$t('message.migrating.failed'),
           errorMethod: () => {
-            this.$emit('close-action')
+            this.closeModal()
           },
           loadingMessage: `${this.$t('message.migrating.processing')} ${this.resource.name}`,
           catchMessage: this.$t('error.fetching.async.job.result'),
           catchMethod: () => {
-            this.$emit('close-action')
+            this.closeModal()
           }
         })
-        this.$emit('close-action')
+        this.closeModal()
       }).catch(error => {
         this.$notification.error({
           message: this.$t('message.request.failed'),
@@ -228,21 +314,6 @@ export default {
       }).finally(() => {
         this.loading = false
       })
-    },
-    handleChangePage (page, pageSize) {
-      this.page = page
-      this.pageSize = pageSize
-      this.fetchData()
-    },
-    handleChangePageSize (currentPage, pageSize) {
-      this.page = currentPage
-      this.pageSize = pageSize
-      this.fetchData()
-    }
-  },
-  filters: {
-    byteToGigabyte: value => {
-      return (value / Math.pow(10, 9)).toFixed(2)
     }
   }
 }
@@ -308,7 +379,26 @@ export default {
 
   }
 
+  .top-spaced {
+    margin-top: 20px;
+  }
+
   .pagination {
     margin-top: 20px;
   }
+
+  .actions {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 20px;
+
+    button {
+      &:not(:last-child) {
+        margin-right: 10px;
+      }
+    }
+  }
+  .table-tooltip-icon {
+    color: rgba(0,0,0,.45);
+  }
 </style>
diff --git a/ui/src/views/storage/MigrateVolume.vue b/ui/src/views/storage/MigrateVolume.vue
index b584aca..15665bb 100644
--- a/ui/src/views/storage/MigrateVolume.vue
+++ b/ui/src/views/storage/MigrateVolume.vue
@@ -16,63 +16,47 @@
 // under the License.
 
 <template>
-  <div class="migrate-volume-container" v-ctrl-enter="submitMigrateVolume">
-    <div class="modal-form">
-      <div v-if="storagePools.length > 0">
-        <a-alert type="warning">
-          <span slot="message" v-html="$t('message.migrate.volume')" />
-        </a-alert>
-        <p class="modal-form__label">{{ $t('label.storagepool') }}</p>
-        <a-select
-          v-model="selectedStoragePool"
-          style="width: 100%;"
-          :autoFocus="storagePools.length > 0"
-          showSearch
-          optionFilterProp="children"
-          :filterOption="(input, option) => {
-            return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
-          }" >
-          <a-select-option v-for="(storagePool, index) in storagePools" :value="storagePool.id" :key="index">
-            {{ storagePool.name }} <span v-if="resource.virtualmachineid">{{ storagePool.suitableformigration ? `(${$t('label.suitable')})` : `(${$t('label.not.suitable')})` }}</span>
-          </a-select-option>
-        </a-select>
-        <template v-if="this.resource.virtualmachineid">
-          <p class="modal-form__label" @click="replaceDiskOffering = !replaceDiskOffering" style="cursor:pointer;">
-            {{ $t('label.usenewdiskoffering') }}
-          </p>
-          <a-checkbox v-model="replaceDiskOffering" />
+  <div class="form" v-ctrl-enter="handleKeyboardSubmit">
+    <a-alert class="top-spaced" type="warning">
+      <span slot="message" v-html="$t('message.migrate.volume')" />
+    </a-alert>
+    <storage-pool-select-view
+      ref="storagePoolSelection"
+      :resource="resource"
+      :suitabilityEnabled="true"
+      @storagePoolsUpdated="handleStoragePoolsChange"
+      @select="handleStoragePoolSelect" />
+    <div class="top-spaced" v-if="storagePools.length > 0">
+      <template v-if="this.resource.virtualmachineid">
+        <p class="modal-form__label" @click="replaceDiskOffering = !replaceDiskOffering" style="cursor:pointer;">
+          {{ $t('label.usenewdiskoffering') }}
+        </p>
+        <a-checkbox v-model="replaceDiskOffering" />
 
-          <template v-if="replaceDiskOffering">
-            <p class="modal-form__label">{{ $t('label.newdiskoffering') }}</p>
-            <a-select
-              v-model="selectedDiskOffering"
-              style="width: 100%;"
-              showSearch
-              optionFilterProp="children"
-              :filterOption="(input, option) => {
-                return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
-              }" >
-              <a-select-option v-for="(diskOffering, index) in diskOfferings" :value="diskOffering.id" :key="index">
-                {{ diskOffering.displaytext }}
-              </a-select-option>
-            </a-select>
-          </template>
+        <template v-if="replaceDiskOffering">
+          <p class="modal-form__label">{{ $t('label.newdiskoffering') }}</p>
+          <a-select
+            :loading="diskOfferingLoading"
+            v-model="selectedDiskOffering"
+            style="width: 100%;"
+            showSearch
+            optionFilterProp="children"
+            :filterOption="(input, option) => {
+              return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
+            }" >
+            <a-select-option v-for="(diskOffering, index) in diskOfferings" :value="diskOffering.id" :key="index">
+              {{ diskOffering.displaytext }}
+            </a-select-option>
+          </a-select>
         </template>
-      </div>
-      <a-alert style="margin-top: 15px" type="warning" v-else>
-        <span slot="message" v-html="$t('message.no.primary.stores')" />
-      </a-alert>
+      </template>
     </div>
 
     <a-divider />
 
     <div class="actions">
-      <a-button @click="closeModal">
-        {{ $t('label.cancel') }}
-      </a-button>
-      <a-button type="primary" ref="submit" @click="submitMigrateVolume">
-        {{ $t('label.ok') }}
-      </a-button>
+      <a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
+      <a-button type="primary" ref="submit" :disabled="!selectedStoragePool" @click="submitForm">{{ $t('label.ok') }}</a-button>
     </div>
 
   </div>
@@ -80,9 +64,13 @@
 
 <script>
 import { api } from '@/api'
+import StoragePoolSelectView from '@/components/view/StoragePoolSelectView'
 
 export default {
   name: 'MigrateVolume',
+  components: {
+    StoragePoolSelectView
+  },
   props: {
     resource: {
       type: Object,
@@ -95,71 +83,67 @@ export default {
       storagePools: [],
       selectedStoragePool: null,
       diskOfferings: [],
+      diskOfferingLoading: false,
       replaceDiskOffering: false,
       selectedDiskOffering: null,
       isSubmitted: false
     }
   },
-  created () {
-    this.fetchStoragePools()
-    this.resource.virtualmachineid && this.fetchDiskOfferings()
+  watch: {
+    replaceDiskOffering (newValue) {
+      if (newValue) {
+        this.fetchDiskOfferings()
+      }
+    }
   },
   methods: {
-    fetchStoragePools () {
-      if (this.resource.virtualmachineid) {
-        api('findStoragePoolsForMigration', {
-          id: this.resource.id
-        }).then(response => {
-          this.storagePools = response.findstoragepoolsformigrationresponse.storagepool || []
-          if (Array.isArray(this.storagePools) && this.storagePools.length) {
-            this.selectedStoragePool = this.storagePools[0].id || ''
-          }
-        }).catch(error => {
-          this.$notifyError(error)
-          this.closeModal()
-        })
-      } else {
-        api('listStoragePools', {
-          zoneid: this.resource.zoneid
-        }).then(response => {
-          this.storagePools = response.liststoragepoolsresponse.storagepool || []
-          this.storagePools = this.storagePools.filter(pool => { return pool.id !== this.resource.storageid })
-          if (Array.isArray(this.storagePools) && this.storagePools.length) {
-            this.selectedStoragePool = this.storagePools[0].id || ''
-          }
-        }).catch(error => {
-          this.$notifyError(error)
-          this.closeModal()
-        })
-      }
-    },
     fetchDiskOfferings () {
+      this.diskOfferingLoading = true
       api('listDiskOfferings', {
         listall: true
       }).then(response => {
         this.diskOfferings = response.listdiskofferingsresponse.diskoffering
-        this.selectedDiskOffering = this.diskOfferings[0].id
       }).catch(error => {
         this.$notifyError(error)
         this.closeModal()
+      }).finally(() => {
+        this.diskOfferingLoading = false
+        if (this.diskOfferings.length > 0) {
+          this.selectedDiskOffering = this.diskOfferings[0].id
+        }
       })
     },
+    handleStoragePoolsChange (storagePools) {
+      this.storagePools = storagePools
+    },
+    handleStoragePoolSelect (storagePool) {
+      this.selectedStoragePool = storagePool
+    },
+    handleKeyboardSubmit () {
+      if (!this.selectedStoragePool) {
+        return
+      }
+      this.submitForm()
+    },
     closeModal () {
-      this.$parent.$parent.close()
+      this.$emit('close-action')
     },
-    submitMigrateVolume () {
+    submitForm () {
       if (this.isSubmitted) return
       if (this.storagePools.length === 0) {
         this.closeModal()
         return
       }
       this.isSubmitted = true
-      api('migrateVolume', {
+      var params = {
         livemigrate: this.resource.vmstate === 'Running',
-        storageid: this.selectedStoragePool,
-        volumeid: this.resource.id,
-        newdiskofferingid: this.replaceDiskOffering ? this.selectedDiskOffering : null
-      }).then(response => {
+        storageid: this.selectedStoragePool.id,
+        volumeid: this.resource.id
+      }
+      if (this.replaceDiskOffering) {
+        params.newdiskofferingid = this.selectedDiskOffering
+      }
+      api('migrateVolume', params).then(response => {
         this.$pollJob({
           jobId: response.migratevolumeresponse.jobid,
           successMessage: this.$t('message.success.migrate.volume'),
@@ -185,14 +169,18 @@ export default {
 </script>
 
 <style scoped lang="scss">
-  .migrate-volume-container {
-    width: 85vw;
+  .form {
+    width: 80vw;
 
-    @media (min-width: 760px) {
-      width: 500px;
+    @media (min-width: 900px) {
+      width: 850px;
     }
   }
 
+  .top-spaced {
+    margin-top: 20px;
+  }
+
   .actions {
     display: flex;
     justify-content: flex-end;
@@ -204,14 +192,4 @@ export default {
       }
     }
   }
-
-  .modal-form {
-    margin-top: -20px;
-
-    &__label {
-      margin-top: 10px;
-      margin-bottom: 5px;
-    }
-
-  }
 </style>
diff --git a/ui/tests/common/index.js b/ui/tests/common/index.js
index e152c71..bd1d6d6 100644
--- a/ui/tests/common/index.js
+++ b/ui/tests/common/index.js
@@ -21,7 +21,7 @@ import mockRouter from '../mock/mockRouter'
 
 import localVue from '../setup'
 import { mount } from '@vue/test-utils'
-import { pollJobPlugin, notifierPlugin, configUtilPlugin, apiMetaUtilPlugin, toLocaleDatePlugin, showIconPlugin, resourceTypePlugin } from '@/utils/plugins'
+import { pollJobPlugin, notifierPlugin, configUtilPlugin, apiMetaUtilPlugin, toLocaleDatePlugin, showIconPlugin, resourceTypePlugin, fileSizeUtilPlugin } from '@/utils/plugins'
 
 localVue.use(pollJobPlugin)
 localVue.use(notifierPlugin)
@@ -30,6 +30,7 @@ localVue.use(apiMetaUtilPlugin)
 localVue.use(toLocaleDatePlugin)
 localVue.use(showIconPlugin)
 localVue.use(resourceTypePlugin)
+localVue.use(fileSizeUtilPlugin)
 
 function createMockRouter (newRoutes = []) {
   let routes = []