summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbin/instance-usage-audit5
-rw-r--r--nova/api/openstack/contrib/simple_tenant_usage.py236
-rw-r--r--nova/compute/api.py69
-rw-r--r--nova/db/api.py17
-rw-r--r--nova/db/sqlalchemy/api.py33
-rw-r--r--nova/tests/api/openstack/contrib/test_createserverext.py2
-rw-r--r--nova/tests/api/openstack/contrib/test_security_groups.py72
-rw-r--r--nova/tests/api/openstack/contrib/test_simple_tenant_usage.py172
-rw-r--r--nova/tests/api/openstack/test_extensions.py1
-rw-r--r--nova/tests/api/openstack/test_servers.py1
10 files changed, 529 insertions, 79 deletions
diff --git a/bin/instance-usage-audit b/bin/instance-usage-audit
index a06c6b1b3..7ce5732e7 100755
--- a/bin/instance-usage-audit
+++ b/bin/instance-usage-audit
@@ -102,9 +102,8 @@ if __name__ == '__main__':
logging.setup()
begin, end = time_period(FLAGS.instance_usage_audit_period)
print "Creating usages for %s until %s" % (str(begin), str(end))
- instances = db.instance_get_active_by_window(context.get_admin_context(),
- begin,
- end)
+ ctxt = context.get_admin_context()
+ instances = db.instance_get_active_by_window_joined(ctxt, begin, end)
print "%s instances" % len(instances)
for instance_ref in instances:
usage_info = utils.usage_from_instance(instance_ref,
diff --git a/nova/api/openstack/contrib/simple_tenant_usage.py b/nova/api/openstack/contrib/simple_tenant_usage.py
new file mode 100644
index 000000000..69b38e229
--- /dev/null
+++ b/nova/api/openstack/contrib/simple_tenant_usage.py
@@ -0,0 +1,236 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# 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 urlparse
+import webob
+
+from datetime import datetime
+from nova import exception
+from nova import flags
+from nova.compute import api
+from nova.api.openstack import extensions
+from nova.api.openstack import views
+from nova.db.sqlalchemy.session import get_session
+from webob import exc
+
+
+FLAGS = flags.FLAGS
+
+
+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):
+ terminated_at = datetime.strptime(terminated_at,
+ "%Y-%m-%d %H:%M:%S.%f")
+
+ if launched_at is not None:
+ if not isinstance(launched_at, datetime):
+ launched_at = datetime.strptime(launched_at,
+ "%Y-%m-%d %H:%M:%S.%f")
+
+ 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 _tenant_usages_for_period(self, context, period_start,
+ period_stop, tenant_id=None, detailed=True):
+
+ compute_api = api.API()
+ instances = compute_api.get_active_by_window(context,
+ period_start,
+ period_stop,
+ tenant_id)
+ from nova import log as logging
+ logging.info(instances)
+ rval = {}
+ flavors = {}
+
+ for instance in instances:
+ info = {}
+ info['hours'] = self._hours_for(instance,
+ period_start,
+ period_stop)
+ flavor_type = instance['instance_type_id']
+
+ if not flavors.get(flavor_type):
+ try:
+ it_ref = compute_api.get_instance_type(context,
+ flavor_type)
+ flavors[flavor_type] = it_ref
+ except exception.InstanceTypeNotFound:
+ # can't bill if there is no instance type
+ continue
+
+ flavor = flavors[flavor_type]
+
+ info['name'] = instance['display_name']
+
+ info['memory_mb'] = flavor['memory_mb']
+ info['local_gb'] = flavor['local_gb']
+ info['vcpus'] = flavor['vcpus']
+
+ info['tenant_id'] = instance['project_id']
+
+ info['flavor'] = flavor['name']
+
+ info['started_at'] = instance['launched_at']
+
+ info['ended_at'] = instance['terminated_at']
+
+ if info['ended_at']:
+ info['state'] = 'terminated'
+ else:
+ info['state'] = instance['state_description']
+
+ now = datetime.utcnow()
+
+ if info['state'] == 'terminated':
+ delta = info['ended_at'] - info['started_at']
+ else:
+ delta = now - info['started_at']
+
+ info['uptime'] = delta.days * 24 * 60 + delta.seconds
+
+ if not info['tenant_id'] 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'] = period_start
+ summary['stop'] = 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 isinstance(dtstr, datetime):
+ return dtstr
+ try:
+ return datetime.strptime(dtstr, "%Y-%m-%dT%H:%M:%S")
+ except:
+ try:
+ return datetime.strptime(dtstr, "%Y-%m-%dT%H:%M:%S.%f")
+ except:
+ return datetime.strptime(dtstr, "%Y-%m-%d %H:%M:%S.%f")
+
+ def _get_datetime_range(self, req):
+ qs = req.environ.get('QUERY_STRING', '')
+ env = urlparse.parse_qs(qs)
+ period_start = self._parse_datetime(env.get('start',
+ [datetime.utcnow().isoformat()])[0])
+ period_stop = self._parse_datetime(env.get('end',
+ [datetime.utcnow().isoformat()])[0])
+
+ detailed = bool(env.get('detailed', False))
+ return (period_start, period_stop, detailed)
+
+ def index(self, req):
+ """Retrive tenant_usage for all tenants"""
+ context = req.environ['nova.context']
+
+ if not context.is_admin and FLAGS.allow_admin_api:
+ return webob.Response(status_int=403)
+
+ (period_start, period_stop, detailed) = self._get_datetime_range(req)
+ usages = self._tenant_usages_for_period(context,
+ period_start,
+ period_stop,
+ detailed=detailed)
+ return {'tenant_usages': usages}
+
+ def show(self, req, id):
+ """Retrive tenant_usage for a specified tenant"""
+ tenant_id = id
+ context = req.environ['nova.context']
+
+ if not context.is_admin and FLAGS.allow_admin_api:
+ if tenant_id != context.project_id:
+ return webob.Response(status_int=403)
+
+ (period_start, period_stop, ignore) = self._get_datetime_range(req)
+ 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 Simple_tenant_usage(extensions.ExtensionDescriptor):
+ def get_name(self):
+ return "SimpleTenantUsage"
+
+ def get_alias(self):
+ return "os-simple-tenant-usage"
+
+ def get_description(self):
+ return "Simple tenant usage extension"
+
+ def get_namespace(self):
+ return "http://docs.openstack.org/ext/os-simple-tenant-usage/api/v1.1"
+
+ def get_updated(self):
+ return "2011-08-19T00:00:00+00:00"
+
+ def get_resources(self):
+ resources = []
+
+ res = extensions.ResourceExtension('os-simple-tenant-usage',
+ SimpleTenantUsageController())
+ resources.append(res)
+
+ return resources
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 595622ba1..e045ef3de 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -19,13 +19,11 @@
"""Handles all requests relating to instances (guest vms)."""
-import eventlet
import novaclient
import re
import time
from nova import block_device
-from nova import db
from nova import exception
from nova import flags
import nova.image
@@ -245,7 +243,7 @@ class API(base.Base):
self.ensure_default_security_group(context)
if key_data is None and key_name:
- key_pair = db.key_pair_get(context, context.user_id, key_name)
+ key_pair = self.db.key_pair_get(context, context.user_id, key_name)
key_data = key_pair['public_key']
if reservation_id is None:
@@ -397,9 +395,9 @@ class API(base.Base):
security_groups = []
for security_group_name in security_group:
- group = db.security_group_get_by_name(context,
- context.project_id,
- security_group_name)
+ group = self.db.security_group_get_by_name(context,
+ context.project_id,
+ security_group_name)
security_groups.append(group['id'])
for security_group_id in security_groups:
@@ -561,8 +559,9 @@ class API(base.Base):
def has_finished_migration(self, context, instance_uuid):
"""Returns true if an instance has a finished migration."""
try:
- db.migration_get_by_instance_and_status(context, instance_uuid,
- 'finished')
+ self.db.migration_get_by_instance_and_status(context,
+ instance_uuid,
+ 'finished')
return True
except exception.NotFound:
return False
@@ -576,14 +575,15 @@ class API(base.Base):
:param context: the security context
"""
try:
- db.security_group_get_by_name(context, context.project_id,
- 'default')
+ self.db.security_group_get_by_name(context,
+ context.project_id,
+ 'default')
except exception.NotFound:
values = {'name': 'default',
'description': 'default',
'user_id': context.user_id,
'project_id': context.project_id}
- db.security_group_create(context, values)
+ self.db.security_group_create(context, values)
def trigger_security_group_rules_refresh(self, context, security_group_id):
"""Called when a rule is added to or removed from a security_group."""
@@ -648,7 +648,7 @@ class API(base.Base):
"""Called when a rule is added to or removed from a security_group"""
hosts = [x['host'] for (x, idx)
- in db.service_get_all_compute_sorted(context)]
+ in self.db.service_get_all_compute_sorted(context)]
for host in hosts:
rpc.cast(context,
self.db.queue_get_for(context, FLAGS.compute_topic, host),
@@ -676,11 +676,11 @@ class API(base.Base):
def add_security_group(self, context, instance_id, security_group_name):
"""Add security group to the instance"""
- security_group = db.security_group_get_by_name(context,
- context.project_id,
- security_group_name)
+ security_group = self.db.security_group_get_by_name(context,
+ context.project_id,
+ security_group_name)
# check if the server exists
- inst = db.instance_get(context, instance_id)
+ inst = self.db.instance_get(context, instance_id)
#check if the security group is associated with the server
if self._is_security_group_associated_with_server(security_group,
instance_id):
@@ -692,21 +692,21 @@ class API(base.Base):
if inst['state'] != power_state.RUNNING:
raise exception.InstanceNotRunning(instance_id=instance_id)
- db.instance_add_security_group(context.elevated(),
- instance_id,
- security_group['id'])
+ self.db.instance_add_security_group(context.elevated(),
+ instance_id,
+ security_group['id'])
rpc.cast(context,
- db.queue_get_for(context, FLAGS.compute_topic, inst['host']),
+ self.db.queue_get_for(context, FLAGS.compute_topic, inst['host']),
{"method": "refresh_security_group_rules",
"args": {"security_group_id": security_group['id']}})
def remove_security_group(self, context, instance_id, security_group_name):
"""Remove the security group associated with the instance"""
- security_group = db.security_group_get_by_name(context,
- context.project_id,
- security_group_name)
+ security_group = self.db.security_group_get_by_name(context,
+ context.project_id,
+ security_group_name)
# check if the server exists
- inst = db.instance_get(context, instance_id)
+ inst = self.db.instance_get(context, instance_id)
#check if the security group is associated with the server
if not self._is_security_group_associated_with_server(security_group,
instance_id):
@@ -718,11 +718,11 @@ class API(base.Base):
if inst['state'] != power_state.RUNNING:
raise exception.InstanceNotRunning(instance_id=instance_id)
- db.instance_remove_security_group(context.elevated(),
- instance_id,
- security_group['id'])
+ self.db.instance_remove_security_group(context.elevated(),
+ instance_id,
+ security_group['id'])
rpc.cast(context,
- db.queue_get_for(context, FLAGS.compute_topic, inst['host']),
+ self.db.queue_get_for(context, FLAGS.compute_topic, inst['host']),
{"method": "refresh_security_group_rules",
"args": {"security_group_id": security_group['id']}})
@@ -816,6 +816,15 @@ class API(base.Base):
"args": {"topic": FLAGS.compute_topic,
"instance_id": instance_id}})
+ def get_active_by_window(self, context, begin, end=None, project_id=None):
+ """Get instances that were continuously active over a window."""
+ return self.db.instance_get_active_by_window(context, begin, end,
+ project_id)
+
+ def get_instance_type(self, context, instance_type_id):
+ """Get an instance type by instance type id."""
+ return self.db.instance_type_get(context, instance_type_id)
+
def get(self, context, instance_id):
"""Get a single instance with the given instance_id."""
# NOTE(sirp): id used to be exclusively integer IDs; now we're
@@ -1015,7 +1024,7 @@ class API(base.Base):
:param extra_properties: dict of extra image properties to include
"""
- instance = db.api.instance_get(context, instance_id)
+ instance = self.db.instance_get(context, instance_id)
properties = {'instance_uuid': instance['uuid'],
'user_id': str(context.user_id),
'image_state': 'creating',
@@ -1044,7 +1053,7 @@ class API(base.Base):
def rebuild(self, context, instance_id, image_href, admin_password,
name=None, metadata=None, files_to_inject=None):
"""Rebuild the given instance with the provided metadata."""
- instance = db.api.instance_get(context, instance_id)
+ instance = self.db.instance_get(context, instance_id)
name = name or instance["display_name"]
if instance["vm_state"] != vm_states.ACTIVE:
diff --git a/nova/db/api.py b/nova/db/api.py
index a2e581fe9..148887635 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -496,9 +496,20 @@ def instance_get_all_by_filters(context, filters):
return IMPL.instance_get_all_by_filters(context, filters)
-def instance_get_active_by_window(context, begin, end=None):
- """Get instances active during a certain time window."""
- return IMPL.instance_get_active_by_window(context, begin, end)
+def instance_get_active_by_window(context, begin, end=None, project_id=None):
+ """Get instances active during a certain time window.
+
+ Specifying a project_id will filter for a certain project."""
+ return IMPL.instance_get_active_by_window(context, begin, end, project_id)
+
+
+def instance_get_active_by_window_joined(context, begin, end=None,
+ project_id=None):
+ """Get instances and joins active during a certain time window.
+
+ Specifying a project_id will filter for a certain project."""
+ return IMPL.instance_get_active_by_window_joined(context, begin, end,
+ project_id)
def instance_get_all_by_user(context, user_id):
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 631e53ceb..c97ff5070 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -1306,21 +1306,40 @@ def instance_get_all_by_filters(context, filters):
return instances
+@require_context
+def instance_get_active_by_window(context, begin, end=None, project_id=None):
+ """Return instances that were continuously active over window."""
+ session = get_session()
+ query = session.query(models.Instance).\
+ filter(models.Instance.launched_at < begin)
+ if end:
+ query = query.filter(or_(models.Instance.terminated_at == None,
+ models.Instance.terminated_at > end))
+ else:
+ query = query.filter(models.Instance.terminated_at == None)
+ if project_id:
+ query = query.filter_by(project_id=project_id)
+ return query.all()
+
+
@require_admin_context
-def instance_get_active_by_window(context, begin, end=None):
- """Return instances that were continuously active over the given window"""
+def instance_get_active_by_window_joined(context, begin, end=None,
+ project_id=None):
+ """Return instances and joins that were continuously active over window."""
session = get_session()
query = session.query(models.Instance).\
- options(joinedload_all('fixed_ips.floating_ips')).\
- options(joinedload('security_groups')).\
- options(joinedload_all('fixed_ips.network')).\
- options(joinedload('instance_type')).\
- filter(models.Instance.launched_at < begin)
+ options(joinedload_all('fixed_ips.floating_ips')).\
+ options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ips.network')).\
+ options(joinedload('instance_type')).\
+ filter(models.Instance.launched_at < begin)
if end:
query = query.filter(or_(models.Instance.terminated_at == None,
models.Instance.terminated_at > end))
else:
query = query.filter(models.Instance.terminated_at == None)
+ if project_id:
+ query = query.filter_by(project_id=project_id)
return query.all()
diff --git a/nova/tests/api/openstack/contrib/test_createserverext.py b/nova/tests/api/openstack/contrib/test_createserverext.py
index d8a5c9e55..089c8e59d 100644
--- a/nova/tests/api/openstack/contrib/test_createserverext.py
+++ b/nova/tests/api/openstack/contrib/test_createserverext.py
@@ -23,6 +23,7 @@ from xml.dom import minidom
import stubout
import webob
+from nova import db
from nova import exception
from nova import flags
from nova import test
@@ -77,6 +78,7 @@ class CreateserverextTest(test.TestCase):
self.injected_files = None
self.networks = None
self.user_data = None
+ self.db = db
def create(self, *args, **kwargs):
if 'injected_files' in kwargs:
diff --git a/nova/tests/api/openstack/contrib/test_security_groups.py b/nova/tests/api/openstack/contrib/test_security_groups.py
index bc1536911..0816a6312 100644
--- a/nova/tests/api/openstack/contrib/test_security_groups.py
+++ b/nova/tests/api/openstack/contrib/test_security_groups.py
@@ -360,7 +360,7 @@ class TestSecurityGroups(test.TestCase):
def test_associate_by_invalid_server_id(self):
body = dict(addSecurityGroup=dict(name='test'))
- self.stubs.Set(nova.db, 'security_group_get_by_name',
+ self.stubs.Set(nova.db.api, 'security_group_get_by_name',
return_security_group)
req = webob.Request.blank('/v1.1/123/servers/invalid/action')
req.headers['Content-Type'] = 'application/json'
@@ -372,7 +372,7 @@ class TestSecurityGroups(test.TestCase):
def test_associate_without_body(self):
req = webob.Request.blank('/v1.1/123/servers/1/action')
body = dict(addSecurityGroup=None)
- self.stubs.Set(nova.db, 'instance_get', return_server)
+ self.stubs.Set(nova.db.api, 'instance_get', return_server)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
req.body = json.dumps(body)
@@ -382,7 +382,7 @@ class TestSecurityGroups(test.TestCase):
def test_associate_no_security_group_name(self):
req = webob.Request.blank('/v1.1/123/servers/1/action')
body = dict(addSecurityGroup=dict())
- self.stubs.Set(nova.db, 'instance_get', return_server)
+ self.stubs.Set(nova.db.api, 'instance_get', return_server)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
req.body = json.dumps(body)
@@ -392,7 +392,7 @@ class TestSecurityGroups(test.TestCase):
def test_associate_security_group_name_with_whitespaces(self):
req = webob.Request.blank('/v1.1/123/servers/1/action')
body = dict(addSecurityGroup=dict(name=" "))
- self.stubs.Set(nova.db, 'instance_get', return_server)
+ self.stubs.Set(nova.db.api, 'instance_get', return_server)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
req.body = json.dumps(body)
@@ -400,9 +400,9 @@ class TestSecurityGroups(test.TestCase):
self.assertEquals(response.status_int, 400)
def test_associate_non_existing_instance(self):
- self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant)
+ self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
body = dict(addSecurityGroup=dict(name="test"))
- self.stubs.Set(nova.db, 'security_group_get_by_name',
+ self.stubs.Set(nova.db.api, 'security_group_get_by_name',
return_security_group)
req = webob.Request.blank('/v1.1/123/servers/10000/action')
req.headers['Content-Type'] = 'application/json'
@@ -412,8 +412,8 @@ class TestSecurityGroups(test.TestCase):
self.assertEquals(response.status_int, 404)
def test_associate_non_running_instance(self):
- self.stubs.Set(nova.db, 'instance_get', return_non_running_server)
- self.stubs.Set(nova.db, 'security_group_get_by_name',
+ self.stubs.Set(nova.db.api, 'instance_get', return_non_running_server)
+ self.stubs.Set(nova.db.api, 'security_group_get_by_name',
return_security_group_without_instances)
body = dict(addSecurityGroup=dict(name="test"))
req = webob.Request.blank('/v1.1/123/servers/1/action')
@@ -424,8 +424,8 @@ class TestSecurityGroups(test.TestCase):
self.assertEquals(response.status_int, 400)
def test_associate_already_associated_security_group_to_instance(self):
- self.stubs.Set(nova.db, 'instance_get', return_server)
- self.stubs.Set(nova.db, 'security_group_get_by_name',
+ self.stubs.Set(nova.db.api, 'instance_get', return_server)
+ self.stubs.Set(nova.db.api, 'security_group_get_by_name',
return_security_group)
body = dict(addSecurityGroup=dict(name="test"))
req = webob.Request.blank('/v1.1/123/servers/1/action')
@@ -436,12 +436,12 @@ class TestSecurityGroups(test.TestCase):
self.assertEquals(response.status_int, 400)
def test_associate(self):
- self.stubs.Set(nova.db, 'instance_get', return_server)
- self.mox.StubOutWithMock(nova.db, 'instance_add_security_group')
- nova.db.instance_add_security_group(mox.IgnoreArg(),
+ self.stubs.Set(nova.db.api, 'instance_get', return_server)
+ self.mox.StubOutWithMock(nova.db.api, 'instance_add_security_group')
+ nova.db.api.instance_add_security_group(mox.IgnoreArg(),
mox.IgnoreArg(),
mox.IgnoreArg())
- self.stubs.Set(nova.db, 'security_group_get_by_name',
+ self.stubs.Set(nova.db.api, 'security_group_get_by_name',
return_security_group_without_instances)
self.mox.ReplayAll()
@@ -454,12 +454,12 @@ class TestSecurityGroups(test.TestCase):
self.assertEquals(response.status_int, 202)
def test_associate_xml(self):
- self.stubs.Set(nova.db, 'instance_get', return_server)
- self.mox.StubOutWithMock(nova.db, 'instance_add_security_group')
- nova.db.instance_add_security_group(mox.IgnoreArg(),
+ self.stubs.Set(nova.db.api, 'instance_get', return_server)
+ self.mox.StubOutWithMock(nova.db.api, 'instance_add_security_group')
+ nova.db.api.instance_add_security_group(mox.IgnoreArg(),
mox.IgnoreArg(),
mox.IgnoreArg())
- self.stubs.Set(nova.db, 'security_group_get_by_name',
+ self.stubs.Set(nova.db.api, 'security_group_get_by_name',
return_security_group_without_instances)
self.mox.ReplayAll()
@@ -483,7 +483,7 @@ class TestSecurityGroups(test.TestCase):
def test_disassociate_by_invalid_server_id(self):
body = dict(removeSecurityGroup=dict(name='test'))
- self.stubs.Set(nova.db, 'security_group_get_by_name',
+ self.stubs.Set(nova.db.api, 'security_group_get_by_name',
return_security_group)
req = webob.Request.blank('/v1.1/123/servers/invalid/action')
req.headers['Content-Type'] = 'application/json'
@@ -495,7 +495,7 @@ class TestSecurityGroups(test.TestCase):
def test_disassociate_without_body(self):
req = webob.Request.blank('/v1.1/123/servers/1/action')
body = dict(removeSecurityGroup=None)
- self.stubs.Set(nova.db, 'instance_get', return_server)
+ self.stubs.Set(nova.db.api, 'instance_get', return_server)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
req.body = json.dumps(body)
@@ -505,7 +505,7 @@ class TestSecurityGroups(test.TestCase):
def test_disassociate_no_security_group_name(self):
req = webob.Request.blank('/v1.1/123/servers/1/action')
body = dict(removeSecurityGroup=dict())
- self.stubs.Set(nova.db, 'instance_get', return_server)
+ self.stubs.Set(nova.db.api, 'instance_get', return_server)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
req.body = json.dumps(body)
@@ -515,7 +515,7 @@ class TestSecurityGroups(test.TestCase):
def test_disassociate_security_group_name_with_whitespaces(self):
req = webob.Request.blank('/v1.1/123/servers/1/action')
body = dict(removeSecurityGroup=dict(name=" "))
- self.stubs.Set(nova.db, 'instance_get', return_server)
+ self.stubs.Set(nova.db.api, 'instance_get', return_server)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
req.body = json.dumps(body)
@@ -523,9 +523,9 @@ class TestSecurityGroups(test.TestCase):
self.assertEquals(response.status_int, 400)
def test_disassociate_non_existing_instance(self):
- self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant)
+ self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
body = dict(removeSecurityGroup=dict(name="test"))
- self.stubs.Set(nova.db, 'security_group_get_by_name',
+ self.stubs.Set(nova.db.api, 'security_group_get_by_name',
return_security_group)
req = webob.Request.blank('/v1.1/123/servers/10000/action')
req.headers['Content-Type'] = 'application/json'
@@ -535,8 +535,8 @@ class TestSecurityGroups(test.TestCase):
self.assertEquals(response.status_int, 404)
def test_disassociate_non_running_instance(self):
- self.stubs.Set(nova.db, 'instance_get', return_non_running_server)
- self.stubs.Set(nova.db, 'security_group_get_by_name',
+ self.stubs.Set(nova.db.api, 'instance_get', return_non_running_server)
+ self.stubs.Set(nova.db.api, 'security_group_get_by_name',
return_security_group)
body = dict(removeSecurityGroup=dict(name="test"))
req = webob.Request.blank('/v1.1/123/servers/1/action')
@@ -547,8 +547,8 @@ class TestSecurityGroups(test.TestCase):
self.assertEquals(response.status_int, 400)
def test_disassociate_already_associated_security_group_to_instance(self):
- self.stubs.Set(nova.db, 'instance_get', return_server)
- self.stubs.Set(nova.db, 'security_group_get_by_name',
+ self.stubs.Set(nova.db.api, 'instance_get', return_server)
+ self.stubs.Set(nova.db.api, 'security_group_get_by_name',
return_security_group_without_instances)
body = dict(removeSecurityGroup=dict(name="test"))
req = webob.Request.blank('/v1.1/123/servers/1/action')
@@ -559,12 +559,12 @@ class TestSecurityGroups(test.TestCase):
self.assertEquals(response.status_int, 400)
def test_disassociate(self):
- self.stubs.Set(nova.db, 'instance_get', return_server)
- self.mox.StubOutWithMock(nova.db, 'instance_remove_security_group')
- nova.db.instance_remove_security_group(mox.IgnoreArg(),
+ self.stubs.Set(nova.db.api, 'instance_get', return_server)
+ self.mox.StubOutWithMock(nova.db.api, 'instance_remove_security_group')
+ nova.db.api.instance_remove_security_group(mox.IgnoreArg(),
mox.IgnoreArg(),
mox.IgnoreArg())
- self.stubs.Set(nova.db, 'security_group_get_by_name',
+ self.stubs.Set(nova.db.api, 'security_group_get_by_name',
return_security_group)
self.mox.ReplayAll()
@@ -577,12 +577,12 @@ class TestSecurityGroups(test.TestCase):
self.assertEquals(response.status_int, 202)
def test_disassociate_xml(self):
- self.stubs.Set(nova.db, 'instance_get', return_server)
- self.mox.StubOutWithMock(nova.db, 'instance_remove_security_group')
- nova.db.instance_remove_security_group(mox.IgnoreArg(),
+ self.stubs.Set(nova.db.api, 'instance_get', return_server)
+ self.mox.StubOutWithMock(nova.db.api, 'instance_remove_security_group')
+ nova.db.api.instance_remove_security_group(mox.IgnoreArg(),
mox.IgnoreArg(),
mox.IgnoreArg())
- self.stubs.Set(nova.db, 'security_group_get_by_name',
+ self.stubs.Set(nova.db.api, 'security_group_get_by_name',
return_security_group)
self.mox.ReplayAll()
diff --git a/nova/tests/api/openstack/contrib/test_simple_tenant_usage.py b/nova/tests/api/openstack/contrib/test_simple_tenant_usage.py
new file mode 100644
index 000000000..2430b9d51
--- /dev/null
+++ b/nova/tests/api/openstack/contrib/test_simple_tenant_usage.py
@@ -0,0 +1,172 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# 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 json
+import webob
+
+from nova import context
+from nova import flags
+from nova import test
+from nova.compute import api
+from nova.tests.api.openstack import fakes
+
+
+FLAGS = flags.FLAGS
+
+SERVERS = 5
+TENANTS = 2
+HOURS = 24
+LOCAL_GB = 10
+MEMORY_MB = 1024
+VCPUS = 2
+STOP = datetime.datetime.utcnow()
+START = STOP - datetime.timedelta(hours=HOURS)
+
+
+def fake_instance_type_get(self, context, instance_type_id):
+ return {'id': 1,
+ 'vcpus': VCPUS,
+ 'local_gb': LOCAL_GB,
+ 'memory_mb': MEMORY_MB,
+ 'name':
+ 'fakeflavor'}
+
+
+def get_fake_db_instance(start, end, instance_id, tenant_id):
+ return {'id': instance_id,
+ 'image_ref': '1',
+ 'project_id': tenant_id,
+ 'user_id': 'fakeuser',
+ 'display_name': 'name',
+ 'state_description': 'state',
+ 'instance_type_id': 1,
+ 'launched_at': start,
+ 'terminated_at': end}
+
+
+def fake_instance_get_active_by_window(self, context, begin, end, project_id):
+ return [get_fake_db_instance(START,
+ STOP,
+ x,
+ "faketenant_%s" % (x / SERVERS))
+ for x in xrange(TENANTS * SERVERS)]
+
+
+class SimpleTenantUsageTest(test.TestCase):
+ def setUp(self):
+ super(SimpleTenantUsageTest, self).setUp()
+ self.stubs.Set(api.API, "get_instance_type",
+ fake_instance_type_get)
+ self.stubs.Set(api.API, "get_active_by_window",
+ fake_instance_get_active_by_window)
+ 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)
+ FLAGS.allow_admin_api = True
+
+ def test_verify_index(self):
+ req = webob.Request.blank(
+ '/v1.1/123/os-simple-tenant-usage?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))
+
+ self.assertEqual(res.status_int, 200)
+ res_dict = json.loads(res.body)
+ usages = res_dict['tenant_usages']
+ from nova import log as logging
+ logging.warn(usages)
+ for i in xrange(TENANTS):
+ self.assertEqual(int(usages[i]['total_hours']),
+ SERVERS * HOURS)
+ self.assertEqual(int(usages[i]['total_local_gb_usage']),
+ SERVERS * LOCAL_GB * HOURS)
+ self.assertEqual(int(usages[i]['total_memory_mb_usage']),
+ SERVERS * MEMORY_MB * HOURS)
+ self.assertEqual(int(usages[i]['total_vcpus_usage']),
+ SERVERS * VCPUS * HOURS)
+ self.assertFalse(usages[i].get('server_usages'))
+
+ def test_verify_detailed_index(self):
+ req = webob.Request.blank(
+ '/v1.1/123/os-simple-tenant-usage?'
+ 'detailed=1&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))
+ self.assertEqual(res.status_int, 200)
+ res_dict = json.loads(res.body)
+ usages = res_dict['tenant_usages']
+ for i in xrange(TENANTS):
+ servers = usages[i]['server_usages']
+ for j in xrange(SERVERS):
+ self.assertEqual(int(servers[j]['hours']), HOURS)
+
+ def test_verify_index_fails_for_nonadmin(self):
+ req = webob.Request.blank(
+ '/v1.1/123/os-simple-tenant-usage?'
+ 'detailed=1&start=%s&end=%s' %
+ (START.isoformat(), STOP.isoformat()))
+ req.method = "GET"
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 403)
+
+ def test_verify_show(self):
+ req = webob.Request.blank(
+ '/v1.1/faketenant_0/os-simple-tenant-usage/'
+ 'faketenant_0?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.user_context))
+ self.assertEqual(res.status_int, 200)
+ res_dict = json.loads(res.body)
+
+ usage = res_dict['tenant_usage']
+ servers = usage['server_usages']
+ self.assertEqual(len(usage['server_usages']), SERVERS)
+ for j in xrange(SERVERS):
+ self.assertEqual(int(servers[j]['hours']), HOURS)
+
+ def test_verify_show_cant_view_other_tenant(self):
+ req = webob.Request.blank(
+ '/v1.1/faketenant_1/os-simple-tenant-usage/'
+ 'faketenant_0?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.alt_user_context))
+ self.assertEqual(res.status_int, 403)
diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py
index 05267d8fb..31443242b 100644
--- a/nova/tests/api/openstack/test_extensions.py
+++ b/nova/tests/api/openstack/test_extensions.py
@@ -95,6 +95,7 @@ class ExtensionControllerTest(test.TestCase):
"Quotas",
"Rescue",
"SecurityGroups",
+ "SimpleTenantUsage",
"VSAs",
"VirtualInterfaces",
"Volumes",
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index f75263c45..19b530d99 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -3258,6 +3258,7 @@ class TestServerInstanceCreation(test.TestCase):
def __init__(self):
self.injected_files = None
self.networks = None
+ self.db = db
def create(self, *args, **kwargs):
if 'injected_files' in kwargs: