Add Quota client API and OSC support

Includes list, show, set and reset of quotas, and show quota defaults.

As per OSC CLI guidelines, long options names shall not use underscore
but single dashes internally between words. ChangeId
Iaf7d251ef4054fa28ef61e02fdebc05c83786e9e will address the newly added
--loadbalancer option for quota setting.

DocImpact
Task: 5751
Task: 5752
Task: 5753
Task: 5754
Task: 5755
Story: 2001235

Change-Id: I336477e723867b5da2a041b441a25a7a611596ee
This commit is contained in:
Carlos Goncalves 2017-11-09 17:21:38 +00:00 committed by Bar RH
parent 6fd1422277
commit 6dcd7405bb
11 changed files with 640 additions and 0 deletions

View File

@ -67,3 +67,10 @@ l7rule
.. autoprogram-cliff:: openstack.load_balancer.v2
:command: loadbalancer l7rule *
=====
quota
=====
.. autoprogram-cliff:: openstack.load_balancer.v2
:command: loadbalancer quota *

View File

@ -31,3 +31,7 @@ BASE_L7POLICY_URL = '/l7policies'
BASE_SINGLE_L7POLICY_URL = BASE_L7POLICY_URL + '/{policy_uuid}'
BASE_L7RULE_URL = BASE_SINGLE_L7POLICY_URL + '/rules'
BASE_SINGLE_L7RULE_URL = BASE_SINGLE_L7POLICY_URL + '/rules/{rule_uuid}'
BASE_QUOTA_URL = '/quotas'
BASE_SINGLE_QUOTA_URL = BASE_QUOTA_URL + '/{uuid}'
BASE_QUOTA_DEFAULT_URL = BASE_QUOTA_URL + '/defaults'

View File

@ -580,6 +580,72 @@ class APIv2(api.BaseAPI):
return response
def quota_list(self, **params):
"""List all quotas
:param params:
Parameters to filter on (not implemented)
:return:
A ``dict`` representing a list of quotas for the project
"""
url = const.BASE_QUOTA_URL
response = self.list(url, **params)
return response
def quota_show(self, project_id):
"""Show a quota
:param string project_id:
ID of the project to show
:return:
A ``dict`` representing the quota for the project
"""
response = self.find(path=const.BASE_QUOTA_URL, value=project_id)
return response
@correct_return_codes
def quota_reset(self, project_id):
"""Reset a quota
:param string project_id:
The ID of the project to reset quotas
:return:
``None``
"""
url = const.BASE_SINGLE_QUOTA_URL.format(uuid=project_id)
response = self.delete(url)
return response
@correct_return_codes
def quota_set(self, project_id, **params):
"""Update a quota's settings
:param string project_id:
The ID of the project to update
:param params:
A ``dict`` of arguments to update project quota
:return:
A ``dict`` representing the updated quota
"""
url = const.BASE_SINGLE_QUOTA_URL.format(uuid=project_id)
response = self.create(url, method='PUT', **params)
return response
def quota_defaults_show(self):
"""Show quota defaults
:return:
A ``dict`` representing a list of quota defaults
"""
url = const.BASE_QUOTA_DEFAULT_URL
response = self.list(url)
return response
class OctaviaClientException(Exception):
"""The base exception class for all exceptions this library raises."""

View File

@ -206,3 +206,20 @@ MONITOR_COLUMNS = (
'type',
'admin_state_up',
)
QUOTA_ROWS = (
'load_balancer',
'listener',
'pool',
'health_monitor',
'member'
)
QUOTA_COLUMNS = (
'project_id',
'load_balancer',
'listener',
'pool',
'health_monitor',
'member'
)

View File

