# Copyright 2014 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 mock
from neutron_lib import constants as n_const
from neutron_lib import context as n_ctx
from neutron_lib import exceptions as n_exc
from neutron_lib.plugins import constants as plugin_constants
from neutron_lib.plugins import directory
from novaclient import api_versions
from novaclient import exceptions as nova_exceptions
from oslo_config import cfg
from oslo_utils import uuidutils
from sqlalchemy.orm import attributes as sql_attr

from neutron.notifiers import nova
from neutron.objects import ports as port_obj
from neutron.tests import base

DEVICE_OWNER_COMPUTE = n_const.DEVICE_OWNER_COMPUTE_PREFIX + 'fake'
DEVICE_OWNER_BAREMETAL = n_const.DEVICE_OWNER_BAREMETAL_PREFIX + 'fake'


class TestNovaNotify(base.BaseTestCase):
    def setUp(self, plugin=None):
        super(TestNovaNotify, self).setUp()
        self.ctx = n_ctx.get_admin_context()
        self.port_uuid = uuidutils.generate_uuid()

        class FakePlugin(object):
            def get_port(self, context, port_id):
                device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
                return {'device_id': device_id,
                        'device_owner': DEVICE_OWNER_COMPUTE,
                        'id': port_id}

        self.nova_notifier = nova.Notifier()
        directory.add_plugin(plugin_constants.CORE, FakePlugin())

    def test_notify_port_status_all_values(self):
        states = [n_const.PORT_STATUS_ACTIVE, n_const.PORT_STATUS_DOWN,
                  n_const.PORT_STATUS_ERROR, n_const.PORT_STATUS_BUILD,
                  sql_attr.NO_VALUE]
        device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
        # test all combinations
        for previous_port_status in states:
            for current_port_status in states:

                port = port_obj.Port(self.ctx, id=self.port_uuid,
                                     device_id=device_id,
                                     device_owner=DEVICE_OWNER_COMPUTE,
                                     status=current_port_status)
                self._record_port_status_changed_helper(current_port_status,
                                                        previous_port_status,
                                                        port)

    def test_port_without_uuid_device_id_no_notify(self):
        port = port_obj.Port(self.ctx, id=self.port_uuid,
                             device_id='compute_probe:',
                             device_owner=DEVICE_OWNER_COMPUTE,
                             status=n_const.PORT_STATUS_ACTIVE)
        self._record_port_status_changed_helper(n_const.PORT_STATUS_ACTIVE,
                                                sql_attr.NO_VALUE,
                                                port)

    def test_port_without_device_owner_no_notify(self):
        device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
        port = port_obj.Port(self.ctx, id=self.port_uuid, device_id=device_id,
                             device_owner="",
                             status=n_const.PORT_STATUS_ACTIVE)
        self._record_port_status_changed_helper(n_const.PORT_STATUS_ACTIVE,
                                                sql_attr.NO_VALUE,
                                                port)

    def test_port_without_device_id_no_notify(self):
        port = port_obj.Port(self.ctx, id=self.port_uuid, device_id="",
                             device_owner=n_const.DEVICE_OWNER_DHCP,
                             status=n_const.PORT_STATUS_ACTIVE)
        self._record_port_status_changed_helper(n_const.PORT_STATUS_ACTIVE,
                                                sql_attr.NO_VALUE,
                                                port)

    def test_non_compute_instances_no_notify(self):
        device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
        port = port_obj.Port(self.ctx, id=self.port_uuid, device_id=device_id,
                             device_owner=n_const.DEVICE_OWNER_DHCP,
                             status=n_const.PORT_STATUS_ACTIVE)
        self._record_port_status_changed_helper(n_const.PORT_STATUS_ACTIVE,
                                                sql_attr.NO_VALUE,
                                                port)

    def _record_port_status_changed_helper(self, current_port_status,
                                           previous_port_status, port):

        if not (port.device_id and port.id and port.device_owner and
                port.device_owner.startswith(
                    n_const.DEVICE_OWNER_COMPUTE_PREFIX) and
                uuidutils.is_uuid_like(port.device_id)):
            return

        if (previous_port_status == n_const.PORT_STATUS_ACTIVE and
                current_port_status == n_const.PORT_STATUS_DOWN):
            event_name = nova.VIF_UNPLUGGED

        elif (previous_port_status in [sql_attr.NO_VALUE,
                                       n_const.PORT_STATUS_DOWN,
                                       n_const.PORT_STATUS_BUILD]
              and current_port_status in [n_const.PORT_STATUS_ACTIVE,
                                          n_const.PORT_STATUS_ERROR]):
            event_name = nova.VIF_PLUGGED

        else:
            return

        status = nova.NEUTRON_NOVA_EVENT_STATUS_MAP.get(current_port_status)
        self.nova_notifier.record_port_status_changed(port,
                                                      current_port_status,
                                                      previous_port_status,
                                                      None)

        event = {'server_uuid': port.device_id, 'status': status,
                 'name': event_name, 'tag': self.port_uuid}
        self.assertEqual(event, port._notify_event)

    def test_update_fixed_ip_changed(self):
        device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
        returned_obj = {'port':
                        {'device_owner': DEVICE_OWNER_COMPUTE,
                         'id': u'bee50827-bcee-4cc8-91c1-a27b0ce54222',
                         'device_id': device_id}}

        expected_event = {'server_uuid': device_id,
                          'name': 'network-changed',
                          'tag': returned_obj['port']['id']}
        event = self.nova_notifier.create_port_changed_event('update_port',
                                                             {}, returned_obj)
        self.assertEqual(event, expected_event)

    def test_create_floatingip_notify(self):
        device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
        returned_obj = {'floatingip':
                        {'port_id': u'bee50827-bcee-4cc8-91c1-a27b0ce54222'}}

        expected_event = {'server_uuid': device_id,
                          'name': 'network-changed',
                          'tag': returned_obj['floatingip']['port_id']}
        event = self.nova_notifier.create_port_changed_event(
            'create_floatingip', {}, returned_obj)
        self.assertEqual(event, expected_event)

    def test_create_floatingip_no_port_id_no_notify(self):
        returned_obj = {'floatingip':
                        {'port_id': None}}

        event = self.nova_notifier.create_port_changed_event(
            'create_floatingip', {}, returned_obj)
        self.assertFalse(event, None)

    def test_delete_floatingip_notify(self):
        device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
        returned_obj = {'floatingip':
                        {'port_id': u'bee50827-bcee-4cc8-91c1-a27b0ce54222'}}

        expected_event = {'server_uuid': device_id,
                          'name': 'network-changed',
                          'tag': returned_obj['floatingip']['port_id']}
        event = self.nova_notifier.create_port_changed_event(
            'delete_floatingip', {}, returned_obj)
        self.assertEqual(expected_event, event)

    def test_delete_floatingip_deleted_port_no_notify(self):
        port_id = 'bee50827-bcee-4cc8-91c1-a27b0ce54222'
        with mock.patch.object(
            directory.get_plugin(), 'get_port',
            side_effect=n_exc.PortNotFound(port_id=port_id)):
            returned_obj = {'floatingip':
                            {'port_id': port_id}}
            event = self.nova_notifier.create_port_changed_event(
                'delete_floatingip', {}, returned_obj)
            self.assertIsNone(event)

    def test_delete_floatingip_no_port_id_no_notify(self):
        returned_obj = {'floatingip':
                        {'port_id': None}}

        event = self.nova_notifier.create_port_changed_event(
            'delete_floatingip', {}, returned_obj)
        self.assertIsNone(event)

    def test_associate_floatingip_notify(self):
        device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
        returned_obj = {'floatingip':
                        {'port_id': u'5a39def4-3d3f-473d-9ff4-8e90064b9cc1'}}
        original_obj = {'port_id': None}

        expected_event = {'server_uuid': device_id,
                          'name': 'network-changed',
                          'tag': returned_obj['floatingip']['port_id']}
        event = self.nova_notifier.create_port_changed_event(
            'update_floatingip', original_obj, returned_obj)
        self.assertEqual(expected_event, event)

    def test_disassociate_floatingip_notify(self):
        device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
        returned_obj = {'floatingip': {'port_id': None}}
        original_obj = {'port_id': '5a39def4-3d3f-473d-9ff4-8e90064b9cc1'}

        expected_event = {'server_uuid': device_id,
                          'name': 'network-changed',
                          'tag': original_obj['port_id']}

        event = self.nova_notifier.create_port_changed_event(
            'update_floatingip', original_obj, returned_obj)
        self.assertEqual(expected_event, event)

    def test_no_notification_notify_nova_on_port_data_changes_false(self):
        cfg.CONF.set_override('notify_nova_on_port_data_changes', False)

        with mock.patch.object(self.nova_notifier,
                               'send_events') as send_events:
            self.nova_notifier.send_network_change('update_floatingip',
                                                   {}, {})
            self.assertFalse(send_events.called, False)

    def test_nova_send_events_returns_bad_list(self):
        with mock.patch.object(
            self.nova_notifier.nclient.server_external_events,
                'create') as nclient_create:
            nclient_create.return_value = 'i am a string!'
            self.nova_notifier.send_events([])

    def test_nova_send_event_rasies_404(self):
        with mock.patch.object(
            self.nova_notifier.nclient.server_external_events,
                'create') as nclient_create:
            nclient_create.side_effect = nova_exceptions.NotFound
            self.nova_notifier.send_events([])

    def test_nova_send_events_raises(self):
        with mock.patch.object(
            self.nova_notifier.nclient.server_external_events,
                'create') as nclient_create:
            nclient_create.side_effect = Exception
            self.nova_notifier.send_events([])

    def test_nova_send_events_returns_non_200(self):
        device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
        with mock.patch.object(
            self.nova_notifier.nclient.server_external_events,
                'create') as nclient_create:
            nclient_create.return_value = [{'code': 404,
                                            'name': 'network-changed',
                                            'server_uuid': device_id}]
            self.nova_notifier.send_events(
                [{'name': 'network-changed', 'server_uuid': device_id}])

    def test_nova_send_events_return_200(self):
        device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
        with mock.patch.object(
            self.nova_notifier.nclient.server_external_events,
                'create') as nclient_create:
            nclient_create.return_value = [{'code': 200,
                                            'name': 'network-changed',
                                            'server_uuid': device_id}]
            self.nova_notifier.send_events(
                [{'name': 'network-changed', 'server_uuid': device_id}])

    def test_nova_send_events_multiple(self):
        device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
        with mock.patch.object(
            self.nova_notifier.nclient.server_external_events,
                'create') as nclient_create:
            nclient_create.return_value = [{'code': 200,
                                            'name': 'network-changed',
                                            'server_uuid': device_id},
                                           {'code': 200,
                                            'name': 'network-changed',
                                            'server_uuid': device_id}]
            self.nova_notifier.send_events([
                {'name': 'network-changed', 'server_uuid': device_id},
                {'name': 'network-changed', 'server_uuid': device_id}])

    def test_reassociate_floatingip_without_disassociate_event(self):
        returned_obj = {'floatingip':
                        {'port_id': 'f5348a16-609a-4971-b0f0-4b8def5235fb'}}
        original_obj = {'port_id': '5a39def4-3d3f-473d-9ff4-8e90064b9cc1'}
        self.nova_notifier._waiting_to_send = True
        self.nova_notifier.send_network_change(
            'update_floatingip', original_obj, returned_obj)
        self.assertEqual(
            2, len(self.nova_notifier.batch_notifier.pending_events))

        returned_obj_non = {'floatingip': {'port_id': None}}
        event_dis = self.nova_notifier.create_port_changed_event(
            'update_floatingip', original_obj, returned_obj_non)
        event_assoc = self.nova_notifier.create_port_changed_event(
            'update_floatingip', original_obj, returned_obj)
        self.assertEqual(
            self.nova_notifier.batch_notifier.pending_events[0], event_dis)
        self.assertEqual(
            self.nova_notifier.batch_notifier.pending_events[1], event_assoc)

    def test_delete_port_notify(self):
        device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
        port_id = 'bee50827-bcee-4cc8-91c1-a27b0ce54222'
        returned_obj = {'port':
                        {'device_owner': DEVICE_OWNER_COMPUTE,
                         'id': port_id,
                         'device_id': device_id}}

        expected_event = {'server_uuid': device_id,
                          'name': nova.VIF_DELETED,
                          'tag': port_id}
        event = self.nova_notifier.create_port_changed_event('delete_port',
                                                             {}, returned_obj)
        self.assertEqual(expected_event, event)

    @mock.patch('novaclient.client.Client')
    def test_endpoint_types(self, mock_client):
        nova.Notifier()
        mock_client.assert_called_once_with(
                                api_versions.APIVersion(nova.NOVA_API_VERSION),
                                session=mock.ANY,
                                region_name=cfg.CONF.nova.region_name,
                                endpoint_type='public',
                                extensions=mock.ANY)

        mock_client.reset_mock()
        cfg.CONF.set_override('endpoint_type', 'internal', 'nova')
        nova.Notifier()
        mock_client.assert_called_once_with(
                                api_versions.APIVersion(nova.NOVA_API_VERSION),
                                session=mock.ANY,
                                region_name=cfg.CONF.nova.region_name,
                                endpoint_type='internal',
                                extensions=mock.ANY)

    def test_notify_port_active_direct(self):
        device_id = '32102d7b-1cf4-404d-b50a-97aae1f55f87'
        port_id = 'bee50827-bcee-4cc8-91c1-a27b0ce54222'
        port = port_obj.Port(self.ctx, id=port_id, device_id=device_id,
                             device_owner=DEVICE_OWNER_COMPUTE)
        expected_event = {'server_uuid': device_id,
                          'name': nova.VIF_PLUGGED,
                          'status': 'completed',
                          'tag': port_id}
        self.nova_notifier.notify_port_active_direct(port)

        self.assertEqual(
            1, len(self.nova_notifier.batch_notifier.pending_events))
        self.assertEqual(expected_event,
                         self.nova_notifier.batch_notifier.pending_events[0])
