OptionsSchemaPlugin.ts 12 KB

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