// Copyright 2021 99cloud // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import React from 'react'; import { inject, observer } from 'mobx-react'; import { toJS } from 'mobx'; import { InputNumber, Badge, message as $message } from 'antd'; import { StepAction } from 'containers/Action'; import globalServerStore from 'stores/nova/instance'; import globalProjectStore from 'stores/keystone/project'; import classnames from 'classnames'; import { isEmpty, isFinite, isString } from 'lodash'; import { getUserData } from 'resources/nova/instance'; import { getGiBValue } from 'utils/index'; import Notify from 'components/Notify'; import ConfirmStep from './ConfirmStep'; import SystemStep from './SystemStep'; import NetworkStep from './NetworkStep'; import BaseStep from './BaseStep'; import styles from './index.less'; export class StepCreate extends StepAction { static id = 'instance-create'; static title = t('Create Instance'); static path = (_, containerProps) => { const { detail, match } = containerProps || {}; if (!detail || isEmpty(detail)) { return '/compute/instance/create'; } if (match.path.indexOf('/compute/server') >= 0) { return `/compute/instance/create?servergroup=${detail.id}`; } }; init() { this.store = globalServerStore; this.projectStore = globalProjectStore; this.getQuota(); this.status = 'success'; this.errorMsg = ''; } static policy = [ 'os_compute_api:servers:create', 'os_compute_api:os-availability-zone:list', ]; static allowed(_, containerProps) { const { isAdminPage = false } = containerProps; return Promise.resolve(!isAdminPage); } async getQuota() { await Promise.all([ this.projectStore.fetchProjectNovaQuota(), this.projectStore.fetchProjectCinderQuota(), ]); this.onCountChange(1); } get disableNext() { return !!this.errorMsg; } get disableSubmit() { return !!this.errorMsg; } get instanceQuota() { const { instances: { left = 0 } = {} } = toJS(this.projectStore.novaQuota) || {}; if (left === -1) { return Infinity; } return left; } get name() { return t('Create instance'); } get enableCinder() { return this.props.rootStore.checkEndpoint('cinder'); } get listUrl() { const { image, volume, servergroup } = this.locationParams; if (image) { return this.getRoutePath('image'); } if (volume) { return this.getRoutePath('volume'); } if (servergroup) { return this.getRoutePath('serverGroupDetail', { id: servergroup }); } return this.getRoutePath('instance'); } get hasConfirmStep() { return false; } get steps() { return [ { title: t('Base Config'), component: BaseStep, }, { title: t('Network Config'), component: NetworkStep, }, { title: t('System Config'), component: SystemStep, }, { title: t('Confirm Config'), component: ConfirmStep, }, ]; } get instanceName() { const { name, count = 1 } = this.values || {}; if (count === 1) { return this.unescape(name); } return this.unescape( new Array(count) .fill(count) .map((_, index) => `${name}-${index + 1}`) .join(', ') ); } get successText() { return t( 'The creation instruction was issued successfully, instance: {name}. \n You can wait for a few seconds to follow the changes of the list data or manually refresh the data to get the final display result.', { action: this.name.toLowerCase(), name: this.instanceName } ); } get showQuota() { return true; } get quotaInfo() { const { instances = {}, cores = {}, ram = {}, } = toJS(this.projectStore.novaQuota) || {}; const { limit } = instances || {}; if (!limit) { return []; } const { data = {} } = this.state; const { count = 1 } = data; const instanceQuotaInfo = { ...instances, add: count, name: 'instance', title: t('Instance'), // type: 'line', }; const { newCPU, newRam } = this.getFlavorInput(); const cpuQuotaInfo = { ...cores, add: newCPU, name: 'cpu', title: t('CPU'), type: 'line', }; const ramQuotaInfo = { ...ram, add: newRam, name: 'ram', title: t('Memory (GiB)'), type: 'line', }; const volumeQuota = this.getVolumeQuota(); const { totalNewCount, totalNewSize } = this.getVolumeInputMap(); const volumeQuotaInfo = { ...volumeQuota.volumes, add: totalNewCount, name: 'volume', title: t('Volume'), type: 'line', }; const volumeSizeQuotaInfo = { ...volumeQuota.gigabytes, add: totalNewSize, name: 'volumeSize', title: t('Volume Size'), type: 'line', }; return [ instanceQuotaInfo, cpuQuotaInfo, ramQuotaInfo, volumeQuotaInfo, volumeSizeQuotaInfo, ]; } get errorText() { const { status } = this.state; if (status === 'error') { return t( 'Unable to create instance: insufficient quota to create resources.' ); } if (this.ipBatchError) { return t( 'Unable to create instance: batch creation is not supported when specifying IP.' ); } return t( 'The creation instruction has been issued, please refresh to see the actual situation in the list.' ); } onCountChange = (value) => { const { data } = this.state; this.setState({ data: { ...data, count: value, }, }); }; getVolumeQuota() { const quotaAll = toJS(this.projectStore.cinderQuota) || {}; const result = {}; Object.keys(quotaAll).forEach((key) => { if (key.includes('volumes') || key.includes('gigabytes')) { result[key] = quotaAll[key]; } }); return result; } getQuotaMessage(value, quota, name) { const { left = 0 } = quota || {}; if (left === -1) { return ''; } if (value > left) { return t( 'Insufficient {name} quota to create resources(left { quota }, input { input }).', { name, quota: left, input: value } ); } return ''; } getVolumeInputMap() { const { data } = this.state; const { systemDisk = {}, dataDisk = [], count = 1, source: { value: sourceValue } = {}, instanceSnapshotDisk = {}, } = data; const newCountMap = {}; const newSizeMap = {}; let totalNewCount = 0; let totalNewSize = 0; if (sourceValue === 'instanceSnapshot' && instanceSnapshotDisk) { const { size, typeOption: { label } = {} } = instanceSnapshotDisk; if (label) { newCountMap[label] = !newCountMap[label] ? 1 : newCountMap[label] + 1; newSizeMap[label] = !newSizeMap[label] ? size : newSizeMap[label] + size; totalNewCount += 1 * count; totalNewSize += size * count; } } else if (systemDisk.type) { const { size } = systemDisk; const { label } = systemDisk.typeOption || {}; newCountMap[label] = !newCountMap[label] ? 1 : newCountMap[label] + 1; newSizeMap[label] = !newSizeMap[label] ? size : newSizeMap[label] + size; totalNewCount += 1 * count; totalNewSize += size * count; } if (dataDisk) { dataDisk.forEach((item) => { if (item.value && item.value.type) { const { size } = item.value; const { label } = item.value.typeOption || {}; newCountMap[label] = !newCountMap[label] ? 1 * count : newCountMap[label] + 1 * count; newSizeMap[label] = !newSizeMap[label] ? size * count : newSizeMap[label] + size * count; totalNewCount += 1 * count; totalNewSize += size * count; } }); } return { totalNewCount, totalNewSize, newCountMap, newSizeMap, }; } checkVolumeQuota() { if (!this.enableCinder) return ''; let msg = ''; const { totalNewCount, totalNewSize, newCountMap, newSizeMap } = this.getVolumeInputMap(); const quotaAll = this.getVolumeQuota(); const totalCountMsg = this.getQuotaMessage( totalNewCount, quotaAll.volumes, t('volume') ); if (totalCountMsg) { return totalCountMsg; } const totalSizeMsg = this.getQuotaMessage( totalNewSize, quotaAll.gigabytes, t('volume gigabytes') ); if (totalSizeMsg) { return totalSizeMsg; } Object.keys(newCountMap).forEach((key) => { const countMsg = this.getQuotaMessage( newCountMap[key], quotaAll[`volumes_${key}`], t('volume type {type}', { type: key }) ); if (countMsg) { msg = countMsg; } }); if (msg) { return msg; } Object.keys(newSizeMap).forEach((key) => { const sizeMsg = this.getQuotaMessage( newSizeMap[key], quotaAll[`gigabytes_${key}`], t('volume type {type} gigabytes', { type: key }) ); if (sizeMsg) { msg = sizeMsg; } }); return msg; } getFlavorInput() { const { data } = this.state; const { flavor = {}, count = 1 } = data; const { selectedRows = [] } = flavor; const { vcpus = 0, ram = 0 } = selectedRows[0] || {}; const ramGiB = getGiBValue(ram); const newCPU = vcpus * count; const newRam = ramGiB * count; return { newCPU, newRam, }; } checkFlavorQuota() { const { newCPU, newRam } = this.getFlavorInput(); const { cores = {}, ram = {} } = this.projectStore.novaQuota; const { left = 0 } = cores || {}; const { left: leftRam = 0 } = ram || {}; if (left !== -1 && left < newCPU) { return this.getQuotaMessage(newCPU, cores, t('CPU')); } if (leftRam !== -1 && leftRam < newRam) { return this.getQuotaMessage(newRam, ram, t('Memory')); } return ''; } get badgeStyle() { return { marginTop: 8, marginBottom: 8, marginLeft: 10, maxWidth: 600 }; } renderBadge() { const flavorMsg = this.checkFlavorQuota(); const volumeMsg = this.checkVolumeQuota(); if (!flavorMsg && !volumeMsg) { this.status = 'success'; this.errorMsg = ''; return null; } this.status = 'error'; const msg = flavorMsg || volumeMsg; if (this.errorMsg !== msg) { $message.error(msg); } this.errorMsg = msg; return (