Просмотр исходного кода

Add VMWare plugin target support

This includes updates to the Options listing, Network and Storage
mappings.
Sergiu Miclea 5 лет назад
Родитель
Сommit
3c870808ce
31 измененных файлов с 647 добавлено и 231 удалено
  1. 7 0
      config.ts
  2. 1 0
      src/@types/Config.ts
  3. 27 1
      src/@types/Endpoint.ts
  4. 3 1
      src/@types/Instance.ts
  5. 26 0
      src/@types/Network.ts
  6. 36 23
      src/components/molecules/MainDetailsTable/MainDetailsTable.tsx
  7. 74 41
      src/components/organisms/EditReplica/EditReplica.tsx
  8. 4 2
      src/components/organisms/MainDetails/MainDetails.tsx
  9. 6 1
      src/components/organisms/MigrationDetailsContent/MigrationDetailsContent.tsx
  10. 3 1
      src/components/organisms/ReplicaDetailsContent/ReplicaDetailsContent.tsx
  11. 50 26
      src/components/organisms/WizardNetworks/WizardNetworks.tsx
  12. 0 1
      src/components/organisms/WizardOptions/WizardOptions.tsx
  13. 26 11
      src/components/organisms/WizardPageContent/WizardPageContent.tsx
  14. 115 27
      src/components/organisms/WizardStorage/WizardStorage.tsx
  15. 2 4
      src/components/organisms/WizardStorage/story.tsx
  16. 27 16
      src/components/organisms/WizardSummary/WizardSummary.tsx
  17. 16 3
      src/components/pages/MigrationDetailsPage/MigrationDetailsPage.tsx
  18. 14 2
      src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.tsx
  19. 23 23
      src/components/pages/WizardPage/WizardPage.tsx
  20. 34 23
      src/plugins/endpoint/default/OptionsSchemaPlugin.ts
  21. 2 0
      src/plugins/endpoint/index.ts
  22. 1 1
      src/plugins/endpoint/openstack/OptionsSchemaPlugin.ts
  23. 2 2
      src/plugins/endpoint/ovm/OptionsSchemaPlugin.ts
  24. 112 0
      src/plugins/endpoint/vmware_vsphere/OptionsSchemaPlugin.ts
  25. 2 2
      src/sources/MigrationSource.ts
  26. 1 1
      src/sources/ReplicaSource.ts
  27. 2 2
      src/sources/WizardSource.ts
  28. 2 2
      src/stores/MigrationStore.ts
  29. 22 8
      src/stores/ProviderStore.ts
  30. 1 1
      src/stores/ReplicaStore.ts
  31. 6 6
      src/stores/WizardStore.ts

+ 7 - 0
config.ts

