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>