Explorar el Código

Add support for Replicas-as-Migrations API feature

Removes migration type from providers, use replica type instead for all
provider schema calls.

Adds 'Replication Count' and 'Shutdown Instances' fields to migration
options.
Sergiu Miclea hace 6 años
padre
commit
a208cb2397

+ 2 - 1
src/components/molecules/FieldInput/FieldInput.jsx

@@ -96,6 +96,7 @@ type Props = {
   label?: string,
   addNullValue?: boolean,
   nullableBoolean?: boolean,
+  description?: string,
   style?: { [string]: mixed },
 }
 @observer
@@ -367,7 +368,7 @@ class FieldInput extends React.Component<Props> {
       return null
     }
 
-    let description = LabelDictionary.getDescription(this.props.name)
+    let description = LabelDictionary.getDescription(this.props.name) || this.props.description
     let marginRight = this.props.layout === 'modal' || description || this.props.required ? '24px' : 0
 
     return (

+ 20 - 17
src/components/organisms/EditReplica/EditReplica.jsx

@@ -39,7 +39,7 @@ import type { Field } from '../../../types/Field'
 import type { Instance, Nic, Disk } from '../../../types/Instance'
 import type { Network, NetworkMap, SecurityGroup } from '../../../types/Network'
 
-import { providerTypes } from '../../../constants'
+import { providerTypes, migrationFields } from '../../../constants'
 import configLoader from '../../../utils/Config'
 import StyleProps from '../../styleUtils/StyleProps'
 
@@ -143,7 +143,6 @@ class EditReplica extends React.Component<Props, State> {
   async loadOptions(endpoint: Endpoint, optionsType: 'source' | 'destination', useCache: boolean, envData: ?{ [string]: mixed }) {
     await providerStore.loadOptionsSchema({
       providerName: endpoint.type,
-      schemaType: this.props.type || 'replica',
       optionsType,
       useCache,
     })
@@ -321,20 +320,23 @@ class EditReplica extends React.Component<Props, State> {
 
   getFieldValue(type: 'source' | 'destination', fieldName: string, defaultValue: any) {
     let currentData = type === 'source' ? this.state.sourceData : this.state.destinationData
-    if (currentData[fieldName] === undefined) {
-      let replicaData = this.parseReplicaData(type === 'source' ? this.props.replica.source_environment
-        : this.props.replica.destination_environment)
-      if (replicaData[fieldName] !== undefined) {
-        return replicaData[fieldName]
-      }
-      let osMapping = /^(windows|linux)_os_image$/.exec(fieldName)
-      if (osMapping) {
-        let osData = replicaData[`migr_image_map/${osMapping[1]}`]
-        return osData
-      }
-      return defaultValue
+    if (currentData[fieldName] !== undefined) {
+      return currentData[fieldName]
+    }
+    let replicaData = this.parseReplicaData(type === 'source' ? this.props.replica.source_environment
+      : this.props.replica.destination_environment)
+    if (replicaData[fieldName] !== undefined) {
+      return replicaData[fieldName]
+    }
+    let osMapping = /^(windows|linux)_os_image$/.exec(fieldName)
+    if (osMapping) {
+      let osData = replicaData[`migr_image_map/${osMapping[1]}`]
+      return osData
+    }
+    if (migrationFields.find(f => f.name === fieldName) && this.props.replica[fieldName]) {
+      return this.props.replica[fieldName]
     }
-    return currentData[fieldName]
+    return defaultValue
   }
 
   getSelectedNetworks(): NetworkMap[] {
@@ -429,7 +431,7 @@ class EditReplica extends React.Component<Props, State> {
     }
     return (
       <WizardOptions
-        wizardType={`replica-${type}-options-edit`}
+        wizardType={`${this.props.type || 'replica'}-${type}-options-edit`}
         getFieldValue={(f, d) => this.getFieldValue(type, f, d)}
         fields={fields}
         hasStorageMap={type === 'source' ? false : this.hasStorageMap()}
@@ -443,7 +445,8 @@ class EditReplica extends React.Component<Props, State> {
         useAdvancedOptions
         layout="modal"
         optionsLoading={optionsLoading}
-        optionsLoadingSkipFields={[...optionsLoadingSkipFields, 'description', 'execute_now', 'execute_now_options', 'default_storage']}
+        optionsLoadingSkipFields={[...optionsLoadingSkipFields, 'description', 'execute_now', 'execute_now_options',
+          'default_storage', ...migrationFields.map(f => f.name)]}
       />
     )
   }

+ 15 - 10
src/components/organisms/ReplicaMigrationOptions/ReplicaMigrationOptions.jsx

@@ -42,7 +42,7 @@ const Form = styled.div`
   display: flex;
   flex-wrap: wrap;
   margin-left: -64px;
-  width: 300px;
+  justify-content: space-between;
   margin: 0 auto 46px auto;
 `
 const Buttons = styled.div`
@@ -51,7 +51,7 @@ const Buttons = styled.div`
   width: 100%;
 `
 const FieldInputStyled = styled(FieldInput)`
-  width: 319px;
+  width: 200px;
   justify-content: space-between;
   margin-bottom: 32px;
 `
@@ -81,7 +81,7 @@ let defaultFields: Field[] = [
 @observer
 class ReplicaMigrationOptions extends React.Component<Props, State> {
   state = {
-    fields: defaultFields,
+    fields: [...defaultFields],
   }
 
   componentDidMount() {
@@ -93,13 +93,15 @@ class ReplicaMigrationOptions extends React.Component<Props, State> {
   }
 
   handleValueChange(field: Field, value: boolean) {
-    let foundField = this.state.fields.find(f => f.name === field.name)
-    if (!foundField) {
-      return
-    }
-    foundField.value = value
+    let fields = this.state.fields.map(f => {
+      let newField = { ...f }
+      if (f.name === field.name) {
+        newField.value = value
+      }
+      return newField
+    })
 
-    this.setState({ fields: [...this.state.fields] })
+    this.setState({ fields })
   }
 
   render() {
@@ -110,10 +112,13 @@ class ReplicaMigrationOptions extends React.Component<Props, State> {
           {this.state.fields.map(field => {
             return (
               <FieldInputStyled
+                width={200}
                 key={field.name}
                 name={field.name}
                 type={field.type}
-                value={field.value}
+                value={field.value || field.default}
+                minimum={field.minimum}
+                maximum={field.maximum}
                 layout="page"
                 label={LabelDictionary.get(field.name)}
                 onChange={value => this.handleValueChange(field, value)}

+ 11 - 9
src/components/organisms/WizardOptions/WizardOptions.jsx

@@ -28,7 +28,7 @@ import type { Field } from '../../../types/Field'
 import type { Instance } from '../../../types/Instance'
 import type { StorageBackend } from '../../../types/Endpoint'
 
-import { executionOptions } from '../../../constants'
+import { executionOptions, migrationFields } from '../../../constants'
 import LabelDictionary from '../../../utils/LabelDictionary'
 import Palette from '../../styleUtils/Palette'
 
@@ -165,15 +165,14 @@ class WizardOptions extends React.Component<Props> {
       fieldsSchema.push({ name: 'execute_now', type: 'boolean', default: true })
       let executeNowValue = this.getFieldValue('execute_now', true)
       if (executeNowValue) {
-        fieldsSchema = [
-          ...fieldsSchema,
-          {
-            name: 'execute_now_options',
-            type: 'object',
-            properties: executionOptions,
-          },
-        ]
+        fieldsSchema.push({
+          name: 'execute_now_options',
+          type: 'object',
+          properties: executionOptions,
+        })
       }
+    } else if (this.props.wizardType === 'migration' || this.props.wizardType === 'migration-destination-options-edit') {
+      fieldsSchema = [...fieldsSchema, ...migrationFields]
     }
 
     if (this.props.hasStorageMap && this.props.useAdvancedOptions && this.props.storageBackends && this.props.storageBackends.length > 0) {
@@ -229,6 +228,9 @@ class WizardOptions extends React.Component<Props> {
         key={field.name}
         name={field.name}
         type={field.type}
+        minimum={field.minimum}
+        maximum={field.maximum}
+        description={field.description}
         password={this.isPassword(field.name)}
         enum={field.enum}
         addNullValue

+ 2 - 2
src/components/organisms/WizardOptions/test.jsx

@@ -65,7 +65,7 @@ describe('WizardOptions Component', () => {
 
   it('has description and required field in simple tab', () => {
     let wrapper = wrap({ fields, selectedInstances: [], wizardType: 'migration' })
-    expect(wrapper.findPartialId('field-').length).toBe(3)
+    expect(wrapper.findPartialId('field-').length).toBe(5)
     expect(wrapper.find('field-description').length).toBe(1)
     expect(wrapper.find('field-required_string_field').length).toBe(1)
   })
@@ -88,7 +88,7 @@ describe('WizardOptions Component', () => {
 
   it('renders correct number of fields in advanced tab', () => {
     let wrapper = wrap({ fields, selectedInstances: [], useAdvancedOptions: true, wizardType: 'migration' })
-    expect(wrapper.findPartialId('field-').length).toBe(fields.length + 2)
+    expect(wrapper.findPartialId('field-').length).toBe(fields.length + 4)
   })
 
   it('renders correct field info', () => {

+ 5 - 13
src/components/organisms/WizardPageContent/WizardPageContent.jsx

@@ -33,7 +33,7 @@ import WizardSummary from '../WizardSummary'
 
 import StyleProps from '../../styleUtils/StyleProps'
 import Palette from '../../styleUtils/Palette'
-import { providerTypes, wizardPages } from '../../../constants'
+import { providerTypes, wizardPages, migrationFields } from '../../../constants'
 import configLoader from '../../../utils/Config'
 
 import type { WizardData, WizardPage } from '../../../types/WizardData'
@@ -208,17 +208,7 @@ class WizardPageContent extends React.Component<Props, State> {
   }
 
   getProvidersType(type: string) {
-    if (this.props.type === 'replica') {
-      if (type === 'source') {
-        return providerTypes.SOURCE_REPLICA
-      }
-      return providerTypes.TARGET_REPLICA
-    }
-
-    if (type === 'source') {
-      return providerTypes.SOURCE_MIGRATION
-    }
-    return providerTypes.TARGET_MIGRATION
+    return type === 'source' ? providerTypes.SOURCE_REPLICA : providerTypes.TARGET_REPLICA
   }
 
   getProviders(type: string): string[] {
@@ -408,7 +398,9 @@ class WizardPageContent extends React.Component<Props, State> {
           <WizardOptions
             loading={this.props.providerStore.destinationSchemaLoading || this.props.providerStore.destinationOptionsPrimaryLoading}
             optionsLoading={this.props.providerStore.destinationOptionsSecondaryLoading}
-            optionsLoadingSkipFields={[...getOptionsLoadingSkipFields('destination'), 'description', 'execute_now', 'execute_now_options', 'default_storage']}
+            optionsLoadingSkipFields={[
+              ...getOptionsLoadingSkipFields('destination'), 'description', 'execute_now',
+              'execute_now_options', 'default_storage', ...migrationFields.map(f => f.name)]}
             selectedInstances={this.props.wizardData.selectedInstances}
             fields={this.props.providerStore.destinationSchema}
             onChange={this.props.onDestOptionsChange}

+ 18 - 0
src/components/organisms/WizardSummary/WizardSummary.jsx

@@ -25,6 +25,7 @@ import StyleProps from '../../styleUtils/StyleProps'
 import Palette from '../../styleUtils/Palette'
 import LabelDictionary from '../../../utils/LabelDictionary'
 import DateUtils from '../../../utils/DateUtils'
+import { migrationFields } from '../../../constants'
 import type { Schedule } from '../../../types/Schedule'
 import type { WizardData } from '../../../types/WizardData'
 import type { StorageMap, StorageBackend } from '../../../types/Endpoint'
@@ -291,16 +292,33 @@ class WizardSummary extends React.Component<Props> {
       </Option>
     )
 
+    let migrationOptions = [
+      (
+        <Option>
+          <OptionLabel>Shutdown Instances</OptionLabel>
+          <OptionValue>{this.getDefaultOption('shutdown_instances') ? 'Yes' : 'No'}</OptionValue>
+        </Option>
+      ),
+      (
+        <Option>
+          <OptionLabel>Replication Count</OptionLabel>
+          <OptionValue>{(this.props.data.destOptions && this.props.data.destOptions.replication_count) || 2}</OptionValue>
+        </Option>
+      ),
+    ]
+
     return (
       <Section>
         <SectionTitle>{type} Target Options</SectionTitle>
         <OptionsList>
           {this.props.wizardType === 'replica' ? executeNowOption : null}
+          {this.props.wizardType === 'migration' ? migrationOptions : null}
           {this.props.data.selectedInstances && this.props.data.selectedInstances.length > 1 ? separateVmOption : null}
           {data.destOptions ? Object.keys(data.destOptions).map(optionName => {
             if (
               optionName === 'execute_now' ||
               optionName === 'separate_vm' ||
+              migrationFields.find(f => f.name === optionName) ||
               !data.destOptions || data.destOptions[optionName] == null || data.destOptions[optionName] === ''
             ) {
               return null

+ 0 - 2
src/components/pages/AssessmentDetailsPage/AssessmentDetailsPage.jsx

@@ -245,13 +245,11 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
     let endpointType = this.getLocalData().endpoint.type
     providerStore.loadOptionsSchema({
       providerName: endpointType,
-      schemaType: 'replica',
       optionsType: 'destination',
     }).then(() => {
       this.setState({ replicaSchema: providerStore.destinationSchema })
       return providerStore.loadOptionsSchema({
         providerName: endpointType,
-        schemaType: 'migration',
         optionsType: 'destination',
       })
     }).then(() => {

+ 0 - 1
src/components/pages/MigrationDetailsPage/MigrationDetailsPage.jsx

@@ -83,7 +83,6 @@ class MigrationDetailsPage extends React.Component<Props, State> {
       // This allows the values to be displayed with their allocated names instead of their IDs
       await providerStore.loadOptionsSchema({
         providerName: endpoint.type,
-        schemaType: details.type,
         optionsType: 'destination',
         useCache: true,
         quietError: true,

+ 0 - 1
src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.jsx

@@ -102,7 +102,6 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       // This allows the values to be displayed with their allocated names instead of their IDs
       await providerStore.loadOptionsSchema({
         providerName: endpoint.type,
-        schemaType: details.type,
         optionsType: 'destination',
         useCache: true,
         quietError: true,

+ 0 - 4
src/components/pages/WizardPage/WizardPage.jsx

@@ -222,7 +222,6 @@ class WizardPage extends React.Component<Props, State> {
     }
     await providerStore.loadOptionsSchema({
       providerName: source.type,
-      schemaType: this.state.type,
       optionsType: 'source',
       useCache: true,
     })
@@ -244,7 +243,6 @@ class WizardPage extends React.Component<Props, State> {
     // Preload destination options schema
     await providerStore.loadOptionsSchema({
       providerName: target.type,
-      schemaType: this.state.type,
       optionsType: 'destination',
       useCache: true,
     })
@@ -353,7 +351,6 @@ class WizardPage extends React.Component<Props, State> {
     }
     await providerStore.loadOptionsSchema({
       providerName: endpoint.type,
-      schemaType: this.state.type,
       optionsType,
     })
     await providerStore.getOptionsValues({
@@ -404,7 +401,6 @@ class WizardPage extends React.Component<Props, State> {
       }
       await providerStore.loadOptionsSchema({
         providerName: endpoint.type,
-        schemaType: this.state.type,
         optionsType,
         useCache: true,
       })

+ 17 - 2
src/constants.js

@@ -46,8 +46,6 @@ export const navigationMenu = [
 
 // https://github.com/cloudbase/coriolis/blob/master/coriolis/constants.py
 export const providerTypes = {
-  TARGET_MIGRATION: 1,
-  SOURCE_MIGRATION: 2,
   TARGET_REPLICA: 4,
   SOURCE_REPLICA: 8,
   CONNECTION: 16,
@@ -80,6 +78,23 @@ export const executionOptions = [
   },
 ]
 
+export const migrationFields = [
+  {
+    name: 'shutdown_instances',
+    type: 'boolean',
+    default: false,
+    description: 'Whether or not Coriolis should power off the source VM before performing the final incremental sync. This guarantees consistency of the exported VM\'s filesystems, but implies downtime for the source VM during the final sync.',
+  },
+  {
+    name: 'replication_count',
+    type: 'integer',
+    minimum: 1,
+    maximum: 10,
+    default: 2,
+    description: 'The number of times to incrementally sync the disks of the source VM. This can be paired with "Shutdown Instances" to allow for the live syncing of the source VM, and shutting it off before the final incremental sync.',
+  },
+]
+
 export const wizardPages = [
   { id: 'type', title: 'New', breadcrumb: 'Type' },
   { id: 'source', title: 'Select your source cloud', breadcrumb: 'Source Cloud' },

+ 2 - 1
src/plugins/endpoint/default/OptionsSchemaPlugin.js

@@ -22,7 +22,7 @@ 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 { executionOptions } from '../../../constants'
+import { executionOptions, migrationFields } from '../../../constants'
 
 const migrationImageOsTypes = ['windows', 'linux']
 
@@ -73,6 +73,7 @@ export const defaultFillMigrationImageMapValues = (field: Field, option: OptionV
 export const defaultGetDestinationEnv = (options: ?{ [string]: mixed }, oldOptions?: ?{ [string]: mixed }): any => {
   let env = {}
   let specialOptions = ['execute_now', 'separate_vm', 'skip_os_morphing', 'default_storage', 'description']
+    .concat(migrationFields.map(f => f.name))
     .concat(executionOptions.map(o => o.name))
     .concat(migrationImageOsTypes.map(o => `${o}_os_image`))
 

+ 3 - 1
src/sources/MigrationSource.js

@@ -100,6 +100,8 @@ class MigrationSource {
         ...opts.destEnv,
         ...destParser.getDestinationEnv(opts.updatedDestEnv),
       },
+      shutdown_instances: Boolean(opts.updatedDestEnv && opts.updatedDestEnv.shutdown_instances),
+      replication_count: (opts.updatedDestEnv && opts.updatedDestEnv.replication_count) || 2,
       instances: opts.instanceNames,
       notes: getValue('description') || '',
     }
@@ -162,7 +164,7 @@ class MigrationSource {
       },
     }
     options.forEach(o => {
-      payload.migration[o.name] = o.value || false
+      payload.migration[o.name] = o.value || o.default || false
     })
 
     let response = await Api.send({

+ 2 - 4
src/sources/ProviderSource.js

@@ -34,10 +34,8 @@ class ProviderSource {
     return response.data.providers
   }
 
-  async loadOptionsSchema(providerName: string, schemaType: 'migration' | 'replica', optionsType: 'source' | 'destination', useCache?: ?boolean, quietError?: ?boolean): Promise<Field[]> {
-    let schemaTypeInt = schemaType === 'migration' ?
-      optionsType === 'source' ? providerTypes.SOURCE_MIGRATION : providerTypes.TARGET_MIGRATION :
-      optionsType === 'source' ? providerTypes.SOURCE_REPLICA : providerTypes.TARGET_REPLICA
+  async loadOptionsSchema(providerName: string, optionsType: 'source' | 'destination', useCache?: ?boolean, quietError?: ?boolean): Promise<Field[]> {
+    let schemaTypeInt = optionsType === 'source' ? providerTypes.SOURCE_REPLICA : providerTypes.TARGET_REPLICA
 
     let response = await Api.send({
       url: `${servicesUrl.coriolis}/${Api.projectId}/providers/${providerName}/schemas/${schemaTypeInt}`,

+ 5 - 0
src/sources/WizardSource.js

@@ -47,6 +47,11 @@ class WizardSource {
       payload[type].source_environment = sourceParser.getDestinationEnv(data.sourceOptions)
     }
 
+    if (type === 'migration') {
+      payload[type].shutdown_instances = Boolean(data.destOptions && data.destOptions.shutdown_instances)
+      payload[type].replication_count = (data.destOptions && data.destOptions.replication_count) || 2
+    }
+
     let response = await Api.send({
       url: `${servicesUrl.coriolis}/${Api.projectId}/${type}s`,
       method: 'POST',

+ 2 - 3
src/stores/ProviderStore.js

@@ -150,12 +150,11 @@ class ProviderStore {
 
   @action async loadOptionsSchema(options: {
     providerName: string,
-    schemaType: 'migration' | 'replica',
     optionsType: 'source' | 'destination',
     useCache?: boolean,
     quietError?: boolean,
   }): Promise<Field[]> {
-    let { schemaType, providerName, optionsType, useCache, quietError } = options
+    let { providerName, optionsType, useCache, quietError } = options
 
     if (optionsType === 'source') {
       this.sourceSchemaLoading = true
@@ -164,7 +163,7 @@ class ProviderStore {
     }
 
     try {
-      let fields: Field[] = await ProviderSource.loadOptionsSchema(providerName, schemaType, optionsType, useCache, quietError)
+      let fields: Field[] = await ProviderSource.loadOptionsSchema(providerName, optionsType, useCache, quietError)
       this.loadOptionsSchemaSuccess(fields, optionsType)
       return fields
     } catch (err) {