@ -0,0 +1,185 @@
# 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.
#
"""Quota action implementation"""
from cliff import lister
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
from octaviaclient.osc.v2 import constants as const
from octaviaclient.osc.v2 import utils as v2_utils
class ListQuota(lister.Lister):
"""List quotas"""
def get_parser(self, prog_name):
parser = super(ListQuota, self).get_parser(prog_name)
parser.add_argument(
'--project',
metavar='<project-id>',
help="Name or UUID of the project."
)
return parser
def take_action(self, parsed_args):
columns = const.QUOTA_COLUMNS
attrs = v2_utils.get_listener_attrs(self.app.client_manager,
parsed_args)
data = self.app.client_manager.load_balancer.quota_list(**attrs)
formatters = {'quotas': v2_utils.format_list}
return (columns,
(utils.get_dict_properties(s, columns, formatters=formatters)
for s in data['quotas']))
class ShowQuota(command.ShowOne):
"""Show the quota details for a project"""
def get_parser(self, prog_name):
parser = super(ShowQuota, self).get_parser(prog_name)
parser.add_argument(
'project',
metavar='<project>',
help="Name or UUID of the project."
)
return parser
def take_action(self, parsed_args):
rows = const.QUOTA_ROWS
attrs = v2_utils.get_quota_attrs(self.app.client_manager,
parsed_args)
project_id = attrs.pop('project_id')
data = self.app.client_manager.load_balancer.quota_show(
project_id=project_id
)
return (rows, (utils.get_dict_properties(data, rows)))
class ShowQuotaDefaults(command.ShowOne):
"""Show quota defaults"""
def take_action(self, parsed_args):
rows = const.QUOTA_ROWS
data = self.app.client_manager.load_balancer.quota_defaults_show()
return (rows, (utils.get_dict_properties(data['quota'], rows)))
class SetQuota(command.ShowOne):
"""Update a quota"""
@staticmethod
def _check_attrs(attrs):
args = ['health_monitor', 'listener', 'load_balancer', 'member',
'pool']
if not any(arg in attrs for arg in args):
args = [arg.replace('_', '') for arg in args]
msg = ('Missing required argument. Requires at least one of:%s' %
','.join((' --%s' % arg) for arg in args))
raise exceptions.CommandError(msg)
def get_parser(self, prog_name):
parser = super(SetQuota, self).get_parser(prog_name)
quota_group = parser.add_argument_group(
"Quota limits",
description='At least one of the following arguments is required.'
)
quota_group.add_argument(
'--healthmonitor',
dest='health_monitor',
metavar='<health_monitor>',
help=('New value for the health monitor quota. Value -1 means '
'unlimited.')
)
quota_group.add_argument(
'--listener',
metavar='<listener>',
help=('New value for the listener quota. Value -1 means '
'unlimited.')
)
quota_group.add_argument(
'--loadbalancer',
dest='load_balancer',
metavar='<load_balancer>',
help=('New value for the load balancer quota limit. Value -1 '
'means unlimited.')
)
quota_group.add_argument(
'--member',
metavar='<member>',
help=('New value for the member quota limit. Value -1 means '
'unlimited.')
)
quota_group.add_argument(
'--pool',
metavar='<pool>',
help=('New value for the pool quota limit. Value -1 means '
'unlimited.')
)
parser.add_argument(
'project',
metavar='<project>',
help="Name or UUID of the project."
)
return parser
def take_action(self, parsed_args):
rows = const.QUOTA_ROWS
attrs = v2_utils.get_quota_attrs(self.app.client_manager,
parsed_args)
self._check_attrs(attrs)
project_id = attrs.pop('project_id')
body = {'quota': attrs}
data = self.app.client_manager.load_balancer.quota_set(project_id,
json=body)
return (rows, (utils.get_dict_properties(data['quota'], rows)))
class ResetQuota(command.Command):
"""Resets quotas to default quotas"""
def get_parser(self, prog_name):
parser = super(ResetQuota, self).get_parser(prog_name)
parser.add_argument(
'project',
metavar="<project>",
help="Project to reset quotas (name or ID)"
)
return parser
def take_action(self, parsed_args):
attrs = v2_utils.get_quota_attrs(self.app.client_manager,
parsed_args)
project_id = attrs.pop('project_id')
self.app.client_manager.load_balancer.quota_reset(
project_id=project_id)

