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 <pearl1...@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>