diff --git a/nova/exception.py b/nova/exception.py index 65598516e131..a8e055ddd44a 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -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.") diff --git a/nova/scheduler/client/report.py b/nova/scheduler/client/report.py index 609b3718952a..09e0279935e9 100644 --- a/nova/scheduler/client/report.py +++ b/nova/scheduler/client/report.py @@ -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': , + 'ram': }, + {'user': {'cores': , + 'ram': }, + :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 diff --git a/nova/tests/unit/scheduler/client/test_report.py b/nova/tests/unit/scheduler/client/test_report.py index 23bc1307ed48..ffd408218f99 100644 --- a/nova/tests/unit/scheduler/client/test_report.py +++ b/nova/tests/unit/scheduler/client/test_report.py @@ -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)