View File

@ -376,6 +376,26 @@ def get_health_monitor_attrs(client_manager, parsed_args):
return attrs
def get_quota_attrs(client_manager, parsed_args):
attr_map = {
'health_monitor': ('health_monitor', int),
'listener': ('listener', int),
'load_balancer': ('load_balancer', int),
'member': ('member', int),
'pool': ('pool', int),
'project': (
'project_id',
'project',
client_manager.identity
),
}
_attrs = vars(parsed_args)
attrs = _map_attrs(_attrs, attr_map)
return attrs
def format_list(data):
return '\n'.join(i['id'] for i in data)

View File

@ -31,6 +31,7 @@ FAKE_ME = uuidutils.generate_uuid()
FAKE_L7PO = uuidutils.generate_uuid()
FAKE_L7RU = uuidutils.generate_uuid()
FAKE_HM = uuidutils.generate_uuid()
FAKE_PRJ = uuidutils.generate_uuid()
LIST_LB_RESP = {
@ -74,6 +75,16 @@ LIST_HM_RESP = {
{'id': uuidutils.generate_uuid()}]
}
LIST_QT_RESP = {
'quotas':
[{'health_monitor': -1},
{'listener': -1},
{'load_balancer': 5},
{'member': 10},
{'pool': 20},
{'project': uuidutils.generate_uuid()}]
}
SINGLE_LB_RESP = {'loadbalancer': {'id': FAKE_LB, 'name': 'lb1'}}
SINGLE_LB_UPDATE = {"loadbalancer": {"admin_state_up": False}}
SINGLE_LB_STATS_RESP = {'bytes_in': '0'}
@ -96,6 +107,9 @@ SINGLE_L7RU_UPDATE = {'rule': {'admin_state_up': False}}
SINGLE_HM_RESP = {'healthmonitor': {'id': FAKE_ME}}
SINGLE_HM_UPDATE = {'healthmonitor': {'admin_state_up': False}}
SINGLE_QT_RESP = {'quota': {'pool': -1}}
SINGLE_QT_UPDATE = {'quota': {'pool': -1}}
class TestLoadBalancerv2(utils.TestCase):
@ -727,3 +741,66 @@ class TestLoadBalancer(TestLoadBalancerv2):
self._error_message,
self.api.health_monitor_delete,
FAKE_HM)
def test_list_quota_no_options(self):
self.requests_mock.register_uri(
'GET',
FAKE_URL + 'quotas',
json=LIST_QT_RESP,
status_code=200,
)
ret = self.api.quota_list()
self.assertEqual(LIST_QT_RESP, ret)
def test_show_quota(self):
self.requests_mock.register_uri(
'GET',
FAKE_URL + 'quotas/' + FAKE_PRJ,
json=SINGLE_QT_RESP,
status_code=200
)
ret = self.api.quota_show(FAKE_PRJ)
self.assertEqual(SINGLE_QT_RESP['quota'], ret)
def test_set_quota(self):
self.requests_mock.register_uri(
'PUT',
FAKE_URL + 'quotas/' + FAKE_PRJ,
json=SINGLE_QT_UPDATE,
status_code=200
)
ret = self.api.quota_set(FAKE_PRJ, json=SINGLE_QT_UPDATE)
self.assertEqual(SINGLE_QT_UPDATE, ret)
def test_set_quota_error(self):
self.requests_mock.register_uri(
'PUT',
FAKE_URL + 'quotas/' + FAKE_PRJ,
text='{"faultstring": "%s"}' % self._error_message,
status_code=400
)
self.assertRaisesRegex(lb.OctaviaClientException,
self._error_message,
self.api.quota_set,
FAKE_PRJ, json=SINGLE_QT_UPDATE)
def test_reset_quota(self):
self.requests_mock.register_uri(
'DELETE',
FAKE_URL + 'quotas/' + FAKE_PRJ,
status_code=200
)
ret = self.api.quota_reset(FAKE_PRJ)
self.assertEqual(200, ret.status_code)
def test_reset_quota_error(self):
self.requests_mock.register_uri(
'DELETE',
FAKE_URL + 'quotas/' + FAKE_PRJ,
text='{"faultstring": "%s"}' % self._error_message,
status_code=400
)
self.assertRaisesRegex(lb.OctaviaClientException,
self._error_message,
self.api.quota_reset,
FAKE_PRJ)