@@ -44,6 +44,7 @@ const conf: Config = {
    * If `requiredValues` is provided, the field specified there needs to have a
    * certain value (specified in values)
    * in order to make the options API call.
+   * If `relistFields` is provided, the options call will be made if any of the relist fields are changed.
    */
   extraOptionsApiCalls: [
     {
@@ -77,6 +78,12 @@ const conf: Config = {
       types: ['destination'],
       requiredFields: ['compartment', 'availability_domain', 'vcn_compartment'],
     },
+    {
+      name: 'vmware_vsphere',
+      types: ['destination'],
+      requiredFields: ['import_datacenter'],
+      relistFields: ['import_cluster'],
+    },
   ],
 
   /*

+ 1 - 0
src/@types/Config.ts

@@ -4,6 +4,7 @@ type ExtraOption = {
   name: string,
   types: Type[],
   requiredFields: string[],
+  relistFields?: string[],
   requiredValues?: {
     field: string,
     values: string[],

+ 27 - 1
src/@types/Endpoint.ts

@@ -48,8 +48,11 @@ export type OptionValues = {
 }
 
 export type StorageBackend = {
-  id: string,
+  id: string | null,
   name: string,
+  additional_provider_properties?: {
+    supported_bus_types?: string[]
+  }
 }
 
 export type Storage = {
@@ -61,4 +64,27 @@ export type StorageMap = {
   type: 'backend' | 'disk',
   source: Disk,
   target: StorageBackend,
+  targetBusType?: string | null
+}
+
+export const EndpointUtils = {
+  getBusTypeStorageId: (storageBackends: StorageBackend[], id: string | null): { busType: string | null, id: string | null } => {
+    const idMatches = /(.*):(.*)/.exec(String(id))
+    if (!idMatches) {
+      return { busType: null, id }
+    }
+    const actualId = idMatches[1]
+    const busType = idMatches[2]
+
+    for (let i = 0; i < storageBackends.length; i += 1) {
+      if (storageBackends[i].id === actualId) {
+        if (storageBackends[i].additional_provider_properties?.supported_bus_types?.find(p => p === busType)) {
+          return { id: actualId, busType }
+        }
+        return { id: actualId, busType: null }
+      }
+    }
+
+    return { id, busType: null }
+  },
 }

+ 3 - 1
src/@types/Instance.ts

@@ -58,4 +58,6 @@ export type InstanceScript = {
   fileName: string | null,
 }
 
-export const shortenId = (id: string) => id.replace(/(^.*?)-.*-(.*$)/, '$1-...-$2')
+export const InstanceUtils = {
+  shortenId: (id: string) => id.replace(/(^.*?)-.*-(.*$)/, '$1-...-$2'),
+}

+ 26 - 0
src/@types/Network.ts

@@ -22,11 +22,37 @@ export type SecurityGroup = string | {
 export type Network = {
   name: string,
   id: string,
+  // The `security_groups` field is currently used only by OCI
   security_groups?: SecurityGroup[],
+  // The `port_keys` field is currenlty used only by VMWare
+  port_keys?: string[],
 }
 
 export type NetworkMap = {
   sourceNic: Nic,
   targetNetwork: Network | null,
   targetSecurityGroups?: SecurityGroup[] | null,
+  targetPortKey?: string | null
+}
+
+export const NetworkUtils = {
+  getPortKeyNetworkId: (networks: Network[], id: string): {portKey: string | null, id: string} => {
+    const idMatches = /(.*):(.*)/.exec(String(id))
+    if (!idMatches) {
+      return { portKey: null, id }
+    }
+    const actualId = idMatches[1]
+    const portKey = idMatches[2]
+
+    for (let i = 0; i < networks.length; i += 1) {
+      if (networks[i].id === actualId) {
+        if (networks[i].port_keys?.find(p => p === portKey)) {
+          return { id: actualId, portKey }
+        }
+        return { id: actualId, portKey: null }
+      }
+    }
+
+    return { id, portKey: null }
+  },
 }

+ 36 - 23
src/components/molecules/MainDetailsTable/MainDetailsTable.tsx

@@ -26,13 +26,14 @@ import {
   isNetworkMapSourceDest, TransferItem,
 } from '../../../@types/MainItem'
 import type { Instance, Nic, Disk } from '../../../@types/Instance'
-import type { Network } from '../../../@types/Network'
+import { Network, NetworkUtils } from '../../../@types/Network'
 
 import instanceIcon from './images/instance.svg'
 import networkIcon from './images/network.svg'
 import storageIcon from './images/storage.svg'
 import arrowIcon from './images/arrow.svg'
 import { MinionPool } from '../../../@types/MinionPool'
+import { EndpointUtils, StorageBackend } from '../../../@types/Endpoint'
 
 const GlobalStyle = createGlobalStyle`
   .ReactCollapse--collapse {
@@ -153,6 +154,7 @@ export type Props = {
   instancesDetails: Instance[],
   networks?: Network[],
   minionPools: MinionPool[]
+  storageBackends: StorageBackend[]
 }
 type State = {
   openedRows: string[],
@@ -228,31 +230,41 @@ class MainDetailsTable extends React.Component<Props, State> {
     )
   }
 
-  renderStorage(instance: Instance) {
-    const storageMapping = this.props.item && this.props.item.storage_mappings
+  renderStorage(instance: Instance, type: 'backend' | 'disk') {
+    const storageMapping = this.props.item?.storage_mappings
     const transferResult = this.getTransferResult(instance)
     const rows: React.ReactNode[] = []
+    const diskFieldName = type === 'backend' ? 'storage_backend_identifier' : 'id'
+    const mappingFieldName = type === 'backend' ? 'source' : 'disk_id'
+    const storageMappingFieldName = type === 'backend' ? 'backend_mappings' : 'disk_mappings'
+
     instance.devices.disks.forEach(disk => {
-      const sourceName = disk.id
-      const mappedDisk = storageMapping && storageMapping.disk_mappings
-        && storageMapping.disk_mappings.find(m => String(m.disk_id) === String(disk.id))
+      const sourceName = disk[diskFieldName] || ''
+      const mappedDisk = (storageMapping?.[storageMappingFieldName] as any)
+        ?.find((m: any) => String(m[mappingFieldName]) === String(disk[diskFieldName]))
       let destinationName: React.ReactNode
       let destinationKey: string
+      const defaultBusTypeInfo = EndpointUtils.getBusTypeStorageId(this.props.storageBackends, this.props.item?.storage_mappings?.default || null)
 
       if (disk.disabled) {
         destinationKey = disk.disabled.info || disk.disabled.message
         destinationName = <span style={{ color: Palette.grayscale[5] }}>{destinationKey}</span>
       } else {
-        destinationName = (
-          this.props.item && this.props.item.storage_mappings
-          && this.props.item.storage_mappings.default
-        ) || 'Default'
+        destinationName = defaultBusTypeInfo.id || 'Default'
         destinationKey = destinationName as string
       }
+      let destinationBody: string[] = []
 
       if (mappedDisk) {
-        destinationName = mappedDisk.destination
+        const busTypeInfo = EndpointUtils.getBusTypeStorageId(this.props.storageBackends, mappedDisk?.destination)
+
+        destinationName = busTypeInfo.id
         destinationKey = destinationName as string
+        if (busTypeInfo.busType) {
+          destinationBody.push(`Bus Type: ${busTypeInfo.busType}`)
+        }
+      } else if (defaultBusTypeInfo.busType) {
+        destinationBody.push(`Bus Type: ${defaultBusTypeInfo.busType}`)
       }
       const getBody = (d: Disk): string[] => {
         const body: string[] = []
@@ -271,14 +283,14 @@ class MainDetailsTable extends React.Component<Props, State> {
         return body
       }
       const sourceBody = getBody(disk)
-      let destinationBody: string[] = []
+
       if (transferResult) {
         const transferDisk = transferResult.devices.disks
           .find(d => d.storage_backend_identifier === destinationName)
         if (transferDisk) {
           destinationName = transferDisk.name || transferDisk.id
           destinationKey = destinationName as string
-          destinationBody = getBody(transferDisk)
+          destinationBody = destinationBody.concat(getBody(transferDisk))
         }
       } else if (this.props.item?.type === 'migration' && (
         this.props.item.last_execution_status === 'RUNNING'
@@ -327,18 +339,14 @@ class MainDetailsTable extends React.Component<Props, State> {
           return body
         }
         const destNetMapObj = destinationNetworkMap[nic.network_name]
-        const destinationNetworkId = isNetworkMapSecurityGroups(destNetMapObj)
-          ? destNetMapObj.id : destNetMapObj
-        const destinationNetwork = this.props.networks
-          && this.props.networks.find(n => n.id === destinationNetworkId)
-
+        const portKeyInfo = NetworkUtils.getPortKeyNetworkId(this.props.networks || [], destNetMapObj as any)
+        const destinationNetworkId = isNetworkMapSecurityGroups(destNetMapObj) ? destNetMapObj.id : portKeyInfo.id
+        const destinationNetwork = this.props.networks?.find(n => n.id === destinationNetworkId)
         const sourceBody = getBody(nic)
 
         let destinationBody: string[] = []
-        if (isNetworkMapSecurityGroups(destNetMapObj)
-          && destNetMapObj.security_groups && destNetMapObj.security_groups.length) {
-          const destSecGroupsInfo = (destinationNetwork && destinationNetwork.security_groups) || []
-
+        if (isNetworkMapSecurityGroups(destNetMapObj) && destNetMapObj.security_groups?.length) {
+          const destSecGroupsInfo = (destinationNetwork?.security_groups) || []
           const secNames = destNetMapObj.security_groups.map(s => {
             const foundSecGroupInfo = destSecGroupsInfo.find(si => (typeof si === 'string' ? si === s : si.id === s))
             return foundSecGroupInfo && typeof foundSecGroupInfo !== 'string' && foundSecGroupInfo.name ? foundSecGroupInfo.name : s
@@ -346,6 +354,10 @@ class MainDetailsTable extends React.Component<Props, State> {
           destinationBody = [`Security Groups: ${secNames.join(', ')}`]
         }
 
+        if (portKeyInfo.portKey != null) {
+          destinationBody = [`Port Key: ${portKeyInfo.portKey}`]
+        }
+
         let destinationNetworkName = destinationNetworkId
         if (destinationNetwork) {
           destinationNetworkName = destinationNetwork.name
@@ -438,7 +450,8 @@ class MainDetailsTable extends React.Component<Props, State> {
             <InstanceBody>
               {this.renderInstanceDetails(instance)}
               {this.renderNetworks(instance)}
-              {this.renderStorage(instance)}
+              {this.renderStorage(instance, 'disk')}
+              {this.renderStorage(instance, 'backend')}
             </InstanceBody>
           </InstanceInfo>
         ))}

+ 74 - 41
src/components/organisms/EditReplica/EditReplica.tsx

@@ -27,7 +27,7 @@ import StatusImage from '../../atoms/StatusImage'
 import Modal from '../../molecules/Modal'
 import Panel from '../../molecules/Panel'
 import { isOptionsPageValid } from '../WizardPageContent'
-import WizardNetworks from '../WizardNetworks'
+import WizardNetworks, { WizardNetworksChangeObject } from '../WizardNetworks'
 import WizardOptions, { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from '../WizardOptions'
 import WizardStorage from '../WizardStorage/WizardStorage'
 
@@ -35,12 +35,16 @@ import type {
   UpdateData, TransferItemDetails, MigrationItemDetails,
 } from '../../../@types/MainItem'
 import type { NavigationItem } from '../../molecules/Panel'
-import type { Endpoint, StorageBackend, StorageMap } from '../../../@types/Endpoint'
+import {
+  Endpoint, EndpointUtils, StorageBackend, StorageMap,
+} from '../../../@types/Endpoint'
 import type { Field } from '../../../@types/Field'
 import type {
-  Instance, Nic, Disk, InstanceScript,
+  Instance, InstanceScript,
 } from '../../../@types/Instance'
-import type { Network, NetworkMap, SecurityGroup } from '../../../@types/Network'
+import {
+  Network, NetworkMap, NetworkUtils, SecurityGroup,
+} from '../../../@types/Network'
 
 import { providerTypes, migrationFields } from '../../../constants'
 import configLoader from '../../../utils/Config'
@@ -106,7 +110,7 @@ type State = {
   sourceData: any,
   updateDisabled: boolean,
   selectedNetworks: NetworkMap[],
-  defaultStorage: string | null | undefined,
+  defaultStorage: { value: string | null, busType?: string | null } | undefined,
   storageMap: StorageMap[],
   sourceFailed: boolean,
   destinationFailedMessage: string | null,
@@ -139,20 +143,25 @@ class EditReplica extends React.Component<Props, State> {
   getStorageMap(storageBackends: StorageBackend[]): StorageMap[] {
     const storageMap: StorageMap[] = []
     const currentStorage = this.props.replica.storage_mappings
-    const buildStorageMap = (type: 'backend' | 'disk', mapping: any) => {
-      const backend = storageBackends.find(b => b.name === mapping.destination)
-      return {
+    const buildStorageMap = (type: 'backend' | 'disk', mapping: any): StorageMap => {
+      const busTypeInfo = EndpointUtils.getBusTypeStorageId(storageBackends, mapping.destination)
+      const backend = storageBackends.find(b => b.name === busTypeInfo.id)
+      const newStorageMap: StorageMap = {
         type,
         source: { storage_backend_identifier: mapping.source, id: mapping.disk_id },
-        target: { name: mapping.destination, id: backend ? backend.id : mapping.destination },
+        target: { name: busTypeInfo.id!, id: backend ? backend.id : busTypeInfo.id },
       }
+      if (busTypeInfo.busType) {
+        newStorageMap.targetBusType = busTypeInfo.busType
+      }
+      return newStorageMap
     }
-    const backendMappings = (currentStorage && currentStorage.backend_mappings) || []
+    const backendMappings = currentStorage?.backend_mappings || []
     backendMappings.forEach(mapping => {
       storageMap.push(buildStorageMap('backend', mapping))
     })
 
-    const diskMappings = (currentStorage && currentStorage.disk_mappings) || []
+    const diskMappings = currentStorage?.disk_mappings || []
     diskMappings.forEach(mapping => {
       storageMap.push(buildStorageMap('disk', mapping))
     })
@@ -163,6 +172,9 @@ class EditReplica extends React.Component<Props, State> {
         && m.source[fieldName] === String(mapping.source[fieldName]))
       if (existingMapping) {
         existingMapping.target = mapping.target
+        if (mapping.targetBusType !== undefined) {
+          existingMapping.targetBusType = mapping.targetBusType
+        }
       } else {
         storageMap.push(mapping)
       }
@@ -178,8 +190,9 @@ class EditReplica extends React.Component<Props, State> {
     if (networkMap) {
       Object.keys(networkMap).forEach(sourceNetworkName => {
         const destNetObj: any = networkMap[sourceNetworkName]
+        const portKeyInfo = NetworkUtils.getPortKeyNetworkId(this.props.networks, destNetObj)
         const destNetId = String(typeof destNetObj === 'string' || !destNetObj
-          || !destNetObj.id ? destNetObj : destNetObj.id)
+          || !destNetObj.id ? portKeyInfo.id : destNetObj.id)
 
         const network = this.props.networks.find(n => n.name === destNetId || n.id === destNetId) || null
         const mapping: NetworkMap = {
@@ -189,7 +202,7 @@ class EditReplica extends React.Component<Props, State> {
           targetNetwork: network,
         }
         if (destNetObj.security_groups) {
-          const destSecGroupsInfo = (network && network.security_groups) || []
+          const destSecGroupsInfo = network?.security_groups || []
           const secInfo = destNetObj.security_groups.map((s: SecurityGroup) => {
             const foundSecGroupInfo = destSecGroupsInfo
               .find((si: any) => (si.id ? si.id === s : si === s))
@@ -197,6 +210,9 @@ class EditReplica extends React.Component<Props, State> {
           })
           mapping.targetSecurityGroups = secInfo
         }
+        if (portKeyInfo.portKey) {
+          mapping.targetPortKey = portKeyInfo.portKey
+        }
         selectedNetworks.push(mapping)
       })
     }
@@ -208,11 +224,30 @@ class EditReplica extends React.Component<Props, State> {
     return selectedNetworks
   }
 
-  getDefaultStorage() {
-    const storageMappings = this.props.replica.storage_mappings
-    const replicaDefaultStorage = storageMappings && storageMappings.default
-    return this.state.defaultStorage !== undefined
-      ? this.state.defaultStorage : replicaDefaultStorage
+  getDefaultStorage(): { value: string | null, busType?: string | null } {
+    if (this.state.defaultStorage) {
+      return this.state.defaultStorage
+    }
+
+    const buildDefaultStorage = (defaultValue: string | null | undefined) => {
+      const busTypeInfo = EndpointUtils.getBusTypeStorageId(endpointStore.storageBackends, defaultValue || null)
+      const defaultStorage: { value: string | null, busType?: string | null } = {
+        value: busTypeInfo.id,
+      }
+      if (busTypeInfo.busType) {
+        defaultStorage.busType = busTypeInfo.busType
+      }
+      return defaultStorage
+    }
+
+    if (this.props.replica.storage_mappings?.default) {
+      return buildDefaultStorage(this.props.replica.storage_mappings.default)
+    }
+
+    if (endpointStore.storageConfigDefault) {
+      return buildDefaultStorage(endpointStore.storageConfigDefault)
+    }
+    return { value: null }
   }
 
   getFieldValue(type: 'source' | 'destination', fieldName: string, defaultValue: any, parentFieldName?: string) {
@@ -282,10 +317,6 @@ class EditReplica extends React.Component<Props, State> {
     minionPoolStore.loadMinionPools()
     await providerStore.loadProviders()
 
-    if (this.hasStorageMap()) {
-      endpointStore.loadStorage(this.props.destinationEndpoint.id, {})
-    }
-
     const loadAllOptions = async (type: 'source' | 'destination') => {
       const endpoint = type === 'source' ? this.props.sourceEndpoint : this.props.destinationEndpoint
       try {
@@ -500,8 +531,11 @@ class EditReplica extends React.Component<Props, State> {
       }
     } else {
       try {
-        const replicaDefaultStorage = this.props.replica.storage_mappings
-          && this.props.replica.storage_mappings.default
+        const defaultStorage = EndpointUtils.getBusTypeStorageId(endpointStore.storageBackends, this.props.replica.storage_mappings?.default || null)
+        const replicaDefaultStorage: { value: string | null, busType?: string | null } = {
+          value: defaultStorage.id,
+          busType: defaultStorage.busType,
+        }
         const migration: MigrationItemDetails = await migrationStore.recreate(
           this.props.replica as any,
           this.props.sourceEndpoint,
@@ -520,15 +554,16 @@ class EditReplica extends React.Component<Props, State> {
     }
   }
 
-  handleNetworkChange(
-    sourceNic: Nic,
-    targetNetwork: Network,
-    targetSecurityGroups: SecurityGroup[] | null | undefined,
-  ) {
+  handleNetworkChange(changeObject: WizardNetworksChangeObject) {
     const networkMap = this.state.selectedNetworks
-      .filter(n => n.sourceNic.network_name !== sourceNic.network_name)
+      .filter(n => n.sourceNic.network_name !== changeObject.nic.network_name)
     this.setState({
-      selectedNetworks: [...networkMap, { sourceNic, targetNetwork, targetSecurityGroups }],
+      selectedNetworks: [...networkMap, {
+        sourceNic: changeObject.nic,
+        targetNetwork: changeObject.network,
+        targetSecurityGroups: changeObject.securityGroups,
+        targetPortKey: changeObject.portKey,
+      }],
     })
   }
 
@@ -557,12 +592,12 @@ class EditReplica extends React.Component<Props, State> {
     }))
   }
 
-  handleStorageChange(source: Disk, target: StorageBackend, type: 'backend' | 'disk') {
+  handleStorageChange(mapping: StorageMap) {
     this.setState(prevState => {
-      const diskFieldName = type === 'backend' ? 'storage_backend_identifier' : 'id'
+      const diskFieldName = mapping.type === 'backend' ? 'storage_backend_identifier' : 'id'
       const storageMap = prevState.storageMap
-        .filter(n => n.type !== type || n.source[diskFieldName] !== source[diskFieldName])
-      storageMap.push({ source, target, type })
+        .filter(n => n.type !== mapping.type || n.source[diskFieldName] !== mapping.source[diskFieldName])
+      storageMap.push(mapping)
 
       return { storageMap }
     })
@@ -614,7 +649,6 @@ class EditReplica extends React.Component<Props, State> {
         selectedInstances={type === 'destination' ? this.props.instancesDetails : null}
         hasStorageMap={type === 'source' ? false : this.hasStorageMap()}
         storageBackends={endpointStore.storageBackends}
-        storageConfigDefault={endpointStore.storageConfigDefault}
         onChange={(f, v, fp) => { this.handleFieldChange(type, f, v, fp) }}
         oneColumnStyle={{
           marginTop: '-16px', display: 'flex', flexDirection: 'column', width: '100%', alignItems: 'center',
@@ -644,13 +678,12 @@ class EditReplica extends React.Component<Props, State> {
     return (
       <WizardStorage
         defaultStorage={this.getDefaultStorage()}
-        onDefaultStorageChange={defaultStorage => { this.setState({ defaultStorage }) }}
-        storageConfigDefault={endpointStore.storageConfigDefault}
+        onDefaultStorageChange={(value, busType) => { this.setState({ defaultStorage: { value, busType } }) }}
         defaultStorageLayout="modal"
         storageBackends={endpointStore.storageBackends}
         instancesDetails={this.props.instancesDetails}
         storageMap={this.getStorageMap(endpointStore.storageBackends)}
-        onChange={(s, t, type) => { this.handleStorageChange(s, t, type) }}
+        onChange={mapping => { this.handleStorageChange(mapping) }}
         style={{ padding: '32px 32px 0 32px', width: 'calc(100% - 64px)' }}
         titleWidth={160}
         onScrollableRef={ref => { this.scrollableRef = ref }}
@@ -665,8 +698,8 @@ class EditReplica extends React.Component<Props, State> {
         loadingInstancesDetails={this.props.instancesDetailsLoading}
         networks={this.props.networks}
         loading={this.props.networksLoading}
-        onChange={(nic, network, secGroups) => {
-          this.handleNetworkChange(nic, network, secGroups)
+        onChange={change => {
+          this.handleNetworkChange(change)
         }}
         selectedNetworks={this.getSelectedNetworks()}
         style={{ padding: '32px 32px 0 32px', width: 'calc(100% - 64px)' }}

+ 4 - 2
src/components/organisms/MainDetails/MainDetails.tsx

@@ -25,7 +25,7 @@ import MainDetailsTable from '../../molecules/MainDetailsTable'
 import PasswordValue from '../../atoms/PasswordValue'
 
 import type { Instance } from '../../../@types/Instance'
-import type { Endpoint } from '../../../@types/Endpoint'
+import type { Endpoint, StorageBackend } from '../../../@types/Endpoint'
 import type { Network } from '../../../@types/Network'
 import type { Field as FieldType } from '../../../@types/Field'
 import fieldHelper from '../../../@types/Field'
@@ -133,6 +133,7 @@ const PropertyValue = styled.div<any>`
 type Props = {
   item?: TransferItem | null,
   minionPools: MinionPool[]
+  storageBackends: StorageBackend[]
   destinationSchema: FieldType[],
   destinationSchemaLoading: boolean,
   sourceSchema: FieldType[],
@@ -219,7 +220,7 @@ class MainDetails extends React.Component<Props, State> {
         })
       } else if (value && typeof value === 'object') {
         properties = properties.concat(Object.keys(value).map(p => {
-          if (p === 'disk_mappings') {
+          if (p === 'disk_mappings' || p === 'backend_mappings') {
             return null
           }
           let fieldName = pn
@@ -436,6 +437,7 @@ class MainDetails extends React.Component<Props, State> {
             minionPools={this.props.minionPools}
             instancesDetails={this.props.instancesDetails}
             networks={this.props.networks}
+            storageBackends={this.props.storageBackends}
           />
         )}
         {this.renderLoading()}

+ 6 - 1
src/components/organisms/MigrationDetailsContent/MigrationDetailsContent.tsx

@@ -23,10 +23,11 @@ import Tasks from '../Tasks'
 import StyleProps from '../../styleUtils/StyleProps'
 
 import type { Instance } from '../../../@types/Instance'
-import type { Endpoint } from '../../../@types/Endpoint'
+import type { Endpoint, StorageBackend } from '../../../@types/Endpoint'
 import type { Field } from '../../../@types/Field'
 import { MigrationItemDetails } from '../../../@types/MainItem'
 import { MinionPool } from '../../../@types/MinionPool'
+import { Network } from '../../../@types/Network'
 
 const Wrapper = styled.div<any>`
   display: flex;
@@ -58,8 +59,10 @@ type Props = {
   itemId: string
   minionPools: MinionPool[]
   detailsLoading: boolean,
+  storageBackends: StorageBackend[]
   instancesDetails: Instance[],
   instancesDetailsLoading: boolean,
+  networks: Network[],
   sourceSchema: Field[],
   sourceSchemaLoading: boolean,
   destinationSchema: Field[],
@@ -91,9 +94,11 @@ class MigrationDetailsContent extends React.Component<Props> {
     return (
       <MainDetails
         item={this.props.item}
+        storageBackends={this.props.storageBackends}
         minionPools={this.props.minionPools}
         instancesDetails={this.props.instancesDetails}
         instancesDetailsLoading={this.props.instancesDetailsLoading}
+        networks={this.props.networks}
         sourceSchema={this.props.sourceSchema}
         sourceSchemaLoading={this.props.sourceSchemaLoading}
         destinationSchema={this.props.destinationSchema}

+ 3 - 1
src/components/organisms/ReplicaDetailsContent/ReplicaDetailsContent.tsx

@@ -23,7 +23,7 @@ import MainDetails from '../MainDetails'
 import Executions from '../Executions'
 import Schedule from '../Schedule'
 import type { Instance } from '../../../@types/Instance'
-import type { Endpoint } from '../../../@types/Endpoint'
+import type { Endpoint, StorageBackend } from '../../../@types/Endpoint'
 import type { Execution, ExecutionTasks } from '../../../@types/Execution'
 import type { Network } from '../../../@types/Network'
 import type { Field } from '../../../@types/Field'
@@ -89,6 +89,7 @@ type Props = {
   executionsTasksLoading: boolean,
   executionsTasks: ExecutionTasks[],
   minionPools: MinionPool[]
+  storageBackends: StorageBackend[]
   onExecutionChange: (executionId: string) => void,
   onCancelExecutionClick: (execution: Execution | null, force?: boolean) => void,
   onDeleteExecutionClick: (execution: Execution | null) => void,
@@ -171,6 +172,7 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
     return (
       <MainDetails
         item={this.props.item}
+        storageBackends={this.props.storageBackends}
         minionPools={this.props.minionPools}
         sourceSchema={this.props.sourceSchema}
         sourceSchemaLoading={this.props.sourceSchemaLoading}

+ 50 - 26
src/components/organisms/WizardNetworks/WizardNetworks.tsx

@@ -22,7 +22,7 @@ import Dropdown from '../../molecules/Dropdown'
 
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
-import { Instance, Nic as NicType, shortenId } from '../../../@types/Instance'
+import { Instance, InstanceUtils, Nic as NicType } from '../../../@types/Instance'
 import type { Network, NetworkMap, SecurityGroup } from '../../../@types/Network'
 
 import networkImage from './images/network.svg'
@@ -111,14 +111,19 @@ const Dropdowns = styled.div<any>`
     }
   }
 `
-
+export type WizardNetworksChangeObject = {
+  nic: NicType,
+  network: Network,
+  securityGroups?: SecurityGroup[]
+  portKey?: string | null
+}
 type Props = {
   loading: boolean,
   loadingInstancesDetails: boolean,
   networks: Network[],
   instancesDetails: Instance[],
   selectedNetworks?: NetworkMap[] | null,
-  onChange: (nic: NicType, network: Network, securityGroups?: SecurityGroup[]) => void,
+  onChange: (changeObject: WizardNetworksChangeObject) => void,
   style?: any,
   titleWidth?: number,
 }
@@ -158,9 +163,9 @@ class WizardNetworks extends React.Component<Props> {
     return this.props.networks.length > 10 ? (
       <AutocompleteDropdown
         width={StyleProps.inputSizes.large.width}
-        selectedItem={selectedNetwork ? selectedNetwork.targetNetwork : null}
+        selectedItem={selectedNetwork?.targetNetwork || null}
         items={this.props.networks}
-        onChange={(item: Network) => { this.props.onChange(nic, item) }}
+        onChange={(network: Network) => { this.props.onChange({ nic, network }) }}
         labelField="name"
         valueField="id"
       />
@@ -171,26 +176,20 @@ class WizardNetworks extends React.Component<Props> {
           centered
           noSelectionMessage="Select Network"
           noItemsMessage={this.props.loading ? 'Loading ...' : 'No networks found'}
-          selectedItem={selectedNetwork ? selectedNetwork.targetNetwork : null}
+          selectedItem={selectedNetwork?.targetNetwork || null}
           items={this.props.networks}
           labelField="name"
           valueField="id"
-          onChange={(item: Network) => {
-            this.props.onChange(nic, item)
-          }}
-          data-test-id={`wNetworks-dropdown-${nic.id}`}
+          onChange={(network: Network) => { this.props.onChange({ nic, network }) }}
         />
       )
   }
 
   renderSecGroupsDropdown(selectedNetwork: NetworkMap | null | undefined, nic: NicType) {
     const MAX_SELECTED_GROUPS = 5
-    const hasSecurityGroups: boolean = Boolean(this.props.networks
-      .find(n => n.security_groups && n.security_groups.length))
-    const securityGroups = selectedNetwork && selectedNetwork.targetNetwork
-      && selectedNetwork.targetNetwork.security_groups
-    let selectedSecGroups: SecurityGroup[] = (selectedNetwork
-      && selectedNetwork.targetSecurityGroups) || []
+    const hasSecurityGroups: boolean = Boolean(this.props.networks.find(n => n.security_groups?.length))
+    const securityGroups = selectedNetwork?.targetNetwork?.security_groups
+    let selectedSecGroups: SecurityGroup[] = selectedNetwork?.targetSecurityGroups || []
     return hasSecurityGroups && this.props.networks.length ? (
       <Dropdown
         width={StyleProps.inputSizes.large.width}
@@ -204,10 +203,8 @@ class WizardNetworks extends React.Component<Props> {
         labelField="name"
         valueField="id"
         onChange={(item: any) => {
-          if (selectedSecGroups
-            .find((i: any) => (i.id && item.id ? i.id === item.id : i === item))) {
-            selectedSecGroups = selectedSecGroups
-              .filter((i: any) => (i.id && item.id ? i.id !== item.id : i !== item))
+          if (selectedSecGroups.find((i: any) => (i.id && item.id ? i.id === item.id : i === item))) {
+            selectedSecGroups = selectedSecGroups.filter((i: any) => (i.id && item.id ? i.id !== item.id : i !== item))
           } else {
             selectedSecGroups = [...selectedSecGroups, item]
           }
@@ -217,19 +214,46 @@ class WizardNetworks extends React.Component<Props> {
           if (selectedSecGroups.length > MAX_SELECTED_GROUPS) {
             selectedSecGroups.splice(MAX_SELECTED_GROUPS - 1, 1)
           }
-          this.props.onChange(nic, selectedNetwork.targetNetwork!, selectedSecGroups)
+          this.props.onChange({ nic, network: selectedNetwork.targetNetwork!, securityGroups: selectedSecGroups })
         }}
       />
     ) : null
   }
 
+  renderPortKeysDropdown(selectedNetwork: NetworkMap | null | undefined, nic: NicType) {
+    const portKeys = selectedNetwork?.targetNetwork?.port_keys
+    if (!portKeys || !portKeys.length || !this.props.networks.length) {
+      return null
+    }
+    type DropdownItem = { label: string, value: string | null }
+    const portKeysDict: DropdownItem[] = portKeys.map(p => ({
+      label: p,
+      value: p,
+    }))
+    portKeysDict.unshift({ label: 'Choose Port Key', value: null })
+
+    const selectedPortKey: string | null = selectedNetwork?.targetPortKey || null
+    return (
+      <Dropdown
+        width={StyleProps.inputSizes.large.width}
+        noSelectionMessage="Choose Port Key"
+        centered
+        items={portKeysDict}
+        selectedItem={selectedPortKey}
+        onChange={(item: DropdownItem) => {
+          this.props.onChange({ nic, network: selectedNetwork!.targetNetwork!, portKey: item.value })
+        }}
+      />
+    )
+  }
+
   renderNics() {
     if (this.isLoading()) {
       return null
     }
     let nics: NicType[] = []
     this.props.instancesDetails.forEach(instance => {
-      if (!instance.devices || !instance.devices.nics) {
+      if (!instance.devices?.nics) {
         return
       }
       instance.devices.nics.forEach(nic => {
@@ -240,7 +264,7 @@ class WizardNetworks extends React.Component<Props> {
       })
     })
 
-    if (nics.length === 0 && this.props.selectedNetworks && this.props.selectedNetworks.length) {
+    if (nics.length === 0 && this.props.selectedNetworks?.length) {
       nics = this.props.selectedNetworks.map(n => n.sourceNic)
     }
 
@@ -259,10 +283,9 @@ class WizardNetworks extends React.Component<Props> {
               return true
             }
             return false
-          }).map(instance => `${instance.name} (${shortenId(instance.instance_name || instance.id)})`)
+          }).map(instance => `${instance.name} (${InstanceUtils.shortenId(instance.instance_name || instance.id)})`)
 
-          const selectedNetwork = this.props.selectedNetworks
-            && this.props.selectedNetworks.find(n => n.sourceNic.network_name === nic.network_name)
+          const selectedNetwork = this.props.selectedNetworks?.find(n => n.sourceNic.network_name === nic.network_name)
           return (
             <Nic key={nic.id} data-test-id="networkItem">
               <NetworkImage />
@@ -278,6 +301,7 @@ class WizardNetworks extends React.Component<Props> {
               <Dropdowns>
                 {this.renderNetworksDropdown(selectedNetwork, nic)}
                 {this.renderSecGroupsDropdown(selectedNetwork, nic)}
+                {this.renderPortKeysDropdown(selectedNetwork, nic)}
               </Dropdowns>
             </Nic>
           )

+ 0 - 1
src/components/organisms/WizardOptions/WizardOptions.tsx

@@ -146,7 +146,6 @@ type Props = {
   useAdvancedOptions?: boolean,
   hasStorageMap: boolean,
   storageBackends?: StorageBackend[],
-  storageConfigDefault?: string,
   onAdvancedOptionsToggle?: (showAdvanced: boolean) => void,
   wizardType: string,
   oneColumnStyle?: { [prop: string]: any },

+ 26 - 11
src/components/organisms/WizardPageContent/WizardPageContent.tsx

@@ -23,7 +23,7 @@ import InfoIcon from '../../atoms/InfoIcon'
 import WizardBreadcrumbs from '../../molecules/WizardBreadcrumbs'
 import WizardEndpointList from '../WizardEndpointList'
 import WizardInstances from '../WizardInstances'
-import WizardNetworks from '../WizardNetworks'
+import WizardNetworks, { WizardNetworksChangeObject } from '../WizardNetworks'
 import WizardStorage from '../WizardStorage'
 import WizardOptions from '../WizardOptions'
 import WizardScripts from '../WizardScripts'
@@ -36,12 +36,11 @@ import { providerTypes, wizardPages, migrationFields } from '../../../constants'
 import configLoader from '../../../utils/Config'
 
 import type { WizardData, WizardPage } from '../../../@types/WizardData'
-import type { Endpoint, StorageBackend, StorageMap } from '../../../@types/Endpoint'
+import { Endpoint, EndpointUtils, StorageMap } from '../../../@types/Endpoint'
 import type {
-  Instance, Nic, Disk, InstanceScript,
+  Instance, InstanceScript,
 } from '../../../@types/Instance'
 import type { Field } from '../../../@types/Field'
-import type { Network, SecurityGroup } from '../../../@types/Network'
 import type { Schedule as ScheduleType } from '../../../@types/Schedule'
 import instanceStore from '../../../stores/InstanceStore'
 import providerStore from '../../../stores/ProviderStore'
@@ -178,7 +177,7 @@ type Props = {
   wizardData: WizardData,
   schedules: ScheduleType[],
   storageMap: StorageMap[],
-  defaultStorage: string | null,
+  defaultStorage: { value: string | null, busType?: string | null } | undefined,
   hasStorageMap: boolean,
   hasSourceOptions: boolean,
   pages: WizardPage[],
@@ -196,9 +195,9 @@ type Props = {
   onInstancePageClick: (page: number) => void,
   onDestOptionsChange: (field: Field, value: any, parentFieldName?: string) => void,
   onSourceOptionsChange: (field: Field, value: any, parentFieldName?: string) => void,
-  onNetworkChange: (nic: Nic, network: Network, secGroups?: SecurityGroup[]) => void,
-  onStorageChange: (sourceStorage: Disk, targetStorage: StorageBackend, type: 'backend' | 'disk') => void,
-  onDefaultStorageChange: (value: string | null) => void,
+  onNetworkChange: (changeObject: WizardNetworksChangeObject) => void,
+  onStorageChange: (mapping: StorageMap) => void,
+  onDefaultStorageChange: (value: string | null, busType?: string | null) => void,
   onAddScheduleClick: (schedule: ScheduleType) => void,
   onScheduleChange: (scheduleId: string, schedule: ScheduleType) => void,
   onScheduleRemove: (scheudleId: string) => void,
@@ -376,6 +375,24 @@ class WizardPageContent extends React.Component<Props, State> {
       return optionsLoadingRequiredFields
     }
 
+    const getDefaultStorage = (): { value: string | null, busType?: string | null } => {
+      if (this.props.defaultStorage) {
+        return this.props.defaultStorage
+      }
+
+      if (endpointStore.storageConfigDefault) {
+        const busTypeInfo = EndpointUtils.getBusTypeStorageId(endpointStore.storageBackends, endpointStore.storageConfigDefault || null)
+        const defaultStorage: { value: string | null, busType?: string | null } = {
+          value: busTypeInfo.id,
+        }
+        if (busTypeInfo.busType) {
+          defaultStorage.busType = busTypeInfo.busType
+        }
+        return defaultStorage
+      }
+      return { value: null }
+    }
+
     switch (this.props.page.id) {
       case 'type':
         body = (
@@ -473,7 +490,6 @@ class WizardPageContent extends React.Component<Props, State> {
             useAdvancedOptions={this.state.useAdvancedOptions}
             hasStorageMap={this.props.hasStorageMap}
             storageBackends={this.props.endpointStore.storageBackends}
-            storageConfigDefault={this.props.endpointStore.storageConfigDefault}
             wizardType={this.props.type}
             onAdvancedOptionsToggle={useAdvancedOptions => {
               this.handleAdvancedOptionsToggle(useAdvancedOptions)
@@ -502,8 +518,7 @@ class WizardPageContent extends React.Component<Props, State> {
             instancesDetails={this.props.instanceStore.instancesDetails}
             storageMap={this.props.storageMap}
             onChange={this.props.onStorageChange}
-            storageConfigDefault={this.props.endpointStore.storageConfigDefault}
-            defaultStorage={this.props.defaultStorage}
+            defaultStorage={getDefaultStorage()}
             onDefaultStorageChange={this.props.onDefaultStorageChange}
             defaultStorageLayout="page"
           />

+ 115 - 27
src/components/organisms/WizardStorage/WizardStorage.tsx

@@ -22,7 +22,7 @@ import InfoIcon from '../../atoms/InfoIcon'
 
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
-import { Instance, Disk, shortenId } from '../../../@types/Instance'
+import { Instance, Disk, InstanceUtils } from '../../../@types/Instance'
 import type { StorageBackend, StorageMap } from '../../../@types/Endpoint'
 
 import backendImage from './images/backend.svg'
@@ -97,6 +97,22 @@ const ArrowImage = styled.div<any>`
   flex-grow: 1;
   margin-right: 16px;
 `
+const Dropdowns = styled.div<any>`
+  > div {
+    margin-bottom: 16px;
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+`
+const DefaultDropdowns = styled.div<any>`
+  display: flex;
+  margin-bottom: 0;
+  margin-left: -16px;
+  > div {
+    margin-left: 16px;
+  }
+`
 const NoStorageMessage = styled.div<any>`
   display: flex;
   flex-direction: column;
@@ -151,10 +167,9 @@ export type Props = {
   instancesDetails: Instance[],
   storageMap: StorageMap[] | null | undefined,
   defaultStorageLayout: 'modal' | 'page',
-  defaultStorage: string | null | undefined,
-  storageConfigDefault: string | null | undefined,
-  onDefaultStorageChange: (value: string | null) => void,
-  onChange: (sourceStorage: Disk, targetStorage: StorageBackend, type: 'backend' | 'disk') => void,
+  defaultStorage: { value: string | null, busType?: string | null },
+  onDefaultStorageChange: (value: string | null, busType?: string | null) => void,
+  onChange: (newMapping: StorageMap) => void,
   onScrollableRef?: (ref: HTMLElement) => void,
   style?: any,
   titleWidth?: number,
@@ -173,27 +188,60 @@ class WizardStorage extends React.Component<Props> {
     )
   }
 
+  renderDisabledDisk(disk: Disk) {
+    return (
+      <DiskDisabledMessage>
+        {disk.disabled!.message}{disk.disabled!.info
+          ? <InfoIcon text={disk.disabled!.info} /> : null}
+      </DiskDisabledMessage>
+    )
+  }
+
+  renderBusTypeDropdown(selectedStorageMap: StorageMap | null | undefined) {
+    if (!selectedStorageMap) {
+      return null
+    }
+    type DropdownItem = {label: string, value: string | null}
+    const storageBusTypes: DropdownItem[] | undefined = this.props.storageBackends
+      .find(s => s.id === selectedStorageMap.target.id)?.additional_provider_properties?.supported_bus_types?.map(value => ({
+        label: value,
+        value,
+      }))
+    if (!storageBusTypes || !storageBusTypes.length) {
+      return null
+    }
+    storageBusTypes.unshift({
+      label: 'Choose a Bus Type',
+      value: null,
+    })
+    const selectedBusType = selectedStorageMap?.targetBusType
+
+    return (
+      <Dropdown
+        width={StyleProps.inputSizes.large.width}
+        noSelectionMessage="Choose a Bus Type"
+        centered
+        items={storageBusTypes}
+        selectedItem={selectedBusType}
+        onChange={(item: DropdownItem) => {
+          this.props.onChange({ ...selectedStorageMap, targetBusType: item.value })
+        }}
+      />
+    )
+  }
+
   renderStorageDropdown(
-    storageItems: Array<StorageBackend | { id: string | null, name: string }>,
-    selectedItem: StorageBackend | null,
+    storageItems: Array<StorageBackend>,
+    selectedItem: StorageBackend | null | undefined,
     disk: Disk,
     type: 'backend' | 'disk',
   ) {
-    if (disk.disabled && type === 'disk') {
-      return (
-        <DiskDisabledMessage>
-          {disk.disabled.message}{disk.disabled.info
-            ? <InfoIcon text={disk.disabled.info} /> : null}
-        </DiskDisabledMessage>
-      )
-    }
-
     return storageItems.length > 10 ? (
       <AutocompleteDropdown
         width={StyleProps.inputSizes.large.width}
         selectedItem={selectedItem}
         items={storageItems}
-        onChange={(item: StorageBackend) => { this.props.onChange(disk, item, type) }}
+        onChange={(item: StorageBackend) => { this.props.onChange({ source: disk, target: item, type }) }}
         labelField="name"
         valueField="id"
       />
@@ -208,7 +256,7 @@ class WizardStorage extends React.Component<Props> {
           items={storageItems}
           labelField="name"
           valueField="id"
-          onChange={(item: StorageBackend) => { this.props.onChange(disk, item, type) }}
+          onChange={(item: StorageBackend) => { this.props.onChange({ source: disk, target: item, type }) }}
           data-test-id={`${TEST_ID}-${type}-destination`}
         />
       )
@@ -253,11 +301,10 @@ class WizardStorage extends React.Component<Props> {
                 return true
               }
               return false
-            }).map(instance => `${instance.name} (${shortenId(instance.instance_name || instance.id)})`)
+            }).map(instance => `${instance.name} (${InstanceUtils.shortenId(instance.instance_name || instance.id)})`)
 
-            const selectedStorage = storageMap && storageMap.find(s => s.type === type
+            const selectedStorageMapping = storageMap?.find(s => s.type === type
                 && String(s.source[diskFieldName]) === String(disk[diskFieldName]))
-            const selectedItem = selectedStorage ? selectedStorage.target : null
             const diskNameParsed = parseDiskName(disk[diskFieldName])
             return (
               <StorageItem key={disk[diskFieldName]}>
@@ -273,7 +320,14 @@ class WizardStorage extends React.Component<Props> {
                   ) : null}
                 </StorageTitle>
                 <ArrowImage />
-                {this.renderStorageDropdown(storageItems, selectedItem, disk, type)}
+                <Dropdowns>
+                  {disk.disabled && type === 'disk' ? this.renderDisabledDisk(disk) : (
+                    <>
+                      {this.renderStorageDropdown(storageItems, selectedStorageMapping?.target, disk, type)}
+                      {this.renderBusTypeDropdown(selectedStorageMapping)}
+                    </>
+                  )}
+                </Dropdowns>
               </StorageItem>
             )
           })}
@@ -318,10 +372,7 @@ class WizardStorage extends React.Component<Props> {
         { label: 'Choose a value', value: null },
         ...items,
       ]
-      const selectedItem = items.find(i => i.value === (
-        this.props.defaultStorage !== undefined
-          ? this.props.defaultStorage : this.props.storageConfigDefault
-      ))
+      const selectedItem = items.find(i => i.value === this.props.defaultStorage.value)
       const commonProps = {
         width: StyleProps.inputSizes.regular.width,
         selectedItem,
@@ -345,6 +396,40 @@ class WizardStorage extends React.Component<Props> {
         )
     }
 
+    const renderDefaultBusTypeDropdown = () => {
+      if (!this.props.defaultStorage || !this.props.defaultStorage.value) {
+        return null
+      }
+
+      type DropdownItem = { label: string, value: string | null }
+      const storageBusTypes: DropdownItem[] | undefined = this.props.storageBackends
+        .find(s => s.id === this.props.defaultStorage?.value)?.additional_provider_properties?.supported_bus_types?.map(value => ({
+          label: value,
+          value,
+        }))
+      if (!storageBusTypes || !storageBusTypes.length) {
+        return null
+      }
+      storageBusTypes.unshift({
+        label: 'Choose a Bus Type',
+        value: null,
+      })
+      const selectedBusType = this.props.defaultStorage.busType
+
+      return (
+        <Dropdown
+          width={StyleProps.inputSizes.regular.width}
+          noSelectionMessage="Choose a Bus Type"
+          centered
+          items={storageBusTypes}
+          selectedItem={selectedBusType}
+          onChange={(item: DropdownItem) => {
+            this.props.onDefaultStorageChange(this.props.defaultStorage?.value || null, item.value)
+          }}
+        />
+      )
+    }
+
     return (
       <StorageWrapper>
         <StorageSection>
@@ -360,7 +445,10 @@ class WizardStorage extends React.Component<Props> {
             <StorageImage backend="backend" />
             <StorageTitle width={this.props.titleWidth || 320}>
               <StorageName>
-                {renderDropdown()}
+                <DefaultDropdowns>
+                  {renderDropdown()}
+                  {renderDefaultBusTypeDropdown()}
+                </DefaultDropdowns>
               </StorageName>
             </StorageTitle>
           </StorageItem>

+ 2 - 4
src/components/organisms/WizardStorage/story.tsx

@@ -68,8 +68,7 @@ storiesOf('WizardStorage', module)
       instancesDetails={instancesDetails}
       storageMap={null}
       defaultStorageLayout="page"
-      defaultStorage="backend-1"
-      storageConfigDefault={null}
+      defaultStorage={{ value: 'backend-1' }}
       onDefaultStorageChange={() => { }}
       onChange={() => { }}
     />
@@ -80,8 +79,7 @@ storiesOf('WizardStorage', module)
       instancesDetails={instancesDetails}
       storageMap={null}
       defaultStorageLayout="modal"
-      defaultStorage="backend-1"
-      storageConfigDefault={null}
+      defaultStorage={{ value: 'backend-1' }}
       onDefaultStorageChange={() => { }}
       onChange={() => { }}
     />

+ 27 - 16
src/components/organisms/WizardSummary/WizardSummary.tsx

@@ -122,19 +122,13 @@ const InstanceRowSubtitle = styled.div<any>`
 const SourceNetwork = styled.div<any>`
   width: 50%;
   margin-right: 16px;
+  overflow-wrap: break-word;
 `
 const NetworkArrow = styled.div<any>`
   width: 32px;
   height: 16px;
   background: url('${networkArrowImage}') center no-repeat;
 `
-const StorageTarget = styled.div<any>`
-  width: 50%;
-  text-align: right;
-  margin-left: 20px;
-  text-overflow: ellipsis;
-  overflow: hidden;
-`
 const TargetNetwork = styled.div<any>`
   width: 50%;
   text-align: right;
@@ -147,7 +141,10 @@ const TargetNetworkName = styled.div<any>`
   width: 100%;
   text-overflow: ellipsis;
   overflow: hidden;
-  margin-top: 16px;
+  margin-top: 8px;
+  &:first-child {
+    margin-top: 16px;
+  }
 `
 const OptionsList = styled.div<any>``
 const Option = styled.div<any>`
@@ -179,7 +176,7 @@ type Props = {
   wizardType: 'replica' | 'migration',
   schedules: Schedule[],
   minionPools: MinionPool[]
-  defaultStorage: string | null,
+  defaultStorage: { value: string | null, busType?: string | null } | undefined,
   storageMap: StorageMap[],
   instancesDetails: Instance[],
   sourceSchema: Field[],
@@ -429,10 +426,16 @@ class WizardSummary extends React.Component<Props> {
       ),
     ]
 
-    const defaultStorageOption = (
+    const renderDefaultStorageOption = () => (
       <Option>
         <OptionLabel>Default Storage</OptionLabel>
-        <OptionValue>{this.props.defaultStorage}</OptionValue>
+        <OptionValue>{this.props.defaultStorage!.value}{this.props.defaultStorage!.busType ? (
+          <>
+            <br />
+            Bus Type: {this.props.defaultStorage!.busType}
+          </>
+        ) : null}
+        </OptionValue>
       </Option>
     )
 
@@ -444,7 +447,7 @@ class WizardSummary extends React.Component<Props> {
           {this.props.wizardType === 'migration' ? migrationOptions : null}
           {this.props.data.selectedInstances
             && this.props.data.selectedInstances.length > 1 ? separateVmOption : null}
-          {this.props.defaultStorage ? defaultStorageOption : null}
+          {this.props.defaultStorage ? renderDefaultStorageOption() : null}
           {data.destOptions ? Object.keys(data.destOptions).map(optionName => {
             if (
               optionName === 'execute_now'
@@ -490,11 +493,11 @@ class WizardSummary extends React.Component<Props> {
     }
     const fieldName = type === 'backend' ? 'storage_backend_identifier' : 'id'
 
-    let fullStorageMap: { source: Disk, target: StorageBackend | null }[] = disks
+    let fullStorageMap: { source: Disk, target: StorageBackend | null, busType?: string | null }[] = disks
       .filter(d => d[fieldName]).map(disk => {
         const diskMapped = storageMap.find(s => s.source[fieldName] === disk[fieldName])
         if (diskMapped) {
-          return { source: diskMapped.source, target: diskMapped.target }
+          return { source: diskMapped.source, target: diskMapped.target, busType: diskMapped.targetBusType }
         }
         return { source: disk, target: null }
       })
@@ -519,7 +522,12 @@ class WizardSummary extends React.Component<Props> {
             >
               <SourceNetwork>{mapping.source[fieldName]}</SourceNetwork>
               <NetworkArrow />
-              <StorageTarget>{mapping.target ? mapping.target.name : 'Default'}</StorageTarget>
+              <TargetNetwork>
+                <TargetNetworkName>{mapping.target ? mapping.target.name : 'Default'}</TargetNetworkName>
+                {mapping.busType ? (
+                  <TargetNetworkName>Bus Type: {mapping.busType}</TargetNetworkName>
+                ) : null}
+              </TargetNetwork>
             </Row>
           ))}
         </Table>
@@ -544,9 +552,12 @@ class WizardSummary extends React.Component<Props> {
               <NetworkArrow />
               <TargetNetwork>
                 <TargetNetworkName data-test-id="wSummary-networkTarget">{mapping.targetNetwork!.name}</TargetNetworkName>
-                {mapping.targetSecurityGroups && mapping.targetSecurityGroups.length ? (
+                {mapping.targetSecurityGroups?.length ? (
                   <TargetNetworkName>Security Groups: {mapping.targetSecurityGroups.map(s => (typeof s === 'string' ? s : s.name)).join(', ')}</TargetNetworkName>
                 ) : null}
+                {mapping.targetPortKey ? (
+                  <TargetNetworkName>Port Key: {mapping.targetPortKey}</TargetNetworkName>
+                ) : null}
               </TargetNetwork>
             </Row>
           ))}

+ 16 - 3
src/components/pages/MigrationDetailsPage/MigrationDetailsPage.tsx

@@ -41,6 +41,7 @@ import type { Field } from '../../../@types/Field'
 import type { InstanceScript } from '../../../@types/Instance'
 import minionPoolStore from '../../../stores/MinionPoolStore'
 import { getTransferItemTitle } from '../../../@types/MainItem'
+import { providerTypes } from '../../../constants'
 
 const Wrapper = styled.div<any>``
 
@@ -151,13 +152,23 @@ class MigrationDetailsPage extends React.Component<Props, State> {
       minionPoolStore.loadMinionPools()
     }
 
+    await providerStore.loadProviders()
+
+    const targetEndpoint = endpointStore.endpoints
+      .find(e => e.id === details.destination_endpoint_id)
+    const hasStorageMap = targetEndpoint ? (providerStore.providers && providerStore.providers[targetEndpoint.type]
+      ? !!providerStore.providers[targetEndpoint.type]
+        .types.find(t => t === providerTypes.STORAGE)
+      : false) : false
+    if (hasStorageMap) {
+      endpointStore.loadStorage(details.destination_endpoint_id, details.destination_environment)
+    }
+
     networkStore.loadNetworks(details.destination_endpoint_id, details.destination_environment, {
       quietError: true,
       cache,
     })
 
-    const targetEndpoint = endpointStore.endpoints
-      .find(e => e.id === details.destination_endpoint_id)
     instanceStore.loadInstancesDetails({
       endpointId: details.origin_endpoint_id,
       instances: details.instances.map(n => ({ id: n })),
@@ -370,7 +381,9 @@ class MigrationDetailsPage extends React.Component<Props, State> {
               item={migrationStore.migrationDetails}
               itemId={this.props.match.params.id}
               instancesDetails={instanceStore.instancesDetails}
-              instancesDetailsLoading={instanceStore.loadingInstancesDetails}
+              instancesDetailsLoading={instanceStore.loadingInstancesDetails || endpointStore.storageLoading || providerStore.providersLoading}
+              storageBackends={endpointStore.storageBackends}
+              networks={networkStore.networks}
               sourceSchema={providerStore.sourceSchema}
               sourceSchemaLoading={providerStore.sourceSchemaLoading
               || providerStore.sourceOptionsPrimaryLoading

+ 14 - 2
src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.tsx

@@ -180,7 +180,6 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   async loadIsEditable(replicaDetails: ReplicaItemDetails) {
     const targetEndpointId = replicaDetails.destination_endpoint_id
     const sourceEndpointId = replicaDetails.origin_endpoint_id
-    await providerStore.loadProviders()
     await ObjectUtils.waitFor(() => endpointStore.endpoints.length > 0)
     const sourceEndpoint = endpointStore.endpoints.find(e => e.id === sourceEndpointId)
     const targetEndpoint = endpointStore.endpoints.find(e => e.id === targetEndpointId)
@@ -205,7 +204,10 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     }
     minionPoolStore.loadMinionPools()
 
+    await providerStore.loadProviders()
+
     this.loadIsEditable(replica)
+
     networkStore.loadNetworks(replica.destination_endpoint_id, replica.destination_environment, {
       quietError: true,
       cache,
@@ -213,6 +215,15 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
 
     const targetEndpoint = endpointStore.endpoints
       .find(e => e.id === replica.destination_endpoint_id)
+
+    const hasStorageMap = targetEndpoint ? (providerStore.providers && providerStore.providers[targetEndpoint.type]
+      ? !!providerStore.providers[targetEndpoint.type]
+        .types.find(t => t === providerTypes.STORAGE)
+      : false) : false
+    if (hasStorageMap) {
+      endpointStore.loadStorage(replica.destination_endpoint_id, replica.destination_environment)
+    }
+
     instanceStore.loadInstancesDetails({
       endpointId: replica.origin_endpoint_id,
       instances: replica.instances.map(n => ({ id: n })),
@@ -578,8 +589,9 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
               item={replica}
               itemId={this.replicaId}
               instancesDetails={instanceStore.instancesDetails}
-              instancesDetailsLoading={instanceStore.loadingInstancesDetails}
+              instancesDetailsLoading={instanceStore.loadingInstancesDetails || endpointStore.storageLoading || providerStore.providersLoading}
               endpoints={endpointStore.endpoints}
+              storageBackends={endpointStore.storageBackends}
               scheduleStore={scheduleStore}
               networks={networkStore.networks}
               minionPools={minionPoolStore.minionPools}

+ 23 - 23
src/components/pages/WizardPage/WizardPage.tsx

@@ -35,18 +35,18 @@ import replicaStore from '../../../stores/ReplicaStore'
 import KeyboardManager from '../../../utils/KeyboardManager'
 import { wizardPages, executionOptions, providerTypes } from '../../../constants'
 
-import type { Endpoint as EndpointType, StorageBackend } from '../../../@types/Endpoint'
+import type { Endpoint as EndpointType, StorageMap } from '../../../@types/Endpoint'
 import type {
-  Instance, Nic, Disk, InstanceScript,
+  Instance, InstanceScript,
 } from '../../../@types/Instance'
 import type { Field } from '../../../@types/Field'
-import type { Network, SecurityGroup } from '../../../@types/Network'
 import type { Schedule } from '../../../@types/Schedule'
 import type { WizardPage as WizardPageType } from '../../../@types/WizardData'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import { ProviderTypes } from '../../../@types/Providers'
 import { TransferItem, ReplicaItem } from '../../../@types/MainItem'
 import minionPoolStore from '../../../stores/MinionPoolStore'
+import { WizardNetworksChangeObject } from '../../organisms/WizardNetworks/WizardNetworks'
 
 const Wrapper = styled.div<any>``
 
@@ -262,15 +262,13 @@ class WizardPage extends React.Component<Props, State> {
       useCache: true,
     })
     wizardStore.fillWithDefaultValues('source', providerStore.sourceSchema)
+    await this.loadExtraOptions(null, 'source')
   }
 
   async handleTargetEndpointChange(target: EndpointType) {
     wizardStore.updateData({ target, networks: null, destOptions: null })
     wizardStore.clearStorageMap()
     wizardStore.updateUrlState()
-    if (this.pages.find(p => p.id === 'storage')) {
-      endpointStore.loadStorage(target.id, {})
-    }
     // Preload destination options schema
     await providerStore.loadOptionsSchema({
       providerName: target.type,
@@ -286,6 +284,7 @@ class WizardPage extends React.Component<Props, State> {
       useCache: true,
     })
     wizardStore.fillWithDefaultValues('destination', providerStore.destinationSchema)
+    await this.loadExtraOptions(null, 'destination')
   }
 
   handleAddEndpoint(newEndpointType: ProviderTypes, newEndpointFromSource: boolean) {
@@ -363,20 +362,23 @@ class WizardPage extends React.Component<Props, State> {
     wizardStore.updateUrlState()
   }
 
-  handleNetworkChange(
-    sourceNic: Nic, targetNetwork: Network, targetSecurityGroups?: SecurityGroup[] | null,
-  ) {
-    wizardStore.updateNetworks({ sourceNic, targetNetwork, targetSecurityGroups })
+  handleNetworkChange(changeObject: WizardNetworksChangeObject) {
+    wizardStore.updateNetworks({
+      sourceNic: changeObject.nic,
+      targetNetwork: changeObject.network,
+      targetSecurityGroups: changeObject.securityGroups,
+      targetPortKey: changeObject.portKey,
+    })
     wizardStore.updateUrlState()
   }
 
-  handleDefaultStorageChange(value: string | null) {
-    wizardStore.updateDefaultStorage(value)
+  handleDefaultStorageChange(value: string | null, busType?: string | null) {
+    wizardStore.updateDefaultStorage({ value, busType })
     wizardStore.updateUrlState()
   }
 
-  handleStorageChange(source: Disk, target: StorageBackend, type: 'backend' | 'disk') {
-    wizardStore.updateStorage({ source, target, type })
+  handleStorageChange(mapping: StorageMap) {
+    wizardStore.updateStorage(mapping)
     wizardStore.updateUrlState()
   }
 
@@ -506,10 +508,6 @@ class WizardPage extends React.Component<Props, State> {
         if (!target) {
           return
         }
-        // Preload Storage Mapping
-        if (this.pages.find(p => p.id === 'storage')) {
-          endpointStore.loadStorage(target.id, {})
-        }
         // Preload destination options schema
         loadOptions(target, 'destination')
         break
@@ -521,6 +519,8 @@ class WizardPage extends React.Component<Props, State> {
         break
       }
       case 'networks':
+        // Preload storage API calls
+        endpointStore.loadStorage(wizardStore.data.target!.id, wizardStore.data.destOptions)
         this.loadNetworks(true)
         break
       default:
@@ -712,12 +712,12 @@ class WizardPage extends React.Component<Props, State> {
               onSourceOptionsChange={(field, value, parent) => {
                 this.handleSourceOptionsChange(field, value, parent)
               }}
-              onNetworkChange={(sourceNic, targetNetwork, secGroups) => {
-                this.handleNetworkChange(sourceNic, targetNetwork, secGroups)
+              onNetworkChange={(changeObject: WizardNetworksChangeObject) => {
+                this.handleNetworkChange(changeObject)
               }}
-              onDefaultStorageChange={value => { this.handleDefaultStorageChange(value) }}
-              onStorageChange={(source, target, type) => {
-                this.handleStorageChange(source, target, type)
+              onDefaultStorageChange={(d, b) => { this.handleDefaultStorageChange(d, b) }}
+              onStorageChange={mapping => {
+                this.handleStorageChange(mapping)
               }}
               onAddScheduleClick={schedule => { this.handleAddScheduleClick(schedule) }}
               onScheduleChange={(scheduleId, data) => {

+ 34 - 23
src/plugins/endpoint/default/OptionsSchemaPlugin.ts

@@ -193,39 +193,42 @@ export default class OptionsSchemaParser {
 
   static getNetworkMap(networkMappings: NetworkMap[] | null | undefined) {
     const payload: any = {}
-    if (networkMappings && networkMappings.length) {
-      const hasSecurityGroups = Boolean(networkMappings
-        .find(nm => nm.targetNetwork!.security_groups))
-      networkMappings.forEach(mapping => {
-        let target
-        if (hasSecurityGroups) {
-          target = {
-            id: mapping.targetNetwork!.id,
-            security_groups: mapping.targetSecurityGroups
-              ? mapping.targetSecurityGroups.map(s => (typeof s === 'string' ? s : s.id))
-              : [],
-          }
-        } else {
-          target = mapping.targetNetwork!.id
-        }
-        payload[mapping.sourceNic.network_name] = target
-      })
+    if (!networkMappings?.length) {
+      return payload
     }
+    const hasSecurityGroups = Boolean(networkMappings.find(nm => nm.targetNetwork!.security_groups))
+    networkMappings.forEach(mapping => {
+      let target
+      if (hasSecurityGroups) {
+        target = {
+          id: mapping.targetNetwork!.id,
+          security_groups: mapping.targetSecurityGroups ? mapping.targetSecurityGroups.map(s => (typeof s === 'string' ? s : s.id)) : [],
+        }
+      } else if (mapping.targetPortKey != null) {
+        target = `${mapping.targetNetwork!.id}:${mapping.targetPortKey}`
+      } else {
+        target = mapping.targetNetwork!.id
+      }
+      payload[mapping.sourceNic.network_name] = target
+    })
     return payload
   }
 
   static getStorageMap(
-    defaultStorage: string | null | undefined,
+    defaultStorage: { value: string | null, busType?: string | null } | undefined,
     storageMap: StorageMap[] | null,
     configDefault?: string | null,
   ) {
-    if (!defaultStorage && !storageMap) {
+    if (!defaultStorage?.value && !storageMap) {
       return null
     }
 
     const payload: any = {}
-    if (defaultStorage) {
-      payload.default = defaultStorage
+    if (defaultStorage?.value) {
+      payload.default = defaultStorage.value
+      if (defaultStorage.busType) {
+        payload.default += `:${defaultStorage.busType}`
+      }
     }
 
     if (!storageMap) {
@@ -237,13 +240,21 @@ export default class OptionsSchemaParser {
         return
       }
 
+      const getDestination = () => {
+        let destination = mapping.target.id === null ? configDefault : mapping.target.name
+        if (mapping.targetBusType) {
+          destination += `:${mapping.targetBusType}`
+        }
+        return destination
+      }
+
       if (mapping.type === 'backend') {
         if (!payload.backend_mappings) {
           payload.backend_mappings = []
         }
         payload.backend_mappings.push({
           source: mapping.source.storage_backend_identifier,
-          destination: mapping.target.id === null ? configDefault : mapping.target.name,
+          destination: getDestination(),
         })
       } else {
         if (!payload.disk_mappings) {
@@ -251,7 +262,7 @@ export default class OptionsSchemaParser {
         }
         payload.disk_mappings.push({
           disk_id: mapping.source.id.toString(),
-          destination: mapping.target.id === null ? configDefault : mapping.target.name,
+          destination: getDestination(),
         })
       }
     })

+ 2 - 0
src/plugins/endpoint/index.ts

@@ -24,6 +24,7 @@ import OpenstackContentPlugin from './openstack/ContentPlugin'
 
 import DefaultOptionsSchemaPlugin from './default/OptionsSchemaPlugin'
 import OvmOptionsSchemaPlugin from './ovm/OptionsSchemaPlugin'
+import VmwareOptionsSchemaPlugin from './vmware_vsphere/OptionsSchemaPlugin'
 import OpenstackOptionsSchemaPlugin from './openstack/OptionsSchemaPlugin'
 
 import DefaultInstanceInfoPlugin from './default/InstanceInfoPlugin'
@@ -54,6 +55,7 @@ export const OptionsSchemaPlugin = {
       default: DefaultOptionsSchemaPlugin,
       oracle_vm: OvmOptionsSchemaPlugin,
       openstack: OpenstackOptionsSchemaPlugin,
+      vmware_vsphere: VmwareOptionsSchemaPlugin,
     }
     if (hasKey(map, provider)) {
       return map[provider]

+ 1 - 1
src/plugins/endpoint/openstack/OptionsSchemaPlugin.ts

@@ -96,7 +96,7 @@ export default class OptionsSchemaParser {
   }
 
   static getStorageMap(
-    defaultStorage: string | null,
+    defaultStorage: { value: string | null, busType?: string | null },
     storageMap: StorageMap[] | null,
     configDefault?: string | null,
   ) {

+ 2 - 2
src/plugins/endpoint/ovm/OptionsSchemaPlugin.ts

@@ -89,12 +89,12 @@ export default class OptionsSchemaParser {
     return env
   }
 
-  static getNetworkMap(networkMappings: NetworkMap[] | null) {
+  static getNetworkMap(networkMappings: NetworkMap[] | null | undefined) {
     return DefaultOptionsSchemaPlugin.getNetworkMap(networkMappings)
   }
 
   static getStorageMap(
-    defaultStorage: string | null,
+    defaultStorage: { value: string | null, busType?: string | null },
     storageMap: StorageMap[] | null,
     configDefault?: string | null,
   ) {

+ 112 - 0
src/plugins/endpoint/vmware_vsphere/OptionsSchemaPlugin.ts

@@ -0,0 +1,112 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import DefaultOptionsSchemaPlugin, {
+  defaultFillMigrationImageMapValues,
+  defaultFillFieldValues,
+  defaultGetDestinationEnv,
+  defaultGetMigrationImageMap,
+} from '../default/OptionsSchemaPlugin'
+
+import type { InstanceScript } from '../../../@types/Instance'
+import type { Field } from '../../../@types/Field'
+import type { OptionValues, StorageMap } from '../../../@types/Endpoint'
+import type { SchemaProperties, SchemaDefinitions } from '../../../@types/Schema'
+import type { NetworkMap } from '../../../@types/Network'
+import { UserScriptData } from '../../../@types/MainItem'
+
+export default class OptionsSchemaParser {
+  static migrationImageMapFieldName = 'migr_template_name_map'
+
+  static parseSchemaToFields(
+    schema: SchemaProperties,
+    schemaDefinitions: SchemaDefinitions | null | undefined,
+    dictionaryKey: string,
+  ) {
+    const fields = DefaultOptionsSchemaPlugin
+      .parseSchemaToFields(schema, schemaDefinitions, dictionaryKey)
+    fields.forEach(f => {
+      if (
+        f.name !== 'migr_template_username_map'
+        && f.name !== 'migr_template_password_map'
+        && f.name !== 'migr_template_name_map'
+      ) {
+        return
+      }
+
+      const password = f.name === 'migr_template_password_map'
+      f.properties = [
+        {
+          type: 'string',
+          name: 'windows',
+          password,
+        },
+        {
+          type: 'string',
+          name: 'linux',
+          password,
+        },
+      ]
+    })
+
+    return fields
+  }
+
+  static fillFieldValues(field: Field, options: OptionValues[]) {
+    const option = options.find(f => f.name === field.name)
+    if (!option) {
+      return
+    }
+    if (!defaultFillMigrationImageMapValues(
+      field,
+      option,
+      this.migrationImageMapFieldName,
+    )) {
+      defaultFillFieldValues(field, option)
+    }
+  }
+
+  static getDestinationEnv(options: { [prop: string]: any } | null, oldOptions?: any) {
+    const env = {
+      ...defaultGetDestinationEnv(options, oldOptions),
+      ...defaultGetMigrationImageMap(
+        options,
+        oldOptions,
+        this.migrationImageMapFieldName,
+      ),
+    }
+    return env
+  }
+
+  static getNetworkMap(networkMappings: NetworkMap[] | null | undefined) {
+    return DefaultOptionsSchemaPlugin.getNetworkMap(networkMappings)
+  }
+
+  static getStorageMap(
+    defaultStorage: { value: string | null, busType?: string | null },
+    storageMap: StorageMap[] | null,
+    configDefault?: string | null,
+  ) {
+    return DefaultOptionsSchemaPlugin.getStorageMap(defaultStorage, storageMap, configDefault)
+  }
+
+  static getUserScripts(
+    uploadedUserScripts: InstanceScript[],
+    removedUserScripts: InstanceScript[],
+    userScriptData: UserScriptData | null | undefined,
+  ) {
+    return DefaultOptionsSchemaPlugin
+      .getUserScripts(uploadedUserScripts, removedUserScripts, userScriptData)
+  }
+}

+ 2 - 2
src/sources/MigrationSource.ts

@@ -127,8 +127,8 @@ class MigrationSource {
     updatedSourceEnv?: { [prop: string]: any } | null,
     storageMappings?: { [prop: string]: any } | null,
     updatedStorageMappings: StorageMap[] | null,
-    defaultStorage?: string | null,
-    updatedDefaultStorage?: string | null,
+    defaultStorage?: { value: string | null, busType?: string | null },
+    updatedDefaultStorage?: { value: string | null, busType?: string | null },
     networkMappings?: any,
     updatedNetworkMappings: NetworkMap[] | null,
     defaultSkipOsMorphing: boolean | null,

+ 1 - 1
src/sources/ReplicaSource.ts

@@ -214,7 +214,7 @@ class ReplicaSource {
     replica: ReplicaItemDetails,
     destinationEndpoint: Endpoint,
     updateData: UpdateData,
-    defaultStorage: string | null | undefined,
+    defaultStorage: { value: string | null, busType?: string | null },
     storageConfigDefault: string,
   }): Promise<Execution> {
     const {

+ 2 - 2
src/sources/WizardSource.ts

@@ -29,7 +29,7 @@ class WizardSource {
   async create(
     type: string,
     data: WizardData,
-    defaultStorage: string | null,
+    defaultStorage: { value: string | null, busType?: string | null } | undefined,
     storageMap: StorageMap[],
     uploadedUserScripts: InstanceScript[],
   ): Promise<TransferItem> {
@@ -110,7 +110,7 @@ class WizardSource {
   async createMultiple(
     type: string,
     data: WizardData,
-    defaultStorage: string | null,
+    defaultStorage: { value: string | null, busType?: string | null } | undefined,
     storageMap: StorageMap[],
     uploadedUserScripts: InstanceScript[],
   ) {

+ 2 - 2
src/stores/MigrationStore.ts

@@ -68,8 +68,8 @@ class MigrationStore {
     sourceEndpoint: Endpoint,
     destEndpoint: Endpoint,
     updateData: UpdateData,
-    defaultStorage: string | null | undefined,
-    updatedDefaultStorage: string | null | undefined,
+    defaultStorage: { value: string | null, busType?: string | null },
+    updatedDefaultStorage: { value: string | null, busType?: string | null } | undefined,
     replicationCount: number | null | undefined,
   ): Promise<MigrationItemDetails> {
     const migrationResult = await MigrationSource.recreate({

+ 22 - 8
src/stores/ProviderStore.ts

@@ -45,13 +45,15 @@ export const getFieldChangeOptions = (config: {
   }
   const requiredFields = providerWithEnvOptions.requiredFields
   const requiredValues = providerWithEnvOptions.requiredValues
+  const relistFields = providerWithEnvOptions.relistFields
 
   const findFieldInSchema = (name: string) => schema.find(f => f.name === name)
 
-  const validFields = requiredFields.filter(fn => {
+  const filterValidField = (fn: string) => {
     const schemaField = findFieldInSchema(fn)
     if (data) {
       // This is for 'list_all_networks' field, which requires options calls after each value change
+      // @TODO: refactor to use `relistFields` option
       if (schemaField && schemaField.type === 'boolean') {
         return true
       }
@@ -59,7 +61,7 @@ export const getFieldChangeOptions = (config: {
         return false
       }
       const defaultValue = data[fn] === undefined && schemaField && schemaField.default
-      const requiredValue = requiredValues && requiredValues.find(f => f.field === fn)
+      const requiredValue = requiredValues?.find(f => f.field === fn)
       if (defaultValue != null) {
         if (requiredValue) {
           return Boolean(requiredValue.values.find(v => v === defaultValue))
@@ -72,16 +74,23 @@ export const getFieldChangeOptions = (config: {
       return data[fn]
     }
     return false
-  })
+  }
+
+  const requiredValidFields = requiredFields.filter(filterValidField)
+  const relistValidFields = relistFields?.filter(filterValidField)
+
+  const relistField = relistFields?.find(fn => fn === field?.name)
 
-  const isCurrentFieldValid = field
-    ? validFields.find(fn => (field ? fn === field.name : false)) : true
-  if (validFields.length !== requiredFields.length || !isCurrentFieldValid) {
+  const isCurrentFieldValid = field ? (
+    requiredValidFields.find(fn => fn === field.name)
+    || relistField
+  ) : true
+  if (requiredValidFields.length !== requiredFields.length || !isCurrentFieldValid) {
     return null
   }
 
   const envData: any = {}
-  validFields.forEach(fn => {
+  const setEnvDataValue = (fn: string) => {
     envData[fn] = data ? data[fn] : null
     if (envData[fn] == null) {
       const schemaField = findFieldInSchema(fn)
@@ -89,8 +98,13 @@ export const getFieldChangeOptions = (config: {
         envData[fn] = schemaField.default
       }
     }
+  }
+  requiredValidFields.forEach(fn => {
+    setEnvDataValue(fn)
+  })
+  relistValidFields?.forEach(fn => {
+    setEnvDataValue(fn)
   })
-
   return envData
 }
 

+ 1 - 1
src/stores/ReplicaStore.ts

@@ -235,7 +235,7 @@ class ReplicaStore {
     replica: ReplicaItemDetails,
     destinationEndpoint: Endpoint,
     updateData: UpdateData,
-    defaultStorage: string | null | undefined,
+    defaultStorage: { value: string | null, busType?: string | null },
     storageConfigDefault: string,
   }) {
     await ReplicaSource.update(options)

+ 6 - 6
src/stores/WizardStore.ts

@@ -68,7 +68,7 @@ class WizardStore {
 
   @observable schedules: Schedule[] = []
 
-  @observable defaultStorage: string | null = null
+  @observable defaultStorage: { value: string | null, busType?: string | null } | undefined
 
   @observable storageMap: StorageMap[] = []
 
@@ -186,8 +186,8 @@ class WizardStore {
     this.data.networks.push(network)
   }
 
-  @action updateDefaultStorage(value: string | null) {
-    this.defaultStorage = value
+  @action updateDefaultStorage(defaultStorage: { value: string | null, busType?: string | null }) {
+    this.defaultStorage = defaultStorage
   }
 
   @action updateStorage(storage: StorageMap) {
@@ -200,7 +200,7 @@ class WizardStore {
 
   @action clearStorageMap() {
     this.storageMap = []
-    this.defaultStorage = null
+    this.defaultStorage = undefined
   }
 
   @action addSchedule(schedule: Schedule) {
@@ -236,7 +236,7 @@ class WizardStore {
   @action async create(
     type: string,
     data: WizardData,
-    defaultStorage: string | null,
+    defaultStorage: { value: string | null, busType?: string | null } | undefined,
     storageMap: StorageMap[],
     uploadedUserScripts: InstanceScript[],
   ): Promise<void> {
@@ -255,7 +255,7 @@ class WizardStore {
   @action async createMultiple(
     type: string,
     data: WizardData,
-    defaultStorage: string | null,
+    defaultStorage: { value: string | null, busType?: string | null } | undefined,
     storageMap: StorageMap[],
     uploadedUserScripts: InstanceScript[],
   ): Promise<boolean> {