diff --git a/doc/source/contributor/bgp_mode_stretched_l2_design.rst b/doc/source/contributor/bgp_mode_stretched_l2_design.rst new file mode 100644 index 00000000..63e5f355 --- /dev/null +++ b/doc/source/contributor/bgp_mode_stretched_l2_design.rst @@ -0,0 +1,304 @@ +.. + This work is licensed under a Creative Commons Attribution 3.0 Unported + License. + + http://creativecommons.org/licenses/by/3.0/legalcode + + Convention for heading levels in Neutron devref: + ======= Heading 0 (reserved for the title in a document) + ------- Heading 1 + ~~~~~~~ Heading 2 + +++++++ Heading 3 + ''''''' Heading 4 + (Avoid deeper levels because they do not render well.) + +==================================================== +OVN BGP Agent: Design of the stretched L2 BGP Driver +==================================================== + +Purpose +------- + +The main reason for adding the L2 BGP driver is to announce networks via BGP +that are not masqueraded (SNAT disabled) and can communicate directly via +routing. The driver requires that all routers to be announced are in a L2 +provider network and that the BGP Neighbor and Speaker are also part of this +network. The whole tenant networks are announced via the external gateway IP +of the router, instead of as /32 (or /128 for IPv6). This means that the +tenant networks can be routed directly via the router IP and failover can run +completely via BFD in OVN. No additional BGP announcements are needed incase +of failover of routers, but only ARP/GARP in the respective L2 network. + +The resulting routes are the same on all instances of the ovn-bgp-agent and +are not bound to the machine the agent is running on. For redundancy reasons +it is recommended to run multiple instances. + +Overview +-------- + +The OVN BGP Agent is a Python-based daemon that can run on any node. However, +it is recommended to run the L2 BGP driver on the gateway node since all +requirements are already met there, e.g. connectivity to the L2 provider +network. The driver connects to the OVN SouthBound database (OVN SB DB) for +all information and responds to events via it. It uses a VRF to create the +routes locally and FRR to announce them to the BGP peer. The VRF is completely +isolated and is not used for anything else than announcing routes via FRR. +The tenant routers/networks cannot be reached from the VRF either. + + .. note:: + + Note it is only intended for the N/S traffic, the E/W traffic will work + exactly the same as before, i.e., VMs are connected through geneve + tunnels. + + +The agent provides a multi-driver implementation that allows you to configure +it for specific infrastructure running on top of OVN, for instance OpenStack +or Kubernetes/OpenShift. +This simple design allows the agent to implement different drivers, depending +on what OVN SB DB events are being watched (watchers examples at +``ovn_bgp_agent/drivers/openstack/watchers/``), and what actions are +triggered in reaction to them (drivers examples at +``ovn_bgp_agent/drivers/openstack/XXXX_driver.py``, implementing the +``ovn_bgp_agent/drivers/driver_api.py``). + +A common driver API is defined exposing the next methods: + +- ``expose_ip`` and ``withdraw_ip``: used to expose/withdraw IPs/Networks for + OVN ports. + +- ``expose_subnet``, ``update_subnet`` and ``withdraw_subnet``: used to + expose/withdraw subnets through the external router gateway ip. + +OVN SB DB Events +~~~~~~~~~~~~~~~~ + +The watcher associated to this BGP driver detect the relevant events on the +OVN SB DB to call the driver functions to configure BGP and linux kernel +networking accordingly. + +The BGP watcher detects OVN Southbound Database events at the ``Port_Binding`` +and ``Load_Balancer`` tables. It creates new event classes named +``PortBindingChassisEvent`` and ``OVNLBMemberEvent``, that all the events +watched for BGP use as the base (inherit from). + +The driver react specifically to the following events: + +- ``PortBindingChassisCreatedEvent``: Detects when a port of type + ``""`` (empty double-qoutes), ``virtual``, or ``chassisredirect`` gets + attached to the OVN chassis where the agent is running. This is the case for + VM or amphora LB ports on the provider networks, VM or amphora LB ports on + tenant networks with a FIP associated, and neutron gateway router ports + (CR-LRPs). It calls ``expose_ip`` driver method to perform the needed + actions to expose it. + +- ``PortBindingChassisDeletedEvent``: Detects when a port of type + ``""`` (empty double-quotes), ``virtual``, or ``chassisredirect`` gets + detached from the OVN chassis where the agent is running. This is the case + for VM or amphora LB ports on the provider networks, VM or amphora LB ports + on tenant networks with a FIP associated, and neutron gateway router ports + (CR-LRPs). It calls ``withdraw_ip`` driver method to perform the needed + actions to withdraw the exposed BGP route. + +- ``SubnetRouterAttachedEvent``: Detects when a patch port gets created. + This means a subnet is attached to a router. If this port is associated to + a cr-lrp port, the subnet will get announced. + +- ``SubnetRouterDetachedEvent``: Same as previous one, but for the deletion + of the port. It calls ``withdraw_subnet``. + +- ``SubnetRouterUpdateEvent``: Detects when a subnet/IP is added to an + existing patch port. This can happen when multiple subnets are generated + from an address pool and added to the same router. + It calls ``update_subnet``. + +Driver Logic +~~~~~~~~~~~~ + +The stretched L2 BGP driver is responsible for announcing all tenant networks +that match the corresponding address scope (if used for filtering subnets). +If the config option ``address_scopes`` is not set, all tenant networks will +be announced via the corresponding provider network router IP. + +BGP Advertisement ++++++++++++++++++ + +The OVN BGP Agent is in charge of triggering FRR (IP routing protocol +suite for Linux which includes protocol daemons for BGP, OSPF, RIP, +among others) to advertise/withdraw directly connected routes via BGP. +To do that, when the agent starts, it ensures that: + +- FRR local instance is reconfigured to leak routes for a new VRF. To do that + it uses ``vtysh shell``. It connects to the existing FRR socket ( + ``--vty_socket`` option) and executes the next commands, passing them through + a file (``-c FILE_NAME`` option): + + .. code-block:: ini + + LEAK_VRF_TEMPLATE = ''' + router bgp {{ bgp_as }} + address-family ipv4 unicast + import vrf {{ vrf_name }} + exit-address-family + + address-family ipv6 unicast + import vrf {{ vrf_name }} + exit-address-family + + router bgp {{ bgp_as }} vrf {{ vrf_name }} + bgp router-id {{ bgp_router_id }} + address-family ipv4 unicast + redistribute kernel + exit-address-family + + address-family ipv6 unicast + redistribute kernel + exit-address-family + + ''' + + +- There is a VRF created (the one leaked in the previous step) by default + with name ``bgp_vrf``. + +- There is a dummy interface type (by default named ``bgp-nic``), associated to + the previously created VRF device. + + +Then, to expose the tenant networks as they are created (or upon +initialization or re-sync), since the FRR configuration has the +``redistribute kernel`` option enabled, the only action needed to +expose/withdraw the tenant networks is to add/remove the routes in +the ``bgp_vrf_table_id`` table. Then it relies on Zebra to do the BGP +advertisement, as Zebra detects the addition/deletion of the routes in the +table and advertises/withdraw the route. In order to add these routes we have +to make the Linux kernel believe that it can reach the respective router IPs. +For this we use link-local routes pointing to the interface of the VRF. If we +use the provider network ``111.111.111.0/24``, a router with the IP +``111.111.111.17/24`` on the gateway port and the tenant subnet ``192.168.0.0/24``, +the route would be added like this (same logic applies to IPv6): + + .. code-block:: ini + + $ ip route add 111.111.111.0/24 dev bgp-nic table 10 + $ ip route add 192.168.0.0/24 via 111.111.111.17 table 10 + + + .. note:: + + The link-local route for the provider network is also announced and is + only removed when no router to be announced has a gateway port on the + network. Since all BGP peers should also be on this network, the BGP + neighbor will prefer its connected route over the announced link-local + route. + +On the BGP neighbor side, the route should look like this: + + .. code-block:: ini + + $ ip route show + 192.168.0.0/24 via 111.111.111.17 + +Driver API +++++++++++ + +The BGP driver needs to implement the ``driver_api.py`` interface with the +following functions: + +- ``expose_ip``: Creates the routes for all tenant networks and announces + them via FRR. If no subnets are connected to this port, nothing is + announced. + +- ``withdraw_ip``: Removes all routes for the tenant networks and withdraws + them from FRR. + +- ``expose_subnet``: Announces the tenant network via the router IP if this + router has an external gateway port. + +- ``withdraw_subnet``: Withdraws the tenant network if this + router has an external gateway port. + +- ``update_subnet``: Does the same as ``expose_subnet`` / ``withdraw_subnet`` + and is called when a subnet is added or removed from the port. + + +Agent deployment +~~~~~~~~~~~~~~~~ + +The agent can be deployed anywhere as long as it is in the respective L2 +network that is to be announced. In addition, OVS agent must be installed on +the machine (from which it reads SB DB address) and it must be possible to +connect to the Southbound Database. The L2 network can be filtered via the +address scope, so it is not necessary that the agent has access to all L2 +provider networks, but only the one in which it is to peer. Unlike the +``ovn_bgp_driver``, it announces all routes regardless of which chassis they +are on. + +As an example of how to start the OVN BGP Agent on the nodes, see the commands +below: + + .. code-block:: ini + + $ python setup.py install + $ cat bgp-agent.conf + # sample configuration that can be adapted based on needs + [DEFAULT] + debug=True + reconcile_interval=120 + driver=ovn_stretched_l2_bgp_driver + address_scopes=2237917c7b12489a84de4ef384a2bcae + + $ sudo bgp-agent --config-dir bgp-agent.conf + .... + + +Note that the OVN BGP Agent operates under the next assumptions: + +- A dynamic routing solution, in this case FRR, is deployed and + advertises/withdraws routes added/deleted to/from the vrf routing table. + A sample config for FRR is: + + .. code-block:: ini + + frr version 7.0 + frr defaults traditional + hostname cmp-1-0 + log file /var/log/frr/frr.log debugging + log timestamp precision 3 + service integrated-vtysh-config + line vty + + debug bgp neighbor-events + debug bgp updates + + router bgp 64999 + bgp router-id 172.30.1.1 + neighbor pg peer-group + neighbor 172.30.1.2 remote-as 64998 + address-family ipv6 unicast + redistribute kernel + neighbor pg activate + neighbor pg route-map IMPORT in + neighbor pg route-map EXPORT out + exit-address-family + + address-family ipv4 unicast + redistribute kernel + neighbor pg activate + neighbor pg route-map IMPORT in + neighbor pg route-map EXPORT out + exit-address-family + + route-map EXPORT deny 100 + + route-map EXPORT permit 1 + match interface bgp-nic + + route-map IMPORT deny 1 + + line vty + +Limitations +----------- + +- TBD \ No newline at end of file diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 865b13ed..e4fff7bd 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -7,4 +7,5 @@ bgp_mode_design evpn_mode_design + bgp_mode_stretched_l2_design diff --git a/ovn_bgp_agent/config.py b/ovn_bgp_agent/config.py index 7063e4c7..27942ad5 100644 --- a/ovn_bgp_agent/config.py +++ b/ovn_bgp_agent/config.py @@ -81,6 +81,10 @@ agent_opts = [ default=4789, help='The UDP port used for EVPN VXLAN communication. By ' 'default 4789 is being used.'), + cfg.BoolOpt('clear_vrf_routes_on_startup', + help='If enabled, all routes are removed from the VRF table' + '(specified by bgp_vrf_table_id option) at startup.', + default=False), cfg.StrOpt('bgp_nic', default='bgp-nic', help='The name of the interface used within the VRF ' @@ -94,6 +98,11 @@ agent_opts = [ help='The Routing Table ID that the VRF (bgp_vrf option) ' 'should use. If it does not exist, this table will be ' 'created.'), + cfg.ListOpt('address_scopes', + default=None, + help='Allows to filter on the address scope. Only networks' + ' with the same address scope on the provider and' + ' internal interface are announced.'), ] root_helper_opts = [ diff --git a/ovn_bgp_agent/constants.py b/ovn_bgp_agent/constants.py index e219c093..0cbb1401 100644 --- a/ovn_bgp_agent/constants.py +++ b/ovn_bgp_agent/constants.py @@ -50,3 +50,6 @@ OVS_PATCH_PROVNET_PORT_PREFIX = 'patch-provnet-' LINK_UP = "up" LINK_DOWN = "down" + +SUBNET_POOL_ADDR_SCOPE4 = "neutron:subnet_pool_addr_scope4" +SUBNET_POOL_ADDR_SCOPE6 = "neutron:subnet_pool_addr_scope6" diff --git a/ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py b/ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py new file mode 100644 index 00000000..d1003320 --- /dev/null +++ b/ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py @@ -0,0 +1,550 @@ +# Copyright 2021 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 +import dataclasses +import ipaddress +import threading + +from oslo_concurrency import lockutils +from oslo_config import cfg +from oslo_log import log as logging + +from ovn_bgp_agent import constants +from ovn_bgp_agent.drivers import driver_api +from ovn_bgp_agent.drivers.openstack.utils import frr +from ovn_bgp_agent.drivers.openstack.utils import ovn +from ovn_bgp_agent.drivers.openstack.utils import ovs +from ovn_bgp_agent.drivers.openstack.watchers import bgp_watcher as watcher +from ovn_bgp_agent.utils import linux_net + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + +OVN_TABLES = ["Port_Binding", "Chassis", "Datapath_Binding"] + + +@dataclasses.dataclass(frozen=True, eq=True) +class HashedRoute: + network: str + prefix_len: int + dst: str + + +class OVNBGPStretchedL2Driver(driver_api.AgentDriverBase): + def __init__(self): + self.ovn_local_cr_lrps = {} + self.vrf_routes = set() + self.ovn_routing_tables_routes = collections.defaultdict() + self.allowed_address_scopes = set(CONF.address_scopes or []) + self.propagated_lrp_ports = {} + + self._sb_idl = None + self._post_fork_event = threading.Event() + + @property + def sb_idl(self): + if not self._sb_idl: + self._post_fork_event.wait() + return self._sb_idl + + @sb_idl.setter + def sb_idl(self, val): + self._sb_idl = val + + def start(self): + self.ovs_idl = ovs.OvsIdl() + self.ovs_idl.start(CONF.ovsdb_connection) + + # Ensure FRR is configured to leak the routes + # NOTE: If we want to recheck this every X time, we should move it + # inside the sync function instead + frr.vrf_leak( + CONF.bgp_vrf, + CONF.bgp_AS, + CONF.bgp_router_id, + template=frr.LEAK_VRF_KERNEL_TEMPLATE, + ) + + LOG.debug("Setting up VRF %s", CONF.bgp_vrf) + linux_net.ensure_vrf(CONF.bgp_vrf, CONF.bgp_vrf_table_id) + # Create OVN dummy device + linux_net.ensure_ovn_device(CONF.bgp_nic, CONF.bgp_vrf) + + # Clear vrf routing table + if CONF.clear_vrf_routes_on_startup: + linux_net.delete_routes_from_table(CONF.bgp_vrf_table_id) + + self.chassis = self.ovs_idl.get_own_chassis_name() + self.ovn_remote = self.ovs_idl.get_ovn_remote() + LOG.debug("Loaded chassis %s.", self.chassis) + if self.allowed_address_scopes: + LOG.debug("Configured allowed address scopes: %s", + ", ".join(self.allowed_address_scopes)) + + events = () + for event in self._get_events(): + event_class = getattr(watcher, event) + events += (event_class(self),) + + self._post_fork_event.clear() + # TODO(lucasagomes): The OVN package in the ubuntu LTS is old + # and does not support Chassis_Private. Once the package is updated + # we can remove this fallback mode. + try: + self.sb_idl = ovn.OvnSbIdl( + self.ovn_remote, + chassis=self.chassis, + tables=OVN_TABLES + ["Chassis_Private"], + events=events, + ).start() + except AssertionError: + self.sb_idl = ovn.OvnSbIdl( + self.ovn_remote, + chassis=self.chassis, + tables=OVN_TABLES, + events=events, + ).start() + + # Now IDL connections can be safely used + self._post_fork_event.set() + + def _get_events(self): + return set( + [ + "SubnetRouterAttachedEvent", + "SubnetRouterUpdateEvent", + "SubnetRouterDetachedEvent", + "PortBindingChassisCreatedEvent", + "PortBindingChassisDeletedEvent", + ] + ) + + @lockutils.synchronized("bgp") + def sync(self): + self.ovn_local_cr_lrps = {} + self.ovn_routing_tables_routes = collections.defaultdict() + self.vrf_routes = set() + self.propagated_lrp_ports = {} + + LOG.debug("Syncing current routes.") + + # Get all current exposed routes + vrf_routes = linux_net.get_routes_on_tables([CONF.bgp_vrf_table_id]) + + for cr_lrp_port in self.sb_idl.get_cr_lrp_ports(): + if not cr_lrp_port.mac or len(cr_lrp_port.mac[0].split(" ")) <= 1: + continue + + self._expose_cr_lrp(cr_lrp_port.mac[0].split(" ")[1:], cr_lrp_port) + + # remove all left over routes + delete_routes = [] + for route in vrf_routes: + r = HashedRoute( + network=route.dst, + prefix_len=route.dst_len, + dst=route.gateway if route.gateway else None) + if r not in self.vrf_routes: + delete_routes.append(route) + + linux_net.delete_ip_routes(delete_routes) + + def _add_route(self, network, prefix_len, dst=None): + LOG.debug("Adding BGP route for Network %s/%d via %s", + network, prefix_len, dst) + + linux_net.add_ip_route( + self.ovn_routing_tables_routes, + network, + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=prefix_len, + via=dst) + r = HashedRoute( + network=network, + prefix_len=prefix_len, + dst=dst) + self.vrf_routes.add(r) + + LOG.debug("Added BGP route for Network %s/%d via %s", + network, prefix_len, dst) + + def _del_route(self, network, prefix_len, dst=None): + LOG.debug("Deleting BGP route for Network %s/%d via %s", + network, prefix_len, dst) + + linux_net.del_ip_route( + self.ovn_routing_tables_routes, + network, + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=prefix_len, + via=dst) + r = HashedRoute( + network=network, + prefix_len=prefix_len, + dst=dst) + if r in self.vrf_routes: + self.vrf_routes.remove(r) + + LOG.debug("Deleted BGP route for Network %s/%d via %s", + network, prefix_len, dst) + + def _address_scope_allowed(self, scope1, scope2, ip_version): + if not self.allowed_address_scopes: + # No address scopes to filter on => announce everything + return True + + if scope1[ip_version] != scope2[ip_version]: + # Not the same address scope => don't announce + return False + + if scope1[ip_version] not in self.allowed_address_scopes: + # This address scope does not match => don't announce + return False + + return True + + def _get_addr_scopes(self, port): + return { + constants.IP_VERSION_4: port.external_ids.get( + constants.SUBNET_POOL_ADDR_SCOPE4 + ), + constants.IP_VERSION_6: port.external_ids.get( + constants.SUBNET_POOL_ADDR_SCOPE6 + ), + } + + @lockutils.synchronized("bgp") + def expose_subnet(self, ip, row): + cr_lrp = self.sb_idl.is_router_gateway_on_any_chassis(row.datapath) + if not cr_lrp: + return + + self._ensure_network_exposed(row, cr_lrp.logical_port) + + @lockutils.synchronized("bgp") + def update_subnet(self, old, row): + cr_lrp = self.sb_idl.is_router_gateway_on_any_chassis(row.datapath) + if not cr_lrp or not cr_lrp.mac or len(cr_lrp.mac[0].split(" ")) <= 1: + return + + current_ips = row.mac[0].split(" ")[1:] + previous_ips = ( + old.mac[0].split(" ")[1:] + if old.mac or len(old.mac[0].split(" ")) > 1 + else [] + ) + add_ips = list( + filter(lambda ip: ip not in previous_ips, current_ips)) + delete_ips = list( + filter(lambda ip: ip not in current_ips, previous_ips)) + + self._update_network(row, cr_lrp.logical_port, add_ips, delete_ips) + + @lockutils.synchronized("bgp") + def withdraw_subnet(self, ip, row): + port_info = self.propagated_lrp_ports.get(row.logical_port) + if not port_info: + return + + self._withdraw_subnet(port_info, port_info["cr_lrp"]) + + gateway = self.ovn_local_cr_lrps.get(port_info["cr_lrp"]) + if gateway and row.logical_port in gateway["lrp_ports"]: + gateway["lrp_ports"].remove(row.logical_port) + self.propagated_lrp_ports.pop(row.logical_port) + + def _withdraw_subnet(self, port_info, cr_lrp): + gateway = self.ovn_local_cr_lrps.get(cr_lrp) + if not gateway: + # If we dont have it cached then its either not existing or + # or we got an event while starting up which then the sync + # function can fix. + return + gateway_ips = gateway["ips"] + + subnets = [ + ipaddress.ip_network(subnet) + for subnet in port_info["subnets"]] + + for gateway_ip in gateway_ips: + for subnet in subnets: + if gateway_ip.version != subnet.version: + continue + + self._del_route( + network=str(subnet.network_address), + prefix_len=subnet.prefixlen, + dst=str(gateway_ip.ip)) + + # Check if can delete the link-local route + exposed_routes = linux_net.get_exposed_routes_on_network( + [CONF.bgp_vrf_table_id], + gateway_ip.network) + + if not exposed_routes: + self._del_route( + network=str(gateway_ip.network.network_address), + prefix_len=gateway_ip.network.prefixlen) + + @lockutils.synchronized('bgp') + def withdraw_ip(self, ips, row, associated_port=None): + if not (row.type == constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE and + row.logical_port.startswith("cr-")): + return + self._withdraw_cr_lrp(ips, row) + + def _withdraw_cr_lrp(self, ips, row): + if self.allowed_address_scopes: + # Validate address scopes + address_scopes = self.ovn_local_cr_lrps[row.logical_port][ + "address_scopes"] + if not any([ + scope in self.allowed_address_scopes + for scope in address_scopes.values()]): + return + + # Check if there are networks attached to the router, + # and if so, remove them locally + lrp_ports = self.ovn_local_cr_lrps[row.logical_port]["lrp_ports"] + for lrp_logical_port in lrp_ports: + port_info = self.propagated_lrp_ports.get(lrp_logical_port) + if not port_info: + continue + # withdraw network + self._withdraw_subnet(port_info, row.logical_port) + self.propagated_lrp_ports.pop(lrp_logical_port, None) + + self.ovn_local_cr_lrps.pop(row.logical_port, None) + + @lockutils.synchronized("bgp") + def expose_ip(self, ips, row, associated_port=None): + if not (row.type == constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE and + row.logical_port.startswith("cr-")): + return + self._expose_cr_lrp(ips, row) + + def _expose_cr_lrp(self, ips, row): + LOG.debug("Adding BGP route for CR-LRP Port %s", row.logical_port) + # Keeping information about the associated network for + # tenant network advertisement + self.ovn_local_cr_lrps[row.logical_port] = { + "ips": [ipaddress.ip_interface(ip) for ip in ips], + "address_scopes": {}, + "lrp_ports": set(), + } + + if self.allowed_address_scopes: + # Validate address scopes + patch_port = row.logical_port.split("cr-lrp-")[1] + port = self.sb_idl.get_port_by_name(patch_port) + if not port: + LOG.error("Patchport %s for CR-LRP %s missing, skipping.", + patch_port, row.logical_port) + return + address_scopes = self._get_addr_scopes(port) + self.ovn_local_cr_lrps[row.logical_port][ + "address_scopes"] = address_scopes + if not any([ + scope in self.allowed_address_scopes + for scope in address_scopes.values()]): + return + + # Check if there are networks attached to the router, + # and if so, add the needed routes + lrp_ports = self.sb_idl.get_lrp_ports_for_router(row.datapath) + for lrp in lrp_ports: + if ( + lrp.chassis or + not lrp.logical_port.startswith("lrp-") or + "chassis-redirect-port" in lrp.options.keys() + ): + continue + # expose network + self._ensure_network_exposed(lrp, row.logical_port) + + def _update_network(self, router_port, gateway_port, add_ips, delete_ips): + gateway = self.ovn_local_cr_lrps.get(gateway_port) + if not gateway: + # If we dont have it cached then its either not existing or + # or we got an event while starting up which then the sync + # function can fix. + return + gateway_ips = gateway["ips"] + if not router_port.mac or len(router_port.mac[0].split(" ")) <= 1: + return + + # get all ips from the router port + ips_to_add = [ipaddress.ip_interface(ip) for ip in add_ips] + + ips_to_delete = [ipaddress.ip_interface(ip) for ip in delete_ips] + + for router_ip in ips_to_add + ips_to_delete: + if router_ip in gateway_ips: + return + + address_scopes = None + if self.allowed_address_scopes: + patch_port = router_port.logical_port.split("lrp-")[1] + port = self.sb_idl.get_port_by_name(patch_port) + if not port: + LOG.error("Patchport %s for CR-LRP %s missing, skipping.", + patch_port, gateway_port) + return + address_scopes = self._get_addr_scopes(port) + # if we should filter on address scopes and this port has no + # address scopes set we do not need to go further + if not any(address_scopes.values()): + return + + subnets = set() + for gateway_ip in gateway_ips: + for router_ip in ips_to_add: + if gateway_ip.version != router_ip.version: + continue + + if not self._address_scope_allowed( + gateway["address_scopes"], + address_scopes, + router_ip.version): + continue + + # Add link-local route + self._add_route( + network=str(gateway_ip.network.network_address), + prefix_len=gateway_ip.network.prefixlen) + + # add route for the tenant network pointing to the + # gateway ip + self._add_route( + network=str(router_ip.network.network_address), + prefix_len=router_ip.network.prefixlen, + dst=str(gateway_ip.ip)) + subnets.add(str(router_ip.network)) + + for router_ip in ips_to_delete: + if gateway_ip.version != router_ip.version: + continue + + if not self._address_scope_allowed( + gateway["address_scopes"], + address_scopes, + router_ip.version): + continue + + self._del_route( + network=str(router_ip.network.network_address), + prefix_len=router_ip.network.prefixlen, + dst=str(gateway_ip.ip)) + + # We only need to check this if we really deleted a route for + # a tenant network + if ips_to_delete: + # Check if can delete the link-local route + exposed_routes = linux_net.get_exposed_routes_on_network( + [CONF.bgp_vrf_table_id], + gateway_ip.network + ) + + if not exposed_routes: + self._del_route( + network=str(gateway_ip.network.network_address), + prefix_len=gateway_ip.network.prefixlen) + + self.ovn_local_cr_lrps[gateway_port]["lrp_ports"].add( + router_port.logical_port) + self.propagated_lrp_ports[router_port.logical_port] = { + "cr_lrp": gateway_port, + "subnets": subnets + } + + def _ensure_network_exposed(self, router_port, gateway_port): + gateway = self.ovn_local_cr_lrps.get(gateway_port) + if not gateway: + # If we dont have it cached then its either not existing or + # or we got an event while starting up which then the sync + # function can fix. + return + gateway_ips = gateway["ips"] + if not router_port.mac or len(router_port.mac[0].split(" ")) <= 1: + return + + # get all ips from the router port + router_ips = [ + ipaddress.ip_interface(ip) + for ip in router_port.mac[0].split(" ")[1:]] + + for router_ip in router_ips: + if router_ip in gateway_ips: + return + + address_scopes = None + if self.allowed_address_scopes: + patch_port = router_port.logical_port.split("lrp-")[1] + port = self.sb_idl.get_port_by_name(patch_port) + if not port: + LOG.error("Patchport %s for CR-LRP %s missing, skipping.", + patch_port, gateway_port) + return + address_scopes = self._get_addr_scopes(port) + # if we have address scopes configured and none of them matches + # for this port, we can skip further processing + if not any(address_scopes.values()): + return + + subnets = set() + for gateway_ip in gateway_ips: + for router_ip in router_ips: + if gateway_ip.version != router_ip.version: + continue + + if not self._address_scope_allowed( + gateway["address_scopes"], + address_scopes, + router_ip.version): + continue + + # Add link-local route + self._add_route( + network=str(gateway_ip.network.network_address), + prefix_len=gateway_ip.network.prefixlen) + + # add route for the tenant network pointing to the + # gateway ip + self._add_route( + network=str(router_ip.network.network_address), + prefix_len=router_ip.network.prefixlen, + dst=str(gateway_ip.ip)) + subnets.add(str(router_ip.network)) + + if subnets: + self.ovn_local_cr_lrps[gateway_port]["lrp_ports"].add( + router_port.logical_port) + self.propagated_lrp_ports[router_port.logical_port] = { + "cr_lrp": gateway_port, + "subnets": subnets + } + + @lockutils.synchronized("bgp") + def expose_remote_ip(self, ip_address): + raise NotImplementedError() + + @lockutils.synchronized("bgp") + def withdraw_remote_ip(self, ip_address): + raise NotImplementedError() diff --git a/ovn_bgp_agent/drivers/openstack/utils/frr.py b/ovn_bgp_agent/drivers/openstack/utils/frr.py index defb8970..2d92dac0 100644 --- a/ovn_bgp_agent/drivers/openstack/utils/frr.py +++ b/ovn_bgp_agent/drivers/openstack/utils/frr.py @@ -70,6 +70,28 @@ router bgp {{ bgp_as }} vrf {{ vrf_name }} ''' +LEAK_VRF_KERNEL_TEMPLATE = ''' +router bgp {{ bgp_as }} + address-family ipv4 unicast + import vrf {{ vrf_name }} + exit-address-family + + address-family ipv6 unicast + import vrf {{ vrf_name }} + exit-address-family + +router bgp {{ bgp_as }} vrf {{ vrf_name }} + bgp router-id {{ bgp_router_id }} + address-family ipv4 unicast + redistribute kernel + exit-address-family + + address-family ipv6 unicast + redistribute kernel + exit-address-family + +''' + def _get_router_id(): output = ovn_bgp_agent.privileged.vtysh.run_vtysh_command( @@ -96,7 +118,7 @@ def _run_vtysh_config_with_tempfile(vrf_config): f.close() -def vrf_leak(vrf, bgp_as, bgp_router_id=None): +def vrf_leak(vrf, bgp_as, bgp_router_id=None, template=LEAK_VRF_TEMPLATE): LOG.info("Add VRF leak for VRF %s on router bgp %s", vrf, bgp_as) if not bgp_router_id: bgp_router_id = _get_router_id() @@ -104,7 +126,7 @@ def vrf_leak(vrf, bgp_as, bgp_router_id=None): LOG.error("Unknown router-id, needed for route leaking") return - vrf_template = Template(LEAK_VRF_TEMPLATE) + vrf_template = Template(template) vrf_config = vrf_template.render(vrf_name=vrf, bgp_as=bgp_as, bgp_router_id=bgp_router_id) _run_vtysh_config_with_tempfile(vrf_config) diff --git a/ovn_bgp_agent/drivers/openstack/utils/ovn.py b/ovn_bgp_agent/drivers/openstack/utils/ovn.py index 2448b1a8..4db45ee1 100644 --- a/ovn_bgp_agent/drivers/openstack/utils/ovn.py +++ b/ovn_bgp_agent/drivers/openstack/utils/ovn.py @@ -165,13 +165,18 @@ class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend): rows = self.db_list_rows('Port_Binding').execute(check_error=True) return [r for r in rows if r.chassis and r.chassis[0].name == chassis] + def get_cr_lrp_ports(self): + return self.db_find_rows( + "Port_Binding", + ("type", "=", constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE), + ).execute(check_error=True) + def get_cr_lrp_ports_on_chassis(self, chassis): - rows = self.db_find_rows( - 'Port_Binding', - ('type', '=', constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE) - ).execute(check_error=True) - return [r.logical_port for r in rows - if r.chassis and r.chassis[0].name == chassis] + return [ + r.logical_port + for r in self.get_cr_lrp_ports() + if r.chassis and r.chassis[0].name == chassis + ] def get_cr_lrp_nat_addresses_info(self, cr_lrp_port_name, chassis, sb_idl): # NOTE: Assuming logical_port format is "cr-lrp-XXXX" @@ -222,6 +227,15 @@ class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend): except IndexError: pass + def is_router_gateway_on_any_chassis(self, datapath): + port_info = self.get_ports_on_datapath( + datapath, constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE) + try: + if port_info and port_info[0].chassis[0].name: + return port_info[0] + except IndexError: + pass + def get_lrp_port_for_datapath(self, datapath): for row in self.get_ports_on_datapath( datapath, constants.OVN_PATCH_VIF_PORT_TYPE): diff --git a/ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py b/ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py index 4821e0ed..9f0830d8 100644 --- a/ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py +++ b/ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py @@ -175,6 +175,40 @@ class SubnetRouterAttachedEvent(base_watcher.PortBindingChassisEvent): self.agent.expose_subnet(ip_address, row) +class SubnetRouterUpdateEvent(base_watcher.PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_UPDATE,) + super(SubnetRouterUpdateEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + # This will match if the mac field has changed between old and row. + # This can happen when you have multiple subnets in the same network, + # those will be added/removed to/from the same lrp-port in the mac + # field. + # Format: + # mac = [ff:ff:ff:ff:ff:ff subnet1/cidr subnet2/cidr [...]] + try: + # single and dual-stack format + if (not self._check_ip_associated(row.mac[0]) and + not self._check_ip_associated(old.mac[0])): + return False + return ( + not row.chassis and + row.logical_port.startswith("lrp-") and + "chassis-redirect-port" not in row.options.keys() and + old.mac != row.mac + ) + except (IndexError, AttributeError): + return False + + def run(self, event, row, old): + if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: + return + with _SYNC_STATE_LOCK.read_lock(): + self.agent.update_subnet(old, row) + + class SubnetRouterDetachedEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_DELETE,) diff --git a/ovn_bgp_agent/tests/unit/drivers/openstack/test_ovn_stretched_l2_bgp_driver.py b/ovn_bgp_agent/tests/unit/drivers/openstack/test_ovn_stretched_l2_bgp_driver.py new file mode 100644 index 00000000..d9e72c51 --- /dev/null +++ b/ovn_bgp_agent/tests/unit/drivers/openstack/test_ovn_stretched_l2_bgp_driver.py @@ -0,0 +1,1243 @@ +# Copyright 2022 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from oslo_config import cfg + +from ovn_bgp_agent import config +from ovn_bgp_agent import constants +from ovn_bgp_agent.drivers.openstack import ovn_stretched_l2_bgp_driver +from ovn_bgp_agent.drivers.openstack.utils import frr +from ovn_bgp_agent.drivers.openstack.utils import ovn +from ovn_bgp_agent.drivers.openstack.utils import ovs +from ovn_bgp_agent.tests import base as test_base +from ovn_bgp_agent.tests.unit import fakes +from ovn_bgp_agent.utils import linux_net + +import ipaddress + +CONF = cfg.CONF + + +class TestHashedRoute(test_base.TestCase): + def setUp(self): + super(TestHashedRoute, self).setUp() + self.table = set() + self.route = ovn_stretched_l2_bgp_driver.HashedRoute( + "192.168.0.0", 24, "192.168.1.1") + self.invalid_route = ovn_stretched_l2_bgp_driver.HashedRoute( + "192.168.0.0", 24, "192.168.1.2") + self.table.add(self.route) + + def test_lookup(self): + self.assertTrue(self.route in self.table) + self.assertFalse(self.invalid_route in self.table) + + def test_delete(self): + self.table.remove(self.route) + self.assertEqual(0, len(self.table)) + + +class TestOVNBGPStretchedL2Driver(test_base.TestCase): + def setUp(self): + super(TestOVNBGPStretchedL2Driver, self).setUp() + config.register_opts() + CONF.set_override( + "address_scopes", + "11111111-1111-1111-1111-11111111,22222222-2222-2222-2222-22222222", # NOQA E501 + ) + self.bgp_driver = ovn_stretched_l2_bgp_driver.OVNBGPStretchedL2Driver() + self.bgp_driver._post_fork_event = mock.Mock() + self.bgp_driver.sb_idl = mock.Mock() + self.sb_idl = self.bgp_driver.sb_idl + self.bgp_driver.chassis = "fake-chassis" + # self.bgp_driver.ovn_routing_tables = {self.bridge: 'fake-table'} + # self.bgp_driver.ovn_bridge_mappings = {'fake-network': self.bridge} + + self.mock_sbdb = mock.patch.object(ovn, "OvnSbIdl").start() + self.mock_ovs_idl = mock.patch.object(ovs, "OvsIdl").start() + self.ipv4 = "192.168.1.17" + self.ipv6 = "2002::1234:abcd:ffff:c0a8:101" + self.fip = "172.24.4.33" + self.mac = "aa:bb:cc:dd:ee:ff" + self.bgp_driver.ovs_idl = self.mock_ovs_idl + + self.test_route_ipv4 = ovn_stretched_l2_bgp_driver.HashedRoute( + network="192.168.1.0", + prefix_len=24, + dst="10.0.0.1", + ) + + self.test_route_ipv6 = ovn_stretched_l2_bgp_driver.HashedRoute( + network="fdcc:8cf2:d40c:2::", + prefix_len=64, + dst="fd51:f4b3:872:eda::1", + ) + + self.addr_scopev4 = "11111111-1111-1111-1111-11111111" + self.addr_scopev6 = "22222222-2222-2222-2222-22222222" + self.addr_scope = { + constants.IP_VERSION_4: self.addr_scopev4, + constants.IP_VERSION_6: self.addr_scopev6, + } + + self.addr_scope_external_ids = { + "neutron:subnet_pool_addr_scope4": self.addr_scopev4, + "neutron:subnet_pool_addr_scope6": self.addr_scopev6, + } + + self.cr_lrp0 = mock.Mock() + self.cr_lrp0.mac = [ + "ff:ff:ff:ff:ff:00 10.0.0.1/24 fd51:f4b3:872:eda::1/64" + ] + self.cr_lrp0.datapath = "fake-router-dp" + self.cr_lrp0.type = constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE + self.cr_lrp0.logical_port = "cr-lrp-fake-port" + + self.lp0 = mock.Mock() + self.lp0.external_ids = self.addr_scope_external_ids + + self.router_port = fakes.create_object( + { + "name": "fake-router-port", + "mac": [ + "ff:ff:ff:ff:ff:01 192.168.1.1/24 fdcc:8cf2:d40c:2::1/64" + ], + "logical_port": "lrp-fake-logical-port", + } + ) + + self.fake_patch_port = fakes.create_object( + { + "name": "fake-patch-port", + "mac": [ + "ff:ff:ff:ff:ff:01 192.168.1.1/24 fdcc:8cf2:d40c:2::1/64" + ], + "external_ids": self.addr_scope_external_ids, + "logical_port": "fake-port", + } + ) + + # Mock pyroute2.NDB context manager object + self.mock_ndb = mock.patch.object(linux_net.pyroute2, "NDB").start() + self.fake_ndb = self.mock_ndb().__enter__() + + @mock.patch.object(linux_net, "ensure_ovn_device") + @mock.patch.object(linux_net, "delete_routes_from_table") + @mock.patch.object(frr, "vrf_leak") + def test_start(self, mock_vrf, mock_delete_routes, mock_ensure_ovn_device): + CONF.set_override("clear_vrf_routes_on_startup", True) + + self.bgp_driver.start() + + mock_vrf.assert_called_once_with( + CONF.bgp_vrf, + CONF.bgp_AS, + CONF.bgp_router_id, + template=frr.LEAK_VRF_KERNEL_TEMPLATE, + ) + # Assert connections were started + self.mock_ovs_idl().start.assert_called_once_with( + CONF.ovsdb_connection + ) + self.mock_sbdb().start.assert_called_once_with() + mock_delete_routes.assert_called_once_with(CONF.bgp_vrf_table_id) + mock_ensure_ovn_device.assert_called_once_with( + CONF.bgp_nic, CONF.bgp_vrf) + + @mock.patch.object(linux_net, "ensure_ovn_device") + @mock.patch.object(linux_net, "delete_routes_from_table") + @mock.patch.object(frr, "vrf_leak") + def test_start_clear_routes( + self, mock_vrf, mock_delete_routes, mock_ensure_ovn_device): + CONF.set_override("clear_vrf_routes_on_startup", False) + + self.bgp_driver.start() + + mock_vrf.assert_called_once_with( + CONF.bgp_vrf, + CONF.bgp_AS, + CONF.bgp_router_id, + template=frr.LEAK_VRF_KERNEL_TEMPLATE, + ) + # Assert connections were started + self.mock_ovs_idl().start.assert_called_once_with( + CONF.ovsdb_connection + ) + self.mock_sbdb().start.assert_called_once_with() + mock_delete_routes.assert_not_called() + mock_ensure_ovn_device.assert_called_once_with( + CONF.bgp_nic, CONF.bgp_vrf) + + @mock.patch.object(linux_net, "add_ip_route") + def test__add_route(self, mock_add_route): + for test_route in [self.test_route_ipv4, self.test_route_ipv6]: + self.bgp_driver._add_route( + test_route.network, + test_route.prefix_len, + test_route.dst, + ) + + mock_add_route.assert_called_with( + mock.ANY, + test_route.network, + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=test_route.prefix_len, + via=test_route.dst, + ) + + self.assertTrue(test_route in self.bgp_driver.vrf_routes) + + @mock.patch.object(linux_net, "del_ip_route") + def test__del_route(self, mock_del_route): + self.bgp_driver.vrf_routes.add(self.test_route_ipv4) + self.bgp_driver.vrf_routes.add(self.test_route_ipv6) + for test_route in [self.test_route_ipv4, self.test_route_ipv6]: + self.bgp_driver._del_route( + test_route.network, + test_route.prefix_len, + test_route.dst, + ) + + mock_del_route.assert_called_with( + mock.ANY, + test_route.network, + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=test_route.prefix_len, + via=test_route.dst, + ) + + self.assertTrue(test_route not in self.bgp_driver.vrf_routes) + + def test__get_addr_scopes(self): + addr_scopes = self.bgp_driver._get_addr_scopes(self.lp0) + self.assertEqual(self.addr_scope, addr_scopes) + + def test__address_scope_allowed(self): + test_scope2 = { + constants.IP_VERSION_4: self.addr_scopev4, + constants.IP_VERSION_6: "33333333-3333-3333-3333-33333333", + } + + self.assertTrue( + self.bgp_driver._address_scope_allowed( + self.addr_scope, + test_scope2, + constants.IP_VERSION_4, + ) + ) + + self.assertFalse( + self.bgp_driver._address_scope_allowed( + self.addr_scope, + test_scope2, + constants.IP_VERSION_6, + ) + ) + + def test__address_scope_not_allowed_scope(self): + test_scope1 = { + constants.IP_VERSION_4: "33333333-3333-3333-3333-33333333", + constants.IP_VERSION_6: "33333333-3333-3333-3333-33333333", + } + + test_scope2 = { + constants.IP_VERSION_4: "33333333-3333-3333-3333-33333333", + constants.IP_VERSION_6: "33333333-3333-3333-3333-33333333", + } + + self.assertFalse( + self.bgp_driver._address_scope_allowed( + test_scope1, + test_scope2, + constants.IP_VERSION_4, + ) + ) + + self.assertFalse( + self.bgp_driver._address_scope_allowed( + test_scope1, + test_scope2, + constants.IP_VERSION_6, + ) + ) + + def test__address_scope_allowed_no_scope(self): + self.bgp_driver.allowed_address_scopes = set() + test_scope2 = { + constants.IP_VERSION_4: None, + constants.IP_VERSION_6: None, + } + + self.assertTrue( + self.bgp_driver._address_scope_allowed( + self.addr_scope, + test_scope2, + constants.IP_VERSION_4, + ) + ) + + self.assertTrue( + self.bgp_driver._address_scope_allowed( + self.addr_scope, + test_scope2, + constants.IP_VERSION_6, + ) + ) + + def test__address_scope_allowed_no_match(self): + test_scope2 = { + constants.IP_VERSION_4: None, + constants.IP_VERSION_6: "44444444-4444-4444-4444-44444444", + } + + self.assertFalse( + self.bgp_driver._address_scope_allowed( + self.addr_scope, + test_scope2, + constants.IP_VERSION_4, + ) + ) + + self.assertFalse( + self.bgp_driver._address_scope_allowed( + self.addr_scope, + test_scope2, + constants.IP_VERSION_6, + ) + ) + + def test_expose_subnet(self): + mock__ensure_network_exposed = mock.patch.object( + self.bgp_driver, "_ensure_network_exposed" + ).start() + self.sb_idl.is_router_gateway_on_any_chassis.return_value = ( + self.cr_lrp0 + ) + row = mock.Mock() + row.datapath = "fake-dp" + + self.bgp_driver.expose_subnet(None, row) + + self.sb_idl.is_router_gateway_on_any_chassis.assert_called_once_with( + row.datapath + ) + + mock__ensure_network_exposed.assert_called_once_with( + row, self.cr_lrp0.logical_port + ) + + def test_expose_subnet_no_gateway_port(self): + mock__ensure_network_exposed = mock.patch.object( + self.bgp_driver, "_ensure_network_exposed" + ).start() + self.sb_idl.is_router_gateway_on_any_chassis.return_value = None + row = mock.Mock() + row.datapath = "fake-dp" + + self.bgp_driver.expose_subnet(None, row) + + self.sb_idl.is_router_gateway_on_any_chassis.assert_called_once_with( + row.datapath + ) + + mock__ensure_network_exposed.assert_not_called() + + def test_update_subnet(self): + mock__update_network = mock.patch.object( + self.bgp_driver, "_update_network" + ).start() + self.sb_idl.is_router_gateway_on_any_chassis.return_value = ( + self.cr_lrp0 + ) + old = mock.Mock() + old.mac = ["ff:ff:ff:ff:ff:01 1.1.1.1/24 2.2.2.2/24"] + + row = mock.Mock() + row.datapath = "fake-dp" + row.mac = ["ff:ff:ff:ff:ff:01 2.2.2.2/24 3.3.3.3/24"] + + self.bgp_driver.update_subnet(old, row) + + self.sb_idl.is_router_gateway_on_any_chassis.assert_called_once_with( + row.datapath + ) + + mock__update_network.assert_called_once_with( + row, self.cr_lrp0.logical_port, ["3.3.3.3/24"], ["1.1.1.1/24"] + ) + + @mock.patch.object(linux_net, "get_exposed_routes_on_network") + @mock.patch.object(linux_net, "del_ip_route") + @mock.patch.object(linux_net, "add_ip_route") + def test__update_network( + self, + mock_add_ip_route, + mock_del_ip_route, + mock_get_exposed_routes_on_network, + ): + gateway = {} + gateway["ips"] = [ + ipaddress.ip_interface(ip) + for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] + ] + gateway["address_scopes"] = self.addr_scope + gateway["lrp_ports"] = set() + self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} + + self.router_lrp = mock.Mock() + self.router_lrp.mac = [ + "ff:ff:ff:ff:ff:01 192.168.1.1/24 fdcc:8cf2:d40c:2::1/64" + ] + + add_ips = ["192.168.1.1/24", "fdcc:8cf2:d40c:2::1/64"] + delete_ips = ["192.168.0.1/24"] + + mock_get_exposed_routes_on_network.side_effect = ( + ["route-v4"], + ["route-v6"], + ) + + self.sb_idl.get_port_by_name.return_value = self.fake_patch_port + + self.bgp_driver._update_network( + self.router_port, "gateway_port", add_ips, delete_ips + ) + + self.sb_idl.get_port_by_name.assert_called_once_with( + "fake-logical-port" + ) + + expected_calls = [ + mock.call( + mock.ANY, + "10.0.0.0", + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=26, + via=None, + ), + mock.call( + mock.ANY, + "192.168.1.0", + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=24, + via="10.0.0.10", + ), + mock.call( + mock.ANY, + "fd51:f4b3:872:eda::", + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=64, + via=None, + ), + mock.call( + mock.ANY, + "fdcc:8cf2:d40c:2::", + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=64, + via="fd51:f4b3:872:eda::10", + ), + ] + + mock_add_ip_route.assert_has_calls(expected_calls) + mock_del_ip_route.assert_called_once_with( + mock.ANY, + "192.168.0.0", + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=24, + via="10.0.0.10", + ) + + self.assertDictEqual( + self.bgp_driver.propagated_lrp_ports, + { + self.router_port.logical_port: { + "cr_lrp": "gateway_port", + "subnets": { + "fdcc:8cf2:d40c:2::/64", + "192.168.1.0/24" + } + } + } + ) + + @mock.patch.object(linux_net, "get_exposed_routes_on_network") + @mock.patch.object(linux_net, "del_ip_route") + @mock.patch.object(linux_net, "add_ip_route") + def test__update_network_no_gateway( + self, + mock_add_ip_route, + mock_del_ip_route, + mock_get_exposed_routes_on_network, + ): + self.bgp_driver.ovn_local_cr_lrps = {} + + self.router_lrp = mock.Mock() + self.router_lrp.mac = [ + "ff:ff:ff:ff:ff:01 192.168.1.1/24 fdcc:8cf2:d40c:2::1/64" + ] + + add_ips = ["192.168.1.1/24", "fdcc:8cf2:d40c:2::1/64"] + delete_ips = ["192.168.0.1/24"] + + self.bgp_driver._update_network( + self.router_port, "gateway_port", add_ips, delete_ips + ) + + mock_get_exposed_routes_on_network.assert_not_called() + mock_del_ip_route.assert_not_called() + mock_add_ip_route.assert_not_called() + self.sb_idl.get_port_by_name.assert_not_called() + + @mock.patch.object(linux_net, "get_exposed_routes_on_network") + @mock.patch.object(linux_net, "del_ip_route") + @mock.patch.object(linux_net, "add_ip_route") + def test__update_network_no_mac( + self, + mock_add_ip_route, + mock_del_ip_route, + mock_get_exposed_routes_on_network, + ): + gateway = {} + gateway["ips"] = [ + ipaddress.ip_interface(ip) + for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] + ] + gateway["address_scopes"] = self.addr_scope + self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} + + self.router_port.mac = [] + + add_ips = ["192.168.1.1/24", "fdcc:8cf2:d40c:2::1/64"] + delete_ips = ["192.168.0.1/24"] + + self.bgp_driver._update_network( + self.router_port, "gateway_port", add_ips, delete_ips + ) + + mock_get_exposed_routes_on_network.assert_not_called() + mock_del_ip_route.assert_not_called() + mock_add_ip_route.assert_not_called() + self.sb_idl.get_port_by_name.assert_not_called() + + def test_withdraw_subnet(self): + mock__withdraw_subnet = mock.patch.object( + self.bgp_driver, "_withdraw_subnet" + ).start() + + row = mock.Mock() + row.datapath = "fake-dp" + row.logical_port = "fake-lport" + port_info = { + "cr_lrp": self.cr_lrp0.logical_port, + "subnets": { + "fdcc:8cf2:d40c:2::/64", + "192.168.1.0/24" + } + } + + self.bgp_driver.propagated_lrp_ports = { + row.logical_port: port_info, + "another_lrp_port": {} + } + self.bgp_driver.ovn_local_cr_lrps = { + self.cr_lrp0.logical_port: { + "lrp_ports": set([row.logical_port, "another_lrp_port"]) + } + } + + self.bgp_driver.withdraw_subnet(None, row) + + mock__withdraw_subnet.assert_called_once_with( + port_info, self.cr_lrp0.logical_port + ) + self.assertDictEqual( + self.bgp_driver.propagated_lrp_ports, + { + "another_lrp_port": {} + } + ) + self.assertDictEqual( + self.bgp_driver.ovn_local_cr_lrps, + { + self.cr_lrp0.logical_port: { + "lrp_ports": set(["another_lrp_port"]) + } + } + ) + + @mock.patch.object(linux_net, "add_ip_route") + def test__ensure_network_exposed(self, mock_add_ip_route): + gateway = {} + gateway["ips"] = [ + ipaddress.ip_interface(ip) + for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] + ] + gateway["address_scopes"] = self.addr_scope + gateway["lrp_ports"] = set() + self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} + + self.router_lrp = mock.Mock() + self.router_lrp.mac = [ + "ff:ff:ff:ff:ff:01 192.168.1.1/24 fdcc:8cf2:d40c:2::1/64" + ] + + self.sb_idl.get_port_by_name.return_value = self.fake_patch_port + + self.bgp_driver._ensure_network_exposed( + self.router_port, "gateway_port" + ) + + self.sb_idl.get_port_by_name.assert_called_once_with( + "fake-logical-port" + ) + + expected_calls = [ + mock.call( + mock.ANY, + "10.0.0.0", + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=26, + via=None, + ), + mock.call( + mock.ANY, + "192.168.1.0", + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=24, + via="10.0.0.10", + ), + mock.call( + mock.ANY, + "fd51:f4b3:872:eda::", + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=64, + via=None, + ), + mock.call( + mock.ANY, + "fdcc:8cf2:d40c:2::", + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=64, + via="fd51:f4b3:872:eda::10", + ), + ] + + mock_add_ip_route.assert_has_calls(expected_calls) + self.assertDictEqual( + self.bgp_driver.ovn_local_cr_lrps, + { + 'gateway_port': { + 'address_scopes': { + 4: '11111111-1111-1111-1111-11111111', + 6: '22222222-2222-2222-2222-22222222'}, + 'ips': [ + ipaddress.IPv4Interface('10.0.0.10/26'), + ipaddress.IPv6Interface('fd51:f4b3:872:eda::10/64')], + 'lrp_ports': {'lrp-fake-logical-port'} + } + } + ) + self.assertDictEqual( + self.bgp_driver.propagated_lrp_ports, + { + "lrp-fake-logical-port": { + 'cr_lrp': 'gateway_port', + 'subnets': {'192.168.1.0/24', 'fdcc:8cf2:d40c:2::/64'} + } + } + ) + + @mock.patch.object(linux_net, "add_ip_route") + def test__ensure_network_exposed_invalid_addr_scopes( + self, + mock_add_ip_route + ): + gateway = {} + gateway["ips"] = [ + ipaddress.ip_interface(ip) + for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] + ] + + # Both of them are valid but none of them matches to the correct + # IP version + gateway["address_scopes"] = { + constants.IP_VERSION_4: self.addr_scopev6, + constants.IP_VERSION_6: self.addr_scopev4, + } + gateway["lrp_ports"] = set() + self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} + + self.router_lrp = mock.Mock() + self.router_lrp.mac = [ + "ff:ff:ff:ff:ff:01 192.168.1.1/24 fdcc:8cf2:d40c:2::1/64" + ] + + self.sb_idl.get_port_by_name.return_value = self.fake_patch_port + + self.bgp_driver._ensure_network_exposed( + self.router_port, "gateway_port" + ) + + self.sb_idl.get_port_by_name.assert_called_once_with( + "fake-logical-port" + ) + mock_add_ip_route.assert_not_called() + self.assertDictEqual( + self.bgp_driver.ovn_local_cr_lrps, + { + 'gateway_port': { + 'address_scopes': { + 4: '22222222-2222-2222-2222-22222222', + 6: '11111111-1111-1111-1111-11111111'}, + 'ips': [ + ipaddress.IPv4Interface('10.0.0.10/26'), + ipaddress.IPv6Interface('fd51:f4b3:872:eda::10/64')], + 'lrp_ports': set() + } + } + ) + self.assertDictEqual( + self.bgp_driver.propagated_lrp_ports, + {} + ) + + @mock.patch.object(linux_net, "add_ip_route") + def test__ensure_network_exposed_no_gateway(self, mock_add_ip_route): + self.bgp_driver.ovn_local_cr_lrps = {} + + self.bgp_driver._ensure_network_exposed( + self.router_port, "gateway_port" + ) + + self.sb_idl.get_port_by_name.assert_not_called() + mock_add_ip_route.assert_not_called() + self.assertDictEqual( + self.bgp_driver.propagated_lrp_ports, + {} + ) + + @mock.patch.object(linux_net, "add_ip_route") + def test__ensure_network_exposed_duplicate_ip(self, mock_add_ip_route): + gateway = {} + gateway["ips"] = [ + ipaddress.ip_interface(ip) + for ip in ["192.168.1.1/24", "fd51:f4b3:872:eda::10/64"] + ] + gateway["address_scopes"] = self.addr_scope + self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} + + self.bgp_driver._ensure_network_exposed( + self.router_port, "gateway_port" + ) + + self.sb_idl.get_port_by_name.assert_not_called() + mock_add_ip_route.assert_not_called() + self.assertDictEqual( + self.bgp_driver.propagated_lrp_ports, + {} + ) + + @mock.patch.object(linux_net, "add_ip_route") + def test__ensure_network_exposed_port_not_existing( + self, + mock_add_ip_route + ): + mock__get_addr_scopes = mock.patch.object( + self.bgp_driver, "_get_addr_scopes" + ).start() + gateway = {} + gateway["ips"] = [ + ipaddress.ip_interface(ip) + for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] + ] + gateway["address_scopes"] = self.addr_scope + self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} + + self.sb_idl.get_port_by_name.return_value = [] + self.bgp_driver._ensure_network_exposed( + self.router_port, "gateway_port" + ) + mock__get_addr_scopes.assert_not_called() + mock_add_ip_route.assert_not_called() + self.assertDictEqual( + self.bgp_driver.propagated_lrp_ports, + {} + ) + + @mock.patch.object(linux_net, "add_ip_route") + def test__ensure_network_exposed_port_addr_scope_no_match( + self, + mock_add_ip_route + ): + gateway = {} + gateway["ips"] = [ + ipaddress.ip_interface(ip) + for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] + ] + gateway["address_scopes"] = { + constants.IP_VERSION_4: "address_scope_v4", + constants.IP_VERSION_6: "address_scope_v6", + } + self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} + + self.sb_idl.get_port_by_name.return_value = self.fake_patch_port + self.bgp_driver._ensure_network_exposed( + self.router_port, "gateway_port" + ) + + self.sb_idl.get_port_by_name.assert_called_once_with( + "fake-logical-port" + ) + mock_add_ip_route.assert_not_called() + self.assertDictEqual( + self.bgp_driver.propagated_lrp_ports, + {} + ) + + @mock.patch.object(linux_net, "get_exposed_routes_on_network") + @mock.patch.object(linux_net, "del_ip_route") + def test__withdraw_subnet( + self, mock_del_ip_route, mock_get_exposed_routes_on_network + ): + gateway = {} + gateway["ips"] = [ + ipaddress.ip_interface(ip) + for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] + ] + gateway["address_scopes"] = self.addr_scope + gateway["lrp_ports"] = set([""]) + self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} + port_info = { + "cr_lrp": self.cr_lrp0.logical_port, + "subnets": { + "fdcc:8cf2:d40c:2::/64", + "192.168.1.0/24" + } + } + + mock_get_exposed_routes_on_network.side_effect = (["route-v4"], []) + + self.bgp_driver._withdraw_subnet(port_info, "gateway_port") + + expected_calls = [ + mock.call( + mock.ANY, + "192.168.1.0", + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=24, + via="10.0.0.10", + ), + mock.call( + mock.ANY, + "fdcc:8cf2:d40c:2::", + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=64, + via="fd51:f4b3:872:eda::10", + ), + mock.call( + mock.ANY, + "fd51:f4b3:872:eda::", + CONF.bgp_vrf_table_id, + CONF.bgp_nic, + vlan=None, + mask=64, + via=None, + ), + ] + + mock_del_ip_route.assert_has_calls(expected_calls) + + @mock.patch.object(linux_net, "get_exposed_routes_on_network") + @mock.patch.object(linux_net, "del_ip_route") + def test__withdraw_subnet_no_gateway( + self, mock_del_ip_route, mock_get_exposed_routes_on_network + ): + self.bgp_driver.ovn_local_cr_lrps = {} + self.bgp_driver._withdraw_subnet(self.router_port, "gateway_port") + mock_del_ip_route.assert_not_called() + mock_get_exposed_routes_on_network.assert_not_called() + + @mock.patch.object(linux_net, "delete_ip_routes") + @mock.patch.object(linux_net, "get_routes_on_tables") + def test_sync(self, mock_get_routes_on_tables, mock_delete_ip_routes): + def create_route(dst, dst_len, gateway): + m = mock.Mock([]) + m.dst = dst + m.dst_len = dst_len + m.gateway = gateway + return m + + def create_hashed_route(dst, dst_len, gateway): + return ovn_stretched_l2_bgp_driver.HashedRoute( + network=dst, + prefix_len=dst_len, + dst=gateway, + ) + + mock__expose_cr_lrp = mock.patch.object( + self.bgp_driver, "_expose_cr_lrp" + ).start() + + vrf_routes = [ + create_hashed_route(dst, dst_len, gateway) + for (dst, dst_len, gateway) in [ + ("192.168.1.0", 24, "10.0.0.1"), + ("10.0.0.0", 24, None), + ("fdcc:8cf2:d40c:2::", 64, "fd51:f4b3:872:eda::1"), + ("fd51:f4b3:872:eda::", 64, None), + ] + ] + + # really hacky way to get the routes into self.bgp_driver.vrf_routes + mock__expose_cr_lrp.side_effect = ( + lambda _, __: self.bgp_driver.vrf_routes.update(vrf_routes) + ) + + self.sb_idl.get_cr_lrp_ports.return_value = [self.cr_lrp0] + + delete_route = create_route("192.168.0.0", 24, "10.0.0.1") + routes = [ + create_route(dst, dst_len, gateway) + for (dst, dst_len, gateway) in [ + ("192.168.1.0", 24, "10.0.0.1"), + ("10.0.0.0", 24, None), + ("fdcc:8cf2:d40c:2::", 64, "fd51:f4b3:872:eda::1"), + ("fd51:f4b3:872:eda::", 64, None), + ] + ] + routes.append(delete_route) + + mock_get_routes_on_tables.return_value = routes + + self.bgp_driver.sync() + + mock_get_routes_on_tables.assert_called_once_with( + [CONF.bgp_vrf_table_id] + ) + mock_delete_ip_routes.assert_called_once_with([delete_route]) + mock__expose_cr_lrp.assert_called_once_with( + ["10.0.0.1/24", "fd51:f4b3:872:eda::1/64"], self.cr_lrp0 + ) + + def test_withdraw_ip(self): + mock__withdraw_cr_lrp = mock.patch.object( + self.bgp_driver, "_withdraw_cr_lrp" + ).start() + + self.bgp_driver.withdraw_ip(None, self.cr_lrp0) + + mock__withdraw_cr_lrp.assert_called_once_with(None, self.cr_lrp0) + + def test__withdraw_cr_lrp(self): + mock__withdraw_subnet = mock.patch.object( + self.bgp_driver, "_withdraw_subnet" + ).start() + + gateway = {} + gateway["ips"] = [ + ipaddress.ip_interface(ip) + for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] + ] + gateway["address_scopes"] = self.addr_scope + self.bgp_driver.ovn_local_cr_lrps = { + self.cr_lrp0.logical_port: gateway + } + gateway["lrp_ports"] = set([ + "lrp-lrp_port0", + "lrp-lrp_port1", + "lrp-lrp_port2", + ]) + + lrp_port0 = { + "cr_lrp": self.cr_lrp0.logical_port, + "subnets": set([ + "fdcc:8cf2:d40c:1::/64", + "192.168.0.0/24"]) + } + lrp_port1 = { + "cr_lrp": self.cr_lrp0.logical_port, + "subnets": set([ + "fdcc:8cf2:d40c:2::/64", + "192.168.1.0/24"]) + } + + self.bgp_driver.propagated_lrp_ports = { + "lrp-lrp_port0": lrp_port0, + "lrp-lrp_port1": lrp_port1, + } + self.bgp_driver.ovn_local_cr_lrps = { + self.cr_lrp0.logical_port: gateway + } + + self.bgp_driver._withdraw_cr_lrp(None, self.cr_lrp0) + mock__withdraw_subnet.assert_has_calls( + [ + mock.call(lrp_port0, self.cr_lrp0.logical_port), + mock.call(lrp_port1, self.cr_lrp0.logical_port), + ], + any_order=True + ) + self.assertDictEqual( + self.bgp_driver.ovn_local_cr_lrps, + {} + ) + self.assertDictEqual( + self.bgp_driver.propagated_lrp_ports, + {} + ) + + def test__withdraw_cr_lrp_invalid_addr_scope(self): + mock__withdraw_subnet = mock.patch.object( + self.bgp_driver, "_withdraw_subnet" + ).start() + + gateway = { + "address_scopes": { + constants.IP_VERSION_4: '', + constants.IP_VERSION_6: '', + } + } + gateway["lrp_ports"] = set([ + "lrp-lrp_port0", + "lrp-lrp_port1", + "lrp-lrp_port2", + ]) + self.bgp_driver.propagated_lrp_ports = { + "lrp-lrp_port0": {}, + "lrp-lrp_port1": {}, + "lrp-lrp_port2": {}, + } + self.bgp_driver.ovn_local_cr_lrps = { + self.cr_lrp0.logical_port: gateway + } + + self.bgp_driver._withdraw_cr_lrp(None, self.cr_lrp0) + + self.sb_idl.get_lrp_ports_for_router.assert_not_called() + mock__withdraw_subnet.assert_not_called() + + def test_expose_ip(self): + mock__expose_cr_lrp = mock.patch.object( + self.bgp_driver, "_expose_cr_lrp" + ).start() + + ips = ["10.0.0.1/24", "fd51:f4b3:872:eda::1/64"] + + self.bgp_driver.expose_ip(ips, self.cr_lrp0) + + mock__expose_cr_lrp.assert_called_once_with(ips, self.cr_lrp0) + + def test_expose_ip_invalid_type(self): + mock__expose_cr_lrp = mock.patch.object( + self.bgp_driver, "_expose_cr_lrp" + ).start() + + patch_port = mock.Mock() + patch_port.type = constants.OVN_PATCH_VIF_PORT_TYPE + + self.bgp_driver.expose_ip([], patch_port) + + mock__expose_cr_lrp.assert_not_called() + + def test__expose_cr_lrp(self): + mock__ensure_network_exposed = mock.patch.object( + self.bgp_driver, "_ensure_network_exposed" + ).start() + + lrp_port0 = fakes.create_object( + { + "name": "fake-port-lrp0", + "type": constants.OVN_PATCH_VIF_PORT_TYPE, + "chassis": "fake-chassis1", + "external_ids": {}, + "logical_port": "lrp-lrp_port0", + "options": { + "chassis-redirect-port": self.cr_lrp0.logical_port, + }, + } + ) + + lrp_port1 = fakes.create_object( + { + "name": "fake-port-lrp1", + "type": constants.OVN_PATCH_VIF_PORT_TYPE, + "mac": ["aa:bb:cc:dd:ee:ee 192.168.1.12 192.168.1.13"], + "logical_port": "lrp-lrp_port1", + "chassis": "fake-chassis1", + "external_ids": {}, + "options": {}, + } + ) + + lrp_port2 = fakes.create_object( + { + "name": "fake-port-lrp2", + "type": constants.OVN_PATCH_VIF_PORT_TYPE, + "mac": [], + "logical_port": "lrp-lrp_port2", + "chassis": "fake-chassis1", + "external_ids": {}, + "options": {}, + } + ) + + lrp_port3 = fakes.create_object( + { + "name": "fake-port-lrp3", + "type": constants.OVN_PATCH_VIF_PORT_TYPE, + "mac": [], + "logical_port": "lrp-lrp_port3", + "chassis": "", + "up": [False], + "external_ids": {}, + "options": {}, + } + ) + + lrp_port4 = fakes.create_object( + { + "name": "fake-port-lrp4", + "type": constants.OVN_PATCH_VIF_PORT_TYPE, + "mac": [], + "logical_port": "lrp-lrp_port4", + "chassis": "", + "up": [False], + "options": {}, + } + ) + + lrp_port5 = fakes.create_object( + { + "name": "fake-port-lrp5", + "type": constants.OVN_PATCH_VIF_PORT_TYPE, + "mac": [], + "logical_port": "lrp-lrp_port5", + "chassis": "", + "up": [False], + "options": { + "chassis-redirect-port": self.cr_lrp0.logical_port, + }, + } + ) + + self.sb_idl.get_lrp_ports_for_router.return_value = [ + lrp_port0, + lrp_port1, + lrp_port2, + lrp_port3, + lrp_port4, + lrp_port5, + ] + + self.sb_idl.get_port_by_name.return_value = self.fake_patch_port + + ips = ["10.0.0.1/24", "fd51:f4b3:872:eda::1/64"] + + self.bgp_driver._expose_cr_lrp(ips, self.cr_lrp0) + + mock__ensure_network_exposed.assert_has_calls( + [ + mock.call(lrp_port3, self.cr_lrp0.logical_port), + mock.call(lrp_port4, self.cr_lrp0.logical_port), + ] + ) + + self.sb_idl.get_port_by_name.assert_called_once_with("fake-port") + + self.assertEqual( + { + self.cr_lrp0.logical_port: { + "ips": [ipaddress.ip_interface(ip) for ip in ips], + "address_scopes": self.addr_scope, + "lrp_ports": set() + } + }, + self.bgp_driver.ovn_local_cr_lrps) + + def test__expose_cr_lrp_no_port(self): + mock__ensure_network_exposed = mock.patch.object( + self.bgp_driver, "_ensure_network_exposed" + ).start() + + self.sb_idl.get_port_by_name.return_value = [] + + ips = ["10.0.0.1/24", "fd51:f4b3:872:eda::1/64"] + + self.bgp_driver._expose_cr_lrp(ips, self.cr_lrp0) + + mock__ensure_network_exposed.assert_not_called() + self.sb_idl.get_port_by_name.assert_called_once_with("fake-port") + + def test__expose_cr_lrp_no_addr_scope(self): + mock__ensure_network_exposed = mock.patch.object( + self.bgp_driver, "_ensure_network_exposed" + ).start() + mock__get_addr_scopes = mock.patch.object( + self.bgp_driver, "_get_addr_scopes" + ).start() + + self.sb_idl.get_port_by_name.return_value = self.fake_patch_port + + mock__get_addr_scopes.return_value = { + constants.IP_VERSION_4: "address_scope_v4", + constants.IP_VERSION_6: "address_scope_v6", + } + + self.bgp_driver._expose_cr_lrp([], self.cr_lrp0) + + self.sb_idl.get_port_by_name.assert_called_once_with("fake-port") + mock__get_addr_scopes.assert_called_once_with(self.fake_patch_port) + self.sb_idl.get_lrp_ports_for_router.assert_not_called() + mock__ensure_network_exposed.assert_not_called() + + def test_expose_remote_ip(self): + self.assertRaises( + NotImplementedError, + self.bgp_driver.expose_remote_ip, + "1.2.3.4/24") + + def test_withdraw_remote_ip(self): + self.assertRaises( + NotImplementedError, + self.bgp_driver.withdraw_remote_ip, + "1.2.3.4/24") diff --git a/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovn.py b/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovn.py index c46da3e9..ee97477a 100644 --- a/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovn.py +++ b/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovn.py @@ -291,6 +291,28 @@ class TestOvsdbSbOvnIdl(test_base.TestCase): def test_is_router_gateway_on_chassis_not_on_chassis(self): self._test_is_router_gateway_on_chassis(match=False) + def _test_is_router_gateway_on_any_chassis(self, match=True): + if match: + ch = fakes.create_object({'name': 'chassis-0'}) + else: + ch = fakes.create_object({'name': ''}) + port = '39c38ce6-f0ea-484e-a57c-aec0d4e961a5' + with mock.patch.object(self.sb_idl, 'get_ports_on_datapath') as m_dp: + row = fakes.create_object({'logical_port': port, 'chassis': [ch]}) + m_dp.return_value = [row, ] + ret = self.sb_idl.is_router_gateway_on_any_chassis('fake-dp') + + if match: + self.assertEqual(row, ret) + else: + self.assertIsNone(ret) + + def test_is_router_gateway_on_any_chassis(self): + self._test_is_router_gateway_on_any_chassis() + + def test_is_router_gateway_on_chassis_not_on_any_chassis(self): + self._test_is_router_gateway_on_any_chassis(match=False) + def _test_get_lrp_port_for_datapath(self, has_options=True): peer = '75c793bd-d865-48f3-8f05-68ba4239d14e' with mock.patch.object(self.sb_idl, 'get_ports_on_datapath') as m_dp: diff --git a/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_bgp_watcher.py b/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_bgp_watcher.py index 8c439811..ce57e4e1 100644 --- a/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_bgp_watcher.py +++ b/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_bgp_watcher.py @@ -391,6 +391,87 @@ class TestSubnetRouterAttachedEvent(test_base.TestCase): self.agent.expose_subnet.assert_not_called() +class TestSubnetRouterUpdateEvent(test_base.TestCase): + + def setUp(self): + super(TestSubnetRouterUpdateEvent, self).setUp() + self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' + self.agent = mock.Mock(chassis=self.chassis) + self.event = bgp_watcher.SubnetRouterUpdateEvent(self.agent) + + def test_match_fn(self): + row = utils.create_row(chassis=[], logical_port='lrp-fake', + mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], + options={}) + self.assertTrue(self.event.match_fn(mock.Mock(), row, mock.Mock())) + + def test_match_fn_not_single_or_dual_stack(self): + row = utils.create_row(chassis=[], logical_port='lrp-fake', + mac=['aa:bb:cc:dd:ee:ff'], + options={}) + old = utils.create_row(chassis=[], logical_port='lrp-fake', + mac=['aa:bb:cc:dd:ee:ff'], + options={}) + self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) + + def test_match_fn_not_lrp(self): + row = utils.create_row(chassis=[], logical_port='fake-lp', + mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], + options={}) + self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) + + def test_match_fn_chassis_redirect(self): + row = utils.create_row(chassis=[], logical_port='lrp-fake', + mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], + options={'chassis-redirect-port': True}) + self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) + + def test_match_fn_chassis_set(self): + row = utils.create_row(chassis=[mock.Mock()], logical_port='lrp-fake', + mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], + options={}) + self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) + + def test_match_fn_mac_not_changed(self): + row = utils.create_row(chassis=[], logical_port='lrp-fake', + mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], + options={}) + old = utils.create_row(chassis=[], logical_port='lrp-fake', + mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], + options={}) + self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) + + def test_match_fn_mac_changed(self): + row = utils.create_row(chassis=[], logical_port='lrp-fake', + mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], + options={}) + old = utils.create_row(chassis=[], logical_port='lrp-fake', + mac=['aa:bb:cc:dd:ee:ff 10.10.1.16 10.10.1.17'], + options={}) + self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) + + def test_match_fn_index_error(self): + row = utils.create_row(chassis=[], logical_port='lrp-fake', + mac=[], options={}) + self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) + + def test_run(self): + row = utils.create_row(type=constants.OVN_PATCH_VIF_PORT_TYPE, + mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) + old = utils.create_row(type=constants.OVN_PATCH_VIF_PORT_TYPE, + mac=['aa:bb:cc:dd:ee:ff']) + self.event.run(mock.Mock(), row, old) + self.agent.update_subnet.assert_called_once_with(old, row) + + def test_run_wrong_type(self): + row = utils.create_row(type='coxinha', + mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) + old = utils.create_row(type='coxinha', + mac=['aa:bb:cc:dd:ee:ff']) + self.event.run(mock.Mock(), row, old) + self.agent.update_subnet.assert_not_called() + + class TestSubnetRouterDetachedEvent(test_base.TestCase): def setUp(self): diff --git a/ovn_bgp_agent/tests/unit/utils/test_linux_net.py b/ovn_bgp_agent/tests/unit/utils/test_linux_net.py index 3b289261..9fa7aae0 100644 --- a/ovn_bgp_agent/tests/unit/utils/test_linux_net.py +++ b/ovn_bgp_agent/tests/unit/utils/test_linux_net.py @@ -39,6 +39,9 @@ class TestLinuxNet(test_base.TestCase): self.dev = 'ethfake' self.mac = 'aa:bb:cc:dd:ee:ff' self.bridge = 'br-fake' + self.table_id = 100 + self.network = ipaddress.IPv4Network("10.10.1.0/24") + self.network_v6 = ipaddress.IPv6Network("2002:0:0:1234:0:0:0:0/64") def test_get_ip_version_v4(self): self.assertEqual(4, linux_net.get_ip_version('%s/32' % self.ip)) @@ -238,6 +241,66 @@ class TestLinuxNet(test_base.TestCase): self.assertEqual([self.ip, self.ipv6], ret) + def test_get_exposed_routes_on_network_v4(self): + route0 = mock.MagicMock( + dst=mock.Mock(), + table=self.table_id, + scope=1, + proto=11, + gateway=self.ip, + ) + route1 = mock.MagicMock( + dst=mock.Mock(), + table=self.table_id, + scope=1, + proto=11, + gateway=self.ipv6, + ) + route2 = mock.MagicMock( + dst=mock.Mock(), + table=self.table_id, + scope=1, + proto=11, + gateway=None, + ) + + self.fake_ndb.routes.dump.return_value = [route0, route1, route2] + ret = linux_net.get_exposed_routes_on_network( + [self.table_id], self.network + ) + + self.assertEqual([route0], ret) + + def test_get_exposed_routes_on_network_v6(self): + route0 = mock.MagicMock( + dst=mock.Mock(), + table=self.table_id, + scope=1, + proto=11, + gateway=self.ip, + ) + route1 = mock.MagicMock( + dst=mock.Mock(), + table=self.table_id, + scope=1, + proto=11, + gateway=self.ipv6, + ) + route2 = mock.MagicMock( + dst=mock.Mock(), + table=self.table_id, + scope=1, + proto=11, + gateway=None, + ) + + self.fake_ndb.routes.dump.return_value = [route0, route1, route2] + ret = linux_net.get_exposed_routes_on_network( + [self.table_id], self.network_v6 + ) + + self.assertEqual([route1], ret) + def test_get_ovn_ip_rules(self): rule0 = mock.Mock(table=7, dst=10, dst_len=128, family='fake') rule1 = mock.Mock(table=7, dst=11, dst_len=32, family='fake') diff --git a/ovn_bgp_agent/utils/linux_net.py b/ovn_bgp_agent/utils/linux_net.py index 0a4802b1..334d90b8 100644 --- a/ovn_bgp_agent/utils/linux_net.py +++ b/ovn_bgp_agent/utils/linux_net.py @@ -282,6 +282,20 @@ def get_exposed_ips_on_network(nic, network): return exposed_ips +def get_exposed_routes_on_network(table_ids, network): + with pyroute2.NDB() as ndb: + # NOTE: skip bgp routes (proto 186) + return [ + r + for r in ndb.routes.dump() + if r.table in table_ids and + r.dst != "" and + r.gateway is not None and + r.proto != 186 and + ipaddress.ip_address(r.gateway) in network + ] + + def get_ovn_ip_rules(routing_table): # get the rules pointing to ovn bridges ovn_ip_rules = {} diff --git a/setup.cfg b/setup.cfg index b5593f4c..d5b03b5a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = ovn-bgp-agent -summary = The OVN BGP Agent allows to expose VMs/Containers through BGP on OVN +summary = The OVN BGP Agent allows to expose VMs/Containers/Networks through BGP on OVN description-file = README.rst author = OpenStack @@ -37,6 +37,7 @@ console_scripts = ovn_bgp_agent.drivers = ovn_bgp_driver = ovn_bgp_agent.drivers.openstack.ovn_bgp_driver:OVNBGPDriver ovn_evpn_driver = ovn_bgp_agent.drivers.openstack.ovn_evpn_driver:OVNEVPNDriver + ovn_bgp_stretched_l2_driver = ovn_bgp_agent.drivers.openstack.ovn_stretched_l2_bgp_driver:OVNBGPStretchedL2Driver oslo.config.opts = ovnbgpagent = ovn_bgp_agent.config:list_opts