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 2020/08/12 11:25:28 UTC
[cloudstack-primate] branch master updated: storage: Form to
Migrate data between Image stores (#326)
This is an automated email from the ASF dual-hosted git repository.
rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-primate.git
The following commit(s) were added to refs/heads/master by this push:
new 342fd63 storage: Form to Migrate data between Image stores (#326)
342fd63 is described below
commit 342fd63bfe85e2e969d38a76435584bc52b48662
Author: Pearl Dsilva <pe...@gmail.com>
AuthorDate: Wed Aug 12 16:55:22 2020 +0530
storage: Form to Migrate data between Image stores (#326)
Enable migration of data between secondary storage pools - addresses feature: apache/cloudstack#4053
---
src/components/view/DetailsTab.vue | 10 +-
src/components/view/ListView.vue | 4 +-
src/components/widgets/Status.vue | 2 +
src/config/section/infra/secondaryStorages.js | 41 ++++-
src/config/section/network.js | 3 +-
src/locales/en.json | 5 +-
src/views/infra/MigrateData.vue | 221 ++++++++++++++++++++++++++
7 files changed, 280 insertions(+), 6 deletions(-)
diff --git a/src/components/view/DetailsTab.vue b/src/components/view/DetailsTab.vue
index bdb63d9..78c85a6 100644
--- a/src/components/view/DetailsTab.vue
+++ b/src/components/view/DetailsTab.vue
@@ -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
}
}
}
diff --git a/src/components/view/ListView.vue b/src/components/view/ListView.vue
index b646bd7..bc27abc 100644
--- a/src/components/view/ListView.vue
+++ b/src/components/view/ListView.vue
@@ -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>
diff --git a/src/components/widgets/Status.vue b/src/components/widgets/Status.vue
index 1597787..d67f6bd 100644
--- a/src/components/widgets/Status.vue
+++ b/src/components/widgets/Status.vue
@@ -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':
diff --git a/src/config/section/infra/secondaryStorages.js b/src/config/section/infra/secondaryStorages.js
index 17dc107..7395ff9 100644
--- a/src/config/section/infra/secondaryStorages.js
+++ b/src/config/section/infra/secondaryStorages.js
@@ -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')
@@ -32,6 +45,14 @@ export default {
}],
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',
docHelp: 'installguide/configuration.html#add-secondary-storage',
@@ -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 }
}
]
}
diff --git a/src/config/section/network.js b/src/config/section/network.js
index 300b631..c1b7e72 100644
--- a/src/config/section/network.js
+++ b/src/config/section/network.js
@@ -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
diff --git a/src/locales/en.json b/src/locales/en.json
index 50b93f8..1fd3860 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -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",
diff --git a/src/views/infra/MigrateData.vue b/src/views/infra/MigrateData.vue
new file mode 100644
index 0000000..6129ee1
--- /dev/null
+++ b/src/views/infra/MigrateData.vue
@@ -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>