diff --git a/src/pages/compute/containers/Instance/actions/StepCreate/BaseStep/index.jsx b/src/pages/compute/containers/Instance/actions/StepCreate/BaseStep/index.jsx index c24f4aed..a8fd59cb 100644 --- a/src/pages/compute/containers/Instance/actions/StepCreate/BaseStep/index.jsx +++ b/src/pages/compute/containers/Instance/actions/StepCreate/BaseStep/index.jsx @@ -15,9 +15,11 @@ import React from 'react'; import { inject, observer } from 'mobx-react'; import { toJS } from 'mobx'; +import { Row, Col } from 'antd'; import { volumeStatus, canCreateInstance } from 'resources/cinder/volume'; import globalServerStore from 'stores/nova/instance'; import globalImageStore from 'stores/glance/image'; +import globalInstanceSnapshotStore from 'stores/glance/instance-snapshot'; import globalVolumeTypeStore from 'stores/cinder/volume-type'; import globalAvailabilityZoneStore from 'stores/nova/zone'; import { VolumeStore } from 'stores/cinder/volume'; @@ -26,6 +28,8 @@ import { getImageSystemTabs, getImageOS, getImageColumns, + imageFormats, + imageStatus, } from 'resources/glance/image'; import Base from 'components/Form'; import InstanceVolume from 'components/FormItem/InstanceVolume'; @@ -38,10 +42,13 @@ export class BaseStep extends Base { this.imageStore = globalImageStore; this.volumeStore = new VolumeStore(); this.volumeTypeStore = globalVolumeTypeStore; + this.instanceSnapshotStore = globalInstanceSnapshotStore; this.getAvailZones(); this.getImages(); this.getVolumeTypes(); this.getVolumes(); + this.getInstanceSnapshots(); + this.initSourceChange(); } get title() { @@ -57,10 +64,12 @@ export class BaseStep extends Base { } get defaultValue() { - const { volume } = this.locationParams; + const { volume, snapshot } = this.locationParams; let source = this.imageSourceType; if (volume) { source = this.volumeSourceType; + } else if (snapshot) { + source = this.snapshotSourceType; } const values = { systemDisk: this.defaultVolumeType, @@ -101,6 +110,17 @@ export class BaseStep extends Base { })); } + get snapshots() { + const { snapshot } = this.locationParams; + if (!snapshot) { + const { + list: { data }, + } = this.instanceSnapshotStore; + return data || []; + } + return [toJS(this.instanceSnapshotStore.detail)]; + } + get enableCinder() { return this.props.rootStore.checkEndpoint('cinder'); } @@ -137,13 +157,20 @@ export class BaseStep extends Base { } get sourceTypes() { - const { image, volume } = this.locationParams; - const types = [{ label: t('Image'), value: 'image', disabled: volume }]; + const { image, snapshot, volume } = this.locationParams; + const types = [ + { label: t('Image'), value: 'image', disabled: volume || snapshot }, + { + label: t('Instance Snapshot'), + value: 'instanceSnapshot', + disabled: image || volume, + }, + ]; if (this.enableCinder) { types.push({ label: t('Bootable Volume'), value: 'bootableVolume', - disabled: image, + disabled: image || snapshot, }); } return types; @@ -153,6 +180,10 @@ export class BaseStep extends Base { return this.sourceTypes.find((it) => it.value === 'image'); } + get snapshotSourceType() { + return this.sourceTypes.find((it) => it.value === 'instanceSnapshot'); + } + get volumeSourceType() { return this.enableCinder ? this.sourceTypes.find((it) => it.value === 'bootableVolume') @@ -169,8 +200,8 @@ export class BaseStep extends Base { } async getImages() { - const { volume, image } = this.locationParams; - if (volume) { + const { volume, image, snapshot } = this.locationParams; + if (volume || snapshot) { return; } if (image) { @@ -193,8 +224,8 @@ export class BaseStep extends Base { } async getVolumes() { - const { image, volume } = this.locationParams; - if (image) { + const { image, snapshot, volume } = this.locationParams; + if (image || snapshot) { return; } if (!this.enableCinder) { @@ -221,6 +252,21 @@ export class BaseStep extends Base { } } + async getInstanceSnapshots() { + const { snapshot } = this.locationParams; + if (!snapshot) { + this.instanceSnapshotStore.fetchList(); + return; + } + await this.instanceSnapshotStore.fetchDetail({ id: snapshot }); + if (snapshot) { + this.updateFormValue('instanceSnapshot', { + selectedRowKeys: [snapshot], + selectedRows: this.snapshots.filter((it) => it.id === snapshot), + }); + } + } + onImageTabChange = (value) => { this.setState({ imageTab: value, @@ -240,7 +286,7 @@ export class BaseStep extends Base { }; get nameForStateUpdate() { - return ['source', 'image', 'bootableVolume', 'flavor']; + return ['source', 'image', 'instanceSnapshot', 'bootableVolume', 'flavor']; } getSystemDiskMinSize() { @@ -250,8 +296,13 @@ export class BaseStep extends Base { const { min_disk = 0, size = 0 } = this.state.image || {}; const sizeGiB = Math.ceil(size / 1024 / 1024 / 1024); imageSize = Math.max(min_disk, sizeGiB, 1); + return Math.max(flavorSize, imageSize, 1); } - return Math.max(flavorSize, imageSize, 1); + if (this.sourceTypeIsSnapshot) { + const { instanceSnapshotMinSize = 0 } = this.state; + return Math.max(flavorSize, instanceSnapshotMinSize, 1); + } + return Math.max(flavorSize, 1); } get sourceTypeIsImage() { @@ -259,6 +310,11 @@ export class BaseStep extends Base { return source === this.imageSourceType.value; } + get sourceTypeIsSnapshot() { + const { source } = this.state; + return source === this.snapshotSourceType.value; + } + get sourceTypeIsVolume() { const { source } = this.state; return source === this.volumeSourceType.value; @@ -274,12 +330,87 @@ export class BaseStep extends Base { return ''; } + initSourceChange() { + const { snapshot, volume } = this.locationParams; + if (snapshot) { + this.onSourceChange(this.snapshotSourceType); + } else if (volume) { + this.onSourceChange(this.volumeSourceType); + } else { + this.onSourceChange(this.imageSourceType); + } + } + onFlavorChange = (value) => { this.updateContext({ flavor: value, }); }; + onInstanceSnapshotChange = async (value) => { + const { min_disk, size, id } = value.selectedRows[0] || {}; + if (!id) { + this.updateContext({ + instanceSnapshotDisk: null, + }); + this.setState({ + instanceSnapshotDisk: null, + instanceSnapshotMinSize: 0, + }); + return; + } + const detail = await this.instanceSnapshotStore.fetchDetail({ id }); + const { + snapshotDetail: { size: snapshotSize = 0, volume_type_id } = {}, + block_device_mapping = '', + volumeDetail, + } = detail; + if (!volumeDetail) { + this.updateContext({ + instanceSnapshotDisk: null, + }); + this.setState({ + instanceSnapshotDisk: null, + instanceSnapshotMinSize: 0, + }); + } + const minSize = Math.max(min_disk, size, snapshotSize); + let bdm = {}; + try { + bdm = JSON.parse(block_device_mapping); + } catch (e) {} + const { volume_type } = volumeDetail; + const { delete_on_termination } = bdm[0] || {}; + const deleteType = delete_on_termination ? 1 : 0; + const deleteTypeLabel = delete_on_termination + ? t('Deleted with the instance') + : t('Not deleted with the instance'); + const volumeTypeId = + volume_type_id || + (this.volumeTypes.find((it) => it.label === volume_type) || {}).value; + const volumeTypeItem = this.volumeTypes.find( + (it) => it.value === volumeTypeId + ); + const instanceSnapshotDisk = volumeDetail + ? { + type: volumeTypeId, + typeOption: volumeTypeItem, + size: snapshotSize, + deleteType, + deleteTypeLabel, + } + : null; + + this.updateFormValue('instanceSnapshotDisk', instanceSnapshotDisk); + this.updateContext({ + instanceSnapshotDisk, + }); + this.setState({ + instanceSnapshotDisk, + instanceSnapshotMinSize: minSize, + }); + }; + onBootableVolumeChange = (value) => { this.updateContext({ bootableVolume: value, @@ -304,10 +435,80 @@ export class BaseStep extends Base { }); }; + getInstanceSnapshotDisk = () => { + const { instanceSnapshotDisk } = this.state; + const { instanceSnapshotDisk: oldDisk } = this.props.context; + return instanceSnapshotDisk || oldDisk; + }; + + renderSnapshotDisk = () => { + const disk = this.getInstanceSnapshotDisk(); + if (disk === null) { + return null; + } + const { deleteTypeLabel, typeOption = {}, size } = disk || {}; + if (!size) { + return null; + } + const style = { + marginRight: 10, + maxWidth: '20%', + }; + return ( + + + {t('Type')} + {typeOption.label} + + + {t('Size')} + {size} + GiB + + {deleteTypeLabel} + + ); + }; + get imageColumns() { return getImageColumns(this); } + get instanceSnapshotColumns() { + return [ + { + title: t('Name'), + dataIndex: 'name', + }, + { + title: t('Disk Format'), + dataIndex: 'disk_format', + render: (value) => imageFormats[value] || '-', + }, + { + title: t('Min System Disk'), + dataIndex: 'min_disk', + render: (text) => `${text}GiB`, + }, + { + title: t('Min Memory'), + dataIndex: 'min_ram', + render: (text) => `${text / 1024}GiB`, + }, + { + title: t('Status'), + dataIndex: 'status', + render: (value) => imageStatus[value] || '-', + }, + { + title: t('Created At'), + dataIndex: 'created_at', + isHideable: true, + valueRender: 'sinceTime', + }, + ]; + } + get volumeColumns() { return [ { @@ -339,7 +540,12 @@ export class BaseStep extends Base { } get showSystemDisk() { - return this.enableCinder && this.sourceTypeIsImage; + const snapshotDisk = this.getInstanceSnapshotDisk(); + return ( + this.enableCinder && + (this.sourceTypeIsImage || + (this.sourceTypeIsSnapshot && snapshotDisk === null)) + ); } getFlavorComponent() { @@ -424,6 +630,24 @@ export class BaseStep extends Base { selectedLabel: t('Image'), onTabChange: this.onImageTabChange, }, + { + name: 'instanceSnapshot', + label: t('Instance Snapshot'), + type: 'select-table', + data: this.snapshots, + required: this.sourceTypeIsSnapshot, + isMulti: false, + hidden: !this.sourceTypeIsSnapshot, + display: this.sourceTypeIsSnapshot, + onChange: this.onInstanceSnapshotChange, + filterParams: [ + { + label: t('Name'), + name: 'name', + }, + ], + columns: this.instanceSnapshotColumns, + }, { name: 'bootableVolume', label: t('Bootable Volume'), @@ -457,6 +681,12 @@ export class BaseStep extends Base { extra: t('Disk size is limited by the min disk of flavor, image, etc.'), onChange: this.onSystemDiskChange, }, + { + name: 'instanceSnapshotDisk', + label: t('System Disk'), + hidden: this.showSystemDisk, + component: this.renderSnapshotDisk(), + }, { name: 'dataDisk', label: t('Data Disk'), diff --git a/src/pages/compute/containers/Instance/actions/StepCreate/ConfirmStep/index.jsx b/src/pages/compute/containers/Instance/actions/StepCreate/ConfirmStep/index.jsx index 70463ad4..743097fb 100644 --- a/src/pages/compute/containers/Instance/actions/StepCreate/ConfirmStep/index.jsx +++ b/src/pages/compute/containers/Instance/actions/StepCreate/ConfirmStep/index.jsx @@ -49,10 +49,18 @@ export class ConfirmStep extends Base { getSystemDisk() { if (!this.enableCinder) return null; const { context } = this.props; - const { systemDisk, source } = context; - return source.value === 'bootableVolume' - ? this.getBootableVolumeDisk() - : this.getDisk(systemDisk); + const { + systemDisk, + source: { value } = {}, + instanceSnapshotDisk, + } = context; + if (value === 'bootableVolume') { + return this.getBootableVolumeDisk(); + } + if (value === 'instanceSnapshot' && instanceSnapshotDisk !== null) { + return this.getDisk(instanceSnapshotDisk); + } + return this.getDisk(systemDisk); } getDataDisk() { diff --git a/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx b/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx index d6e9bead..c732a695 100644 --- a/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx +++ b/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx @@ -275,12 +275,28 @@ export class StepCreate extends StepAction { getVolumeInputMap() { const { data } = this.state; - const { systemDisk = {}, dataDisk = [], count = 1 } = data; + const { + systemDisk = {}, + dataDisk = [], + count = 1, + source: { value: sourceValue } = {}, + instanceSnapshotDisk = {}, + } = data; const newCountMap = {}; const newSizeMap = {}; let totalNewCount = 0; let totalNewSize = 0; - if (systemDisk.type) { + 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; @@ -460,6 +476,7 @@ export class StepCreate extends StepAction { dataDisk, image, instanceSnapshot, + instanceSnapshotDisk, source, systemDisk, } = values; @@ -477,7 +494,7 @@ export class StepCreate extends StepAction { } let rootVolume = {}; if (sourceValue !== 'bootableVolume') { - const { deleteType, type, size } = systemDisk; + const { deleteType, type, size } = systemDisk || {}; rootVolume = { boot_index: 0, uuid: imageRef, @@ -487,6 +504,13 @@ export class StepCreate extends StepAction { volume_type: type, delete_on_termination: deleteType === 1, }; + if (sourceValue === 'instanceSnapshot') { + if (instanceSnapshotDisk) { + delete rootVolume.volume_size; + delete rootVolume.volume_type; + delete rootVolume.delete_on_termination; + } + } } else { rootVolume = { boot_index: 0, diff --git a/src/pages/compute/containers/InstanceSnapshot/actions/CreateInstance.jsx b/src/pages/compute/containers/InstanceSnapshot/actions/CreateInstance.jsx new file mode 100644 index 00000000..62266b50 --- /dev/null +++ b/src/pages/compute/containers/InstanceSnapshot/actions/CreateInstance.jsx @@ -0,0 +1,34 @@ +// Copyright 2022 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 { inject, observer } from 'mobx-react'; +import CreateInstance from 'pages/compute/containers/Instance/actions/StepCreate'; + +export class StepCreate extends CreateInstance { + static id = 'instance-create'; + + static title = t('Create Instance'); + + static path(item) { + return `/compute/instance/create?snapshot=${item.id}`; + } + + static policy = 'os_compute_api:servers:create'; + + static allowed(item) { + return Promise.resolve(item.status === 'active'); + } +} + +export default inject('rootStore')(observer(StepCreate)); diff --git a/src/pages/compute/containers/InstanceSnapshot/actions/index.jsx b/src/pages/compute/containers/InstanceSnapshot/actions/index.jsx index 43b39b10..dcc8fa67 100644 --- a/src/pages/compute/containers/InstanceSnapshot/actions/index.jsx +++ b/src/pages/compute/containers/InstanceSnapshot/actions/index.jsx @@ -13,6 +13,7 @@ // limitations under the License. import CreateVolume from './CreateVolume'; +import CreateInstance from './CreateInstance'; import Edit from './Edit'; import Delete from './Delete'; @@ -20,6 +21,9 @@ const actionConfigs = { rowActions: { firstAction: Edit, moreActions: [ + { + action: CreateInstance, + }, { action: CreateVolume, },