/*
Copyright (C) 2017 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 .
*/
import moment from 'moment'
import { OptionsSchemaPlugin } from '../plugins/endpoint'
import DefaultOptionsSchemaPlugin from '../plugins/endpoint/default/OptionsSchemaPlugin'
import { sortTasks } from './ReplicaSource'
import Api from '../utils/ApiCaller'
import type { InstanceScript } from '../@types/Instance'
import type { Field } from '../@types/Field'
import type { NetworkMap } from '../@types/Network'
import type { Endpoint, StorageMap } from '../@types/Endpoint'
import configLoader from '../utils/Config'
import { ProgressUpdate, Task } from '../@types/Task'
import {
MigrationItem, MigrationItemOptions, MigrationItemDetails, UserScriptData,
} from '../@types/MainItem'
import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from '../components/organisms/WizardOptions/WizardOptions'
class MigrationSourceUtils {
static sortTaskUpdates(updates: ProgressUpdate[]) {
if (!updates) {
return
}
updates.sort((a, b) => {
const sortNull = !a && b ? 1 : a && !b ? -1 : !a && !b ? 0 : false
if (sortNull !== false) {
return sortNull
}
return a.index - b.index
})
}
static sortMigrations(migrations: any[]) {
migrations.sort((a: any, b: any) => moment(b.created_at).diff(moment(a.created_at)))
migrations.forEach((migration: { tasks: Task[] }) => {
sortTasks(migration.tasks, MigrationSourceUtils.sortTaskUpdates)
})
}
}
class MigrationSource {
async getMigrations(skipLog?: boolean): Promise {
const response = await Api.send({
url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/migrations`,
skipLog,
})
const migrations = response.data.migrations
MigrationSourceUtils.sortMigrations(migrations)
return migrations
}
async getMigration(migrationId: string, skipLog?: boolean): Promise {
const response = await Api.send({
url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/migrations/${migrationId}`,
skipLog,
})
const migration = response.data.migration
sortTasks(migration.tasks, MigrationSourceUtils.sortTaskUpdates)
return migration
}
async recreateFullCopy(migration: MigrationItemOptions): Promise {
const {
origin_endpoint_id, destination_endpoint_id, destination_environment,
network_map, instances, storage_mappings, notes, destination_minion_pool_id,
origin_minion_pool_id, instance_osmorphing_minion_pool_mappings,
} = migration
const payload: any = {
migration: {
origin_endpoint_id,
destination_endpoint_id,
destination_environment,
network_map,
instances,
storage_mappings,
notes,
destination_minion_pool_id,
origin_minion_pool_id,
instance_osmorphing_minion_pool_mappings,
},
}
if (migration.skip_os_morphing != null) {
payload.migration.skip_os_morphing = migration.skip_os_morphing
}
if (migration.source_environment) {
payload.migration.source_environment = migration.source_environment
}
payload.migration.shutdown_instances = Boolean(migration.shutdown_instances)
payload.migration.replication_count = migration.replication_count || 2
const response = await Api.send({
url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/migrations`,
method: 'POST',
data: payload,
})
return response.data.migration
}
async recreate(opts: {
sourceEndpoint: Endpoint,
destEndpoint: Endpoint,
instanceNames: string[],
destEnv: { [prop: string]: any } | null,
updatedDestEnv: { [prop: string]: any } | null,
sourceEnv?: { [prop: string]: any } | null,
updatedSourceEnv?: { [prop: string]: any } | null,
storageMappings?: { [prop: string]: any } | null,
updatedStorageMappings: StorageMap[] | null,
defaultStorage?: string | null,
updatedDefaultStorage?: string | null,
networkMappings?: any,
updatedNetworkMappings: NetworkMap[] | null,
defaultSkipOsMorphing: boolean | null,
replicationCount?: number | null,
migration: MigrationItemDetails,
uploadedScripts: InstanceScript[]
removedScripts: InstanceScript[]
}): Promise {
const getValue = (fieldName: string): string | null => {
const updatedDestEnv = opts.updatedDestEnv && opts.updatedDestEnv[fieldName]
return updatedDestEnv != null ? updatedDestEnv
: (opts.destEnv && opts.destEnv[fieldName])
}
const sourceParser = OptionsSchemaPlugin.for(opts.sourceEndpoint.type)
const destParser = OptionsSchemaPlugin.for(opts.destEndpoint.type)
const payload: any = {}
payload.migration = {
origin_endpoint_id: opts.sourceEndpoint.id,
destination_endpoint_id: opts.destEndpoint.id,
shutdown_instances: Boolean(opts.updatedDestEnv && opts.updatedDestEnv.shutdown_instances),
replication_count: (opts.updatedDestEnv
&& opts.updatedDestEnv.replication_count) || opts.replicationCount || 2,
instances: opts.instanceNames,
notes: getValue('title') || getValue('notes') || '',
}
const skipOsMorphingValue = getValue('skip_os_morphing')
if (skipOsMorphingValue != null) {
payload.migration.skip_os_morphing = skipOsMorphingValue
} else if (opts.defaultSkipOsMorphing != null) {
payload.migration.skip_os_morphing = opts.defaultSkipOsMorphing
}
if (opts.networkMappings
|| (opts.updatedNetworkMappings && opts.updatedNetworkMappings.length)) {
payload.migration.network_map = {
...opts.networkMappings,
...destParser.getNetworkMap(opts.updatedNetworkMappings),
}
}
if ((opts.storageMappings && Object.keys(opts.storageMappings).length)
|| (opts.updatedStorageMappings && opts.updatedStorageMappings.length)) {
payload.migration.storage_mappings = {
...opts.storageMappings,
...destParser
.getStorageMap(opts.updatedDefaultStorage
|| opts.defaultStorage, opts.updatedStorageMappings),
}
}
const { migration } = opts
const sourceEnv: any = {
...opts.sourceEnv,
}
const updatedSourceEnv = opts.updatedSourceEnv
? sourceParser.getDestinationEnv(opts.updatedSourceEnv) : {}
const sourceMinionPoolId = opts?.updatedSourceEnv?.minion_pool_id
|| migration.origin_minion_pool_id
if (sourceMinionPoolId) {
payload.migration.origin_minion_pool_id = sourceMinionPoolId
}
payload.migration.source_environment = {
...sourceEnv,
...updatedSourceEnv,
}
const destEnv: any = {
...opts.destEnv,
}
const updatedDestEnv = opts.updatedDestEnv
? sourceParser.getDestinationEnv(opts.updatedDestEnv) : {}
const destMinionPoolId = opts?.updatedDestEnv?.minion_pool_id
|| migration.destination_minion_pool_id
if (destMinionPoolId) {
payload.migration.destination_minion_pool_id = destMinionPoolId
}
const updatedDestEnvMappings = updatedDestEnv[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] || {}
const oldMappings = migration[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] || {}
const mergedMappings = { ...oldMappings, ...updatedDestEnvMappings }
if (Object.keys(mergedMappings).length) {
const newMappings: any = {}
Object.keys(mergedMappings).forEach(k => {
if (mergedMappings[k] !== null) {
newMappings[k] = mergedMappings[k]
}
})
payload.migration[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] = newMappings
}
delete updatedDestEnv[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS]
payload.migration.destination_environment = {
...destEnv,
...updatedDestEnv,
}
if (opts.uploadedScripts?.length || opts.removedScripts?.length || migration.user_scripts) {
payload.migration.user_scripts = DefaultOptionsSchemaPlugin
.getUserScripts(
opts.uploadedScripts || [],
opts.removedScripts || [],
migration.user_scripts,
)
}
const response = await Api.send({
url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/migrations`,
method: 'POST',
data: payload,
})
return response.data.migration
}
async cancel(migrationId: string, force?: boolean | null): Promise {
const data: any = { cancel: null }
if (force) {
data.cancel = { force: true }
}
await Api.send({
url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/migrations/${migrationId}/actions`,
method: 'POST',
data,
})
return migrationId
}
async delete(migrationId: string): Promise {
await Api.send({
url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/migrations/${migrationId}`,
method: 'DELETE',
})
return migrationId
}
async migrateReplica(
replicaId: string,
options: Field[],
uploadedUserScripts: InstanceScript[],
removedUserScripts: InstanceScript[],
userScriptData: UserScriptData | null | undefined,
minionPoolMappings: { [instance: string]: string },
): Promise {
const payload: any = {
migration: {
replica_id: replicaId,
},
}
options.forEach(o => {
payload.migration[o.name] = o.value || o.default || false
})
if (uploadedUserScripts.length || removedUserScripts.length || userScriptData) {
payload.migration.user_scripts = DefaultOptionsSchemaPlugin
.getUserScripts(uploadedUserScripts, removedUserScripts, userScriptData)
}
if (Object.keys(minionPoolMappings).length) {
const newMappings: any = {}
Object.keys(minionPoolMappings).forEach(k => {
if (minionPoolMappings[k] !== null) {
newMappings[k] = minionPoolMappings[k]
}
})
payload.migration[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] = newMappings
} else {
payload.migration[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] = null
}
const response = await Api.send({
url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/migrations`,
method: 'POST',
data: payload,
})
return response.data.migration
}
}
export default new MigrationSource()