#    Copyright 2013 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.

from unittest import mock

from oslo_utils.fixture import uuidsentinel
from oslo_utils import timeutils
from oslo_versionedobjects import base as ovo_base
from oslo_versionedobjects import exception as ovo_exc

from nova.compute import manager as compute_manager
from nova import context
from nova.db.main import api as db
from nova import exception
from nova import objects
from nova.objects import aggregate
from nova.objects import service
from nova import test
from nova.tests import fixtures
from nova.tests.unit.objects import test_compute_node
from nova.tests.unit.objects import test_objects


NOW = timeutils.utcnow().replace(microsecond=0)


def _fake_service(**kwargs):
    fake_service = {
        'created_at': NOW,
        'updated_at': None,
        'deleted_at': None,
        'deleted': False,
        'id': 123,
        'uuid': uuidsentinel.service,
        'host': 'fake-host',
        'binary': 'nova-compute',
        'topic': 'fake-service-topic',
        'report_count': 1,
        'forced_down': False,
        'disabled': False,
        'disabled_reason': None,
        'last_seen_up': None,
        'version': service.SERVICE_VERSION,
        }
    fake_service.update(kwargs)
    return fake_service


fake_service = _fake_service()

OPTIONAL = ['availability_zone', 'compute_node']


class _TestServiceObject(object):
    def supported_hv_specs_comparator(self, expected, obj_val):
        obj_val = [inst.to_list() for inst in obj_val]
        self.assertJsonEqual(expected, obj_val)

    def pci_device_pools_comparator(self, expected, obj_val):
        obj_val = obj_val.obj_to_primitive()
        self.assertJsonEqual(expected, obj_val)

    def comparators(self):
        return {'stats': self.assertJsonEqual,
                'host_ip': self.assertJsonEqual,
                'supported_hv_specs': self.supported_hv_specs_comparator,
                'pci_device_pools': self.pci_device_pools_comparator}

    def subs(self):
        return {'supported_hv_specs': 'supported_instances',
                'pci_device_pools': 'pci_stats'}

    def _test_query(self, db_method, obj_method, *args, **kwargs):
        db_exception = kwargs.pop('db_exception', None)
        if db_exception:
            with mock.patch.object(db, db_method, side_effect=db_exception) \
                    as mock_db_method:
                obj = getattr(service.Service, obj_method)(self.context, *args,
                                                           **kwargs)
                self.assertIsNone(obj)
                mock_db_method.assert_called_once_with(self.context, *args,
                                                       **kwargs)
        else:
            with mock.patch.object(db, db_method, return_value=fake_service) \
                    as mock_db_method:
                obj = getattr(service.Service, obj_method)(self.context, *args,
                                                           **kwargs)
                self.compare_obj(obj, fake_service, allow_missing=OPTIONAL)
                mock_db_method.assert_called_once_with(self.context, *args,
                                                       **kwargs)

    def test_get_by_id(self):
        self._test_query('service_get', 'get_by_id', 123)

    def test_get_by_uuid(self):
        self._test_query('service_get_by_uuid', 'get_by_uuid',
                         uuidsentinel.service_uuid)

    def test_get_by_host_and_topic(self):
        self._test_query('service_get_by_host_and_topic',
                         'get_by_host_and_topic', 'fake-host', 'fake-topic')

    def test_get_by_host_and_binary(self):
        self._test_query('service_get_by_host_and_binary',
                         'get_by_host_and_binary', 'fake-host', 'fake-binary')

    def test_get_by_host_and_binary_raises(self):
        self._test_query('service_get_by_host_and_binary',
                         'get_by_host_and_binary', 'fake-host', 'fake-binary',
                         db_exception=exception.HostBinaryNotFound(
                             host='fake-host', binary='fake-binary'))

    def test_get_by_compute_host(self):
        self._test_query('service_get_by_compute_host', 'get_by_compute_host',
                         'fake-host')

    def test_get_by_args(self):
        self._test_query('service_get_by_host_and_binary', 'get_by_args',
                         'fake-host', 'fake-binary')

    @mock.patch.object(db, 'service_create', return_value=fake_service)
    def test_create(self, mock_service_create):
        service_obj = service.Service(context=self.context)
        service_obj.host = 'fake-host'
        service_obj.uuid = uuidsentinel.service2
        service_obj.create()
        self.assertEqual(fake_service['id'], service_obj.id)
        self.assertEqual(service.SERVICE_VERSION, service_obj.version)
        mock_service_create.assert_called_once_with(
                       self.context, {'host': 'fake-host',
                                      'uuid': uuidsentinel.service2,
                                      'version': fake_service['version']})

    @mock.patch('nova.objects.service.uuidutils.generate_uuid',
                return_value=uuidsentinel.service3)
    @mock.patch.object(db, 'service_create', return_value=fake_service)
    def test_create_without_uuid_generates_one(
            self, mock_service_create, generate_uuid):
        service_obj = service.Service(context=self.context)
        service_obj.create()
        create_args = mock_service_create.call_args[0][1]
        self.assertEqual(generate_uuid.return_value, create_args['uuid'])

    @mock.patch.object(db, 'service_create', return_value=fake_service)
    def test_recreate_fails(self, mock_service_create):
        service_obj = service.Service(context=self.context)
        service_obj.host = 'fake-host'
        service_obj.create()
        self.assertRaises(exception.ObjectActionError, service_obj.create)
        mock_service_create(self.context, {'host': 'fake-host',
                                         'version': fake_service['version']})

    @mock.patch('nova.objects.Service._send_notification')
    @mock.patch.object(db, 'service_update', return_value=fake_service)
    def test_save(self, mock_service_update, mock_notify):
        service_obj = service.Service(context=self.context)
        service_obj.id = 123
        service_obj.host = 'fake-host'
        service_obj.save()
        self.assertEqual(service.SERVICE_VERSION, service_obj.version)
        mock_service_update.assert_called_once_with(
            self.context, 123, {'host': 'fake-host',
                                'version': fake_service['version']})

    @mock.patch.object(db, 'service_create',
                       return_value=fake_service)
    def test_set_id_failure(self, db_mock):
        service_obj = service.Service(context=self.context,
                                      binary='nova-compute')
        service_obj.create()
        self.assertRaises(ovo_exc.ReadOnlyFieldError, setattr,
                          service_obj, 'id', 124)

    @mock.patch('nova.objects.Service._send_notification')
    @mock.patch.object(db, 'service_destroy')
    def _test_destroy(self, mock_service_destroy, mock_notify):
        service_obj = service.Service(context=self.context)
        service_obj.id = 123
        service_obj.destroy()
        mock_service_destroy.assert_called_once_with(self.context, 123)

    def test_destroy(self):
        # The test harness needs db.service_destroy to work,
        # so avoid leaving it broken here after we're done
        orig_service_destroy = db.service_destroy
        try:
            self._test_destroy()
        finally:
            db.service_destroy = orig_service_destroy

    @mock.patch.object(db, 'service_get_all_by_topic',
                       return_value=[fake_service])
    def test_get_by_topic(self, mock_service_get):
        services = service.ServiceList.get_by_topic(self.context, 'fake-topic')
        self.assertEqual(1, len(services))
        self.compare_obj(services[0], fake_service, allow_missing=OPTIONAL)
        mock_service_get.assert_called_once_with(self.context, 'fake-topic')

    @mock.patch('nova.db.main.api.service_get_all_by_binary')
    def test_get_by_binary(self, mock_get):
        mock_get.return_value = [fake_service]
        services = service.ServiceList.get_by_binary(self.context,
                                                     'fake-binary')
        self.assertEqual(1, len(services))
        mock_get.assert_called_once_with(self.context,
                                         'fake-binary',
                                         include_disabled=False)

    @mock.patch('nova.db.main.api.service_get_all_by_binary')
    def test_get_by_binary_disabled(self, mock_get):
        mock_get.return_value = [_fake_service(disabled=True)]
        services = service.ServiceList.get_by_binary(self.context,
                                                     'fake-binary',
                                                     include_disabled=True)
        self.assertEqual(1, len(services))
        mock_get.assert_called_once_with(self.context,
                                         'fake-binary',
                                         include_disabled=True)

    @mock.patch('nova.db.main.api.service_get_all_by_binary')
    def test_get_by_binary_both(self, mock_get):
        mock_get.return_value = [_fake_service(),
                                 _fake_service(disabled=True)]
        services = service.ServiceList.get_by_binary(self.context,
                                                     'fake-binary',
                                                     include_disabled=True)
        self.assertEqual(2, len(services))
        mock_get.assert_called_once_with(self.context,
                                         'fake-binary',
                                         include_disabled=True)

    @mock.patch.object(db, 'service_get_all_by_host',
                       return_value=[fake_service])
    def test_get_by_host(self, mock_service_get):
        services = service.ServiceList.get_by_host(self.context, 'fake-host')
        self.assertEqual(1, len(services))
        self.compare_obj(services[0], fake_service, allow_missing=OPTIONAL)
        mock_service_get.assert_called_once_with(self.context, 'fake-host')

    @mock.patch.object(db, 'service_get_all', return_value=[fake_service])
    def test_get_all(self, mock_get_all):
        services = service.ServiceList.get_all(self.context, disabled=False)
        self.assertEqual(1, len(services))
        self.compare_obj(services[0], fake_service, allow_missing=OPTIONAL)
        mock_get_all.assert_called_once_with(self.context, disabled=False)

    @mock.patch.object(db, 'service_get_all')
    @mock.patch.object(aggregate.AggregateList, 'get_by_metadata_key')
    def test_get_all_with_az(self, mock_get_by_key, mock_get_all):
        agg = aggregate.Aggregate(context=self.context)
        agg.name = 'foo'
        agg.metadata = {'availability_zone': 'test-az'}
        agg.create()
        agg.hosts = [fake_service['host']]
        mock_get_by_key.return_value = [agg]
        mock_get_all.return_value = [dict(fake_service, topic='compute')]
        services = service.ServiceList.get_all(self.context, set_zones=True)
        self.assertEqual(1, len(services))
        self.assertEqual('test-az', services[0].availability_zone)
        mock_get_all.assert_called_once_with(self.context, disabled=None)
        mock_get_by_key.assert_called_once_with(self.context,
                         'availability_zone', hosts=set(agg.hosts))

    @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host')
    def test_compute_node(self, mock_get):
        fake_compute_node = objects.ComputeNode._from_db_object(
            self.context, objects.ComputeNode(),
            test_compute_node.fake_compute_node)
        mock_get.return_value = [fake_compute_node]
        service_obj = service.Service(id=123, host="fake-host",
                                      binary="nova-compute")
        service_obj._context = self.context
        self.assertEqual(service_obj.compute_node,
                         fake_compute_node)
        # Make sure it doesn't re-fetch this
        service_obj.compute_node
        mock_get.assert_called_once_with(self.context, 'fake-host')

    @mock.patch.object(db, 'service_get_all_computes_by_hv_type')
    def test_get_all_computes_by_hv_type(self, mock_get_all):
        mock_get_all.return_value = [fake_service]
        services = service.ServiceList.get_all_computes_by_hv_type(
            self.context, 'hv-type')
        self.assertEqual(1, len(services))
        self.compare_obj(services[0], fake_service, allow_missing=OPTIONAL)
        mock_get_all.assert_called_once_with(self.context, 'hv-type',
                                             include_disabled=False)

    def test_load_when_orphaned(self):
        service_obj = service.Service()
        service_obj.id = 123
        self.assertRaises(exception.OrphanedObjectError,
                          getattr, service_obj, 'compute_node')

    @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host')
    def test_obj_make_compatible_for_compute_node(self, get_all_by_host):
        service_obj = objects.Service(context=self.context)
        fake_service_dict = fake_service.copy()
        fake_compute_obj = objects.ComputeNode(host=fake_service['host'],
                                               service_id=fake_service['id'])
        get_all_by_host.return_value = [fake_compute_obj]

        versions = ovo_base.obj_tree_get_versions('Service')
        versions['ComputeNode'] = '1.10'
        service_obj.obj_make_compatible_from_manifest(fake_service_dict, '1.9',
                                                      versions)
        self.assertEqual(
            fake_compute_obj.obj_to_primitive(target_version='1.10',
                                              version_manifest=versions),
            fake_service_dict['compute_node'])

    @mock.patch('nova.db.main.api.service_get_minimum_version')
    def test_get_minimum_version_none(self, mock_get):
        mock_get.return_value = None
        self.assertEqual(0,
                         objects.Service.get_minimum_version(self.context,
                                                             'nova-compute'))
        mock_get.assert_called_once_with(self.context, ['nova-compute'])

    @mock.patch('nova.db.main.api.service_get_minimum_version')
    def test_get_minimum_version(self, mock_get):
        mock_get.return_value = {'nova-compute': 123}
        self.assertEqual(123,
                         objects.Service.get_minimum_version(self.context,
                                                             'nova-compute'))
        mock_get.assert_called_once_with(self.context, ['nova-compute'])

    @mock.patch('nova.db.main.api.service_get_minimum_version')
    @mock.patch('nova.objects.service.LOG')
    def test_get_minimum_version_checks_binary(self, mock_log, mock_get):
        mock_get.return_value = None
        self.assertEqual(0,
                         objects.Service.get_minimum_version(self.context,
                                                             'nova-compute'))
        self.assertFalse(mock_log.warning.called)
        self.assertRaises(exception.ObjectActionError,
                         objects.Service.get_minimum_version,
                          self.context,
                          'compute')
        self.assertTrue(mock_log.warning.called)

    @mock.patch('nova.db.main.api.service_get_minimum_version')
    def test_get_minimum_version_with_caching(self, mock_get):
        objects.Service.enable_min_version_cache()
        mock_get.return_value = {'nova-compute': 123}
        self.assertEqual(123,
                         objects.Service.get_minimum_version(self.context,
                                                             'nova-compute'))
        self.assertEqual({"nova-compute": 123},
                         objects.Service._MIN_VERSION_CACHE)
        self.assertEqual(123,
                         objects.Service.get_minimum_version(self.context,
                                                             'nova-compute'))
        mock_get.assert_called_once_with(self.context, ['nova-compute'])
        objects.Service._SERVICE_VERSION_CACHING = False
        objects.Service.clear_min_version_cache()

    @mock.patch('nova.db.main.api.service_get_minimum_version')
    def test_get_min_version_multiple_with_old(self, mock_gmv):
        mock_gmv.return_value = {'nova-api': None,
                                 'nova-scheduler': 2,
                                 'nova-conductor': 3}

        binaries = ['nova-api', 'nova-api', 'nova-conductor',
                    'nova-conductor', 'nova-api']
        minimum = objects.Service.get_minimum_version_multi(self.context,
                                                            binaries)
        self.assertEqual(0, minimum)

    @mock.patch('nova.db.main.api.service_get_minimum_version')
    def test_get_min_version_multiple(self, mock_gmv):
        mock_gmv.return_value = {'nova-api': 1,
                                 'nova-scheduler': 2,
                                 'nova-conductor': 3}

        binaries = ['nova-api', 'nova-api', 'nova-conductor',
                    'nova-conductor', 'nova-api']
        minimum = objects.Service.get_minimum_version_multi(self.context,
                                                            binaries)
        self.assertEqual(1, minimum)

    @mock.patch('nova.objects.Service._send_notification')
    @mock.patch('nova.db.main.api.service_get_minimum_version',
                return_value={'nova-compute': 2})
    def test_create_above_minimum(self, mock_get, mock_notify):
        with mock.patch('nova.objects.service.SERVICE_VERSION',
                        new=3):
            objects.Service(context=self.context,
                            binary='nova-compute').create()

    @mock.patch('nova.objects.Service._send_notification')
    @mock.patch('nova.db.main.api.service_get_minimum_version',
                return_value={'nova-compute': 2})
    def test_create_equal_to_minimum(self, mock_get, mock_notify):
        with mock.patch('nova.objects.service.SERVICE_VERSION',
                        new=2):
            objects.Service(context=self.context,
                            binary='nova-compute').create()

    @mock.patch('nova.db.main.api.service_get_minimum_version',
                return_value={'nova-compute': 2})
    def test_create_below_minimum(self, mock_get):
        with mock.patch('nova.objects.service.SERVICE_VERSION',
                        new=1):
            self.assertRaises(exception.ServiceTooOld,
                              objects.Service(context=self.context,
                                              binary='nova-compute',
                                              ).create)

    @mock.patch('nova.objects.base.NovaObject'
                '.obj_make_compatible_from_manifest', new=mock.Mock())
    def test_obj_make_compatible_from_manifest_strips_uuid(self):
        s = service.Service()
        primitive = {'uuid': uuidsentinel.service}
        s.obj_make_compatible_from_manifest(primitive, '1.20', mock.Mock())
        self.assertNotIn('uuid', primitive)


