mirror of https://github.com/apache/cloudstack.git
ui: option to migrate vm with volumes to same pool (#11703)
Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
parent
8dcfc7c767
commit
8627c60b95
|
|
@ -380,6 +380,7 @@
|
|||
"label.app.name": "CloudStack",
|
||||
"label.application.policy.set": "Application Policy Set",
|
||||
"label.apply": "Apply",
|
||||
"label.apply.to.all": "Apply to all",
|
||||
"label.apply.tungsten.firewall.policy": "Apply Firewall Policy",
|
||||
"label.apply.tungsten.network.policy": "Apply Network Policy",
|
||||
"label.apply.tungsten.tag": "Apply tag",
|
||||
|
|
@ -3692,6 +3693,7 @@
|
|||
"message.vnf.nic.move.down.fail": "Failed to move down this NIC",
|
||||
"message.vnf.no.credentials": "No credentials found for the VNF appliance.",
|
||||
"message.vnf.select.networks": "Please select the relevant network for each VNF NIC.",
|
||||
"message.volume.pool.apply.to.all": "Selected storage pool will be applied to all existing volumes of the instance.",
|
||||
"message.volume.state.allocated": "The volume is allocated but has not been created yet.",
|
||||
"message.volume.state.attaching": "The volume is attaching to a volume from Ready state.",
|
||||
"message.volume.state.copying": "The volume is being copied from the image store to primary storage, in case it's an uploaded volume.",
|
||||
|
|
|
|||
|
|
@ -206,13 +206,19 @@ export default {
|
|||
closeVolumeStoragePoolSelector () {
|
||||
this.selectedVolumeForStoragePoolSelection = {}
|
||||
},
|
||||
handleVolumeStoragePoolSelection (volumeId, storagePool) {
|
||||
handleVolumeStoragePoolSelection (volumeId, storagePool, applyToAll) {
|
||||
for (const volume of this.volumes) {
|
||||
if (volume.id === volumeId) {
|
||||
if (applyToAll) {
|
||||
volume.selectedstorageid = storagePool.id
|
||||
volume.selectedstoragename = storagePool.name
|
||||
volume.selectedstorageclusterid = storagePool.clusterid
|
||||
break
|
||||
} else {
|
||||
if (volume.id === volumeId) {
|
||||
volume.selectedstorageid = storagePool.id
|
||||
volume.selectedstoragename = storagePool.name
|
||||
volume.selectedstorageclusterid = storagePool.clusterid
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
this.updateVolumeToStoragePoolSelection()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,15 @@
|
|||
:autoAssignAllowed="autoAssignAllowed"
|
||||
@select="handleSelect" />
|
||||
|
||||
<a-form-item
|
||||
class="top-spaced">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.apply.to.all')" :tooltip="$t('message.volume.pool.apply.to.all')"/>
|
||||
</template>
|
||||
<a-switch
|
||||
v-model:checked="applyToAll" />
|
||||
</a-form-item>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<div class="actions">
|
||||
|
|
@ -36,11 +45,13 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import TooltipLabel from '@/components/widgets/TooltipLabel'
|
||||
import StoragePoolSelectView from '@/components/view/StoragePoolSelectView'
|
||||
|
||||
export default {
|
||||
name: 'VolumeStoragePoolSelectionForm',
|
||||
components: {
|
||||
TooltipLabel,
|
||||
StoragePoolSelectView
|
||||
},
|
||||
props: {
|
||||
|
|
@ -70,7 +81,8 @@ export default {
|
|||
},
|
||||
data () {
|
||||
return {
|
||||
selectedStoragePool: null
|
||||
selectedStoragePool: null,
|
||||
applyToAll: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -95,7 +107,7 @@ export default {
|
|||
}
|
||||
},
|
||||
submitForm () {
|
||||
this.$emit('select', this.resource.id, this.selectedStoragePool)
|
||||
this.$emit('select', this.resource.id, this.selectedStoragePool, this.applyToAll)
|
||||
this.closeModal()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
class="top-spaced"
|
||||
:placeholder="$t('label.search')"
|
||||
v-model:value="searchQuery"
|
||||
@search="fetchData"
|
||||
@search="fetchHostsForMigration"
|
||||
v-focus="true" />
|
||||
<a-table
|
||||
class="top-spaced"
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
</a-pagination>
|
||||
|
||||
<a-form-item
|
||||
v-if="isUserVm"
|
||||
v-if="isUserVm && hasVolumes"
|
||||
class="top-spaced">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.migrate.with.storage')" :tooltip="$t('message.migrate.with.storage')"/>
|
||||
|
|
@ -106,9 +106,29 @@
|
|||
v-model:checked="migrateWithStorage"
|
||||
:disabled="!selectedHost || !selectedHost.id || selectedHost.id === -1" />
|
||||
</a-form-item>
|
||||
|
||||
<a-radio-group
|
||||
v-if="migrateWithStorage"
|
||||
v-model:value="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="migrateWithStorage && migrateMode == 1">
|
||||
<storage-pool-select-view
|
||||
ref="storagePoolSelection"
|
||||
:autoAssignAllowed="false"
|
||||
:resource="resource"
|
||||
@select="handleStoragePoolChange" />
|
||||
</div>
|
||||
<instance-volumes-storage-pool-select-list-view
|
||||
ref="volumeToPoolSelect"
|
||||
v-if="migrateWithStorage"
|
||||
v-if="migrateWithStorage && migrateMode !== 1"
|
||||
class="top-spaced"
|
||||
:resource="resource"
|
||||
:clusterId="selectedHost.id ? selectedHost.clusterid : null"
|
||||
|
|
@ -118,7 +138,7 @@
|
|||
|
||||
<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>
|
||||
<a-button type="primary" ref="submit" :disabled="!selectedHost.id || (migrateWithStorage && migrateMode === 1 && !volumeToPoolSelection.length)" @click="submitForm">{{ $t('label.ok') }}</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -126,12 +146,14 @@
|
|||
<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: 'VMMigrateWizard',
|
||||
components: {
|
||||
TooltipLabel,
|
||||
StoragePoolSelectView,
|
||||
InstanceVolumesStoragePoolSelectListView
|
||||
},
|
||||
props: {
|
||||
|
|
@ -188,6 +210,7 @@ export default {
|
|||
}
|
||||
],
|
||||
migrateWithStorage: false,
|
||||
migrateMode: 1,
|
||||
volumeToPoolSelection: [],
|
||||
volumes: []
|
||||
}
|
||||
|
|
@ -198,6 +221,9 @@ export default {
|
|||
computed: {
|
||||
isUserVm () {
|
||||
return this.$route.meta.resourceType === 'UserVm'
|
||||
},
|
||||
hasVolumes () {
|
||||
return this.volumes && this.volumes.length > 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -212,6 +238,10 @@ export default {
|
|||
return array !== null && array !== undefined && Array.isArray(array) && array.length > 0
|
||||
},
|
||||
fetchData () {
|
||||
this.fetchHostsForMigration()
|
||||
this.fetchVolumes()
|
||||
},
|
||||
fetchHostsForMigration () {
|
||||
this.loading = true
|
||||
api('findHostsForMigration', {
|
||||
virtualmachineid: this.resource.id,
|
||||
|
|
@ -239,17 +269,16 @@ export default {
|
|||
handleChangePage (page, pageSize) {
|
||||
this.page = page
|
||||
this.pageSize = pageSize
|
||||
this.fetchData()
|
||||
this.fetchHostsForMigration()
|
||||
},
|
||||
handleChangePageSize (currentPage, pageSize) {
|
||||
this.page = currentPage
|
||||
this.pageSize = pageSize
|
||||
this.fetchData()
|
||||
this.fetchHostsForMigration()
|
||||
},
|
||||
handleSelectedHostChange (host) {
|
||||
if (host.id === -1) {
|
||||
this.migrateWithStorage = false
|
||||
this.fetchVolumes()
|
||||
}
|
||||
this.selectedHost = host
|
||||
this.selectedVolumeForStoragePoolSelection = {}
|
||||
|
|
@ -258,6 +287,17 @@ export default {
|
|||
this.$refs.volumeToPoolSelect.resetSelection()
|
||||
}
|
||||
},
|
||||
handleMigrateModeChange () {
|
||||
this.volumeToPoolSelection = []
|
||||
},
|
||||
handleStoragePoolChange (storagePool) {
|
||||
this.volumeToPoolSelection = []
|
||||
for (const volume of this.volumes) {
|
||||
if (storagePool && storagePool.id && storagePool.id !== -1) {
|
||||
this.volumeToPoolSelection.push({ volume: volume.id, pool: storagePool.id })
|
||||
}
|
||||
}
|
||||
},
|
||||
handleVolumeToPoolChange (volumeToPool) {
|
||||
this.volumeToPoolSelection = volumeToPool
|
||||
},
|
||||
|
|
@ -268,7 +308,7 @@ export default {
|
|||
listAll: true,
|
||||
virtualmachineid: this.resource.id
|
||||
}).then(response => {
|
||||
this.volumes = response.listvolumesresponse.volume
|
||||
this.volumes = response?.listvolumesresponse?.volume || []
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
|
|
@ -277,7 +317,7 @@ export default {
|
|||
if (this.selectedHost.requiresStorageMotion || this.volumeToPoolSelection.length > 0) {
|
||||
return true
|
||||
}
|
||||
if (this.selectedHost.id === -1 && this.volumes && this.volumes.length > 0) {
|
||||
if (this.selectedHost.id === -1 && this.hasVolumes) {
|
||||
for (var volume of this.volumes) {
|
||||
if (volume.storagetype === 'local') {
|
||||
return true
|
||||
|
|
@ -305,7 +345,7 @@ export default {
|
|||
var params = this.selectedHost.id === -1
|
||||
? { autoselect: true, virtualmachineid: this.resource.id }
|
||||
: { hostid: this.selectedHost.id, virtualmachineid: this.resource.id }
|
||||
if (this.migrateWithStorage) {
|
||||
if (this.migrateWithStorage && this.volumeToPoolSelection && this.volumeToPoolSelection.length > 0) {
|
||||
for (var i = 0; i < this.volumeToPoolSelection.length; i++) {
|
||||
const mapping = this.volumeToPoolSelection[i]
|
||||
params['migrateto[' + i + '].volume'] = mapping.volume
|
||||
|
|
|
|||
|
|
@ -126,8 +126,8 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
if (Object.keys(originalFunc).length > 0) {
|
||||
Object.keys(originalFunc).forEach(key => {
|
||||
switch (key) {
|
||||
case 'fetchData':
|
||||
wrapper.vm.fetchData = originalFunc[key]
|
||||
case 'fetchHostsForMigration':
|
||||
wrapper.vm.fetchHostsForMigration = originalFunc[key]
|
||||
break
|
||||
default:
|
||||
break
|
||||
|
|
@ -137,11 +137,11 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
})
|
||||
|
||||
describe('Methods', () => {
|
||||
describe('fetchData()', () => {
|
||||
describe('fetchHostsForMigration()', () => {
|
||||
it('API should be called with resource is empty and searchQuery is empty', async (done) => {
|
||||
await mockAxios.mockResolvedValue({ findhostsformigrationresponse: { count: 0, host: [] } })
|
||||
await wrapper.setProps({ resource: {} })
|
||||
await wrapper.vm.fetchData()
|
||||
await wrapper.vm.fetchHostsForMigration()
|
||||
await flushPromises()
|
||||
|
||||
expect(mockAxios).toHaveBeenCalled()
|
||||
|
|
@ -164,7 +164,7 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
it('API should be called with resource.id is empty and searchQuery is empty', async (done) => {
|
||||
await mockAxios.mockResolvedValue({ findhostsformigrationresponse: { count: 0, host: [] } })
|
||||
await wrapper.setProps({ resource: { id: null } })
|
||||
await wrapper.vm.fetchData()
|
||||
await wrapper.vm.fetchHostsForMigration()
|
||||
await flushPromises()
|
||||
|
||||
expect(mockAxios).toHaveBeenCalled()
|
||||
|
|
@ -187,7 +187,7 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
it('API should be called with resource.id is not empty and searchQuery is empty', async (done) => {
|
||||
await mockAxios.mockResolvedValue({ findhostsformigrationresponse: { count: 0, host: [] } })
|
||||
await wrapper.setProps({ resource: { id: 'test-id-value' } })
|
||||
await wrapper.vm.fetchData()
|
||||
await wrapper.vm.fetchHostsForMigration()
|
||||
await flushPromises()
|
||||
|
||||
expect(mockAxios).toHaveBeenCalled()
|
||||
|
|
@ -211,7 +211,7 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
await mockAxios.mockResolvedValue({ findhostsformigrationresponse: { count: 0, host: [] } })
|
||||
await wrapper.setProps({ resource: { id: 'test-id-value' } })
|
||||
await wrapper.setData({ searchQuery: 'test-query-value' })
|
||||
await wrapper.vm.fetchData()
|
||||
await wrapper.vm.fetchHostsForMigration()
|
||||
await flushPromises()
|
||||
|
||||
expect(mockAxios).toHaveBeenCalled()
|
||||
|
|
@ -239,7 +239,7 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
page: 2,
|
||||
pageSize: 20
|
||||
})
|
||||
await wrapper.vm.fetchData()
|
||||
await wrapper.vm.fetchHostsForMigration()
|
||||
await flushPromises()
|
||||
|
||||
expect(mockAxios).toHaveBeenCalled()
|
||||
|
|
@ -262,7 +262,7 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
it('check hosts, totalCount when api is called with response result is empty', async (done) => {
|
||||
await mockAxios.mockResolvedValue({ findhostsformigrationresponse: { count: 0, host: [] } })
|
||||
await wrapper.setProps({ resource: {} })
|
||||
await wrapper.vm.fetchData()
|
||||
await wrapper.vm.fetchHostsForMigration()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.vm.hosts).toEqual([])
|
||||
|
|
@ -285,7 +285,7 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
}
|
||||
})
|
||||
await wrapper.setProps({ resource: {} })
|
||||
await wrapper.vm.fetchData()
|
||||
await wrapper.vm.fetchHostsForMigration()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.vm.hosts).toEqual([{
|
||||
|
|
@ -305,7 +305,7 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
|
||||
await mockAxios.mockRejectedValue(mockError)
|
||||
await wrapper.setProps({ resource: {} })
|
||||
await wrapper.vm.fetchData()
|
||||
await wrapper.vm.fetchHostsForMigration()
|
||||
await flushPromises()
|
||||
await flushPromises()
|
||||
|
||||
|
|
@ -543,14 +543,14 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
await mockAxios.mockResolvedValue(mockData)
|
||||
await wrapper.setProps({
|
||||
resource: {
|
||||
id: 'test-resource-id',
|
||||
id: 'test-resource-id-err',
|
||||
name: 'test-resource-name'
|
||||
}
|
||||
})
|
||||
await wrapper.setData({
|
||||
selectedHost: {
|
||||
requiresStorageMotion: true,
|
||||
id: 'test-host-id',
|
||||
id: 'test-host-id-err',
|
||||
name: 'test-host-name'
|
||||
}
|
||||
})
|
||||
|
|
@ -572,14 +572,14 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
await mockAxios.mockResolvedValue(mockData)
|
||||
await wrapper.setProps({
|
||||
resource: {
|
||||
id: 'test-resource-id',
|
||||
id: 'test-resource-id-catch',
|
||||
name: 'test-resource-name'
|
||||
}
|
||||
})
|
||||
await wrapper.setData({
|
||||
selectedHost: {
|
||||
requiresStorageMotion: true,
|
||||
id: 'test-host-id',
|
||||
id: 'test-host-id-catch',
|
||||
name: 'test-host-name'
|
||||
}
|
||||
})
|
||||
|
|
@ -599,7 +599,7 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
await wrapper.setData({
|
||||
selectedHost: {
|
||||
requiresStorageMotion: true,
|
||||
id: 'test-host-id',
|
||||
id: 'test-host-id-no-res',
|
||||
name: 'test-host-name'
|
||||
}
|
||||
})
|
||||
|
|
@ -617,11 +617,11 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
})
|
||||
|
||||
describe('handleChangePage()', () => {
|
||||
it('check page, pageSize and fetchData() when handleChangePage() is called', async (done) => {
|
||||
originalFunc.fetchData = wrapper.vm.fetchData
|
||||
wrapper.vm.fetchData = jest.fn()
|
||||
it('check page, pageSize and fetchHostsForMigration() when handleChangePage() is called', async (done) => {
|
||||
originalFunc.fetchHostsForMigration = wrapper.vm.fetchHostsForMigration
|
||||
wrapper.vm.fetchHostsForMigration = jest.fn()
|
||||
|
||||
const fetchData = jest.spyOn(wrapper.vm, 'fetchData').mockImplementation(() => {})
|
||||
const fetchHostsForMigration = jest.spyOn(wrapper.vm, 'fetchHostsForMigration').mockImplementation(() => {})
|
||||
await wrapper.setProps({ resource: {} })
|
||||
await wrapper.setData({
|
||||
page: 1,
|
||||
|
|
@ -632,17 +632,17 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
|
||||
expect(wrapper.vm.page).toEqual(2)
|
||||
expect(wrapper.vm.pageSize).toEqual(20)
|
||||
expect(fetchData).toBeCalled()
|
||||
expect(fetchHostsForMigration).toBeCalled()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleChangePageSize()', () => {
|
||||
it('check page, pageSize and fetchData() when handleChangePageSize() is called', async (done) => {
|
||||
originalFunc.fetchData = wrapper.vm.fetchData
|
||||
wrapper.vm.fetchData = jest.fn()
|
||||
it('check page, pageSize and fetchHostsForMigration() when handleChangePageSize() is called', async (done) => {
|
||||
originalFunc.fetchHostsForMigration = wrapper.vm.fetchHostsForMigration
|
||||
wrapper.vm.fetchHostsForMigration = jest.fn()
|
||||
|
||||
const fetchData = jest.spyOn(wrapper.vm, 'fetchData').mockImplementation(() => {})
|
||||
const fetchHostsForMigration = jest.spyOn(wrapper.vm, 'fetchHostsForMigration').mockImplementation(() => {})
|
||||
await wrapper.setProps({ resource: {} })
|
||||
await wrapper.setData({
|
||||
page: 1,
|
||||
|
|
@ -653,7 +653,7 @@ describe('Views > compute > MigrateWizard.vue', () => {
|
|||
|
||||
expect(wrapper.vm.page).toEqual(2)
|
||||
expect(wrapper.vm.pageSize).toEqual(20)
|
||||
expect(fetchData).toBeCalled()
|
||||
expect(fetchHostsForMigration).toBeCalled()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue