diff --git a/releasenotes/notes/Support-Quota-Of-Magnum-Cluster-67f1fba7a4adba4d.yaml b/releasenotes/notes/Support-Quota-Of-Magnum-Cluster-67f1fba7a4adba4d.yaml new file mode 100644 index 00000000..5b10a94a --- /dev/null +++ b/releasenotes/notes/Support-Quota-Of-Magnum-Cluster-67f1fba7a4adba4d.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Support Quota of Magnum cluster + + 1. Show quota info when create cluster + + 2. Support quota setting of magnum cluster when manage project quota \ No newline at end of file diff --git a/src/client/magnum/index.js b/src/client/magnum/index.js index 9fc9eec0..c3962059 100644 --- a/src/client/magnum/index.js +++ b/src/client/magnum/index.js @@ -37,6 +37,22 @@ export class MagnumClient extends Base { key: 'clustertemplates', responseKey: 'clustertemplate', }, + { + key: 'quotas', + subResources: [ + { + name: 'cluster', + key: 'Cluster', + }, + ], + extendOperations: [ + { + name: 'updateQuota', + key: 'Cluster', + method: 'patch', + }, + ], + }, ]; } } diff --git a/src/layouts/admin-menu.jsx b/src/layouts/admin-menu.jsx index 92f3899a..91b2eef9 100644 --- a/src/layouts/admin-menu.jsx +++ b/src/layouts/admin-menu.jsx @@ -918,14 +918,14 @@ const renderMenu = (t) => { }, { path: '/container-infra/clusters-admin', - name: t('Cluster Instance'), + name: t('Clusters'), key: 'containerInfraClustersAdmin', endpoints: 'magnum', level: 1, children: [ { path: /^\/container-infra\/clusters-admin\/detail\/.[^/]+$/, - name: t('Cluster Instance Detail'), + name: t('Cluster Detail'), key: 'containerInfraClusterDetailAdmin', level: 2, routePath: '/container-infra/clusters-admin/detail/:id', diff --git a/src/locales/en.json b/src/locales/en.json index 8135b614..13cb278e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -285,7 +285,6 @@ "CPU Usages (Core)": "CPU Usages (Core)", "CPU value is { cpu }, NUMA CPU value is { totalCpu }, need to be equal. ": "CPU value is { cpu }, NUMA CPU value is { totalCpu }, need to be equal. ", "CPU(Core)": "CPU(Core)", - "CPUs": "CPUs", "CREATE COMPLETE": "CREATE COMPLETE", "CREATE FAILED": "CREATE FAILED", "CREATE IN PROGRESS": "CREATE IN PROGRESS", @@ -361,8 +360,6 @@ "Cluster Detail": "Cluster Detail", "Cluster Distro": "Cluster Distro", "Cluster Info": "Cluster Info", - "Cluster Instance": "Cluster Instance", - "Cluster Instance Detail": "Cluster Instance Detail", "Cluster Management": "Cluster Management", "Cluster Name": "Cluster Name", "Cluster Network": "Cluster Network", @@ -372,6 +369,7 @@ "Cluster Templates": "Cluster Templates", "Cluster Type": "Cluster Type", "Clusters": "Clusters", + "Clusters Management": "Clusters Management", "Cocos (Keeling) Islands": "Cocos (Keeling) Islands", "Code": "Code", "Cold Migrate": "Cold Migrate", @@ -442,9 +440,11 @@ "Container Stopping": "Container Stopping", "Container Version": "Container Version", "Containers": "Containers", + "Containers CPU": "Containers CPU", "Containers Disk (GiB)": "Containers Disk (GiB)", "Containers Info": "Containers Info", "Containers Management": "Containers Management", + "Containers Memory (MiB)": "Containers Memory (MiB)", "Content": "Content", "Content Type": "Content Type", "Control Location": "Control Location", @@ -635,8 +635,8 @@ "Delete Bandwidth Ingress Rules": "Delete Bandwidth Ingress Rules", "Delete Capsule": "Delete Capsule", "Delete Certificate": "Delete Certificate", - "Delete Clusters": "Delete Clusters", - "Delete Clusters Templates": "Delete Clusters Templates", + "Delete Cluster": "Delete Cluster", + "Delete Cluster Template": "Delete Cluster Template", "Delete Complete": "Delete Complete", "Delete Configuration": "Delete Configuration", "Delete Container": "Delete Container", @@ -686,7 +686,6 @@ "Delete Share Type": "Delete Share Type", "Delete Static Route": "Delete Static Route", "Delete Subnet": "Delete Subnet", - "Delete Template": "Delete Template", "Delete User": "Delete User", "Delete VPN": "Delete VPN", "Delete VPN EndPoint Groups": "Delete VPN EndPoint Groups", @@ -2326,6 +2325,7 @@ "The ip of external members can be any, including the public network ip.": "The ip of external members can be any, including the public network ip.", "The key pair allows you to SSH into your newly created instance. You can select an existing key pair, import a key pair, or generate a new key pair.": "The key pair allows you to SSH into your newly created instance. You can select an existing key pair, import a key pair, or generate a new key pair.", "The kill signal to send": "The kill signal to send", + "The limit of cluster instance greater than or equal to 1.": "The limit of cluster instance greater than or equal to 1.", "The maximum batch size is {size}, that is, the size of the port range cannot exceed {size}.": "The maximum batch size is {size}, that is, the size of the port range cannot exceed {size}.", "The maximum transmission unit (MTU) value to address fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6.": "The maximum transmission unit (MTU) value to address fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6.", "The min size is {size} GiB": "The min size is {size} GiB", diff --git a/src/locales/zh.json b/src/locales/zh.json index 69d90aca..e019b05c 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -285,7 +285,6 @@ "CPU Usages (Core)": "CPU用量 (核)", "CPU value is { cpu }, NUMA CPU value is { totalCpu }, need to be equal. ": "CPU核数是 { cpu },NUMA节点的CPU核数是{ totalCpu },需要一致。", "CPU(Core)": "CPU(核数)", - "CPUs": "CPU", "CREATE COMPLETE": "创建完成", "CREATE FAILED": "创建失败", "CREATE IN PROGRESS": "创建中", @@ -361,8 +360,6 @@ "Cluster Detail": "集群详情", "Cluster Distro": "集群发行版", "Cluster Info": "集群信息", - "Cluster Instance": "集群实例", - "Cluster Instance Detail": "集群实例详情", "Cluster Management": "集群管理", "Cluster Name": "集群名称", "Cluster Network": "集群网络", @@ -372,6 +369,7 @@ "Cluster Templates": "集群模板", "Cluster Type": "集群类型", "Clusters": "集群", + "Clusters Management": "集群管理", "Cocos (Keeling) Islands": "科科斯群岛", "Code": "编码", "Cold Migrate": "冷迁移", @@ -442,9 +440,11 @@ "Container Stopping": "容器关闭中", "Container Version": "容器版本", "Containers": "容器", + "Containers CPU": "容器 CPU", "Containers Disk (GiB)": "容器硬盘 (GiB)", "Containers Info": "容器信息", "Containers Management": "容器管理", + "Containers Memory (MiB)": "容器内存 (MiB)", "Content": "内容", "Content Type": "内容类型", "Control Location": "控制端", @@ -635,8 +635,8 @@ "Delete Bandwidth Ingress Rules": "删除带宽入方向限制", "Delete Capsule": "删除集合", "Delete Certificate": "删除证书", - "Delete Clusters": "删除集群", - "Delete Clusters Templates": "删除集群模板", + "Delete Cluster": "删除集群", + "Delete Cluster Template": "删除集群模板", "Delete Complete": "删除完成", "Delete Configuration": "删除配置", "Delete Container": "删除容器", @@ -686,7 +686,6 @@ "Delete Share Type": "删除共享类型", "Delete Static Route": "删除静态路由", "Delete Subnet": "删除子网", - "Delete Template": "删除模板", "Delete User": "删除用户", "Delete VPN": "删除VPN", "Delete VPN EndPoint Groups": "删除VPN端点组", @@ -2326,6 +2325,7 @@ "The ip of external members can be any, including the public network ip.": "外部成员的IP可以是任何IP,包括公网IP。", "The key pair allows you to SSH into your newly created instance. You can select an existing key pair, import a key pair, or generate a new key pair.": "密钥对允许您SSH到您新创建的实例。 您可以选择一个已存在的密钥对、导入一个密钥对或生成一个新的密钥对。", "The kill signal to send": "要发送的终止信号", + "The limit of cluster instance greater than or equal to 1.": "集群实例的配额必须大于或者等于1。", "The maximum batch size is {size}, that is, the size of the port range cannot exceed {size}.": "批量的上限为{size}个,即端口范围大小不可超过{size}。", "The maximum transmission unit (MTU) value to address fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6.": "地址片段的最大传输单位。IPv4最小68,IPv6最小1280。", "The min size is {size} GiB": "最小内存为 {size} GiB", diff --git a/src/pages/base/containers/Overview/components/QuotaOverview.jsx b/src/pages/base/containers/Overview/components/QuotaOverview.jsx index 285d631f..2df6bfdb 100644 --- a/src/pages/base/containers/Overview/components/QuotaOverview.jsx +++ b/src/pages/base/containers/Overview/components/QuotaOverview.jsx @@ -112,12 +112,23 @@ export const zunQuotaCard = { text: t('Containers'), key: 'zun_containers', }, - { text: t('CPUs'), key: 'zun_cpu' }, - { text: t('Memory (MiB)'), key: 'zun_memory' }, + { text: t('Containers CPU'), key: 'zun_cpu' }, + { text: t('Containers Memory (MiB)'), key: 'zun_memory' }, { text: t('Containers Disk (GiB)'), key: 'zun_disk' }, ], }; +export const magnumQuotaCard = { + text: t('Clusters Management'), + type: 'magnum', + value: [ + { + text: t('Clusters'), + key: 'magnum_cluster', + }, + ], +}; + export const troveQuotaCard = { text: t('Database'), type: 'trove', @@ -210,6 +221,10 @@ export class QuotaOverview extends Component { return globalRootStore.checkEndpoint('zun'); } + get enableMagnum() { + return globalRootStore.checkEndpoint('magnum'); + } + get enableTrove() { return ( globalRootStore.checkEndpoint('trove') && globalRootStore.hasAdminOnlyRole @@ -237,6 +252,9 @@ export class QuotaOverview extends Component { if (this.enableZun) { newList.push(zunQuotaCard); } + if (this.enableMagnum) { + newList.push(magnumQuotaCard); + } if (this.enableTrove) { newList.push(troveQuotaCard); } diff --git a/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx b/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx index c3160b3f..40eede9d 100644 --- a/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx +++ b/src/pages/compute/containers/Instance/actions/StepCreate/index.jsx @@ -71,7 +71,7 @@ export class StepCreate extends StepAction { }); await Promise.all([ this.projectStore.fetchProjectNovaQuota(), - this.projectStore.fetchProjectCinderQuota(), + this.enableCinder ? this.projectStore.fetchProjectCinderQuota() : null, ]); this.setState({ quotaLoading: false, diff --git a/src/pages/container-infra/containers/ClusterTemplates/actions/Delete.jsx b/src/pages/container-infra/containers/ClusterTemplates/actions/Delete.jsx index 08948ce3..1c0b3e60 100644 --- a/src/pages/container-infra/containers/ClusterTemplates/actions/Delete.jsx +++ b/src/pages/container-infra/containers/ClusterTemplates/actions/Delete.jsx @@ -19,11 +19,11 @@ export default class Delete extends ConfirmAction { } get title() { - return t('Delete Template'); + return t('Delete Cluster Template'); } get actionName() { - return t('Delete Clusters Templates'); + return t('Delete Cluster Template'); } get isDanger() { diff --git a/src/pages/container-infra/containers/Clusters/actions/Delete.jsx b/src/pages/container-infra/containers/Clusters/actions/Delete.jsx index c8271a80..125039cd 100644 --- a/src/pages/container-infra/containers/Clusters/actions/Delete.jsx +++ b/src/pages/container-infra/containers/Clusters/actions/Delete.jsx @@ -21,11 +21,11 @@ export default class DeleteClusters extends ConfirmAction { } get title() { - return t('Delete Clusters'); + return t('Delete Cluster'); } get actionName() { - return t('Delete Clusters'); + return t('Delete Cluster'); } get buttonText() { diff --git a/src/pages/container-infra/containers/Clusters/actions/StepCreate/StepNodeSpec/index.jsx b/src/pages/container-infra/containers/Clusters/actions/StepCreate/StepNodeSpec/index.jsx index 4c04a738..40549116 100644 --- a/src/pages/container-infra/containers/Clusters/actions/StepCreate/StepNodeSpec/index.jsx +++ b/src/pages/container-infra/containers/Clusters/actions/StepCreate/StepNodeSpec/index.jsx @@ -113,6 +113,11 @@ export class StepNodeSpec extends Base { type: 'input-int', min: 1, required: true, + onChange: (value) => { + this.updateContext({ + master_count: value, + }); + }, }, { name: 'masterFlavor', @@ -131,6 +136,11 @@ export class StepNodeSpec extends Base { type: 'input-int', min: 1, required: true, + onChange: (value) => { + this.updateContext({ + node_count: value, + }); + }, }, { name: 'flavor', diff --git a/src/pages/container-infra/containers/Clusters/actions/StepCreate/index.jsx b/src/pages/container-infra/containers/Clusters/actions/StepCreate/index.jsx index d5477668..60c31615 100644 --- a/src/pages/container-infra/containers/Clusters/actions/StepCreate/index.jsx +++ b/src/pages/container-infra/containers/Clusters/actions/StepCreate/index.jsx @@ -11,8 +11,12 @@ // limitations under the License. import { inject, observer } from 'mobx-react'; +import { toJS } from 'mobx'; import { StepAction } from 'src/containers/Action'; import globalClustersStore from 'src/stores/magnum/clusters'; +import globalProjectStore from 'stores/keystone/project'; +import { getGiBValue } from 'utils'; +import { message as $message } from 'antd'; import StepInfo from './StepInfo'; import StepNodeSpec from './StepNodeSpec'; import StepNetworks from './StepNetworks'; @@ -22,6 +26,10 @@ import StepLabel from './StepLabel'; export class StepCreate extends StepAction { init() { this.store = globalClustersStore; + this.projectStore = globalProjectStore; + this.state.quotaLoading = true; + this.getQuota(); + this.errorMsg = ''; } static id = 'create-cluster'; @@ -37,7 +45,7 @@ export class StepCreate extends StepAction { } get name() { - return t('Create Instance'); + return t('Create Cluster'); } get listUrl() { @@ -73,6 +81,170 @@ export class StepCreate extends StepAction { ]; } + get enableCinder() { + return this.props.rootStore.checkEndpoint('cinder'); + } + + get showQuota() { + return true; + } + + async getQuota() { + this.setState({ + quotaLoading: true, + }); + await Promise.all([ + this.projectStore.fetchProjectNovaQuota(), + this.projectStore.fetchProjectMagnumQuota(), + this.enableCinder ? this.projectStore.fetchProjectCinderQuota() : null, + ]); + this.setState({ + quotaLoading: false, + }); + } + + get disableNext() { + return !!this.errorMsg; + } + + get disableSubmit() { + return !!this.errorMsg; + } + + get quotaInfo() { + const { quotaLoading } = this.state; + if (quotaLoading) { + return []; + } + const quotaError = this.checkQuotaInput(); + + const { magnum_cluster = {} } = toJS(this.projectStore.magnumQuota) || {}; + const clusterQuotaInfo = { + ...magnum_cluster, + add: quotaError ? 0 : 1, + name: 'cluster', + title: t('Clusters'), + }; + + const { + instances = {}, + cores = {}, + ram = {}, + } = toJS(this.projectStore.novaQuota) || {}; + const instanceQuotaInfo = { + ...instances, + add: quotaError ? 0 : 1, + name: 'instance', + title: t('Instance'), + type: 'line', + }; + + const { newCPU, newRam } = this.getFlavorInput(); + const cpuQuotaInfo = { + ...cores, + add: quotaError ? 0 : newCPU, + name: 'cpu', + title: t('CPU'), + type: 'line', + }; + + const ramQuotaInfo = { + ...ram, + add: quotaError ? 0 : newRam, + name: 'ram', + title: t('Memory (GiB)'), + type: 'line', + }; + + const quotaInfo = [ + clusterQuotaInfo, + instanceQuotaInfo, + cpuQuotaInfo, + ramQuotaInfo, + ]; + + return quotaInfo; + } + + checkInstanceQuota() { + const { quotaLoading } = this.state; + if (quotaLoading) { + return ''; + } + const { instances = {} } = this.projectStore.novaQuota || {}; + const { left = 0 } = instances; + if (left === 0) { + return this.getQuotaMessage(1, instances, t('Instance')); + } + return ''; + } + + getFlavorInput() { + const { data = {} } = this.state; + const { + flavor: { selectedRows = [] } = {}, + node_count = 1, + masterFlavor: { selectedRows: selectedRowsMaster = [] } = {}, + master_count = 1, + } = data; + const { vcpus = 0, ram = 0 } = selectedRows[0] || {}; + const ramGiB = getGiBValue(ram); + const { vcpus: vcpusMaster = 0, ram: ramMaster = 0 } = + selectedRowsMaster[0] || {}; + const ramGiBMaster = getGiBValue(ramMaster); + const newCPU = vcpus * node_count + vcpusMaster * master_count; + const newRam = ramGiB * node_count + ramGiBMaster * master_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 ''; + } + + checkQuotaInput() { + const instanceMsg = this.checkInstanceQuota(); + const flavorMsg = this.checkFlavorQuota(); + const error = instanceMsg || flavorMsg; + if (!error) { + this.status = 'success'; + this.errorMsg = ''; + return ''; + } + this.status = 'error'; + if (this.errorMsg !== error) { + $message.error(error); + } + this.errorMsg = error; + return error; + } + + 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 ''; + } + onSubmit = (values) => { const { additionalLabels, diff --git a/src/pages/container-service/containers/Containers/actions/StepCreate/index.jsx b/src/pages/container-service/containers/Containers/actions/StepCreate/index.jsx index 4a3a8f06..df2c51c0 100644 --- a/src/pages/container-service/containers/Containers/actions/StepCreate/index.jsx +++ b/src/pages/container-service/containers/Containers/actions/StepCreate/index.jsx @@ -123,7 +123,7 @@ export class StepCreate extends StepAction { ...cpu, add: canAdd ? cpuCount : 0, name: 'cpu', - title: t('CPU'), + title: t('Containers CPU'), type: 'line', }; @@ -131,7 +131,7 @@ export class StepCreate extends StepAction { ...memory, add: canAdd ? memoryCount : 0, name: 'memory', - title: t('Memory (MiB)'), + title: t('Containers Memory (MiB)'), type: 'line', }; @@ -139,7 +139,7 @@ export class StepCreate extends StepAction { ...disk, add: canAdd ? diskCount : 0, name: 'disk', - title: t('Disk (GiB)'), + title: t('Containers Disk (GiB)'), type: 'line', }; diff --git a/src/pages/identity/containers/Project/actions/ManageQuota.jsx b/src/pages/identity/containers/Project/actions/ManageQuota.jsx index 02029085..5d98f25b 100644 --- a/src/pages/identity/containers/Project/actions/ManageQuota.jsx +++ b/src/pages/identity/containers/Project/actions/ManageQuota.jsx @@ -15,6 +15,7 @@ import { inject, observer } from 'mobx-react'; import globalProjectStore, { ProjectStore } from 'stores/keystone/project'; import React from 'react'; +import { Spin } from 'antd'; import { ModalAction } from 'containers/Action'; import { VolumeTypeStore } from 'stores/cinder/volume-type'; import { @@ -23,6 +24,7 @@ import { shareQuotaCard, zunQuotaCard, troveQuotaCard, + magnumQuotaCard, } from 'pages/base/containers/Overview/components/QuotaOverview'; export class ManageQuota extends ModalAction { @@ -53,6 +55,10 @@ export class ManageQuota extends ModalAction { return this.props.rootStore.checkEndpoint('zun'); } + get enableMagnum() { + return this.props.rootStore.checkEndpoint('magnum'); + } + get enableTrove() { return ( this.props.rootStore.checkEndpoint('trove') && @@ -150,6 +156,9 @@ export class ManageQuota extends ModalAction { if (this.enableZun) { newQuotaCardList.push(zunQuotaCard); } + if (this.enableMagnum) { + newQuotaCardList.push(magnumQuotaCard); + } if (this.enableTrove) { newQuotaCardList.push(troveQuotaCard); } @@ -197,7 +206,27 @@ export class ManageQuota extends ModalAction { return [labelItem, ...items]; } + getMagnumFormItems() { + const formItems = this.getFormItemsByCards('magnum'); + return formItems.map((it) => { + if (it.name === 'magnum_cluster') { + it.min = 1; + it.tip = t('The limit of cluster instance greater than or equal to 1.'); + } + return it; + }); + } + get formItems() { + if (this.projectStore.quotaLoading) { + return [ + { + name: 'loading', + label: '', + component: , + }, + ]; + } const computeFormItems = this.getComputeFormItems(); const networkFormItems = this.getFormItemsByCards('networks'); const form = [...computeFormItems, ...networkFormItems]; @@ -207,6 +236,9 @@ export class ManageQuota extends ModalAction { if (this.enableZun) { form.push(...this.getFormItemsByCards('zun')); } + if (this.enableMagnum) { + form.push(...this.getMagnumFormItems()); + } if (this.enableTrove) { form.push(...this.getFormItemsByCards('trove')); } @@ -236,11 +268,13 @@ export class ManageQuota extends ModalAction { volumeTypes, share, zun, + magnum, ...others } = values; return { project_id, data: others, + current_quota: this.projectStore.quota, }; } diff --git a/src/stores/keystone/project.js b/src/stores/keystone/project.js index 1b5c1ead..3807bc87 100644 --- a/src/stores/keystone/project.js +++ b/src/stores/keystone/project.js @@ -39,12 +39,18 @@ export class ProjectStore extends Base { @observable zunQuota = {}; + @observable + magnumQuota = {}; + @observable troveQuota = {}; @observable groupRoleList = []; + @observable + quotaLoading = false; + get client() { return client.keystone.projects; } @@ -85,6 +91,10 @@ export class ProjectStore extends Base { return client.zun.quotas; } + get magnumQuotaClient() { + return client.magnum.quotas.cluster; + } + get troveQuotaClient() { return client.trove.quotas; } @@ -220,6 +230,10 @@ export class ProjectStore extends Base { return globalRootStore.checkEndpoint('zun'); } + get enableMagnum() { + return globalRootStore.checkEndpoint('magnum'); + } + get enableTrove() { return ( globalRootStore.checkEndpoint('trove') && globalRootStore.hasAdminOnlyRole @@ -260,6 +274,7 @@ export class ProjectStore extends Base { @action async fetchProjectQuota({ project_id, withKeyPair = false }) { + this.quotaLoading = true; const promiseArr = [ this.novaQuotaClient.detail(project_id), this.neutronQuotaClient.details(project_id), @@ -279,6 +294,10 @@ export class ProjectStore extends Base { }) : null ); + promiseArr.push( + this.enableMagnum ? this.magnumQuotaClient.list(project_id) : null, + this.enableMagnum ? client.magnum.clusters.list() : null + ); promiseArr.push( this.enableTrove ? this.troveQuotaClient.show(project_id) : null ); @@ -289,6 +308,8 @@ export class ProjectStore extends Base { cinderResult, shareResult, zunResult, + magnumResult, + magnumInstanceResult, troveResult, keyPairResult, ] = await Promise.all(promiseArr); @@ -298,6 +319,8 @@ export class ProjectStore extends Base { const { quota: neutronQuota } = neutronResult; const { quota_set: shareQuota = {} } = shareResult || {}; const zunQuota = zunResult || {}; + const { hard_limit, id: clusterQuotaId } = magnumResult || {}; + const { clusters = [] } = magnumInstanceResult || {}; const { quotas: troveQuota = [] } = troveResult || {}; this.updateNovaQuota(novaQuota); const renameShareQuota = Object.keys(shareQuota).reduce((pre, cur) => { @@ -310,6 +333,13 @@ export class ProjectStore extends Base { pre[key] = zunQuota[cur]; return pre; }, {}); + const magnumQuota = { + magnum_cluster: { + limit: hard_limit, + in_use: clusters.length, + }, + magnum_cluster_id: clusterQuotaId, + }; const renameTroveQuota = troveQuota.reduce((pre, cur) => { const key = `trove_${cur.resource}`; pre[key] = cur; @@ -321,6 +351,7 @@ export class ProjectStore extends Base { ...neutronQuota, ...renameShareQuota, ...renameZunQuota, + ...magnumQuota, ...renameTroveQuota, }; if (withKeyPair) { @@ -330,6 +361,7 @@ export class ProjectStore extends Base { } const newQuota = this.updateQuotaData(quota); this.quota = newQuota; + this.quotaLoading = false; return newQuota; } @@ -446,6 +478,19 @@ export class ProjectStore extends Base { return zunReqBody; } + getMagnumQuotaBody(data, project_id) { + if (!this.enableMagnum) { + return {}; + } + const { magnum_cluster } = data; + const magnumReqBody = this.omitNil({ + project_id, + resource: 'Cluster', + hard_limit: magnum_cluster, + }); + return magnumReqBody; + } + getTroveQuotaBody(data) { if (!this.enableTrove) { return {}; @@ -460,12 +505,13 @@ export class ProjectStore extends Base { return troveReqBody; } - async updateQuota(project_id, data) { + async updateQuota(project_id, data, current_quota) { const novaReqBody = this.getNovaQuotaBody(data); const cinderReqBody = this.getCinderQuotaBody(data); const neutronReqBody = this.getNeutronQuotaBody(data); const shareReqBody = this.getShareQuotaBody(data); const zunReqBody = this.getZunQuotaBody(data); + const magnumReqBody = this.getMagnumQuotaBody(data, project_id); const troveReqBody = this.getTroveQuotaBody(data); const reqs = []; if (!isEmpty(novaReqBody.quota_set)) { @@ -483,6 +529,15 @@ export class ProjectStore extends Base { if (!isEmpty(zunReqBody)) { reqs.push(client.zun.quotas.update(project_id, zunReqBody)); } + if (!isEmpty(magnumReqBody)) { + // if magnum_cluster_id is existed, it means the quota has been initialized + const { magnum_cluster_id } = current_quota || {}; + if (magnum_cluster_id) { + reqs.push(client.magnum.quotas.updateQuota(project_id, magnumReqBody)); + } else { + reqs.push(client.magnum.quotas.create(magnumReqBody)); + } + } if (!isEmpty(troveReqBody)) { reqs.push(client.trove.quotas.update(project_id, troveReqBody)); } @@ -491,9 +546,9 @@ export class ProjectStore extends Base { } @action - async updateProjectQuota({ project_id, data }) { + async updateProjectQuota({ project_id, data, current_quota }) { this.isSubmitting = true; - const result = await this.updateQuota(project_id, data); + const result = await this.updateQuota(project_id, data, current_quota); this.isSubmitting = false; return result; } @@ -625,6 +680,24 @@ export class ProjectStore extends Base { return zunQuota; } + @action + async fetchProjectMagnumQuota(projectId) { + const [quotas, clustersRes] = await Promise.all([ + this.magnumQuotaClient.list(projectId || this.currentProjectId), + client.magnum.clusters.list(), + ]); + const { hard_limit } = this.updateQuotaData(quotas); + const { clusters = [] } = clustersRes || {}; + const magnumQuota = this.updateQuotaData({ + magnum_cluster: { + limit: hard_limit, + in_use: clusters.length, + }, + }); + this.magnumQuota = magnumQuota; + return magnumQuota; + } + @action async fetchProjectTroveQuota(projectId) { const { quotas = [] } = await this.troveQuotaClient.show(