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], );