From 65ef524d5546ee499f085f0780fd502bd980a9ac Mon Sep 17 00:00:00 2001 From: He Jie Xu Date: Thu, 21 Aug 2014 17:36:55 +0800 Subject: [PATCH] Port simple_tenant_usage into v2.1 The simple_tenant_usage extension is removed from v3 API. For now, we need porting it back. And also try to share unittests between v2 and v2.1. Partially implements blueprint v2-on-v3-api Change-Id: Ie0c36fcd37fb188c1f1b57f04733bfa267b65201 --- .../server-post-req.json | 16 + .../server-post-resp.json | 16 + .../simple-tenant-usage-get-specific.json | 27 ++ .../simple-tenant-usage-get.json | 13 + etc/nova/policy.json | 3 + .../compute/plugins/v3/simple_tenant_usage.py | 291 ++++++++++++++++++ .../contrib/test_simple_tenant_usage.py | 112 ++++--- nova/tests/fake_policy.py | 2 + .../server-post-req.json.tpl | 16 + .../server-post-resp.json.tpl | 16 + .../simple-tenant-usage-get-specific.json.tpl | 27 ++ .../simple-tenant-usage-get.json.tpl | 13 + .../integrated/v3/test_simple_tenant_usage.py | 60 ++++ setup.cfg | 1 + 14 files changed, 565 insertions(+), 48 deletions(-) create mode 100644 doc/v3/api_samples/os-simple-tenant-usage/server-post-req.json create mode 100644 doc/v3/api_samples/os-simple-tenant-usage/server-post-resp.json create mode 100644 doc/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get-specific.json create mode 100644 doc/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get.json create mode 100644 nova/api/openstack/compute/plugins/v3/simple_tenant_usage.py create mode 100644 nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/server-post-req.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/server-post-resp.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get-specific.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get.json.tpl create mode 100644 nova/tests/integrated/v3/test_simple_tenant_usage.py diff --git a/doc/v3/api_samples/os-simple-tenant-usage/server-post-req.json b/doc/v3/api_samples/os-simple-tenant-usage/server-post-req.json new file mode 100644 index 0000000000..d88eb41222 --- /dev/null +++ b/doc/v3/api_samples/os-simple-tenant-usage/server-post-req.json @@ -0,0 +1,16 @@ +{ + "server" : { + "name" : "new-server-test", + "imageRef" : "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "flavorRef" : "http://openstack.example.com/openstack/flavors/1", + "metadata" : { + "My Server Name" : "Apache1" + }, + "personality" : [ + { + "path" : "/etc/banner.txt", + "contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" + } + ] + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/os-simple-tenant-usage/server-post-resp.json b/doc/v3/api_samples/os-simple-tenant-usage/server-post-resp.json new file mode 100644 index 0000000000..497cbcbc53 --- /dev/null +++ b/doc/v3/api_samples/os-simple-tenant-usage/server-post-resp.json @@ -0,0 +1,16 @@ +{ + "server": { + "adminPass": "tpKL9n5BHKFv", + "id": "3edb83e6-2c90-41c1-bf80-0b61472b4c19", + "links": [ + { + "href": "http://openstack.example.com/v3/servers/3edb83e6-2c90-41c1-bf80-0b61472b4c19", + "rel": "self" + }, + { + "href": "http://openstack.example.com/servers/3edb83e6-2c90-41c1-bf80-0b61472b4c19", + "rel": "bookmark" + } + ] + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get-specific.json b/doc/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get-specific.json new file mode 100644 index 0000000000..6ab0e1fd5f --- /dev/null +++ b/doc/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get-specific.json @@ -0,0 +1,27 @@ +{ + "tenant_usage": { + "server_usages": [ + { + "ended_at": null, + "flavor": "m1.tiny", + "hours": 1.0, + "instance_id": "1f1deceb-17b5-4c04-84c7-e0d4499c8fe0", + "local_gb": 1, + "memory_mb": 512, + "name": "new-server-test", + "started_at": "2012-10-08T20:10:44.541277", + "state": "active", + "tenant_id": "openstack", + "uptime": 3600, + "vcpus": 1 + } + ], + "start": "2012-10-08T20:10:44.587336", + "stop": "2012-10-08T21:10:44.587336", + "tenant_id": "openstack", + "total_hours": 1.0, + "total_local_gb_usage": 1.0, + "total_memory_mb_usage": 512.0, + "total_vcpus_usage": 1.0 + } +} diff --git a/doc/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get.json b/doc/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get.json new file mode 100644 index 0000000000..ba45c4b5cf --- /dev/null +++ b/doc/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get.json @@ -0,0 +1,13 @@ +{ + "tenant_usages": [ + { + "start": "2012-10-08T21:10:44.587336", + "stop": "2012-10-08T22:10:44.587336", + "tenant_id": "openstack", + "total_hours": 1.0, + "total_local_gb_usage": 1.0, + "total_memory_mb_usage": 512.0, + "total_vcpus_usage": 1.0 + } + ] +} diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 89544a8009..0cd5f27143 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -239,6 +239,9 @@ "compute_extension:v3:os-shelve:shelve:discoverable": "", "compute_extension:v3:os-shelve:shelve_offload": "rule:admin_api", "compute_extension:simple_tenant_usage:show": "rule:admin_or_owner", + "compute_extension::v3:os-simple-tenant-usage:discoverable": "", + "compute_extension::v3:os-simple-tenant-usage:show": "rule:admin_or_owner", + "compute_extension::v3:os-simple-tenant-usage:list": "rule:admin_api", "compute_extension:v3:os-suspend-server:discoverable": "", "compute_extension:v3:os-suspend-server:suspend": "rule:admin_or_owner", "compute_extension:v3:os-suspend-server:resume": "rule:admin_or_owner", diff --git a/nova/api/openstack/compute/plugins/v3/simple_tenant_usage.py b/nova/api/openstack/compute/plugins/v3/simple_tenant_usage.py new file mode 100644 index 0000000000..97edb79205 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/simple_tenant_usage.py @@ -0,0 +1,291 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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 datetime + +import iso8601 +import six.moves.urllib.parse as urlparse +from webob import exc + +from nova.api.openstack import extensions +from nova import exception +from nova.i18n import _ +from nova import objects +from nova.objects import instance as instance_obj +from nova.openstack.common import timeutils + +ALIAS = "os-simple-tenant-usage" +authorize_show = extensions.extension_authorizer('compute', + 'v3:%s:show' % ALIAS) +authorize_list = extensions.extension_authorizer('compute', + 'v3:%s:list' % ALIAS) + + +def parse_strtime(dstr, fmt): + try: + return timeutils.parse_strtime(dstr, fmt) + except (TypeError, ValueError) as e: + raise exception.InvalidStrTime(reason=unicode(e)) + + +class SimpleTenantUsageController(object): + def _hours_for(self, instance, period_start, period_stop): + launched_at = instance.launched_at + terminated_at = instance.terminated_at + if terminated_at is not None: + if not isinstance(terminated_at, datetime.datetime): + # NOTE(mriedem): Instance object DateTime fields are + # timezone-aware so convert using isotime. + terminated_at = timeutils.parse_isotime(terminated_at) + + if launched_at is not None: + if not isinstance(launched_at, datetime.datetime): + launched_at = timeutils.parse_isotime(launched_at) + + if terminated_at and terminated_at < period_start: + return 0 + # nothing if it started after the usage report ended + if launched_at and launched_at > period_stop: + return 0 + if launched_at: + # if instance launched after period_started, don't charge for first + start = max(launched_at, period_start) + if terminated_at: + # if instance stopped before period_stop, don't charge after + stop = min(period_stop, terminated_at) + else: + # instance is still running, so charge them up to current time + stop = period_stop + dt = stop - start + seconds = (dt.days * 3600 * 24 + dt.seconds + + dt.microseconds / 100000.0) + + return seconds / 3600.0 + else: + # instance hasn't launched, so no charge + return 0 + + def _get_flavor(self, context, instance, flavors_cache): + """Get flavor information from the instance's system_metadata, + allowing a fallback to lookup by-id for deleted instances only. + """ + try: + return instance.get_flavor() + except KeyError: + if not instance.deleted: + # Only support the fallback mechanism for deleted instances + # that would have been skipped by migration #153 + raise + + flavor_type = instance.instance_type_id + if flavor_type in flavors_cache: + return flavors_cache[flavor_type] + + try: + flavor_ref = objects.Flavor.get_by_id(context, flavor_type) + flavors_cache[flavor_type] = flavor_ref + except exception.FlavorNotFound: + # can't bill if there is no flavor + flavor_ref = None + + return flavor_ref + + def _tenant_usages_for_period(self, context, period_start, + period_stop, tenant_id=None, detailed=True): + + instances = objects.InstanceList.get_active_by_window_joined( + context, period_start, period_stop, tenant_id, + expected_attrs=instance_obj.INSTANCE_DEFAULT_FIELDS) + rval = {} + flavors = {} + + for instance in instances: + info = {} + info['hours'] = self._hours_for(instance, + period_start, + period_stop) + flavor = self._get_flavor(context, instance, flavors) + if not flavor: + info['flavor'] = '' + else: + info['flavor'] = flavor.name + + info['instance_id'] = instance.uuid + info['name'] = instance.display_name + + info['memory_mb'] = instance.memory_mb + info['local_gb'] = instance.root_gb + instance.ephemeral_gb + info['vcpus'] = instance.vcpus + + info['tenant_id'] = instance.project_id + + # NOTE(mriedem): We need to normalize the start/end times back + # to timezone-naive so the response doesn't change after the + # conversion to objects. + info['started_at'] = timeutils.normalize_time(instance.launched_at) + + info['ended_at'] = ( + timeutils.normalize_time(instance.terminated_at) if + instance.terminated_at else None) + + if info['ended_at']: + info['state'] = 'terminated' + else: + info['state'] = instance.vm_state + + now = timeutils.utcnow() + + if info['state'] == 'terminated': + delta = info['ended_at'] - info['started_at'] + else: + delta = now - info['started_at'] + + info['uptime'] = delta.days * 24 * 3600 + delta.seconds + + if info['tenant_id'] not in rval: + summary = {} + summary['tenant_id'] = info['tenant_id'] + if detailed: + summary['server_usages'] = [] + summary['total_local_gb_usage'] = 0 + summary['total_vcpus_usage'] = 0 + summary['total_memory_mb_usage'] = 0 + summary['total_hours'] = 0 + summary['start'] = timeutils.normalize_time(period_start) + summary['stop'] = timeutils.normalize_time(period_stop) + rval[info['tenant_id']] = summary + + summary = rval[info['tenant_id']] + summary['total_local_gb_usage'] += info['local_gb'] * info['hours'] + summary['total_vcpus_usage'] += info['vcpus'] * info['hours'] + summary['total_memory_mb_usage'] += (info['memory_mb'] * + info['hours']) + + summary['total_hours'] += info['hours'] + if detailed: + summary['server_usages'].append(info) + + return rval.values() + + def _parse_datetime(self, dtstr): + if not dtstr: + value = timeutils.utcnow() + elif isinstance(dtstr, datetime.datetime): + value = dtstr + else: + for fmt in ["%Y-%m-%dT%H:%M:%S", + "%Y-%m-%dT%H:%M:%S.%f", + "%Y-%m-%d %H:%M:%S.%f"]: + try: + value = parse_strtime(dtstr, fmt) + break + except exception.InvalidStrTime: + pass + else: + msg = _("Datetime is in invalid format") + raise exception.InvalidStrTime(reason=msg) + + # NOTE(mriedem): Instance object DateTime fields are timezone-aware + # so we have to force UTC timezone for comparing this datetime against + # instance object fields and still maintain backwards compatibility + # in the API. + if value.utcoffset() is None: + value = value.replace(tzinfo=iso8601.iso8601.Utc()) + return value + + def _get_datetime_range(self, req): + qs = req.environ.get('QUERY_STRING', '') + env = urlparse.parse_qs(qs) + # NOTE(lzyeval): env.get() always returns a list + period_start = self._parse_datetime(env.get('start', [None])[0]) + period_stop = self._parse_datetime(env.get('end', [None])[0]) + + if not period_start < period_stop: + msg = _("Invalid start time. The start time cannot occur after " + "the end time.") + raise exc.HTTPBadRequest(explanation=msg) + + detailed = env.get('detailed', ['0'])[0] == '1' + return (period_start, period_stop, detailed) + + @extensions.expected_errors(400) + def index(self, req): + """Retrieve tenant_usage for all tenants.""" + context = req.environ['nova.context'] + + authorize_list(context) + + try: + (period_start, period_stop, detailed) = self._get_datetime_range( + req) + except exception.InvalidStrTime as e: + raise exc.HTTPBadRequest(explanation=e.format_message()) + + now = timeutils.parse_isotime(timeutils.strtime()) + if period_stop > now: + period_stop = now + usages = self._tenant_usages_for_period(context, + period_start, + period_stop, + detailed=detailed) + return {'tenant_usages': usages} + + @extensions.expected_errors(400) + def show(self, req, id): + """Retrieve tenant_usage for a specified tenant.""" + tenant_id = id + context = req.environ['nova.context'] + + authorize_show(context, {'project_id': tenant_id}) + + try: + (period_start, period_stop, ignore) = self._get_datetime_range( + req) + except exception.InvalidStrTime as e: + raise exc.HTTPBadRequest(explanation=e.format_message()) + + now = timeutils.parse_isotime(timeutils.strtime()) + if period_stop > now: + period_stop = now + usage = self._tenant_usages_for_period(context, + period_start, + period_stop, + tenant_id=tenant_id, + detailed=True) + if len(usage): + usage = usage[0] + else: + usage = {} + return {'tenant_usage': usage} + + +class SimpleTenantUsage(extensions.V3APIExtensionBase): + """Simple tenant usage extension.""" + + name = "SimpleTenantUsage" + alias = ALIAS + version = 1 + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension(ALIAS, + SimpleTenantUsageController()) + resources.append(res) + + return resources + + def get_controller_extensions(self): + return [] diff --git a/nova/tests/api/openstack/compute/contrib/test_simple_tenant_usage.py b/nova/tests/api/openstack/compute/contrib/test_simple_tenant_usage.py index 4a94be2241..86d78e994d 100644 --- a/nova/tests/api/openstack/compute/contrib/test_simple_tenant_usage.py +++ b/nova/tests/api/openstack/compute/contrib/test_simple_tenant_usage.py @@ -19,7 +19,10 @@ from lxml import etree import mock import webob -from nova.api.openstack.compute.contrib import simple_tenant_usage +from nova.api.openstack.compute.contrib import simple_tenant_usage as \ + simple_tenant_usage_v2 +from nova.api.openstack.compute.plugins.v3 import simple_tenant_usage as \ + simple_tenant_usage_v21 from nova.compute import flavors from nova.compute import vm_states from nova import context @@ -102,9 +105,13 @@ def fake_instance_get_active_by_window_joined(context, begin, end, @mock.patch.object(db, 'instance_get_active_by_window_joined', fake_instance_get_active_by_window_joined) -class SimpleTenantUsageTest(test.TestCase): +class SimpleTenantUsageTestV21(test.TestCase): + url = '/v3/os-simple-tenant-usage' + alt_url = '/v3/os-simple-tenant-usage' + policy_rule_prefix = "compute_extension:v3:os-simple-tenant-usage" + def setUp(self): - super(SimpleTenantUsageTest, self).setUp() + super(SimpleTenantUsageTestV21, self).setUp() self.admin_context = context.RequestContext('fakeadmin_0', 'faketenant_0', is_admin=True) @@ -114,21 +121,20 @@ class SimpleTenantUsageTest(test.TestCase): self.alt_user_context = context.RequestContext('fakeadmin_0', 'faketenant_1', is_admin=False) - self.flags( - osapi_compute_extension=[ - 'nova.api.openstack.compute.contrib.select_extensions'], - osapi_compute_ext_list=['Simple_tenant_usage']) + + def _get_wsgi_app(self, context): + return fakes.wsgi_app_v3(fake_auth_context=context, + init_only=('servers', + 'os-simple-tenant-usage')) def _test_verify_index(self, start, stop): req = webob.Request.blank( - '/v2/faketenant_0/os-simple-tenant-usage?start=%s&end=%s' % + self.url + '?start=%s&end=%s' % (start.isoformat(), stop.isoformat())) req.method = "GET" req.headers["content-type"] = "application/json" - res = req.get_response(fakes.wsgi_app( - fake_auth_context=self.admin_context, - init_only=('os-simple-tenant-usage',))) + res = req.get_response(self._get_wsgi_app(self.admin_context)) self.assertEqual(res.status_int, 200) res_dict = jsonutils.loads(res.body) @@ -160,15 +166,12 @@ class SimpleTenantUsageTest(test.TestCase): def _get_tenant_usages(self, detailed=''): req = webob.Request.blank( - '/v2/faketenant_0/os-simple-tenant-usage?' - 'detailed=%s&start=%s&end=%s' % + self.url + '?detailed=%s&start=%s&end=%s' % (detailed, START.isoformat(), STOP.isoformat())) req.method = "GET" req.headers["content-type"] = "application/json" - res = req.get_response(fakes.wsgi_app( - fake_auth_context=self.admin_context, - init_only=('os-simple-tenant-usage',))) + res = req.get_response(self._get_wsgi_app(self.admin_context)) self.assertEqual(res.status_int, 200) res_dict = jsonutils.loads(res.body) return res_dict['tenant_usages'] @@ -194,15 +197,12 @@ class SimpleTenantUsageTest(test.TestCase): def _test_verify_show(self, start, stop): tenant_id = 0 req = webob.Request.blank( - '/v2/faketenant_0/os-simple-tenant-usage/' - 'faketenant_%s?start=%s&end=%s' % + self.url + '/faketenant_%s?start=%s&end=%s' % (tenant_id, start.isoformat(), stop.isoformat())) req.method = "GET" req.headers["content-type"] = "application/json" - res = req.get_response(fakes.wsgi_app( - fake_auth_context=self.user_context, - init_only=('os-simple-tenant-usage',))) + res = req.get_response(self._get_wsgi_app(self.user_context)) self.assertEqual(res.status_int, 200) res_dict = jsonutils.loads(res.body) @@ -220,14 +220,13 @@ class SimpleTenantUsageTest(test.TestCase): def test_verify_show_cant_view_other_tenant(self): req = webob.Request.blank( - '/v2/faketenant_1/os-simple-tenant-usage/' - 'faketenant_0?start=%s&end=%s' % + self.alt_url + '/faketenant_0?start=%s&end=%s' % (START.isoformat(), STOP.isoformat())) req.method = "GET" req.headers["content-type"] = "application/json" rules = { - "compute_extension:simple_tenant_usage:show": + self.policy_rule_prefix + ":show": common_policy.parse_rule([ ["role:admin"], ["project_id:%(project_id)s"] ]) @@ -235,9 +234,7 @@ class SimpleTenantUsageTest(test.TestCase): policy.set_rules(rules) try: - res = req.get_response(fakes.wsgi_app( - fake_auth_context=self.alt_user_context, - init_only=('os-simple-tenant-usage',))) + res = req.get_response(self._get_wsgi_app(self.alt_user_context)) self.assertEqual(res.status_int, 403) finally: policy.reset() @@ -246,40 +243,34 @@ class SimpleTenantUsageTest(test.TestCase): future = NOW + datetime.timedelta(hours=HOURS) tenant_id = 0 req = webob.Request.blank( - '/v2/faketenant_0/os-simple-tenant-usage/' + self.url + '/' 'faketenant_%s?start=%s&end=%s' % (tenant_id, future.isoformat(), NOW.isoformat())) req.method = "GET" req.headers["content-type"] = "application/json" - res = req.get_response(fakes.wsgi_app( - fake_auth_context=self.user_context, - init_only=('os-simple-tenant-usage',))) + res = req.get_response(self._get_wsgi_app(self.user_context)) self.assertEqual(res.status_int, 400) def test_get_tenants_usage_with_invalid_start_date(self): tenant_id = 0 req = webob.Request.blank( - '/v2/faketenant_0/os-simple-tenant-usage/' + self.url + '/' 'faketenant_%s?start=%s&end=%s' % (tenant_id, "xxxx", NOW.isoformat())) req.method = "GET" req.headers["content-type"] = "application/json" - res = req.get_response(fakes.wsgi_app( - fake_auth_context=self.user_context, - init_only=('os-simple-tenant-usage',))) + res = req.get_response(self._get_wsgi_app(self.user_context)) self.assertEqual(res.status_int, 400) def _test_get_tenants_usage_with_one_date(self, date_url_param): req = webob.Request.blank( - '/v2/faketenant_0/os-simple-tenant-usage/' + self.url + '/' 'faketenant_0?%s' % date_url_param) req.method = "GET" req.headers["content-type"] = "application/json" - res = req.get_response(fakes.wsgi_app( - fake_auth_context=self.user_context, - init_only=('os-simple-tenant-usage',))) + res = req.get_response(self._get_wsgi_app(self.user_context)) self.assertEqual(200, res.status_int) def test_get_tenants_usage_with_no_start_date(self): @@ -291,6 +282,20 @@ class SimpleTenantUsageTest(test.TestCase): 'start=%s' % (NOW - datetime.timedelta(5)).isoformat()) +class SimpleTenantUsageTestV2(SimpleTenantUsageTestV21): + url = '/v2/faketenant_0/os-simple-tenant-usage' + alt_url = '/v2/faketenant_1/os-simple-tenant-usage' + policy_rule_prefix = "compute_extension:simple_tenant_usage" + + def _get_wsgi_app(self, context): + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Simple_tenant_usage']) + return fakes.wsgi_app(fake_auth_context=context, + init_only=('os-simple-tenant-usage', )) + + class SimpleTenantUsageSerializerTest(test.TestCase): def _verify_server_usage(self, raw_usage, tree): self.assertEqual('server_usage', tree.tag) @@ -324,7 +329,7 @@ class SimpleTenantUsageSerializerTest(test.TestCase): self.assertEqual(len(not_seen), 0) def test_serializer_show(self): - serializer = simple_tenant_usage.SimpleTenantUsageTemplate() + serializer = simple_tenant_usage_v2.SimpleTenantUsageTemplate() today = timeutils.utcnow() yesterday = today - datetime.timedelta(days=1) raw_usage = dict( @@ -371,7 +376,7 @@ class SimpleTenantUsageSerializerTest(test.TestCase): self._verify_tenant_usage(raw_usage, tree) def test_serializer_index(self): - serializer = simple_tenant_usage.SimpleTenantUsagesTemplate() + serializer = simple_tenant_usage_v2.SimpleTenantUsagesTemplate() today = timeutils.utcnow() yesterday = today - datetime.timedelta(days=1) raw_usages = [dict( @@ -458,10 +463,11 @@ class SimpleTenantUsageSerializerTest(test.TestCase): self._verify_tenant_usage(raw_usages[idx], child) -class SimpleTenantUsageControllerTest(test.TestCase): +class SimpleTenantUsageControllerTestV21(test.TestCase): + controller = simple_tenant_usage_v21.SimpleTenantUsageController() + def setUp(self): - super(SimpleTenantUsageControllerTest, self).setUp() - self.controller = simple_tenant_usage.SimpleTenantUsageController() + super(SimpleTenantUsageControllerTestV21, self).setUp() self.context = context.RequestContext('fakeuser', 'fake-project') @@ -510,16 +516,26 @@ class SimpleTenantUsageControllerTest(test.TestCase): self.assertIsNone(flavor) -class SimpleTenantUsageUtils(test.NoDBTestCase): +class SimpleTenantUsageControllerTestV2(SimpleTenantUsageControllerTestV21): + controller = simple_tenant_usage_v2.SimpleTenantUsageController() + + +class SimpleTenantUsageUtilsV21(test.NoDBTestCase): + simple_tenant_usage = simple_tenant_usage_v21 + def test_valid_string(self): - dt = simple_tenant_usage.parse_strtime("2014-02-21T13:47:20.824060", - "%Y-%m-%dT%H:%M:%S.%f") + dt = self.simple_tenant_usage.parse_strtime( + "2014-02-21T13:47:20.824060", "%Y-%m-%dT%H:%M:%S.%f") self.assertEqual(datetime.datetime( microsecond=824060, second=20, minute=47, hour=13, day=21, month=2, year=2014), dt) def test_invalid_string(self): self.assertRaises(exception.InvalidStrTime, - simple_tenant_usage.parse_strtime, + self.simple_tenant_usage.parse_strtime, "2014-02-21 13:47:20.824060", "%Y-%m-%dT%H:%M:%S.%f") + + +class SimpleTenantUsageUtilsV2(SimpleTenantUsageUtilsV21): + simple_tenant_usage = simple_tenant_usage_v2 diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index d0fd1abc01..8eb8857267 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -281,6 +281,8 @@ policy_data = """ "compute_extension:v3:os-shelve:shelve_offload": "", "compute_extension:simple_tenant_usage:show": "", "compute_extension:simple_tenant_usage:list": "", + "compute_extension:v3:os-simple-tenant-usage:show": "", + "compute_extension:v3:os-simple-tenant-usage:list": "", "compute_extension:unshelve": "", "compute_extension:v3:os-shelve:unshelve": "", "compute_extension:v3:os-suspend-server:suspend": "", diff --git a/nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/server-post-req.json.tpl b/nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/server-post-req.json.tpl new file mode 100644 index 0000000000..d3916d1aa6 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/server-post-req.json.tpl @@ -0,0 +1,16 @@ +{ + "server" : { + "name" : "new-server-test", + "imageRef" : "%(host)s/openstack/images/%(image_id)s", + "flavorRef" : "%(host)s/openstack/flavors/1", + "metadata" : { + "My Server Name" : "Apache1" + }, + "personality" : [ + { + "path" : "/etc/banner.txt", + "contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" + } + ] + } +} diff --git a/nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/server-post-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/server-post-resp.json.tpl new file mode 100644 index 0000000000..adfaaa381e --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/server-post-resp.json.tpl @@ -0,0 +1,16 @@ +{ + "server": { + "adminPass": "%(password)s", + "id": "%(id)s", + "links": [ + { + "href": "%(host)s/v3/servers/%(uuid)s", + "rel": "self" + }, + { + "href": "%(host)s/servers/%(uuid)s", + "rel": "bookmark" + } + ] + } +} diff --git a/nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get-specific.json.tpl b/nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get-specific.json.tpl new file mode 100644 index 0000000000..f37083013d --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get-specific.json.tpl @@ -0,0 +1,27 @@ +{ + "tenant_usage": { + "server_usages": [ + { + "ended_at": null, + "flavor": "m1.tiny", + "hours": 1.0, + "instance_id": "%(uuid)s", + "local_gb": 1, + "memory_mb": 512, + "name": "new-server-test", + "started_at": "%(strtime)s", + "state": "active", + "tenant_id": "openstack", + "uptime": 3600, + "vcpus": 1 + } + ], + "start": "%(strtime)s", + "stop": "%(strtime)s", + "tenant_id": "openstack", + "total_hours": 1.0, + "total_local_gb_usage": 1.0, + "total_memory_mb_usage": 512.0, + "total_vcpus_usage": 1.0 + } +} diff --git a/nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get.json.tpl b/nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get.json.tpl new file mode 100644 index 0000000000..25b5ff2b84 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-simple-tenant-usage/simple-tenant-usage-get.json.tpl @@ -0,0 +1,13 @@ +{ + "tenant_usages": [ + { + "start": "%(strtime)s", + "stop": "%(strtime)s", + "tenant_id": "openstack", + "total_hours": 1.0, + "total_local_gb_usage": 1.0, + "total_memory_mb_usage": 512.0, + "total_vcpus_usage": 1.0 + } + ] +} diff --git a/nova/tests/integrated/v3/test_simple_tenant_usage.py b/nova/tests/integrated/v3/test_simple_tenant_usage.py new file mode 100644 index 0000000000..111aa2e5e7 --- /dev/null +++ b/nova/tests/integrated/v3/test_simple_tenant_usage.py @@ -0,0 +1,60 @@ +# Copyright 2014 IBM Corp. +# +# 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 datetime +import urllib + +from nova.openstack.common import timeutils +from nova.tests.integrated.v3 import test_servers + + +class SimpleTenantUsageSampleJsonTest(test_servers.ServersSampleBase): + extension_name = "os-simple-tenant-usage" + + def setUp(self): + """setUp method for simple tenant usage.""" + super(SimpleTenantUsageSampleJsonTest, self).setUp() + + started = timeutils.utcnow() + now = started + datetime.timedelta(hours=1) + + timeutils.set_time_override(started) + self._post_server() + timeutils.set_time_override(now) + + self.query = { + 'start': str(started), + 'end': str(now) + } + + def tearDown(self): + """tearDown method for simple tenant usage.""" + super(SimpleTenantUsageSampleJsonTest, self).tearDown() + timeutils.clear_time_override() + + def test_get_tenants_usage(self): + # Get api sample to get all tenants usage request. + response = self._do_get('os-simple-tenant-usage?%s' % ( + urllib.urlencode(self.query))) + subs = self._get_regexes() + self._verify_response('simple-tenant-usage-get', subs, response, 200) + + def test_get_tenant_usage_details(self): + # Get api sample to get specific tenant usage request. + tenant_id = 'openstack' + response = self._do_get('os-simple-tenant-usage/%s?%s' % (tenant_id, + urllib.urlencode(self.query))) + subs = self._get_regexes() + self._verify_response('simple-tenant-usage-get-specific', subs, + response, 200) diff --git a/setup.cfg b/setup.cfg index 46b34e46c1..ed63def531 100644 --- a/setup.cfg +++ b/setup.cfg @@ -109,6 +109,7 @@ nova.api.v3.extensions = servers = nova.api.openstack.compute.plugins.v3.servers:Servers services = nova.api.openstack.compute.plugins.v3.services:Services shelve = nova.api.openstack.compute.plugins.v3.shelve:Shelve + simple_tenant_usage = nova.api.openstack.compute.plugins.v3.simple_tenant_usage:SimpleTenantUsage suspend_server = nova.api.openstack.compute.plugins.v3.suspend_server:SuspendServer versions = nova.api.openstack.compute.plugins.v3.versions:Versions