Skip to content

Commit 200f89b

Browse files
RitchieVincentrohityadavcloud
authored andcommitted
storage: Volume storage migration action (#72)
Adds a custom volume storage migration form This fixes #70 Signed-off-by: Rohit Yadav <[email protected]>
1 parent 185e604 commit 200f89b

File tree

7 files changed

+281
-7
lines changed

7 files changed

+281
-7
lines changed

Diff for: ui/src/config/section/image.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,12 @@ export default {
140140
icon: 'cloud-download',
141141
label: 'Download ISO',
142142
dataView: true,
143-
args: ['zoneid', 'mode']
143+
args: ['zoneid', 'mode'],
144+
mapping: {
145+
mode: {
146+
value: (record) => { return 'HTTP_DOWNLOAD' }
147+
}
148+
}
144149
},
145150
{
146151
api: 'updateIsoPermissions',

Diff for: ui/src/config/section/storage.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ export default {
116116
label: 'Migrate Volume',
117117
args: ['volumeid', 'storageid', 'livemigrate'],
118118
dataView: true,
119-
show: (record) => { return 'virtualmachineid' in record && record.virtualmachineid },
119+
show: (record) => { return record && record.state === 'Ready' },
120+
popup: true,
121+
component: () => import('@/views/storage/MigrateVolume.vue'),
120122
mapping: {
121123
volumeid: {
122124
value: (record) => { return record.id }

Diff for: ui/src/main.js

+2
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ import './core/use'
2727
import './core/ext'
2828
import './permission' // permission control
2929
import './utils/filter' // global filter
30+
import { pollJobPlugin } from './utils/plugins'
3031

3132
Vue.config.productionTip = false
3233
Vue.use(VueAxios, router)
34+
Vue.use(pollJobPlugin)
3335

3436
new Vue({
3537
router,

Diff for: ui/src/utils/plugins.js

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
import { api } from '@/api'
19+
import { message, notification } from 'ant-design-vue'
20+
21+
export const pollJobPlugin = {
22+
23+
install (Vue) {
24+
Vue.prototype.$pollJob = function (options) {
25+
/**
26+
* @param {String} jobId
27+
* @param {String} [successMessage=Success]
28+
* @param {Function} [successMethod=() => {}]
29+
* @param {String} [errorMessage=Error]
30+
* @param {Function} [errorMethod=() => {}]
31+
* @param {String} [loadingMessage=Loading...]
32+
* @param {String} [catchMessage=Error caught]
33+
* @param {Function} [catchMethod=() => {}]
34+
* @param {Number} [loadingDuration=3]
35+
*/
36+
const {
37+
jobId,
38+
successMessage = 'Success',
39+
successMethod = () => {},
40+
errorMessage = 'Error',
41+
errorMethod = () => {},
42+
loadingMessage = 'Loading...',
43+
catchMessage = 'Error caught',
44+
catchMethod = () => {},
45+
loadingDuration = 3
46+
} = options
47+
48+
api('queryAsyncJobResult', { jobId }).then(json => {
49+
const result = json.queryasyncjobresultresponse
50+
51+
if (result.jobstatus === 1) {
52+
message.success(successMessage)
53+
successMethod()
54+
} else if (result.jobstatus === 2) {
55+
notification.error({
56+
message: errorMessage,
57+
description: result.jobresult.errortext
58+
})
59+
errorMethod()
60+
} else if (result.jobstatus === 0) {
61+
message
62+
.loading(loadingMessage, loadingDuration)
63+
.then(() => this.$pollJob(options))
64+
}
65+
}).catch(e => {
66+
console.error(`${catchMessage} - ${e}`)
67+
notification.error({
68+
message: 'Error',
69+
description: catchMessage
70+
})
71+
catchMethod && catchMethod()
72+
})
73+
}
74+
}
75+
76+
}

Diff for: ui/src/views/AutogenView.vue

+5-1
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,8 @@ export default {
265265
mixins: [mixinDevice],
266266
provide: function () {
267267
return {
268-
parentFetchData: this.fetchData
268+
parentFetchData: this.fetchData,
269+
parentToggleLoading: this.toggleLoading
269270
}
270271
},
271272
data () {
@@ -718,6 +719,9 @@ export default {
718719
changeResource (resource) {
719720
this.treeSelected = resource
720721
this.resource = this.treeSelected
722+
},
723+
toggleLoading () {
724+
this.loading = !this.loading
721725
}
722726
}
723727
}

Diff for: ui/src/views/compute/MigrateWizard.vue

-4
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,6 @@ export default {
179179
display: flex;
180180
justify-content: flex-end;
181181
}
182-
183182
}
184183
185184
.host-item {
@@ -199,7 +198,6 @@ export default {
199198
@media (min-width: 760px) {
200199
flex-direction: row;
201200
}
202-
203201
}
204202
205203
&__value {
@@ -216,9 +214,7 @@ export default {
216214
margin-right: 40px;
217215
margin-left: 40px;
218216
}
219-
220217
}
221-
222218
}
223219
224220
&__title {

Diff for: ui/src/views/storage/MigrateVolume.vue

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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="migrate-volume-container">
20+
21+
<div class="modal-form">
22+
<p class="modal-form__label">{{ $t('storagePool') }}</p>
23+
<a-select v-model="selectedStoragePool" style="width: 100%;">
24+
<a-select-option v-for="(storagePool, index) in storagePools" :value="storagePool.id" :key="index">
25+
{{ storagePool.name }}
26+
</a-select-option>
27+
</a-select>
28+
<template v-if="this.resource.virtualmachineid">
29+
<p class="modal-form__label" @click="replaceDiskOffering = !replaceDiskOffering" style="cursor:pointer;">
30+
{{ $t('useNewDiskOffering') }}
31+
</p>
32+
<a-checkbox v-model="replaceDiskOffering" />
33+
34+
<template v-if="replaceDiskOffering">
35+
<p class="modal-form__label">{{ $t('newDiskOffering') }}</p>
36+
<a-select v-model="selectedDiskOffering" style="width: 100%;">
37+
<a-select-option v-for="(diskOffering, index) in diskOfferings" :value="diskOffering.id" :key="index">
38+
{{ diskOffering.displaytext }}
39+
</a-select-option>
40+
</a-select>
41+
</template>
42+
43+
</template>
44+
</div>
45+
46+
<a-divider />
47+
48+
<div class="actions">
49+
<a-button @click="closeModal">
50+
{{ $t('Cancel') }}
51+
</a-button>
52+
<a-button type="primary" @click="submitMigrateVolume">
53+
{{ $t('OK') }}
54+
</a-button>
55+
</div>
56+
57+
</div>
58+
</template>
59+
60+
<script>
61+
import { api } from '@/api'
62+
63+
export default {
64+
name: 'MigrateVolume',
65+
props: {
66+
resource: {
67+
type: Object,
68+
required: true
69+
}
70+
},
71+
inject: ['parentFetchData', 'parentToggleLoading'],
72+
data () {
73+
return {
74+
storagePools: [],
75+
selectedStoragePool: null,
76+
diskOfferings: [],
77+
replaceDiskOffering: !!this.resource.virtualmachineid,
78+
selectedDiskOffering: null
79+
}
80+
},
81+
created () {
82+
this.fetchStoragePools()
83+
this.resource.virtualmachineid && this.fetchDiskOfferings()
84+
},
85+
methods: {
86+
fetchStoragePools () {
87+
api('listStoragePools', {
88+
zoneid: this.resource.zoneid
89+
}).then(response => {
90+
this.storagePools = response.liststoragepoolsresponse.storagepool
91+
this.selectedStoragePool = this.storagePools[0].id
92+
}).catch(error => {
93+
this.$notification.error({
94+
message: `Error ${error.response.status}`,
95+
description: error.response.data.errorresponse.errortext
96+
})
97+
this.closeModal()
98+
})
99+
},
100+
fetchDiskOfferings () {
101+
api('listDiskOfferings', {
102+
listall: true
103+
}).then(response => {
104+
this.diskOfferings = response.listdiskofferingsresponse.diskoffering
105+
this.selectedDiskOffering = this.diskOfferings[0].id
106+
}).catch(error => {
107+
this.$notification.error({
108+
message: `Error ${error.response.status}`,
109+
description: error.response.data.errorresponse.errortext
110+
})
111+
this.closeModal()
112+
})
113+
},
114+
closeModal () {
115+
this.$parent.$parent.close()
116+
},
117+
submitMigrateVolume () {
118+
this.closeModal()
119+
this.parentToggleLoading()
120+
api('migrateVolume', {
121+
livemigrate: this.resource.vmstate === 'Running',
122+
storageid: this.selectedStoragePool,
123+
volumeid: this.resource.id,
124+
newdiskofferingid: this.replaceDiskOffering ? this.selectedDiskOffering : null
125+
}).then(response => {
126+
this.$pollJob({
127+
jobId: response.migratevolumeresponse.jobid,
128+
successMessage: `Successfully migrated volume`,
129+
successMethod: () => {
130+
this.parentFetchData()
131+
this.parentToggleLoading()
132+
},
133+
errorMessage: 'Migrating volume failed',
134+
errorMethod: () => {
135+
this.parentFetchData()
136+
this.parentToggleLoading()
137+
},
138+
loadingMessage: `Migrating volume...`,
139+
catchMessage: 'Error encountered while fetching async job result',
140+
catchMethod: () => {
141+
this.parentFetchData()
142+
this.parentToggleLoading()
143+
}
144+
})
145+
}).catch(error => {
146+
this.$notification.error({
147+
message: `Error ${error.response.status}`,
148+
description: error.response.data.errorresponse.errortext
149+
})
150+
this.closeModal()
151+
})
152+
}
153+
}
154+
}
155+
</script>
156+
157+
<style scoped lang="scss">
158+
.migrate-volume-container {
159+
width: 95vw;
160+
max-width: 100%;
161+
162+
@media (min-width: 760px) {
163+
width: 50vw;
164+
}
165+
}
166+
167+
.actions {
168+
display: flex;
169+
justify-content: flex-end;
170+
margin-top: 20px;
171+
172+
button {
173+
&:not(:last-child) {
174+
margin-right: 10px;
175+
}
176+
}
177+
}
178+
179+
.modal-form {
180+
margin-top: -20px;
181+
182+
&__label {
183+
font-weight: bold;
184+
margin-top: 10px;
185+
margin-bottom: 5px;
186+
}
187+
188+
}
189+
</style>

0 commit comments

Comments
 (0)