View File

@ -264,3 +264,28 @@ class FakeHM(object):
loaded=True)
return hm
class FakeQT(object):
"""Fake one or more Quota."""
@staticmethod
def create_one_quota(attrs=None):
attrs = attrs or {}
qt_info = {
"health_monitor": -1,
"listener": None,
"load_balancer": 5,
"member": 50,
"pool": None,
"project_id": uuidutils.generate_uuid(dashed=True)
}
qt_info.update(attrs)
qt = fakes.FakeResource(
info=copy.deepcopy(qt_info),
loaded=True)
return qt

View File

@ -0,0 +1,227 @@
# 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 copy
import mock
from osc_lib import exceptions
from octaviaclient.osc.v2 import quota
from octaviaclient.tests.unit.osc.v2 import fakes as qt_fakes
AUTH_TOKEN = "foobar"
AUTH_URL = "http://192.0.2.2"
class TestQuota(qt_fakes.TestLoadBalancerv2):
_qt = qt_fakes.FakeQT.create_one_quota()
columns = ('project_id', 'load_balancer', 'listener', 'pool',
'health_monitor', 'member')
datalist = (
(
_qt.project_id,
_qt.load_balancer,
_qt.listener,
_qt.pool,
_qt.health_monitor,
_qt.member
),
)
info = {
'quotas':
[{
"project_id": _qt.project_id,
"load_balancer": _qt.load_balancer,
"listener": _qt.listener,
"pool": _qt.pool,
"health_monitor": _qt.health_monitor,
"member": _qt.member
}]
}
qt_info = copy.deepcopy(info)
def setUp(self):
super(TestQuota, self).setUp()
self.qt_mock = self.app.client_manager.load_balancer.load_balancers
self.qt_mock.reset_mock()
self.api_mock = mock.Mock()
self.api_mock.quota_list.return_value = self.qt_info
lb_client = self.app.client_manager
lb_client.load_balancer = self.api_mock
class TestQuotaList(TestQuota):
def setUp(self):
super(TestQuotaList, self).setUp()
self.cmd = quota.ListQuota(self.app, None)
def test_quota_list_no_options(self):
arglist = []
verifylist = []
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.quota_list.assert_called_with()
self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist, tuple(data))
class TestQuotaShow(TestQuota):
def setUp(self):
super(TestQuotaShow, self).setUp()
self.api_mock = mock.Mock()
self.api_mock.quota_list.return_value = self.qt_info
self.api_mock.quota_show.return_value = {
'quota': self.qt_info['quotas'][0]}
lb_client = self.app.client_manager
lb_client.load_balancer = self.api_mock
self.cmd = quota.ShowQuota(self.app, None)
@mock.patch('octaviaclient.osc.v2.utils.get_quota_attrs')
def test_quota_show(self, mock_attrs):
mock_attrs.return_value = self.qt_info['quotas'][0]
arglist = [self._qt.project_id]
verifylist = [
('project', self._qt.project_id),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.api_mock.quota_show.assert_called_with(
project_id=self._qt.project_id)
class TestQuotaDefaultsShow(TestQuota):
qt_defaults = {
"health_monitor": 1,
"listener": 2,
"load_balancer": 3,
"member": 4,
"pool": 5
}
def setUp(self):
super(TestQuotaDefaultsShow, self).setUp()
self.api_mock = mock.Mock()
self.api_mock.quota_defaults_show.return_value = {
'quota': self.qt_defaults}
lb_client = self.app.client_manager
lb_client.load_balancer = self.api_mock
self.cmd = quota.ShowQuotaDefaults(self.app, None)
def test_quota_defaults_show(self):
arglist = []
verifylist = []
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
rows, data = self.cmd.take_action(parsed_args)
data = dict(zip(rows, data))
self.api_mock.quota_defaults_show.assert_called_with()
self.assertEqual(self.qt_defaults, data)
class TestQuotaSet(TestQuota):
def setUp(self):
super(TestQuotaSet, self).setUp()
self.api_mock = mock.Mock()
self.api_mock.quota_set.return_value = {
'quota': self.qt_info['quotas'][0]}
lb_client = self.app.client_manager
lb_client.load_balancer = self.api_mock
self.cmd = quota.SetQuota(self.app, None)
@mock.patch('octaviaclient.osc.v2.utils.get_quota_attrs')
def test_quota_set(self, mock_attrs):
mock_attrs.return_value = {
'project_id': self._qt.project_id,
'health_monitor': '-1',
'listener': '1',
'load_balancer': '2',
'member': '3',
'pool': '4'
}
arglist = [self._qt.project_id, '--healthmonitor', '-1', '--listener',
'1', '--loadbalancer', '2', '--member', '3', '--pool', '4']
verifylist = [
('project', self._qt.project_id),
('health_monitor', '-1'),
('listener', '1'),
('load_balancer', '2'),
('member', '3'),
('pool', '4')
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.api_mock.quota_set.assert_called_with(
self._qt.project_id, json={'quota': {'health_monitor': '-1',
'listener': '1',
'load_balancer': '2',
'member': '3',
'pool': '4'}})
@mock.patch('octaviaclient.osc.v2.utils.get_quota_attrs')
def test_quota_set_no_args(self, mock_attrs):
project_id = ['fake_project_id']
mock_attrs.return_value = {'project_id': project_id}
arglist = [project_id]
verifylist = []
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.assertRaises(exceptions.CommandError, self.cmd.take_action,
parsed_args)
self.assertNotCalled(self.api_mock.quota_set)
class TestQuotaReset(TestQuota):
def setUp(self):
super(TestQuotaReset, self).setUp()
self.cmd = quota.ResetQuota(self.app, None)
@mock.patch('octaviaclient.osc.v2.utils.get_quota_attrs')
def test_quota_reset(self, mock_attrs):
# create new quota, otherwise other quota tests will fail occasionally
# due to a race condition
project_id = 'fake_project_id'
attrs = {'project_id': project_id}
qt_reset = qt_fakes.FakeQT.create_one_quota(attrs)
mock_attrs.return_value = qt_reset.to_dict()
arglist = [project_id]
verifylist = [
('project', project_id)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.api_mock.quota_reset.assert_called_with(
project_id=qt_reset.project_id)

View File

@ -0,0 +1,7 @@
---
features:
- |
Octavia quota support for the OpenStack client plugin.
* List, show, set, reset quotas
* Show quota defaults

View File

@ -63,6 +63,11 @@ openstack.load_balancer.v2 =
loadbalancer_healthmonitor_show = octaviaclient.osc.v2.health_monitor:ShowHealthMonitor
loadbalancer_healthmonitor_delete = octaviaclient.osc.v2.health_monitor:DeleteHealthMonitor
loadbalancer_healthmonitor_set = octaviaclient.osc.v2.health_monitor:SetHealthMonitor
loadbalancer_quota_list = octaviaclient.osc.v2.quota:ListQuota
loadbalancer_quota_show = octaviaclient.osc.v2.quota:ShowQuota
loadbalancer_quota_defaults_show = octaviaclient.osc.v2.quota:ShowQuotaDefaults
loadbalancer_quota_reset = octaviaclient.osc.v2.quota:ResetQuota
loadbalancer_quota_set = octaviaclient.osc.v2.quota:SetQuota
[build_sphinx]
source-dir = doc/source