diff --git a/ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py b/ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py index c682d88e..43bb57fa 100644 --- a/ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py +++ b/ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py @@ -154,7 +154,8 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase): watcher.OVNPFCreateEvent(self), watcher.OVNPFDeleteEvent(self), watcher.ChassisRedirectCreateEvent(self), - watcher.ChassisRedirectDeleteEvent(self)} + watcher.ChassisRedirectDeleteEvent(self), + watcher.NATMACAddedEvent(self)} if CONF.exposing_method == constants.EXPOSE_METHOD_VRF: # For vrf we require more information on the logical_switch diff --git a/ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py b/ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py index 9badc7df..1b293aec 100644 --- a/ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py +++ b/ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py @@ -99,3 +99,11 @@ class ChassisCreateEvent(ChassisCreateEventBase): class ChassisPrivateCreateEvent(ChassisCreateEventBase): table = 'Chassis_Private' + + +class DnatSnatUpdatedBaseEvent(Event): + def __init__(self, bgp_agent): + events = (self.ROW_UPDATE) + table = 'NAT' + super().__init__( + bgp_agent, events, table, (('type', '=', 'dnat_and_snat'),)) diff --git a/ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py b/ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py index d08be335..a2f6f081 100644 --- a/ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py +++ b/ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py @@ -873,3 +873,60 @@ class OVNPFDeleteEvent(OVNPFBaseEvent): def _run(self, event, row, old): with _SYNC_STATE_LOCK.read_lock(): self.agent.withdraw_ovn_pf_lb_fip(row) + + +class NATMACAddedEvent(base_watcher.DnatSnatUpdatedBaseEvent): + def match_fn(self, event, row, old): + try: + lsp_id = row.logical_port[0] + except IndexError: + LOG.error("NAT entry %s has no logical port set.", row.uuid) + return False + + lsp = self.agent.nb_idl.lsp_get(lsp_id).execute() + + if lsp is None: + LOG.error("Logical Switch Port %(lsp)s for NAT entry %(nat)s " + "was not found in OVN NB DB.", { + 'lsp': lsp_id, + 'nat': row.uuid}) + return False + + if lsp.type != constants.OVN_VM_VIF_PORT_TYPE: + return False + + try: + if lsp.options['requested-chassis'] != self.agent.chassis: + return False + except KeyError: + return False + + try: + if old.external_mac and old.external_mac[0] == row.external_mac[0]: + return False + except (AttributeError, IndexError): + return False + + # This is required to be able to expose the FIP, there is no point in + # continuing if the external_id is not set + if constants.OVN_FIP_NET_EXT_ID_KEY not in row.external_ids: + LOG.error("NAT entry %(nat)s does not have %(fip_net)s set in " + "external_ids", { + 'nat': row.uuid, + 'fip_net': constants.OVN_FIP_NET_EXT_ID_KEY}) + return False + + if not row.external_ip: + LOG.error("NAT entry %s does not have external_ip set", row.uuid) + return False + + return True + + def run(self, event, row, old): + lsp = self.agent.nb_idl.lsp_get(row.logical_port[0]).execute() + # The key is present, it was checked in match_fn + net_id = row.external_ids[constants.OVN_FIP_NET_EXT_ID_KEY] + ls_name = "neutron-{}".format(net_id) + with _SYNC_STATE_LOCK.read_lock(): + self.agent.expose_fip( + row.external_ip, row.external_mac[0], ls_name, lsp) diff --git a/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_nb_bgp_watcher.py b/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_nb_bgp_watcher.py index 5fc2578f..15b04f02 100644 --- a/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_nb_bgp_watcher.py +++ b/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_nb_bgp_watcher.py @@ -1610,3 +1610,111 @@ class TestOVNLBDeleteEvent(test_base.TestCase): self.agent.withdraw_ovn_lb_vip.assert_not_called() self.agent.withdraw_ovn_lb_fip.assert_called_once_with(old) + + +class TestNATMACAddedEvent(test_base.TestCase): + def setUp(self): + super().setUp() + self.chassis = 'fake-chassis' + self.agent = mock.Mock(chassis=self.chassis) + self.event = nb_bgp_watcher.NATMACAddedEvent( + self.agent) + self.nat_entry = utils.create_row( + _uuid='uuid', + logical_port=['port-id'], + external_ids={constants.OVN_FIP_NET_EXT_ID_KEY: 'net-id'}, + external_ip='ext-ip', + external_mac=['ext-mac'] + ) + + def _call_match(self, old=mock.ANY): + return self.event.match_fn(mock.ANY, self.nat_entry, old) + + def test_run(self): + self.event.run(mock.ANY, self.nat_entry, None) + + def test_match_fn_no_logical_port(self): + self.nat_entry.logical_port = "" + self.assertFalse(self._call_match()) + + def test_match_fn_lsp_not_found(self): + self.agent.nb_idl.lsp_get.return_value.execute.return_value = None + self.assertFalse(self._call_match()) + + def test_match_fn_bad_lsp_type(self): + self.agent.nb_idl.lsp_get.return_value.execute.return_value.type = 'b' + self.assertFalse(self._call_match()) + + def test_match_fn_no_requested_chassis(self): + lsp = self.agent.nb_idl.lsp_get.return_value.execute.return_value + lsp.type = constants.OVN_VM_VIF_PORT_TYPE + lsp.options = {} + self.assertFalse(self._call_match()) + + def test_match_fn_different_chassis(self): + lsp = self.agent.nb_idl.lsp_get.return_value.execute.return_value + lsp.type = constants.OVN_VM_VIF_PORT_TYPE + lsp.options = {'requested-chassis': 'foo'} + self.assertFalse(self._call_match()) + + def test_match_fn_external_mac_untouched(self): + lsp = self.agent.nb_idl.lsp_get.return_value.execute.return_value + lsp.type = constants.OVN_VM_VIF_PORT_TYPE + lsp.options = {'requested-chassis': self.chassis} + old = utils.create_row( + _uuid='uuid', + ) + self.assertFalse(self._call_match(old)) + + def test_match_fn_external_mac_unset(self): + lsp = self.agent.nb_idl.lsp_get.return_value.execute.return_value + lsp.type = constants.OVN_VM_VIF_PORT_TYPE + lsp.options = {'requested-chassis': self.chassis} + old = utils.create_row( + _uuid='uuid', + external_mac=['old-mac'] + ) + self.nat_entry.external_mac = "" + self.assertFalse(self._call_match(old)) + + def test_match_fn_external_mac_already_set(self): + lsp = self.agent.nb_idl.lsp_get.return_value.execute.return_value + lsp.type = constants.OVN_VM_VIF_PORT_TYPE + lsp.options = {'requested-chassis': self.chassis} + old = utils.create_row( + _uuid='uuid', + external_mac=self.nat_entry.external_mac + ) + self.assertFalse(self._call_match(old)) + + def test_match_fn_ext_ids_missing_net_id(self): + lsp = self.agent.nb_idl.lsp_get.return_value.execute.return_value + lsp.type = constants.OVN_VM_VIF_PORT_TYPE + lsp.options = {'requested-chassis': self.chassis} + old = utils.create_row( + _uuid='uuid', + external_mac="" + ) + del self.nat_entry.external_ids[constants.OVN_FIP_NET_EXT_ID_KEY] + self.assertFalse(self._call_match(old)) + + def test_match_fn_missing_ext_ip(self): + lsp = self.agent.nb_idl.lsp_get.return_value.execute.return_value + lsp.type = constants.OVN_VM_VIF_PORT_TYPE + lsp.options = {'requested-chassis': self.chassis} + old = utils.create_row( + _uuid='uuid', + external_mac="" + ) + self.nat_entry.external_ip = "" + self.assertFalse(self._call_match(old)) + + def test_match_fn_passed(self): + lsp = self.agent.nb_idl.lsp_get.return_value.execute.return_value + lsp.type = constants.OVN_VM_VIF_PORT_TYPE + lsp.options = {'requested-chassis': self.chassis} + old = utils.create_row( + _uuid='uuid', + external_mac="" + ) + self.assertTrue(self._call_match(old))