ReplicaSource.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. /*
  2. Copyright (C) 2017 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. import Api from '../utils/ApiCaller'
  15. import { OptionsSchemaPlugin } from '../plugins/endpoint'
  16. import DefaultOptionsSchemaPlugin from '../plugins/endpoint/default/OptionsSchemaPlugin'
  17. import configLoader from '../utils/Config'
  18. import type { UpdateData, ReplicaItem, ReplicaItemDetails } from '../@types/MainItem'
  19. import type { Execution, ExecutionTasks } from '../@types/Execution'
  20. import type { Endpoint } from '../@types/Endpoint'
  21. import type { Task, ProgressUpdate } from '../@types/Task'
  22. import type { Field } from '../@types/Field'
  23. import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from '../components/organisms/WizardOptions/WizardOptions'
  24. export const sortTasks = (
  25. tasks?: Task[], taskUpdatesSortFunction?: (updates: ProgressUpdate[]) => void,
  26. ) => {
  27. if (!tasks) {
  28. return
  29. }
  30. let sortedTasks: any[] = []
  31. let buffer: Task[] = []
  32. let runningBuffer: Task[] = []
  33. let completedBuffer: Task[] = []
  34. if (!taskUpdatesSortFunction) {
  35. return
  36. }
  37. tasks.forEach(task => {
  38. taskUpdatesSortFunction(task.progress_updates)
  39. buffer.push(task)
  40. if (task.status === 'RUNNING') {
  41. runningBuffer.push(task)
  42. } else if (task.status === 'COMPLETED' || task.status === 'ERROR') {
  43. completedBuffer.push(task)
  44. } else {
  45. if (runningBuffer.length >= 2) {
  46. sortedTasks = sortedTasks.concat([...completedBuffer, ...runningBuffer, task])
  47. } else {
  48. sortedTasks = sortedTasks.concat([...buffer])
  49. }
  50. buffer = []
  51. runningBuffer = []
  52. completedBuffer = []
  53. }
  54. })
  55. if (buffer.length) {
  56. if (runningBuffer.length >= 2) {
  57. sortedTasks = sortedTasks.concat([...completedBuffer, ...runningBuffer])
  58. } else {
  59. sortedTasks = sortedTasks.concat([...buffer])
  60. }
  61. }
  62. tasks.splice(0, tasks.length, ...sortedTasks)
  63. }
  64. export class ReplicaSourceUtils {
  65. static filterDeletedExecutions(executions?: Execution[]) {
  66. if (!executions || !executions.length) {
  67. return []
  68. }
  69. return executions.filter(execution => execution.deleted_at == null)
  70. }
  71. static sortReplicas(replicas: ReplicaItem[]) {
  72. replicas
  73. .sort(
  74. (a, b) => new Date(b.updated_at || b.created_at).getTime()
  75. - new Date(a.updated_at || a.created_at)
  76. .getTime(),
  77. )
  78. }
  79. static sortExecutions(executions: Execution[]) {
  80. executions.sort((a, b) => a.number - b.number)
  81. }
  82. static sortTaskUpdates(updates: ProgressUpdate[]) {
  83. if (!updates) {
  84. return
  85. }
  86. updates.sort((a, b) => a.index - b.index)
  87. }
  88. }
  89. class ReplicaSource {
  90. async getReplicas(skipLog?: boolean, quietError?: boolean): Promise<ReplicaItem[]> {
  91. const response = await Api.send({
  92. url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas`,
  93. skipLog,
  94. quietError,
  95. })
  96. const replicas: ReplicaItem[] = response.data.replicas
  97. ReplicaSourceUtils.sortReplicas(replicas)
  98. return replicas
  99. }
  100. async getReplicaDetails(options: {
  101. replicaId: string, polling?: boolean
  102. }): Promise<ReplicaItemDetails> {
  103. const { replicaId, polling } = options
  104. const response = await Api.send({
  105. url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}`,
  106. skipLog: polling,
  107. })
  108. const replica: ReplicaItemDetails = response.data.replica
  109. replica.executions = ReplicaSourceUtils.filterDeletedExecutions(replica.executions)
  110. ReplicaSourceUtils.sortExecutions(replica.executions)
  111. return replica
  112. }
  113. async getExecutionTasks(options: {
  114. replicaId: string,
  115. executionId?: string,
  116. polling?: boolean,
  117. }): Promise<ExecutionTasks> {
  118. const {
  119. replicaId, executionId, polling,
  120. } = options
  121. const response = await Api.send({
  122. url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}/executions/${executionId}`,
  123. skipLog: polling,
  124. quietError: true,
  125. })
  126. const execution: ExecutionTasks = response.data.execution
  127. sortTasks(execution.tasks, ReplicaSourceUtils.sortTaskUpdates)
  128. return execution
  129. }
  130. async execute(replicaId: string, fields?: Field[]): Promise<ExecutionTasks> {
  131. const payload: any = { execution: { shutdown_instances: false } }
  132. if (fields) {
  133. fields.forEach(f => {
  134. payload.execution[f.name] = f.value || false
  135. })
  136. }
  137. const response = await Api.send({
  138. url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}/executions`,
  139. method: 'POST',
  140. data: payload,
  141. })
  142. const execution: ExecutionTasks = response.data.execution
  143. sortTasks(execution.tasks, ReplicaSourceUtils.sortTaskUpdates)
  144. return execution
  145. }
  146. async cancelExecution(
  147. options: { replicaId: string, executionId?: string, force?: boolean },
  148. ): Promise<string> {
  149. const data: any = { cancel: null }
  150. if (options.force) {
  151. data.cancel = { force: true }
  152. }
  153. let lastExecutionId = options.executionId
  154. if (!lastExecutionId) {
  155. const replicaDetails = await this.getReplicaDetails({ replicaId: options.replicaId })
  156. const lastExecution = replicaDetails.executions[replicaDetails.executions.length - 1]
  157. if (lastExecution.status !== 'RUNNING') {
  158. return options.replicaId
  159. }
  160. lastExecutionId = lastExecution.id
  161. }
  162. await Api.send({
  163. url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${options.replicaId}/executions/${lastExecutionId}/actions`,
  164. method: 'POST',
  165. data,
  166. })
  167. return options.replicaId
  168. }
  169. async deleteExecution(replicaId: string, executionId: string): Promise<string> {
  170. await Api.send({
  171. url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}/executions/${executionId}`,
  172. method: 'DELETE',
  173. })
  174. return replicaId
  175. }
  176. async delete(replicaId: string): Promise<string> {
  177. await Api.send({
  178. url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}`,
  179. method: 'DELETE',
  180. })
  181. return replicaId
  182. }
  183. async deleteDisks(replicaId: string): Promise<Execution> {
  184. const response = await Api.send({
  185. url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}/actions`,
  186. method: 'POST',
  187. data: { 'delete-disks': null },
  188. })
  189. return response.data.execution
  190. }
  191. async update(options: {
  192. replica: ReplicaItemDetails,
  193. sourceEndpoint: Endpoint,
  194. destinationEndpoint: Endpoint,
  195. updateData: UpdateData,
  196. defaultStorage: string | null | undefined,
  197. storageConfigDefault: string,
  198. }): Promise<Execution> {
  199. const {
  200. replica,
  201. destinationEndpoint,
  202. updateData,
  203. defaultStorage,
  204. storageConfigDefault,
  205. sourceEndpoint,
  206. } = options
  207. const sourceParser = OptionsSchemaPlugin.for(sourceEndpoint.type)
  208. const destinationParser = OptionsSchemaPlugin.for(destinationEndpoint.type)
  209. const payload: any = { replica: {} }
  210. if (updateData.destination.title) {
  211. payload.replica.notes = updateData.destination.title
  212. }
  213. if (updateData.network.length > 0) {
  214. payload.replica.network_map = destinationParser.getNetworkMap(updateData.network)
  215. }
  216. if (Object.keys(updateData.source).length > 0) {
  217. const sourceEnv = sourceParser.getDestinationEnv(updateData.source, replica.source_environment)
  218. if (updateData.source.minion_pool_id !== undefined) {
  219. payload.replica.origin_minion_pool_id = updateData.source.minion_pool_id
  220. }
  221. if (Object.keys(sourceEnv).length) {
  222. payload.replica.source_environment = sourceEnv
  223. }
  224. }
  225. if (Object.keys(updateData.destination).length > 0) {
  226. const destEnv = destinationParser.getDestinationEnv(updateData.destination,
  227. { ...replica, ...replica.destination_environment })
  228. const newMinionMappings = destEnv[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS]
  229. if (newMinionMappings) {
  230. payload.replica[
  231. INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS
  232. ] = newMinionMappings
  233. }
  234. delete destEnv[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS]
  235. if (updateData.destination.minion_pool_id !== undefined) {
  236. payload.replica.destination_minion_pool_id = updateData.destination.minion_pool_id
  237. }
  238. if (Object.keys(destEnv).length) {
  239. payload.replica.destination_environment = destEnv
  240. }
  241. }
  242. if (defaultStorage || updateData.storage.length > 0) {
  243. payload.replica.storage_mappings = destinationParser
  244. .getStorageMap(defaultStorage, updateData.storage, storageConfigDefault)
  245. }
  246. if (updateData.uploadedScripts?.length || updateData.removedScripts?.length) {
  247. payload.replica.user_scripts = DefaultOptionsSchemaPlugin
  248. .getUserScripts(
  249. updateData.uploadedScripts || [],
  250. updateData.removedScripts || [],
  251. replica.user_scripts,
  252. )
  253. }
  254. const response = await Api.send({
  255. url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replica.id}`,
  256. method: 'PUT',
  257. data: payload,
  258. })
  259. return response.data
  260. }
  261. }
  262. export default new ReplicaSource()