# 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
import nova.conf
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


CONF = nova.conf.CONF


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,
                                     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)])


class SimpleTenantUsageTestV21(test.TestCase):
    version = '2.1'
    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)
        self.num_cells = len(objects.CellMappingList.get_all(
            self.admin_context))

    def _test_verify_index(self, start, stop, limit=None):
        url = '?start=%s&end=%s'
        if limit:
            url += '&limit=%s' % (limit)
        req = fakes.HTTPRequest.blank(url %
                    (start.isoformat(), stop.isoformat()),
                    version=self.version)
        req.environ['nova.context'] = self.admin_context
        res_dict = self.controller.index(req)

        usages = res_dict['tenant_usages']

        if limit:
            num = 1
        else:
            # NOTE(danms): We call our fake data mock once per cell,
            # and the default fixture has two cells (cell0 and cell1),
            # so all our math will be doubled.
            num = self.num_cells

        for i in range(TENANTS):
            self.assertEqual(SERVERS * HOURS * num,
                             int(usages[i]['total_hours']))
            self.assertEqual(SERVERS * (ROOT_GB + EPHEMERAL_GB) * HOURS * num,
                             int(usages[i]['total_local_gb_usage']))
            self.assertEqual(SERVERS * MEMORY_MB * HOURS * num,
                             int(usages[i]['total_memory_mb_usage']))
            self.assertEqual(SERVERS * VCPUS * HOURS * num,
                             int(usages[i]['total_vcpus_usage']))
            self.assertFalse(usages[i].get('server_usages'))

        if limit:
            self.assertIn('tenant_usages_links', res_dict)
            self.assertEqual('next', res_dict['tenant_usages_links'][0]['rel'])
        else:
            self.assertNotIn('tenant_usages_links', res_dict)

    # 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()),
                    version=self.version)
        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,
                                    limit=None, marker=None):
            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, limit=None):
        tenant_id = 1
        url = '?start=%s&end=%s'
        if limit:
            url += '&limit=%s' % (limit)
        req = fakes.HTTPRequest.blank(url %
                    (start.isoformat(), stop.isoformat()),
                    version=self.version)
        req.environ['nova.context'] = self.user_context
        res_dict = self.controller.show(req, tenant_id)

        if limit:
            num = 1
        else:
            # NOTE(danms): We call our fake data mock once per cell,
            # and the default fixture has two cells (cell0 and cell1),
            # so all our math will be doubled.
            num = self.num_cells

        usage = res_dict['tenant_usage']
        servers = usage['server_usages']
        self.assertEqual(TENANTS * SERVERS * num, 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)

        if limit:
            self.assertIn('tenant_usage_links', res_dict)
            self.assertEqual('next', res_dict['tenant_usage_links'][0]['rel'])
        else:
            self.assertNotIn('tenant_usage_links', res_dict)

    def test_verify_show_cannot_view_other_tenant(self):
        req = fakes.HTTPRequest.blank('?start=%s&end=%s' %
                    (START.isoformat(), STOP.isoformat()),
                    version=self.version)
        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()),
                    version=self.version)
        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()),
                    version=self.version)
        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,
                                      version=self.version)
        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())

    def test_index_additional_query_parameters(self):
        req = fakes.HTTPRequest.blank('?start=%s&end=%s&additional=1' %
                (START.isoformat(), STOP.isoformat()),
                version=self.version)
        res = self.controller.index(req)
        self.assertIn('tenant_usages', res)

    def _test_index_duplicate_query_parameters_validation(self, params):
        for param, value in params.items():
            req = fakes.HTTPRequest.blank('?start=%s&%s=%s&%s=%s' %
                    (START.isoformat(), param, value, param, value),
                    version=self.version)

            res = self.controller.index(req)
            self.assertIn('tenant_usages', res)

    def test_index_duplicate_query_parameters_validation(self):
        params = {
            'start': START.isoformat(),
            'end': STOP.isoformat(),
            'detailed': 1
        }
        self._test_index_duplicate_query_parameters_validation(params)

    def test_show_additional_query_parameters(self):
        req = fakes.HTTPRequest.blank('?start=%s&end=%s&additional=1' %
                (START.isoformat(), STOP.isoformat()),
                version=self.version)
        res = self.controller.show(req, 1)
        self.assertIn('tenant_usage', res)

    def _test_show_duplicate_query_parameters_validation(self, params):
        for param, value in params.items():
            req = fakes.HTTPRequest.blank('?start=%s&%s=%s&%s=%s' %
                    (START.isoformat(), param, value, param, value),
                    version=self.version)

            res = self.controller.show(req, 1)
            self.assertIn('tenant_usage', res)

    def test_show_duplicate_query_parameters_validation(self):
        params = {
            'start': START.isoformat(),
            'end': STOP.isoformat()
        }
        self._test_show_duplicate_query_parameters_validation(params)


