Add get_usages_counts_for_quota to SchedulerReportClient

This adds a method for requesting /usages from placement for the
purpose of counting quota usage for cores and ram. It is used in the
next patch in the series.

Part of blueprint count-quota-usage-from-placement

Change-Id: I35f98f88f8353602e1bfc135f35d1b7bc9ba42a4
This commit is contained in:
melanie witt 2019-04-10 23:07:44 +00:00
parent 85e287d3ef
commit 54408d0ce4
3 changed files with 156 additions and 0 deletions

View File

@ -2257,6 +2257,11 @@ class InventoryInUse(InvalidInventory):
pass
class UsagesRetrievalFailed(NovaException):
msg_fmt = _("Failed to retrieve usages for project '%(project_id)s' and "
"user '%(user_id)s'.")
class UnsupportedPointerModelRequested(Invalid):
msg_fmt = _("Pointer model '%(model)s' requested is not supported by "
"host.")

View File

@ -49,6 +49,7 @@ POST_RPS_RETURNS_PAYLOAD_API_VERSION = '1.20'
AGGREGATE_GENERATION_VERSION = '1.19'
NESTED_PROVIDER_API_VERSION = '1.14'
POST_ALLOCATIONS_API_VERSION = '1.13'
GET_USAGES_VERSION = '1.9'
AggInfo = collections.namedtuple('AggInfo', ['aggregates', 'generation'])
TraitInfo = collections.namedtuple('TraitInfo', ['traits', 'generation'])
@ -2315,3 +2316,67 @@ class SchedulerReportClient(object):
new_aggs = existing_aggs - set([agg_uuid])
self.set_aggregates_for_provider(
context, rp_uuid, new_aggs, use_cache=False, generation=gen)
@staticmethod
def _handle_usages_error_from_placement(resp, project_id, user_id=None):
msg = ('[%(placement_req_id)s] Failed to retrieve usages for project '
'%(project_id)s and user %(user_id)s. Got %(status_code)d: '
'%(err_text)s')
args = {'placement_req_id': get_placement_request_id(resp),
'project_id': project_id,
'user_id': user_id or 'N/A',
'status_code': resp.status_code,
'err_text': resp.text}
LOG.error(msg, args)
raise exception.UsagesRetrievalFailed(project_id=project_id,
user_id=user_id or 'N/A')
@retrying.retry(stop_max_attempt_number=4,
retry_on_exception=lambda e: isinstance(
e, ks_exc.ConnectFailure))
def _get_usages(self, context, project_id, user_id=None):
url = '/usages?project_id=%s' % project_id
if user_id:
url = ''.join([url, '&user_id=%s' % user_id])
return self.get(url, version=GET_USAGES_VERSION,
global_request_id=context.global_id)
def get_usages_counts_for_quota(self, context, project_id, user_id=None):
"""Get the usages counts for the purpose of counting quota usage.
:param context: The request context
:param project_id: The project_id to count across
:param user_id: The user_id to count across
:returns: A dict containing the project-scoped and user-scoped counts
if user_id is specified. For example:
{'project': {'cores': <count across project>,
'ram': <count across project>},
{'user': {'cores': <count across user>,
'ram': <count across user>},
:raises: `exception.UsagesRetrievalFailed` if a placement API call
fails
"""
total_counts = {'project': {}}
# First query counts across all users of a project
resp = self._get_usages(context, project_id)
if resp:
data = resp.json()
# The response from placement will not contain a resource class if
# there is no usage. We can consider a missing class to be 0 usage.
cores = data['usages'].get(orc.VCPU, 0)
ram = data['usages'].get(orc.MEMORY_MB, 0)
total_counts['project'] = {'cores': cores, 'ram': ram}
else:
self._handle_usages_error_from_placement(resp, project_id)
# If specified, second query counts across one user in the project
if user_id:
resp = self._get_usages(context, project_id, user_id=user_id)
if resp:
data = resp.json()
cores = data['usages'].get(orc.VCPU, 0)
ram = data['usages'].get(orc.MEMORY_MB, 0)
total_counts['user'] = {'cores': cores, 'ram': ram}
else:
self._handle_usages_error_from_placement(resp, project_id,
user_id=user_id)
return total_counts