class TestServiceObject(test_objects._LocalTest,
                        _TestServiceObject):
    pass


class TestRemoteServiceObject(test_objects._RemoteTest,
                              _TestServiceObject):
    pass


class TestServiceVersion(test.NoDBTestCase):
    def setUp(self):
        self.ctxt = context.get_admin_context()
        super(TestServiceVersion, self).setUp()

    def _collect_things(self):
        data = {
            'compute_rpc': compute_manager.ComputeManager.target.version,
        }
        return data

    def test_version(self):
        calculated = self._collect_things()
        self.assertEqual(
            len(service.SERVICE_VERSION_HISTORY), service.SERVICE_VERSION + 1,
            'Service version %i has no history. Please update '
            'nova.objects.service.SERVICE_VERSION_HISTORY '
            'and add %s to it' % (service.SERVICE_VERSION, repr(calculated)))
        current = service.SERVICE_VERSION_HISTORY[service.SERVICE_VERSION]
        self.assertEqual(
            current, calculated,
            'Changes detected that require a SERVICE_VERSION change. Please '
            'increment nova.objects.service.SERVICE_VERSION, and make sure it '
            'is equal to nova.compute.manager.ComputeManager.target.version.')

    def test_version_in_init(self):
        self.assertRaises(exception.ObjectActionError,
                          objects.Service,
                          version=123)

    def test_version_set_on_init(self):
        self.assertEqual(service.SERVICE_VERSION,
                         objects.Service().version)

    def test_version_loaded_from_db(self):
        fake_version = fake_service['version'] + 1
        fake_different_service = dict(fake_service)
        fake_different_service['version'] = fake_version
        obj = objects.Service()
        obj._from_db_object(self.ctxt, obj, fake_different_service)
        self.assertEqual(fake_version, obj.version)


