feature: Support quota in magnum service

1.Show quota info when create cluster
2.Support quota setting of magnum cluster when manage project quota

Change-Id: I90d1bc3c2bcb2aa95d728c245dd1b742d3fe6ae1
This commit is contained in:
xusongfu 2022-12-05 15:07:20 +08:00
parent 719e26ebaa
commit 7a4d17ee19
14 changed files with 359 additions and 28 deletions

View File

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

View File

@ -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',
},
],
},
];
}
}

View File

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

View File

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

View File

@ -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最小68IPv6最小1280。",
"The min size is {size} GiB": "最小内存为 {size} GiB",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: <Spin />,
},
];
}
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,
};
}

View File

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