# Copyright 2016 Red Hat, Inc.
#
#    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 networking_ovn.common import constants as ovn_const
from networking_ovn import ovn_db_sync
from networking_ovn.tests.unit.ml2 import test_mech_driver


@mock.patch('networking_ovn.l3.l3_ovn.OVNL3RouterPlugin._sb_ovn', mock.Mock())
class TestOvnNbSyncML2(test_mech_driver.OVNMechanismDriverTestCase):

    l3_plugin = 'networking_ovn.l3.l3_ovn.OVNL3RouterPlugin'

    def setUp(self):
        super(TestOvnNbSyncML2, self).setUp()

        self.subnet = {'cidr': '10.0.0.0/24',
                       'id': 'subnet1',
                       'subnetpool_id': None,
                       'name': 'private-subnet',
                       'enable_dhcp': True,
                       'network_id': 'n1',
                       'tenant_id': 'tenant1',
                       'gateway_ip': '10.0.0.1',
                       'ip_version': 4,
                       'shared': False}
        self.matches = ["", "", "", ""]

        self.networks = [{'id': 'n1',
                          'mtu': 1450,
                          'provider:physical_network': 'physnet1',
                          'provider:segmentation_id': 1000},
                         {'id': 'n2',
                          'mtu': 1450},
                         {'id': 'n4',
                          'mtu': 1450,
                          'provider:physical_network': 'physnet2'}]

        self.subnets = [{'id': 'n1-s1',
                         'network_id': 'n1',
                         'enable_dhcp': True,
                         'cidr': '10.0.0.0/24',
                         'tenant_id': 'tenant1',
                         'gateway_ip': '10.0.0.1',
                         'dns_nameservers': [],
                         'host_routes': [],
                         'ip_version': 4},
                        {'id': 'n1-s2',
                         'network_id': 'n1',
                         'enable_dhcp': True,
                         'cidr': 'fd79:e1c:a55::/64',
                         'tenant_id': 'tenant1',
                         'gateway_ip': 'fd79:e1c:a55::1',
                         'dns_nameservers': [],
                         'host_routes': [],
                         'ip_version': 6},
                        {'id': 'n2',
                         'network_id': 'n2',
                         'enable_dhcp': True,
                         'cidr': '20.0.0.0/24',
                         'tenant_id': 'tenant1',
                         'gateway_ip': '20.0.0.1',
                         'dns_nameservers': [],
                         'host_routes': [],
                         'ip_version': 4}]

        self.security_groups = [
            {'id': 'sg1', 'tenant_id': 'tenant1',
             'security_group_rules': [{'remote_group_id': None,
                                       'direction': 'ingress',
                                       'remote_ip_prefix': '0.0.0.0/0',
                                       'protocol': 'tcp',
                                       'ethertype': 'IPv4',
                                       'tenant_id': 'tenant1',
                                       'port_range_max': 65535,
                                       'port_range_min': 1,
                                       'id': 'ruleid1',
                                       'security_group_id': 'sg1'}],
             'name': 'all-tcp'},
            {'id': 'sg2', 'tenant_id': 'tenant1',
             'security_group_rules': [{'remote_group_id': 'sg2',
                                       'direction': 'egress',
                                       'remote_ip_prefix': '0.0.0.0/0',
                                       'protocol': 'tcp',
                                       'ethertype': 'IPv4',
                                       'tenant_id': 'tenant1',
                                       'port_range_max': 65535,
                                       'port_range_min': 1,
                                       'id': 'ruleid1',
                                       'security_group_id': 'sg2'}],
             'name': 'all-tcpe'}]

        self.ports = [
            {'id': 'p1n1',
             'device_owner': 'compute:None',
             'fixed_ips':
                 [{'subnet_id': 'b142f5e3-d434-4740-8e88-75e8e5322a40',
                   'ip_address': '10.0.0.4'},
                  {'subnet_id': 'subnet1',
                   'ip_address': 'fd79:e1c:a55::816:eff:eff:ff2'}],
             'security_groups': ['sg1'],
             'network_id': 'n1'},
            {'id': 'p2n1',
             'device_owner': 'compute:None',
             'fixed_ips':
                 [{'subnet_id': 'b142f5e3-d434-4740-8e88-75e8e5322a40',
                   'ip_address': '10.0.0.4'},
                  {'subnet_id': 'subnet1',
                   'ip_address': 'fd79:e1c:a55::816:eff:eff:ff2'}],
             'security_groups': ['sg2'],
             'network_id': 'n1',
             'extra_dhcp_opts': [{'ip_version': 6,
                                  'opt_name': 'domain-search',
                                  'opt_value': 'foo-domain'}]},
            {'id': 'p1n2',
             'device_owner': 'compute:None',
             'fixed_ips':
                 [{'subnet_id': 'b142f5e3-d434-4740-8e88-75e8e5322a40',
                   'ip_address': '10.0.0.4'},
                  {'subnet_id': 'subnet1',
                   'ip_address': 'fd79:e1c:a55::816:eff:eff:ff2'}],
             'security_groups': ['sg1'],
             'network_id': 'n2',
             'extra_dhcp_opts': [{'ip_version': 4,
                                  'opt_name': 'tftp-server',
                                  'opt_value': '20.0.0.20'},
                                 {'ip_version': 4,
                                  'opt_name': 'dns-server',
                                  'opt_value': '8.8.8.8'},
                                 {'ip_version': 6,
                                  'opt_name': 'domain-search',
                                  'opt_value': 'foo-domain'}]},
            {'id': 'p2n2',
             'device_owner': 'compute:None',
             'fixed_ips':
                 [{'subnet_id': 'b142f5e3-d434-4740-8e88-75e8e5322a40',
                   'ip_address': '10.0.0.4'},
                  {'subnet_id': 'subnet1',
                   'ip_address': 'fd79:e1c:a55::816:eff:eff:ff2'}],
             'security_groups': ['sg2'],
             'network_id': 'n2'},
            {'id': 'fp1',
             'device_owner': 'network:floatingip',
             'fixed_ips':
                 [{'subnet_id': 'ext-subnet',
                   'ip_address': '90.0.0.10'}],
             'network_id': 'ext-net'}]
        self.acls_ovn = {
            'lport1':
            # ACLs need to be removed by the sync tool
            [{'id': 'acl1', 'priority': 00, 'policy': 'allow',
              'lswitch': 'lswitch1', 'lport': 'lport1'}],
            'lport2':
            [{'id': 'acl2', 'priority': 00, 'policy': 'drop',
             'lswitch': 'lswitch2', 'lport': 'lport2'}],
            # ACLs need to be kept as-is by the sync tool
            'p2n2':
            [{'lport': 'p2n2', 'direction': 'to-lport',
              'log': False, 'lswitch': 'neutron-n2',
              'priority': 1001, 'action': 'drop',
             'external_ids': {'neutron:lport': 'p2n2'},
              'match': 'outport == "p2n2" && ip'},
             {'lport': 'p2n2', 'direction': 'to-lport',
              'log': False, 'lswitch': 'neutron-n2',
              'priority': 1002, 'action': 'allow',
              'external_ids': {'neutron:lport': 'p2n2'},
              'match': 'outport == "p2n2" && ip4 && '
              'ip4.src == 10.0.0.0/24 && udp && '
              'udp.src == 67 && udp.dst == 68'}]}
        self.address_sets_ovn = {
            'as_ip4_sg1': {'external_ids': {ovn_const.OVN_SG_NAME_EXT_ID_KEY:
                                            'all-tcp'},
                           'name': 'as_ip4_sg1',
                           'addresses': ['10.0.0.4']},
            'as_ip4_sg2': {'external_ids': {ovn_const.OVN_SG_NAME_EXT_ID_KEY:
                                            'all-tcpe'},
                           'name': 'as_ip4_sg2',
                           'addresses': []},
            'as_ip6_sg2': {'external_ids': {ovn_const.OVN_SG_NAME_EXT_ID_KEY:
                                            'all-tcpe'},
                           'name': 'as_ip6_sg2',
                           'addresses': ['fd79:e1c:a55::816:eff:eff:ff2',
                                         'fd79:e1c:a55::816:eff:eff:ff3']},
            'as_ip4_del': {'external_ids': {ovn_const.OVN_SG_NAME_EXT_ID_KEY:
                                            'all-delete'},
                           'name': 'as_ip4_delete',
                           'addresses': ['10.0.0.4']},
            }

        self.routers = [{'id': 'r1', 'routes': [{'nexthop': '20.0.0.100',
                         'destination': '11.0.0.0/24'}, {
                         'nexthop': '20.0.0.101',
                         'destination': '12.0.0.0/24'}],
                         'gw_port_id': 'gpr1',
                         'external_gateway_info': {
                             'network_id': "ext-net", 'enable_snat': True,
                             'external_fixed_ips': [
                                 {'subnet_id': 'ext-subnet',
                                  'ip_address': '90.0.0.2'}]}},
                        {'id': 'r2', 'routes': [{'nexthop': '40.0.0.100',
                         'destination': '30.0.0.0/24'}],
                         'gw_port_id': 'gpr2',
                         'external_gateway_info': {
                             'network_id': "ext-net", 'enable_snat': True,
                             'external_fixed_ips': [
                                 {'subnet_id': 'ext-subnet',
                                  'ip_address': '100.0.0.2'}]}},
                        {'id': 'r4', 'routes': []}]

        self.get_sync_router_ports = [
            {'fixed_ips': [{'subnet_id': 'subnet1',
                            'ip_address': '192.168.1.1'}],
             'id': 'p1r1',
             'device_id': 'r1',
             'mac_address': 'fa:16:3e:d7:fd:5f'},
            {'fixed_ips': [{'subnet_id': 'subnet2',
                            'ip_address': '192.168.2.1'}],
             'id': 'p1r2',
             'device_id': 'r2',
             'mac_address': 'fa:16:3e:d6:8b:ce'},
            {'fixed_ips': [{'subnet_id': 'subnet4',
                            'ip_address': '192.168.4.1'}],
             'id': 'p1r4',
             'device_id': 'r4',
             'mac_address': 'fa:16:3e:12:34:56'}]

        self.floating_ips = [{'router_id': 'r1',
                              'floating_ip_address': '90.0.0.10',
                              'fixed_ip_address': '172.16.0.10'},
                             {'router_id': 'r1',
                              'floating_ip_address': '90.0.0.12',
                              'fixed_ip_address': '172.16.2.12'},
                             {'router_id': 'r2',
                              'floating_ip_address': '100.0.0.10',
                              'fixed_ip_address': '192.168.2.10'}]

        self.lrouters_with_rports = [{'name': 'r3',
                                      'ports': {'p1r3': ['fake']},
                                      'static_routes': [],
                                      'snats': [],
                                      'dnat_and_snats': []},
                                     {'name': 'r4',
                                      'ports': {'p1r4':
                                                ['fdad:123:456::1/64',
                                                 'fdad:789:abc::1/64']},
                                      'static_routes': [],
                                      'snats': [],
                                      'dnat_and_snats': []},
                                     {'name': 'r1',
                                      'ports': {'p3r1': ['fake']},
                                      'static_routes':
                                      [{'nexthop': '20.0.0.100',
                                        'destination': '11.0.0.0/24'},
                                       {'nexthop': '20.0.0.100',
                                        'destination': '10.0.0.0/24'}],
                                      'snats':
                                      [{'logical_ip': '172.16.0.0/24',
                                        'external_ip': '90.0.0.2',
                                        'type': 'snat'},
                                       {'logical_ip': '172.16.1.0/24',
                                        'external_ip': '90.0.0.2',
                                        'type': 'snat'}],
                                      'dnat_and_snats':
                                      [{'logical_ip': '172.16.0.10',
                                        'external_ip': '90.0.0.10',
                                        'type': 'dnat_and_snat'},
                                       {'logical_ip': '172.16.1.11',
                                        'external_ip': '90.0.0.11',
                                        'type': 'dnat_and_snat'}]}]

        self.lswitches_with_ports = [{'name': 'neutron-n1',
                                      'ports': ['p1n1', 'p3n1'],
                                      'provnet_port': None},
                                     {'name': 'neutron-n3',
                                      'ports': ['p1n3', 'p2n3'],
                                      'provnet_port': None},
                                     {'name': 'neutron-n4',
                                      'ports': [],
                                      'provnet_port': 'provnet-n4'}]

        self.lrport_networks = ['fdad:123:456::1/64', 'fdad:cafe:a1b2::1/64']

    def _fake_get_ovn_dhcp_options(self, subnet, network, server_mac=None,
                                   metadata_port_ip=None):
        if subnet['id'] == 'n1-s1':
            return {'cidr': '10.0.0.0/24',
                    'options': {'server_id': '10.0.0.1',
                                'server_mac': '01:02:03:04:05:06',
                                'lease_time': str(12 * 60 * 60),
                                'mtu': '1450',
                                'router': '10.0.0.1'},
                    'external_ids': {'subnet_id': 'n1-s1'}}
        return {'cidr': '', 'options': '', 'external_ids': {}}

    def _fake_get_external_router_and_gateway_ip(self, ctx, router):
        return {'r1': ('90.0.0.2', '90.0.0.1'),
                'r2': ('100.0.0.2', '100.0.0.1')}.get(router['id'], ('', ''))

    def _fake_get_v4_network_of_all_router_ports(self, ctx, router_id):
        return {'r1': ['172.16.0.0/24', '172.16.2.0/24'],
                'r2': ['192.168.2.0/24']}.get(router_id, [])

    def _test_mocks_helper(self, ovn_nb_synchronizer):
        core_plugin = ovn_nb_synchronizer.core_plugin
        ovn_api = ovn_nb_synchronizer.ovn_api
        ovn_driver = ovn_nb_synchronizer.ovn_driver
        l3_plugin = ovn_nb_synchronizer.l3_plugin

        core_plugin.get_networks = mock.Mock()
        core_plugin.get_networks.return_value = self.networks
        core_plugin.get_subnets = mock.Mock()
        core_plugin.get_subnets.return_value = self.subnets
        # following block is used for acl syncing unit-test

        # With the given set of values in the unit testing,
        # 19 neutron acls should have been there,
        # 4 acls are returned as current ovn acls,
        # two of which will match with neutron.
        # So, in this example 17 will be added, 2 removed
        core_plugin.get_ports = mock.Mock()
        core_plugin.get_ports.return_value = self.ports
        mock.patch(
            "networking_ovn.common.acl._get_subnet_from_cache",
            return_value=self.subnet
        ).start()
        mock.patch(
            "networking_ovn.common.acl.acl_remote_group_id",
            side_effect=self.matches
        ).start()
        core_plugin.get_security_group = mock.MagicMock(
            side_effect=self.security_groups)
        ovn_nb_synchronizer.get_acls = mock.Mock()
        ovn_nb_synchronizer.get_acls.return_value = self.acls_ovn
        core_plugin.get_security_groups = mock.MagicMock(
            return_value=self.security_groups)
        ovn_nb_synchronizer.get_address_sets = mock.Mock()
        ovn_nb_synchronizer.get_address_sets.return_value =\
            self.address_sets_ovn
        # end of acl-sync block

        # The following block is used for router and router port syncing tests
        # With the give set of values in the unit test,
        # The Neutron db has Routers r1 and r2 present.
        # The OVN db has Routers r1 and r3 present.
        # During the sync r2 will need to be created and r3 will need
        # to be deleted from the OVN db. When Router r3 is deleted, all LRouter
        # ports associated with r3 is deleted too.
        #
        # Neutron db has Router ports p1r1 in Router r1 and p1r2 in Router r2
        # OVN db has p1r3 in Router 3 and p3r1 in Router 1.
        # During the sync p1r1 and p1r2 will be added and p1r3 and p3r1
        # will be deleted from the OVN db
        l3_plugin.get_routers = mock.Mock()
        l3_plugin.get_routers.return_value = self.routers
        l3_plugin.get_external_router_and_gateway_ip = mock.Mock()
        l3_plugin.get_external_router_and_gateway_ip.side_effect = \
            self._fake_get_external_router_and_gateway_ip
        l3_plugin._get_sync_interfaces = mock.Mock()
        l3_plugin._get_sync_interfaces.return_value = (
            self.get_sync_router_ports)
        ovn_nb_synchronizer._ovn_client = mock.Mock()
        ovn_nb_synchronizer._ovn_client._get_networks_for_router_port. \
            return_value = self.lrport_networks
        ovn_nb_synchronizer._ovn_client._get_v4_network_of_all_router_ports. \
            side_effect = self._fake_get_v4_network_of_all_router_ports
        # end of router-sync block
        l3_plugin.get_floatingips = mock.Mock()
        l3_plugin.get_floatingips.return_value = self.floating_ips
        ovn_api.get_all_logical_switches_with_ports = mock.Mock()
        ovn_api.get_all_logical_switches_with_ports.return_value = (
            self.lswitches_with_ports)

        ovn_api.get_all_logical_routers_with_rports = mock.Mock()
        ovn_api.get_all_logical_routers_with_rports.return_value = (
            self.lrouters_with_rports)

        ovn_api.transaction = mock.MagicMock()

        ovn_nb_synchronizer._ovn_client.create_network = mock.Mock()
        ovn_nb_synchronizer._ovn_client.create_port = mock.Mock()
        ovn_driver.validate_and_get_data_from_binding_profile = mock.Mock()
        ovn_nb_synchronizer._ovn_client.create_port = mock.Mock()
        ovn_nb_synchronizer._ovn_client.create_port.return_value = mock.ANY
        ovn_nb_synchronizer._ovn_client._create_provnet_port = mock.Mock()
        ovn_api.delete_lswitch = mock.Mock()
        ovn_api.delete_lswitch_port = mock.Mock()

        ovn_api.delete_lrouter = mock.Mock()
        ovn_api.delete_lrouter_port = mock.Mock()
        ovn_api.add_static_route = mock.Mock()
        ovn_api.delete_static_route = mock.Mock()
        ovn_api.get_all_dhcp_options.return_value = {
            'subnets': {'n1-s1': {'cidr': '10.0.0.0/24',
                                  'options':
                                  {'server_id': '10.0.0.1',
                                   'server_mac': '01:02:03:04:05:06',
                                   'lease_time': str(12 * 60 * 60),
                                   'mtu': '1450',
                                   'router': '10.0.0.1'},
                                  'external_ids': {'subnet_id': 'n1-s1'},
                                  'uuid': 'UUID1'},
                        'n1-s3': {'cidr': '30.0.0.0/24',
                                  'options':
                                  {'server_id': '30.0.0.1',
                                   'server_mac': '01:02:03:04:05:06',
                                   'lease_time': str(12 * 60 * 60),
                                   'mtu': '1450',
                                   'router': '30.0.0.1'},
                                  'external_ids': {'subnet_id': 'n1-s3'},
                                  'uuid': 'UUID2'}},
            'ports_v4': {'p1n2': {'cidr': '10.0.0.0/24',
                                  'options': {'server_id': '10.0.0.1',
                                              'server_mac':
                                                  '01:02:03:04:05:06',
                                              'lease_time': '1000',
                                              'mtu': '1400',
                                              'router': '10.0.0.1'},
                                  'external_ids': {'subnet_id': 'n1-s1',
                                                   'port_id': 'p1n2'},
                                  'uuid': 'UUID3'},
                         'p5n2': {'cidr': '10.0.0.0/24',
                                  'options': {'server_id': '10.0.0.1',
                                              'server_mac':
                                                  '01:02:03:04:05:06',
                                              'lease_time': '1000',
                                              'mtu': '1400',
                                              'router': '10.0.0.1'},
                                  'external_ids': {'subnet_id': 'n1-s1',
                                                   'port_id': 'p5n2'},
                                  'uuid': 'UUID4'}},
            'ports_v6': {'p1n1': {'cidr': 'fd79:e1c:a55::/64',
                                  'options': {'server_id': '01:02:03:04:05:06',
                                              'mtu': '1450'},
                                  'external_ids': {'subnet_id': 'fake',
                                                   'port_id': 'p1n1'},
                                  'uuid': 'UUID5'},
                         'p1n2': {'cidr': 'fd79:e1c:a55::/64',
                                  'options': {'server_id': '01:02:03:04:05:06',
                                              'mtu': '1450'},
                                  'external_ids': {'subnet_id': 'fake',
                                                   'port_id': 'p1n2'},
                                  'uuid': 'UUID6'}}}

        ovn_api.create_address_set = mock.Mock()
        ovn_api.delete_address_set = mock.Mock()
        ovn_api.update_address_set = mock.Mock()
        ovn_nb_synchronizer._ovn_client._add_subnet_dhcp_options = mock.Mock()
        ovn_nb_synchronizer._ovn_client._get_ovn_dhcp_options = mock.Mock()
        ovn_nb_synchronizer._ovn_client._get_ovn_dhcp_options.side_effect = (
            self._fake_get_ovn_dhcp_options)
        ovn_api.delete_dhcp_options = mock.Mock()

    def _test_ovn_nb_sync_helper(self, ovn_nb_synchronizer,
                                 networks, ports,
                                 routers, router_ports,
                                 create_router_list, create_router_port_list,
                                 update_router_port_list,
                                 del_router_list, del_router_port_list,
                                 create_network_list, create_port_list,
                                 create_provnet_port_list,
                                 del_network_list, del_port_list,
                                 add_static_route_list, del_static_route_list,
                                 add_snat_list, del_snat_list,
                                 add_floating_ip_list, del_floating_ip_list,
                                 add_address_set_list, del_address_set_list,
                                 update_address_set_list,
                                 add_subnet_dhcp_options_list,
                                 delete_dhcp_options_list):
        self._test_mocks_helper(ovn_nb_synchronizer)

        core_plugin = ovn_nb_synchronizer.core_plugin
        ovn_api = ovn_nb_synchronizer.ovn_api
        mock.patch("networking_ovn.ovsdb.impl_idl_ovn.get_connection").start()

        ovn_nb_synchronizer.do_sync()

        get_security_group_calls = [mock.call(mock.ANY, sg['id'])
                                    for sg in self.security_groups]
        self.assertEqual(len(self.security_groups),
                         core_plugin.get_security_group.call_count)
        core_plugin.get_security_group.assert_has_calls(
            get_security_group_calls, any_order=True)

        self.assertEqual(
            len(create_network_list),
            ovn_nb_synchronizer._ovn_client.create_network.call_count)
        create_network_calls = [mock.call(net['net'], None, None)
                                for net in create_network_list]
        ovn_nb_synchronizer._ovn_client.create_network.assert_has_calls(
            create_network_calls, any_order=True)

        self.assertEqual(
            len(create_port_list),
            ovn_nb_synchronizer._ovn_client.create_port.call_count)
        create_port_calls = [mock.call(port) for port in create_port_list]
        ovn_nb_synchronizer._ovn_client.create_port.assert_has_calls(
            create_port_calls, any_order=True)

        create_provnet_port_calls = [
            mock.call(mock.ANY, mock.ANY,
                      network['provider:physical_network'],
                      network['provider:segmentation_id'])
            for network in create_provnet_port_list]
        self.assertEqual(
            len(create_provnet_port_list),
            ovn_nb_synchronizer._ovn_client._create_provnet_port.call_count)
        ovn_nb_synchronizer._ovn_client._create_provnet_port.assert_has_calls(
            create_provnet_port_calls, any_order=True)

        self.assertEqual(len(del_network_list),
                         ovn_api.delete_lswitch.call_count)
        delete_lswitch_calls = [mock.call(lswitch_name=net_name)
                                for net_name in del_network_list]
        ovn_api.delete_lswitch.assert_has_calls(
            delete_lswitch_calls, any_order=True)

        self.assertEqual(len(del_port_list),
                         ovn_api.delete_lswitch_port.call_count)
        delete_lswitch_port_calls = [mock.call(lport_name=port['id'],
                                               lswitch_name=port['lswitch'])
                                     for port in del_port_list]
        ovn_api.delete_lswitch_port.assert_has_calls(
            delete_lswitch_port_calls, any_order=True)

        add_route_calls = [mock.call(mock.ANY, ip_prefix=route['destination'],
                                     nexthop=route['nexthop'])
                           for route in add_static_route_list]
        ovn_api.add_static_route.assert_has_calls(add_route_calls,
                                                  any_order=True)
        self.assertEqual(len(add_static_route_list),
                         ovn_api.add_static_route.call_count)
        del_route_calls = [mock.call(mock.ANY, ip_prefix=route['destination'],
                                     nexthop=route['nexthop'])
                           for route in del_static_route_list]
        ovn_api.delete_static_route.assert_has_calls(del_route_calls,
                                                     any_order=True)
        self.assertEqual(len(del_static_route_list),
                         ovn_api.delete_static_route.call_count)

        add_nat_list = add_snat_list + add_floating_ip_list
        add_nat_calls = [mock.call(mock.ANY, **nat) for nat in add_nat_list]
        ovn_api.add_nat_rule_in_lrouter.assert_has_calls(add_nat_calls,
                                                         any_order=True)
        self.assertEqual(len(add_nat_list),
                         ovn_api.add_nat_rule_in_lrouter.call_count)

        del_nat_list = del_snat_list + del_floating_ip_list
        del_nat_calls = [mock.call(mock.ANY, **nat) for nat in del_nat_list]
        ovn_api.delete_nat_rule_in_lrouter.assert_has_calls(del_nat_calls,
                                                            any_order=True)
        self.assertEqual(len(del_nat_list),
                         ovn_api.delete_nat_rule_in_lrouter.call_count)

        create_router_calls = [mock.call(r, add_external_gateway=False)
                               for r in create_router_list]
        self.assertEqual(
            len(create_router_list),
            ovn_nb_synchronizer._ovn_client.create_router.call_count)
        ovn_nb_synchronizer._ovn_client.create_router.assert_has_calls(
            create_router_calls, any_order=True)

        create_router_port_calls = [mock.call(p['device_id'],
                                              mock.ANY)
                                    for p in create_router_port_list]
        self.assertEqual(
            len(create_router_port_list),
            ovn_nb_synchronizer._ovn_client.create_router_port.call_count)
        ovn_nb_synchronizer._ovn_client.create_router_port.assert_has_calls(
            create_router_port_calls,
            any_order=True)

        self.assertEqual(len(del_router_list),
                         ovn_api.delete_lrouter.call_count)
        update_router_port_calls = [mock.call(r, p, self.lrport_networks)
                                    for (r, p) in update_router_port_list]
        self.assertEqual(
            len(update_router_port_list),
            ovn_nb_synchronizer._ovn_client.update_router_port.call_count)
        ovn_nb_synchronizer._ovn_client.update_router_port.assert_has_calls(
            update_router_port_calls,
            any_order=True)

        delete_lrouter_calls = [mock.call(r['router'])
                                for r in del_router_list]
        ovn_api.delete_lrouter.assert_has_calls(
            delete_lrouter_calls, any_order=True)

        self.assertEqual(
            len(del_router_port_list),
            ovn_api.delete_lrouter_port.call_count)
        delete_lrouter_port_calls = [mock.call(port['id'],
                                               port['router'], if_exists=False)
                                     for port in del_router_port_list]
        ovn_api.delete_lrouter_port.assert_has_calls(
            delete_lrouter_port_calls, any_order=True)

        create_address_set_calls = [mock.call(**a)
                                    for a in add_address_set_list]
        self.assertEqual(
            len(add_address_set_list),
            ovn_api.create_address_set.call_count)
        ovn_api.create_address_set.assert_has_calls(
            create_address_set_calls, any_order=True)

        del_address_set_calls = [mock.call(**d)
                                 for d in del_address_set_list]
        self.assertEqual(
            len(del_address_set_list),
            ovn_api.delete_address_set.call_count)
        ovn_api.delete_address_set.assert_has_calls(
            del_address_set_calls, any_order=True)

        update_address_set_calls = [mock.call(**u)
                                    for u in update_address_set_list]
        self.assertEqual(
            len(update_address_set_list),
            ovn_api.update_address_set.call_count)
        ovn_api.update_address_set.assert_has_calls(
            update_address_set_calls, any_order=True)

        self.assertEqual(
            len(add_subnet_dhcp_options_list),
            ovn_nb_synchronizer._ovn_client._add_subnet_dhcp_options.
            call_count)
        add_subnet_dhcp_options_calls = [
            mock.call(subnet, net, mock.ANY, metadata_port_ip=mock.ANY)
            for (subnet, net) in add_subnet_dhcp_options_list]
        ovn_nb_synchronizer._ovn_client._add_subnet_dhcp_options. \
            assert_has_calls(add_subnet_dhcp_options_calls, any_order=True)

        self.assertEqual(ovn_api.delete_dhcp_options.call_count,
                         len(delete_dhcp_options_list))
        delete_dhcp_options_calls = [
            mock.call(dhcp_opt_uuid)
            for dhcp_opt_uuid in delete_dhcp_options_list]
        ovn_api.delete_dhcp_options.assert_has_calls(
            delete_dhcp_options_calls, any_order=True)

    def test_ovn_nb_sync_mode_repair(self):
        create_network_list = [{'net': {'id': 'n2', 'mtu': 1450},
                                'ext_ids': {}}]
        del_network_list = ['neutron-n3']
        del_port_list = [{'id': 'p3n1', 'lswitch': 'neutron-n1'},
                         {'id': 'p1n1', 'lswitch': 'neutron-n1'}]
        create_port_list = self.ports
        for port in create_port_list:
            if port['id'] in ['p1n1', 'fp1']:
                # this will be skipped by the logic,
                # because p1n1 is already in lswitch-port list
                # and fp1 is a floating IP port
                create_port_list.remove(port)
        create_provnet_port_list = [{'id': 'n1', 'mtu': 1450,
                                     'provider:physical_network': 'physnet1',
                                     'provider:segmentation_id': 1000}]
        create_router_list = [{
            'id': 'r2', 'routes': [
                {'nexthop': '40.0.0.100', 'destination': '30.0.0.0/24'}],
            'gw_port_id': 'gpr2',
            'external_gateway_info': {
                'network_id': "ext-net", 'enable_snat': True,
                'external_fixed_ips': [{
                    'subnet_id': 'ext-subnet',
                    'ip_address': '100.0.0.2'}]}}]

        # Test adding and deleting routes snats fips behaviors for router r1
        # existing in both neutron DB and OVN DB.
        # Test adding behaviors for router r2 only existing in neutron DB.
        # Static routes with destination 0.0.0.0/0 are default gateway routes
        add_static_route_list = [{'nexthop': '20.0.0.101',
                                  'destination': '12.0.0.0/24'},
                                 {'nexthop': '90.0.0.1',
                                  'destination': '0.0.0.0/0'},
                                 {'nexthop': '40.0.0.100',
                                  'destination': '30.0.0.0/24'},
                                 {'nexthop': '100.0.0.1',
                                  'destination': '0.0.0.0/0'}]
        del_static_route_list = [{'nexthop': '20.0.0.100',
                                  'destination': '10.0.0.0/24'}]
        add_snat_list = [{'logical_ip': '172.16.2.0/24',
                          'external_ip': '90.0.0.2',
                          'type': 'snat'},
                         {'logical_ip': '192.168.2.0/24',
                          'external_ip': '100.0.0.2',
                          'type': 'snat'}]
        del_snat_list = [{'logical_ip': '172.16.1.0/24',
                          'external_ip': '90.0.0.2',
                          'type': 'snat'}]
        add_floating_ip_list = [{'logical_ip': '172.16.2.12',
                                 'external_ip': '90.0.0.12',
                                 'type': 'dnat_and_snat'},
                                {'logical_ip': '192.168.2.10',
                                 'external_ip': '100.0.0.10',
                                 'type': 'dnat_and_snat'}]
        del_floating_ip_list = [{'logical_ip': '172.16.1.11',
                                 'external_ip': '90.0.0.11',
                                 'type': 'dnat_and_snat'}]

        del_router_list = [{'router': 'neutron-r3'}]
        del_router_port_list = [{'id': 'lrp-p3r1', 'router': 'neutron-r1'}]
        create_router_port_list = self.get_sync_router_ports[:2]
        update_router_port_list = [('r4', self.get_sync_router_ports[2])]
        update_router_port_list[0][1].update(
            {'networks': self.lrport_networks})

        add_address_set_list = [
            {'external_ids': {ovn_const.OVN_SG_NAME_EXT_ID_KEY:
                              'all-tcp'},
             'name': 'as_ip6_sg1',
             'addresses': ['fd79:e1c:a55::816:eff:eff:ff2']}]
        del_address_set_list = [{'name': 'as_ip4_del'}]
        update_address_set_list = [
            {'addrs_remove': [],
             'addrs_add': ['10.0.0.4'],
             'name': 'as_ip4_sg2'},
            {'addrs_remove': ['fd79:e1c:a55::816:eff:eff:ff3'],
             'addrs_add': [],
             'name': 'as_ip6_sg2'}]

        add_subnet_dhcp_options_list = [(self.subnets[2], self.networks[1]),
                                        (self.subnets[1], self.networks[0])]
        delete_dhcp_options_list = ['UUID2', 'UUID4', 'UUID5']

        ovn_nb_synchronizer = ovn_db_sync.OvnNbSynchronizer(
            self.plugin, self.mech_driver._nb_ovn, 'repair', self.mech_driver)
        self._test_ovn_nb_sync_helper(ovn_nb_synchronizer,
                                      self.networks,
                                      self.ports,
                                      self.routers,
                                      self.get_sync_router_ports,
                                      create_router_list,
                                      create_router_port_list,
                                      update_router_port_list,
                                      del_router_list, del_router_port_list,
                                      create_network_list, create_port_list,
                                      create_provnet_port_list,
                                      del_network_list, del_port_list,
                                      add_static_route_list,
                                      del_static_route_list,
                                      add_snat_list,
                                      del_snat_list,
                                      add_floating_ip_list,
                                      del_floating_ip_list,
                                      add_address_set_list,
                                      del_address_set_list,
                                      update_address_set_list,
                                      add_subnet_dhcp_options_list,
                                      delete_dhcp_options_list)

    def test_ovn_nb_sync_mode_log(self):
        create_network_list = []
        create_port_list = []
        create_provnet_port_list = []
        del_network_list = []
        del_port_list = []
        create_router_list = []
        create_router_port_list = []
        update_router_port_list = []
        del_router_list = []
        del_router_port_list = []
        add_static_route_list = []
        del_static_route_list = []
        add_snat_list = []
        del_snat_list = []
        add_floating_ip_list = []
        del_floating_ip_list = []
        add_address_set_list = []
        del_address_set_list = []
        update_address_set_list = []
        add_subnet_dhcp_options_list = []
        delete_dhcp_options_list = []

        ovn_nb_synchronizer = ovn_db_sync.OvnNbSynchronizer(
            self.plugin, self.mech_driver._nb_ovn, 'log', self.mech_driver)
        self._test_ovn_nb_sync_helper(ovn_nb_synchronizer,
                                      self.networks,
                                      self.ports,
                                      self.routers,
                                      self.get_sync_router_ports,
                                      create_router_list,
                                      create_router_port_list,
                                      update_router_port_list,
                                      del_router_list, del_router_port_list,
                                      create_network_list, create_port_list,
                                      create_provnet_port_list,
                                      del_network_list, del_port_list,
                                      add_static_route_list,
                                      del_static_route_list,
                                      add_snat_list,
                                      del_snat_list,
                                      add_floating_ip_list,
                                      del_floating_ip_list,
                                      add_address_set_list,
                                      del_address_set_list,
                                      update_address_set_list,
                                      add_subnet_dhcp_options_list,
                                      delete_dhcp_options_list)


