OptionsSchemaPlugin.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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. /* eslint-disable no-param-reassign */
  15. import Utils from '@src/utils/ObjectUtils'
  16. import type { Field } from '@src/@types/Field'
  17. import type { OptionValues, StorageMap } from '@src/@types/Endpoint'
  18. import type { SchemaProperties, SchemaDefinitions } from '@src/@types/Schema'
  19. import type { NetworkMap } from '@src/@types/Network'
  20. import type { InstanceScript } from '@src/@types/Instance'
  21. import { executionOptions, migrationFields } from '@src/constants'
  22. import { UserScriptData } from '@src/@types/MainItem'
  23. import { defaultSchemaToFields } from './ConnectionSchemaPlugin'
  24. const migrationImageOsTypes = ['windows', 'linux']
  25. export const defaultFillFieldValues = (field: Field, option: OptionValues) => {
  26. if (field.type === 'string') {
  27. field.enum = [...option.values]
  28. if (option.config_default) {
  29. field.default = typeof option.config_default === 'string' ? option.config_default : option.config_default.id
  30. }
  31. }
  32. if (field.type === 'array') {
  33. field.enum = [...option.values]
  34. }
  35. if (field.type === 'boolean' && option.config_default != null) {
  36. field.default = typeof option.config_default === 'boolean' ? option.config_default : option.config_default === 'true'
  37. }
  38. }
  39. export const defaultFillMigrationImageMapValues = (opts: {
  40. field: Field,
  41. option: OptionValues,
  42. migrationImageMapFieldName: string,
  43. requiresWindowsImage: boolean,
  44. }): boolean => {
  45. const {
  46. field, option, migrationImageMapFieldName, requiresWindowsImage,
  47. } = opts
  48. if (field.name !== migrationImageMapFieldName) {
  49. return false
  50. }
  51. field.properties = migrationImageOsTypes.map(os => {
  52. const values = (option.values as any)
  53. .filter((v: { os_type: string }) => v.os_type === os || v.os_type === 'unknown')
  54. .sort((v1: { os_type: string }, v2: { os_type: string }) => {
  55. if (v1.os_type === 'unknown' && v2.os_type !== 'unknown') {
  56. return 1
  57. } if (v1.os_type !== 'unknown' && v2.os_type === 'unknown') {
  58. return -1
  59. }
  60. return 0
  61. })
  62. const unknownIndex = values.findIndex((v: { os_type: string }) => v.os_type === 'unknown')
  63. if (unknownIndex > -1 && values.filter((v: { os_type: string }) => v.os_type === 'unknown').length < values.length) {
  64. values.splice(unknownIndex, 0, { separator: true })
  65. }
  66. let defaultValue = null
  67. if (option?.config_default && Object.prototype.hasOwnProperty.call(option.config_default, os)) {
  68. // @ts-ignore
  69. defaultValue = option.config_default[os]
  70. }
  71. return {
  72. name: os,
  73. type: 'string',
  74. enum: values,
  75. default: defaultValue,
  76. required: os === 'linux' || (requiresWindowsImage && os === 'windows'),
  77. }
  78. })
  79. return true
  80. }
  81. export const defaultGetDestinationEnv = (
  82. options?: { [prop: string]: any } | null,
  83. oldOptions?: { [prop: string]: any } | null,
  84. ): any => {
  85. const env: any = {}
  86. const specialOptions = ['execute_now', 'execute_now_options', 'separate_vm', 'skip_os_morphing', 'title', 'minion_pool_id']
  87. .concat(migrationFields.map(f => f.name))
  88. .concat(executionOptions.map(o => o.name))
  89. .concat(migrationImageOsTypes)
  90. if (!options) {
  91. return env
  92. }
  93. Object.keys(options).forEach(optionName => {
  94. const value = options[optionName]
  95. if (specialOptions.find(o => o === optionName) || value == null || value === '') {
  96. return
  97. }
  98. if (Array.isArray(value)) {
  99. env[optionName] = value
  100. } else if (typeof value === 'object') {
  101. const oldValue = oldOptions?.[optionName] || {}
  102. const mergedValue: any = { ...oldValue, ...value }
  103. const newValue: any = {}
  104. Object.keys(mergedValue).forEach(k => {
  105. if (mergedValue[k] !== null) {
  106. newValue[k] = mergedValue[k]
  107. }
  108. })
  109. env[optionName] = newValue
  110. } else {
  111. env[optionName] = options ? Utils.trim(optionName, value) : null
  112. }
  113. })
  114. return env
  115. }
  116. export const defaultGetMigrationImageMap = (
  117. options: { [prop: string]: any } | null | undefined,
  118. oldOptions: any,
  119. migrationImageMapFieldName: string,
  120. ) => {
  121. const env: any = {}
  122. const usableOptions = options
  123. if (!usableOptions) {
  124. return env
  125. }
  126. const hasMigrationMap = Object.keys(usableOptions).find(k => k === migrationImageMapFieldName)
  127. if (!hasMigrationMap) {
  128. return env
  129. }
  130. migrationImageOsTypes.forEach(os => {
  131. let value = usableOptions[migrationImageMapFieldName][os]
  132. // Make sure the migr. image mapping has all the OSes filled,
  133. // even if only one OS mapping was updated,
  134. // ie. don't send just the updated OS map to the server, send them all if one was updated.
  135. if (!value) {
  136. value = oldOptions?.[migrationImageMapFieldName]?.[os]
  137. if (!value) {
  138. return
  139. }
  140. }
  141. if (!env[migrationImageMapFieldName]) {
  142. env[migrationImageMapFieldName] = {}
  143. }
  144. env[migrationImageMapFieldName][os] = value
  145. })
  146. return env
  147. }
  148. export default class OptionsSchemaParser {
  149. static migrationImageMapFieldName = 'migr_image_map'
  150. static parseSchemaToFields(opts: {
  151. schema: SchemaProperties,
  152. schemaDefinitions?: SchemaDefinitions | null,
  153. dictionaryKey?: string,
  154. requiresWindowsImage?: boolean,
  155. }) {
  156. const {
  157. schema, schemaDefinitions, dictionaryKey,
  158. } = opts
  159. return defaultSchemaToFields(schema, schemaDefinitions, dictionaryKey)
  160. }
  161. static sortFields(fields: Field[]) {
  162. fields.sort((a, b) => {
  163. if (a.required && !b.required) {
  164. return -1
  165. }
  166. if (!a.required && b.required) {
  167. return 1
  168. }
  169. return a.name.localeCompare(b.name)
  170. })
  171. }
  172. static fillFieldValues(opts: {
  173. field: Field,
  174. options: OptionValues[],
  175. requiresWindowsImage: boolean,
  176. customFieldName?: string,
  177. }) {
  178. const {
  179. field, options, requiresWindowsImage, customFieldName,
  180. } = opts
  181. const option = options.find(f => (customFieldName ? f.name === customFieldName : f.name === field.name))
  182. if (!option) {
  183. return
  184. }
  185. if (!defaultFillMigrationImageMapValues({
  186. field,
  187. option,
  188. migrationImageMapFieldName: this.migrationImageMapFieldName,
  189. requiresWindowsImage,
  190. })) {
  191. defaultFillFieldValues(field, option)
  192. }
  193. }
  194. static getDestinationEnv(options?: { [prop: string]: any } | null, oldOptions?: any) {
  195. const env = {
  196. ...defaultGetDestinationEnv(
  197. options,
  198. oldOptions,
  199. ),
  200. ...defaultGetMigrationImageMap(
  201. options,
  202. oldOptions,
  203. this.migrationImageMapFieldName,
  204. ),
  205. }
  206. return env
  207. }
  208. static getNetworkMap(networkMappings: NetworkMap[] | null | undefined) {
  209. const payload: any = {}
  210. if (!networkMappings?.length) {
  211. return payload
  212. }
  213. const hasSecurityGroups = Boolean(networkMappings.find(nm => nm.targetNetwork!.security_groups))
  214. networkMappings.forEach(mapping => {
  215. let target
  216. if (hasSecurityGroups) {
  217. target = {
  218. id: mapping.targetNetwork!.id,
  219. security_groups: mapping.targetSecurityGroups ? mapping.targetSecurityGroups.map(s => (typeof s === 'string' ? s : s.id)) : [],
  220. }
  221. } else if (mapping.targetPortKey != null) {
  222. target = `${mapping.targetNetwork!.id}:${mapping.targetPortKey}`
  223. } else {
  224. target = mapping.targetNetwork!.id
  225. }
  226. payload[mapping.sourceNic.network_name] = target
  227. })
  228. return payload
  229. }
  230. static getStorageMap(
  231. defaultStorage: { value: string | null, busType?: string | null } | undefined,
  232. storageMap?: StorageMap[] | null,
  233. configDefault?: string | null,
  234. ) {
  235. if (!defaultStorage?.value && !storageMap) {
  236. return null
  237. }
  238. const payload: any = {}
  239. if (defaultStorage?.value) {
  240. payload.default = defaultStorage.value
  241. if (defaultStorage.busType) {
  242. payload.default += `:${defaultStorage.busType}`
  243. }
  244. }
  245. if (!storageMap) {
  246. return payload
  247. }
  248. storageMap.forEach(mapping => {
  249. if (mapping.target.id === null && !configDefault) {
  250. return
  251. }
  252. const getDestination = () => {
  253. let destination = mapping.target.id === null ? configDefault : mapping.target.name
  254. if (mapping.targetBusType) {
  255. destination += `:${mapping.targetBusType}`
  256. }
  257. return destination
  258. }
  259. if (mapping.type === 'backend') {
  260. if (!payload.backend_mappings) {
  261. payload.backend_mappings = []
  262. }
  263. payload.backend_mappings.push({
  264. source: mapping.source.storage_backend_identifier,
  265. destination: getDestination(),
  266. })
  267. } else {
  268. if (!payload.disk_mappings) {
  269. payload.disk_mappings = []
  270. }
  271. payload.disk_mappings.push({
  272. disk_id: mapping.source.id.toString(),
  273. destination: getDestination(),
  274. })
  275. }
  276. })
  277. return payload
  278. }
  279. static getUserScripts(
  280. uploadedUserScripts: InstanceScript[],
  281. removedUserScripts: InstanceScript[],
  282. userScriptData: UserScriptData | null | undefined,
  283. ) {
  284. const payload: any = userScriptData || {}
  285. const setPayload = (scripts: InstanceScript[], scriptProp: 'global' | 'instanceId', payloadProp: 'global' | 'instances') => {
  286. if (!scripts.length) {
  287. return
  288. }
  289. payload[payloadProp] = payload[payloadProp] || {}
  290. scripts.forEach(script => {
  291. const scriptValue = script[scriptProp]
  292. if (!scriptValue) {
  293. throw new Error(`The uploaded script structure is missing the '${scriptProp}' property`)
  294. }
  295. payload[payloadProp][scriptValue] = script.scriptContent
  296. })
  297. }
  298. setPayload(removedUserScripts.filter(s => s.global), 'global', 'global')
  299. setPayload(removedUserScripts.filter(s => s.instanceId), 'instanceId', 'instances')
  300. setPayload(uploadedUserScripts.filter(s => s.global), 'global', 'global')
  301. setPayload(uploadedUserScripts.filter(s => s.instanceId), 'instanceId', 'instances')
  302. return payload
  303. }
  304. }