From acf48c768a8fc4072799474acc92b80fbba2cfa7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 21 Nov 2024 16:57:09 +0000 Subject: [PATCH] api: Add response body schemas for simple tenant usage APIs Change-Id: I839aba62047777bf83eb3a52c6d633f8eb7348bc Signed-off-by: Stephen Finucane --- .../compute/schemas/simple_tenant_usage.py | 127 ++++++++++++++++++ .../openstack/compute/simple_tenant_usage.py | 13 +- .../compute/test_simple_tenant_usage.py | 69 +++++----- 3 files changed, 174 insertions(+), 35 deletions(-) diff --git a/nova/api/openstack/compute/schemas/simple_tenant_usage.py b/nova/api/openstack/compute/schemas/simple_tenant_usage.py index 20ef7961e863..eb6d847d7158 100644 --- a/nova/api/openstack/compute/schemas/simple_tenant_usage.py +++ b/nova/api/openstack/compute/schemas/simple_tenant_usage.py @@ -11,9 +11,12 @@ # 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 from nova.api.validation import parameter_types +from nova.api.validation import response_types +from nova.compute import vm_states index_query = { @@ -56,3 +59,127 @@ index_query_v275['additionalProperties'] = False show_query_v275 = copy.deepcopy(show_query_v240) show_query_v275['additionalProperties'] = False + +_server_usage_response = { + 'type': 'object', + 'properties': { + 'ended_at': {'type': ['string', 'null'], 'format': 'date-time'}, + 'flavor': {'type': 'string'}, + 'hours': {'type': 'number'}, + 'instance_id': {'type': 'string', 'format': 'uuid'}, + 'local_gb': {'type': 'integer', 'minimum': 0}, + 'memory_mb': {'type': 'integer', 'minimum': 1}, + 'name': {'type': 'string'}, + 'started_at': {'type': 'string', 'format': 'date-time'}, + 'state': { + 'type': 'string', + 'enum': [ + vm_states.ACTIVE, + vm_states.BUILDING, + # vm_states.DELETED is ignored in favour of 'terminated' + vm_states.ERROR, + vm_states.PAUSED, + vm_states.RESCUED, + vm_states.RESIZED, + vm_states.SHELVED, + vm_states.SHELVED_OFFLOADED, + # vm_states.SOFT_DELETED is ignored in favour of 'terminated' + vm_states.STOPPED, + vm_states.SUSPENDED, + 'terminated', + ], + }, + 'tenant_id': parameter_types.project_id, + 'uptime': {'type': 'integer', 'minimum': 0}, + 'vcpus': {'type': 'integer', 'minimum': 1}, + }, + 'required': [ + # local_gb, memory_mb and vcpus can be omitted if the instance is not + # found + 'ended_at', + 'flavor', + 'hours', + 'instance_id', + 'name', + 'state', + 'started_at', + 'tenant_id', + 'uptime', + ], + 'additionalProperties': False, +} + +_usage_response = { + 'type': 'object', + 'properties': { + 'server_usages': { + 'type': 'array', + 'items': _server_usage_response, + }, + 'start': {'type': 'string', 'format': 'date-time'}, + 'stop': {'type': 'string', 'format': 'date-time'}, + 'tenant_id': parameter_types.project_id, + # these are number instead of integer since the underlying values are + # floats after multiplication by hours (a float) + 'total_hours': {'type': 'number', 'minimum': 0}, + 'total_local_gb_usage': {'type': 'number', 'minimum': 0}, + 'total_memory_mb_usage': {'type': 'number', 'minimum': 0}, + 'total_vcpus_usage': {'type': 'number', 'minimum': 0}, + }, + 'required': [ + 'start', + 'stop', + 'tenant_id', + 'total_hours', + 'total_local_gb_usage', + 'total_memory_mb_usage', + 'total_vcpus_usage', + ], + 'additionalProperties': False, +} + +index_response = { + 'type': 'object', + 'properties': { + 'tenant_usages': { + 'type': 'array', + 'items': _usage_response, + }, + }, + 'required': ['tenant_usages'], + 'additionalProperties': False, +} + +index_response_v240 = copy.deepcopy(index_response) +index_response_v240['properties']['tenant_usages_links'] = ( + response_types.collection_links +) + +show_response = { + 'type': 'object', + # if there are no usages for the tenant, we return an empty object rather + # than an object with all zero values, thus, oneOf + 'properties': { + 'tenant_usage': { + 'oneOf': [ + copy.deepcopy(_usage_response), + { + 'type': 'object', + 'properties': {}, + 'required': [], + 'additionalProperties': False, + }, + ], + }, + }, + 'required': ['tenant_usage'], + 'additionalProperties': False, +} +show_response['properties']['tenant_usage']['oneOf'][0]['required'].append( + 'server_usages' +) + +show_response_v240 = copy.deepcopy(show_response) +show_response_v240['properties']['tenant_usage_links'] = ( + response_types.collection_links +) diff --git a/nova/api/openstack/compute/simple_tenant_usage.py b/nova/api/openstack/compute/simple_tenant_usage.py index 35b00bc62d64..f618208d851f 100644 --- a/nova/api/openstack/compute/simple_tenant_usage.py +++ b/nova/api/openstack/compute/simple_tenant_usage.py @@ -44,6 +44,7 @@ def parse_strtime(dstr, fmt): raise exception.InvalidStrTime(reason=str(e)) +@validation.validated class SimpleTenantUsageController(wsgi.Controller): _view_builder_class = usages_view.ViewBuilder @@ -247,7 +248,7 @@ class SimpleTenantUsageController(wsgi.Controller): value = value.replace(tzinfo=iso8601.UTC) return value - def _get_datetime_range(self, req): + def _parse_qs_params(self, req): qs = req.environ.get('QUERY_STRING', '') env = urlparse.parse_qs(qs) # NOTE(lzyeval): env.get() always returns a list @@ -265,6 +266,8 @@ class SimpleTenantUsageController(wsgi.Controller): @validation.query_schema(schema.index_query, '2.1', '2.39') @validation.query_schema(schema.index_query_v240, '2.40', '2.74') @validation.query_schema(schema.index_query_v275, '2.75') + @validation.response_body_schema(schema.index_response, '2.1', '2.39') + @validation.response_body_schema(schema.index_response_v240, '2.40') @wsgi.expected_errors(400) def index(self, req): """Retrieve tenant_usage for all tenants.""" @@ -280,8 +283,7 @@ class SimpleTenantUsageController(wsgi.Controller): context.can(stu_policies.POLICY_ROOT % 'list') try: - (period_start, period_stop, detailed) = self._get_datetime_range( - req) + period_start, period_stop, detailed = self._parse_qs_params(req) except exception.InvalidStrTime as e: raise exc.HTTPBadRequest(explanation=e.format_message()) @@ -313,6 +315,8 @@ class SimpleTenantUsageController(wsgi.Controller): @validation.query_schema(schema.show_query, '2.1', '2.39') @validation.query_schema(schema.show_query_v240, '2.40', '2.74') @validation.query_schema(schema.show_query_v275, '2.75') + @validation.response_body_schema(schema.show_response, '2.1', '2.39') + @validation.response_body_schema(schema.show_response_v240, '2.40') @wsgi.expected_errors(400) def show(self, req, id): """Retrieve tenant_usage for a specified tenant.""" @@ -330,8 +334,7 @@ class SimpleTenantUsageController(wsgi.Controller): {'project_id': tenant_id}) try: - (period_start, period_stop, ignore) = self._get_datetime_range( - req) + period_start, period_stop, ignore = self._parse_qs_params(req) except exception.InvalidStrTime as e: raise exc.HTTPBadRequest(explanation=e.format_message()) diff --git a/nova/tests/unit/api/openstack/compute/test_simple_tenant_usage.py b/nova/tests/unit/api/openstack/compute/test_simple_tenant_usage.py index a7dcfae5580d..9c245f2c992c 100644 --- a/nova/tests/unit/api/openstack/compute/test_simple_tenant_usage.py +++ b/nova/tests/unit/api/openstack/compute/test_simple_tenant_usage.py @@ -115,16 +115,19 @@ def fake_get_active_deleted_flavorless(cls, context, begin, end=None, limit=None, marker=None): # First get some normal instances to have actual usage instances = [ - _fake_instance(START, STOP, x, - project_id or 'faketenant_%s' % (x // SERVERS)) - for x in range(TENANTS * SERVERS)] + _fake_instance( + START, STOP, x, + project_id or getattr(uuids, 'faketenant_%s' % (x // SERVERS)) + ) for x in range(TENANTS * SERVERS) + ] # Then get some deleted instances with no flavor to test bugs 1643444 and # 1692893 (duplicates) instances.extend([ _fake_instance_deleted_flavorless( context, START, STOP, x, - project_id or 'faketenant_%s' % (x // SERVERS)) - for x in range(TENANTS * SERVERS)]) + project_id or getattr(uuids, 'faketenant_%s' % (x // SERVERS)) + ) for x in range(TENANTS * SERVERS) + ]) return objects.InstanceList(objects=instances) @@ -134,9 +137,10 @@ def fake_get_active_by_window_joined(cls, context, begin, end=None, expected_attrs=None, use_slave=False, limit=None, marker=None): return objects.InstanceList(objects=[ - _fake_instance(START, STOP, x, - project_id or 'faketenant_%s' % (x // SERVERS)) - for x in range(TENANTS * SERVERS)]) + _fake_instance( + START, STOP, x, + project_id or getattr(uuids, 'faketenant_%s' % (x // SERVERS)) + ) for x in range(TENANTS * SERVERS)]) class SimpleTenantUsageTestV21(test.TestCase): @@ -146,15 +150,18 @@ class SimpleTenantUsageTestV21(test.TestCase): def setUp(self): super(SimpleTenantUsageTestV21, self).setUp() - self.admin_context = context.RequestContext('fakeadmin_0', - 'faketenant_0', - is_admin=True) - self.user_context = context.RequestContext('fakeadmin_0', - 'faketenant_0', - is_admin=False) - self.alt_user_context = context.RequestContext('fakeadmin_0', - 'faketenant_1', - is_admin=False) + self.admin_context = context.RequestContext( + uuids.fakeadmin_0, + uuids.faketenant_0, + is_admin=True) + self.user_context = context.RequestContext( + uuids.fakeadmin_0, + uuids.faketenant_0, + is_admin=False) + self.alt_user_context = context.RequestContext( + uuids.fakeadmin_0, + uuids.faketenant_1, + is_admin=False) self.num_cells = len(objects.CellMappingList.get_all( self.admin_context)) @@ -276,7 +283,7 @@ class SimpleTenantUsageTestV21(test.TestCase): @mock.patch('nova.objects.InstanceList.get_active_by_window_joined', fake_get_active_by_window_joined) def _test_verify_show(self, start, stop, limit=None): - tenant_id = 1 + tenant_id = uuids.tenant_id url = '?start=%s&end=%s' if limit: url += '&limit=%s' % (limit) @@ -319,22 +326,24 @@ class SimpleTenantUsageTestV21(test.TestCase): (future.isoformat(), NOW.isoformat()), version=self.version) req.environ['nova.context'] = self.user_context - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.show, req, 'faketenant_0') + self.assertRaises( + webob.exc.HTTPBadRequest, + self.controller.show, req, uuids.faketenant_0) def test_get_tenants_usage_with_invalid_start_date(self): req = fakes.HTTPRequest.blank('?start=%s&end=%s' % ("xxxx", NOW.isoformat()), version=self.version) req.environ['nova.context'] = self.user_context - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.show, req, 'faketenant_0') + self.assertRaises( + webob.exc.HTTPBadRequest, + self.controller.show, req, uuids.faketenant_0) def _test_get_tenants_usage_with_one_date(self, date_url_param): req = fakes.HTTPRequest.blank('?%s' % date_url_param, version=self.version) req.environ['nova.context'] = self.user_context - res = self.controller.show(req, 'faketenant_0') + res = self.controller.show(req, uuids.faketenant_0) self.assertIn('tenant_usage', res) def test_get_tenants_usage_with_no_start_date(self): @@ -382,7 +391,7 @@ class SimpleTenantUsageTestV21(test.TestCase): (START.isoformat(), param, value, param, value), version=self.version) - res = self.controller.show(req, 1) + res = self.controller.show(req, uuids.tenant_id) self.assertIn('tenant_usage', res) def test_show_duplicate_query_parameters_validation(self): @@ -453,15 +462,16 @@ class SimpleTenantUsageTestV2_75(SimpleTenantUsageTestV40): req = fakes.HTTPRequest.blank('?start=%s&end=%s&additional=1' % (START.isoformat(), STOP.isoformat()), version='2.74') - res = self.controller.show(req, 1) + res = self.controller.show(req, uuids.tenant_id) self.assertIn('tenant_usage', res) def test_show_additional_query_parameters(self): req = fakes.HTTPRequest.blank('?start=%s&end=%s&additional=1' % (START.isoformat(), STOP.isoformat()), version=self.version) - self.assertRaises(exception.ValidationError, self.controller.show, - req, 1) + self.assertRaises( + exception.ValidationError, self.controller.show, + req, uuids.tenant_id) class SimpleTenantUsageLimitsTestV21(test.TestCase): @@ -470,7 +480,6 @@ class SimpleTenantUsageLimitsTestV21(test.TestCase): def setUp(self): super(SimpleTenantUsageLimitsTestV21, self).setUp() self.controller = simple_tenant_usage_v21.SimpleTenantUsageController() - self.tenant_id = 1 def _get_request(self, url): url = url % (START.isoformat(), STOP.isoformat()) @@ -484,7 +493,7 @@ class SimpleTenantUsageLimitsTestV21(test.TestCase): @mock.patch('nova.objects.InstanceList.get_active_by_window_joined') def test_limit_defaults_to_conf_max_limit_show(self, mock_get): req = self._get_request('?start=%s&end=%s') - self.controller.show(req, self.tenant_id) + self.controller.show(req, uuids.tenant_id) self.assert_limit(mock_get, CONF.api.max_limit) @mock.patch('nova.objects.InstanceList.get_active_by_window_joined') @@ -506,7 +515,7 @@ class SimpleTenantUsageLimitsTestV240(SimpleTenantUsageLimitsTestV21): @mock.patch('nova.objects.InstanceList.get_active_by_window_joined') def test_limit_and_marker_show(self, mock_get): req = self._get_request('?start=%s&end=%s&limit=3&marker=some-marker') - self.controller.show(req, self.tenant_id) + self.controller.show(req, uuids.tenant_id) self.assert_limit_and_marker(mock_get, 3, 'some-marker') @mock.patch('nova.objects.InstanceList.get_active_by_window_joined')