View File

@ -4071,3 +4071,89 @@ class TestAggregateAddRemoveHost(SchedulerReportClientTestCase):
mock_set_aggs.assert_has_calls([mock.call(
self.context, uuids.cn1, set([]), use_cache=False,
generation=gen) for gen in gens])
class TestUsages(SchedulerReportClientTestCase):
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.get')
def test_get_usages_counts_for_quota_fail(self, mock_get):
# First call with project fails
mock_get.return_value = fake_requests.FakeResponse(500, content='err')
self.assertRaises(exception.UsagesRetrievalFailed,
self.client.get_usages_counts_for_quota,
self.context, 'fake-project')
mock_get.assert_called_once_with(
'/usages?project_id=fake-project', version='1.9',
global_request_id=self.context.global_id)
# Second call with project + user fails
mock_get.reset_mock()
fake_good_response = fake_requests.FakeResponse(
200, content=jsonutils.dumps(
{'usages': {orc.VCPU: 2,
orc.MEMORY_MB: 512}}))
mock_get.side_effect = [fake_good_response,
fake_requests.FakeResponse(500, content='err')]
self.assertRaises(exception.UsagesRetrievalFailed,
self.client.get_usages_counts_for_quota,
self.context, 'fake-project', user_id='fake-user')
self.assertEqual(2, mock_get.call_count)
call1 = mock.call(
'/usages?project_id=fake-project', version='1.9',
global_request_id=self.context.global_id)
call2 = mock.call(
'/usages?project_id=fake-project&user_id=fake-user', version='1.9',
global_request_id=self.context.global_id)
mock_get.assert_has_calls([call1, call2])
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.get')
def test_get_usages_counts_for_quota_retries(self, mock_get):
# Two attempts have a ConnectFailure and the third succeeds
fake_project_response = fake_requests.FakeResponse(
200, content=jsonutils.dumps(
{'usages': {orc.VCPU: 2,
orc.MEMORY_MB: 512}}))
mock_get.side_effect = [ks_exc.ConnectFailure,
ks_exc.ConnectFailure,
fake_project_response]
counts = self.client.get_usages_counts_for_quota(self.context,
'fake-project')
self.assertEqual(3, mock_get.call_count)
expected = {'project': {'cores': 2, 'ram': 512}}
self.assertDictEqual(expected, counts)
# Project query succeeds, first project + user query has a
# ConnectFailure, second project + user query succeeds
mock_get.reset_mock()
fake_user_response = fake_requests.FakeResponse(
200, content=jsonutils.dumps(
{'usages': {orc.VCPU: 1,
orc.MEMORY_MB: 256}}))
mock_get.side_effect = [fake_project_response,
ks_exc.ConnectFailure,
fake_user_response]
counts = self.client.get_usages_counts_for_quota(
self.context, 'fake-project', user_id='fake-user')
self.assertEqual(3, mock_get.call_count)
expected['user'] = {'cores': 1, 'ram': 256}
self.assertDictEqual(expected, counts)
# Three attempts in a row have a ConnectFailure
mock_get.reset_mock()
mock_get.side_effect = [ks_exc.ConnectFailure] * 4
self.assertRaises(ks_exc.ConnectFailure,
self.client.get_usages_counts_for_quota,
self.context, 'fake-project')
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.get')
def test_get_usages_counts_default_zero(self, mock_get):
# A project and user are not yet consuming any resources.
fake_response = fake_requests.FakeResponse(
200, content=jsonutils.dumps({'usages': {}}))
mock_get.side_effect = [fake_response, fake_response]
counts = self.client.get_usages_counts_for_quota(
self.context, 'fake-project', user_id='fake-user')
self.assertEqual(2, mock_get.call_count)
expected = {'project': {'cores': 0, 'ram': 0},
'user': {'cores': 0, 'ram': 0}}
self.assertDictEqual(expected, counts)