mirror of https://github.com/apache/cloudstack.git
storage: Form to Migrate data between Image stores (#326)
Enable migration of data between secondary storage pools - addresses feature: apache/cloudstack#4053 Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
parent
a3d2450f88
commit
7faed91d53
|
|
@ -18,7 +18,7 @@
|
|||
<template>
|
||||
<a-list
|
||||
size="small"
|
||||
:dataSource="projectname ? [...$route.meta.details.filter(x => x !== 'account'), 'projectname'] : $route.meta.details">
|
||||
:dataSource="fetchDetails()">
|
||||
<a-list-item slot="renderItem" slot-scope="item" v-if="item in resource">
|
||||
<div>
|
||||
<strong>{{ item === 'service' ? $t('label.supportedservices') : $t('label.' + String(item).toLowerCase()) }}</strong>
|
||||
|
|
@ -107,6 +107,14 @@ export default {
|
|||
projectAdmins.push(Object.keys(owner).includes('user') ? owner.account + '(' + owner.user + ')' : owner.account)
|
||||
}
|
||||
this.resource.account = projectAdmins.join()
|
||||
},
|
||||
fetchDetails () {
|
||||
var details = this.$route.meta.details
|
||||
if (typeof details === 'function') {
|
||||
details = details()
|
||||
}
|
||||
details = this.projectname ? [...details.filter(x => x !== 'account'), 'projectname'] : details
|
||||
return details
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,7 +220,9 @@
|
|||
<router-link v-if="$router.resolve('/zone/' + record.zoneid).route.name !== '404'" :to="{ path: '/zone/' + record.zoneid }">{{ text }}</router-link>
|
||||
<span v-else>{{ text }}</span>
|
||||
</span>
|
||||
|
||||
<a slot="readonly" slot-scope="text, record">
|
||||
<status :text="record.readonly ? 'ReadOnly' : 'ReadWrite'" />
|
||||
</a>
|
||||
<div slot="order" slot-scope="text, record" class="shift-btns">
|
||||
<a-tooltip placement="top">
|
||||
<template slot="title">{{ $t('label.move.to.top') }}</template>
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export default {
|
|||
case 'Setup':
|
||||
case 'Started':
|
||||
case 'Successfully Installed':
|
||||
case 'ReadWrite':
|
||||
case 'True':
|
||||
case 'Up':
|
||||
case 'enabled':
|
||||
|
|
@ -100,6 +101,7 @@ export default {
|
|||
case 'Error':
|
||||
case 'False':
|
||||
case 'Stopped':
|
||||
case 'ReadOnly':
|
||||
status = 'error'
|
||||
break
|
||||
case 'Migrating':
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
import store from '@/store'
|
||||
|
||||
export default {
|
||||
name: 'imagestore',
|
||||
|
|
@ -21,8 +22,20 @@ export default {
|
|||
icon: 'picture',
|
||||
docHelp: 'adminguide/storage.html#secondary-storage',
|
||||
permission: ['listImageStores'],
|
||||
columns: ['name', 'url', 'protocol', 'scope', 'zonename'],
|
||||
details: ['name', 'id', 'url', 'protocol', 'provider', 'scope', 'zonename'],
|
||||
columns: () => {
|
||||
var fields = ['name', 'url', 'protocol', 'scope', 'zonename']
|
||||
if (store.getters.apis.listImageStores.params.filter(x => x.name === 'readonly').length > 0) {
|
||||
fields.push('readonly')
|
||||
}
|
||||
return fields
|
||||
},
|
||||
details: () => {
|
||||
var fields = ['name', 'id', 'url', 'protocol', 'provider', 'scope', 'zonename']
|
||||
if (store.getters.apis.listImageStores.params.filter(x => x.name === 'readonly').length > 0) {
|
||||
fields.push('readonly')
|
||||
}
|
||||
return fields
|
||||
},
|
||||
tabs: [{
|
||||
name: 'details',
|
||||
component: () => import('@/components/view/DetailsTab.vue')
|
||||
|
|
@ -31,6 +44,14 @@ export default {
|
|||
component: () => import('@/components/view/SettingsTab.vue')
|
||||
}],
|
||||
actions: [
|
||||
{
|
||||
api: 'migrateSecondaryStorageData',
|
||||
icon: 'drag',
|
||||
label: 'label.migrate.data.from.image.store',
|
||||
listView: true,
|
||||
popup: true,
|
||||
component: () => import('@/views/infra/MigrateData.vue')
|
||||
},
|
||||
{
|
||||
api: 'addImageStore',
|
||||
icon: 'plus',
|
||||
|
|
@ -46,6 +67,22 @@ export default {
|
|||
label: 'label.action.delete.secondary.storage',
|
||||
message: 'message.action.delete.secondary.storage',
|
||||
dataView: true
|
||||
},
|
||||
{
|
||||
api: 'updateImageStore',
|
||||
icon: 'stop',
|
||||
label: 'Make Image store read-only',
|
||||
dataView: true,
|
||||
defaultArgs: { readonly: true },
|
||||
show: (record) => { return record.readonly === false }
|
||||
},
|
||||
{
|
||||
api: 'updateImageStore',
|
||||
icon: 'check-circle',
|
||||
label: 'Make Image store read-write',
|
||||
dataView: true,
|
||||
defaultArgs: { readonly: false },
|
||||
show: (record) => { return record.readonly === true }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,7 +250,8 @@ export default {
|
|||
name: 'firewall',
|
||||
component: () => import('@/views/network/FirewallRules.vue'),
|
||||
networkServiceFilter: networkService => networkService.filter(x => x.name === 'Firewall').length > 0
|
||||
}, {
|
||||
},
|
||||
{
|
||||
name: 'portforwarding',
|
||||
component: () => import('@/views/network/PortForwarding.vue'),
|
||||
networkServiceFilter: networkService => networkService.filter(x => x.name === 'PortForwarding').length > 0
|
||||
|
|
|
|||
|
|
@ -1333,7 +1333,7 @@
|
|||
"label.metrics.network.usage": "Network Usage",
|
||||
"label.metrics.network.write": "Write",
|
||||
"label.metrics.num.cpu.cores": "Cores",
|
||||
|
||||
"label.migrate.data.from.image.store": "Migrate Data from Image store",
|
||||
"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",
|
||||
|
|
@ -1685,6 +1685,7 @@
|
|||
"label.rbdmonitor": "Ceph monitor",
|
||||
"label.rbdpool": "Ceph pool",
|
||||
"label.rbdsecret": "Cephx secret",
|
||||
"label.readonly": "Read-Only",
|
||||
"label.read": "Read",
|
||||
"label.read.io": "Read (IO)",
|
||||
"label.reason": "Reason",
|
||||
|
|
@ -2995,9 +2996,11 @@
|
|||
"message.security.group.usage": "(Use <strong>Ctrl-click</strong> to select all applicable security groups)",
|
||||
"message.select.a.zone": "A zone typically corresponds to a single datacenter. Multiple zones help make the cloud more reliable by providing physical isolation and redundancy.",
|
||||
"message.select.affinity.groups": "Please select any affinity groups you want this VM to belong to:",
|
||||
"message.select.destination.image.stores": "Please select Image Store(s) to which data is to be migrated to",
|
||||
"message.select.instance": "Please select an instance.",
|
||||
"message.select.iso": "Please select an ISO for your new virtual instance.",
|
||||
"message.select.item": "Please select an item.",
|
||||
"message.select.migration.policy": "Please select a migration Policy",
|
||||
"message.select.security.groups": "Please select security group(s) for your new VM",
|
||||
"message.select.template": "Please select a template for your new virtual instance.",
|
||||
"message.select.tier": "Please select a tier",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,221 @@
|
|||
// 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-layout">
|
||||
<a-spin :spinning="loading">
|
||||
<a-form :form="form" @submit="handleSubmit" layout="vertical">
|
||||
<a-form-item
|
||||
:label="$t('migrate.from')">
|
||||
<a-select
|
||||
v-decorator="['srcpool', {
|
||||
initialValue: selectedStore,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.error.select'),
|
||||
}]
|
||||
}]"
|
||||
:loading="loading"
|
||||
@change="val => { selectedStore = val }"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="store in imageStores"
|
||||
:key="store.id"
|
||||
>{{ store.name || opt.url }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label="$t('migrate.to')">
|
||||
<a-select
|
||||
v-decorator="['destpools', {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.select.destination.image.stores'),
|
||||
}]
|
||||
}]"
|
||||
mode="multiple"
|
||||
:loading="loading"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="store in imageStores"
|
||||
v-if="store.id !== selectedStore"
|
||||
:key="store.id"
|
||||
>{{ store.name || opt.url }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('migrationPolicy')">
|
||||
<a-select
|
||||
v-decorator="['migrationtype', {
|
||||
initialValue: 'Complete',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.select.migration.policy'),
|
||||
}]
|
||||
}]"
|
||||
:loading="loading"
|
||||
>
|
||||
<a-select-option value="Complete">Complete</a-select-option>
|
||||
<a-select-option value="Balance">Balance</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<div :span="24" class="action-button">
|
||||
<a-button @click="closeAction">{{ this.$t('Cancel') }}</a-button>
|
||||
<a-button :loading="loading" type="primary" @click="handleSubmit">{{ this.$t('OK') }}</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { api } from '@/api'
|
||||
export default {
|
||||
name: 'MigrateData',
|
||||
inject: ['parentFetchData'],
|
||||
data () {
|
||||
return {
|
||||
imageStores: [],
|
||||
loading: false,
|
||||
selectedStore: ''
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
mounted () {
|
||||
this.fetchImageStores()
|
||||
},
|
||||
methods: {
|
||||
fetchImageStores () {
|
||||
this.loading = true
|
||||
api('listImageStores').then(json => {
|
||||
this.imageStores = json.listimagestoresresponse.imagestore || []
|
||||
this.selectedStore = this.imageStores[0].id || ''
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
return
|
||||
}
|
||||
const params = {}
|
||||
for (const key in values) {
|
||||
const input = values[key]
|
||||
if (input === undefined) {
|
||||
continue
|
||||
}
|
||||
if (key === 'destpools') {
|
||||
params[key] = input.join(',')
|
||||
} else {
|
||||
params[key] = input
|
||||
}
|
||||
}
|
||||
|
||||
const title = 'Data Migration'
|
||||
this.loading = true
|
||||
|
||||
const result = this.migrateData(params, title)
|
||||
result.then(json => {
|
||||
const result = json.jobresult
|
||||
const success = result.imagestore.success || false
|
||||
const message = result.imagestore.message || ''
|
||||
if (success) {
|
||||
this.$notification.success({
|
||||
message: title,
|
||||
description: message
|
||||
})
|
||||
} else {
|
||||
this.$notification.error({
|
||||
message: title,
|
||||
description: message,
|
||||
duration: 0
|
||||
})
|
||||
}
|
||||
}).catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
this.loading = false
|
||||
this.parentFetchData()
|
||||
this.closeAction()
|
||||
})
|
||||
},
|
||||
migrateData (args, title) {
|
||||
return new Promise((resolve, reject) => {
|
||||
api('migrateSecondaryStorageData', args).then(async json => {
|
||||
const jobId = json.migratesecondarystoragedataresponse.jobid
|
||||
if (jobId) {
|
||||
const result = await this.pollJob(jobId, title)
|
||||
if (result.jobstatus === 2) {
|
||||
reject(result.jobresult.errortext)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
}
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
async pollJob (jobId, title) {
|
||||
return new Promise(resolve => {
|
||||
const asyncJobInterval = setInterval(() => {
|
||||
api('queryAsyncJobResult', { jobId }).then(async json => {
|
||||
const result = json.queryasyncjobresultresponse
|
||||
if (result.jobstatus === 0) {
|
||||
return
|
||||
}
|
||||
this.$store.dispatch('AddAsyncJob', {
|
||||
title: title,
|
||||
jobid: jobId,
|
||||
description: 'imagestore',
|
||||
status: 'progress',
|
||||
silent: true
|
||||
})
|
||||
clearInterval(asyncJobInterval)
|
||||
resolve(result)
|
||||
})
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
closeAction () {
|
||||
this.$emit('close-action')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.form-layout {
|
||||
width: 85vw;
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
width: 40vw;
|
||||
}
|
||||
}
|
||||
|
||||
.action-button {
|
||||
text-align: right;
|
||||
|
||||
button {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue