Procházet zdrojové kódy

Merge pull request #618 from smiclea/vmware-target

Add VMWare plugin target support CORWEB-254
Nashwan Azhari před 5 roky
rodič
revize
f835a5d478
33 změnil soubory, kde provedl 711 přidání a 235 odebrání
  1. 16 0
      config.ts
  2. 15 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. 18 4
      src/components/organisms/ReplicaMigrationOptions/ReplicaMigrationOptions.tsx
  12. 50 26
      src/components/organisms/WizardNetworks/WizardNetworks.tsx
  13. 0 1
      src/components/organisms/WizardOptions/WizardOptions.tsx
  14. 26 11
      src/components/organisms/WizardPageContent/WizardPageContent.tsx
  15. 115 27
      src/components/organisms/WizardStorage/WizardStorage.tsx
  16. 2 4
      src/components/organisms/WizardStorage/story.tsx
  17. 27 16
      src/components/organisms/WizardSummary/WizardSummary.tsx
  18. 18 3
      src/components/pages/MigrationDetailsPage/MigrationDetailsPage.tsx
  19. 16 2
      src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.tsx
  20. 10 0
      src/components/pages/ReplicasPage/ReplicasPage.tsx
  21. 23 23
      src/components/pages/WizardPage/WizardPage.tsx
  22. 34 23
      src/plugins/endpoint/default/OptionsSchemaPlugin.ts
  23. 2 0
      src/plugins/endpoint/index.ts
  24. 1 1
      src/plugins/endpoint/openstack/OptionsSchemaPlugin.ts
  25. 2 2
      src/plugins/endpoint/ovm/OptionsSchemaPlugin.ts
  26. 112 0
      src/plugins/endpoint/vmware_vsphere/OptionsSchemaPlugin.ts
  27. 2 2
      src/sources/MigrationSource.ts
  28. 1 1
      src/sources/ReplicaSource.ts
  29. 2 2
      src/sources/WizardSource.ts
  30. 11 2
      src/stores/MigrationStore.ts
  31. 22 8
      src/stores/ProviderStore.ts
  32. 1 1
      src/stores/ReplicaStore.ts
  33. 6 6
      src/stores/WizardStore.ts

+ 16 - 0
config.ts

