Merge "feat: support instance snapshot create instance"
This commit is contained in:
commit
6c746303c6
@ -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 (
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<span style={style}>{t('Type')}</span>
|
||||
{typeOption.label}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<span style={style}>{t('Size')}</span>
|
||||
{size}
|
||||
<span style={style}>GiB</span>
|
||||
</Col>
|
||||
<Col span={8}>{deleteTypeLabel}</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
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'),
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
@ -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));
|
@ -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,
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user