class TestOvnSbSyncML2(test_mech_driver.OVNMechanismDriverTestCase):

    def test_ovn_sb_sync(self):
        ovn_sb_synchronizer = ovn_db_sync.OvnSbSynchronizer(
            self.plugin,
            self.mech_driver._sb_ovn,
            self.mech_driver)
        ovn_api = ovn_sb_synchronizer.ovn_api
        hostname_with_physnets = {'hostname1': ['physnet1', 'physnet2'],
                                  'hostname2': ['physnet1']}
        ovn_api.get_chassis_hostname_and_physnets.return_value = (
            hostname_with_physnets)
        ovn_driver = ovn_sb_synchronizer.ovn_driver
        ovn_driver.update_segment_host_mapping = mock.Mock()
        hosts_in_neutron = {'hostname2', 'hostname3'}

        with mock.patch.object(ovn_db_sync.segments_db,
                               'get_hosts_mapped_with_segments',
                               return_value=hosts_in_neutron):
            ovn_sb_synchronizer.sync_hostname_and_physical_networks(mock.ANY)
            all_hosts = set(hostname_with_physnets.keys()) | hosts_in_neutron
            self.assertEqual(
                len(all_hosts),
                ovn_driver.update_segment_host_mapping.call_count)
            update_segment_host_mapping_calls = [mock.call(
                host, hostname_with_physnets[host])
                for host in hostname_with_physnets]
            update_segment_host_mapping_calls += [
                mock.call(host, []) for host in
                hosts_in_neutron - set(hostname_with_physnets.keys())]
            ovn_driver.update_segment_host_mapping.assert_has_calls(
                update_segment_host_mapping_calls, any_order=True)
