Merge "feat: support instance snapshot create instance"

This commit is contained in:
Zuul 2022-07-19 15:00:41 +00:00 committed by Gerrit Code Review
commit 6c746303c6
5 changed files with 318 additions and 18 deletions

View File

@ -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'),

View File

@ -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() {

View File

@ -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,

View File

@ -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));

View File

@ -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,
},