class SimpleTenantUsageTestV40(SimpleTenantUsageTestV21):
    version = '2.40'

    @mock.patch('nova.objects.InstanceList.get_active_by_window_joined',
                fake_get_active_by_window_joined)
    def test_next_links_show(self):
        self._test_verify_show(START, STOP,
                               limit=SERVERS * TENANTS)

    @mock.patch('nova.objects.InstanceList.get_active_by_window_joined',
                fake_get_active_by_window_joined)
    def test_next_links_index(self):
        self._test_verify_index(START, STOP,
                                limit=SERVERS * TENANTS)

    @mock.patch('nova.objects.InstanceList.get_active_by_window_joined',
                fake_get_active_by_window_joined)
    def test_index_duplicate_query_parameters_validation(self):
        params = {
            'start': START.isoformat(),
            'end': STOP.isoformat(),
            'detailed': 1,
            'limit': 1,
            'marker': 1
        }
        self._test_index_duplicate_query_parameters_validation(params)

    @mock.patch('nova.objects.InstanceList.get_active_by_window_joined',
                fake_get_active_by_window_joined)
    def test_show_duplicate_query_parameters_validation(self):
        params = {
            'start': START.isoformat(),
            'end': STOP.isoformat(),
            'limit': 1,
            'marker': 1
        }
        self._test_show_duplicate_query_parameters_validation(params)


class SimpleTenantUsageLimitsTestV21(test.TestCase):
    version = '2.1'

    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())
        return fakes.HTTPRequest.blank(url, version=self.version)

    def assert_limit(self, mock_get, limit):
        mock_get.assert_called_with(
            mock.ANY, mock.ANY, mock.ANY, mock.ANY, expected_attrs=['flavor'],
            limit=1000, marker=None)

    @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.assert_limit(mock_get, CONF.api.max_limit)

    @mock.patch('nova.objects.InstanceList.get_active_by_window_joined')
    def test_limit_defaults_to_conf_max_limit_index(self, mock_get):
        req = self._get_request('?start=%s&end=%s')
        self.controller.index(req)
        self.assert_limit(mock_get, CONF.api.max_limit)


class SimpleTenantUsageLimitsTestV240(SimpleTenantUsageLimitsTestV21):
    version = '2.40'

    def assert_limit_and_marker(self, mock_get, limit, marker):
        # NOTE(danms): Make sure we called at least once with the marker
        mock_get.assert_any_call(
            mock.ANY, mock.ANY, mock.ANY, mock.ANY, expected_attrs=['flavor'],
            limit=3, marker=marker)

    @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.assert_limit_and_marker(mock_get, 3, 'some-marker')

    @mock.patch('nova.objects.InstanceList.get_active_by_window_joined')
    def test_limit_and_marker_index(self, mock_get):
        req = self._get_request('?start=%s&end=%s&limit=3&marker=some-marker')
        self.controller.index(req)
        self.assert_limit_and_marker(mock_get, 3, 'some-marker')

    @mock.patch('nova.objects.InstanceList.get_active_by_window_joined')
    def test_marker_not_found_show(self, mock_get):
        mock_get.side_effect = exception.MarkerNotFound(marker='some-marker')
        req = self._get_request('?start=%s&end=%s&limit=3&marker=some-marker')
        self.assertRaises(
            webob.exc.HTTPBadRequest, self.controller.show, req, 1)

    @mock.patch('nova.objects.InstanceList.get_active_by_window_joined')
    def test_marker_not_found_index(self, mock_get):
        mock_get.side_effect = exception.MarkerNotFound(marker='some-marker')
        req = self._get_request('?start=%s&end=%s&limit=3&marker=some-marker')
        self.assertRaises(
            webob.exc.HTTPBadRequest, self.controller.index, req)

    def test_index_with_invalid_non_int_limit(self):
        req = self._get_request('?start=%s&end=%s&limit=-3')
        self.assertRaises(exception.ValidationError,
                          self.controller.index, req)

    def test_index_with_invalid_string_limit(self):
        req = self._get_request('?start=%s&end=%s&limit=abc')
        self.assertRaises(exception.ValidationError,
                          self.controller.index, req)

    def test_index_duplicate_query_with_invalid_string_limit(self):
        req = self._get_request('?start=%s&end=%s&limit=3&limit=abc')
        self.assertRaises(exception.ValidationError,
                          self.controller.index, req)

    def test_show_with_invalid_non_int_limit(self):
        req = self._get_request('?start=%s&end=%s&limit=-3')
        self.assertRaises(exception.ValidationError,
                          self.controller.show, req)

    def test_show_with_invalid_string_limit(self):
        req = self._get_request('?start=%s&end=%s&limit=abc')
        self.assertRaises(exception.ValidationError,
                          self.controller.show, req)

    def test_show_duplicate_query_with_invalid_string_limit(self):
        req = self._get_request('?start=%s&end=%s&limit=3&limit=abc')
        self.assertRaises(exception.ValidationError,
                          self.controller.show, req)


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")
