mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-05-14 06:51:46 +00:00
feat: allow migrate mutliple VMs functionality and localization (#863)
* feat: enhance VM migration functionality and localization Signed-off-by: Andy Lee <andy.lee@suse.com> * refactor: add allVmsOnTargetNode method and update migration titles in localization Signed-off-by: Andy Lee <andy.lee@suse.com> * feat: enhance migration error handling and update localization for migration dialog Signed-off-by: Andy Lee <andy.lee@suse.com> --------- Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
parent
cd933bdbf8
commit
9de065a5c9
@ -1,15 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import { NODE } from '@shell/config/types';
|
import { NODE } from '@shell/config/types';
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
import { Card } from '@components/Card';
|
import { Card } from '@components/Card';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { HCI } from '../types';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
@ -62,28 +59,46 @@ export default {
|
|||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
vmi() {
|
anyCpuPinning() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
return this.resources.some((r) => r.isCpuPinning);
|
||||||
const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI);
|
},
|
||||||
const resource = vmiResources.find((VMI) => VMI.id === this.actionResource?.id) || null;
|
|
||||||
|
|
||||||
return resource;
|
vmsByNode() {
|
||||||
|
const groups = {};
|
||||||
|
|
||||||
|
for (const r of this.resources) {
|
||||||
|
const node = r.nodeName || '';
|
||||||
|
const name = r.nameDisplay || r.name || r.id;
|
||||||
|
|
||||||
|
if (!groups[node]) {
|
||||||
|
groups[node] = [];
|
||||||
|
}
|
||||||
|
groups[node].push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(groups).map(([node, vms]) => ({ node, vms })).sort((a, b) => a.node.localeCompare(b.node));
|
||||||
},
|
},
|
||||||
|
|
||||||
cpuPinningAlertMessage() {
|
cpuPinningAlertMessage() {
|
||||||
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
|
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
allVmsOnTargetNode() {
|
||||||
|
if (!this.nodeName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.resources.every((r) => r.nodeName === this.nodeName);
|
||||||
|
},
|
||||||
|
|
||||||
nodeNameList() {
|
nodeNameList() {
|
||||||
const nodes = this.$store.getters['harvester/all'](NODE);
|
const nodes = this.$store.getters['harvester/all'](NODE);
|
||||||
|
|
||||||
return nodes.filter((n) => {
|
return nodes.filter((n) => {
|
||||||
const isNotSelfNode = !!this.availableNodes.includes(n.id);
|
|
||||||
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
|
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
|
||||||
const isCpuPinning = this.actionResource?.isCpuPinning;
|
const matchingCpuManagerConfig = !this.anyCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
||||||
const matchingCpuManagerConfig = !isCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
|
|
||||||
|
|
||||||
return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig;
|
return isNotWitnessNode && matchingCpuManagerConfig;
|
||||||
}).map((n) => {
|
}).map((n) => {
|
||||||
let label = n?.metadata?.name;
|
let label = n?.metadata?.name;
|
||||||
const value = n?.metadata?.name;
|
const value = n?.metadata?.name;
|
||||||
@ -126,7 +141,29 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false);
|
// Filter out VMs already running on the selected node
|
||||||
|
const toMigrate = this.resources.filter((r) => r.nodeName !== this.nodeName);
|
||||||
|
|
||||||
|
// await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
||||||
|
// We want to show all migration errors if there are multiple VMs, so we use allSettled here and handle the results accordingly.
|
||||||
|
const results = await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
|
||||||
|
|
||||||
|
const failedMigrations = results
|
||||||
|
.map((result, index) => ({ resource: toMigrate[index], result }))
|
||||||
|
.filter(({ result }) => result.status === 'rejected');
|
||||||
|
|
||||||
|
if (failedMigrations.length) {
|
||||||
|
this['errors'] = failedMigrations.flatMap(({ resource, result }) => {
|
||||||
|
const vmName = resource?.nameDisplay || resource?.name || resource?.metadata?.name || this.$store.getters['i18n/t']('generic.unknown');
|
||||||
|
const error = result.reason?.data || result.reason;
|
||||||
|
const messages = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
return messages.map((message) => `${ vmName }: ${ message }`);
|
||||||
|
});
|
||||||
|
buttonDone(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close();
|
this.close();
|
||||||
@ -146,17 +183,35 @@ export default {
|
|||||||
<template>
|
<template>
|
||||||
<Card :show-highlight-border="false">
|
<Card :show-highlight-border="false">
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ t('harvester.modal.migration.title') }}
|
{{ t('harvester.modal.migration.vmMigrationTitle', { count: resources.length }) }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="actionResource?.isCpuPinning"
|
v-if="anyCpuPinning"
|
||||||
color="warning"
|
color="warning"
|
||||||
:label="cpuPinningAlertMessage"
|
:label="cpuPinningAlertMessage"
|
||||||
/>
|
/>
|
||||||
|
<p>
|
||||||
|
{{ t('harvester.modal.migration.selectedVMs') }}
|
||||||
|
</p>
|
||||||
|
<ul class="vm-list">
|
||||||
|
<li
|
||||||
|
v-for="group in vmsByNode"
|
||||||
|
:key="group.node"
|
||||||
|
>
|
||||||
|
{{ group.node || t('harvester.modal.migration.unknownNode') }}: {{ group.vms.join(', ') }}
|
||||||
|
<span
|
||||||
|
v-if="nodeName && group.node === nodeName"
|
||||||
|
class="already-on-target"
|
||||||
|
>
|
||||||
|
({{ t('harvester.modal.migration.alreadyOnTarget') }})
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="nodeName"
|
v-model:value="nodeName"
|
||||||
|
class="mt-15"
|
||||||
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
||||||
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
||||||
:options="nodeNameList"
|
:options="nodeNameList"
|
||||||
@ -183,7 +238,7 @@ export default {
|
|||||||
|
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
mode="apply"
|
mode="apply"
|
||||||
:disabled="!nodeName"
|
:disabled="!nodeName || allVmsOnTargetNode"
|
||||||
@click="apply"
|
@click="apply"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -201,4 +256,16 @@ export default {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.already-on-target {
|
||||||
|
color: var(--warning);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-list {
|
||||||
|
list-style: disc;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -144,6 +144,10 @@ harvester:
|
|||||||
migration:
|
migration:
|
||||||
failedMessage: Latest migration failed!
|
failedMessage: Latest migration failed!
|
||||||
title: Migration
|
title: Migration
|
||||||
|
vmMigrationTitle: '{count, plural, one {Migrating # VM} other {Migrating # VMs}}'
|
||||||
|
selectedVMs: "The following virtual machine(s) will be migrated to the target node"
|
||||||
|
unknownNode: (unknown node)
|
||||||
|
alreadyOnTarget: Already on Target
|
||||||
fields:
|
fields:
|
||||||
nodeName:
|
nodeName:
|
||||||
label: Target Node
|
label: Target Node
|
||||||
@ -248,6 +252,7 @@ harvester:
|
|||||||
suspendSchedule: Suspend
|
suspendSchedule: Suspend
|
||||||
restoreExistingVM: Replace Existing
|
restoreExistingVM: Replace Existing
|
||||||
migrate: Migrate
|
migrate: Migrate
|
||||||
|
vmMigrate: Virtual Machine Migration
|
||||||
cpuAndMemoryHotplug: Edit CPU and Memory
|
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||||
abortMigration: Abort Migration
|
abortMigration: Abort Migration
|
||||||
storageMigration: Storage Migration
|
storageMigration: Storage Migration
|
||||||
|
|||||||
@ -226,10 +226,12 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
label: this.t('harvester.action.ejectCDROM')
|
label: this.t('harvester.action.ejectCDROM')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'migrateVM',
|
action: 'migrateVM',
|
||||||
enabled: !!this.actions?.migrate,
|
enabled: !!this.actions?.migrate,
|
||||||
icon: 'icon icon-copy',
|
icon: 'icon icon-copy',
|
||||||
label: this.t('harvester.action.migrate')
|
label: this.t('harvester.action.vmMigrate'),
|
||||||
|
bulkable: true,
|
||||||
|
bulkAction: 'migrateVM'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'abortMigrationVM',
|
action: 'abortMigrationVM',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user