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

Add user scripts support to replica

User scripts can now be added when creating a replica and passed on when
creating a migration from that replica.
Sergiu Miclea 5 лет назад
Родитель
Сommit
025fe83754

+ 1 - 1
package.json

@@ -5,7 +5,7 @@
   "scripts": {
     "start": "node ./server",
     "build": "webpack --config webpack.prod.js",
-    "ui-dev": "webpack-dev-server --config webpack.dev.js",
+    "client-dev": "webpack-dev-server --config webpack.dev.js",
     "server-dev": "nodemon -e ts,js -w server/**/",
     "server-debug": "node --inspect server",
     "tsc": "npx tsc --skipLibCheck",

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

@@ -52,7 +52,7 @@ export type InstanceBase = {
 } & Partial<Instance>
 
 export type InstanceScript = {
-  global?: string | null,
+  global?: 'windows' | 'linux' | null,
   instanceId?: string | null,
   scriptContent: string,
   fileName: string,

+ 14 - 3
src/@types/MainItem.ts

@@ -13,7 +13,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 import type { Execution } from './Execution'
-import type { Instance } from './Instance'
+import type { Instance, InstanceScript } from './Instance'
 import type { NetworkMap } from './Network'
 import type { StorageMap } from './Endpoint'
 import { Task } from './Task'
@@ -33,6 +33,7 @@ export type UpdateData = {
   source: any,
   network: NetworkMap[],
   storage: StorageMap[],
+  uploadedScripts: InstanceScript[],
 }
 type NetworkMapSecurityGroups = { id: string, security_groups?: string[] }
 type NetworkMapSourceDest = {
@@ -77,17 +78,27 @@ type BaseItem = {
   network_map?: TransferNetworkMap,
   last_execution_status: string
   user_id: string
-  instance_osmorphing_minion_pool_mappings?: {[instanceName: string]: string}
+  instance_osmorphing_minion_pool_mappings?: { [instanceName: string]: string }
+  user_scripts?: UserScriptData
 }
 
 export type ReplicaItem = BaseItem & {
   type: 'replica',
 }
 
+export type UserScriptData = {
+  global?: {
+    linux?: string | null
+    windows?: string | null
+  }
+  instances?: {
+    [instanceName: string]: string | null
+  }
+}
+
 export type MigrationItem = BaseItem & {
   type: 'migration',
   replica_id?: string,
-  user_scripts?: any
 }
 
 export type MigrationItemOptions = MigrationItem & {

+ 46 - 1
src/components/organisms/EditReplica/EditReplica.tsx

@@ -37,7 +37,9 @@ import type {
 import type { NavigationItem } from '../../molecules/Panel'
 import type { Endpoint, StorageBackend, StorageMap } from '../../../@types/Endpoint'
 import type { Field } from '../../../@types/Field'
-import type { Instance, Nic, Disk } from '../../../@types/Instance'
+import type {
+  Instance, Nic, Disk, InstanceScript,
+} from '../../../@types/Instance'
 import type { Network, NetworkMap, SecurityGroup } from '../../../@types/Network'
 
 import { providerTypes, migrationFields } from '../../../constants'
@@ -45,6 +47,7 @@ import configLoader from '../../../utils/Config'
 import StyleProps from '../../styleUtils/StyleProps'
 import LoadingButton from '../../molecules/LoadingButton/LoadingButton'
 import minionPoolStore from '../../../stores/MinionPoolStore'
+import WizardScripts from '../WizardScripts/WizardScripts'
 
 const PanelContent = styled.div<any>`
   display: flex;
@@ -106,6 +109,7 @@ type State = {
   storageMap: StorageMap[],
   sourceFailed: boolean,
   destinationFailedMessage: string | null,
+  uploadedScripts: InstanceScript[],
 }
 
 @observer
@@ -118,6 +122,7 @@ class EditReplica extends React.Component<Props, State> {
     selectedNetworks: [],
     defaultStorage: undefined,
     storageMap: [],
+    uploadedScripts: [],
     sourceFailed: false,
     destinationFailedMessage: null,
   }
@@ -463,6 +468,7 @@ class EditReplica extends React.Component<Props, State> {
       destination: this.state.destinationData,
       network: this.state.selectedNetworks.length > 0 ? this.getSelectedNetworks() : [],
       storage: this.state.storageMap,
+      uploadedScripts: this.state.uploadedScripts,
     }
     if (this.props.type === 'replica') {
       try {
@@ -512,6 +518,22 @@ class EditReplica extends React.Component<Props, State> {
     })
   }
 
+  handleCancelScript(global: 'windows' | 'linux' | null, instanceName: string | null) {
+    this.setState(prevState => ({
+      uploadedScripts: prevState.uploadedScripts
+        .filter(s => (global ? s.global !== global : s.instanceId !== instanceName)),
+    }))
+  }
+
+  handleScriptUpload(script: InstanceScript) {
+    this.setState(prevState => ({
+      uploadedScripts: [
+        ...prevState.uploadedScripts,
+        script,
+      ],
+    }))
+  }
+
   handleStorageChange(source: Disk, target: StorageBackend, type: 'backend' | 'disk') {
     this.setState(prevState => {
       const diskFieldName = type === 'backend' ? 'storage_backend_identifier' : 'id'
@@ -630,6 +652,21 @@ class EditReplica extends React.Component<Props, State> {
     )
   }
 
+  renderUserScripts() {
+    return (
+      <WizardScripts
+        instances={this.props.instancesDetails}
+        loadingInstances={this.props.instancesDetailsLoading}
+        onScriptUpload={s => { this.handleScriptUpload(s) }}
+        onCancelScript={(g, i) => { this.handleCancelScript(g, i) }}
+        uploadedScripts={this.state.uploadedScripts}
+        userScriptData={this.props.replica?.user_scripts}
+        scrollableRef={(r: HTMLElement) => { this.scrollableRef = r }}
+        style={{ padding: '32px 32px 0 32px', width: 'calc(100% - 64px)' }}
+      />
+    )
+  }
+
   renderContent() {
     let content = null
     switch (this.state.selectedPanel) {
@@ -642,6 +679,9 @@ class EditReplica extends React.Component<Props, State> {
       case 'network_mapping':
         content = this.renderNetworkMapping()
         break
+      case 'user_scripts':
+        content = this.renderUserScripts()
+        break
       case 'storage_mapping':
         content = this.renderStorageMapping()
         break
@@ -704,6 +744,11 @@ class EditReplica extends React.Component<Props, State> {
         label: 'Network Mapping',
         loading: this.isLoadingNetwork(),
       },
+      {
+        value: 'user_scripts',
+        label: 'User Scripts',
+        loading: this.props.instancesDetailsLoading,
+      },
     ]
 
     if (this.hasStorageMap()) {

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

@@ -251,6 +251,7 @@ class ReplicaMigrationOptions extends React.Component<Props, State> {
         onScriptUpload={s => { this.handleScriptUpload(s) }}
         onCancelScript={(g, i) => { this.handleCanceScript(g, i) }}
         uploadedScripts={this.state.uploadedScripts}
+        userScriptData={this.props.transferItem?.user_scripts}
         scrollableRef={(r: HTMLElement) => { this.scrollableRef = r }}
         layout="modal"
       />

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

@@ -508,6 +508,7 @@ class WizardPageContent extends React.Component<Props, State> {
             onScriptUpload={this.props.onUserScriptUpload}
             onCancelScript={this.props.onCancelUploadedScript}
             uploadedScripts={this.props.uploadedUserScripts}
+            userScriptData={null}
           />
         )
         break

+ 37 - 8
src/components/organisms/WizardScripts/WizardScripts.tsx

@@ -28,6 +28,8 @@ import FileUtils from '../../../utils/FileUtils'
 import scriptItemImage from './images/script-item.svg'
 
 import type { Instance, InstanceScript } from '../../../@types/Instance'
+import { UserScriptData } from '../../../@types/MainItem'
+import DomUtils from '../../../utils/DomUtils'
 
 const Wrapper = styled.div<any>`
   width: 100%;
@@ -125,14 +127,22 @@ const FakeFileInput = styled.input`
   opacity: 0;
   top: -99999px;
 `
+const DownloadScriptData = styled.div`
+  color: ${Palette.primary};
+  cursor: pointer;
+  font-size: 12px;
+  margin-top: 8px;
+`
 
 type Props = {
   instances: Instance[],
   uploadedScripts: InstanceScript[],
   layout?: 'modal' | 'page',
   loadingInstances?: boolean,
+  userScriptData: UserScriptData | null | undefined
+  style?: React.CSSProperties
   onScriptUpload: (instanceScript: InstanceScript) => void,
-  onCancelScript: (global: string | null, instanceName: string | null) => void,
+  onCancelScript: (global: 'windows' | 'linux' | null, instanceName: string | null) => void,
   onScrollableRef?: (ref: HTMLElement) => void,
   scrollableRef?: (r: HTMLElement) => void
 }
@@ -147,7 +157,7 @@ class WizardScripts extends React.Component<Props> {
 
   async handleFileUpload(
     files: FileList | null,
-    global: string | null,
+    global: 'windows' | 'linux' | null,
     instanceId: string | null,
   ) {
     if (!files || !files.length) {
@@ -163,8 +173,12 @@ class WizardScripts extends React.Component<Props> {
     })
   }
 
+  handleScriptDataDownload(scriptData: string, fileName: string) {
+    DomUtils.download(scriptData, fileName)
+  }
+
   renderScriptItem(
-    global: string | null,
+    global: 'windows' | 'linux' | null,
     instanceId: string | null,
     title: string,
     subtitle?: string,
@@ -173,6 +187,12 @@ class WizardScripts extends React.Component<Props> {
       s => (s.instanceId
         ? s.instanceId === instanceId : s.global ? s.global === global : false),
     )
+    let scriptData: string | null | undefined = null
+    if (global) {
+      scriptData = this.props.userScriptData?.global?.[global]
+    } else if (instanceId) {
+      scriptData = this.props.userScriptData?.instances?.[instanceId]
+    }
 
     return (
       <Script key={title}>
@@ -181,6 +201,13 @@ class WizardScripts extends React.Component<Props> {
           <NameLabel>
             <NameLabelTitle>{title}</NameLabelTitle>
             {subtitle ? <NameLabelSubtitle>{subtitle}</NameLabelSubtitle> : null}
+            {scriptData ? (
+              <DownloadScriptData onClick={() => {
+                this.handleScriptDataDownload(scriptData as string, title.toLowerCase().replaceAll(' ', '_'))
+              }}
+              >Download the current script
+              </DownloadScriptData>
+            ) : null}
           </NameLabel>
         </Name>
         {uploadedScript ? (
@@ -279,11 +306,13 @@ class WizardScripts extends React.Component<Props> {
 
   render() {
     return (
-      <Wrapper ref={(r: HTMLElement) => {
-        if (this.props.onScrollableRef) {
-          this.props.onScrollableRef(r)
-        }
-      }}
+      <Wrapper
+        style={this.props.style}
+        ref={(r: HTMLElement) => {
+          if (this.props.onScrollableRef) {
+            this.props.onScrollableRef(r)
+          }
+        }}
       >
         {this.renderScriptGroup('global')}
         {this.renderScriptGroup('instance')}

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

@@ -254,6 +254,7 @@ class MigrationDetailsPage extends React.Component<Props, State> {
       replicaId,
       options,
       userScripts,
+      migrationStore.migrationDetails?.user_scripts,
       minionPoolMappings,
     )
     this.props.history.push(`/migrations/${migration.id}/tasks`)

+ 1 - 0
src/components/pages/MigrationsPage/MigrationsPage.tsx

@@ -145,6 +145,7 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
           migration.replica_id,
           replicaMigrationFields,
           [],
+          migration.user_scripts,
           migration.instance_osmorphing_minion_pool_mappings || {},
         )
       } else {

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

@@ -409,6 +409,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       replica.id,
       options,
       uploadedScripts,
+      replica.user_scripts,
       minionPoolMappings,
     )
     notificationStore.alert('Migration successfully created from replica.', 'success', {

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

@@ -161,6 +161,7 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
         fields,
         uploadedScripts.filter(s => !s.instanceId
           || replica.instances.find(i => i === s.instanceId)),
+        replica.user_scripts,
         replica.instance_osmorphing_minion_pool_mappings || {},
       )))
     notificationStore.alert('Migrations successfully created from replicas.', 'success')

+ 1 - 1
src/constants.ts

@@ -96,7 +96,7 @@ export const wizardPages: WizardPage[] = [
   { id: 'networks', title: 'Networks', breadcrumb: 'Networks' },
   { id: 'storage', title: 'Storage Mapping', breadcrumb: 'Storage' },
   {
-    id: 'scripts', title: 'User Scripts', breadcrumb: 'Scripts', excludeFrom: 'replica',
+    id: 'scripts', title: 'User Scripts', breadcrumb: 'Scripts',
   },
   {
     id: 'schedule', title: 'Schedule', breadcrumb: 'Schedule', excludeFrom: 'migration',

+ 8 - 4
src/plugins/endpoint/default/OptionsSchemaPlugin.ts

@@ -24,6 +24,7 @@ import type { SchemaProperties, SchemaDefinitions } from '../../../@types/Schema
 import type { NetworkMap } from '../../../@types/Network'
 import type { InstanceScript } from '../../../@types/Instance'
 import { executionOptions, migrationFields } from '../../../constants'
+import { UserScriptData } from '../../../@types/MainItem'
 
 const migrationImageOsTypes = ['windows', 'linux']
 
@@ -257,18 +258,21 @@ export default class OptionsSchemaParser {
     return payload
   }
 
-  static getUserScripts(uploadedUserScripts: InstanceScript[]) {
-    const payload: any = {}
+  static getUserScripts(
+    uploadedUserScripts: InstanceScript[],
+    userScriptData: UserScriptData | null | undefined,
+  ) {
+    const payload: any = userScriptData || {}
     const globalScripts = uploadedUserScripts.filter(s => s.global)
     if (globalScripts.length) {
-      payload.global = {}
+      payload.global = payload.global || {}
       globalScripts.forEach(script => {
         payload.global[script.global || ''] = script.scriptContent
       })
     }
     const instanceScripts = uploadedUserScripts.filter(s => s.instanceId)
     if (instanceScripts.length) {
-      payload.instances = {}
+      payload.instances = payload.instances || {}
       instanceScripts.forEach(script => {
         payload.instances[script.instanceId || ''] = script.scriptContent
       })

+ 6 - 2
src/plugins/endpoint/openstack/OptionsSchemaPlugin.ts

@@ -24,6 +24,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 { UserScriptData } from '../../../@types/MainItem'
 
 export default class OptionsSchemaParser {
   static migrationImageMapFieldName = DefaultOptionsSchemaPlugin.migrationImageMapFieldName
@@ -102,7 +103,10 @@ export default class OptionsSchemaParser {
     return DefaultOptionsSchemaPlugin.getStorageMap(defaultStorage, storageMap, configDefault)
   }
 
-  static getUserScripts(uploadedUserScripts: InstanceScript[]) {
-    return DefaultOptionsSchemaPlugin.getUserScripts(uploadedUserScripts)
+  static getUserScripts(
+    uploadedUserScripts: InstanceScript[],
+    userScriptData: UserScriptData | null | undefined,
+  ) {
+    return DefaultOptionsSchemaPlugin.getUserScripts(uploadedUserScripts, userScriptData)
   }
 }

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

@@ -24,6 +24,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 { UserScriptData } from '../../../@types/MainItem'
 
 export default class OptionsSchemaParser {
   static migrationImageMapFieldName = 'migr_template_map'
@@ -100,7 +101,10 @@ export default class OptionsSchemaParser {
     return DefaultOptionsSchemaPlugin.getStorageMap(defaultStorage, storageMap, configDefault)
   }
 
-  static getUserScripts(uploadedUserScripts: InstanceScript[]) {
-    return DefaultOptionsSchemaPlugin.getUserScripts(uploadedUserScripts)
+  static getUserScripts(
+    uploadedUserScripts: InstanceScript[],
+    userScriptData: UserScriptData | null | undefined,
+  ) {
+    return DefaultOptionsSchemaPlugin.getUserScripts(uploadedUserScripts, userScriptData)
   }
 }

+ 12 - 6
src/sources/MigrationSource.ts

@@ -26,7 +26,9 @@ import type { Endpoint, StorageMap } from '../@types/Endpoint'
 
 import configLoader from '../utils/Config'
 import { Task } from '../@types/Task'
-import { MigrationItem, MigrationItemOptions, MigrationItemDetails } from '../@types/MainItem'
+import {
+  MigrationItem, MigrationItemOptions, MigrationItemDetails, UserScriptData,
+} from '../@types/MainItem'
 import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from '../components/organisms/WizardOptions/WizardOptions'
 
 class MigrationSourceUtils {
@@ -130,7 +132,8 @@ class MigrationSource {
     updatedNetworkMappings: NetworkMap[] | null,
     defaultSkipOsMorphing: boolean | null,
     replicationCount?: number | null,
-    migration: MigrationItemDetails
+    migration: MigrationItemDetails,
+    uploadedScripts: InstanceScript[]
   }): Promise<MigrationItemDetails> {
     const getValue = (fieldName: string): string | null => {
       const updatedDestEnv = opts.updatedDestEnv && opts.updatedDestEnv[fieldName]
@@ -223,8 +226,9 @@ class MigrationSource {
       ...updatedDestEnv,
     }
 
-    if (migration.user_scripts) {
-      payload.migration.user_scripts = migration.user_scripts
+    if (opts.uploadedScripts?.length || migration.user_scripts) {
+      payload.migration.user_scripts = DefaultOptionsSchemaPlugin
+        .getUserScripts(opts.uploadedScripts, migration.user_scripts)
     }
 
     const response = await Api.send({
@@ -260,6 +264,7 @@ class MigrationSource {
     replicaId: string,
     options: Field[],
     userScripts: InstanceScript[],
+    userScriptData: UserScriptData | null | undefined,
     minionPoolMappings: { [instance: string]: string },
   ): Promise<MigrationItem> {
     const payload: any = {
@@ -271,8 +276,9 @@ class MigrationSource {
       payload.migration[o.name] = o.value || o.default || false
     })
 
-    if (userScripts.length) {
-      payload.migration.user_scripts = DefaultOptionsSchemaPlugin.getUserScripts(userScripts)
+    if (userScripts.length || userScriptData) {
+      payload.migration.user_scripts = DefaultOptionsSchemaPlugin
+        .getUserScripts(userScripts, userScriptData)
     }
 
     if (Object.keys(minionPoolMappings).length) {

+ 6 - 0
src/sources/ReplicaSource.ts

@@ -16,6 +16,7 @@ import moment from 'moment'
 
 import Api from '../utils/ApiCaller'
 import { OptionsSchemaPlugin } from '../plugins/endpoint'
+import DefaultOptionsSchemaPlugin from '../plugins/endpoint/default/OptionsSchemaPlugin'
 
 import configLoader from '../utils/Config'
 import type { UpdateData, ReplicaItem, ReplicaItemDetails } from '../@types/MainItem'
@@ -267,6 +268,11 @@ class ReplicaSource {
         .getStorageMap(defaultStorage, updateData.storage, storageConfigDefault)
     }
 
+    if (updateData.uploadedScripts?.length) {
+      payload.replica.user_scripts = DefaultOptionsSchemaPlugin
+        .getUserScripts(updateData.uploadedScripts, replica.user_scripts)
+    }
+
     const response = await Api.send({
       url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replica.id}`,
       method: 'PUT',

+ 5 - 5
src/sources/WizardSource.ts

@@ -93,12 +93,12 @@ class WizardSource {
       data.destOptions && data.destOptions.shutdown_instances,
     )
 
+    if (uploadedUserScripts.length) {
+      payload[type].user_scripts = destParser.getUserScripts(uploadedUserScripts, {})
+    }
+
     if (type === 'migration') {
-      payload[type].replication_count = (
-        data.destOptions && data.destOptions.replication_count) || 2
-      if (uploadedUserScripts.length) {
-        payload[type].user_scripts = destParser.getUserScripts(uploadedUserScripts)
-      }
+      payload[type].replication_count = data.destOptions?.replication_count || 2
     }
 
     const response = await Api.send({

+ 4 - 1
src/stores/MigrationStore.ts

@@ -15,7 +15,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import { observable, action, runInAction } from 'mobx'
 
 import type {
-  UpdateData, MigrationItem, MigrationItemDetails, MigrationItemOptions,
+  UpdateData, MigrationItem, MigrationItemDetails, MigrationItemOptions, UserScriptData,
 } from '../@types/MainItem'
 import type { Field } from '../@types/Field'
 import type { Endpoint } from '../@types/Endpoint'
@@ -89,6 +89,7 @@ class MigrationStore {
       updatedNetworkMappings: updateData.network,
       defaultSkipOsMorphing: this.getDefaultSkipOsMorphing(migration),
       replicationCount,
+      uploadedScripts: updateData.uploadedScripts,
     })
     return migrationResult
   }
@@ -124,12 +125,14 @@ class MigrationStore {
     replicaId: string,
     options: Field[],
     userScripts: InstanceScript[],
+    userScriptData: UserScriptData | null | undefined,
     minionPoolMappings: { [instance: string]: string },
   ) {
     const migration = await MigrationSource.migrateReplica(
       replicaId,
       options,
       userScripts,
+      userScriptData,
       minionPoolMappings,
     )
     runInAction(() => {