nova/nova/tests/unit/api/openstack/compute/test_simple_tenant_usage.py

376 lines
15 KiB
Python

# 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 mock
from oslo_policy import policy as oslo_policy
from oslo_utils import timeutils
from six.moves import range
import webob
from nova.api.openstack.compute import simple_tenant_usage as \
simple_tenant_usage_v21
from nova.compute import vm_states
from nova import context
from nova import exception
from nova import objects
from nova import policy
from nova import test
from nova.tests.unit.api.openstack import fakes
from nova.tests import uuidsentinel as uuids
SERVERS = 5
TENANTS = 2
HOURS = 24
ROOT_GB = 10
EPHEMERAL_GB = 20
MEMORY_MB = 1024
VCPUS = 2
NOW = timeutils.utcnow()
START = NOW - datetime.timedelta(hours=HOURS)
STOP = NOW
FAKE_INST_TYPE = {'id': 1,
'vcpus': VCPUS,
'root_gb': ROOT_GB,
'ephemeral_gb': EPHEMERAL_GB,
'memory_mb': MEMORY_MB,
'name': 'fakeflavor',
'flavorid': 'foo',
'rxtx_factor': 1.0,
'vcpu_weight': 1,
'swap': 0,
'created_at': None,
'updated_at': None,
'deleted_at': None,
'deleted': 0,
'disabled': False,
'is_public': True,
'extra_specs': {'foo': 'bar'}}
def _fake_instance(start, end, instance_id, tenant_id,
vm_state=vm_states.ACTIVE):
flavor = objects.Flavor(**FAKE_INST_TYPE)
return objects.Instance(
deleted=False,
id=instance_id,
uuid=getattr(uuids, 'instance_%d' % instance_id),
image_ref='1',
project_id=tenant_id,
user_id='fakeuser',
display_name='name',
instance_type_id=FAKE_INST_TYPE['id'],
launched_at=start,
terminated_at=end,
vm_state=vm_state,
memory_mb=MEMORY_MB,
vcpus=VCPUS,
root_gb=ROOT_GB,
ephemeral_gb=EPHEMERAL_GB,
flavor=flavor)
def _fake_instance_deleted_flavorless(context, start, end, instance_id,
tenant_id, vm_state=vm_states.ACTIVE):
return objects.Instance(
context=context,
deleted=instance_id,
id=instance_id,
uuid=getattr(uuids, 'instance_%d' % instance_id),
image_ref='1',
project_id=tenant_id,
user_id='fakeuser',
display_name='name',
instance_type_id=FAKE_INST_TYPE['id'],
launched_at=start,
terminated_at=end,
deleted_at=start,
vm_state=vm_state,
memory_mb=MEMORY_MB,
vcpus=VCPUS,
root_gb=ROOT_GB,
ephemeral_gb=EPHEMERAL_GB)
@classmethod
def fake_get_active_deleted_flavorless(cls, context, begin, end=None,
project_id=None, host=None,
expected_attrs=None, use_slave=False,
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)]
# 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)])
return objects.InstanceList(objects=instances)
@classmethod
def fake_get_active_by_window_joined(cls, context, begin, end=None,
project_id=None, host=None,
expected_attrs=None, use_slave=False):
return objects.InstanceList(objects=[
_fake_instance(START, STOP, x,
project_id or 'faketenant_%s' % (x / SERVERS))
for x in range(TENANTS * SERVERS)])
class SimpleTenantUsageTestV21(test.TestCase):
policy_rule_prefix = "os_compute_api:os-simple-tenant-usage"
controller = simple_tenant_usage_v21.SimpleTenantUsageController()
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)
def _test_verify_index(self, start, stop):
req = fakes.HTTPRequest.blank('?start=%s&end=%s' %
(start.isoformat(), stop.isoformat()))
req.environ['nova.context'] = self.admin_context
res_dict = self.controller.index(req)
usages = res_dict['tenant_usages']
for i in range(TENANTS):
self.assertEqual(SERVERS * HOURS, int(usages[i]['total_hours']))
self.assertEqual(SERVERS * (ROOT_GB + EPHEMERAL_GB) * HOURS,
int(usages[i]['total_local_gb_usage']))
self.assertEqual(SERVERS * MEMORY_MB * HOURS,
int(usages[i]['total_memory_mb_usage']))
self.assertEqual(SERVERS * VCPUS * HOURS,
int(usages[i]['total_vcpus_usage']))
self.assertFalse(usages[i].get('server_usages'))
# NOTE(artom) Test for bugs 1643444 and 1692893 (duplicates). We simulate a
# situation where an instance has been deleted (moved to shadow table) and
# its corresponding instance_extra row has been archived (deleted from
# shadow table).
@mock.patch('nova.objects.InstanceList.get_active_by_window_joined',
fake_get_active_deleted_flavorless)
@mock.patch.object(
objects.Instance, '_load_flavor',
side_effect=exception.InstanceNotFound(instance_id='fake-id'))
def test_verify_index_deleted_flavorless(self, mock_load):
with mock.patch.object(self.controller, '_get_flavor',
return_value=None):
self._test_verify_index(START, STOP)
@mock.patch('nova.objects.InstanceList.get_active_by_window_joined',
fake_get_active_by_window_joined)
def test_verify_index(self):
self._test_verify_index(START, STOP)
@mock.patch('nova.objects.InstanceList.get_active_by_window_joined',
fake_get_active_by_window_joined)
def test_verify_index_future_end_time(self):
future = NOW + datetime.timedelta(hours=HOURS)
self._test_verify_index(START, future)
def test_verify_show(self):
self._test_verify_show(START, STOP)
def test_verify_show_future_end_time(self):
future = NOW + datetime.timedelta(hours=HOURS)
self._test_verify_show(START, future)
@mock.patch('nova.objects.InstanceList.get_active_by_window_joined',
fake_get_active_by_window_joined)
def _get_tenant_usages(self, detailed=''):
req = fakes.HTTPRequest.blank('?detailed=%s&start=%s&end=%s' %
(detailed, START.isoformat(), STOP.isoformat()))
req.environ['nova.context'] = self.admin_context
# Make sure that get_active_by_window_joined is only called with
# expected_attrs=['flavor'].
orig_get_active_by_window_joined = (
objects.InstanceList.get_active_by_window_joined)
def fake_get_active_by_window_joined(context, begin, end=None,
project_id=None, host=None,
expected_attrs=None,
use_slave=False):
self.assertEqual(['flavor'], expected_attrs)
return orig_get_active_by_window_joined(context, begin, end,
project_id, host,
expected_attrs, use_slave)
with mock.patch.object(objects.InstanceList,
'get_active_by_window_joined',
side_effect=fake_get_active_by_window_joined):
res_dict = self.controller.index(req)
return res_dict['tenant_usages']
def test_verify_detailed_index(self):
usages = self._get_tenant_usages('1')
for i in range(TENANTS):
servers = usages[i]['server_usages']
for j in range(SERVERS):
self.assertEqual(HOURS, int(servers[j]['hours']))
def test_verify_simple_index(self):
usages = self._get_tenant_usages(detailed='0')
for i in range(TENANTS):
self.assertIsNone(usages[i].get('server_usages'))
def test_verify_simple_index_empty_param(self):
# NOTE(lzyeval): 'detailed=&start=..&end=..'
usages = self._get_tenant_usages()
for i in range(TENANTS):
self.assertIsNone(usages[i].get('server_usages'))
@mock.patch('nova.objects.InstanceList.get_active_by_window_joined',
fake_get_active_by_window_joined)
def _test_verify_show(self, start, stop):
tenant_id = 1
req = fakes.HTTPRequest.blank('?start=%s&end=%s' %
(start.isoformat(), stop.isoformat()))
req.environ['nova.context'] = self.user_context
res_dict = self.controller.show(req, tenant_id)
usage = res_dict['tenant_usage']
servers = usage['server_usages']
self.assertEqual(TENANTS * SERVERS, len(usage['server_usages']))
server_uuids = [getattr(uuids, 'instance_%d' % x)
for x in range(SERVERS)]
for j in range(SERVERS):
delta = STOP - START
# NOTE(javeme): cast seconds from float to int for clarity
uptime = int(delta.total_seconds())
self.assertEqual(uptime, int(servers[j]['uptime']))
self.assertEqual(HOURS, int(servers[j]['hours']))
self.assertIn(servers[j]['instance_id'], server_uuids)
def test_verify_show_cannot_view_other_tenant(self):
req = fakes.HTTPRequest.blank('?start=%s&end=%s' %
(START.isoformat(), STOP.isoformat()))
req.environ['nova.context'] = self.alt_user_context
rules = {
self.policy_rule_prefix + ":show": [
["role:admin"], ["project_id:%(project_id)s"]]
}
policy.set_rules(oslo_policy.Rules.from_dict(rules))
try:
self.assertRaises(exception.PolicyNotAuthorized,
self.controller.show, req, 'faketenant_0')
finally:
policy.reset()
def test_get_tenants_usage_with_bad_start_date(self):
future = NOW + datetime.timedelta(hours=HOURS)
req = fakes.HTTPRequest.blank('?start=%s&end=%s' %
(future.isoformat(), NOW.isoformat()))
req.environ['nova.context'] = self.user_context
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.show, req, 'faketenant_0')
def test_get_tenants_usage_with_invalid_start_date(self):
req = fakes.HTTPRequest.blank('?start=%s&end=%s' %
("xxxx", NOW.isoformat()))
req.environ['nova.context'] = self.user_context
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.show, req, 'faketenant_0')
def _test_get_tenants_usage_with_one_date(self, date_url_param):
req = fakes.HTTPRequest.blank('?%s' % date_url_param)
req.environ['nova.context'] = self.user_context
res = self.controller.show(req, 'faketenant_0')
self.assertIn('tenant_usage', res)
def test_get_tenants_usage_with_no_start_date(self):
self._test_get_tenants_usage_with_one_date(
'end=%s' % (NOW + datetime.timedelta(5)).isoformat())
def test_get_tenants_usage_with_no_end_date(self):
self._test_get_tenants_usage_with_one_date(
'start=%s' % (NOW - datetime.timedelta(5)).isoformat())
class SimpleTenantUsageControllerTestV21(test.TestCase):
controller = simple_tenant_usage_v21.SimpleTenantUsageController()
def setUp(self):
super(SimpleTenantUsageControllerTestV21, self).setUp()
self.context = context.RequestContext('fakeuser', 'fake-project')
self.inst_obj = _fake_instance(START, STOP, instance_id=1,
tenant_id=self.context.project_id,
vm_state=vm_states.DELETED)
@mock.patch('nova.objects.Instance.get_flavor',
side_effect=exception.NotFound())
def test_get_flavor_from_non_deleted_with_id_fails(self, fake_get_flavor):
# If an instance is not deleted and missing type information from
# instance.flavor, then that's a bug
self.assertRaises(exception.NotFound,
self.controller._get_flavor, self.context,
self.inst_obj, {})
@mock.patch('nova.objects.Instance.get_flavor',
side_effect=exception.NotFound())
def test_get_flavor_from_deleted_with_notfound(self, fake_get_flavor):
# If the flavor is not found from the instance and the instance is
# deleted, attempt to look it up from the DB and if found we're OK.
self.inst_obj.deleted = 1
flavor = self.controller._get_flavor(self.context, self.inst_obj, {})
self.assertEqual(objects.Flavor, type(flavor))
self.assertEqual(FAKE_INST_TYPE['id'], flavor.id)
@mock.patch('nova.objects.Instance.get_flavor',
side_effect=exception.NotFound())
def test_get_flavor_from_deleted_with_id_of_deleted(self, fake_get_flavor):
# Verify the legacy behavior of instance_type_id pointing to a
# missing type being non-fatal
self.inst_obj.deleted = 1
self.inst_obj.instance_type_id = 99
flavor = self.controller._get_flavor(self.context, self.inst_obj, {})
self.assertIsNone(flavor)
class SimpleTenantUsageUtilsV21(test.NoDBTestCase):
simple_tenant_usage = simple_tenant_usage_v21
def test_valid_string(self):
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,
self.simple_tenant_usage.parse_strtime,
"2014-02-21 13:47:20.824060",
"%Y-%m-%dT%H:%M:%S.%f")