@@ -44,6 +44,7 @@ const conf: Config = {
    * If `requiredValues` is provided, the field specified there needs to have a
    * If `requiredValues` is provided, the field specified there needs to have a
    * certain value (specified in values)
    * certain value (specified in values)
    * in order to make the options API call.
    * 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: [
   extraOptionsApiCalls: [
     {
     {
@@ -77,8 +78,23 @@ const conf: Config = {
       types: ['destination'],
       types: ['destination'],
       requiredFields: ['compartment', 'availability_domain', 'vcn_compartment'],
       requiredFields: ['compartment', 'availability_domain', 'vcn_compartment'],
     },
     },
+    {
+      name: 'vmware_vsphere',
+      types: ['destination'],
+      requiredFields: ['import_datacenter'],
+      relistFields: ['import_cluster', 'migr_minion_cluster'],
+    },
   ],
   ],
 
 
+  providerMigrationOptions: {
+    vmware_vsphere: {
+      cloneDiskDisabledOptions: {
+        defaultValue: false,
+        description: 'Replica Deployments on VMware do not currently support Replica disk cloning',
+      },
+    },
+  },
+
   /*
   /*
   Lower number means that the provider will appear sooner in the list.
   Lower number means that the provider will appear sooner in the list.
   Equal number means alphabetical order within the same group number.
   Equal number means alphabetical order within the same group number.

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

@@ -1,9 +1,12 @@
+import { ProviderTypes } from './Providers'
+
 type Type = 'source' | 'destination'
 type Type = 'source' | 'destination'
 
 
 type ExtraOption = {
 type ExtraOption = {
   name: string,
   name: string,
   types: Type[],
   types: Type[],
   requiredFields: string[],
   requiredFields: string[],
+  relistFields?: string[],
   requiredValues?: {
   requiredValues?: {
     field: string,
     field: string,
     values: string[],
     values: string[],
@@ -19,6 +22,17 @@ export type Services = {
   coriolisLicensing: string,
   coriolisLicensing: string,
 }
 }
 
 
+export type ProviderMigrationCloneDiskDisabledOption = {
+  defaultValue: boolean
+  description: string
+}
+
+export type ProviderMigrationOptions = {
+  [provider in ProviderTypes]?: {
+    cloneDiskDisabledOptions: ProviderMigrationCloneDiskDisabledOption
+  }
+}
+
 export type Config = {
 export type Config = {
   disabledPages: string[],
   disabledPages: string[],
   showUserDomainInput: boolean,
   showUserDomainInput: boolean,
@@ -35,4 +49,5 @@ export type Config = {
   mainListItemsPerPage: number,
   mainListItemsPerPage: number,
   servicesUrls: Services,
   servicesUrls: Services,
   maxMinionPoolEventsPerPage: number,
   maxMinionPoolEventsPerPage: number,
+  providerMigrationOptions: ProviderMigrationOptions
 }
 }

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

@@ -48,8 +48,11 @@ export type OptionValues = {
 }
 }
 
 
 export type StorageBackend = {
 export type StorageBackend = {
-  id: string,
+  id: string | null,
   name: string,
   name: string,
+  additional_provider_properties?: {
+    supported_bus_types?: string[]
+  }
 }
 }
 
 
 export type Storage = {
 export type Storage = {
@@ -61,4 +64,27 @@ export type StorageMap = {
   type: 'backend' | 'disk',
   type: 'backend' | 'disk',
   source: Disk,
   source: Disk,
   target: StorageBackend,
   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,
   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 = {
 export type Network = {
   name: string,
   name: string,
   id: string,
   id: string,
+  // The `security_groups` field is currently used only by OCI
   security_groups?: SecurityGroup[],
   security_groups?: SecurityGroup[],
+  // The `port_keys` field is currenlty used only by VMWare
+  port_keys?: string[],
 }
 }
 
 
 export type NetworkMap = {
 export type NetworkMap = {
   sourceNic: Nic,
   sourceNic: Nic,
   targetNetwork: Network | null,
   targetNetwork: Network | null,
   targetSecurityGroups?: SecurityGroup[] | 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,
   isNetworkMapSourceDest, TransferItem,
 } from '../../../@types/MainItem'
 } from '../../../@types/MainItem'
 import type { Instance, Nic, Disk } from '../../../@types/Instance'
 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 instanceIcon from './images/instance.svg'
 import networkIcon from './images/network.svg'
 import networkIcon from './images/network.svg'
 import storageIcon from './images/storage.svg'
 import storageIcon from './images/storage.svg'
 import arrowIcon from './images/arrow.svg'
 import arrowIcon from './images/arrow.svg'
 import { MinionPool } from '../../../@types/MinionPool'
 import { MinionPool } from '../../../@types/MinionPool'
+import { EndpointUtils, StorageBackend } from '../../../@types/Endpoint'
 
 
 const GlobalStyle = createGlobalStyle`
 const GlobalStyle = createGlobalStyle`
   .ReactCollapse--collapse {
   .ReactCollapse--collapse {
@@ -153,6 +154,7 @@ export type Props = {
   instancesDetails: Instance[],
   instancesDetails: Instance[],
   networks?: Network[],
   networks?: Network[],
   minionPools: MinionPool[]
   minionPools: MinionPool[]
+  storageBackends: StorageBackend[]
 }
 }
 type State = {
 type State = {
   openedRows: string[],
   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 transferResult = this.getTransferResult(instance)
     const rows: React.ReactNode[] = []
     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 => {
     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 destinationName: React.ReactNode
       let destinationKey: string
       let destinationKey: string
+      const defaultBusTypeInfo = EndpointUtils.getBusTypeStorageId(this.props.storageBackends, this.props.item?.storage_mappings?.default || null)
 
 
       if (disk.disabled) {
       if (disk.disabled) {
         destinationKey = disk.disabled.info || disk.disabled.message
         destinationKey = disk.disabled.info || disk.disabled.message
         destinationName = <span style={{ color: Palette.grayscale[5] }}>{destinationKey}</span>
         destinationName = <span style={{ color: Palette.grayscale[5] }}>{destinationKey}</span>
       } else {
       } 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
         destinationKey = destinationName as string
       }
       }
+      let destinationBody: string[] = []
 
 
       if (mappedDisk) {
       if (mappedDisk) {
-        destinationName = mappedDisk.destination
+        const busTypeInfo = EndpointUtils.getBusTypeStorageId(this.props.storageBackends, mappedDisk?.destination)
+
+        destinationName = busTypeInfo.id
         destinationKey = destinationName as string
         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 getBody = (d: Disk): string[] => {
         const body: string[] = []
         const body: string[] = []
@@ -271,14 +283,14 @@ class MainDetailsTable extends React.Component<Props, State> {
         return body
         return body
       }
       }
       const sourceBody = getBody(disk)
       const sourceBody = getBody(disk)
-      let destinationBody: string[] = []
+
       if (transferResult) {
       if (transferResult) {
         const transferDisk = transferResult.devices.disks
         const transferDisk = transferResult.devices.disks
           .find(d => d.storage_backend_identifier === destinationName)
           .find(d => d.storage_backend_identifier === destinationName)
         if (transferDisk) {
         if (transferDisk) {
           destinationName = transferDisk.name || transferDisk.id
           destinationName = transferDisk.name || transferDisk.id
           destinationKey = destinationName as string
           destinationKey = destinationName as string
-          destinationBody = getBody(transferDisk)
+          destinationBody = destinationBody.concat(getBody(transferDisk))
         }
         }
       } else if (this.props.item?.type === 'migration' && (
       } else if (this.props.item?.type === 'migration' && (
         this.props.item.last_execution_status === 'RUNNING'
         this.props.item.last_execution_status === 'RUNNING'
@@ -327,18 +339,14 @@ class MainDetailsTable extends React.Component<Props, State> {
           return body
           return body
         }
         }
         const destNetMapObj = destinationNetworkMap[nic.network_name]
         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)
         const sourceBody = getBody(nic)
 
 
         let destinationBody: string[] = []
         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 secNames = destNetMapObj.security_groups.map(s => {
             const foundSecGroupInfo = destSecGroupsInfo.find(si => (typeof si === 'string' ? si === s : si.id === s))
             const foundSecGroupInfo = destSecGroupsInfo.find(si => (typeof si === 'string' ? si === s : si.id === s))
             return foundSecGroupInfo && typeof foundSecGroupInfo !== 'string' && foundSecGroupInfo.name ? foundSecGroupInfo.name : 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(', ')}`]
           destinationBody = [`Security Groups: ${secNames.join(', ')}`]
         }
         }
 
 
+        if (portKeyInfo.portKey != null) {
+          destinationBody = [`Port Key: ${portKeyInfo.portKey}`]
+        }
+
         let destinationNetworkName = destinationNetworkId
         let destinationNetworkName = destinationNetworkId
         if (destinationNetwork) {
         if (destinationNetwork) {
           destinationNetworkName = destinationNetwork.name
           destinationNetworkName = destinationNetwork.name
@@ -438,7 +450,8 @@ class MainDetailsTable extends React.Component<Props, State> {
             <InstanceBody>
             <InstanceBody>
               {this.renderInstanceDetails(instance)}
               {this.renderInstanceDetails(instance)}
               {this.renderNetworks(instance)}
               {this.renderNetworks(instance)}
-              {this.renderStorage(instance)}
+              {this.renderStorage(instance, 'disk')}
+              {this.renderStorage(instance, 'backend')}
             </InstanceBody>
             </InstanceBody>
           </InstanceInfo>
           </InstanceInfo>
         ))}
         ))}

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

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

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

@@ -23,10 +23,11 @@ import Tasks from '../Tasks'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
 
 
 import type { Instance } from '../../../@types/Instance'
 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 type { Field } from '../../../@types/Field'
 import { MigrationItemDetails } from '../../../@types/MainItem'
 import { MigrationItemDetails } from '../../../@types/MainItem'
 import { MinionPool } from '../../../@types/MinionPool'
 import { MinionPool } from '../../../@types/MinionPool'
+import { Network } from '../../../@types/Network'
 
 
 const Wrapper = styled.div<any>`
 const Wrapper = styled.div<any>`
   display: flex;
   display: flex;
@@ -58,8 +59,10 @@ type Props = {
   itemId: string
   itemId: string
   minionPools: MinionPool[]
   minionPools: MinionPool[]
   detailsLoading: boolean,
   detailsLoading: boolean,
+  storageBackends: StorageBackend[]
   instancesDetails: Instance[],
   instancesDetails: Instance[],
   instancesDetailsLoading: boolean,
   instancesDetailsLoading: boolean,
+  networks: Network[],
   sourceSchema: Field[],
   sourceSchema: Field[],
   sourceSchemaLoading: boolean,
   sourceSchemaLoading: boolean,
   destinationSchema: Field[],
   destinationSchema: Field[],
@@ -91,9 +94,11 @@ class MigrationDetailsContent extends React.Component<Props> {
     return (
     return (
       <MainDetails
       <MainDetails
         item={this.props.item}
         item={this.props.item}
+        storageBackends={this.props.storageBackends}
         minionPools={this.props.minionPools}
         minionPools={this.props.minionPools}
         instancesDetails={this.props.instancesDetails}
         instancesDetails={this.props.instancesDetails}
         instancesDetailsLoading={this.props.instancesDetailsLoading}
         instancesDetailsLoading={this.props.instancesDetailsLoading}
+        networks={this.props.networks}
         sourceSchema={this.props.sourceSchema}
         sourceSchema={this.props.sourceSchema}
         sourceSchemaLoading={this.props.sourceSchemaLoading}
         sourceSchemaLoading={this.props.sourceSchemaLoading}
         destinationSchema={this.props.destinationSchema}
         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 Executions from '../Executions'
 import Schedule from '../Schedule'
 import Schedule from '../Schedule'
 import type { Instance } from '../../../@types/Instance'
 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 { Execution, ExecutionTasks } from '../../../@types/Execution'
 import type { Network } from '../../../@types/Network'
 import type { Network } from '../../../@types/Network'
 import type { Field } from '../../../@types/Field'
 import type { Field } from '../../../@types/Field'
@@ -89,6 +89,7 @@ type Props = {
   executionsTasksLoading: boolean,
   executionsTasksLoading: boolean,
   executionsTasks: ExecutionTasks[],
   executionsTasks: ExecutionTasks[],
   minionPools: MinionPool[]
   minionPools: MinionPool[]
+  storageBackends: StorageBackend[]
   onExecutionChange: (executionId: string) => void,
   onExecutionChange: (executionId: string) => void,
   onCancelExecutionClick: (execution: Execution | null, force?: boolean) => void,
   onCancelExecutionClick: (execution: Execution | null, force?: boolean) => void,
   onDeleteExecutionClick: (execution: Execution | null) => void,
   onDeleteExecutionClick: (execution: Execution | null) => void,
@@ -171,6 +172,7 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
     return (
     return (
       <MainDetails
       <MainDetails
         item={this.props.item}
         item={this.props.item}
+        storageBackends={this.props.storageBackends}
         minionPools={this.props.minionPools}
         minionPools={this.props.minionPools}
         sourceSchema={this.props.sourceSchema}
         sourceSchema={this.props.sourceSchema}
         sourceSchemaLoading={this.props.sourceSchemaLoading}
         sourceSchemaLoading={this.props.sourceSchemaLoading}

+ 18 - 4
src/components/organisms/ReplicaMigrationOptions/ReplicaMigrationOptions.tsx

@@ -33,6 +33,7 @@ import type { Instance, InstanceScript } from '../../../@types/Instance'
 import { TransferItemDetails } from '../../../@types/MainItem'
 import { TransferItemDetails } from '../../../@types/MainItem'
 import { MinionPool } from '../../../@types/MinionPool'
 import { MinionPool } from '../../../@types/MinionPool'
 import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from '../WizardOptions/WizardOptions'
 import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from '../WizardOptions/WizardOptions'
+import { ProviderMigrationCloneDiskDisabledOption } from '../../../@types/Config'
 
 
 const Wrapper = styled.div<any>`
 const Wrapper = styled.div<any>`
   display: flex;
   display: flex;
@@ -83,6 +84,7 @@ type Props = {
   minionPools: MinionPool[]
   minionPools: MinionPool[]
   loadingInstances: boolean,
   loadingInstances: boolean,
   defaultSkipOsMorphing?: boolean | null,
   defaultSkipOsMorphing?: boolean | null,
+  disabledCloneDisk?: ProviderMigrationCloneDiskDisabledOption | null,
   onCancelClick: () => void,
   onCancelClick: () => void,
   onMigrateClick: (
   onMigrateClick: (
     fields: Field[],
     fields: Field[],
@@ -116,9 +118,20 @@ class ReplicaMigrationOptions extends React.Component<Props, State> {
     const mappings = this.props.transferItem?.instance_osmorphing_minion_pool_mappings || {}
     const mappings = this.props.transferItem?.instance_osmorphing_minion_pool_mappings || {}
 
 
     this.setState({
     this.setState({
-      fields: replicaMigrationFields.map(f => (f.name === 'skip_os_morphing' ? (
-        { ...f, value: this.props.defaultSkipOsMorphing || null }
-      ) : f)),
+      fields: replicaMigrationFields.map(f => {
+        if (f.name === 'skip_os_morphing') {
+          return { ...f, value: this.props.defaultSkipOsMorphing || null }
+        }
+        if (f.name === 'clone_disks' && this.props.disabledCloneDisk) {
+          return {
+            ...f,
+            disabled: true,
+            value: this.props.disabledCloneDisk.defaultValue,
+            description: this.props.disabledCloneDisk.description,
+          }
+        }
+        return f
+      }),
       minionPoolMappings: { ...mappings },
       minionPoolMappings: { ...mappings },
     })
     })
   }
   }
@@ -200,7 +213,8 @@ class ReplicaMigrationOptions extends React.Component<Props, State> {
         layout="page"
         layout="page"
         label={field.label || LabelDictionary.get(field.name)}
         label={field.label || LabelDictionary.get(field.name)}
         onChange={value => this.handleValueChange(field, value)}
         onChange={value => this.handleValueChange(field, value)}
-        description={LabelDictionary.getDescription(field.name)}
+        description={field.description || LabelDictionary.getDescription(field.name)}
+        disabled={field.disabled}
       />
       />
     )
     )
   }
   }

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

@@ -22,7 +22,7 @@ import Dropdown from '../../molecules/Dropdown'
 
 
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 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 type { Network, NetworkMap, SecurityGroup } from '../../../@types/Network'
 
 
 import networkImage from './images/network.svg'
 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 = {
 type Props = {
   loading: boolean,
   loading: boolean,
   loadingInstancesDetails: boolean,
   loadingInstancesDetails: boolean,
   networks: Network[],
   networks: Network[],
   instancesDetails: Instance[],
   instancesDetails: Instance[],
   selectedNetworks?: NetworkMap[] | null,
   selectedNetworks?: NetworkMap[] | null,
-  onChange: (nic: NicType, network: Network, securityGroups?: SecurityGroup[]) => void,
+  onChange: (changeObject: WizardNetworksChangeObject) => void,
   style?: any,
   style?: any,
   titleWidth?: number,
   titleWidth?: number,
 }
 }
@@ -158,9 +163,9 @@ class WizardNetworks extends React.Component<Props> {
     return this.props.networks.length > 10 ? (
     return this.props.networks.length > 10 ? (
       <AutocompleteDropdown
       <AutocompleteDropdown
         width={StyleProps.inputSizes.large.width}
         width={StyleProps.inputSizes.large.width}
-        selectedItem={selectedNetwork ? selectedNetwork.targetNetwork : null}
+        selectedItem={selectedNetwork?.targetNetwork || null}
         items={this.props.networks}
         items={this.props.networks}
-        onChange={(item: Network) => { this.props.onChange(nic, item) }}
+        onChange={(network: Network) => { this.props.onChange({ nic, network }) }}
         labelField="name"
         labelField="name"
         valueField="id"
         valueField="id"
       />
       />
@@ -171,26 +176,20 @@ class WizardNetworks extends React.Component<Props> {
           centered
           centered
           noSelectionMessage="Select Network"
           noSelectionMessage="Select Network"
           noItemsMessage={this.props.loading ? 'Loading ...' : 'No networks found'}
           noItemsMessage={this.props.loading ? 'Loading ...' : 'No networks found'}
-          selectedItem={selectedNetwork ? selectedNetwork.targetNetwork : null}
+          selectedItem={selectedNetwork?.targetNetwork || null}
           items={this.props.networks}
           items={this.props.networks}
           labelField="name"
           labelField="name"
           valueField="id"
           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) {
   renderSecGroupsDropdown(selectedNetwork: NetworkMap | null | undefined, nic: NicType) {
     const MAX_SELECTED_GROUPS = 5
     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 ? (
     return hasSecurityGroups && this.props.networks.length ? (
       <Dropdown
       <Dropdown
         width={StyleProps.inputSizes.large.width}
         width={StyleProps.inputSizes.large.width}
@@ -204,10 +203,8 @@ class WizardNetworks extends React.Component<Props> {
         labelField="name"
         labelField="name"
         valueField="id"
         valueField="id"
         onChange={(item: any) => {
         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 {
           } else {
             selectedSecGroups = [...selectedSecGroups, item]
             selectedSecGroups = [...selectedSecGroups, item]
           }
           }
@@ -217,19 +214,46 @@ class WizardNetworks extends React.Component<Props> {
           if (selectedSecGroups.length > MAX_SELECTED_GROUPS) {
           if (selectedSecGroups.length > MAX_SELECTED_GROUPS) {
             selectedSecGroups.splice(MAX_SELECTED_GROUPS - 1, 1)
             selectedSecGroups.splice(MAX_SELECTED_GROUPS - 1, 1)
           }
           }
-          this.props.onChange(nic, selectedNetwork.targetNetwork!, selectedSecGroups)
+          this.props.onChange({ nic, network: selectedNetwork.targetNetwork!, securityGroups: selectedSecGroups })
         }}
         }}
       />
       />
     ) : null
     ) : 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() {
   renderNics() {
     if (this.isLoading()) {
     if (this.isLoading()) {
       return null
       return null
     }
     }
     let nics: NicType[] = []
     let nics: NicType[] = []
     this.props.instancesDetails.forEach(instance => {
     this.props.instancesDetails.forEach(instance => {
-      if (!instance.devices || !instance.devices.nics) {
+      if (!instance.devices?.nics) {
         return
         return
       }
       }
       instance.devices.nics.forEach(nic => {
       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)
       nics = this.props.selectedNetworks.map(n => n.sourceNic)
     }
     }
 
 
@@ -259,10 +283,9 @@ class WizardNetworks extends React.Component<Props> {
               return true
               return true
             }
             }
             return false
             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 (
           return (
             <Nic key={nic.id} data-test-id="networkItem">
             <Nic key={nic.id} data-test-id="networkItem">
               <NetworkImage />
               <NetworkImage />
@@ -278,6 +301,7 @@ class WizardNetworks extends React.Component<Props> {
               <Dropdowns>
               <Dropdowns>
                 {this.renderNetworksDropdown(selectedNetwork, nic)}
                 {this.renderNetworksDropdown(selectedNetwork, nic)}
                 {this.renderSecGroupsDropdown(selectedNetwork, nic)}
                 {this.renderSecGroupsDropdown(selectedNetwork, nic)}
+                {/* {this.renderPortKeysDropdown(selectedNetwork, nic)} */}
               </Dropdowns>
               </Dropdowns>
             </Nic>
             </Nic>
           )
           )

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

