/* 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 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' import type { Execution, ExecutionTasks } from '../@types/Execution' import type { Endpoint } from '../@types/Endpoint' import type { Task, ProgressUpdate } from '../@types/Task' import type { Field } from '../@types/Field' import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from '../components/organisms/WizardOptions/WizardOptions' export const sortTasks = ( tasks?: Task[], taskUpdatesSortFunction?: (updates: ProgressUpdate[]) => void, ) => { if (!tasks) { return } let sortedTasks: any[] = [] let buffer: Task[] = [] let runningBuffer: Task[] = [] let completedBuffer: Task[] = [] if (!taskUpdatesSortFunction) { return } tasks.forEach(task => { taskUpdatesSortFunction(task.progress_updates) buffer.push(task) if (task.status === 'RUNNING') { runningBuffer.push(task) } else if (task.status === 'COMPLETED' || task.status === 'ERROR') { completedBuffer.push(task) } else { if (runningBuffer.length >= 2) { sortedTasks = sortedTasks.concat([...completedBuffer, ...runningBuffer, task]) } else { sortedTasks = sortedTasks.concat([...buffer]) } buffer = [] runningBuffer = [] completedBuffer = [] } }) if (buffer.length) { if (runningBuffer.length >= 2) { sortedTasks = sortedTasks.concat([...completedBuffer, ...runningBuffer]) } else { sortedTasks = sortedTasks.concat([...buffer]) } } tasks.splice(0, tasks.length, ...sortedTasks) } export class ReplicaSourceUtils { static filterDeletedExecutions(executions?: Execution[]) { if (!executions || !executions.length) { return [] } return executions.filter(execution => execution.deleted_at == null) } static sortReplicas(replicas: ReplicaItem[]) { replicas .sort( (a, b) => new Date(b.updated_at || b.created_at).getTime() - new Date(a.updated_at || a.created_at) .getTime(), ) } static sortExecutions(executions: Execution[]) { executions.sort((a, b) => a.number - b.number) } static sortTaskUpdates(updates: ProgressUpdate[]) { if (!updates) { return } updates.sort((a, b) => a.index - b.index) } } class ReplicaSource { async getReplicas(skipLog?: boolean, quietError?: boolean): Promise { const response = await Api.send({ url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas`, skipLog, quietError, }) const replicas: ReplicaItem[] = response.data.replicas ReplicaSourceUtils.sortReplicas(replicas) return replicas } async getReplicaDetails(options: { replicaId: string, polling?: boolean }): Promise { const { replicaId, polling } = options const response = await Api.send({ url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}`, skipLog: polling, }) const replica: ReplicaItemDetails = response.data.replica replica.executions = ReplicaSourceUtils.filterDeletedExecutions(replica.executions) ReplicaSourceUtils.sortExecutions(replica.executions) return replica } async getExecutionTasks(options: { replicaId: string, executionId?: string, polling?: boolean, }): Promise { const { replicaId, executionId, polling, } = options const response = await Api.send({ url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}/executions/${executionId}`, skipLog: polling, quietError: true, }) const execution: ExecutionTasks = response.data.execution sortTasks(execution.tasks, ReplicaSourceUtils.sortTaskUpdates) return execution } async execute(replicaId: string, fields?: Field[]): Promise { const payload: any = { execution: { shutdown_instances: false } } if (fields) { fields.forEach(f => { payload.execution[f.name] = f.value || false }) } const response = await Api.send({ url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}/executions`, method: 'POST', data: payload, }) const execution: ExecutionTasks = response.data.execution sortTasks(execution.tasks, ReplicaSourceUtils.sortTaskUpdates) return execution } async cancelExecution( options: { replicaId: string, executionId?: string, force?: boolean }, ): Promise { const data: any = { cancel: null } if (options.force) { data.cancel = { force: true } } let lastExecutionId = options.executionId if (!lastExecutionId) { const replicaDetails = await this.getReplicaDetails({ replicaId: options.replicaId }) const lastExecution = replicaDetails.executions[replicaDetails.executions.length - 1] if (lastExecution.status !== 'RUNNING') { return options.replicaId } lastExecutionId = lastExecution.id } await Api.send({ url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${options.replicaId}/executions/${lastExecutionId}/actions`, method: 'POST', data, }) return options.replicaId } async deleteExecution(replicaId: string, executionId: string): Promise { await Api.send({ url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}/executions/${executionId}`, method: 'DELETE', }) return replicaId } async delete(replicaId: string): Promise { await Api.send({ url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}`, method: 'DELETE', }) return replicaId } async deleteDisks(replicaId: string): Promise { const response = await Api.send({ url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}/actions`, method: 'POST', data: { 'delete-disks': null }, }) return response.data.execution } async update(options: { replica: ReplicaItemDetails, sourceEndpoint: Endpoint, destinationEndpoint: Endpoint, updateData: UpdateData, defaultStorage: string | null | undefined, storageConfigDefault: string, }): Promise { const { replica, destinationEndpoint, updateData, defaultStorage, storageConfigDefault, sourceEndpoint, } = options const sourceParser = OptionsSchemaPlugin.for(sourceEndpoint.type) const destinationParser = OptionsSchemaPlugin.for(destinationEndpoint.type) const payload: any = { replica: {} } if (updateData.destination.title) { payload.replica.notes = updateData.destination.title } if (updateData.network.length > 0) { payload.replica.network_map = destinationParser.getNetworkMap(updateData.network) } if (Object.keys(updateData.source).length > 0) { const sourceEnv = sourceParser.getDestinationEnv(updateData.source, replica.source_environment) if (updateData.source.minion_pool_id !== undefined) { payload.replica.origin_minion_pool_id = updateData.source.minion_pool_id } if (Object.keys(sourceEnv).length) { payload.replica.source_environment = sourceEnv } } if (Object.keys(updateData.destination).length > 0) { const destEnv = destinationParser.getDestinationEnv(updateData.destination, { ...replica, ...replica.destination_environment }) const newMinionMappings = destEnv[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] if (newMinionMappings) { payload.replica[ INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS ] = newMinionMappings } delete destEnv[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] if (updateData.destination.minion_pool_id !== undefined) { payload.replica.destination_minion_pool_id = updateData.destination.minion_pool_id } if (Object.keys(destEnv).length) { payload.replica.destination_environment = destEnv } } if (defaultStorage || updateData.storage.length > 0) { payload.replica.storage_mappings = destinationParser .getStorageMap(defaultStorage, updateData.storage, storageConfigDefault) } if (updateData.uploadedScripts?.length || updateData.removedScripts?.length) { payload.replica.user_scripts = DefaultOptionsSchemaPlugin .getUserScripts( updateData.uploadedScripts || [], updateData.removedScripts || [], replica.user_scripts, ) } const response = await Api.send({ url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replica.id}`, method: 'PUT', data: payload, }) return response.data } } export default new ReplicaSource()