diff --git a/packages/eslint-plugin/cspell.wordlist.txt b/packages/eslint-plugin/cspell.wordlist.txt
index 86dfe4301..22a709574 100644
--- a/packages/eslint-plugin/cspell.wordlist.txt
+++ b/packages/eslint-plugin/cspell.wordlist.txt
@@ -30,6 +30,7 @@ fullname
cncf
omitempty
nics
+NICS
pnic
virtio
SSHA
diff --git a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json
index 01f75602d..829d6a850 100644
--- a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json
+++ b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json
@@ -28,6 +28,8 @@
"Add source and target providers for the migration.": "Add source and target providers for the migration.",
"All discovered networks have been mapped to the default network.": "All discovered networks have been mapped to the default network.",
"All discovered storages have been mapped to the default storage.": "All discovered storages have been mapped to the default storage.",
+ "All networks detected on the selected VMs require a mapping.": "All networks detected on the selected VMs require a mapping.",
+ "All storages detected on the selected VMs require a mapping.": "All storages detected on the selected VMs require a mapping.",
"API Error": "API Error",
"Application credential ID": "Application credential ID",
"Application credential name": "Application credential name",
@@ -64,9 +66,8 @@
"Controls the interval at which a new snapshot is requested prior to initiating a warm migration. The default value is 60 minutes.": "Controls the interval at which a new snapshot is requested prior to initiating a warm migration. The default value is 60 minutes.",
"Copied": "Copied",
"Copy": "Copy",
+ "Create": "Create",
"Create a migration plan and select VMs from the source provider for migration.": "Create a migration plan and select VMs from the source provider for migration.",
- "Create and edit": "Create and edit",
- "Create and start": "Create and start",
"Create NetworkMap": "Create NetworkMap",
"Create new provider": "Create new provider",
"Create plan": "Create plan",
@@ -160,6 +161,7 @@
"If true, the provider's CA certificate won't be validated.": "If true, the provider's CA certificate won't be validated.",
"If true, the provider's TLS certificate won't be validated.": "If true, the provider's TLS certificate won't be validated.",
"Image": "Image",
+ "Incomplete mapping": "Incomplete mapping",
"Information concerns": "Information concerns",
"Invalid password": "Invalid password",
"Invalid username": "Invalid username",
@@ -194,6 +196,8 @@
"Migrations (last 7 days)": "Migrations (last 7 days)",
"Migrations for virtualization": "Migrations for virtualization",
"MTU": "MTU",
+ "Multiple NICs mapped to Pod Networking ": "Multiple NICs mapped to Pod Networking ",
+ "Multiple NICs on the same network": "Multiple NICs on the same network",
"Must gather cleanup after (hours)": "Must gather cleanup after (hours)",
"Name": "Name",
"Name is primarily intended for creation idempotence and configuration definition. Cannot be updated.": "Name is primarily intended for creation idempotence and configuration definition. Cannot be updated.",
@@ -212,6 +216,8 @@
"Networks": "Networks",
"Networks used by the selected VMs": "Networks used by the selected VMs",
"New name was generated for the Network Map due to naming conflict.": "New name was generated for the Network Map due to naming conflict.",
+ "New name was generated for the Storage Map due to naming conflict.": "New name was generated for the Storage Map due to naming conflict.",
+ "NICs with empty NIC profile": "NICs with empty NIC profile",
"No credentials found.": "No credentials found.",
"No inventory data available.": "No inventory data available.",
"No NetworkMaps found.": "No NetworkMaps found.",
@@ -227,7 +233,6 @@
"No secret.": "No secret.",
"No StorageMaps found.": "No StorageMaps found.",
"No storages in this category": "No storages in this category",
- "No target provider exists ": "No target provider exists ",
"Not Ready": "Not Ready",
"NUMA": "NUMA",
"Number of cluster in provider": "Number of cluster in provider",
@@ -344,6 +349,7 @@
"Storage": "Storage",
"Storage classes": "Storage classes",
"Storage domains": "Storage domains",
+ "Storage Map name re-generated": "Storage Map name re-generated",
"Storage map:": "Storage map:",
"Storage mappings have been re-generated": "Storage mappings have been re-generated",
"StorageMaps": "StorageMaps",
@@ -413,6 +419,9 @@
"Virtual Machine Migrations (last 7 days)": "Virtual Machine Migrations (last 7 days)",
"Virtual machines": "Virtual machines",
"Virtual Machines": "Virtual Machines",
+ "VM(s) with more than one interface mapped to Pod Networking were detected.": "VM(s) with more than one interface mapped to Pod Networking were detected.",
+ "VM(s) with multiple NICs on the same network were detected.": "VM(s) with multiple NICs on the same network were detected.",
+ "VM(s) with NICs that are not linked with a NIC profile were detected.": "VM(s) with NICs that are not linked with a NIC profile were detected.",
"VMs": "VMs",
"VMware only: vSphere product name.": "VMware only: vSphere product name.",
"VMware Virtual Disk Development Kit (VDDK) image, for example: quay.io/kubev2v/vddk:latest .": "VMware Virtual Disk Development Kit (VDDK) image, for example: quay.io/kubev2v/vddk:latest .",
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationPage.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationPage.tsx
index 96dbe1da7..c282fcfc6 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationPage.tsx
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationPage.tsx
@@ -4,22 +4,16 @@ import SectionHeading from 'src/components/headers/SectionHeading';
import { PlanCreateProgress } from 'src/modules/Plans/views/create';
import { ForkliftTrans, useForkliftTranslation } from 'src/utils/i18n';
-import { Alert, AlertVariant, Button, Flex, FlexItem, PageSection } from '@patternfly/react-core';
+import { LoadingDots } from '@kubev2v/common';
+import { Alert, Button, Flex, FlexItem, PageSection } from '@patternfly/react-core';
import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
import { PlansCreateForm } from './components/PlansCreateForm';
-import { StateAlerts } from './components/StateAlerts';
import { startCreate } from './reducer/actions';
-import { GeneralAlerts } from './types';
+import { isDone } from './reducer/helpers';
import { useFetchEffects } from './useFetchEffects';
import { useSaveEffect } from './useSaveEffect';
-const generalMessages = (
- t: (key: string) => string,
-): { [key in GeneralAlerts]: { title: string; body: string } } => ({
- NEXT_VALID_PROVIDER_SELECTED: { title: t('Error'), body: t('No target provider exists ') },
-});
-
const ProvidersCreateVmMigrationPage: FC = () => {
const { t } = useForkliftTranslation();
const history = useHistory();
@@ -35,25 +29,29 @@ const ProvidersCreateVmMigrationPage: FC = () => {
return <>>;
}
+ if (!isDone(state.flow.initialLoading) && !state.flow.apiError) {
+ return ;
+ }
+
return (
- }
- variant="info"
- title={t('How to create a migration plan')}
- >
-
- To migrate virtual machines select target provider, namespace, mappings and click the{' '}
- Create button to crete the plan.
-
-
-
-
+
+ }
+ variant="info"
+ title={t('How to create a migration plan')}
+ >
+
+ To migrate virtual machines select target provider, namespace, mappings and click the{' '}
+ Create button to crete the plan.
+
+
-
+
-
+
+
{state.flow.apiError && (
{
{state.flow.apiError.message || state.flow.apiError.toString()}
)}
- ({
- key,
- ...generalMessages(t)[key],
- }))}
- />
-
-
-
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/EditableDescriptionItem.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/EditableDescriptionItem.tsx
index a90c5d5c8..873fcbe6b 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/EditableDescriptionItem.tsx
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/EditableDescriptionItem.tsx
@@ -15,7 +15,8 @@ export const EditableDescriptionItem: FC<{
content: ReactNode;
ariaEditLabel: string;
onEdit: () => void;
-}> = ({ title, content, ariaEditLabel = 'Edit', onEdit }) => (
+ isDisabled?: boolean;
+}> = ({ title, content, ariaEditLabel = 'Edit', onEdit, isDisabled = false }) => (
{title}
@@ -25,9 +26,9 @@ export const EditableDescriptionItem: FC<{
className="forklift-page-editable-description-item-button"
style={{ paddingTop: 0 }}
variant="plain"
- icon={}
- aria-label={ariaEditLabel}
- onClick={onEdit}
+ {...(isDisabled
+ ? {}
+ : { icon: , onClick: onEdit, 'aria-Label': ariaEditLabel })}
/>
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansCreateForm.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansCreateForm.tsx
index e4c54c88d..69e7470c1 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansCreateForm.tsx
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansCreateForm.tsx
@@ -54,7 +54,7 @@ import { StateAlerts } from './StateAlerts';
const buildNetworkMessages = (
t: (key: string) => string,
-): { [key in NetworkAlerts]: { title: string; body: string } } => ({
+): { [key in NetworkAlerts]: { title: string; body: string; blocker?: boolean } } => ({
NET_MAP_NAME_REGENERATED: {
title: t('Network Map name re-generated'),
body: t('New name was generated for the Network Map due to naming conflict.'),
@@ -63,17 +63,46 @@ const buildNetworkMessages = (
title: t('Network mappings have been re-generated'),
body: t('All discovered networks have been mapped to the default network.'),
},
+ MULTIPLE_NICS_ON_THE_SAME_NETWORK: {
+ title: t('Multiple NICs on the same network'),
+ body: t('VM(s) with multiple NICs on the same network were detected.'),
+ },
+ OVIRT_NICS_WITH_EMPTY_PROFILE: {
+ title: t('NICs with empty NIC profile'),
+ body: t('VM(s) with NICs that are not linked with a NIC profile were detected.'),
+ blocker: true,
+ },
+ UNMAPPED_NETWORKS: {
+ title: t('Incomplete mapping'),
+ body: t('All networks detected on the selected VMs require a mapping.'),
+ blocker: true,
+ },
+ MULTIPLE_NICS_MAPPED_TO_POD_NETWORKING: {
+ title: t('Multiple NICs mapped to Pod Networking '),
+ body: t('VM(s) with more than one interface mapped to Pod Networking were detected.'),
+ blocker: true,
+ },
});
const buildStorageMessages = (
t: (key: string) => string,
-): { [key in StorageAlerts]: { title: string; body: string } } => ({
+): { [key in StorageAlerts]: { title: string; body: string; blocker?: boolean } } => ({
STORAGE_MAPPING_REGENERATED: {
title: t('Storage mappings have been re-generated'),
body: t('All discovered storages have been mapped to the default storage.'),
},
+ STORAGE_MAP_NAME_REGENERATED: {
+ title: t('Storage Map name re-generated'),
+ body: t('New name was generated for the Storage Map due to naming conflict.'),
+ },
+ UNMAPPED_STORAGES: {
+ title: t('Incomplete mapping'),
+ body: t('All storages detected on the selected VMs require a mapping.'),
+ blocker: true,
+ },
});
export const PlansCreateForm = ({
+ children,
state: {
underConstruction: { plan, netMap, storageMap },
validation,
@@ -99,6 +128,7 @@ export const PlansCreateForm = ({
},
dispatch,
}: {
+ children?;
state: CreateVmMigrationPageState;
dispatch: (action: PageAction) => void;
}) => {
@@ -137,6 +167,7 @@ export const PlansCreateForm = ({
}
>
+ {children}
dispatch(setPlanName(value?.trim() ?? ''))}
/>
@@ -170,6 +202,7 @@ export const PlansCreateForm = ({
content={plan.metadata.name}
ariaEditLabel={t('Edit plan name')}
onEdit={() => setIsNameEdited(true)}
+ isDisabled={flow.editingDone}
/>
)}
dispatch(setPlanTargetProvider(value))}
validated={validation.targetProvider}
id="targetProvider"
+ isDisabled={flow.editingDone}
>
{[
setIsTargetProviderEdited(true)}
+ isDisabled={flow.editingDone}
/>
)}
{isTargetNamespaceEdited ||
@@ -264,6 +299,7 @@ export const PlansCreateForm = ({
onChange={(value) => dispatch(setPlanTargetNamespace(value))}
validated={validation.targetNamespace}
id="targetNamespace"
+ isDisabled={flow.editingDone}
>
{[
setIsTargetNamespaceEdited(true)}
+ isDisabled={flow.editingDone}
/>
)}
@@ -318,6 +355,14 @@ export const PlansCreateForm = ({
+ ({
+ key,
+ ...networkMessages[key],
+ }))}
+ onClose={(key) => dispatch(removeAlert(key as NetworkAlerts))}
+ />
({
@@ -356,6 +401,14 @@ export const PlansCreateForm = ({
+ ({
+ key,
+ ...storageMessages[key],
+ }))}
+ onClose={(key) => dispatch(removeAlert(key as StorageAlerts))}
+ />
({
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/StateAlerts.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/StateAlerts.tsx
index 44543f06b..4fe57f0bc 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/StateAlerts.tsx
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/StateAlerts.tsx
@@ -4,15 +4,17 @@ import { Alert, AlertActionCloseButton, AlertVariant } from '@patternfly/react-c
export const StateAlerts: FC<{
variant: AlertVariant;
- messages: { key: string; title: string; body: string }[];
+ messages: { key: string; title: string; body: string; blocker?: boolean }[];
onClose?: (key: string) => void;
}> = ({ variant, messages, onClose }) => (
<>
- {messages.map(({ key, title, body }) => (
+ {messages.map(({ key, title, body, blocker }) => (
onClose(key)} /> : undefined}
+ actionClose={
+ onClose && !blocker ? onClose(key)} /> : undefined
+ }
variant={variant}
title={title}
>
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/actions.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/actions.ts
index 4d2471f11..9837e8bd4 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/actions.ts
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/actions.ts
@@ -13,7 +13,7 @@ import {
import { InventoryNetwork } from '../../../hooks/useNetworks';
import { InventoryStorage } from '../../../hooks/useStorages';
-import { GeneralAlerts, Mapping, NetworkAlerts, StorageAlerts } from '../types';
+import { Mapping, NetworkAlerts, StorageAlerts } from '../types';
export const POD_NETWORK = 'Pod Networking';
export const DEFAULT_NAMESPACE = 'default';
@@ -147,8 +147,8 @@ export interface PlanAvailableSourceStorages {
error?: Error;
}
-export interface PlanNickProfiles {
- nickProfiles: OVirtNicProfile[];
+export interface PlanNicProfiles {
+ nicProfiles: OVirtNicProfile[];
loading: boolean;
error?: Error;
}
@@ -164,7 +164,7 @@ export interface PlanError {
}
export interface PlanAlert {
- alertKey: NetworkAlerts | StorageAlerts | GeneralAlerts;
+ alertKey: NetworkAlerts | StorageAlerts;
}
export interface PlanMapping {
@@ -204,8 +204,8 @@ export const setPlanName = (name: string): PageAction => ({
type: 'SET_AVAILABLE_PROVIDERS',
payload: {
@@ -217,8 +217,8 @@ export const setAvailableProviders = (
export const setExistingPlans = (
existingPlans: V1beta1Plan[],
- loaded: boolean,
- error: Error,
+ loaded?: boolean,
+ error?: Error,
): PageAction => ({
type: 'SET_EXISTING_PLANS',
payload: {
@@ -230,8 +230,8 @@ export const setExistingPlans = (
export const setExistingNetMaps = (
existingNetMaps: V1beta1NetworkMap[],
- loaded: boolean,
- error: Error,
+ loaded?: boolean,
+ error?: Error,
): PageAction => ({
type: 'SET_EXISTING_NET_MAPS',
payload: {
@@ -243,8 +243,8 @@ export const setExistingNetMaps = (
export const setExistingStorageMaps = (
existingStorageMaps: V1beta1StorageMap[],
- loaded: boolean,
- error: Error,
+ loaded?: boolean,
+ error?: Error,
): PageAction => ({
type: 'SET_EXISTING_STORAGE_MAPS',
payload: {
@@ -256,7 +256,7 @@ export const setExistingStorageMaps = (
export const setAvailableTargetNamespaces = (
availableTargetNamespaces: OpenShiftNamespace[],
- loading: boolean,
+ loading?: boolean,
error?: Error,
): PageAction => ({
type: 'SET_AVAILABLE_TARGET_NAMESPACES',
@@ -307,7 +307,7 @@ export const deleteNetworkMapping = ({
export const setAvailableTargetNetworks = (
availableTargetNetworks: OpenShiftNetworkAttachmentDefinition[],
- loading: boolean,
+ loading?: boolean,
error?: Error,
): PageAction => ({
type: 'SET_AVAILABLE_TARGET_NETWORKS',
@@ -316,7 +316,7 @@ export const setAvailableTargetNetworks = (
export const setAvailableSourceNetworks = (
availableSourceNetworks: InventoryNetwork[],
- loading: boolean,
+ loading?: boolean,
error?: Error,
): PageAction => ({
type: 'SET_AVAILABLE_SOURCE_NETWORKS',
@@ -325,7 +325,7 @@ export const setAvailableSourceNetworks = (
export const setAvailableSourceStorages = (
availableSourceStorages: InventoryStorage[],
- loading: boolean,
+ loading?: boolean,
error?: Error,
): PageAction => ({
type: 'SET_AVAILABLE_SOURCE_STORAGES',
@@ -338,7 +338,7 @@ export const setAvailableSourceStorages = (
export const setAvailableTargetStorages = (
availableTargetStorages: OpenShiftStorageClass[],
- loading: boolean,
+ loading?: boolean,
error?: Error,
): PageAction => ({
type: 'SET_AVAILABLE_TARGET_STORAGES',
@@ -346,17 +346,17 @@ export const setAvailableTargetStorages = (
});
export const setNicProfiles = (
- nickProfiles: OVirtNicProfile[],
- nicProfilesLoading: boolean,
- nicProfilesError: Error,
-): PageAction => ({
+ nicProfiles: OVirtNicProfile[],
+ nicProfilesLoading?: boolean,
+ nicProfilesError?: Error,
+): PageAction => ({
type: 'SET_NICK_PROFILES',
- payload: { nickProfiles, loading: nicProfilesLoading, error: nicProfilesError },
+ payload: { nicProfiles: nicProfiles, loading: nicProfilesLoading, error: nicProfilesError },
});
export const setDisks = (
disks: (OVirtDisk | OpenstackVolume)[],
- loading: boolean,
+ loading?: boolean,
error?: Error,
): PageAction => ({
type: 'SET_DISKS',
@@ -374,7 +374,7 @@ export const setAPiError = (error: Error): PageAction => ({
type: 'REMOVE_ALERT',
payload: { alertKey },
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/calculateMappings.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/calculateMappings.ts
index e4af5ff95..be0790803 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/calculateMappings.ts
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/calculateMappings.ts
@@ -4,7 +4,15 @@ import { universalComparator } from '@kubev2v/common';
import { CreateVmMigrationPageState } from '../types';
-import { POD_NETWORK } from './actions';
+import {
+ POD_NETWORK,
+ SET_AVAILABLE_SOURCE_NETWORKS,
+ SET_AVAILABLE_SOURCE_STORAGES,
+ SET_AVAILABLE_TARGET_NETWORKS,
+ SET_AVAILABLE_TARGET_STORAGES,
+ SET_DISKS,
+ SET_NICK_PROFILES,
+} from './actions';
export const calculateNetworks = (
draft: Draft,
@@ -14,9 +22,13 @@ export const calculateNetworks = (
underConstruction: { plan },
calculatedOnce: { sourceNetworkLabelToId, networkIdsUsedBySelectedVms },
calculatedPerNamespace: { sourceNetworks, targetNetworks, networkMappings },
- flow: { nicProfilesLoaded, sourceNetworkLoaded, targetNetworksLoaded },
+ flow: { initialLoading },
} = draft;
- if (!sourceNetworkLoaded || !nicProfilesLoaded || !targetNetworksLoaded) {
+ if (
+ !initialLoading[SET_AVAILABLE_SOURCE_NETWORKS] ||
+ !initialLoading[SET_NICK_PROFILES] ||
+ !initialLoading[SET_AVAILABLE_TARGET_NETWORKS]
+ ) {
return {
sourceNetworks,
targetNetworks,
@@ -68,10 +80,14 @@ export const calculateStorages = (
underConstruction: { plan },
calculatedOnce: { sourceStorageLabelToId, storageIdsUsedBySelectedVms },
calculatedPerNamespace: { storageMappings, targetStorages, sourceStorages },
- flow: { disksLoaded, sourceStoragesLoaded, targetStoragesLoaded },
+ flow: { initialLoading },
} = draft;
- if (!sourceStoragesLoaded || !targetStoragesLoaded || !disksLoaded) {
+ if (
+ !initialLoading[SET_AVAILABLE_SOURCE_STORAGES] ||
+ !initialLoading[SET_AVAILABLE_TARGET_STORAGES] ||
+ !initialLoading[SET_DISKS]
+ ) {
// wait for all resources
return {
storageMappings,
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/createInitialState.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/createInitialState.ts
index cab4f0a67..b9fecf42f 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/createInitialState.ts
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/createInitialState.ts
@@ -6,11 +6,18 @@ import {
import { networkMapTemplate, planTemplate, storageMapTemplate } from '../../create/templates';
import { toId, VmData } from '../../details';
-import { CreateVmMigrationPageState } from '../types';
+import {
+ CreateVmMigrationPageState,
+ MULTIPLE_NICS_ON_THE_SAME_NETWORK,
+ OVIRT_NICS_WITH_EMPTY_PROFILE,
+} from '../types';
+import { SET_DISKS, SET_NICK_PROFILES } from './actions';
import { getNamespacesUsedBySelectedVms } from './getNamespacesUsedBySelectedVms';
import { getNetworksUsedBySelectedVms } from './getNetworksUsedBySelectedVMs';
import { getStoragesUsedBySelectedVms } from './getStoragesUsedBySelectedVMs';
+import { hasMultipleNicsOnTheSameNetwork } from './hasMultipleNicsOnTheSameNetwork';
+import { hasNicWithEmptyProfile } from './hasNicWithEmptyProfile';
import { generateName, getObjectRef, resourceFieldsForType } from './helpers';
export const createInitialState = ({
@@ -25,124 +32,128 @@ export const createInitialState = ({
namespace: string;
sourceProvider: V1beta1Provider;
selectedVms: VmData[];
-}): CreateVmMigrationPageState => ({
- underConstruction: {
- plan: {
- ...planTemplate,
- metadata: {
- ...planTemplate?.metadata,
- name: generateName(sourceProvider.metadata.name),
- namespace,
- },
- spec: {
- ...planTemplate?.spec,
- provider: {
- source: getObjectRef(sourceProvider),
- destination: undefined,
+}): CreateVmMigrationPageState => {
+ const hasVmNicWithEmptyProfile = hasNicWithEmptyProfile(sourceProvider, selectedVms);
+ return {
+ underConstruction: {
+ plan: {
+ ...planTemplate,
+ metadata: {
+ ...planTemplate?.metadata,
+ name: generateName(sourceProvider.metadata.name),
+ namespace,
+ },
+ spec: {
+ ...planTemplate?.spec,
+ provider: {
+ source: getObjectRef(sourceProvider),
+ destination: undefined,
+ },
+ targetNamespace: undefined,
+ vms: selectedVms.map((data) => ({ name: data.name, id: toId(data) })),
},
- targetNamespace: undefined,
- vms: selectedVms.map((data) => ({ name: data.name, id: toId(data) })),
},
- },
- netMap: {
- ...networkMapTemplate,
- metadata: {
- ...networkMapTemplate?.metadata,
- name: generateName(sourceProvider.metadata.name),
- namespace,
+ netMap: {
+ ...networkMapTemplate,
+ metadata: {
+ ...networkMapTemplate?.metadata,
+ name: generateName(sourceProvider.metadata.name),
+ namespace,
+ },
+ spec: {
+ ...networkMapTemplate?.spec,
+ provider: {
+ source: getObjectRef(sourceProvider),
+ destination: undefined,
+ },
+ },
},
- spec: {
- ...networkMapTemplate?.spec,
- provider: {
- source: getObjectRef(sourceProvider),
- destination: undefined,
+ storageMap: {
+ ...storageMapTemplate,
+ metadata: {
+ ...storageMapTemplate?.metadata,
+ name: generateName(sourceProvider.metadata.name),
+ namespace,
+ },
+ spec: {
+ ...storageMapTemplate?.spec,
+ provider: {
+ source: getObjectRef(sourceProvider),
+ destination: undefined,
+ },
},
},
},
- storageMap: {
- ...storageMapTemplate,
- metadata: {
- ...storageMapTemplate?.metadata,
- name: generateName(sourceProvider.metadata.name),
- namespace,
+
+ existingResources: {
+ plans: [],
+ providers: [],
+ targetNamespaces: [],
+ targetNetworks: [],
+ sourceNetworks: [],
+ targetStorages: [],
+ sourceStorages: [],
+ nicProfiles: [],
+ disks: [],
+ netMaps: [],
+ storageMaps: [],
+ },
+ receivedAsParams: {
+ selectedVms,
+ sourceProvider,
+ namespace,
+ },
+ validation: {
+ planName: 'default',
+ targetNamespace: 'default',
+ targetProvider: 'default',
+ networkMappings: hasVmNicWithEmptyProfile ? 'error' : 'default',
+ storageMappings: 'default',
+ },
+ alerts: {
+ networkMappings: {
+ errors: hasVmNicWithEmptyProfile ? [OVIRT_NICS_WITH_EMPTY_PROFILE] : [],
+ warnings: hasMultipleNicsOnTheSameNetwork(selectedVms)
+ ? [MULTIPLE_NICS_ON_THE_SAME_NETWORK]
+ : [],
},
- spec: {
- ...storageMapTemplate?.spec,
- provider: {
- source: getObjectRef(sourceProvider),
- destination: undefined,
- },
+ storageMappings: {
+ errors: [],
+ warnings: [],
},
},
- },
-
- existingResources: {
- plans: [],
- providers: [],
- targetNamespaces: [],
- targetNetworks: [],
- sourceNetworks: [],
- targetStorages: [],
- sourceStorages: [],
- nickProfiles: [],
- disks: [],
- netMaps: [],
- },
- receivedAsParams: {
- selectedVms,
- sourceProvider,
- namespace,
- },
- validation: {
- planName: 'default',
- targetNamespace: 'default',
- targetProvider: 'default',
- },
- alerts: {
- general: {
- errors: [],
- warnings: [],
+ calculatedOnce: {
+ vmFieldsFactory: resourceFieldsForType(sourceProvider?.spec?.type as ProviderType),
+ networkIdsUsedBySelectedVms:
+ sourceProvider.spec?.type !== 'ovirt' ? getNetworksUsedBySelectedVms(selectedVms, []) : [],
+ sourceNetworkLabelToId: {},
+ sourceStorageLabelToId: {},
+ storageIdsUsedBySelectedVms: ['ovirt', 'openstack'].includes(sourceProvider.spec?.type)
+ ? []
+ : getStoragesUsedBySelectedVms(selectedVms, []),
+ namespacesUsedBySelectedVms:
+ sourceProvider.spec?.type === 'openshift'
+ ? getNamespacesUsedBySelectedVms(selectedVms)
+ : [],
},
- networkMappings: {
- errors: [],
- warnings: [],
+ calculatedPerNamespace: {
+ targetNetworks: [],
+ targetStorages: [],
+ sourceNetworks: [],
+ networkMappings: undefined,
+ sourceStorages: [],
+ storageMappings: undefined,
},
- storageMappings: {
- errors: [],
- warnings: [],
+ workArea: {
+ targetProvider: undefined,
},
- },
- calculatedOnce: {
- vmFieldsFactory: resourceFieldsForType(sourceProvider?.spec?.type as ProviderType),
- networkIdsUsedBySelectedVms:
- sourceProvider.spec?.type !== 'ovirt' ? getNetworksUsedBySelectedVms(selectedVms, []) : [],
- sourceNetworkLabelToId: {},
- sourceStorageLabelToId: {},
- storageIdsUsedBySelectedVms: ['ovirt', 'openstack'].includes(sourceProvider.spec?.type)
- ? []
- : getStoragesUsedBySelectedVms(selectedVms, []),
- namespacesUsedBySelectedVms:
- sourceProvider.spec?.type === 'openshift' ? getNamespacesUsedBySelectedVms(selectedVms) : [],
- },
- calculatedPerNamespace: {
- targetNetworks: [],
- targetStorages: [],
- sourceNetworks: [],
- networkMappings: undefined,
- sourceStorages: [],
- storageMappings: undefined,
- },
- workArea: {
- targetProvider: undefined,
- },
- flow: {
- editingDone: false,
- apiError: undefined,
- disksLoaded: false,
- nicProfilesLoaded: false,
- sourceNetworkLoaded: false,
- sourceStoragesLoaded: false,
- targetNetworksLoaded: false,
- targetStoragesLoaded: false,
- },
-});
+ flow: {
+ editingDone: false,
+ apiError: undefined,
+ initialLoading: {
+ [SET_DISKS]: !['ovirt', 'openstack'].includes(sourceProvider.spec?.type),
+ [SET_NICK_PROFILES]: sourceProvider.spec?.type !== 'ovirt',
+ },
+ },
+ };
+};
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/getNetworksUsedBySelectedVMs.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/getNetworksUsedBySelectedVMs.ts
index 82a2677a0..906816b25 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/getNetworksUsedBySelectedVMs.ts
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/getNetworksUsedBySelectedVMs.ts
@@ -1,4 +1,4 @@
-import { OVirtNicProfile } from '@kubev2v/types';
+import { OVirtNicProfile, ProviderVirtualMachine } from '@kubev2v/types';
import { VmData } from '../../details';
@@ -13,31 +13,38 @@ export const getNetworksUsedBySelectedVms = (
new Set(
selectedVMs
?.map(({ vm }) => vm)
- .flatMap((vm) => {
- switch (vm.providerType) {
- case 'vsphere': {
- return vm.networks?.map((network) => network?.id);
- }
- case 'openstack': {
- return Object.keys(vm?.addresses ?? {});
- }
- case 'ovirt': {
- const vmNicProfiles = vm.nics?.map((nic) =>
- nicProfiles.find((nicProfile) => nicProfile?.id === nic?.profile),
- );
- const networkIds = vmNicProfiles?.map((nicProfile) => nicProfile?.network);
- return networkIds;
- }
- case 'openshift': {
- return vm?.object?.spec?.template?.spec?.networks?.map((network) =>
- network?.pod ? POD_NETWORK : network?.multus?.networkName,
- );
- }
- default:
- return [];
- }
- })
+ .flatMap((vm) => toNetworks(vm, nicProfiles))
.filter(Boolean),
),
);
};
+
+export const toNetworks = (vm: ProviderVirtualMachine, nicProfiles?: OVirtNicProfile[]) =>
+ toNetworksOrProfiles(vm).map((network) =>
+ vm.providerType === 'ovirt' && nicProfiles
+ ? nicProfiles.find((nicProfile) => nicProfile?.id === network)?.network
+ : network,
+ );
+
+export const toNetworksOrProfiles = (vm) => {
+ switch (vm.providerType) {
+ case 'vsphere': {
+ return vm?.networks?.map((network) => network?.id) ?? [];
+ }
+ case 'openstack': {
+ return Object.keys(vm?.addresses ?? {});
+ }
+ case 'ovirt': {
+ return vm?.nics?.map((nic) => nic?.profile) ?? [];
+ }
+ case 'openshift': {
+ return (
+ vm?.object?.spec?.template?.spec?.networks?.map((network) =>
+ network?.pod ? POD_NETWORK : network?.multus?.networkName,
+ ) ?? []
+ );
+ }
+ default:
+ return [];
+ }
+};
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/hasMultipleNicsOnTheSameNetwork.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/hasMultipleNicsOnTheSameNetwork.ts
new file mode 100644
index 000000000..61534ed8d
--- /dev/null
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/hasMultipleNicsOnTheSameNetwork.ts
@@ -0,0 +1,22 @@
+import { OVirtNicProfile } from '@kubev2v/types';
+
+import { VmData } from '../../details';
+
+import { toNetworks } from './getNetworksUsedBySelectedVMs';
+
+/**
+ * Special case of multiplePodNetworkMappings condition.
+ * If multiple NICs are on the same (source) network then resulting mapping may be invalid.
+ * Note for oVirt: if NIC profiles are missing then the validation is based only on NIC profile IDs.
+ * However such check is incomplete: it may happen that different profiles resolve to the same network.
+ * @link https://github.com/kubev2v/forklift/blob/3673837fdedd0ab92df11bce00c31f116abbc126/pkg/controller/plan/validation.go#L363
+ */
+export const hasMultipleNicsOnTheSameNetwork = (
+ selectedVms: VmData[],
+ nicProfiles?: OVirtNicProfile[],
+): boolean =>
+ selectedVms
+ .map(({ vm }) => toNetworks(vm, nicProfiles))
+ // filter out invalid networks
+ .map((networks) => networks.filter(Boolean))
+ .some((networks) => networks.length !== new Set(networks).size);
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/hasMultiplePodNetworkMappings.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/hasMultiplePodNetworkMappings.ts
new file mode 100644
index 000000000..77d71ff68
--- /dev/null
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/hasMultiplePodNetworkMappings.ts
@@ -0,0 +1,27 @@
+import { OVirtNicProfile } from '@kubev2v/types';
+
+import { VmData } from '../../details';
+import { Mapping } from '../types';
+
+import { POD_NETWORK } from './actions';
+import { toNetworks } from './getNetworksUsedBySelectedVMs';
+
+/**
+ * Equivalent of multiplePodNetworkMappings condition.
+ * @link https://github.com/kubev2v/forklift/blob/3673837fdedd0ab92df11bce00c31f116abbc126/pkg/controller/plan/validation.go#L363
+ */
+export const hasMultiplePodNetworkMappings = (
+ mappings: Mapping[],
+ selectedVMs: VmData[],
+ sourceNetworkLabelToId: { [label: string]: string },
+ nicProfiles?: OVirtNicProfile[],
+) => {
+ const netIdsMappedToPodNet = new Set(
+ mappings
+ ?.filter(({ destination }) => destination === POD_NETWORK)
+ ?.map(({ source }) => sourceNetworkLabelToId[source]) ?? [],
+ );
+ return selectedVMs
+ .map(({ vm }) => toNetworks(vm, nicProfiles))
+ .some((networks) => networks.filter((id) => netIdsMappedToPodNet.has(id)).length >= 2);
+};
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/hasNicWithEmptyProfile.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/hasNicWithEmptyProfile.ts
new file mode 100644
index 000000000..7fb451c42
--- /dev/null
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/hasNicWithEmptyProfile.ts
@@ -0,0 +1,33 @@
+import { OVirtVM, V1beta1Provider } from '@kubev2v/types';
+
+import { VmData } from '../../details';
+
+/**
+ * (oVirt only) Special case of unmapped networks. If the profile is missing then NIC cannot be linked with any oVirt network.
+ * This prevents mapping from oVirt network to the target network.
+ * @example
+ * {
+ "id": "4619ac8a-165b-4ad2-90e4-89b356f423be",
+ "name": "nic3",
+ "interface": "virtio",
+ "plugged": true,
+ "ipAddress": null,
+ "profile": "",
+ "mac": "00:1a:4a:16:01:3f"
+ },
+ @link https://github.com/kubev2v/forklift/blob/3673837fdedd0ab92df11bce00c31f116abbc126/pkg/controller/plan/validation.go#L331
+ @link https://github.com/kubev2v/forklift/blob/3673837fdedd0ab92df11bce00c31f116abbc126/pkg/controller/plan/adapter/ovirt/validator.go#L50
+ */
+export const hasNicWithEmptyProfile = (
+ sourceProvider: V1beta1Provider,
+ selectedVms: VmData[],
+): boolean => {
+ if (sourceProvider.spec?.type !== 'ovirt') {
+ return false;
+ }
+
+ return selectedVms
+ .map(({ vm }): OVirtVM => vm.providerType === 'ovirt' && vm)
+ .filter(Boolean)
+ .some(({ nics }) => nics.some(({ profile }) => !profile));
+};
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/helpers.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/helpers.ts
index 2649b7579..ec3872535 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/helpers.ts
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/helpers.ts
@@ -6,6 +6,7 @@ import { DefaultRow, ResourceFieldFactory, RowProps, withTr } from '@kubev2v/com
import {
IoK8sApimachineryPkgApisMetaV1ObjectMeta,
OpenShiftNamespace,
+ OVirtNicProfile,
ProviderType,
V1beta1Plan,
V1beta1Provider,
@@ -26,13 +27,20 @@ import { VSphereVirtualMachinesCells } from '../../details/tabs/VirtualMachines/
import {
CreateVmMigrationPageState,
Mapping,
+ MappingSource,
+ MULTIPLE_NICS_MAPPED_TO_POD_NETWORKING,
NETWORK_MAPPING_REGENERATED,
NetworkAlerts,
+ OVIRT_NICS_WITH_EMPTY_PROFILE,
STORAGE_MAPPING_REGENERATED,
StorageAlerts,
+ UNMAPPED_NETWORKS,
+ UNMAPPED_STORAGES,
} from '../types';
+import { CreateVmMigration } from './actions';
import { calculateNetworks, calculateStorages } from './calculateMappings';
+import { hasMultiplePodNetworkMappings } from './hasMultiplePodNetworkMappings';
export const validateUniqueName = (name: string, existingNames: string[]) =>
existingNames.every((existingName) => existingName !== name);
@@ -133,9 +141,9 @@ export const recalculateStorages = (draft) => {
...draft.calculatedPerNamespace,
...calculateStorages(draft),
};
-
+ executeStorageMappingValidation(draft);
if (
- storageMappings?.length &&
+ storageMappings &&
!areMappingsEqual(storageMappings, draft.calculatedPerNamespace.storageMappings)
) {
addIfMissing(STORAGE_MAPPING_REGENERATED, draft.alerts.storageMappings.warnings);
@@ -148,6 +156,7 @@ export const recalculateNetworks = (draft) => {
...draft.calculatedPerNamespace,
...calculateNetworks(draft),
};
+ executeNetworkMappingValidation(draft);
if (
networkMappings &&
!areMappingsEqual(networkMappings, draft.calculatedPerNamespace.networkMappings)
@@ -221,6 +230,15 @@ export const addIfMissing = (key: T, keys: T[]) => {
keys.push(key);
};
+export const removeIfPresent = (key: T, keys: T[]) => {
+ console.warn('removeIfPresent', key, keys);
+ const index = keys.findIndex((k) => k === key);
+ if (index === -1) {
+ return;
+ }
+ keys.splice(index, 1);
+};
+
export const alreadyInUseBySelectedVms = ({
namespace,
sourceProvider,
@@ -235,3 +253,91 @@ export const alreadyInUseBySelectedVms = ({
sourceProvider.spec?.url === targetProvider?.spec?.url &&
sourceProvider.spec?.type === 'openshift' &&
namespacesUsedBySelectedVms.some((name) => name === namespace);
+
+export const generateUniqueName = (
+ startName: string,
+ baseName: string,
+ existingMaps: { metadata?: IoK8sApimachineryPkgApisMetaV1ObjectMeta }[],
+) => {
+ const names = existingMaps.map((n) => n.metadata?.name).filter(Boolean);
+ let currentName: string = startName;
+ while (!validateUniqueName(currentName, names)) {
+ currentName = generateName(baseName);
+ }
+ return currentName;
+};
+
+export const validateNetworkMapping = ({
+ sources,
+ errors,
+ mappings,
+ selectedVms,
+ sourceNetworkLabelToId,
+ nicProfiles,
+}: {
+ sources: MappingSource[];
+ errors: NetworkAlerts[];
+ mappings: Mapping[];
+ selectedVms: VmData[];
+ sourceNetworkLabelToId: { [label: string]: string };
+ nicProfiles?: OVirtNicProfile[];
+}): [boolean, NetworkAlerts][] => [
+ [sources.some((src) => src.usedBySelectedVms && !src.isMapped), UNMAPPED_NETWORKS],
+ [errors.includes(OVIRT_NICS_WITH_EMPTY_PROFILE), OVIRT_NICS_WITH_EMPTY_PROFILE],
+ [
+ hasMultiplePodNetworkMappings(mappings, selectedVms, sourceNetworkLabelToId, nicProfiles),
+ MULTIPLE_NICS_MAPPED_TO_POD_NETWORKING,
+ ],
+];
+
+export const executeNetworkMappingValidation = (draft: Draft) => {
+ const {
+ calculatedPerNamespace: cpn,
+ alerts: {
+ networkMappings: { errors },
+ },
+ receivedAsParams: { selectedVms },
+ calculatedOnce: { sourceNetworkLabelToId },
+ existingResources: { nicProfiles },
+ validation,
+ } = draft;
+ validation.networkMappings = validateNetworkMapping({
+ errors,
+ mappings: cpn.networkMappings,
+ selectedVms,
+ sourceNetworkLabelToId,
+ sources: cpn.sourceNetworks,
+ nicProfiles,
+ }).reduce((validation, [hasFailed, alert]) => {
+ hasFailed ? addIfMissing(alert, errors) : removeIfPresent(alert, errors);
+ return hasFailed ? 'error' : validation;
+ }, 'default');
+};
+
+export const validateStorageMapping = ({
+ sources,
+}: {
+ sources: MappingSource[];
+}): [boolean, StorageAlerts][] => [
+ [sources.some((src) => src.usedBySelectedVms && !src.isMapped), UNMAPPED_STORAGES],
+];
+
+export const executeStorageMappingValidation = (draft: Draft) => {
+ const {
+ calculatedPerNamespace: cpn,
+ alerts: {
+ storageMappings: { errors },
+ },
+ validation,
+ } = draft;
+ validation.storageMappings = validateStorageMapping({ sources: cpn.sourceStorages }).reduce(
+ (validation, [hasFailed, alert]) => {
+ hasFailed ? addIfMissing(alert, errors) : removeIfPresent(alert, errors);
+ return hasFailed ? 'error' : validation;
+ },
+ 'default',
+ );
+};
+
+export const isDone = (initialLoading: { [key in CreateVmMigration]?: boolean }) =>
+ Object.values(initialLoading).every((value) => value);
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/reducer.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/reducer.ts
index 7689a0a8c..eb5dbb2c8 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/reducer.ts
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/reducer.ts
@@ -5,8 +5,11 @@ import { getIsTarget } from '../../../utils';
import {
CreateVmMigrationPageState,
Mapping,
+ MULTIPLE_NICS_ON_THE_SAME_NETWORK,
NET_MAP_NAME_REGENERATED,
NetworkAlerts,
+ STORAGE_MAP_NAME_REGENERATED,
+ StorageAlerts,
} from '../types';
import {
@@ -28,9 +31,10 @@ import {
PlanError,
PlanExistingNetMaps,
PlanExistingPlans,
+ PlanExistingStorageMaps,
PlanMapping,
PlanName,
- PlanNickProfiles,
+ PlanNicProfiles,
PlanTargetNamespace,
PlanTargetProvider,
POD_NETWORK,
@@ -47,6 +51,7 @@ import {
SET_DISKS,
SET_EXISTING_NET_MAPS,
SET_EXISTING_PLANS,
+ SET_EXISTING_STORAGE_MAPS,
SET_NAME,
SET_NICK_PROFILES,
SET_TARGET_NAMESPACE,
@@ -56,17 +61,19 @@ import {
import { addMapping, deleteMapping, replaceMapping } from './changeMapping';
import { getNetworksUsedBySelectedVms } from './getNetworksUsedBySelectedVMs';
import { getStoragesUsedBySelectedVms } from './getStoragesUsedBySelectedVMs';
+import { hasMultipleNicsOnTheSameNetwork } from './hasMultipleNicsOnTheSameNetwork';
import {
addIfMissing,
alreadyInUseBySelectedVms,
- generateName,
+ executeNetworkMappingValidation,
+ executeStorageMappingValidation,
+ generateUniqueName,
recalculateNetworks,
recalculateStorages,
setTargetNamespace,
setTargetProvider,
validatePlanName,
validateTargetNamespace,
- validateUniqueName,
} from './helpers';
import { mapSourceNetworksToLabels, mapSourceStoragesToLabels } from './mapSourceToLabels';
@@ -77,21 +84,14 @@ const handlers: {
) => CreateVmMigrationPageState | void;
} = {
[SET_NAME](draft, { payload: { name } }: PageAction) {
- if (draft.flow.editingDone) {
- return;
- }
draft.underConstruction.plan.metadata.name = name;
draft.validation.planName = validatePlanName(name, draft.existingResources.plans);
- return draft;
},
[SET_TARGET_NAMESPACE](
draft,
{ payload: { targetNamespace } }: PageAction,
) {
- if (!draft.flow.editingDone) {
- setTargetNamespace(draft, targetNamespace);
- }
- return draft;
+ setTargetNamespace(draft, targetNamespace);
},
[SET_TARGET_PROVIDER](
draft,
@@ -100,30 +100,21 @@ const handlers: {
const {
underConstruction: { plan },
existingResources,
- flow: { editingDone },
} = draft;
// avoid side effects if no real change
- if (!editingDone && plan.spec.provider?.destination?.name !== targetProviderName) {
+ if (plan.spec.provider?.destination?.name !== targetProviderName) {
setTargetProvider(draft, targetProviderName, existingResources.providers);
}
- return draft;
},
[SET_AVAILABLE_PROVIDERS](
draft,
- {
- payload: { availableProviders, loading },
- }: PageAction,
+ { payload: { availableProviders } }: PageAction,
) {
const {
- flow,
existingResources,
underConstruction: { plan },
workArea,
} = draft;
- if (loading || flow.editingDone) {
- return draft;
- }
- console.warn(SET_AVAILABLE_PROVIDERS, availableProviders);
existingResources.providers = availableProviders;
const oldTarget = workArea.targetProvider;
const resolvedDestination = availableProviders
@@ -142,24 +133,19 @@ const handlers: {
},
[SET_EXISTING_PLANS](
draft,
- { payload: { existingPlans, loading } }: PageAction,
+ { payload: { existingPlans } }: PageAction,
) {
// triggered from useEffect on any data change
- if (loading || draft.flow.editingDone) {
- return draft;
- }
- console.warn(SET_EXISTING_PLANS, existingPlans);
draft.existingResources.plans = existingPlans;
draft.validation.planName = validatePlanName(
draft.underConstruction.plan.metadata.name,
existingPlans,
);
- return draft;
},
[SET_AVAILABLE_TARGET_NAMESPACES](
draft,
{
- payload: { availableTargetNamespaces, loading },
+ payload: { availableTargetNamespaces },
}: PageAction,
) {
// triggered from useEffect on any data change
@@ -172,10 +158,9 @@ const handlers: {
flow: { editingDone },
receivedAsParams: { sourceProvider },
} = draft;
- if (loading || editingDone) {
+ if (editingDone) {
return;
}
- console.warn(SET_AVAILABLE_TARGET_NAMESPACES, availableTargetNamespaces);
existingResources.targetNamespaces = availableTargetNamespaces;
const alreadyInUse = (namespace: string) =>
@@ -214,31 +199,20 @@ const handlers: {
[SET_AVAILABLE_TARGET_NETWORKS](
draft,
{
- payload: { availableTargetNetworks, loading },
+ payload: { availableTargetNetworks },
}: PageAction,
) {
// triggered from useEffect on any data change
- if (loading || draft.flow.editingDone) {
- return draft;
- }
- console.warn(SET_AVAILABLE_TARGET_NETWORKS, availableTargetNetworks);
draft.existingResources.targetNetworks = availableTargetNetworks;
- draft.flow.targetNetworksLoaded = true;
-
recalculateNetworks(draft);
},
[SET_AVAILABLE_SOURCE_NETWORKS](
draft,
{
- payload: { availableSourceNetworks, loading },
+ payload: { availableSourceNetworks },
}: PageAction,
) {
- if (loading || draft.flow.editingDone) {
- return draft;
- }
- console.warn(SET_AVAILABLE_SOURCE_NETWORKS, availableSourceNetworks);
draft.existingResources.sourceNetworks = availableSourceNetworks;
- draft.flow.sourceNetworkLoaded = true;
draft.calculatedOnce.sourceNetworkLabelToId =
mapSourceNetworksToLabels(availableSourceNetworks);
@@ -248,32 +222,22 @@ const handlers: {
[SET_AVAILABLE_TARGET_STORAGES](
draft,
{
- payload: { availableTargetStorages, loading },
+ payload: { availableTargetStorages },
}: PageAction,
) {
// triggered from useEffect on any data change
- if (loading || draft.flow.editingDone) {
- return draft;
- }
- console.warn(SET_AVAILABLE_TARGET_STORAGES, availableTargetStorages);
draft.existingResources.targetStorages = availableTargetStorages;
- draft.flow.targetStoragesLoaded = true;
recalculateStorages(draft);
},
[SET_AVAILABLE_SOURCE_STORAGES](
draft,
{
- payload: { availableSourceStorages, loading },
+ payload: { availableSourceStorages },
}: PageAction,
) {
// triggered from useEffect on any data change
- if (loading || draft.flow.editingDone) {
- return draft;
- }
- console.warn(SET_AVAILABLE_SOURCE_STORAGES, availableSourceStorages);
draft.existingResources.sourceStorages = availableSourceStorages;
- draft.flow.sourceStoragesLoaded = true;
draft.calculatedOnce.sourceStorageLabelToId =
mapSourceStoragesToLabels(availableSourceStorages);
@@ -282,41 +246,33 @@ const handlers: {
},
[SET_NICK_PROFILES](
draft,
- { payload: { nickProfiles, loading } }: PageAction,
+ { payload: { nicProfiles } }: PageAction,
) {
const {
existingResources,
calculatedOnce,
receivedAsParams: { selectedVms },
- flow: { editingDone },
+ alerts,
} = draft;
- if (loading || editingDone) {
- return;
- }
- console.warn(SET_NICK_PROFILES, nickProfiles);
- existingResources.nickProfiles = nickProfiles;
- draft.flow.nicProfilesLoaded = true;
+ existingResources.nicProfiles = nicProfiles;
calculatedOnce.networkIdsUsedBySelectedVms = getNetworksUsedBySelectedVms(
selectedVms,
- nickProfiles,
+ nicProfiles,
);
+ if (hasMultipleNicsOnTheSameNetwork(selectedVms, nicProfiles)) {
+ addIfMissing(MULTIPLE_NICS_ON_THE_SAME_NETWORK, alerts.networkMappings.warnings);
+ }
recalculateNetworks(draft);
},
- [SET_DISKS](draft, { payload: { disks, loading } }: PageAction) {
+ [SET_DISKS](draft, { payload: { disks } }: PageAction) {
// triggered from useEffect on any data change
const {
existingResources,
calculatedOnce,
receivedAsParams: { selectedVms },
- flow: { editingDone },
} = draft;
- if (loading || editingDone) {
- return;
- }
- console.warn(SET_DISKS, disks);
existingResources.disks = disks;
- draft.flow.disksLoaded = true;
calculatedOnce.storageIdsUsedBySelectedVms = getStoragesUsedBySelectedVms(selectedVms, disks);
recalculateStorages(draft);
@@ -326,26 +282,46 @@ const handlers: {
existingResources,
underConstruction: { netMap },
receivedAsParams: { sourceProvider },
- flow: { editingDone },
alerts,
},
- { payload: { existingNetMaps, loading } }: PageAction,
+ { payload: { existingNetMaps } }: PageAction,
) {
// triggered from useEffect on any data change
- if (loading || editingDone) {
- return;
- }
- console.warn(SET_EXISTING_NET_MAPS, existingNetMaps);
existingResources.netMaps = existingNetMaps;
const oldName = netMap.metadata.name;
- const names = existingNetMaps.map((n) => n.metadata?.name).filter(Boolean);
- while (!validateUniqueName(netMap.metadata.name, names)) {
- netMap.metadata.name = generateName(sourceProvider.metadata.name);
- }
+
+ netMap.metadata.name = generateUniqueName(
+ oldName,
+ sourceProvider.metadata.name,
+ existingNetMaps,
+ );
if (oldName !== netMap.metadata.name) {
addIfMissing(NET_MAP_NAME_REGENERATED, alerts.networkMappings.warnings);
}
},
+ [SET_EXISTING_STORAGE_MAPS](
+ {
+ existingResources,
+ underConstruction: { storageMap },
+ receivedAsParams: { sourceProvider },
+ alerts,
+ },
+ { payload: { existingStorageMaps } }: PageAction,
+ ) {
+ // triggered from useEffect on any data change
+ existingResources.storageMaps = existingStorageMaps;
+ const oldName = storageMap.metadata.name;
+
+ storageMap.metadata.name = generateUniqueName(
+ oldName,
+ sourceProvider.metadata.name,
+ existingStorageMaps,
+ );
+
+ if (oldName !== storageMap.metadata.name) {
+ addIfMissing(STORAGE_MAP_NAME_REGENERATED, alerts.storageMappings.warnings);
+ }
+ },
[START_CREATE]({
flow,
underConstruction: { plan, netMap, storageMap },
@@ -353,7 +329,6 @@ const handlers: {
calculatedPerNamespace: { networkMappings, storageMappings },
}) {
// triggered by the user
- console.warn(START_CREATE);
flow.editingDone = true;
netMap.spec.map = networkMappings.map(({ source, destination }) => ({
source: {
@@ -373,31 +348,28 @@ const handlers: {
},
[SET_API_ERROR]({ flow }, { payload: { error } }: PageAction) {
// triggered by the API callback (on failure)
- console.warn(SET_API_ERROR);
flow.apiError = error;
},
- [ADD_NETWORK_MAPPING]({ calculatedPerNamespace: cpn }) {
+ [ADD_NETWORK_MAPPING](draft) {
+ const { calculatedPerNamespace: cpn } = draft;
const { sources, mappings } = addMapping(
cpn.sourceNetworks,
cpn.targetNetworks,
cpn.networkMappings,
);
// triggered by the user
- console.warn(ADD_NETWORK_MAPPING, sources, mappings);
if (sources && mappings) {
cpn.sourceNetworks = sources;
cpn.networkMappings = mappings;
}
+ executeNetworkMappingValidation(draft);
},
- [DELETE_NETWORK_MAPPING](
- { calculatedPerNamespace: cpn },
- { payload: { source } }: PageAction,
- ) {
+ [DELETE_NETWORK_MAPPING](draft, { payload: { source } }: PageAction) {
+ const { calculatedPerNamespace: cpn } = draft;
// triggered by the user
const currentSource = cpn.sourceNetworks.find(
({ label, isMapped }) => label === source && isMapped,
);
- console.warn(DELETE_NETWORK_MAPPING, source, currentSource);
if (currentSource) {
cpn.sourceNetworks = cpn.sourceNetworks.map((m) => ({
...m,
@@ -407,13 +379,14 @@ const handlers: {
({ source }) => source !== currentSource.label,
);
}
+ executeNetworkMappingValidation(draft);
},
[REPLACE_NETWORK_MAPPING](
- { calculatedPerNamespace: cpn },
+ draft,
{ payload: { current, next } }: PageAction,
) {
+ const { calculatedPerNamespace: cpn } = draft;
// triggered by the user
- console.warn(REPLACE_NETWORK_MAPPING, current, next);
const { sources, mappings } = replaceMapping(
cpn.sourceNetworks,
current,
@@ -427,39 +400,39 @@ const handlers: {
if (mappings) {
cpn.networkMappings = mappings;
}
+ executeNetworkMappingValidation(draft);
},
- [ADD_STORAGE_MAPPING]({ calculatedPerNamespace: cpn }) {
+ [ADD_STORAGE_MAPPING](draft) {
+ const { calculatedPerNamespace: cpn } = draft;
const { sources, mappings } = addMapping(
cpn.sourceStorages,
cpn.targetStorages,
cpn.storageMappings,
);
// triggered by the user
- console.warn(ADD_STORAGE_MAPPING, sources, mappings);
if (sources && mappings) {
cpn.sourceStorages = sources;
cpn.storageMappings = mappings;
}
+ executeStorageMappingValidation(draft);
},
- [DELETE_STORAGE_MAPPING](
- { calculatedPerNamespace: cpn },
- { payload: { source } }: PageAction,
- ) {
+ [DELETE_STORAGE_MAPPING](draft, { payload: { source } }: PageAction) {
+ const { calculatedPerNamespace: cpn } = draft;
// triggered by the user
const { sources, mappings } = deleteMapping(cpn.sourceStorages, source, cpn.storageMappings);
- console.warn(DELETE_STORAGE_MAPPING, source, sources);
if (sources && mappings) {
cpn.sourceStorages = sources;
cpn.storageMappings = mappings;
}
+ executeStorageMappingValidation(draft);
},
[REPLACE_STORAGE_MAPPING](
- { calculatedPerNamespace: cpn },
+ draft,
{ payload: { current, next } }: PageAction,
) {
+ const { calculatedPerNamespace: cpn } = draft;
// triggered by the user
- console.warn(REPLACE_STORAGE_MAPPING, current, next);
const { sources, mappings } = replaceMapping(
cpn.sourceStorages,
current,
@@ -473,9 +446,10 @@ const handlers: {
if (mappings) {
cpn.storageMappings = mappings;
}
+ executeStorageMappingValidation(draft);
},
[REMOVE_ALERT](
- { alerts: { networkMappings, storageMappings, general } },
+ { alerts: { networkMappings, storageMappings } },
{ payload: { alertKey } }: PageAction,
) {
[
@@ -483,8 +457,6 @@ const handlers: {
networkMappings.warnings,
storageMappings.errors,
storageMappings.warnings,
- general.errors,
- general.warnings,
].forEach((alerts) => {
const index = alerts.findIndex((key) => key === alertKey);
if (index > -1) {
@@ -494,9 +466,33 @@ const handlers: {
},
};
+const actionsAllowedAfterEditingIsDone: CreateVmMigration[] = [SET_API_ERROR];
+
+// action with data required on page start
+// skip SET_EXISTING_* actions as they only add extra validation
+const actionsTrackedForInitialLoading: CreateVmMigration[] = [
+ SET_AVAILABLE_PROVIDERS,
+ SET_AVAILABLE_SOURCE_NETWORKS,
+ SET_AVAILABLE_SOURCE_STORAGES,
+ SET_AVAILABLE_TARGET_NAMESPACES,
+ SET_AVAILABLE_TARGET_NETWORKS,
+ SET_AVAILABLE_TARGET_STORAGES,
+ SET_DISKS,
+ SET_NICK_PROFILES,
+];
+
export const reducer = (
draft: Draft,
action: PageAction,
) => {
- return handlers?.[action?.type]?.(draft, action) ?? draft;
+ console.warn(`reducer: ${action?.type}`, action?.payload);
+ if (
+ actionsTrackedForInitialLoading.includes(action?.type) &&
+ !draft.flow.initialLoading[action.type]
+ ) {
+ draft.flow.initialLoading[action.type] = true;
+ }
+ return draft.flow.editingDone && !actionsAllowedAfterEditingIsDone.includes[action?.type]
+ ? draft
+ : handlers?.[action?.type]?.(draft, action) ?? draft;
};
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/types.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/types.ts
index 4d7070dde..8580894cb 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/types.ts
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/types.ts
@@ -19,6 +19,8 @@ import { InventoryStorage } from '../../hooks/useStorages';
import { Validation } from '../../utils';
import { VmData } from '../details';
+import { CreateVmMigration } from './reducer/actions';
+
export interface CreateVmMigrationPageState {
underConstruction: {
plan: V1beta1Plan;
@@ -30,12 +32,10 @@ export interface CreateVmMigrationPageState {
planName: Validation;
targetNamespace: Validation;
targetProvider: Validation;
+ networkMappings: Validation;
+ storageMappings: Validation;
};
alerts: {
- general: {
- errors: GeneralAlerts[];
- warnings: GeneralAlerts[];
- };
networkMappings: {
errors: NetworkAlerts[];
warnings: NetworkAlerts[];
@@ -54,9 +54,10 @@ export interface CreateVmMigrationPageState {
sourceNetworks: InventoryNetwork[];
targetStorages: OpenShiftStorageClass[];
sourceStorages: InventoryStorage[];
- nickProfiles: OVirtNicProfile[];
+ nicProfiles: OVirtNicProfile[];
disks: (OVirtDisk | OpenstackVolume)[];
netMaps: V1beta1NetworkMap[];
+ storageMaps: V1beta1StorageMap[];
};
calculatedOnce: {
// calculated on start (exception:for ovirt/openstack we need to fetch disks)
@@ -94,12 +95,7 @@ export interface CreateVmMigrationPageState {
flow: {
editingDone: boolean;
apiError?: Error;
- disksLoaded: boolean;
- nicProfilesLoaded: boolean;
- targetNetworksLoaded: boolean;
- sourceNetworkLoaded: boolean;
- targetStoragesLoaded: boolean;
- sourceStoragesLoaded: boolean;
+ initialLoading: { [keys in CreateVmMigration]?: boolean };
};
}
export interface MappingSource {
@@ -116,9 +112,25 @@ export interface Mapping {
}
export const NET_MAP_NAME_REGENERATED = 'NET_MAP_NAME_REGENERATED';
-export const NEXT_VALID_PROVIDER_SELECTED = 'NEXT_VALID_PROVIDER_SELECTED';
export const NETWORK_MAPPING_REGENERATED = 'NETWORK_MAPPING_REGENERATED';
+export const OVIRT_NICS_WITH_EMPTY_PROFILE = 'OVIRT_NICS_WITH_EMPTY_PROFILE';
+export const MULTIPLE_NICS_ON_THE_SAME_NETWORK = 'MULTIPLE_NICS_ON_THE_SAME_NETWORK';
+export const UNMAPPED_NETWORKS = 'UNMAPPED_NETWORKS';
+export const MULTIPLE_NICS_MAPPED_TO_POD_NETWORKING = 'MULTIPLE_NICS_MAPPED_TO_POD_NETWORKING';
+
export const STORAGE_MAPPING_REGENERATED = 'STORAGE_MAPPING_REGENERATED';
-export type NetworkAlerts = typeof NET_MAP_NAME_REGENERATED | typeof NETWORK_MAPPING_REGENERATED;
-export type StorageAlerts = typeof STORAGE_MAPPING_REGENERATED;
-export type GeneralAlerts = typeof NEXT_VALID_PROVIDER_SELECTED;
+export const STORAGE_MAP_NAME_REGENERATED = 'STORAGE_MAP_NAME_REGENERATED';
+export const UNMAPPED_STORAGES = 'UNMAPPED_STORAGES';
+
+export type NetworkAlerts =
+ | typeof NET_MAP_NAME_REGENERATED
+ | typeof NETWORK_MAPPING_REGENERATED
+ | typeof UNMAPPED_NETWORKS
+ | typeof OVIRT_NICS_WITH_EMPTY_PROFILE
+ | typeof MULTIPLE_NICS_ON_THE_SAME_NETWORK
+ | typeof MULTIPLE_NICS_MAPPED_TO_POD_NETWORKING;
+
+export type StorageAlerts =
+ | typeof STORAGE_MAPPING_REGENERATED
+ | typeof STORAGE_MAP_NAME_REGENERATED
+ | typeof UNMAPPED_STORAGES;
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useFetchEffects.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useFetchEffects.ts
index 3b3ddf4ad..a9d63f81d 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useFetchEffects.ts
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useFetchEffects.ts
@@ -25,6 +25,7 @@ import { getResourceUrl } from '../../utils';
import {
CreateVmMigration,
PageAction,
+ setAPiError,
setAvailableProviders,
setAvailableSourceNetworks,
setAvailableSourceStorages,
@@ -73,8 +74,20 @@ export const useFetchEffects = (): [
);
const {
workArea: { targetProvider },
+ flow: { editingDone },
} = state;
+ const dispatchWithFallback = (
+ action: PageAction,
+ loading: boolean,
+ error: Error,
+ ): void => {
+ if (loading) {
+ return;
+ }
+ error ? dispatch(setAPiError(error)) : dispatch(action);
+ };
+
const [providers, providersLoaded, providerError] = useK8sWatchResource({
groupVersionKind: ProviderModelGroupVersionKind,
namespaced: true,
@@ -82,7 +95,9 @@ export const useFetchEffects = (): [
namespace,
});
useEffect(
- () => dispatch(setAvailableProviders(providers, providersLoaded, providerError)),
+ () =>
+ !editingDone &&
+ dispatchWithFallback(setAvailableProviders(providers), !providersLoaded, providerError),
[providers],
);
@@ -93,7 +108,7 @@ export const useFetchEffects = (): [
namespace,
});
useEffect(
- () => dispatch(setExistingPlans(plans, plansLoaded, plansError)),
+ () => !editingDone && dispatchWithFallback(setExistingPlans(plans), !plansLoaded, plansError),
[plans, plansLoaded, plansError],
);
@@ -104,7 +119,9 @@ export const useFetchEffects = (): [
namespace,
});
useEffect(
- () => dispatch(setExistingNetMaps(netMaps, netMapsLoaded, netMapsError)),
+ () =>
+ !editingDone &&
+ dispatchWithFallback(setExistingNetMaps(netMaps), !netMapsLoaded, netMapsError),
[netMaps, netMapsLoaded, netMapsError],
);
@@ -115,13 +132,18 @@ export const useFetchEffects = (): [
namespace,
});
useEffect(
- () => dispatch(setExistingStorageMaps(stMaps, stMapsLoaded, stMapsError)),
+ () =>
+ !editingDone &&
+ dispatchWithFallback(setExistingStorageMaps(stMaps), !stMapsLoaded, stMapsError),
[stMaps, stMapsLoaded, stMapsError],
);
const [namespaces, nsLoading, nsError] = useNamespaces(targetProvider);
useEffect(
- () => dispatch(setAvailableTargetNamespaces(namespaces, nsLoading, nsError)),
+ () =>
+ targetProvider &&
+ !editingDone &&
+ dispatchWithFallback(setAvailableTargetNamespaces(namespaces), nsLoading, nsError),
[namespaces, nsLoading, nsError, targetProvider],
);
@@ -129,8 +151,12 @@ export const useFetchEffects = (): [
useOpenShiftNetworks(targetProvider);
useEffect(
() =>
- dispatch(
- setAvailableTargetNetworks(targetNetworks, targetNetworksLoading, targetNetworksError),
+ targetProvider &&
+ !editingDone &&
+ dispatchWithFallback(
+ setAvailableTargetNetworks(targetNetworks),
+ targetNetworksLoading,
+ targetNetworksError,
),
[targetNetworks, targetNetworksLoading, targetNetworksError, targetProvider],
);
@@ -139,8 +165,11 @@ export const useFetchEffects = (): [
useSourceStorages(sourceProvider);
useEffect(
() =>
- dispatch(
- setAvailableSourceStorages(sourceStorages, sourceStoragesLoading, sourceStoragesError),
+ !editingDone &&
+ dispatchWithFallback(
+ setAvailableSourceStorages(sourceStorages),
+ sourceStoragesLoading,
+ sourceStoragesError,
),
[sourceStorages, sourceStoragesLoading, sourceStoragesError],
);
@@ -149,8 +178,12 @@ export const useFetchEffects = (): [
useOpenShiftStorages(targetProvider);
useEffect(
() =>
- dispatch(
- setAvailableTargetStorages(targetStorages, targetStoragesLoading, targetStoragesError),
+ targetProvider &&
+ !editingDone &&
+ dispatchWithFallback(
+ setAvailableTargetStorages(targetStorages),
+ targetStoragesLoading,
+ targetStoragesError,
),
[targetStorages, targetStoragesLoading, targetStoragesError, targetProvider],
);
@@ -159,20 +192,25 @@ export const useFetchEffects = (): [
useSourceNetworks(sourceProvider);
useEffect(
() =>
- dispatch(
- setAvailableSourceNetworks(sourceNetworks, sourceNetworksLoading, sourceNetworksError),
+ !editingDone &&
+ dispatchWithFallback(
+ setAvailableSourceNetworks(sourceNetworks),
+ sourceNetworksLoading,
+ sourceNetworksError,
),
[sourceNetworks, sourceNetworksLoading, sourceNetworksError],
);
const [nicProfiles, nicProfilesLoading, nicProfilesError] = useNicProfiles(sourceProvider);
useEffect(
- () => dispatch(setNicProfiles(nicProfiles, nicProfilesLoading, nicProfilesError)),
+ () =>
+ !editingDone &&
+ dispatchWithFallback(setNicProfiles(nicProfiles), nicProfilesLoading, nicProfilesError),
[nicProfiles, nicProfilesLoading, nicProfilesError],
);
const [disks, disksLoading, disksError] = useDisks(sourceProvider);
useEffect(
- () => dispatch(setDisks(disks, disksLoading, disksError)),
+ () => !editingDone && dispatchWithFallback(setDisks(disks), disksLoading, disksError),
[disks, disksLoading, disksError],
);