diff --git a/src/locales/en.json b/src/locales/en.json index 58752a22..f5f307b4 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2236,6 +2236,7 @@ "The instance deleted immediately cannot be restored": "The instance deleted immediately cannot be restored", "The instance has been locked. If you want to do more, please unlock it first.": "The instance has been locked. If you want to do more, please unlock it first.", "The instance is not shut down, unable to restore.": "The instance is not shut down, unable to restore.", + "The instance which is boot from volume will create snapshots for each mounted volumes": "The instance which is boot from volume will create snapshots for each mounted volumes", "The instances in the affinity group are allocated to the same physical machine as much as possible, and when there are no more physical machines to allocate, the normal allocation strategy is returned.": "The instances in the affinity group are allocated to the same physical machine as much as possible, and when there are no more physical machines to allocate, the normal allocation strategy is returned.", "The instances in the affinity group are strictly allocated to the same physical machine. When there are no more physical machines to allocate, the allocation fails.": "The instances in the affinity group are strictly allocated to the same physical machine. When there are no more physical machines to allocate, the allocation fails.", "The instances in the anti-affinity group are allocated to different physical machines as much as possible. When there are no more physical machines to allocate, the normal allocation strategy is returned.": "The instances in the anti-affinity group are allocated to different physical machines as much as possible. When there are no more physical machines to allocate, the normal allocation strategy is returned.", diff --git a/src/locales/zh.json b/src/locales/zh.json index b6da5168..d3f15f8a 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -2236,6 +2236,7 @@ "The instance deleted immediately cannot be restored": "立即删除的云主机无法恢复", "The instance has been locked. If you want to do more, please unlock it first.": "该云主机已被锁定。如果要做更多操作,请先解锁。", "The instance is not shut down, unable to restore.": "云主机不处于关机状态,不支持恢复备份操作。", + "The instance which is boot from volume will create snapshots for each mounted volumes.": "从卷启动的云主机将为每个挂载的卷创建快照。", "The instances in the affinity group are allocated to the same physical machine as much as possible, and when there are no more physical machines to allocate, the normal allocation strategy is returned.": "将亲和组内的云主机尽量分配到不同物理机上,当没有更多物理机可分配时,回归普通分配策略。", "The instances in the affinity group are strictly allocated to the same physical machine. When there are no more physical machines to allocate, the allocation fails.": "将亲和组内的云主机严格分配到同一物理机上,当没有更多物理机可分配时,则分配失败。", "The instances in the anti-affinity group are allocated to different physical machines as much as possible. When there are no more physical machines to allocate, the normal allocation strategy is returned.": "将反亲和组内的云主机尽量分配到不同物理机上,当没有更多物理机可分配时,回归普通分配策略。", diff --git a/src/pages/compute/containers/Instance/actions/CreateSnapshot.jsx b/src/pages/compute/containers/Instance/actions/CreateSnapshot.jsx index c72a6eeb..76feb3bb 100644 --- a/src/pages/compute/containers/Instance/actions/CreateSnapshot.jsx +++ b/src/pages/compute/containers/Instance/actions/CreateSnapshot.jsx @@ -12,12 +12,99 @@ // 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 { Table } from 'antd'; import globalServerStore from 'stores/nova/instance'; import { ModalAction } from 'containers/Action'; -import { checkStatus, isIronicInstance } from 'resources/nova/instance'; -import globalInstanceVolumeStore from 'stores/nova/instance-volume'; +import { + checkStatus, + isIronicInstance, + isBootFromVolume, +} from 'resources/nova/instance'; +import { InstanceVolumeStore } from 'stores/nova/instance-volume'; import globalVolumeTypeStore from 'stores/cinder/volume-type'; +import globalProjectStore from 'stores/keystone/project'; + +export const getWishes = () => { + const { volumesForSnapshot = [] } = globalServerStore; + if (!volumesForSnapshot.length) { + return { + total: 0, + types: {}, + }; + } + const types = volumesForSnapshot.reduce((pre, cur) => { + const { volume_type: type } = cur; + if (pre[type]) { + pre[type] += 1; + } else { + pre[type] = 1; + } + return pre; + }, {}); + return { types, total: volumesForSnapshot.length }; +}; + +export const getQuota = (cinderQuota) => { + const { snapshots: snapshotQuota = {} } = cinderQuota || {}; + const { types = {} } = getWishes(); + const typesQuota = Object.keys(types || {}).reduce((pre, cur) => { + pre[cur] = (cinderQuota || {})[`snapshots_${cur}`] || {}; + return pre; + }, {}); + return { + snapshotQuota, + ...typesQuota, + }; +}; + +export const getZero = (cinderQuota) => { + const { types = {} } = getWishes(); + const allQuota = getQuota(cinderQuota) || {}; + const { snapshotQuota = {} } = allQuota; + const zero = [ + { ...snapshotQuota, add: 0, name: 'snapshot', title: t('Snapshot') }, + ]; + Object.keys(types).forEach((type) => { + const typeQuota = allQuota[type] || {}; + zero.push({ + ...typeQuota, + add: 0, + name: type, + title: t('{name} type snapshots', { name: type }), + type: 'line', + }); + }); + return zero; +}; + +export const getAdd = (cinderQuota) => { + const zero = getZero(cinderQuota); + const { types = {}, total = 0 } = getWishes(); + const allQuota = getQuota(cinderQuota) || {}; + const { snapshotQuota } = allQuota; + const { left = 0 } = snapshotQuota || {}; + if (left !== -1 && left < total) { + return zero; + } + const needQuota = JSON.parse(JSON.stringify(zero)); + needQuota[0].add = total; + let typeQuotaIsOK = true; + Object.keys(types).forEach((type, index) => { + if (typeQuotaIsOK) { + const typeQuota = allQuota[type]; + const { left: typeLeft = 0 } = typeQuota || {}; + const typeTotal = types[type]; + if (typeLeft !== -1 && typeLeft < typeTotal) { + typeQuotaIsOK = false; + } else { + needQuota[index + 1].add = typeTotal; + } + } + }); + return typeQuotaIsOK ? needQuota : zero; +}; export class CreateSnapshot extends ModalAction { static id = 'create-snapshot'; @@ -26,8 +113,9 @@ export class CreateSnapshot extends ModalAction { init() { this.store = globalServerStore; - this.volumeStore = globalInstanceVolumeStore; + this.volumeStore = new InstanceVolumeStore(); this.volumeTypeStore = globalVolumeTypeStore; + this.getQuota(); } get name() { @@ -35,15 +123,117 @@ export class CreateSnapshot extends ModalAction { } get tips() { - return t( - 'A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.' + const volumeTip = t( + 'The instance which is boot from volume will create snapshots for each mounted volumes.' ); + return ( + t( + 'A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.' + ) + volumeTip + ); + } + + static get modalSize() { + return 'middle'; + } + + getModalSize() { + return 'middle'; } get instanceName() { return this.values.snapshot; } + get isBootFromVolume() { + return isBootFromVolume(this.item); + } + + get showQuota() { + return this.isBootFromVolume; + } + + get quotaInfo() { + const { quota, quotaLoading } = this.state; + if (quotaLoading) { + return []; + } + return getAdd(quota); + } + + static get disableSubmit() { + const { volumesForSnapshot = [] } = globalServerStore; + if (!volumesForSnapshot.length) { + return false; + } + const { cinderQuota } = globalProjectStore; + const quotaInfo = getAdd(cinderQuota); + if (quotaInfo[0].add === 0) { + return true; + } + return false; + } + + async getQuota() { + this.store.setVolumesForSnapshot([]); + this.setState({ + quota: {}, + quotaLoading: true, + }); + const reqs = [ + globalProjectStore.fetchProjectCinderQuota(), + this.isBootFromVolume + ? this.volumeStore.fetchList({ serverId: this.item.id }) + : null, + ]; + const [quota, volumes] = await Promise.all(reqs); + this.store.setVolumesForSnapshot(volumes || []); + this.setState({ + quota, + quotaLoading: false, + volumes: volumes || [], + }); + } + + getVolumes() { + if (!this.isBootFromVolume) { + return null; + } + const { volumes = [] } = this.state; + const columns = [ + { + dataIndex: 'id', + title: t('ID/Name'), + render: (value, record) => { + const { name } = record; + return ( + <> +