@@ -146,7 +146,6 @@ type Props = {
   useAdvancedOptions?: boolean,
   useAdvancedOptions?: boolean,
   hasStorageMap: boolean,
   hasStorageMap: boolean,
   storageBackends?: StorageBackend[],
   storageBackends?: StorageBackend[],
-  storageConfigDefault?: string,
   onAdvancedOptionsToggle?: (showAdvanced: boolean) => void,
   onAdvancedOptionsToggle?: (showAdvanced: boolean) => void,
   wizardType: string,
   wizardType: string,
   oneColumnStyle?: { [prop: string]: any },
   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 WizardBreadcrumbs from '../../molecules/WizardBreadcrumbs'
 import WizardEndpointList from '../WizardEndpointList'
 import WizardEndpointList from '../WizardEndpointList'
 import WizardInstances from '../WizardInstances'
 import WizardInstances from '../WizardInstances'
-import WizardNetworks from '../WizardNetworks'
+import WizardNetworks, { WizardNetworksChangeObject } from '../WizardNetworks'
 import WizardStorage from '../WizardStorage'
 import WizardStorage from '../WizardStorage'
 import WizardOptions from '../WizardOptions'
 import WizardOptions from '../WizardOptions'
 import WizardScripts from '../WizardScripts'
 import WizardScripts from '../WizardScripts'
@@ -36,12 +36,11 @@ import { providerTypes, wizardPages, migrationFields } from '../../../constants'
 import configLoader from '../../../utils/Config'
 import configLoader from '../../../utils/Config'
 
 
 import type { WizardData, WizardPage } from '../../../@types/WizardData'
 import type { WizardData, WizardPage } from '../../../@types/WizardData'
-import type { Endpoint, StorageBackend, StorageMap } from '../../../@types/Endpoint'
+import { Endpoint, EndpointUtils, StorageMap } from '../../../@types/Endpoint'
 import type {
 import type {
-  Instance, Nic, Disk, InstanceScript,
+  Instance, InstanceScript,
 } from '../../../@types/Instance'
 } from '../../../@types/Instance'
 import type { Field } from '../../../@types/Field'
 import type { Field } from '../../../@types/Field'
-import type { Network, SecurityGroup } from '../../../@types/Network'
 import type { Schedule as ScheduleType } from '../../../@types/Schedule'
 import type { Schedule as ScheduleType } from '../../../@types/Schedule'
 import instanceStore from '../../../stores/InstanceStore'
 import instanceStore from '../../../stores/InstanceStore'
 import providerStore from '../../../stores/ProviderStore'
 import providerStore from '../../../stores/ProviderStore'
@@ -178,7 +177,7 @@ type Props = {
   wizardData: WizardData,
   wizardData: WizardData,
   schedules: ScheduleType[],
   schedules: ScheduleType[],
   storageMap: StorageMap[],
   storageMap: StorageMap[],
-  defaultStorage: string | null,
+  defaultStorage: { value: string | null, busType?: string | null } | undefined,
   hasStorageMap: boolean,
   hasStorageMap: boolean,
   hasSourceOptions: boolean,
   hasSourceOptions: boolean,
   pages: WizardPage[],
   pages: WizardPage[],
@@ -196,9 +195,9 @@ type Props = {
   onInstancePageClick: (page: number) => void,
   onInstancePageClick: (page: number) => void,
   onDestOptionsChange: (field: Field, value: any, parentFieldName?: string) => void,
   onDestOptionsChange: (field: Field, value: any, parentFieldName?: string) => void,
   onSourceOptionsChange: (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,
   onAddScheduleClick: (schedule: ScheduleType) => void,
   onScheduleChange: (scheduleId: string, schedule: ScheduleType) => void,
   onScheduleChange: (scheduleId: string, schedule: ScheduleType) => void,
   onScheduleRemove: (scheudleId: string) => void,
   onScheduleRemove: (scheudleId: string) => void,
@@ -376,6 +375,24 @@ class WizardPageContent extends React.Component<Props, State> {
       return optionsLoadingRequiredFields
       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) {
     switch (this.props.page.id) {
       case 'type':
       case 'type':
         body = (
         body = (
@@ -473,7 +490,6 @@ class WizardPageContent extends React.Component<Props, State> {
             useAdvancedOptions={this.state.useAdvancedOptions}
             useAdvancedOptions={this.state.useAdvancedOptions}
             hasStorageMap={this.props.hasStorageMap}
             hasStorageMap={this.props.hasStorageMap}
             storageBackends={this.props.endpointStore.storageBackends}
             storageBackends={this.props.endpointStore.storageBackends}
-            storageConfigDefault={this.props.endpointStore.storageConfigDefault}
             wizardType={this.props.type}
             wizardType={this.props.type}
             onAdvancedOptionsToggle={useAdvancedOptions => {
             onAdvancedOptionsToggle={useAdvancedOptions => {
               this.handleAdvancedOptionsToggle(useAdvancedOptions)
               this.handleAdvancedOptionsToggle(useAdvancedOptions)
@@ -502,8 +518,7 @@ class WizardPageContent extends React.Component<Props, State> {
             instancesDetails={this.props.instanceStore.instancesDetails}
             instancesDetails={this.props.instanceStore.instancesDetails}
             storageMap={this.props.storageMap}
             storageMap={this.props.storageMap}
             onChange={this.props.onStorageChange}
             onChange={this.props.onStorageChange}
-            storageConfigDefault={this.props.endpointStore.storageConfigDefault}
-            defaultStorage={this.props.defaultStorage}
+            defaultStorage={getDefaultStorage()}
             onDefaultStorageChange={this.props.onDefaultStorageChange}
             onDefaultStorageChange={this.props.onDefaultStorageChange}
             defaultStorageLayout="page"
             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 Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 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 type { StorageBackend, StorageMap } from '../../../@types/Endpoint'
 
 
 import backendImage from './images/backend.svg'
 import backendImage from './images/backend.svg'
@@ -97,6 +97,22 @@ const ArrowImage = styled.div<any>`
   flex-grow: 1;
   flex-grow: 1;
   margin-right: 16px;
   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>`
 const NoStorageMessage = styled.div<any>`
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
@@ -151,10 +167,9 @@ export type Props = {
   instancesDetails: Instance[],
   instancesDetails: Instance[],
   storageMap: StorageMap[] | null | undefined,
   storageMap: StorageMap[] | null | undefined,
   defaultStorageLayout: 'modal' | 'page',
   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,
   onScrollableRef?: (ref: HTMLElement) => void,
   style?: any,
   style?: any,
   titleWidth?: number,
   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(
   renderStorageDropdown(
-    storageItems: Array<StorageBackend | { id: string | null, name: string }>,
-    selectedItem: StorageBackend | null,
+    storageItems: Array<StorageBackend>,
+    selectedItem: StorageBackend | null | undefined,
     disk: Disk,
     disk: Disk,
     type: 'backend' | '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 ? (
     return storageItems.length > 10 ? (
       <AutocompleteDropdown
       <AutocompleteDropdown
         width={StyleProps.inputSizes.large.width}
         width={StyleProps.inputSizes.large.width}
         selectedItem={selectedItem}
         selectedItem={selectedItem}
         items={storageItems}
         items={storageItems}
-        onChange={(item: StorageBackend) => { this.props.onChange(disk, item, type) }}
+        onChange={(item: StorageBackend) => { this.props.onChange({ source: disk, target: item, type }) }}
         labelField="name"
         labelField="name"
         valueField="id"
         valueField="id"
       />
       />
@@ -208,7 +256,7 @@ class WizardStorage extends React.Component<Props> {
           items={storageItems}
           items={storageItems}
           labelField="name"
           labelField="name"
           valueField="id"
           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`}
           data-test-id={`${TEST_ID}-${type}-destination`}
         />
         />
       )
       )
@@ -253,11 +301,10 @@ class WizardStorage extends React.Component<Props> {
                 return true
                 return true
               }
               }
               return false
               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]))
                 && String(s.source[diskFieldName]) === String(disk[diskFieldName]))
-            const selectedItem = selectedStorage ? selectedStorage.target : null
             const diskNameParsed = parseDiskName(disk[diskFieldName])
             const diskNameParsed = parseDiskName(disk[diskFieldName])
             return (
             return (
               <StorageItem key={disk[diskFieldName]}>
               <StorageItem key={disk[diskFieldName]}>
@@ -273,7 +320,14 @@ class WizardStorage extends React.Component<Props> {
                   ) : null}
                   ) : null}
                 </StorageTitle>
                 </StorageTitle>
                 <ArrowImage />
                 <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>
               </StorageItem>
             )
             )
           })}
           })}
