Skip to content
This repository was archived by the owner on Jan 20, 2021. It is now read-only.

Commit 342fd63

Browse files
authored
storage: Form to Migrate data between Image stores (#326)
Enable migration of data between secondary storage pools - addresses feature: apache/cloudstack#4053
1 parent cef5b2c commit 342fd63

7 files changed

Lines changed: 280 additions & 6 deletions

File tree

src/components/view/DetailsTab.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<template>
1919
<a-list
2020
size="small"
21-
:dataSource="projectname ? [...$route.meta.details.filter(x => x !== 'account'), 'projectname'] : $route.meta.details">
21+
:dataSource="fetchDetails()">
2222
<a-list-item slot="renderItem" slot-scope="item" v-if="item in resource">
2323
<div>
2424
<strong>{{ item === 'service' ? $t('label.supportedservices') : $t('label.' + String(item).toLowerCase()) }}</strong>
@@ -107,6 +107,14 @@ export default {
107107
projectAdmins.push(Object.keys(owner).includes('user') ? owner.account + '(' + owner.user + ')' : owner.account)
108108
}
109109
this.resource.account = projectAdmins.join()
110+
},
111+
fetchDetails () {
112+
var details = this.$route.meta.details
113+
if (typeof details === 'function') {
114+
details = details()
115+
}
116+
details = this.projectname ? [...details.filter(x => x !== 'account'), 'projectname'] : details
117+
return details
110118
}
111119
}
112120
}

src/components/view/ListView.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,9 @@
220220
<router-link v-if="$router.resolve('/zone/' + record.zoneid).route.name !== '404'" :to="{ path: '/zone/' + record.zoneid }">{{ text }}</router-link>
221221
<span v-else>{{ text }}</span>
222222
</span>
223-
223+
<a slot="readonly" slot-scope="text, record">
224+
<status :text="record.readonly ? 'ReadOnly' : 'ReadWrite'" />
225+
</a>
224226
<div slot="order" slot-scope="text, record" class="shift-btns">
225227
<a-tooltip placement="top">
226228
<template slot="title">{{ $t('label.move.to.top') }}</template>

