Skip to content

Commit f8a479c

Browse files
feat: add storage migration operation (#724) (#749)
- add storage migration - add cancel storage migration (cherry picked from commit 9c9f59c) Signed-off-by: Vicente Cheng <freeze.bilsted@gmail.com> Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
1 parent 6d5e584 commit f8a479c

3 files changed

Lines changed: 273 additions & 0 deletions

File tree

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
<script>
2+
import { mapGetters } from 'vuex';
3+
4+
import { PVC } from '@shell/config/types';
5+
import { exceptionToErrorsArray } from '@shell/utils/error';
6+
import { sortBy } from '@shell/utils/sort';
7+
import { HCI } from '../types';
8+
import { parseVolumeClaimTemplates } from '@pkg/harvester/utils/vm';
9+
10+
import { Card } from '@components/Card';
11+
import { Banner } from '@components/Banner';
12+
import AsyncButton from '@shell/components/AsyncButton';
13+
import LabeledSelect from '@shell/components/form/LabeledSelect';
14+
15+
export default {
16+
name: 'HarvesterStorageMigrationDialog',
17+
18+
emits: ['close'],
19+
20+
components: {
21+
AsyncButton, Banner, Card, LabeledSelect
22+
},
23+
24+
props: {
25+
resources: {
26+
type: Array,
27+
required: true
28+
}
29+
},
30+
31+
async fetch() {
32+
this.allPVCs = await this.$store.dispatch('harvester/findAll', { type: PVC });
33+
},
34+
35+
data() {
36+
return {
37+
sourceVolume: '',
38+
targetVolume: '',
39+
errors: [],
40+
allPVCs: [],
41+
};
42+
},
43+
44+
computed: {
45+
...mapGetters({ t: 'i18n/t' }),
46+
47+
actionResource() {
48+
return this.resources[0];
49+
},
50+
51+
sourceVolumeOptions() {
52+
const volumes = this.actionResource.spec?.template?.spec?.volumes || [];
53+
54+
return sortBy(
55+
volumes
56+
.map((v) => v.persistentVolumeClaim?.claimName)
57+
.filter((name) => !!name)
58+
.map((name) => ({
59+
label: name,
60+
value: name
61+
})),
62+
'label'
63+
);
64+
},
65+
66+
namespacePVCs() {
67+
return this.allPVCs.filter((pvc) => pvc.metadata.namespace === this.actionResource.metadata.namespace);
68+
},
69+
70+
vmUsedVolumeNames() {
71+
const allVMs = this.$store.getters['harvester/all'](HCI.VM) || [];
72+
const names = new Set();
73+
74+
allVMs.forEach((vm) => {
75+
// Collect volume names from spec.template.spec.volumes (both PVC and DataVolume references)
76+
const volumes = vm.spec?.template?.spec?.volumes || [];
77+
78+
volumes.forEach((v) => {
79+
const name = v.persistentVolumeClaim?.claimName || v.dataVolume?.name;
80+
81+
if (name) {
82+
names.add(`${ vm.metadata.namespace }/${ name }`);
83+
}
84+
});
85+
86+
// Collect volume names from volumeClaimTemplates annotation
87+
const templates = parseVolumeClaimTemplates(vm);
88+
89+
templates.forEach((t) => {
90+
if (t.metadata?.name) {
91+
names.add(`${ vm.metadata.namespace }/${ t.metadata.name }`);
92+
}
93+
});
94+
});
95+
96+
return names;
97+
},
98+
99+
targetVolumeOptions() {
100+
return sortBy(
101+
this.namespacePVCs
102+
.filter((pvc) => {
103+
// Exclude volumes used by any VM (via spec.volumes or volumeClaimTemplates)
104+
if (this.vmUsedVolumeNames.has(`${ pvc.metadata.namespace }/${ pvc.metadata.name }`)) {
105+
return false;
106+
}
107+
108+
return true;
109+
})
110+
.map((pvc) => ({
111+
label: pvc.metadata.name,
112+
value: pvc.metadata.name
113+
})),
114+
'label'
115+
);
116+
},
117+
118+
disableSave() {
119+
return !this.sourceVolume || !this.targetVolume;
120+
},
121+
},
122+
123+
methods: {
124+
close() {
125+
this.sourceVolume = '';
126+
this.targetVolume = '';
127+
this.errors = [];
128+
this.$emit('close');
129+
},
130+
131+
async apply(buttonDone) {
132+
if (!this.actionResource) {
133+
buttonDone(false);
134+
135+
return;
136+
}
137+
138+
if (!this.sourceVolume) {
139+
const name = this.t('harvester.modal.storageMigration.fields.sourceVolume.label');
140+
141+
this['errors'] = [this.t('validation.required', { key: name })];
142+
buttonDone(false);
143+
144+
return;
145+
}
146+
147+
if (!this.targetVolume) {
148+
const name = this.t('harvester.modal.storageMigration.fields.targetVolume.label');
149+
150+
this['errors'] = [this.t('validation.required', { key: name })];
151+
buttonDone(false);
152+
153+
return;
154+
}
155+
156+
try {
157+
await this.actionResource.doAction('storageMigration', {
158+
sourceVolume: this.sourceVolume,
159+
targetVolume: this.targetVolume
160+
}, {}, false);
161+
162+
buttonDone(true);
163+
this.close();
164+
} catch (err) {
165+
const error = err?.data || err;
166+
167+
this['errors'] = exceptionToErrorsArray(error);
168+
buttonDone(false);
169+
}
170+
},
171+
}
172+
};
173+
</script>
174+
175+
<template>
176+
<Card :show-highlight-border="false">
177+
<template #title>
178+
{{ t('harvester.modal.storageMigration.title') }}
179+
</template>
180+
181+
<template #body>
182+
<LabeledSelect
183+
v-model:value="sourceVolume"
184+
:label="t('harvester.modal.storageMigration.fields.sourceVolume.label')"
185+
:placeholder="t('harvester.modal.storageMigration.fields.sourceVolume.placeholder')"
186+
:options="sourceVolumeOptions"
187+
class="mb-20"
188+
required
189+
/>
190+
191+
<LabeledSelect
192+
v-model:value="targetVolume"
193+
:label="t('harvester.modal.storageMigration.fields.targetVolume.label')"
194+
:placeholder="t('harvester.modal.storageMigration.fields.targetVolume.placeholder')"
195+
:options="targetVolumeOptions"
196+
required
197+
/>
198+
199+
<Banner
200+
v-for="(err, i) in errors"
201+
:key="i"
202+
color="error"
203+
:label="err"
204+
/>
205+
</template>
206+
207+
<template
208+
#actions
209+
class="actions"
210+
>
211+
<div class="buttons">
212+
<button
213+
class="btn role-secondary mr-10"
214+
@click="close"
215+
>
216+
{{ t('generic.cancel') }}
217+
</button>
218+
219+
<AsyncButton
220+
mode="apply"
221+
:disabled="disableSave"
222+
@click="apply"
223+
/>
224+
</div>
225+
</template>
226+
</Card>
227+
</template>
228+
229+
<style lang="scss" scoped>
230+
.actions {
231+
width: 100%;
232+
}
233+
234+
.buttons {
235+
display: flex;
236+
justify-content: flex-end;
237+
width: 100%;
238+
}
239+
</style>

pkg/harvester/l10n/en-us.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ harvester:
123123
namespace: Namespace
124124
message:
125125
success: 'Image { name } created successfully.'
126+
storageMigration:
127+
title: Storage Migration
128+
fields:
129+
sourceVolume:
130+
label: Source Volume
131+
placeholder: Select a source volume
132+
targetVolume:
133+
label: Target Volume
134+
placeholder: Select a target volume
126135
migration:
127136
failedMessage: Latest migration failed!
128137
title: Migration
@@ -232,6 +241,8 @@ harvester:
232241
migrate: Migrate
233242
cpuAndMemoryHotplug: Edit CPU and Memory
234243
abortMigration: Abort Migration
244+
storageMigration: Storage Migration
245+
cancelStorageMigration: Cancel Storage Migration
235246
createTemplate: Generate Template
236247
enableMaintenance: Enable Maintenance Mode
237248
disableMaintenance: Disable Maintenance Mode

pkg/harvester/models/kubevirt.io.virtualmachine.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,18 @@ export default class VirtVm extends HarvesterResource {
199199
icon: 'icon icon-close',
200200
label: this.t('harvester.action.abortMigration')
201201
},
202+
{
203+
action: 'storageMigration',
204+
enabled: !!this.actions?.storageMigration,
205+
icon: 'icon icon-copy',
206+
label: this.t('harvester.action.storageMigration')
207+
},
208+
{
209+
action: 'cancelStorageMigration',
210+
enabled: !!this.actions?.cancelStorageMigration,
211+
icon: 'icon icon-close',
212+
label: this.t('harvester.action.cancelStorageMigration')
213+
},
202214
{
203215
action: 'addHotplugVolume',
204216
enabled: !!this.actions?.addVolume,
@@ -368,6 +380,13 @@ export default class VirtVm extends HarvesterResource {
368380
});
369381
}
370382

383+
storageMigration(resources = this) {
384+
this.$dispatch('promptModal', {
385+
resources,
386+
component: 'HarvesterStorageMigrationDialog'
387+
});
388+
}
389+
371390
backupVM(resources = this) {
372391
this.$dispatch('promptModal', {
373392
resources,
@@ -520,6 +539,10 @@ export default class VirtVm extends HarvesterResource {
520539
this.doActionGrowl('abortMigration', {});
521540
}
522541

542+
cancelStorageMigration() {
543+
this.doActionGrowl('cancelStorageMigration', {});
544+
}
545+
523546
createTemplate(resources = this) {
524547
this.$dispatch('promptModal', {
525548
resources,

0 commit comments

Comments
 (0)