mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-08-16 12:49:14 +00:00
feat(forklift): small fixes
Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
parent
3ecb6bfc4a
commit
5c7dd1953d
@ -18,7 +18,10 @@ defineProps({
|
|||||||
:key="'net-' + idx"
|
:key="'net-' + idx"
|
||||||
class="mapping-entry"
|
class="mapping-entry"
|
||||||
>
|
>
|
||||||
<i class="icon icon-network" />
|
<i
|
||||||
|
class="icon icon-network"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<span>{{ entry }}</span>
|
<span>{{ entry }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -26,7 +29,10 @@ defineProps({
|
|||||||
:key="'stor-' + idx"
|
:key="'stor-' + idx"
|
||||||
class="mapping-entry"
|
class="mapping-entry"
|
||||||
>
|
>
|
||||||
<i class="icon icon-datastore" />
|
<i
|
||||||
|
class="icon icon-datastore"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<span>{{ entry }}</span>
|
<span>{{ entry }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,9 +2,12 @@
|
|||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
import { STORAGE_CLASS, NETWORK_ATTACHMENT } from '@shell/config/types';
|
import { STORAGE_CLASS, NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||||
import { useI18n } from '@shell/composables/useI18n';
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
|
import { randomStr } from '@shell/utils/string';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
|
import { FORKLIFT_NAMESPACE } from '../../config/harvester-map';
|
||||||
import MappingColumn from './MappingColumn.vue';
|
import MappingColumn from './MappingColumn.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -29,6 +32,7 @@ const networkEntries = ref([]);
|
|||||||
const storageEntries = ref([]);
|
const storageEntries = ref([]);
|
||||||
const allNetworkMaps = ref([]);
|
const allNetworkMaps = ref([]);
|
||||||
const allStorageMaps = ref([]);
|
const allStorageMaps = ref([]);
|
||||||
|
const errors = ref([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
|
|
||||||
// Restore from stepData
|
// Restore from stepData
|
||||||
@ -39,12 +43,12 @@ if (props.stepData.storageEntries.length > 0) {
|
|||||||
storageEntries.value = props.stepData.storageEntries;
|
storageEntries.value = props.stepData.storageEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NAMESPACE = 'forklift';
|
const NAMESPACE = FORKLIFT_NAMESPACE;
|
||||||
|
|
||||||
const harvesterNetworkOptions = computed(() => {
|
const harvesterNetworkOptions = computed(() => {
|
||||||
const options = [
|
const options = [
|
||||||
{ label: 'Pod Networking', value: 'pod' },
|
{ label: t('harvester.addons.vmMigration.configureMappings.networkMapping.options.podNetworking'), value: 'pod' },
|
||||||
{ label: 'Ignored', value: 'ignored' },
|
{ label: t('harvester.addons.vmMigration.configureMappings.networkMapping.options.ignored'), value: 'ignored' },
|
||||||
];
|
];
|
||||||
|
|
||||||
harvesterNetworks.value.forEach((net) => {
|
harvesterNetworks.value.forEach((net) => {
|
||||||
@ -167,7 +171,7 @@ const buildNetworkEntries = () => {
|
|||||||
|
|
||||||
if (!networkMap[netKey]) {
|
if (!networkMap[netKey]) {
|
||||||
networkMap[netKey] = {
|
networkMap[netKey] = {
|
||||||
name: net.name || 'Unknown',
|
name: net.name || t('harvester.addons.vmMigration.generic.unknown'),
|
||||||
id: net.id || '',
|
id: net.id || '',
|
||||||
vlanId: net.vlanId || '',
|
vlanId: net.vlanId || '',
|
||||||
target: '',
|
target: '',
|
||||||
@ -199,7 +203,7 @@ const buildStorageEntries = () => {
|
|||||||
if (ds && ds.id) {
|
if (ds && ds.id) {
|
||||||
if (!datastoreMap[ds.id]) {
|
if (!datastoreMap[ds.id]) {
|
||||||
datastoreMap[ds.id] = {
|
datastoreMap[ds.id] = {
|
||||||
name: ds.name || 'Unknown',
|
name: ds.name || t('harvester.addons.vmMigration.generic.unknown'),
|
||||||
id: ds.id,
|
id: ds.id,
|
||||||
type: ds.type || '',
|
type: ds.type || '',
|
||||||
capacity: 0,
|
capacity: 0,
|
||||||
@ -352,7 +356,7 @@ const saveAndReturn = async() => {
|
|||||||
const networkMap = await store.dispatch(`${ inStore }/create`, {
|
const networkMap = await store.dispatch(`${ inStore }/create`, {
|
||||||
type: HCI.FORKLIFT_NETWORK_MAP,
|
type: HCI.FORKLIFT_NETWORK_MAP,
|
||||||
metadata: {
|
metadata: {
|
||||||
name: `${ props.providerName }-network-map-${ props.useAllProviderData ? 'default' : Math.random().toString(36).substring(2, 7) }`,
|
name: `${ props.providerName }-network-map-${ props.useAllProviderData ? 'default' : randomStr(5).toLowerCase() }`,
|
||||||
namespace: NAMESPACE,
|
namespace: NAMESPACE,
|
||||||
ownerReferences: providerOwnerRef,
|
ownerReferences: providerOwnerRef,
|
||||||
},
|
},
|
||||||
@ -364,7 +368,7 @@ const saveAndReturn = async() => {
|
|||||||
const storageMap = await store.dispatch(`${ inStore }/create`, {
|
const storageMap = await store.dispatch(`${ inStore }/create`, {
|
||||||
type: HCI.FORKLIFT_STORAGE_MAP,
|
type: HCI.FORKLIFT_STORAGE_MAP,
|
||||||
metadata: {
|
metadata: {
|
||||||
name: `${ props.providerName }-storage-map-${ props.useAllProviderData ? 'default' : Math.random().toString(36).substring(2, 7) }`,
|
name: `${ props.providerName }-storage-map-${ props.useAllProviderData ? 'default' : randomStr(5).toLowerCase() }`,
|
||||||
namespace: NAMESPACE,
|
namespace: NAMESPACE,
|
||||||
ownerReferences: providerOwnerRef,
|
ownerReferences: providerOwnerRef,
|
||||||
},
|
},
|
||||||
@ -473,7 +477,7 @@ const init = async() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Data resolution failed — entries will use IDs as fallback
|
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedResolveDetails')];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,6 +512,12 @@ init();
|
|||||||
v-else
|
v-else
|
||||||
class="configure-mappings"
|
class="configure-mappings"
|
||||||
>
|
>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
<p class="text-deemphasized line-height-20">
|
<p class="text-deemphasized line-height-20">
|
||||||
{{ t('harvester.addons.vmMigration.configureMappings.description') }}
|
{{ t('harvester.addons.vmMigration.configureMappings.description') }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { SECRET } from '@shell/config/types';
|
|||||||
import { randomStr } from '@shell/utils/string';
|
import { randomStr } from '@shell/utils/string';
|
||||||
import { useI18n } from '@shell/composables/useI18n';
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
|
import { FORKLIFT_NAMESPACE } from '../../config/harvester-map';
|
||||||
|
|
||||||
const CREATE_NEW = '__create_new__';
|
const CREATE_NEW = '__create_new__';
|
||||||
|
|
||||||
@ -143,7 +144,7 @@ watch(selectedProvider, (val) => {
|
|||||||
createdSecret.value = null;
|
createdSecret.value = null;
|
||||||
} else {
|
} else {
|
||||||
const provider = allProviders.value.find(
|
const provider = allProviders.value.find(
|
||||||
(p) => p.metadata.name === val && p.metadata.namespace === 'forklift'
|
(p) => p.metadata.name === val && p.metadata.namespace === FORKLIFT_NAMESPACE
|
||||||
);
|
);
|
||||||
|
|
||||||
if (provider) {
|
if (provider) {
|
||||||
@ -212,7 +213,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
// Edit mode: update existing provider URL + secret, then poll
|
// Edit mode: update existing provider URL + secret, then poll
|
||||||
if (props.editMode && createdProvider.value) {
|
if (props.editMode && createdProvider.value) {
|
||||||
try {
|
try {
|
||||||
const namespace = 'forklift';
|
const namespace = FORKLIFT_NAMESPACE;
|
||||||
|
|
||||||
createdProvider.value.spec.url = url.value;
|
createdProvider.value.spec.url = url.value;
|
||||||
await createdProvider.value.save();
|
await createdProvider.value.save();
|
||||||
@ -253,7 +254,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
connected = true;
|
connected = true;
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
errorMsg = connectionCondition.message || 'Connection failed';
|
errorMsg = connectionCondition.message || t('harvester.addons.vmMigration.errors.connectionFailed');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -263,7 +264,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
connected = true;
|
connected = true;
|
||||||
break;
|
break;
|
||||||
} else if (readyCondition.status === 'False') {
|
} else if (readyCondition.status === 'False') {
|
||||||
errorMsg = readyCondition.message || 'Provider not ready';
|
errorMsg = readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -291,7 +292,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
// For existing providers, just poll for Ready/ConnectionTestSucceeded status
|
// For existing providers, just poll for Ready/ConnectionTestSucceeded status
|
||||||
if (isExistingProvider.value) {
|
if (isExistingProvider.value) {
|
||||||
try {
|
try {
|
||||||
const namespace = 'forklift';
|
const namespace = FORKLIFT_NAMESPACE;
|
||||||
const maxAttempts = 15;
|
const maxAttempts = 15;
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
let connected = false;
|
let connected = false;
|
||||||
@ -316,7 +317,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
connected = true;
|
connected = true;
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
errorMsg = connectionCondition.message || 'Connection failed';
|
errorMsg = connectionCondition.message || t('harvester.addons.vmMigration.errors.connectionFailed');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -326,7 +327,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
connected = true;
|
connected = true;
|
||||||
break;
|
break;
|
||||||
} else if (readyCondition.status === 'False') {
|
} else if (readyCondition.status === 'False') {
|
||||||
errorMsg = readyCondition.message || 'Provider not ready';
|
errorMsg = readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -363,7 +364,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
createdSecret.value = null;
|
createdSecret.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const namespace = 'forklift';
|
const namespace = FORKLIFT_NAMESPACE;
|
||||||
const secretName = `${ providerName.value }-creds-${ randomStr(4).toLowerCase() }`;
|
const secretName = `${ providerName.value }-creds-${ randomStr(4).toLowerCase() }`;
|
||||||
|
|
||||||
// Create Provider first so we have its UID for the ownerReference on the Secret
|
// Create Provider first so we have its UID for the ownerReference on the Secret
|
||||||
@ -440,7 +441,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
connected = true;
|
connected = true;
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
errorMsg = connectionCondition.message || 'Connection failed';
|
errorMsg = connectionCondition.message || t('harvester.addons.vmMigration.errors.connectionFailed');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -450,7 +451,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
connected = true;
|
connected = true;
|
||||||
break;
|
break;
|
||||||
} else if (readyCondition.status === 'False') {
|
} else if (readyCondition.status === 'False') {
|
||||||
errorMsg = readyCondition.message || 'Provider not ready';
|
errorMsg = readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -498,12 +499,17 @@ const testConnection = async(buttonCb) => {
|
|||||||
const init = async() => {
|
const init = async() => {
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
allProviders.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
allProviders.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
||||||
|
|
||||||
allSecrets.value = await store.dispatch(`${ inStore }/findAll`, {
|
allSecrets.value = await store.dispatch(`${ inStore }/findAll`, {
|
||||||
type: SECRET,
|
type: SECRET,
|
||||||
opt: { labelSelector: `ui.forklift/created-for-resource-type=${ HCI.FORKLIFT_PROVIDER }` }
|
opt: { labelSelector: `ui.forklift/created-for-resource-type=${ HCI.FORKLIFT_PROVIDER }` }
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedLoadProviders')];
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -77,7 +77,10 @@ const selectOptions = computed(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="['mapping-arrow', entry.target ? 'text-success' : 'text-deemphasized']">
|
<div :class="['mapping-arrow', entry.target ? 'text-success' : 'text-deemphasized']">
|
||||||
<i class="icon icon-right-arrow-alt" />
|
<i
|
||||||
|
class="icon icon-right-arrow-alt"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mapping-target">
|
<div class="mapping-target">
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
|
|||||||
@ -7,7 +7,9 @@ import { RcItemCard } from '@components/RcItemCard';
|
|||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import MappingsCell from '../MappingsCell';
|
import MappingsCell from '../MappingsCell';
|
||||||
import { useI18n } from '@shell/composables/useI18n';
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
|
import { randomStr } from '@shell/utils/string';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
|
import { FORKLIFT_NAMESPACE } from '../../config/harvester-map';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
providerName: { type: String, default: '' },
|
providerName: { type: String, default: '' },
|
||||||
@ -34,7 +36,7 @@ const loading = ref(true);
|
|||||||
// Restore persisted state
|
// Restore persisted state
|
||||||
planName.value = props.stepData.planName;
|
planName.value = props.stepData.planName;
|
||||||
|
|
||||||
const NAMESPACE = 'forklift';
|
const NAMESPACE = FORKLIFT_NAMESPACE;
|
||||||
const TARGET_NAMESPACE = 'default';
|
const TARGET_NAMESPACE = 'default';
|
||||||
|
|
||||||
watch(planName, (val) => {
|
watch(planName, (val) => {
|
||||||
@ -202,7 +204,7 @@ const startMigrationAction = async() => {
|
|||||||
const migration = await store.dispatch(`${ inStore }/create`, {
|
const migration = await store.dispatch(`${ inStore }/create`, {
|
||||||
type: HCI.FORKLIFT_MIGRATION,
|
type: HCI.FORKLIFT_MIGRATION,
|
||||||
metadata: {
|
metadata: {
|
||||||
name: `${ planName.value }-migration-${ Math.random().toString(36).substring(2, 7) }`,
|
name: `${ planName.value }-migration-${ randomStr(5).toLowerCase() }`,
|
||||||
namespace: NAMESPACE,
|
namespace: NAMESPACE,
|
||||||
ownerReferences: [planOwnerRef],
|
ownerReferences: [planOwnerRef],
|
||||||
},
|
},
|
||||||
@ -333,14 +335,17 @@ defineExpose({ startMigration: startMigrationAction });
|
|||||||
:id="vm.id"
|
:id="vm.id"
|
||||||
:key="vm.id"
|
:key="vm.id"
|
||||||
:variant="'small'"
|
:variant="'small'"
|
||||||
:header="{ title: { text: vm.name }, statuses: [{ icon: 'icon-notify-tick', color: 'text-success' }] }"
|
:header="{ title: { text: vm.name } }"
|
||||||
>
|
>
|
||||||
<template #item-card-content>
|
<template #item-card-content>
|
||||||
<div class="vm-card-content">
|
<div class="vm-card-content">
|
||||||
<div class="vm-card-specs">
|
<div class="vm-card-specs">
|
||||||
<span class="vm-os text-deemphasized">{{ vm.os }}</span>
|
<span class="vm-os text-deemphasized">{{ vm.os }}</span>
|
||||||
<span class="vm-resources">
|
<span class="vm-resources">
|
||||||
<i class="icon icon-disk" />
|
<i
|
||||||
|
class="icon icon-disk"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
{{ vm.cpus }} vCPU • {{ vm.memGB }} • {{ vm.diskDisplay }}
|
{{ vm.cpus }} vCPU • {{ vm.memGB }} • {{ vm.diskDisplay }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
import SortableTable from '@shell/components/SortableTable';
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
import { BadgeState } from '@components/BadgeState';
|
import { BadgeState } from '@components/BadgeState';
|
||||||
import { useI18n } from '@shell/composables/useI18n';
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ const datastoreMap = ref({});
|
|||||||
const sortableTableRef = ref(null);
|
const sortableTableRef = ref(null);
|
||||||
const allVMsSelected = ref(false);
|
const allVMsSelected = ref(false);
|
||||||
const selectedVMIds = ref(new Set());
|
const selectedVMIds = ref(new Set());
|
||||||
|
const errors = ref([]);
|
||||||
let skipNextSelectionEvent = false;
|
let skipNextSelectionEvent = false;
|
||||||
|
|
||||||
const lastFetchedAt = ref(null);
|
const lastFetchedAt = ref(null);
|
||||||
@ -81,13 +83,13 @@ if (props.stepData.tableRows.length > 0) {
|
|||||||
// Sync back to stepData
|
// Sync back to stepData
|
||||||
watch(discoveredVMs, (val) => {
|
watch(discoveredVMs, (val) => {
|
||||||
props.stepData.discoveredVMs = val;
|
props.stepData.discoveredVMs = val;
|
||||||
}, { deep: true });
|
});
|
||||||
watch(selectedVMIds, (val) => {
|
watch(selectedVMIds, (val) => {
|
||||||
props.stepData.selectedVMIds = val;
|
props.stepData.selectedVMIds = val;
|
||||||
}, { deep: true });
|
});
|
||||||
watch(tableRows, (val) => {
|
watch(tableRows, (val) => {
|
||||||
props.stepData.tableRows = val;
|
props.stepData.tableRows = val;
|
||||||
}, { deep: true });
|
});
|
||||||
|
|
||||||
const vmCount = computed(() => discoveredVMs.value.length);
|
const vmCount = computed(() => discoveredVMs.value.length);
|
||||||
const selectedCount = computed(() => selectedVMs.value.length);
|
const selectedCount = computed(() => selectedVMs.value.length);
|
||||||
@ -116,7 +118,7 @@ const headers = [
|
|||||||
labelKey: 'harvester.addons.vmMigration.selectVms.columns.vmName',
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.vmName',
|
||||||
value: 'vmName',
|
value: 'vmName',
|
||||||
sort: ['vmName'],
|
sort: ['vmName'],
|
||||||
subLabel: 'Identifier',
|
subLabel: t('harvester.addons.vmMigration.generic.identifier'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'os',
|
name: 'os',
|
||||||
@ -141,14 +143,14 @@ const headers = [
|
|||||||
labelKey: 'harvester.addons.vmMigration.selectVms.columns.network',
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.network',
|
||||||
value: 'network',
|
value: 'network',
|
||||||
sort: ['network'],
|
sort: ['network'],
|
||||||
subLabel: 'Identifier',
|
subLabel: t('harvester.addons.vmMigration.generic.identifier'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'datastore',
|
name: 'datastore',
|
||||||
labelKey: 'harvester.addons.vmMigration.selectVms.columns.datastore',
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.datastore',
|
||||||
value: 'datastore',
|
value: 'datastore',
|
||||||
sort: ['datastore'],
|
sort: ['datastore'],
|
||||||
subLabel: 'Identifier',
|
subLabel: t('harvester.addons.vmMigration.generic.identifier'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -337,10 +339,12 @@ const refreshVMs = async() => {
|
|||||||
const previousSelectedIds = new Set(selectedVMIds.value);
|
const previousSelectedIds = new Set(selectedVMIds.value);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
errors.value = [];
|
||||||
await fetchVMs();
|
await fetchVMs();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
discoveredVMs.value = [];
|
discoveredVMs.value = [];
|
||||||
tableRows.value = [];
|
tableRows.value = [];
|
||||||
|
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedRefreshVms')];
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedVMIds.value = new Set(
|
selectedVMIds.value = new Set(
|
||||||
@ -387,6 +391,7 @@ const init = async() => {
|
|||||||
await fetchVMs();
|
await fetchVMs();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
discoveredVMs.value = [];
|
discoveredVMs.value = [];
|
||||||
|
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedLoadVms')];
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -407,6 +412,12 @@ init();
|
|||||||
v-else
|
v-else
|
||||||
class="select-vms-step"
|
class="select-vms-step"
|
||||||
>
|
>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
<p class="text-deemphasized line-height-20">
|
<p class="text-deemphasized line-height-20">
|
||||||
{{ t('harvester.addons.vmMigration.selectVms.discovered', { count: vmCount }) }}
|
{{ t('harvester.addons.vmMigration.selectVms.discovered', { count: vmCount }) }}
|
||||||
<router-link
|
<router-link
|
||||||
|
|||||||
@ -94,6 +94,7 @@ export const CSI_SECRETS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const FORKLIFT_API_VERSION = 'forklift.konveyor.io/v1beta1';
|
export const FORKLIFT_API_VERSION = 'forklift.konveyor.io/v1beta1';
|
||||||
|
export const FORKLIFT_NAMESPACE = 'forklift';
|
||||||
|
|
||||||
// Some harvester CRD type is not equal to model file name, define the mapping here
|
// Some harvester CRD type is not equal to model file name, define the mapping here
|
||||||
export const HARVESTER_CRD_MAP = {
|
export const HARVESTER_CRD_MAP = {
|
||||||
|
|||||||
@ -1813,6 +1813,18 @@ harvester:
|
|||||||
'descheduler': 'The virtual machine auto balance optimizes workload scheduling by evicting pods that are not optimally placed according to administrator-defined policies.'
|
'descheduler': 'The virtual machine auto balance optimizes workload scheduling by evicting pods that are not optimally placed according to administrator-defined policies.'
|
||||||
|
|
||||||
vmMigration:
|
vmMigration:
|
||||||
|
generic:
|
||||||
|
unknown: Unknown
|
||||||
|
identifier: Identifier
|
||||||
|
podNetwork: Pod Network
|
||||||
|
errors:
|
||||||
|
failedLoadVms: Failed to load virtual machines
|
||||||
|
failedRefreshVms: Failed to refresh virtual machines
|
||||||
|
failedLoadProviders: Failed to load providers
|
||||||
|
failedResolveDetails: Failed to resolve network and datastore details
|
||||||
|
failedDecodeCredentials: Failed to decode provider credentials
|
||||||
|
connectionFailed: Connection failed
|
||||||
|
providerNotReady: Provider not ready
|
||||||
labels:
|
labels:
|
||||||
dashboard: Migrations
|
dashboard: Migrations
|
||||||
provider: Providers
|
provider: Providers
|
||||||
@ -1905,6 +1917,10 @@ harvester:
|
|||||||
plan: Migration Plan
|
plan: Migration Plan
|
||||||
progress: Progress
|
progress: Progress
|
||||||
mappings: Mappings
|
mappings: Mappings
|
||||||
|
progress:
|
||||||
|
failed: Failed
|
||||||
|
migration: Migration
|
||||||
|
initializingMigration: Initializing migration
|
||||||
configureMappings:
|
configureMappings:
|
||||||
title: Set Mappings
|
title: Set Mappings
|
||||||
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
||||||
@ -1916,6 +1932,9 @@ harvester:
|
|||||||
description: Map VMware port groups to Harvester networks
|
description: Map VMware port groups to Harvester networks
|
||||||
placeholder: Choose a Harvester network
|
placeholder: Choose a Harvester network
|
||||||
template: Use existing network mapping as template
|
template: Use existing network mapping as template
|
||||||
|
options:
|
||||||
|
podNetworking: Pod Networking
|
||||||
|
ignored: Ignored
|
||||||
storageMapping:
|
storageMapping:
|
||||||
title: Storage Mapping
|
title: Storage Mapping
|
||||||
description: Map VMware datastores to Harvester storage classes
|
description: Map VMware datastores to Harvester storage classes
|
||||||
@ -1939,6 +1958,16 @@ harvester:
|
|||||||
warningTitle: "Cold Migration Warning"
|
warningTitle: "Cold Migration Warning"
|
||||||
warningMessage: "All source VMs will be powered off, and downtime will occur during migration. Plan your maintenance window accordingly and notify relevant stakeholders"
|
warningMessage: "All source VMs will be powered off, and downtime will occur during migration. Plan your maintenance window accordingly and notify relevant stakeholders"
|
||||||
startMigration: Start Migration Plan
|
startMigration: Start Migration Plan
|
||||||
|
plan:
|
||||||
|
states:
|
||||||
|
error: Error
|
||||||
|
canceled: Canceled
|
||||||
|
inProgress: In Progress
|
||||||
|
active: Active
|
||||||
|
actions:
|
||||||
|
start: Start
|
||||||
|
restart: Restart
|
||||||
|
stop: Stop
|
||||||
|
|
||||||
vmImport:
|
vmImport:
|
||||||
titles:
|
titles:
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import HarvesterResource from './harvester';
|
import HarvesterResource from './harvester';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { PRODUCT_NAME } from '../config/harvester';
|
import { PRODUCT_NAME } from '../config/harvester';
|
||||||
|
import { randomStr } from '@shell/utils/string';
|
||||||
|
|
||||||
export default class ForkliftPlan extends HarvesterResource {
|
export default class ForkliftPlan extends HarvesterResource {
|
||||||
get listLocation() {
|
get listLocation() {
|
||||||
@ -87,22 +88,22 @@ export default class ForkliftPlan extends HarvesterResource {
|
|||||||
|
|
||||||
get stateDisplay() {
|
get stateDisplay() {
|
||||||
if (this.planFailed) {
|
if (this.planFailed) {
|
||||||
return 'Error';
|
return this.t('harvester.addons.vmMigration.plan.states.error');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.planCritical) {
|
if (this.planCritical) {
|
||||||
return 'Error';
|
return this.t('harvester.addons.vmMigration.plan.states.error');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.planCanceled) {
|
if (this.planCanceled) {
|
||||||
return 'Canceled';
|
return this.t('harvester.addons.vmMigration.plan.states.canceled');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isMigrating) {
|
if (this.isMigrating) {
|
||||||
return 'In Progress';
|
return this.t('harvester.addons.vmMigration.plan.states.inProgress');
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Active';
|
return this.t('harvester.addons.vmMigration.plan.states.active');
|
||||||
}
|
}
|
||||||
|
|
||||||
get stateBackground() {
|
get stateBackground() {
|
||||||
@ -141,7 +142,7 @@ export default class ForkliftPlan extends HarvesterResource {
|
|||||||
action: 'startMigration',
|
action: 'startMigration',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
icon: 'icon icon-play',
|
icon: 'icon icon-play',
|
||||||
label: this.isForkliftDashboard ? 'Restart' : 'Start',
|
label: this.isForkliftDashboard ? this.t('harvester.addons.vmMigration.plan.actions.restart') : this.t('harvester.addons.vmMigration.plan.actions.start'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,7 +151,7 @@ export default class ForkliftPlan extends HarvesterResource {
|
|||||||
action: 'stopMigration',
|
action: 'stopMigration',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
icon: 'icon icon-pause',
|
icon: 'icon icon-pause',
|
||||||
label: 'Stop',
|
label: this.t('harvester.addons.vmMigration.plan.actions.stop'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,7 +216,7 @@ export default class ForkliftPlan extends HarvesterResource {
|
|||||||
const migration = await this.$dispatch('create', {
|
const migration = await this.$dispatch('create', {
|
||||||
type: HCI.FORKLIFT_MIGRATION,
|
type: HCI.FORKLIFT_MIGRATION,
|
||||||
metadata: {
|
metadata: {
|
||||||
name: `${ this.metadata.name }-migration-${ Math.random().toString(36).substring(2, 7) }`,
|
name: `${ this.metadata.name }-migration-${ randomStr(5).toLowerCase() }`,
|
||||||
namespace,
|
namespace,
|
||||||
ownerReferences: [
|
ownerReferences: [
|
||||||
{
|
{
|
||||||
@ -252,7 +253,7 @@ export default class ForkliftPlan extends HarvesterResource {
|
|||||||
remove() {
|
remove() {
|
||||||
const opt = { ...arguments };
|
const opt = { ...arguments };
|
||||||
|
|
||||||
opt.params = { propagationPolicy: 'Background' };
|
opt.params = { propagationPolicy: 'Foreground' };
|
||||||
|
|
||||||
return this._remove(opt);
|
return this._remove(opt);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const rows = computed(() => {
|
|||||||
const netMap = allNetworkMaps.value.find((m) => m.metadata.name === netMapName && m.metadata.namespace === netMapNs);
|
const netMap = allNetworkMaps.value.find((m) => m.metadata.name === netMapName && m.metadata.namespace === netMapNs);
|
||||||
const storMap = allStorageMaps.value.find((m) => m.metadata.name === storMapName && m.metadata.namespace === storMapNs);
|
const storMap = allStorageMaps.value.find((m) => m.metadata.name === storMapName && m.metadata.namespace === storMapNs);
|
||||||
|
|
||||||
plan.networkEntries = (netMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.type === 'pod' ? 'Pod Network' : (e.destination?.name || '-') }`);
|
plan.networkEntries = (netMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.type === 'pod' ? t('harvester.addons.vmMigration.generic.podNetwork') : (e.destination?.name || '-') }`);
|
||||||
plan.storageEntries = (storMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.storageClass || '-' }`);
|
plan.storageEntries = (storMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.storageClass || '-' }`);
|
||||||
|
|
||||||
plan.vmIdsDisplay = (plan.spec?.vms || []).map((vm) => vm.id || vm.name || '').filter(Boolean).join(', ') || '-';
|
plan.vmIdsDisplay = (plan.spec?.vms || []).map((vm) => vm.id || vm.name || '').filter(Boolean).join(', ') || '-';
|
||||||
@ -73,33 +73,33 @@ const rows = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (step.error && !errorMsg) {
|
if (step.error && !errorMsg) {
|
||||||
const reasons = (step.error.reasons || []).join('; ') || 'Error';
|
const reasons = (step.error.reasons || []).join('; ') || t('harvester.addons.vmMigration.plan.states.error');
|
||||||
|
|
||||||
errorMsg = `${ step.name || `Step ${ idx + 1 }` }: ${ reasons }`;
|
errorMsg = `${ step.name || `Step ${ idx + 1 }` }: ${ reasons }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step.phase === 'Failed' && !errorMsg) {
|
if (step.phase === 'Failed' && !errorMsg) {
|
||||||
errorMsg = `${ step.name || `Step ${ idx + 1 }` }: Failed`;
|
errorMsg = `${ step.name || `Step ${ idx + 1 }` }: ${ t('harvester.addons.vmMigration.dashboard.progress.failed') }`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fallback to VM-level error if no step-level error found
|
// Fallback to VM-level error if no step-level error found
|
||||||
if (!errorMsg && vm.error) {
|
if (!errorMsg && vm.error) {
|
||||||
const reasons = (vm.error.reasons || []).join('; ') || 'Failed';
|
const reasons = (vm.error.reasons || []).join('; ') || t('harvester.addons.vmMigration.dashboard.progress.failed');
|
||||||
|
|
||||||
errorMsg = `${ currentStep || vm.error.phase || 'Migration' }: ${ reasons }`;
|
errorMsg = `${ currentStep || vm.error.phase || t('harvester.addons.vmMigration.dashboard.progress.migration') }: ${ reasons }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
overallProgress = Math.round(overallProgress * 10) / 10;
|
overallProgress = Math.round(overallProgress * 10) / 10;
|
||||||
|
|
||||||
// If no step-level error but the plan itself is failed, surface it
|
// If no step-level error but the plan itself is failed, surface it
|
||||||
if (!errorMsg && plan.planFailed) {
|
if (!errorMsg && plan.planFailed) {
|
||||||
errorMsg = `${ currentStep || 'Migration' }: Failed`;
|
errorMsg = `${ currentStep || t('harvester.addons.vmMigration.dashboard.progress.migration') }: ${ t('harvester.addons.vmMigration.dashboard.progress.failed') }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
vmName: vm.name || vm.id || 'Unknown',
|
vmName: vm.name || vm.id || t('harvester.addons.vmMigration.generic.unknown'),
|
||||||
vmId: vm.id || '',
|
vmId: vm.id || '',
|
||||||
progress: overallProgress,
|
progress: overallProgress,
|
||||||
currentStep,
|
currentStep,
|
||||||
@ -111,10 +111,10 @@ const rows = computed(() => {
|
|||||||
// If no migration progress but we have VMs in the spec, show them at 0%
|
// If no migration progress but we have VMs in the spec, show them at 0%
|
||||||
if (plan.vmProgress.length === 0 && plan.spec?.vms?.length > 0) {
|
if (plan.vmProgress.length === 0 && plan.spec?.vms?.length > 0) {
|
||||||
plan.vmProgress = plan.spec.vms.map((vm) => ({
|
plan.vmProgress = plan.spec.vms.map((vm) => ({
|
||||||
vmName: vm.name || vm.id || 'Unknown',
|
vmName: vm.name || vm.id || t('harvester.addons.vmMigration.generic.unknown'),
|
||||||
vmId: vm.id || '',
|
vmId: vm.id || '',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
currentStep: 'Initializing migration',
|
currentStep: t('harvester.addons.vmMigration.dashboard.progress.initializingMigration'),
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
canceled: false,
|
canceled: false,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -203,9 +203,13 @@ const init = async() => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (secret?.data) {
|
if (secret?.data) {
|
||||||
|
try {
|
||||||
stepData.provider.username = atob(secret.data.user || '');
|
stepData.provider.username = atob(secret.data.user || '');
|
||||||
stepData.provider.password = atob(secret.data.password || '');
|
stepData.provider.password = atob(secret.data.password || '');
|
||||||
stepData.provider.skipTlsVerify = atob(secret.data.insecureSkipVerify || '') === 'true';
|
stepData.provider.skipTlsVerify = atob(secret.data.insecureSkipVerify || '') === 'true';
|
||||||
|
} catch (e) {
|
||||||
|
errors.value = [t('harvester.addons.vmMigration.errors.failedDecodeCredentials')];
|
||||||
|
}
|
||||||
stepData.provider.createdSecret = secret;
|
stepData.provider.createdSecret = secret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user