# Copyright 2019 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 collections
from unittest import mock

from neutron_lib import constants as const

from neutron.common.ovn import acl
from neutron.common.ovn import constants as ovn_const
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import impl_idl_ovn
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_client
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_db_sync
from neutron.services.ovn_l3 import plugin as ovn_plugin
from neutron.tests.unit.plugins.ml2.drivers.ovn.mech_driver import \
    test_mech_driver


OvnPortInfo = collections.namedtuple('OvnPortInfo', ['name'])


@mock.patch.object(ovn_plugin.OVNL3RouterPlugin, '_sb_ovn', mock.Mock())
class TestOvnNbSyncML2(test_mech_driver.OVNMechanismDriverTestCase):

    l3_plugin = 'ovn-router'

    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.segments = [{'id': 'seg1',
                          'network_id': 'n1',
                          'physical_network': 'physnet1',
                          'network_type': 'vlan',
                          'segmentation_id': 1000},
                         {'id': 'seg2',
                          'network_id': 'n2',
                          'physical_network': None,
                          'network_type': 'geneve'},
                         {'id': 'seg4',
                          'network_id': 'n4',
                          'physical_network': 'physnet2',
                          'network_type': 'flat'}]
        self.segments_map = {
            k['network_id']: k
            for k in self.segments}

        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.sg_port_groups_ovn = [mock.Mock(), mock.Mock(), mock.Mock()]
        self.sg_port_groups_ovn[0].configure_mock(
            name='pg_sg1',
            external_ids={ovn_const.OVN_SG_EXT_ID_KEY: 'sg1'},
            ports=[],
            acls=[])
        self.sg_port_groups_ovn[1].configure_mock(
            name='pg_unknown_del',
            external_ids={ovn_const.OVN_SG_EXT_ID_KEY: 'sg2'},
            ports=[],
            acls=[])
        self.sg_port_groups_ovn[2].configure_mock(
            name='neutron_pg_drop',
            external_ids=[],
            ports=[],
            acls=[])

        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.ports_ovn = [OvnPortInfo('p1n1'), OvnPortInfo('p1n2'),
                          OvnPortInfo('p2n1'), OvnPortInfo('p2n2'),
                          OvnPortInfo('p3n1'), OvnPortInfo('p3n3')]

        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.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 = [{'id': 'fip1', 'router_id': 'r1',
                              'floating_ip_address': '90.0.0.10',
                              'fixed_ip_address': '172.16.0.10'},
                             {'id': 'fip2', 'router_id': 'r1',
                              'floating_ip_address': '90.0.0.12',
                              'fixed_ip_address': '172.16.2.12'},
                             {'id': 'fip3', 'router_id': 'r2',
                              'floating_ip_address': '100.0.0.10',
                              'fixed_ip_address': '192.168.2.10'},
                             {'id': 'fip4', 'router_id': 'r2',
                              'floating_ip_address': '100.0.0.11',
                              'fixed_ip_address': '192.168.2.11'}]

        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'},
                                       {'logical_ip': '192.168.2.11',
                                        'external_ip': '100.0.0.11',
                                        'type': 'dnat_and_snat',
                                        'external_mac': '01:02:03:04:05:06',
                                        'logical_port': 'vm1'}]}]

        self.lswitches_with_ports = [{'name': 'neutron-n1',
                                      'ports': ['p1n1', 'p3n1'],
                                      'provnet_ports': []},
                                     {'name': 'neutron-n3',
                                      'ports': ['p1n3', 'p2n3'],
                                      'provnet_ports': []},
                                     {'name': 'neutron-n4',
                                      'ports': [],
                                      'provnet_ports': [
                                          'provnet-seg4',
                                          'provnet-orphaned-segment']}]

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

    def get_additional_service_plugins(self):
        p = super(TestOvnNbSyncML2, self).get_additional_service_plugins()
        p.update({'segments': 'neutron.services.segments.plugin.Plugin'})
        return p

    def _fake_get_ovn_dhcp_options(self, subnet, network, server_mac=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_gw_info(self, ctx, router):
        return {
            'r1': [ovn_client.GW_INFO(router_ip='90.0.0.2',
                                      gateway_ip='90.0.0.1',
                                      network_id='', subnet_id='',
                                      ip_version=4,
                                      ip_prefix=const.IPv4_ANY)],
            'r2': [ovn_client.GW_INFO(router_ip='100.0.0.2',
                                      gateway_ip='100.0.0.1',
                                      network_id='', subnet_id='',
                                      ip_version=4,
                                      ip_prefix=const.IPv4_ANY)]
        }.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
        pf_plugin = ovn_nb_synchronizer.pf_plugin
        segments_plugin = ovn_nb_synchronizer.segments_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

        def get_segments(self, filters):
            segs = []
            for segment in self.segments:
                if segment['network_id'] == filters['network_id'][0]:
                    segs.append(segment)
            return segs

        segments_plugin.get_segments = mock.Mock()
        segments_plugin.get_segments.side_effect = (
            lambda x, filters: get_segments(self, filters))

        # 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.object(acl, '_get_subnet_from_cache',
                          return_value=self.subnet).start()
        mock.patch.object(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)
        get_sg_port_groups = mock.MagicMock()
        get_sg_port_groups.execute.return_value = self.sg_port_groups_ovn
        ovn_api.db_list_rows.return_value = get_sg_port_groups
        ovn_api.lsp_list.execute.return_value = self.ports_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_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_nets_and_ipv6_ra_confs_for_router_port.return_value = (
                self.lrport_networks, {'fixed_ips': {}})
        ovn_nb_synchronizer._ovn_client._get_v4_network_of_all_router_ports. \
            side_effect = self._fake_get_v4_network_of_all_router_ports
        ovn_nb_synchronizer._ovn_client._get_gw_info = mock.Mock()
        ovn_nb_synchronizer._ovn_client._get_gw_info.side_effect = (
            self._fake_get_gw_info)
        # end of router-sync block
        l3_plugin.get_floatingips = mock.Mock()
        l3_plugin.get_floatingips.return_value = self.floating_ips
        pf_plugin.get_floatingip_port_forwardings = mock.Mock(return_value=[])
        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.ls_del = 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_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()
        ovn_nb_synchronizer._ovn_client.get_port_dns_records = mock.Mock()
        ovn_nb_synchronizer._ovn_client.get_port_dns_records.return_value = {}

    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_subnet_dhcp_options_list,
                                 delete_dhcp_options_list,
                                 add_port_groups_list,
                                 del_port_groups_list):
        self._test_mocks_helper(ovn_nb_synchronizer)

        ovn_api = ovn_nb_synchronizer.ovn_api
        mock.patch.object(impl_idl_ovn.OvsdbNbOvnIdl, 'from_worker').start()

        ovn_nb_synchronizer.do_sync()

        create_port_groups_calls = [mock.call(**a)
                                    for a in add_port_groups_list]
        self.assertEqual(
            len(add_port_groups_list),
            ovn_api.pg_add.call_count)
        ovn_api.pg_add.assert_has_calls(
            create_port_groups_calls, any_order=True)

        del_port_groups_calls = [mock.call(d)
                                 for d in del_port_groups_list]
        self.assertEqual(
            len(del_port_groups_list),
            ovn_api.pg_del.call_count)
        ovn_api.pg_del.assert_has_calls(
            del_port_groups_calls, any_order=True)

        self.assertEqual(
            len(create_network_list),
            ovn_nb_synchronizer._ovn_client.create_network.call_count)
        create_network_calls = [mock.call(mock.ANY, net['net'])
                                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(mock.ANY, 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(
                network['id'],
                self.segments_map[network['id']],
                txn=mock.ANY)
            for network in create_provnet_port_list
            if network.get('provider:physical_network')]
        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.ls_del.call_count)
        ls_del_calls = [mock.call(net_name)
                        for net_name in del_network_list]
        ovn_api.ls_del.assert_has_calls(
            ls_del_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_calls = [mock.call(mock.ANY, **nat) for nat in add_snat_list]
        ovn_api.add_nat_rule_in_lrouter.assert_has_calls(add_nat_calls,
                                                         any_order=True)
        self.assertEqual(len(add_snat_list),
                         ovn_api.add_nat_rule_in_lrouter.call_count)

        add_fip_calls = [mock.call(nat, txn=mock.ANY)
                         for nat in add_floating_ip_list]
        (ovn_nb_synchronizer._ovn_client._create_or_update_floatingip.
            assert_has_calls(add_fip_calls))
        self.assertEqual(
            len(add_floating_ip_list),
            ovn_nb_synchronizer._ovn_client._create_or_update_floatingip.
            call_count)

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

        del_fip_calls = [mock.call(nat, mock.ANY, txn=mock.ANY) for nat in
                         del_floating_ip_list]
        ovn_nb_synchronizer._ovn_client._delete_floatingip.assert_has_calls(
            del_fip_calls, any_order=True)
        self.assertEqual(
            len(del_floating_ip_list),
            ovn_nb_synchronizer._ovn_client._delete_floatingip.call_count)

        create_router_calls = [mock.call(mock.ANY, 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(mock.ANY, self.routers[i], mock.ANY)
            for i, p in enumerate(create_router_port_list)]
        self.assertEqual(
            len(create_router_port_list),
            ovn_nb_synchronizer._ovn_client._create_lrouter_port.call_count)
        ovn_nb_synchronizer._ovn_client._create_lrouter_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(mock.ANY, p)
                                    for 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)

        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)
            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'},
                         {'id': 'provnet-orphaned-segment',
                          'lswitch': 'neutron-n4'}]
        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'}]
        # fip 100.0.0.11 exists in OVN with distributed type and in Neutron
        # with centralized type. This fip is used to test
        # enable_distributed_floating_ip switch and migration
        add_floating_ip_list = [{'id': 'fip2', 'router_id': 'r1',
                                 'floating_ip_address': '90.0.0.12',
                                 'fixed_ip_address': '172.16.2.12'},
                                {'id': 'fip3', 'router_id': 'r2',
                                 'floating_ip_address': '100.0.0.10',
                                 'fixed_ip_address': '192.168.2.10'},
                                {'id': 'fip4', 'router_id': 'r2',
                                 'floating_ip_address': '100.0.0.11',
                                 'fixed_ip_address': '192.168.2.11'}]
        del_floating_ip_list = [{'logical_ip': '172.16.1.11',
                                 'external_ip': '90.0.0.11',
                                 'type': 'dnat_and_snat'},
                                {'logical_ip': '192.168.2.11',
                                 'external_ip': '100.0.0.11',
                                 'type': 'dnat_and_snat',
                                 'external_mac': '01:02:03:04:05:06',
                                 'logical_port': 'vm1'}]

        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 = [self.get_sync_router_ports[2]]
        update_router_port_list[0].update(
            {'networks': self.lrport_networks})

        add_port_groups_list = [
            {'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'sg2'},
             'name': 'pg_sg2',
             'acls': []}]
        del_port_groups_list = ['pg_unknown_del']

        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, self.mech_driver._sb_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_subnet_dhcp_options_list,
                                      delete_dhcp_options_list,
                                      add_port_groups_list,
                                      del_port_groups_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_subnet_dhcp_options_list = []
        delete_dhcp_options_list = []
        add_port_groups_list = []
        del_port_groups_list = []

        ovn_nb_synchronizer = ovn_db_sync.OvnNbSynchronizer(
            self.plugin, self.mech_driver._nb_ovn, self.mech_driver._sb_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_subnet_dhcp_options_list,
                                      delete_dhcp_options_list,
                                      add_port_groups_list,
                                      del_port_groups_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)