src/components/widgets/Status.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export default {
8787
case 'Setup':
8888
case 'Started':
8989
case 'Successfully Installed':
90+
case 'ReadWrite':
9091
case 'True':
9192
case 'Up':
9293
case 'enabled':
@@ -100,6 +101,7 @@ export default {
100101
case 'Error':
101102
case 'False':
102103
case 'Stopped':
104+
case 'ReadOnly':
103105
status = 'error'
104106
break
105107
case 'Migrating':

src/config/section/infra/secondaryStorages.js

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,28 @@
1414
// KIND, either express or implied. See the License for the
1515
// specific language governing permissions and limitations
1616
// under the License.
17+
import store from '@/store'
1718

1819
export default {
1920
name: 'imagestore',
2021
title: 'label.secondary.storage',
2122
icon: 'picture',
2223
docHelp: 'adminguide/storage.html#secondary-storage',
2324
permission: ['listImageStores'],
24-
columns: ['name', 'url', 'protocol', 'scope', 'zonename'],
25-
details: ['name', 'id', 'url', 'protocol', 'provider', 'scope', 'zonename'],
25+
columns: () => {
26+
var fields = ['name', 'url', 'protocol', 'scope', 'zonename']
27+
if (store.getters.apis.listImageStores.params.filter(x => x.name === 'readonly').length > 0) {
28+
fields.push('readonly')
29+
}
30+
return fields
31+
},
32+
details: () => {
33+
var fields = ['name', 'id', 'url', 'protocol', 'provider', 'scope', 'zonename']
34+
if (store.getters.apis.listImageStores.params.filter(x => x.name === 'readonly').length > 0) {
35+
fields.push('readonly')
36+
}
37+
return fields
38+
},
2639
tabs: [{
2740
name: 'details',
2841
component: () => import('@/components/view/DetailsTab.vue')
@@ -31,6 +44,14 @@ export default {
3144
component: () => import('@/components/view/SettingsTab.vue')
3245
}],
3346
actions: [
47+
{
48+
api: 'migrateSecondaryStorageData',
49+
icon: 'drag',
50+
label: 'label.migrate.data.from.image.store',
51+
listView: true,
52+
popup: true,
53+
component: () => import('@/views/infra/MigrateData.vue')
54+
},
3455
{
3556
api: 'addImageStore',
3657
icon: 'plus',
@@ -46,6 +67,22 @@ export default {
4667
label: 'label.action.delete.secondary.storage',
4768
message: 'message.action.delete.secondary.storage',
4869
dataView: true
70+
},
71+
{
72+
api: 'updateImageStore',
73+
icon: 'stop',
74+
label: 'Make Image store read-only',
75+
dataView: true,
76+
defaultArgs: { readonly: true },
77+
show: (record) => { return record.readonly === false }
78+
},
79+
{
80+
api: 'updateImageStore',
81+
icon: 'check-circle',
82+
label: 'Make Image store read-write',
83+
dataView: true,
84+
defaultArgs: { readonly: false },
85+
show: (record) => { return record.readonly === true }
4986
}
5087
]
5188
}

src/config/section/network.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ export default {
250250
name: 'firewall',
251251
component: () => import('@/views/network/FirewallRules.vue'),
252252
networkServiceFilter: networkService => networkService.filter(x => x.name === 'Firewall').length > 0
253-
}, {
253+
},
254+
{
254255
name: 'portforwarding',
255256
component: () => import('@/views/network/PortForwarding.vue'),
256257
networkServiceFilter: networkService => networkService.filter(x => x.name === 'PortForwarding').length > 0

src/locales/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1333,7 +1333,7 @@
13331333
"label.metrics.network.usage": "Network Usage",
13341334
"label.metrics.network.write": "Write",
13351335
"label.metrics.num.cpu.cores": "Cores",
1336-
1336+
"label.migrate.data.from.image.store": "Migrate Data from Image store",
13371337
"label.migrate.instance.to": "Migrate instance to",
13381338
"label.migrate.instance.to.host": "Migrate instance to another host",
13391339
"label.migrate.instance.to.ps": "Migrate instance to another primary storage",
@@ -1685,6 +1685,7 @@
16851685
"label.rbdmonitor": "Ceph monitor",
16861686
"label.rbdpool": "Ceph pool",
16871687
"label.rbdsecret": "Cephx secret",
1688+
"label.readonly": "Read-Only",
16881689
"label.read": "Read",
16891690
"label.read.io": "Read (IO)",
16901691
"label.reason": "Reason",
@@ -2995,9 +2996,11 @@
29952996
"message.security.group.usage": "(Use <strong>Ctrl-click</strong> to select all applicable security groups)",
29962997
"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.",
29972998
"message.select.affinity.groups": "Please select any affinity groups you want this VM to belong to:",
2999+
"message.select.destination.image.stores": "Please select Image Store(s) to which data is to be migrated to",
29983000
"message.select.instance": "Please select an instance.",
29993001
"message.select.iso": "Please select an ISO for your new virtual instance.",
30003002
"message.select.item": "Please select an item.",
3003+
"message.select.migration.policy": "Please select a migration Policy",
30013004
"message.select.security.groups": "Please select security group(s) for your new VM",
30023005
"message.select.template": "Please select a template for your new virtual instance.",
30033006
"message.select.tier": "Please select a tier",

src/views/infra/MigrateData.vue

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
<template>
19+
<div class="form-layout">
20+
<a-spin :spinning="loading">
21+
<a-form :form="form" @submit="handleSubmit" layout="vertical">
22+
<a-form-item
23+
:label="$t('migrate.from')">
24+
<a-select
25+
v-decorator="['srcpool', {
26+
initialValue: selectedStore,
27+
rules: [
28+
{
29+
required: true,
30+
message: $t('message.error.select'),
31+
}]
32+
}]"
33+
:loading="loading"
34+
@change="val => { selectedStore = val }"
35+
>
36+
<a-select-option
37+
v-for="store in imageStores"
38+
:key="store.id"
39+
>{{ store.name || opt.url }}</a-select-option>
40+
</a-select>
41+
</a-form-item>
42+
<a-form-item
43+
:label="$t('migrate.to')">
44+
<a-select
45+
v-decorator="['destpools', {
46+
rules: [
47+
{
48+
required: true,
49+
message: $t('message.select.destination.image.stores'),
50+
}]
51+
}]"
52+
mode="multiple"
53+
:loading="loading"
54+
>
55+
<a-select-option
56+
v-for="store in imageStores"
57+
v-if="store.id !== selectedStore"
58+
:key="store.id"
59+
>{{ store.name || opt.url }}</a-select-option>
60+
</a-select>
61+
</a-form-item>
62+
<a-form-item :label="$t('migrationPolicy')">
63+
<a-select
64+
v-decorator="['migrationtype', {
65+
initialValue: 'Complete',
66+
rules: [
67+
{
68+
required: true,
69+
message: $t('message.select.migration.policy'),
70+
}]
71+
}]"
72+
:loading="loading"
73+
>
74+
<a-select-option value="Complete">Complete</a-select-option>
75+
<a-select-option value="Balance">Balance</a-select-option>
76+
</a-select>
77+
</a-form-item>
78+
<div :span="24" class="action-button">
79+
<a-button @click="closeAction">{{ this.$t('Cancel') }}</a-button>
80+
<a-button :loading="loading" type="primary" @click="handleSubmit">{{ this.$t('OK') }}</a-button>
81+
</div>
82+
</a-form>
83+
</a-spin>
84+
</div>
85+
</template>
86+
<script>
87+
import { api } from '@/api'
88+
export default {
89+
name: 'MigrateData',
90+
inject: ['parentFetchData'],
91+
data () {
92+
return {
93+
imageStores: [],
94+
loading: false,
95+
selectedStore: ''
96+
}
97+
},
98+
beforeCreate () {
99+
this.form = this.$form.createForm(this)
100+
},
101+
mounted () {
102+
this.fetchImageStores()
103+
},
104+
methods: {
105+
fetchImageStores () {
106+
this.loading = true
107+
api('listImageStores').then(json => {
108+
this.imageStores = json.listimagestoresresponse.imagestore || []
109+
this.selectedStore = this.imageStores[0].id || ''
110+
}).finally(() => {
111+
this.loading = false
112+
})
113+
},
114+
handleSubmit (e) {
115+
e.preventDefault()
116+
this.form.validateFields((err, values) => {
117+
if (err) {
118+
return
119+
}
120+
const params = {}
121+
for (const key in values) {
122+
const input = values[key]
123+
if (input === undefined) {
124+
continue
125+
}
126+
if (key === 'destpools') {
127+
params[key] = input.join(',')
128+
} else {
129+
params[key] = input
130+
}
131+
}
132+
133+
const title = 'Data Migration'
134+
this.loading = true
135+
136+
const result = this.migrateData(params, title)
137+
result.then(json => {
138+
const result = json.jobresult
139+
const success = result.imagestore.success || false
140+
const message = result.imagestore.message || ''
141+
if (success) {
142+
this.$notification.success({
143+
message: title,
144+
description: message
145+
})
146+
} else {
147+
this.$notification.error({
148+
message: title,
149+
description: message,
150+
duration: 0
151+
})
152+
}
153+
}).catch(error => {
154+
console.log(error)
155+
})
156+
this.loading = false
157+
this.parentFetchData()
158+
this.closeAction()
159+
})
160+
},
161+
migrateData (args, title) {
162+
return new Promise((resolve, reject) => {
163+
api('migrateSecondaryStorageData', args).then(async json => {
164+
const jobId = json.migratesecondarystoragedataresponse.jobid
165+
if (jobId) {
166+
const result = await this.pollJob(jobId, title)
167+
if (result.jobstatus === 2) {
168+
reject(result.jobresult.errortext)
169+
return
170+
}
171+
resolve(result)
172+
}
173+
}).catch(error => {
174+
reject(error)
175+
})
176+
})
177+
},
178+
async pollJob (jobId, title) {
179+
return new Promise(resolve => {
180+
const asyncJobInterval = setInterval(() => {
181+
api('queryAsyncJobResult', { jobId }).then(async json => {
182+
const result = json.queryasyncjobresultresponse
183+
if (result.jobstatus === 0) {
184+
return
185+
}
186+
this.$store.dispatch('AddAsyncJob', {
187+
title: title,
188+
jobid: jobId,
189+
description: 'imagestore',
190+
status: 'progress',
191+
silent: true
192+
})
193+
clearInterval(asyncJobInterval)
194+
resolve(result)
195+
})
196+
}, 1000)
197+
})
198+
},
199+
closeAction () {
200+
this.$emit('close-action')
201+
}
202+
}
203+
}
204+
</script>
205+
<style lang="scss" scoped>
206+
.form-layout {
207+
width: 85vw;
208+
209+
@media (min-width: 1000px) {
210+
width: 40vw;
211+
}
212+
}
213+
214+
.action-button {
215+
text-align: right;
216+
217+
button {
218+
margin-right: 5px;
219+
}
220+
}
221+
</style>

0 commit comments

Comments
 (0)