@@ -318,10 +372,7 @@ class WizardStorage extends React.Component<Props> {
         { label: 'Choose a value', value: null },
         { label: 'Choose a value', value: null },
         ...items,
         ...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 = {
       const commonProps = {
         width: StyleProps.inputSizes.regular.width,
         width: StyleProps.inputSizes.regular.width,
         selectedItem,
         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 (
     return (
       <StorageWrapper>
       <StorageWrapper>
         <StorageSection>
         <StorageSection>
@@ -360,7 +445,10 @@ class WizardStorage extends React.Component<Props> {
             <StorageImage backend="backend" />
             <StorageImage backend="backend" />
             <StorageTitle width={this.props.titleWidth || 320}>
             <StorageTitle width={this.props.titleWidth || 320}>
               <StorageName>
               <StorageName>
-                {renderDropdown()}
+                <DefaultDropdowns>
+                  {renderDropdown()}
+                  {/* {renderDefaultBusTypeDropdown()} */}
+                </DefaultDropdowns>
               </StorageName>
               </StorageName>
             </StorageTitle>
             </StorageTitle>
           </StorageItem>
           </StorageItem>

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

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

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

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

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

@@ -41,6 +41,7 @@ import type { Field } from '../../../@types/Field'
 import type { InstanceScript } from '../../../@types/Instance'
 import type { InstanceScript } from '../../../@types/Instance'
 import minionPoolStore from '../../../stores/MinionPoolStore'
 import minionPoolStore from '../../../stores/MinionPoolStore'
 import { getTransferItemTitle } from '../../../@types/MainItem'
 import { getTransferItemTitle } from '../../../@types/MainItem'
+import { providerTypes } from '../../../constants'
 
 
 const Wrapper = styled.div<any>``
 const Wrapper = styled.div<any>``
 
 
@@ -151,13 +152,23 @@ class MigrationDetailsPage extends React.Component<Props, State> {
       minionPoolStore.loadMinionPools()
       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, {
     networkStore.loadNetworks(details.destination_endpoint_id, details.destination_environment, {
       quietError: true,
       quietError: true,
       cache,
       cache,
     })
     })
 
 
-    const targetEndpoint = endpointStore.endpoints
-      .find(e => e.id === details.destination_endpoint_id)
     instanceStore.loadInstancesDetails({
     instanceStore.loadInstancesDetails({
       endpointId: details.origin_endpoint_id,
       endpointId: details.origin_endpoint_id,
       instances: details.instances.map(n => ({ id: n })),
       instances: details.instances.map(n => ({ id: n })),
@@ -370,7 +381,9 @@ class MigrationDetailsPage extends React.Component<Props, State> {
               item={migrationStore.migrationDetails}
               item={migrationStore.migrationDetails}
               itemId={this.props.match.params.id}
               itemId={this.props.match.params.id}
               instancesDetails={instanceStore.instancesDetails}
               instancesDetails={instanceStore.instancesDetails}
-              instancesDetailsLoading={instanceStore.loadingInstancesDetails}
+              instancesDetailsLoading={instanceStore.loadingInstancesDetails || endpointStore.storageLoading || providerStore.providersLoading}
+              storageBackends={endpointStore.storageBackends}
+              networks={networkStore.networks}
               sourceSchema={providerStore.sourceSchema}
               sourceSchema={providerStore.sourceSchema}
               sourceSchemaLoading={providerStore.sourceSchemaLoading
               sourceSchemaLoading={providerStore.sourceSchemaLoading
               || providerStore.sourceOptionsPrimaryLoading
               || providerStore.sourceOptionsPrimaryLoading
@@ -430,6 +443,8 @@ Note that this may lead to scheduled cleanup tasks being forcibly skipped, and t
               loadingInstances={instanceStore.loadingInstancesDetails}
               loadingInstances={instanceStore.loadingInstancesDetails}
               defaultSkipOsMorphing={migrationStore
               defaultSkipOsMorphing={migrationStore
                 .getDefaultSkipOsMorphing(migrationStore.migrationDetails)}
                 .getDefaultSkipOsMorphing(migrationStore.migrationDetails)}
+              disabledCloneDisk={migrationStore.getDisabledCloneDiskOptions(endpointStore.endpoints
+                .find(e => e.id === migrationStore.migrationDetails?.destination_endpoint_id)?.type)}
             />
             />
           </Modal>
           </Modal>
         ) : null}
         ) : null}

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

@@ -180,7 +180,6 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   async loadIsEditable(replicaDetails: ReplicaItemDetails) {
   async loadIsEditable(replicaDetails: ReplicaItemDetails) {
     const targetEndpointId = replicaDetails.destination_endpoint_id
     const targetEndpointId = replicaDetails.destination_endpoint_id
     const sourceEndpointId = replicaDetails.origin_endpoint_id
     const sourceEndpointId = replicaDetails.origin_endpoint_id
-    await providerStore.loadProviders()
     await ObjectUtils.waitFor(() => endpointStore.endpoints.length > 0)
     await ObjectUtils.waitFor(() => endpointStore.endpoints.length > 0)
     const sourceEndpoint = endpointStore.endpoints.find(e => e.id === sourceEndpointId)
     const sourceEndpoint = endpointStore.endpoints.find(e => e.id === sourceEndpointId)
     const targetEndpoint = endpointStore.endpoints.find(e => e.id === targetEndpointId)
     const targetEndpoint = endpointStore.endpoints.find(e => e.id === targetEndpointId)
@@ -205,7 +204,10 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     }
     }
     minionPoolStore.loadMinionPools()
     minionPoolStore.loadMinionPools()
 
 
+    await providerStore.loadProviders()
+
     this.loadIsEditable(replica)
     this.loadIsEditable(replica)
+
     networkStore.loadNetworks(replica.destination_endpoint_id, replica.destination_environment, {
     networkStore.loadNetworks(replica.destination_endpoint_id, replica.destination_environment, {
       quietError: true,
       quietError: true,
       cache,
       cache,
@@ -213,6 +215,15 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
 
 
     const targetEndpoint = endpointStore.endpoints
     const targetEndpoint = endpointStore.endpoints
       .find(e => e.id === replica.destination_endpoint_id)
       .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({
     instanceStore.loadInstancesDetails({
       endpointId: replica.origin_endpoint_id,
       endpointId: replica.origin_endpoint_id,
       instances: replica.instances.map(n => ({ id: n })),
       instances: replica.instances.map(n => ({ id: n })),
@@ -578,8 +589,9 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
               item={replica}
               item={replica}
               itemId={this.replicaId}
               itemId={this.replicaId}
               instancesDetails={instanceStore.instancesDetails}
               instancesDetails={instanceStore.instancesDetails}
-              instancesDetailsLoading={instanceStore.loadingInstancesDetails}
+              instancesDetailsLoading={instanceStore.loadingInstancesDetails || endpointStore.storageLoading || providerStore.providersLoading}
               endpoints={endpointStore.endpoints}
               endpoints={endpointStore.endpoints}
+              storageBackends={endpointStore.storageBackends}
               scheduleStore={scheduleStore}
               scheduleStore={scheduleStore}
               networks={networkStore.networks}
               networks={networkStore.networks}
               minionPools={minionPoolStore.minionPools}
               minionPools={minionPoolStore.minionPools}
@@ -638,6 +650,8 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
               instances={instanceStore.instancesDetails}
               instances={instanceStore.instancesDetails}
               onCancelClick={() => { this.handleCloseMigrationModal() }}
               onCancelClick={() => { this.handleCloseMigrationModal() }}
               onMigrateClick={(o, s, r, m) => { this.migrateReplica(o, s, r, m) }}
               onMigrateClick={(o, s, r, m) => { this.migrateReplica(o, s, r, m) }}
+              disabledCloneDisk={migrationStore.getDisabledCloneDiskOptions(endpointStore.endpoints
+                .find(e => e.id === replicaStore.replicaDetails?.destination_endpoint_id)?.type)}
             />
             />
           </Modal>
           </Modal>
         ) : null}
         ) : null}

+ 10 - 0
src/components/pages/ReplicasPage/ReplicasPage.tsx

@@ -46,6 +46,7 @@ import Palette from '../../styleUtils/Palette'
 import configLoader from '../../../utils/Config'
 import configLoader from '../../../utils/Config'
 import { ReplicaItem } from '../../../@types/MainItem'
 import { ReplicaItem } from '../../../@types/MainItem'
 import userStore from '../../../stores/UserStore'
 import userStore from '../../../stores/UserStore'
+import { ProviderMigrationCloneDiskDisabledOption } from '../../../@types/Config'
 
 
 const Wrapper = styled.div<any>``
 const Wrapper = styled.div<any>``
 
 
@@ -330,6 +331,14 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
       action: () => { this.handleShowDeleteReplicas() },
       action: () => { this.handleShowDeleteReplicas() },
     }]
     }]
 
 
+    let disabledDiskOptions: ProviderMigrationCloneDiskDisabledOption | null = null
+    this.state.selectedReplicas.forEach(r => {
+      const options = migrationStore.getDisabledCloneDiskOptions(this.getEndpoint(r.destination_endpoint_id)?.type)
+      if (options) {
+        disabledDiskOptions = options
+      }
+    })
+
     return (
     return (
       <Wrapper>
       <Wrapper>
         <MainTemplate
         <MainTemplate
@@ -431,6 +440,7 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
                 this.setState({ showCreateMigrationsModal: false, modalIsOpen: false })
                 this.setState({ showCreateMigrationsModal: false, modalIsOpen: false })
               }}
               }}
               onMigrateClick={(options, s) => { this.migrateSelectedReplicas(options, s) }}
               onMigrateClick={(options, s) => { this.migrateSelectedReplicas(options, s) }}
+              disabledCloneDisk={disabledDiskOptions}
             />
             />
           </Modal>
           </Modal>
         ) : null}
         ) : null}

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

@@ -35,18 +35,18 @@ import replicaStore from '../../../stores/ReplicaStore'
 import KeyboardManager from '../../../utils/KeyboardManager'
 import KeyboardManager from '../../../utils/KeyboardManager'
 import { wizardPages, executionOptions, providerTypes } from '../../../constants'
 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 {
 import type {
-  Instance, Nic, Disk, InstanceScript,
+  Instance, InstanceScript,
 } from '../../../@types/Instance'
 } from '../../../@types/Instance'
 import type { Field } from '../../../@types/Field'
 import type { Field } from '../../../@types/Field'
-import type { Network, SecurityGroup } from '../../../@types/Network'
 import type { Schedule } from '../../../@types/Schedule'
 import type { Schedule } from '../../../@types/Schedule'
 import type { WizardPage as WizardPageType } from '../../../@types/WizardData'
 import type { WizardPage as WizardPageType } from '../../../@types/WizardData'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import { ProviderTypes } from '../../../@types/Providers'
 import { ProviderTypes } from '../../../@types/Providers'
 import { TransferItem, ReplicaItem } from '../../../@types/MainItem'
 import { TransferItem, ReplicaItem } from '../../../@types/MainItem'
 import minionPoolStore from '../../../stores/MinionPoolStore'
 import minionPoolStore from '../../../stores/MinionPoolStore'
+import { WizardNetworksChangeObject } from '../../organisms/WizardNetworks/WizardNetworks'
 
 
 const Wrapper = styled.div<any>``
 const Wrapper = styled.div<any>``
 
 
@@ -262,15 +262,13 @@ class WizardPage extends React.Component<Props, State> {
       useCache: true,
       useCache: true,
     })
     })
     wizardStore.fillWithDefaultValues('source', providerStore.sourceSchema)
     wizardStore.fillWithDefaultValues('source', providerStore.sourceSchema)
