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_rebuild = zunclient.osc.v1.containers:RebuildContainer
appcontainer_action_list = zunclient.osc.v1.containers:ActionList appcontainer_action_list = zunclient.osc.v1.containers:ActionList
appcontainer_action_show = zunclient.osc.v1.containers:ActionShow 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] [build_sphinx]
source-dir = doc/source source-dir = doc/source

View File

@@ -31,7 +31,7 @@ if not LOG.handlers:
HEADER_NAME = "OpenStack-API-Version" HEADER_NAME = "OpenStack-API-Version"
SERVICE_TYPE = "container" SERVICE_TYPE = "container"
MIN_API_VERSION = '1.1' MIN_API_VERSION = '1.1'
MAX_API_VERSION = '1.25' MAX_API_VERSION = '1.26'
DEFAULT_API_VERSION = '1.latest' DEFAULT_API_VERSION = '1.latest'
_SUBSTITUTIONS = {} _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()) 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. """Print a `dict` as a table of two columns.
:param dct: `dict` to print :param dct: `dict` to print
:param dict_property: name of the first column :param dict_property: name of the first column
:param wrap: wrapping for the second column :param wrap: wrapping for the second column
:param value_fields: attributes that correspond to columns, in order
""" """
pt = prettytable.PrettyTable([dict_property, 'Value']) pt = prettytable.PrettyTable([dict_property, 'Value'])
if value_fields:
pt = prettytable.PrettyTable([dict_property] + list(value_fields))
pt.align = 'l' pt.align = 'l'
for k, v in dct.items(): for k, v in dct.items():
# convert dict to str to check length # 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)) v = six.text_type(keys_and_vals_to_strs(v))
if wrap > 0: if wrap > 0:
v = textwrap.fill(six.text_type(v), wrap) 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: for line in lines:
pt.add_row([col1, line]) pt.add_row([col1, line])
col1 = '' 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): elif isinstance(v, list):
val = str([str(i) for i in v]) val = str([str(i) for i in v])
pt.add_row([k, val]) 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='', project_domain_id='', project_domain_name='',
user_domain_id='', user_domain_name='', profile=None, user_domain_id='', user_domain_name='', profile=None,
endpoint_override=None, insecure=False, cacert=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): def test_main_option_region(self):
self.make_env() self.make_env()
self._test_main_region( self._test_main_region(
'--zun-api-version 1.25 ' '--zun-api-version 1.26 '
'--os-region-name=myregion service-list', 'myregion') '--os-region-name=myregion service-list', 'myregion')
def test_main_env_region(self): def test_main_env_region(self):
fake_env = dict(utils.FAKE_ENV, OS_REGION_NAME='myregion') fake_env = dict(utils.FAKE_ENV, OS_REGION_NAME='myregion')
self.make_env(fake_env=fake_env) self.make_env(fake_env=fake_env)
self._test_main_region( self._test_main_region(
'--zun-api-version 1.25 ' '--zun-api-version 1.26 '
'service-list', 'myregion') 'service-list', 'myregion')
def test_main_no_region(self): def test_main_no_region(self):
self.make_env() self.make_env()
self._test_main_region( self._test_main_region(
'--zun-api-version 1.25 ' '--zun-api-version 1.26 '
'service-list', None) 'service-list', None)
@mock.patch('zunclient.client.Client') @mock.patch('zunclient.client.Client')
def test_main_endpoint_public(self, mock_client): def test_main_endpoint_public(self, mock_client):
self.make_env() self.make_env()
self.shell( self.shell(
'--zun-api-version 1.25 ' '--zun-api-version 1.26 '
'--endpoint-type publicURL service-list') '--endpoint-type publicURL service-list')
mock_client.assert_called_once_with( mock_client.assert_called_once_with(
username='username', password='password', username='username', password='password',
@@ -284,13 +284,13 @@ class ShellTest(utils.TestCase):
project_domain_id='', project_domain_name='', project_domain_id='', project_domain_name='',
user_domain_id='', user_domain_name='', profile=None, user_domain_id='', user_domain_name='', profile=None,
endpoint_override=None, insecure=False, cacert=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') @mock.patch('zunclient.client.Client')
def test_main_endpoint_internal(self, mock_client): def test_main_endpoint_internal(self, mock_client):
self.make_env() self.make_env()
self.shell( self.shell(
'--zun-api-version 1.25 ' '--zun-api-version 1.26 '
'--endpoint-type internalURL service-list') '--endpoint-type internalURL service-list')
mock_client.assert_called_once_with( mock_client.assert_called_once_with(
username='username', password='password', username='username', password='password',
@@ -300,7 +300,7 @@ class ShellTest(utils.TestCase):
project_domain_id='', project_domain_name='', project_domain_id='', project_domain_name='',
user_domain_id='', user_domain_name='', profile=None, user_domain_id='', user_domain_name='', profile=None,
endpoint_override=None, insecure=False, cacert=None, endpoint_override=None, insecure=False, cacert=None,
version=api_versions.APIVersion('1.25')) version=api_versions.APIVersion('1.26'))
class ShellTestKeystoneV3(ShellTest): class ShellTestKeystoneV3(ShellTest):
@@ -323,7 +323,7 @@ class ShellTestKeystoneV3(ShellTest):
def test_main_endpoint_public(self, mock_client): def test_main_endpoint_public(self, mock_client):
self.make_env(fake_env=FAKE_ENV4) self.make_env(fake_env=FAKE_ENV4)
self.shell( self.shell(
'--zun-api-version 1.25 ' '--zun-api-version 1.26 '
'--endpoint-type publicURL service-list') '--endpoint-type publicURL service-list')
mock_client.assert_called_once_with( mock_client.assert_called_once_with(
username='username', password='password', username='username', password='password',
@@ -334,4 +334,4 @@ class ShellTestKeystoneV3(ShellTest):
user_domain_id='', user_domain_name='Default', user_domain_id='', user_domain_name='Default',
endpoint_override=None, insecure=False, profile=None, endpoint_override=None, insecure=False, profile=None,
cacert=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 containers
from zunclient.v1 import hosts from zunclient.v1 import hosts
from zunclient.v1 import images 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 services
from zunclient.v1 import versions from zunclient.v1 import versions
@@ -132,6 +134,8 @@ class Client(object):
self.capsules = capsules.CapsuleManager(self.http_client) self.capsules = capsules.CapsuleManager(self.http_client)
self.availability_zones = az.AvailabilityZoneManager(self.http_client) self.availability_zones = az.AvailabilityZoneManager(self.http_client)
self.actions = actions.ActionManager(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 @property
def api_version(self): 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 containers_shell
from zunclient.v1 import hosts_shell from zunclient.v1 import hosts_shell
from zunclient.v1 import images_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 services_shell
from zunclient.v1 import versions_shell from zunclient.v1 import versions_shell
@@ -31,4 +33,6 @@ COMMAND_MODULES = [
versions_shell, versions_shell,
capsules_shell, capsules_shell,
actions_shell, actions_shell,
quotas_shell,
quota_classes_shell,
] ]