class TestServiceVersionCells(test.TestCase):

    def setUp(self):
        self.context = context.get_admin_context()
        super(TestServiceVersionCells, self).setUp()

    def _setup_cells(self):
        # NOTE(danms): Override the base class's cell setup so we can have two
        self.cells = fixtures.CellDatabases()
        self.cells.add_cell_database(uuidsentinel.cell1, default=True)
        self.cells.add_cell_database(uuidsentinel.cell2)
        self.useFixture(self.cells)

        cm = objects.CellMapping(context=self.context,
                                 uuid=uuidsentinel.cell1,
                                 name='cell1',
                                 transport_url='fake://nowhere/',
                                 database_connection=uuidsentinel.cell1)
        cm.create()
        cm = objects.CellMapping(context=self.context,
                                 uuid=uuidsentinel.cell2,
                                 name='cell2',
                                 transport_url='fake://nowhere/',
                                 database_connection=uuidsentinel.cell2)
        cm.create()

    def _create_services(self, *versions):
        cells = objects.CellMappingList.get_all(self.context)
        index = 0
        for version in versions:
            service = objects.Service(context=self.context,
                                      binary='nova-compute')
            service.version = version
            cell = cells[index % len(cells)]
            with context.target_cell(self.context, cell):
                service.create()
            index += 1

    @mock.patch('nova.objects.Service._send_notification')
    @mock.patch('nova.objects.Service._check_minimum_version')
    def test_version_all_cells(self, mock_check, mock_notify):
        self._create_services(16, 16, 13, 16)
        self.assertEqual(13, service.get_minimum_version_all_cells(
            self.context, ['nova-compute']))

    @mock.patch('nova.objects.service.LOG')
    def test_get_minimum_version_checks_binary(self, mock_log):
        ex = self.assertRaises(exception.ObjectActionError,
                               service.get_minimum_version_all_cells,
                               self.context, ['compute'])
        self.assertIn('Invalid binary prefix', str(ex))
        self.assertTrue(mock_log.warning.called)

    @mock.patch('nova.context.scatter_gather_all_cells')
    def test_version_all_cells_with_fail(self, mock_scatter):
        mock_scatter.return_value = {
            'foo': {'nova-compute': 13},
            'bar': exception.ServiceNotFound(service_id='fake'),
        }
        self.assertEqual(13, service.get_minimum_version_all_cells(
            self.context, ['nova-compute']))
        self.assertRaises(exception.CellTimeout,
                          service.get_minimum_version_all_cells,
                          self.context, ['nova-compute'],
                          require_all=True)

    @mock.patch('nova.context.scatter_gather_all_cells')
    def test_version_all_cells_with_timeout(self, mock_scatter):
        mock_scatter.return_value = {
            'foo': {'nova-compute': 13},
            'bar': context.did_not_respond_sentinel,
        }
        self.assertEqual(13, service.get_minimum_version_all_cells(
            self.context, ['nova-compute']))
        self.assertRaises(exception.CellTimeout,
                          service.get_minimum_version_all_cells,
                          self.context, ['nova-compute'],
                          require_all=True)

    @mock.patch('nova.context.scatter_gather_all_cells')
    def test_version_all_cells_exclude_zero_service(self, mock_scatter):
        mock_scatter.return_value = {
            'foo': {'nova-compute': 13},
            'bar': {'nova-compute': 0},
        }
        self.assertEqual(13, service.get_minimum_version_all_cells(
            self.context, ['nova-compute']))