+    await this.loadExtraOptions(null, 'source')
   }
   }
 
 
   async handleTargetEndpointChange(target: EndpointType) {
   async handleTargetEndpointChange(target: EndpointType) {
     wizardStore.updateData({ target, networks: null, destOptions: null })
     wizardStore.updateData({ target, networks: null, destOptions: null })
     wizardStore.clearStorageMap()
     wizardStore.clearStorageMap()
     wizardStore.updateUrlState()
     wizardStore.updateUrlState()
-    if (this.pages.find(p => p.id === 'storage')) {
-      endpointStore.loadStorage(target.id, {})
-    }
     // Preload destination options schema
     // Preload destination options schema
     await providerStore.loadOptionsSchema({
     await providerStore.loadOptionsSchema({
       providerName: target.type,
       providerName: target.type,
@@ -286,6 +284,7 @@ class WizardPage extends React.Component<Props, State> {
       useCache: true,
       useCache: true,
     })
     })
     wizardStore.fillWithDefaultValues('destination', providerStore.destinationSchema)
     wizardStore.fillWithDefaultValues('destination', providerStore.destinationSchema)
+    await this.loadExtraOptions(null, 'destination')
   }
   }
 
 
   handleAddEndpoint(newEndpointType: ProviderTypes, newEndpointFromSource: boolean) {
   handleAddEndpoint(newEndpointType: ProviderTypes, newEndpointFromSource: boolean) {
@@ -363,20 +362,23 @@ class WizardPage extends React.Component<Props, State> {
     wizardStore.updateUrlState()
     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()
     wizardStore.updateUrlState()
   }
   }
 
 
-  handleDefaultStorageChange(value: string | null) {
-    wizardStore.updateDefaultStorage(value)
+  handleDefaultStorageChange(value: string | null, busType?: string | null) {
+    wizardStore.updateDefaultStorage({ value, busType })
     wizardStore.updateUrlState()
     wizardStore.updateUrlState()
   }
   }
 
 
-  handleStorageChange(source: Disk, target: StorageBackend, type: 'backend' | 'disk') {
-    wizardStore.updateStorage({ source, target, type })
+  handleStorageChange(mapping: StorageMap) {
+    wizardStore.updateStorage(mapping)
     wizardStore.updateUrlState()
     wizardStore.updateUrlState()
   }
   }
 
 
@@ -506,10 +508,6 @@ class WizardPage extends React.Component<Props, State> {
         if (!target) {
         if (!target) {
           return
           return
         }
         }
-        // Preload Storage Mapping
-        if (this.pages.find(p => p.id === 'storage')) {
-          endpointStore.loadStorage(target.id, {})
-        }
         // Preload destination options schema
         // Preload destination options schema
         loadOptions(target, 'destination')
         loadOptions(target, 'destination')
         break
         break
@@ -521,6 +519,8 @@ class WizardPage extends React.Component<Props, State> {
         break
         break
       }
       }
       case 'networks':
       case 'networks':
+        // Preload storage API calls
+        endpointStore.loadStorage(wizardStore.data.target!.id, wizardStore.data.destOptions)
         this.loadNetworks(true)
         this.loadNetworks(true)
         break
         break
       default:
       default:
@@ -712,12 +712,12 @@ class WizardPage extends React.Component<Props, State> {
               onSourceOptionsChange={(field, value, parent) => {
               onSourceOptionsChange={(field, value, parent) => {
                 this.handleSourceOptionsChange(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) }}
               onAddScheduleClick={schedule => { this.handleAddScheduleClick(schedule) }}
               onScheduleChange={(scheduleId, data) => {
               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) {
   static getNetworkMap(networkMappings: NetworkMap[] | null | undefined) {
     const payload: any = {}
     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
     return payload
   }
   }
 
 
   static getStorageMap(
   static getStorageMap(
-    defaultStorage: string | null | undefined,
+    defaultStorage: { value: string | null, busType?: string | null } | undefined,
     storageMap: StorageMap[] | null,
     storageMap: StorageMap[] | null,
     configDefault?: string | null,
     configDefault?: string | null,
   ) {
   ) {
-    if (!defaultStorage && !storageMap) {
+    if (!defaultStorage?.value && !storageMap) {
       return null
       return null
     }
     }
 
 
     const payload: any = {}
     const payload: any = {}
-    if (defaultStorage) {
-      payload.default = defaultStorage
+    if (defaultStorage?.value) {
+      payload.default = defaultStorage.value
+      if (defaultStorage.busType) {
+        payload.default += `:${defaultStorage.busType}`
+      }
     }
     }
 
 
     if (!storageMap) {
     if (!storageMap) {
@@ -237,13 +240,21 @@ export default class OptionsSchemaParser {
         return
         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 (mapping.type === 'backend') {
         if (!payload.backend_mappings) {
         if (!payload.backend_mappings) {
           payload.backend_mappings = []
           payload.backend_mappings = []
         }
         }
         payload.backend_mappings.push({
         payload.backend_mappings.push({
           source: mapping.source.storage_backend_identifier,
           source: mapping.source.storage_backend_identifier,
-          destination: mapping.target.id === null ? configDefault : mapping.target.name,
+          destination: getDestination(),
         })
         })
       } else {
       } else {
         if (!payload.disk_mappings) {
         if (!payload.disk_mappings) {
@@ -251,7 +262,7 @@ export default class OptionsSchemaParser {
         }
         }
         payload.disk_mappings.push({
         payload.disk_mappings.push({
           disk_id: mapping.source.id.toString(),
           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 DefaultOptionsSchemaPlugin from './default/OptionsSchemaPlugin'
 import OvmOptionsSchemaPlugin from './ovm/OptionsSchemaPlugin'
 import OvmOptionsSchemaPlugin from './ovm/OptionsSchemaPlugin'
+import VmwareOptionsSchemaPlugin from './vmware_vsphere/OptionsSchemaPlugin'
 import OpenstackOptionsSchemaPlugin from './openstack/OptionsSchemaPlugin'
 import OpenstackOptionsSchemaPlugin from './openstack/OptionsSchemaPlugin'
 
 
 import DefaultInstanceInfoPlugin from './default/InstanceInfoPlugin'
 import DefaultInstanceInfoPlugin from './default/InstanceInfoPlugin'
@@ -54,6 +55,7 @@ export const OptionsSchemaPlugin = {
       default: DefaultOptionsSchemaPlugin,
       default: DefaultOptionsSchemaPlugin,
       oracle_vm: OvmOptionsSchemaPlugin,
       oracle_vm: OvmOptionsSchemaPlugin,
       openstack: OpenstackOptionsSchemaPlugin,
       openstack: OpenstackOptionsSchemaPlugin,
+      vmware_vsphere: VmwareOptionsSchemaPlugin,
     }
     }
     if (hasKey(map, provider)) {
     if (hasKey(map, provider)) {
       return map[provider]
       return map[provider]

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

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

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

@@ -89,12 +89,12 @@ export default class OptionsSchemaParser {
     return env
     return env
   }
   }
 
 
-  static getNetworkMap(networkMappings: NetworkMap[] | null) {
+  static getNetworkMap(networkMappings: NetworkMap[] | null | undefined) {
     return DefaultOptionsSchemaPlugin.getNetworkMap(networkMappings)
     return DefaultOptionsSchemaPlugin.getNetworkMap(networkMappings)
   }
   }
 
 
   static getStorageMap(
   static getStorageMap(
-    defaultStorage: string | null,
+    defaultStorage: { value: string | null, busType?: string | null },
     storageMap: StorageMap[] | null,
     storageMap: StorageMap[] | null,
     configDefault?: string | 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_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_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,
     updatedSourceEnv?: { [prop: string]: any } | null,
     storageMappings?: { [prop: string]: any } | null,
     storageMappings?: { [prop: string]: any } | null,
     updatedStorageMappings: StorageMap[] | 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,
     networkMappings?: any,
     updatedNetworkMappings: NetworkMap[] | null,
     updatedNetworkMappings: NetworkMap[] | null,
     defaultSkipOsMorphing: boolean | null,
     defaultSkipOsMorphing: boolean | null,

+ 1 - 1
src/sources/ReplicaSource.ts

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

+ 2 - 2
src/sources/WizardSource.ts

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

+ 11 - 2
src/stores/MigrationStore.ts

@@ -21,6 +21,8 @@ import type { Field } from '../@types/Field'
 import type { Endpoint } from '../@types/Endpoint'
 import type { Endpoint } from '../@types/Endpoint'
 import type { InstanceScript } from '../@types/Instance'
 import type { InstanceScript } from '../@types/Instance'
 import MigrationSource from '../sources/MigrationSource'
 import MigrationSource from '../sources/MigrationSource'
+import configLoader from '../utils/Config'
+import { ProviderTypes } from '../@types/Providers'
 
 
 class MigrationStore {
 class MigrationStore {
   @observable migrations: MigrationItem[] = []
   @observable migrations: MigrationItem[] = []
@@ -59,6 +61,13 @@ class MigrationStore {
     return null
     return null
   }
   }
 
 
+  getDisabledCloneDiskOptions(provider: ProviderTypes | undefined) {
+    if (!provider) {
+      return null
+    }
+    return configLoader.config.providerMigrationOptions[provider]?.cloneDiskDisabledOptions || null
+  }
+
   @action async recreateFullCopy(migration: MigrationItemOptions) {
   @action async recreateFullCopy(migration: MigrationItemOptions) {
     return MigrationSource.recreateFullCopy(migration)
     return MigrationSource.recreateFullCopy(migration)
   }
   }
@@ -68,8 +77,8 @@ class MigrationStore {
     sourceEndpoint: Endpoint,
     sourceEndpoint: Endpoint,
     destEndpoint: Endpoint,
     destEndpoint: Endpoint,
     updateData: UpdateData,
     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,
     replicationCount: number | null | undefined,
   ): Promise<MigrationItemDetails> {
   ): Promise<MigrationItemDetails> {
     const migrationResult = await MigrationSource.recreate({
     const migrationResult = await MigrationSource.recreate({

+ 22 - 8
src/stores/ProviderStore.ts

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

+ 1 - 1
src/stores/ReplicaStore.ts

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

+ 6 - 6
src/stores/WizardStore.ts

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