Add quotas and quota classes

Change-Id: I6ecdb63e633f145614b1a0d57712352dace7acf2
Implements: blueprint quota-support
This commit is contained in:
Kien Nguyen
2018-09-27 17:11:16 +07:00
committed by Hongbin Lu
parent 26f8c6d282
commit 239b3b9cf5
13 changed files with 542 additions and 13 deletions

View File

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

View File

@@ -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 = {}

View File

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

View File

@@ -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='<containers>',
help='The number of containers allowed per project')
parser.add_argument(
'--memory',
metavar='<memory>',
help='The number of megabytes of container RAM '
'allowed per project')
parser.add_argument(
'--cpu',
metavar='<cpu>',
help='The number of container cores or vCPUs '
'allowed per project')
parser.add_argument(
'--disk',
metavar='<disk>',
help='The number of gigabytes of container Disk '
'allowed per project')
parser.add_argument(
'quota_class_name',
metavar='<quota_class_name>',
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='<quota_class_name>',
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)

113
zunclient/osc/v1/quotas.py Normal file
View File

@@ -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='<containers>',
help='The number of containers allowed per project')
parser.add_argument(
'--memory',
metavar='<memory>',
help='The number of megabytes of container RAM '
'allowed per project')
parser.add_argument(
'--cpu',
metavar='<cpu>',
help='The number of container cores or vCPUs '
'allowed per project')
parser.add_argument(
'--disk',
metavar='<disk>',
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})

View File

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

View File

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

View File

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

View File

@@ -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 "<QuotaClass %s>" % 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')

View File

@@ -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='<containers>',
type=int,
help='The number of containers allowed per project')
@utils.arg(
'--cpu',
metavar='<cpu>',
type=int,
help='The number of container cores or vCPUs allowed per project')
@utils.arg(
'--memory',
metavar='<memory>',
type=int,
help='The number of megabytes of container RAM allowed per project')
@utils.arg(
'--disk',
metavar='<disk>',
type=int,
help='The number of gigabytes of container Disk allowed per project')
@utils.arg(
'quota_class_name',
metavar='<quota_class_name>',
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='<quota_class_name>',
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)

50
zunclient/v1/quotas.py Normal file
View File

@@ -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 "<Quota %s>" % 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())

View File

@@ -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='<containers>',
type=int,
help='The number of containers allowed per project')
@utils.arg(
'--cpu',
metavar='<cpu>',
type=int,
help='The number of container cores or vCPUs allowed per project')
@utils.arg(
'--memory',
metavar='<memory>',
type=int,
help='The number of megabytes of container RAM allowed per project')
@utils.arg(
'--disk',
metavar='<disk>',
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()

View File

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