From 239b3b9cf5b30e8dbfa70b482c06cd3e2692a145 Mon Sep 17 00:00:00 2001 From: Kien Nguyen Date: Thu, 27 Sep 2018 17:11:16 +0700 Subject: [PATCH] Add quotas and quota classes Change-Id: I6ecdb63e633f145614b1a0d57712352dace7acf2 Implements: blueprint quota-support --- setup.cfg | 6 ++ zunclient/api_versions.py | 2 +- zunclient/common/cliutils.py | 10 ++- zunclient/osc/v1/quota_classes.py | 91 ++++++++++++++++++++ zunclient/osc/v1/quotas.py | 113 +++++++++++++++++++++++++ zunclient/tests/unit/test_shell.py | 20 ++--- zunclient/tests/unit/v1/test_quotas.py | 91 ++++++++++++++++++++ zunclient/v1/client.py | 4 + zunclient/v1/quota_classes.py | 43 ++++++++++ zunclient/v1/quota_classes_shell.py | 56 ++++++++++++ zunclient/v1/quotas.py | 50 +++++++++++ zunclient/v1/quotas_shell.py | 65 ++++++++++++++ zunclient/v1/shell.py | 4 + 13 files changed, 542 insertions(+), 13 deletions(-) create mode 100644 zunclient/osc/v1/quota_classes.py create mode 100644 zunclient/osc/v1/quotas.py create mode 100644 zunclient/tests/unit/v1/test_quotas.py create mode 100644 zunclient/v1/quota_classes.py create mode 100644 zunclient/v1/quota_classes_shell.py create mode 100644 zunclient/v1/quotas.py create mode 100644 zunclient/v1/quotas_shell.py diff --git a/setup.cfg b/setup.cfg index 9d6a69ce..f0e42ecc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -70,6 +70,12 @@ openstack.container.v1 = appcontainer_rebuild = zunclient.osc.v1.containers:RebuildContainer appcontainer_action_list = zunclient.osc.v1.containers:ActionList appcontainer_action_show = zunclient.osc.v1.containers:ActionShow + appcontainer_quota_get = zunclient.osc.v1.quotas:GetQuota + appcontainer_quota_default = zunclient.osc.v1.quotas:GetDefaultQuota + appcontainer_quota_delete = zunclient.osc.v1.quotas:DeleteQuota + appcontainer_quota_update = zunclient.osc.v1.quotas:UpdateQuota + appcontainer_quota_class_update = zunclient.osc.v1.quotas:UpdateQuotaClass + appcontainer_quota_class_get = zunclient.osc.v1.quotas:GetQuotaClass [build_sphinx] source-dir = doc/source diff --git a/zunclient/api_versions.py b/zunclient/api_versions.py index 01550c21..4cb16167 100644 --- a/zunclient/api_versions.py +++ b/zunclient/api_versions.py @@ -31,7 +31,7 @@ if not LOG.handlers: HEADER_NAME = "OpenStack-API-Version" SERVICE_TYPE = "container" MIN_API_VERSION = '1.1' -MAX_API_VERSION = '1.25' +MAX_API_VERSION = '1.26' DEFAULT_API_VERSION = '1.latest' _SUBSTITUTIONS = {} diff --git a/zunclient/common/cliutils.py b/zunclient/common/cliutils.py index f188cf4e..6430dac8 100644 --- a/zunclient/common/cliutils.py +++ b/zunclient/common/cliutils.py @@ -230,18 +230,21 @@ def keys_and_vals_to_strs(dictionary): return dict((to_str(k), to_str(v)) for k, v in dictionary.items()) -def print_dict(dct, dict_property="Property", wrap=0): +def print_dict(dct, dict_property="Property", wrap=0, value_fields=None): """Print a `dict` as a table of two columns. :param dct: `dict` to print :param dict_property: name of the first column :param wrap: wrapping for the second column + :param value_fields: attributes that correspond to columns, in order """ pt = prettytable.PrettyTable([dict_property, 'Value']) + if value_fields: + pt = prettytable.PrettyTable([dict_property] + list(value_fields)) pt.align = 'l' for k, v in dct.items(): # convert dict to str to check length - if isinstance(v, dict): + if isinstance(v, dict) and not value_fields: v = six.text_type(keys_and_vals_to_strs(v)) if wrap > 0: v = textwrap.fill(six.text_type(v), wrap) @@ -255,6 +258,9 @@ def print_dict(dct, dict_property="Property", wrap=0): for line in lines: pt.add_row([col1, line]) col1 = '' + elif isinstance(v, dict): + vals = [v[field] for field in v if field in value_fields] + pt.add_row([k] + vals) elif isinstance(v, list): val = str([str(i) for i in v]) pt.add_row([k, val]) diff --git a/zunclient/osc/v1/quota_classes.py b/zunclient/osc/v1/quota_classes.py new file mode 100644 index 00000000..b9a38936 --- /dev/null +++ b/zunclient/osc/v1/quota_classes.py @@ -0,0 +1,91 @@ +# 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. + +from osc_lib.command import command +from osc_lib import utils +from oslo_log import log as logging + + +def _quota_class_columns(quota_class): + return quota_class.__dict__.keys() + + +def _get_client(obj, parsed_args): + obj.log.debug("take_action(%s)" % parsed_args) + return obj.app.client_manager.container + + +class UpdateQuotaClass(command.ShowOne): + """Update the quotas for a quota class""" + + log = logging.getLogger(__name__ + ".UpdateQuotaClass") + + def get_parser(self, prog_name): + parser = super(UpdateQuotaClass, self).get_parser(prog_name) + parser.add_argument( + '--containers', + metavar='', + help='The number of containers allowed per project') + parser.add_argument( + '--memory', + metavar='', + help='The number of megabytes of container RAM ' + 'allowed per project') + parser.add_argument( + '--cpu', + metavar='', + help='The number of container cores or vCPUs ' + 'allowed per project') + parser.add_argument( + '--disk', + metavar='', + help='The number of gigabytes of container Disk ' + 'allowed per project') + parser.add_argument( + 'quota_class_name', + metavar='', + help='The name of quota class') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + opts = {} + opts['containers'] = parsed_args.containers + opts['memory'] = parsed_args.memory + opts['cpu'] = parsed_args.cpu + opts['disk'] = parsed_args.disk + quota_class_name = parsed_args.quota_class_name + quota_class = client.quota_classes.update( + quota_class_name, **opts) + columns = _quota_class_columns(quota_class) + return columns, utils.get_item_properties(quota_class, columns) + + +class GetQuotaClass(command.ShowOne): + """List the quotas for a quota class""" + + log = logging.getLogger(__name__ + '.GetQuotaClass') + + def get_parser(self, prog_name): + parser = super(GetQuotaClass, self).get_parser(prog_name) + parser.add_argument( + 'quota_class_name', + metavar='', + help='The name of quota class') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + quota_class_name = parsed_args.quota_class_name + quota_class = client.quota_class.get(quota_class_name) + columns = _quota_class_columns(quota_class_name) + return columns, utils.get_item_properties(quota_class, columns) diff --git a/zunclient/osc/v1/quotas.py b/zunclient/osc/v1/quotas.py new file mode 100644 index 00000000..1505ba6c --- /dev/null +++ b/zunclient/osc/v1/quotas.py @@ -0,0 +1,113 @@ +# 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. + +from osc_lib.command import command +from osc_lib import utils +from oslo_log import log as logging + +from zunclient.i18n import _ + + +def _quota_columns(quota): + return quota._info.keys() + + +def _get_client(obj, parsed_args): + obj.log.debug("take_action(%s)" % parsed_args) + return obj.app.client_manager.container + + +class UpdateQuota(command.ShowOne): + """Update the quotas of the project""" + + log = logging.getLogger(__name__ + ".UpdateQuota") + + def get_parser(self, prog_name): + parser = super(UpdateQuota, self).get_parser(prog_name) + parser.add_argument( + '--containers', + metavar='', + help='The number of containers allowed per project') + parser.add_argument( + '--memory', + metavar='', + help='The number of megabytes of container RAM ' + 'allowed per project') + parser.add_argument( + '--cpu', + metavar='', + help='The number of container cores or vCPUs ' + 'allowed per project') + parser.add_argument( + '--disk', + metavar='', + help='The number of gigabytes of container Disk ' + 'allowed per project') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + opts = {} + opts['containers'] = parsed_args.containers + opts['memory'] = parsed_args.memory + opts['cpu'] = parsed_args.cpu + opts['disk'] = parsed_args.disk + quota = client.quotas.update(**opts) + columns = _quota_columns(quota) + return columns, utils.get_item_properties(quota, columns) + + +class GetQuota(command.ShowOne): + """Get quota of the project""" + + log = logging.getLogger(__name__ + '.GetQuota') + + def get_parser(self, prog_name): + parser = super(GetQuota, self).get_parser(prog_name) + parser.add_argument( + '--usages', + action='store_true', + help='Whether show quota usage statistic or not') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + quota = client.quotas.get(usages=parsed_args.usages) + columns = _quota_columns(quota) + return columns, utils.get_item_properties(quota, columns) + + +class GetDefaultQuota(command.ShowOne): + """Get default quota of the project""" + + log = logging.getLogger(__name__ + '.GetDefeaultQuota') + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + default_quota = client.quotas.defaults() + columns = _quota_columns(default_quota) + return columns, utils.get_item_properties( + default_quota, columns) + + +class DeleteQuota(command.Command): + """Delete quota of the project""" + + log = logging.getLogger(__name__ + '.DeleteQuota') + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + try: + client.quotas.delete() + print(_('Request to delete quotas has been accepted.')) + except Exception as e: + print("Delete for quotas failed: %(e)s" % {'e': e}) diff --git a/zunclient/tests/unit/test_shell.py b/zunclient/tests/unit/test_shell.py index 2a313b1e..1e17f362 100644 --- a/zunclient/tests/unit/test_shell.py +++ b/zunclient/tests/unit/test_shell.py @@ -249,32 +249,32 @@ class ShellTest(utils.TestCase): project_domain_id='', project_domain_name='', user_domain_id='', user_domain_name='', profile=None, endpoint_override=None, insecure=False, cacert=None, - version=api_versions.APIVersion('1.25')) + version=api_versions.APIVersion('1.26')) def test_main_option_region(self): self.make_env() self._test_main_region( - '--zun-api-version 1.25 ' + '--zun-api-version 1.26 ' '--os-region-name=myregion service-list', 'myregion') def test_main_env_region(self): fake_env = dict(utils.FAKE_ENV, OS_REGION_NAME='myregion') self.make_env(fake_env=fake_env) self._test_main_region( - '--zun-api-version 1.25 ' + '--zun-api-version 1.26 ' 'service-list', 'myregion') def test_main_no_region(self): self.make_env() self._test_main_region( - '--zun-api-version 1.25 ' + '--zun-api-version 1.26 ' 'service-list', None) @mock.patch('zunclient.client.Client') def test_main_endpoint_public(self, mock_client): self.make_env() self.shell( - '--zun-api-version 1.25 ' + '--zun-api-version 1.26 ' '--endpoint-type publicURL service-list') mock_client.assert_called_once_with( username='username', password='password', @@ -284,13 +284,13 @@ class ShellTest(utils.TestCase): project_domain_id='', project_domain_name='', user_domain_id='', user_domain_name='', profile=None, endpoint_override=None, insecure=False, cacert=None, - version=api_versions.APIVersion('1.25')) + version=api_versions.APIVersion('1.26')) @mock.patch('zunclient.client.Client') def test_main_endpoint_internal(self, mock_client): self.make_env() self.shell( - '--zun-api-version 1.25 ' + '--zun-api-version 1.26 ' '--endpoint-type internalURL service-list') mock_client.assert_called_once_with( username='username', password='password', @@ -300,7 +300,7 @@ class ShellTest(utils.TestCase): project_domain_id='', project_domain_name='', user_domain_id='', user_domain_name='', profile=None, endpoint_override=None, insecure=False, cacert=None, - version=api_versions.APIVersion('1.25')) + version=api_versions.APIVersion('1.26')) class ShellTestKeystoneV3(ShellTest): @@ -323,7 +323,7 @@ class ShellTestKeystoneV3(ShellTest): def test_main_endpoint_public(self, mock_client): self.make_env(fake_env=FAKE_ENV4) self.shell( - '--zun-api-version 1.25 ' + '--zun-api-version 1.26 ' '--endpoint-type publicURL service-list') mock_client.assert_called_once_with( username='username', password='password', @@ -334,4 +334,4 @@ class ShellTestKeystoneV3(ShellTest): user_domain_id='', user_domain_name='Default', endpoint_override=None, insecure=False, profile=None, cacert=None, - version=api_versions.APIVersion('1.25')) + version=api_versions.APIVersion('1.26')) diff --git a/zunclient/tests/unit/v1/test_quotas.py b/zunclient/tests/unit/v1/test_quotas.py new file mode 100644 index 00000000..4b6583e1 --- /dev/null +++ b/zunclient/tests/unit/v1/test_quotas.py @@ -0,0 +1,91 @@ +# 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 testtools + +from zunclient.tests.unit import utils +from zunclient.v1 import quotas + + +DEFAULT_QUOTAS = { + 'containers': '40', + 'memory': '51200', + 'cpu': '20', + 'disk': '100' +} + +MODIFIED_QUOTAS = { + 'containers': '50', + 'memory': '51200', + 'cpu': '20', + 'disk': '100' +} + +MODIFIED_USAGE_QUOTAS = { + 'containers': { + 'limit': '50', + 'in_use': '30' + }, + 'memory': {}, + 'cpu': {}, + 'disk': {} +} + +fake_responses = { + '/v1/quotas': + { + 'GET': ( + {}, + MODIFIED_QUOTAS + ), + 'PUT': ( + {}, + MODIFIED_QUOTAS + ), + 'DELETE': ( + {}, + None + ) + }, + '/v1/quotas/defaults': + { + 'GET': ( + {}, + DEFAULT_QUOTAS + ) + }, + '/v1/quotas?usages=True': + { + 'GET': ( + {}, + MODIFIED_USAGE_QUOTAS + ) + } +} + + +class QuotaManagerTest(testtools.TestCase): + + def setUp(self): + super(QuotaManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = quotas.QuotaManager(self.api) + + def test_quotas_get_defaults(self): + quotas = self.mgr.defaults() + expect = [ + ('GET', '/v1/quotas/defaults', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(quotas.containers, DEFAULT_QUOTAS['containers']) + self.assertEqual(quotas.memory, DEFAULT_QUOTAS['memory']) + self.assertEqual(quotas.cpu, DEFAULT_QUOTAS['cpu']) + self.assertEqual(quotas.disk, DEFAULT_QUOTAS['disk']) diff --git a/zunclient/v1/client.py b/zunclient/v1/client.py index 3a3c3d65..1010d2b1 100644 --- a/zunclient/v1/client.py +++ b/zunclient/v1/client.py @@ -23,6 +23,8 @@ from zunclient.v1 import capsules from zunclient.v1 import containers from zunclient.v1 import hosts from zunclient.v1 import images +from zunclient.v1 import quota_classes +from zunclient.v1 import quotas from zunclient.v1 import services from zunclient.v1 import versions @@ -132,6 +134,8 @@ class Client(object): self.capsules = capsules.CapsuleManager(self.http_client) self.availability_zones = az.AvailabilityZoneManager(self.http_client) self.actions = actions.ActionManager(self.http_client) + self.quotas = quotas.QuotaManager(self.http_client) + self.quota_classes = quota_classes.QuotaClassManager(self.http_client) @property def api_version(self): diff --git a/zunclient/v1/quota_classes.py b/zunclient/v1/quota_classes.py new file mode 100644 index 00000000..7096ed66 --- /dev/null +++ b/zunclient/v1/quota_classes.py @@ -0,0 +1,43 @@ +# 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. + +from zunclient.common import base + + +class QuotaClass(base.Resource): + def __repr__(self): + return "" % self._info + + +class QuotaClassManager(base.Manager): + resource_class = QuotaClass + + @staticmethod + def _path(quota_class_name): + return '/v1/quota_classes/{}' . format(quota_class_name) + + def get(self, quota_class_name): + return self._list(self._path(quota_class_name))[0] + + def update(self, quota_class_name, containers=None, + memory=None, cpu=None, disk=None): + resources = {} + if cpu is not None: + resources['cpu'] = cpu + if memory is not None: + resources['memory'] = memory + if containers is not None: + resources['containers'] = containers + if disk is not None: + resources['disk'] = disk + return self._update(self._path(quota_class_name), + resources, method='PUT') diff --git a/zunclient/v1/quota_classes_shell.py b/zunclient/v1/quota_classes_shell.py new file mode 100644 index 00000000..c883b620 --- /dev/null +++ b/zunclient/v1/quota_classes_shell.py @@ -0,0 +1,56 @@ +# 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. + +from zunclient.common import cliutils as utils + + +@utils.arg( + '--containers', + metavar='', + type=int, + help='The number of containers allowed per project') +@utils.arg( + '--cpu', + metavar='', + type=int, + help='The number of container cores or vCPUs allowed per project') +@utils.arg( + '--memory', + metavar='', + type=int, + help='The number of megabytes of container RAM allowed per project') +@utils.arg( + '--disk', + metavar='', + type=int, + help='The number of gigabytes of container Disk allowed per project') +@utils.arg( + 'quota_class_name', + metavar='', + help='The name of quota class') +def do_quota_class_update(cs, args): + """Print an updated quotas for a quota class""" + utils.print_dict(cs.quota_classes.update( + args.quota_class_name, + containers=args.containers, + memory=args.memory, + cpu=args.cpu, + disk=args.disk)._info) + + +@utils.arg( + 'quota_class_name', + metavar='', + help='The name of quota class') +def do_quota_class_get(cs, args): + """Print a quotas for a quota class""" + utils.print_dict(cs.quota_classes.get(args.quota_class_name)._info) diff --git a/zunclient/v1/quotas.py b/zunclient/v1/quotas.py new file mode 100644 index 00000000..73a69cc0 --- /dev/null +++ b/zunclient/v1/quotas.py @@ -0,0 +1,50 @@ +# 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. + +from zunclient.common import base + + +class Quota(base.Resource): + def __repr__(self): + return "" % self._info + + +class QuotaManager(base.Manager): + resource_class = Quota + + @staticmethod + def _path(): + return '/v1/quotas' + + def get(self, **kwargs): + if not kwargs.get('usages'): + kwargs = {} + return self._list(self._path(), qparams=kwargs)[0] + + def update(self, containers=None, memory=None, + cpu=None, disk=None): + resources = {} + if cpu is not None: + resources['cpu'] = cpu + if memory is not None: + resources['memory'] = memory + if containers is not None: + resources['containers'] = containers + if disk is not None: + resources['disk'] = disk + return self._update(self._path(), resources, method='PUT') + + def defaults(self): + return self._list(self._path() + '/defaults')[0] + + def delete(self): + return self._delete(self._path()) diff --git a/zunclient/v1/quotas_shell.py b/zunclient/v1/quotas_shell.py new file mode 100644 index 00000000..25b49c09 --- /dev/null +++ b/zunclient/v1/quotas_shell.py @@ -0,0 +1,65 @@ +# 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. + +from zunclient.common import cliutils as utils + + +@utils.arg( + '--containers', + metavar='', + type=int, + help='The number of containers allowed per project') +@utils.arg( + '--cpu', + metavar='', + type=int, + help='The number of container cores or vCPUs allowed per project') +@utils.arg( + '--memory', + metavar='', + type=int, + help='The number of megabytes of container RAM allowed per project') +@utils.arg( + '--disk', + metavar='', + type=int, + help='The number of gigabytes of container Disk allowed per project') +def do_quota_update(cs, args): + """Print an updated quotas for a project""" + utils.print_dict(cs.quotas.update(containers=args.containers, + memory=args.memory, + cpu=args.cpu, + disk=args.disk)._info) + + +@utils.arg( + '--usages', + default=False, + action='store_true', + help='Whether show quota usage statistic or not') +def do_quota_get(cs, args): + """Print a quotas for a project with usages (optional)""" + if args.usages: + utils.print_dict(cs.quotas.get(usages=args.usages)._info, + value_fields=('limit', 'in_use')) + else: + utils.print_dict(cs.quotas.get(usages=args.usages)._info) + + +def do_quota_defaults(cs, args): + """Print a default quotas for a project""" + utils.print_dict(cs.quotas.defaults()._info) + + +def do_quota_delete(cs, args): + """Delete quotas for a project""" + cs.quotas.delete() diff --git a/zunclient/v1/shell.py b/zunclient/v1/shell.py index 7ff05e7c..73c8c4a9 100644 --- a/zunclient/v1/shell.py +++ b/zunclient/v1/shell.py @@ -19,6 +19,8 @@ from zunclient.v1 import capsules_shell from zunclient.v1 import containers_shell from zunclient.v1 import hosts_shell from zunclient.v1 import images_shell +from zunclient.v1 import quota_classes_shell +from zunclient.v1 import quotas_shell from zunclient.v1 import services_shell from zunclient.v1 import versions_shell @@ -31,4 +33,6 @@ COMMAND_MODULES = [ versions_shell, capsules_shell, actions_shell, + quotas_shell, + quota_classes_shell, ]