Merge "api: Add response body schemas for simple tenant usage APIs"
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -43,6 +43,7 @@ def parse_strtime(dstr, fmt):
|
||||
raise exception.InvalidStrTime(reason=str(e))
|
||||
|
||||
|
||||
@validation.validated
|
||||
class SimpleTenantUsageController(wsgi.Controller):
|
||||
|
||||
_view_builder_class = usages_view.ViewBuilder
|
||||
@@ -246,7 +247,7 @@ class SimpleTenantUsageController(wsgi.Controller):
|
||||
value = value.replace(tzinfo=datetime.timezone.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
|
||||
@@ -264,6 +265,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."""
|
||||
@@ -279,8 +282,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())
|
||||
|
||||
@@ -312,6 +314,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."""
|
||||
@@ -329,8 +333,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())
|
||||
|
||||
|
||||
@@ -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,14 +150,17 @@ class SimpleTenantUsageTestV21(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(SimpleTenantUsageTestV21, self).setUp()
|
||||
self.admin_context = context.RequestContext('fakeadmin_0',
|
||||
'faketenant_0',
|
||||
self.admin_context = context.RequestContext(
|
||||
uuids.fakeadmin_0,
|
||||
uuids.faketenant_0,
|
||||
is_admin=True)
|
||||
self.user_context = context.RequestContext('fakeadmin_0',
|
||||
'faketenant_0',
|
||||
self.user_context = context.RequestContext(
|
||||
uuids.fakeadmin_0,
|
||||
uuids.faketenant_0,
|
||||
is_admin=False)
|
||||
self.alt_user_context = context.RequestContext('fakeadmin_0',
|
||||
'faketenant_1',
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user