storage: Volume storage migration action (#72)

Adds a custom volume storage migration form

This fixes #70

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Ritchie Vincent 2019-12-18 12:00:31 +00:00 committed by Rohit Yadav
parent 185e6043e1
commit 200f89bc08
7 changed files with 281 additions and 7 deletions

View File

@ -140,7 +140,12 @@ export default {
icon: 'cloud-download',
label: 'Download ISO',
dataView: true,
args: ['zoneid', 'mode']
args: ['zoneid', 'mode'],
mapping: {
mode: {
value: (record) => { return 'HTTP_DOWNLOAD' }
}
}
},
{
api: 'updateIsoPermissions',

View File

@ -116,7 +116,9 @@ export default {
label: 'Migrate Volume',
args: ['volumeid', 'storageid', 'livemigrate'],
dataView: true,
show: (record) => { return 'virtualmachineid' in record && record.virtualmachineid },
show: (record) => { return record && record.state === 'Ready' },
popup: true,
component: () => import('@/views/storage/MigrateVolume.vue'),
mapping: {
volumeid: {
value: (record) => { return record.id }

View File

@ -27,9 +27,11 @@ import './core/use'
import './core/ext'
import './permission' // permission control
import './utils/filter' // global filter
import { pollJobPlugin } from './utils/plugins'
Vue.config.productionTip = false
Vue.use(VueAxios, router)
Vue.use(pollJobPlugin)
new Vue({
router,

76
ui/src/utils/plugins.js Normal file
View File

@ -0,0 +1,76 @@
// 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.
import { api } from '@/api'
import { message, notification } from 'ant-design-vue'
export const pollJobPlugin = {
install (Vue) {
Vue.prototype.$pollJob = function (options) {
/**
* @param {String} jobId
* @param {String} [successMessage=Success]
* @param {Function} [successMethod=() => {}]
* @param {String} [errorMessage=Error]
* @param {Function} [errorMethod=() => {}]
* @param {String} [loadingMessage=Loading...]
* @param {String} [catchMessage=Error caught]
* @param {Function} [catchMethod=() => {}]
* @param {Number} [loadingDuration=3]
*/
const {
jobId,
successMessage = 'Success',
successMethod = () => {},
errorMessage = 'Error',
errorMethod = () => {},
loadingMessage = 'Loading...',
catchMessage = 'Error caught',
catchMethod = () => {},
loadingDuration = 3
} = options
api('queryAsyncJobResult', { jobId }).then(json => {
const result = json.queryasyncjobresultresponse
if (result.jobstatus === 1) {
message.success(successMessage)
successMethod()
} else if (result.jobstatus === 2) {
notification.error({
message: errorMessage,
description: result.jobresult.errortext
})
errorMethod()
} else if (result.jobstatus === 0) {
message
.loading(loadingMessage, loadingDuration)
.then(() => this.$pollJob(options))
}
}).catch(e => {
console.error(`${catchMessage} - ${e}`)
notification.error({
message: 'Error',
description: catchMessage
})
catchMethod && catchMethod()
})
}
}
}

View File

@ -265,7 +265,8 @@ export default {
mixins: [mixinDevice],
provide: function () {
return {
parentFetchData: this.fetchData
parentFetchData: this.fetchData,
parentToggleLoading: this.toggleLoading
}
},
data () {
@ -718,6 +719,9 @@ export default {
changeResource (resource) {
this.treeSelected = resource
this.resource = this.treeSelected
},
toggleLoading () {
this.loading = !this.loading
}
}
}

View File

@ -179,7 +179,6 @@ export default {
display: flex;
justify-content: flex-end;
}
}
.host-item {
@ -199,7 +198,6 @@ export default {
@media (min-width: 760px) {
flex-direction: row;
}
}
&__value {
@ -216,9 +214,7 @@ export default {
margin-right: 40px;
margin-left: 40px;
}
}
}
&__title {

View File

@ -0,0 +1,189 @@
// 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="migrate-volume-container">
<div class="modal-form">
<p class="modal-form__label">{{ $t('storagePool') }}</p>
<a-select v-model="selectedStoragePool" style="width: 100%;">
<a-select-option v-for="(storagePool, index) in storagePools" :value="storagePool.id" :key="index">
{{ storagePool.name }}
</a-select-option>
</a-select>
<template v-if="this.resource.virtualmachineid">
<p class="modal-form__label" @click="replaceDiskOffering = !replaceDiskOffering" style="cursor:pointer;">
{{ $t('useNewDiskOffering') }}
</p>
<a-checkbox v-model="replaceDiskOffering" />
<template v-if="replaceDiskOffering">
<p class="modal-form__label">{{ $t('newDiskOffering') }}</p>
<a-select v-model="selectedDiskOffering" style="width: 100%;">
<a-select-option v-for="(diskOffering, index) in diskOfferings" :value="diskOffering.id" :key="index">
{{ diskOffering.displaytext }}
</a-select-option>
</a-select>
</template>
</template>
</div>
<a-divider />
<div class="actions">
<a-button @click="closeModal">
{{ $t('Cancel') }}
</a-button>
<a-button type="primary" @click="submitMigrateVolume">
{{ $t('OK') }}
</a-button>
</div>
</div>
</template>
<script>
import { api } from '@/api'
export default {
name: 'MigrateVolume',
props: {
resource: {
type: Object,
required: true
}
},
inject: ['parentFetchData', 'parentToggleLoading'],
data () {
return {
storagePools: [],
selectedStoragePool: null,
diskOfferings: [],
replaceDiskOffering: !!this.resource.virtualmachineid,
selectedDiskOffering: null
}
},
created () {
this.fetchStoragePools()
this.resource.virtualmachineid && this.fetchDiskOfferings()
},
methods: {
fetchStoragePools () {
api('listStoragePools', {
zoneid: this.resource.zoneid
}).then(response => {
this.storagePools = response.liststoragepoolsresponse.storagepool
this.selectedStoragePool = this.storagePools[0].id
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.errorresponse.errortext
})
this.closeModal()
})
},
fetchDiskOfferings () {
api('listDiskOfferings', {
listall: true
}).then(response => {
this.diskOfferings = response.listdiskofferingsresponse.diskoffering
this.selectedDiskOffering = this.diskOfferings[0].id
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.errorresponse.errortext
})
this.closeModal()
})
},
closeModal () {
this.$parent.$parent.close()
},
submitMigrateVolume () {
this.closeModal()
this.parentToggleLoading()
api('migrateVolume', {
livemigrate: this.resource.vmstate === 'Running',
storageid: this.selectedStoragePool,
volumeid: this.resource.id,
newdiskofferingid: this.replaceDiskOffering ? this.selectedDiskOffering : null
}).then(response => {
this.$pollJob({
jobId: response.migratevolumeresponse.jobid,
successMessage: `Successfully migrated volume`,
successMethod: () => {
this.parentFetchData()
this.parentToggleLoading()
},
errorMessage: 'Migrating volume failed',
errorMethod: () => {
this.parentFetchData()
this.parentToggleLoading()
},
loadingMessage: `Migrating volume...`,
catchMessage: 'Error encountered while fetching async job result',
catchMethod: () => {
this.parentFetchData()
this.parentToggleLoading()
}
})
}).catch(error => {
this.$notification.error({
message: `Error ${error.response.status}`,
description: error.response.data.errorresponse.errortext
})
this.closeModal()
})
}
}
}
</script>
<style scoped lang="scss">
.migrate-volume-container {
width: 95vw;
max-width: 100%;
@media (min-width: 760px) {
width: 50vw;
}
}
.actions {
display: flex;
justify-content: flex-end;
margin-top: 20px;
button {
&:not(:last-child) {
margin-right: 10px;
}
}
}
.modal-form {
margin-top: -20px;
&__label {
font-weight: bold;
margin-top: 10px;
margin-bottom: 5px;
}
}
</style>