From f5ef3c8f3109ba6f3f8d67caa4c28ca4392c364f Mon Sep 17 00:00:00 2001 From: Luis Tomas Bolivar Date: Mon, 30 Aug 2021 12:59:23 +0200 Subject: [PATCH] Initial support for BGP Change-Id: Ieed45b80e2860c94a42a8d5d16f5dfe7b515bf2c --- README.rst | 4 +- doc/images/evpn_traffic_flow.png | Bin 0 -> 107697 bytes doc/images/networking-bgpvpn_integration.png | Bin 0 -> 97580 bytes doc/source/contributor/bgp_mode_design.rst | 43 ++ doc/source/contributor/evpn_mode_design.rst | 468 ++++++++++++ doc/source/contributor/index.rst | 10 + doc/source/index.rst | 1 + ovn_bgp_agent/agent.py | 77 ++ ovn_bgp_agent/cmd/__init__.py | 0 ovn_bgp_agent/cmd/agent.py | 20 + ovn_bgp_agent/config.py | 65 ++ ovn_bgp_agent/constants.py | 41 + ovn_bgp_agent/drivers/__init__.py | 0 ovn_bgp_agent/drivers/driver_api.py | 56 ++ ovn_bgp_agent/drivers/openstack/__init__.py | 0 .../drivers/openstack/ovn_bgp_driver.py | 702 ++++++++++++++++++ .../drivers/openstack/utils/__init__.py | 0 ovn_bgp_agent/drivers/openstack/utils/frr.py | 144 ++++ ovn_bgp_agent/drivers/openstack/utils/ovn.py | 249 +++++++ ovn_bgp_agent/drivers/openstack/utils/ovs.py | 319 ++++++++ .../drivers/openstack/watchers/__init__.py | 0 .../drivers/openstack/watchers/bgp_watcher.py | 270 +++++++ ovn_bgp_agent/tests/unit/__init__.py | 0 ovn_bgp_agent/tests/unit/cmd/__init__.py | 0 ovn_bgp_agent/tests/unit/cmd/test_agent.py | 27 + ovn_bgp_agent/tests/unit/test_agent.py | 38 + ovn_bgp_agent/utils/__init__.py | 0 ovn_bgp_agent/utils/linux_net.py | 684 +++++++++++++++++ requirements.txt | 11 + setup.cfg | 7 + tox.ini | 2 +- 31 files changed, 3236 insertions(+), 2 deletions(-) create mode 100644 doc/images/evpn_traffic_flow.png create mode 100644 doc/images/networking-bgpvpn_integration.png create mode 100644 doc/source/contributor/bgp_mode_design.rst create mode 100644 doc/source/contributor/evpn_mode_design.rst create mode 100644 doc/source/contributor/index.rst create mode 100644 ovn_bgp_agent/agent.py create mode 100644 ovn_bgp_agent/cmd/__init__.py create mode 100644 ovn_bgp_agent/cmd/agent.py create mode 100644 ovn_bgp_agent/config.py create mode 100644 ovn_bgp_agent/constants.py create mode 100644 ovn_bgp_agent/drivers/__init__.py create mode 100644 ovn_bgp_agent/drivers/driver_api.py create mode 100644 ovn_bgp_agent/drivers/openstack/__init__.py create mode 100644 ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py create mode 100644 ovn_bgp_agent/drivers/openstack/utils/__init__.py create mode 100644 ovn_bgp_agent/drivers/openstack/utils/frr.py create mode 100644 ovn_bgp_agent/drivers/openstack/utils/ovn.py create mode 100644 ovn_bgp_agent/drivers/openstack/utils/ovs.py create mode 100644 ovn_bgp_agent/drivers/openstack/watchers/__init__.py create mode 100644 ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py create mode 100644 ovn_bgp_agent/tests/unit/__init__.py create mode 100644 ovn_bgp_agent/tests/unit/cmd/__init__.py create mode 100644 ovn_bgp_agent/tests/unit/cmd/test_agent.py create mode 100644 ovn_bgp_agent/tests/unit/test_agent.py create mode 100644 ovn_bgp_agent/utils/__init__.py create mode 100644 ovn_bgp_agent/utils/linux_net.py diff --git a/README.rst b/README.rst index 9551c41b..9172335c 100644 --- a/README.rst +++ b/README.rst @@ -12,4 +12,6 @@ The OVN BGP Agent allows to expose VMs/Containers through BGP on OVN Features -------- -* TODO +* Expose VMs with FIPs or on Provider Networks through BGP on OVN + environments. + diff --git a/doc/images/evpn_traffic_flow.png b/doc/images/evpn_traffic_flow.png new file mode 100644 index 0000000000000000000000000000000000000000..0dc83afbbbfbe63810203131b66e45ae9b401469 GIT binary patch literal 107697 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%0Dm^kEh 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⩔#Jh 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 ZiZ^X~AFFF@es~7Hl;t(#-pg17{y+bEGa>*0 literal 0 HcmV?d00001 diff --git a/doc/images/networking-bgpvpn_integration.png b/doc/images/networking-bgpvpn_integration.png new file mode 100644 index 0000000000000000000000000000000000000000..fdd21f9604d3de37e3545d233294e987157dfbcd GIT binary patch literal 97580 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

`J|gvM0oK6@Nx`)bz9v=ozKU!Bc}; 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

{K)>_;2q!A}Z@OKn9EM>v3#*ECuDN5<< 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 XR)M2BXsEn;G5ekT94!`=S_Q_6!} literal 0 HcmV?d00001 diff --git a/doc/source/contributor/bgp_mode_design.rst b/doc/source/contributor/bgp_mode_design.rst new file mode 100644 index 00000000..99e98bfd --- /dev/null +++ b/doc/source/contributor/bgp_mode_design.rst @@ -0,0 +1,43 @@ +.. + 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.) + +=================================== +Design of OVN Agent with BGP Driver +=================================== + +Purpose +------- + +Overview +-------- + +With the increment of virtualized/containerized workloads it is becoming more +and more common to use pure layer-3 Spine and Leaf network deployments at +datacenters. There are several benefits of this, such as reduced complexity at +scale, reduced failures domains, limiting broadcast traffic, among others. + +Proposed Solution +----------------- + +OVN SB DB Events +~~~~~~~~~~~~~~~~ + +Driver Logic +~~~~~~~~~~~~ + +Traffic flow +~~~~~~~~~~~~ + +Agent deployment +~~~~~~~~~~~~~~~~ \ No newline at end of file diff --git a/doc/source/contributor/evpn_mode_design.rst b/doc/source/contributor/evpn_mode_design.rst new file mode 100644 index 00000000..72d000e6 --- /dev/null +++ b/doc/source/contributor/evpn_mode_design.rst @@ -0,0 +1,468 @@ +.. + 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.) + +==================================== +Design of OVN Agent with EVPN Driver +==================================== + +Purpose +------- + +The purpose of this document is to present the design decision behind +the EVPN Driver for the Networking BGP OVN agent. + +The main purpose of adding support for EVPN is to be able to provide +multitenancy aspects by using BGP in conjunction with EVPN/VXLAN. It allows +tenants to have connectivity between VMs running in different clouds, +with overlapping subnet CIDRs among tenants. + + +Overview +-------- + +The networking bgp ovn agent is a Python based daemon that runs on each node +(e.g., OpenStack controllers and/or compute nodes). It connects to the OVN +Southbound DataBase (OVN SB DB) to detect the specific events it needs to +react to, and then leverages FRR to expose the routes towards the VMs, and +kernel networking capabilities to redirect the traffic once on the nodes to +the OVN overlay. + +This simple design allows the agent to implement different drivers, depending +on what OVN SB DB events are being watched (watchers examples at +``networking_bgp_onn/drivers/openstack/watchers/``), and what actions are +triggered in reaction to them (drivers examples at +``networking_bgp_ovn/drivers/openstack/XXXX_driver.py``, implementing the +``networking_bgp_von/drivers/driver_api.py``). + +A new driver implements the support for EVPN capabilities with multitenancy +(overlapping CIDRs), by leveraging VRFs and EVPN Type-5 Routes. The API used +is the ``networking_bgpvpn`` upstream project, and a new watcher is created to +react to the information being added by it into the OVN SB DB (using the +``external-ids`` field). + + +Proposed Solution +----------------- + +To support EVPN the functionality of the ``networking-bgp-ovn`` agent needs +to be extended with a new driver that performs the extra steps +required for the EVPN configuration and steering the traffic to/from the node +from/to the OVN overlay. The only configuration needed is to enable the +specific driver on the ``bgp-agent.conf`` file. + +This new driver will also require a new watcher to react to the EVPN-related +events. In this case, the EVPN events will be triggered by the addition of +EVPN/VNI information into the relevant OVN ``logical_switch_ports`` at the +OVN SB DB. + +This information is added into OVN DBs by the ``networking-bgpvpn`` projects. +The admin and user API to leverage the EVPN functionality is provided by +extending the ``networking-bgpvpn`` upstream project with a new service plugin +for ML2/OVN. This plugin will annotate the needed information regarding VNI +ids into the OVN DBs by using the ``external-ids`` field. + + +BGPVPN API +~~~~~~~~~~ + +To allow users to expose their tenant networks through EVPN, without worring +about overlapping CIDRs from other tenants, the ``networking-bgpvpn`` +upstream project is leveraged as the API. It fits nicely as it has: + +- An Admin API to define the BGPVPN properties, such as the VNI or the BGP AS + to be used, and to associate it to a given tenant. + +- A Tenant API to allow users to associate the BGPVPN to a router or to a + network. + +This provides an API that allows users to expose their tenant networks, and +admins to provide the needed EVPN/VNI information. Then, we need to enhance +``networking-bgpvpn`` with ML2/OVN support so that the provided information +is stored on the OVN SB DB and consumed by the new driver (when the +watcher detects it). + +The overall arquitecture and integration between the ``networking-bgpvpn`` +and the ``networking-bgp-ovn`` agent are shown in the next figure: + +.. image:: ../../images/networking-bgpvpn_integration.png + :alt: integration components + :align: center + :width: 100% + +There are 3 main components: + +- ``BGPVPN API``: This is the component that enables the association of RT/VNIs + to tenant network/routers. It creates a couple of extra DBs on Neutron to + keep the information. This is the component we leverage, restricting some + of the APIs. + +- ``OVN Service Plugin Driver``: (for ml2/ovs, the equivalent is the bagpipe + driver) This is the component in charge of triggering the extra actions to + notify the backend driver about the changes needed (RPCs for the ml2/ovs + bagpipe driver). In our case it is a simple driver that just integrates with + OVN (OVN NB DB) to ensure the information gets propagated to the + corresponding OVN resource in the OVN Southbound database — by adding the + information into the external_ids field. The Neutron ML2/OVN driver already + copies the external_ids information of the ports from the + ``Logical_Switch_Port`` table at the OVN NB DB into the ``Port_Binding`` + table at the OVN SB DB. Thus the new OVN service plugin driver only needs + to annotate the relevant ports at the ``Logical_Switch_Port`` table with + the required EVPN information (BGP AS number and VNI number) on the + ``external_ids`` field. Then, it gets automatically translated into the + OVN SB DB at the ``Port_Binding`` table, ``external_ids`` field, and + the OVN BGP Agent can react to it. + +- ``Backend driver``, i.e., the networking-bgp-ovn with the EVPN driver: + (for ml2/ovs, the equivalent is the bagpipe-bgp project) + This is the backend driver running on the nodes, in charge of configuring + the networking layer based on the needs. In this case, the agent continues + to consume information from the OVN SB DB (reading the extra information + at external_ids, instead of relying on RPC as in the bagpipe-bgp case), and + adds the needed kernel routing and FRR configuration, as well as OVS flows + to steer the traffic to/from OVN overlay. + + +As regards to the API actions implemented, the user can: + +- Associate the BGPVPN to a network: + The OVN service plugin driver annotates the information into the + ``external_ids`` field of the ``Logical_Switch_Port`` associated to the + network router interface port (ovn patch port). Additionally, the router + where the network is connected also gets the ``Logical_Switch_Port`` + associated to the router gateway port annotated (ovn patch port). + +- Associate the BGPVPN to a router: + The OVN service plugin driver performs the same actions as before, but + annotating all the router interface ports connected to the router (i.e., + all the subnets attached to the router). + + +OVN SB DB Events +~~~~~~~~~~~~~~~~ + +The networking-bgp-ovn watcher that the EVPN driver uses need to detect the +relevant events on the OVN SB DB to call the driver functions to configure +EVPN. +When the VNI information is added/updated/delete to either a router gateway +port (patch port on the Port_Binding table) or a router interface port (also +a patch port on the Port_Binding table), it is clear that some actions need +to be trigger. +However there are other events that should be processed such as: + +- VM creation on a exposed network/router + +- Router exposed being attached/detached from the provider network + +- Subnet exposed being attached/detached from the router + + +The EVPN watcher detects OVN SB DB events of ``RowEvent`` type at the +``Port_Binding`` table. It creates a new event class named +``PortBindingChassisEvent``, that all the rest extend. +The EVPN watcher reacts to the same type of events as the BGP watcher, but +with some differences. Also, it does not react to FIPs related events as +EVPN is only used for tenant networks. + +The specific defined events to react to are: + +- ``PortBindingChassisCreatedEvent`` (set gateway port for router): + Detects when a port of type ``chassisredirect`` gets attached to the OVN + chassis where the agent is running. This is the case for neutron gateway + router ports (CR-LRPs). It calls ``expose_ip`` driver method to decide if + it needs to expose it through EVPN (in case it has related EVPN info + annotated). + +- ``PortBindingChassisDeletedEvent`` (unset gateway port for router): + Detects when a port of type ``chassisredirect`` gets detached from the OVN + chassis where teh agent is running. This is the case for neutron gateway + router ports (CR-LRPs). It calls ``withdraw_ip`` driver method to decide if + it needs to withdraw the exposed EVPN route (in case it had EVPN info + annotated). + +- ``SubnetRouterAttachedEvent`` (add BGPVPN to router/network or attach + subnet to router): Detects when a port of type ``patch`` gets + created/updated with EVPN information (VNI and BGP_AS). These type of + ports can be of 2 types: + + 1) related to the router gateway port and therefore calling the + ``expose_ip`` method, as in the ``PortBindingChassisCreateEvent``. The + different is that in ``PortBindingChassisCreateEvent`` event the port was + being created as a result of attaching the router to the provider network, + while in the ``SubnetRouterAttachedEvent`` event the port was already there + but information related to EVPN was added, i.e., the router was exposed by + associating it a BGPVPN. + + 2) related to the router interface port and therefore calling the + ``expose_subnet`` method. This method will check if the associated gateway + port is on the local chassis (where the agent runs) to proceed with the + configuration steps to redirect the traffic to/from OVN overlay. + +- ``SubnetRouterDetachedEvent`` (remove BGPVPN from router/network or detach + subnet from router): Detects when a port of type ``patch`` gets either + updated (removal of EVPN information) or directly deleted. The same 2 type + of ports as in the previous event can be found, and the method + ``withdraw_ip`` or ``withdraw_subnet`` are called for router gateway and + router interface ports, respectively. + +- ``TenantPortCreatedEvent`` (VM created): + Detects when a port of type ``""`` or ``virtual`` gets updated (chassis + added). It calls the method ``expose_remote_ip``. The method checks if + the port is not on a provider network and the chassis where the agent is + running has the gateway port for the router the VM is connected to. + +- ``TenantPortDeletedEvent`` (VM deleted): + Detects when a port of type ``""`` or ``virtual`` gets updated (chassis + deleted) or deleted. It calls the method ``withdraw_remote_ip``. The method + checks if the port is not on a provider network and the chassis where the + agent is running has the gateway port for the router the VM is connected to. + + +Driver Logic +~~~~~~~~~~~~ + +The EVPN driver is in charge of the networking configuration ensuring that +VMs on tenant networks can be reached through EVPN (N/S traffic). To acomplish +this, it needs to ensure that: + +- VM IPs can be advertized in a node where the traffic could be injected into + OVN overlay, in this case the node where the router gateway port is + scheduled (see limitations subsection). + +- Once the traffic reaches the specific node, the traffic is redirected to the + OVN overlay. + +To do that it needs to: + +1. Create the EVPN related devices when a router gets attached to the provider + network and/or gets a BGPVPN assigned to it. + + - Create the VRF device, using the VNI number as the routing table number + associated to it, as well as for the name suffix: vrf-1001 for vni 1001 + + .. code-block:: ini + + ip link add vrf-1001 type vrf table 1001 + + - Create the VXLAN device, using the VNI number as the vxlan id, as well as + for the name suffix: vxlan-1001 + + .. code-block:: ini + + ip link add vxlan-1001 type vxlan id 1001 dstport 4789 local LOOPBACK_IP nolearning + + - Create the Bridge device, where the vxlan device is connected, and + associate it to the created vrf, also using the VNI number as name suffix: + br-1001 + + .. code-block:: ini + + ip link add name br-1001 type bridge stp_state 0 + ip link set br-1001 master vrf-1001 + ip link set vxlan-1001 master br-1001 + + - Create a dummy device, where the IPs to be exposed will be added. It is + associated to the created vrf, and also using the VNI number as name + suffix: lo-1001 + + .. code-block:: ini + + ip link add name lo-1001 type dummy + ip link set lo-1001 master vrf-1001 + + .. note:: + + The VRF is not associated to an OpenStack tenant but to a router + gateway ports, meaning that if a tenant has several Neutron routers + connected to the provider network, it will have a different VRFs, one + associated with each one of them. + +2. Reconfigure local FRR instance (``frr.conf``) to ensure the new VRF is + exposed. 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 + + ADD_VRF_TEMPLATE = ''' + vrf {{ vrf_name }} + vni {{ vni }} + exit-vrf + + router bgp {{ bgp_as }} vrf {{ vrf_name }} + address-family ipv4 unicast + redistribute connected + exit-address-family + address-family ipv6 unicast + redistribute connected + exit-address-family + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family + + ''' + +3. Connect EVPN to OVN overlay so that traffic can be redirected from the node + to the OVN virtual networking. It needs to: + + - Attach the VRF device to the OVS provider bridge (e.g., br-ex) + + .. code-block:: ini + + ovs-vsctl add-port br-ex vrf-1001 + + - Add route on the VRF routing table for both the router gateway port IP + and the subnet CIDR so that the traffic is redirected to the OVS provider + bridge (e.g., br-ex) + + .. code-block:: ini + + $ ip route show vrf vrf-1001 + 10.0.0.0/26 via 172.24.4.146 dev br-ex + 172.24.4.146 dev br-ex scope link + +4. Add needed OVS flows into the OVS provider bridge (e.g., br-ex) to redirect + the traffic back from OVN to the proper VRF, based on the subnet CIDR and + the router gateway port MAC address. + + .. code-block:: ini + + $ ovs-ofctl add-flow br-ex cookie=0x3e7,priority=1000,ip,in_port=1,dl_src:ROUTER_GATEWAY_PORT_MAC,nw_src=SUBNET_CIDR, actions=mod_dl_dst:BR_EX_MAC,output=VRF_PORT + +5. Add IPs to expose to VRF associated dummy device. This interface is only + used for the purpose of exposing the IPs, but not meant to receive the + traffic. Thus, the local route being automatically added pointing to the + dummy interface on the VRF for that (VM) IP is removed so that the traffic + can get redirected properly to the OVN overlay. + + .. code-block:: ini + + $ ip addr add 10.0.0.5/32 dev lo-1001 + $ ip route show vrf table 1001 | grep local + 10.0.0.5 dev lo-1001 + $ ip route delete local 10.0.0.5 dev 1001 table 1001 + + +Driver API +++++++++++ + +The EVPN driver needs to implement the ``driver_api.py`` interface. +It implements the next functions: + +- ``expose_ip``: Creates all the VRF/VXLAN configuration (devices and its + connection to the OVN overlay) as well as the VRF configuration at FRR + (steps 1 to 3). It also checks if there are subnets and VMs connected to + the ovn gateway router port that must be exposed through EVPN (steps 4-5). + +- ``withdraw_ip``: removes the above configuration (devices and FRR + configuration). + +- ``expose_subnet``: add kernel and ovs networking configuration to ensure + traffic can go from the node to the OVN overlay, and viceversa, for IPs + within the subnet CIDR and on the right VRF -- step 4. + +- ``withdraw_subnet``: removes the above kernel and ovs networking + configuration. + +- ``expose_remote_ip``: EVPN expose VM tenant network IPs through the chassis + hosting the ovn gateway port for the router where the VM is connected. + It ensures traffic destinated to the VM IP arrives to this node (step 5). + The previous steps ensure the traffic is redirected to the OVN overlay + once on the node. + +- ``withdraw_remote_ip``: EVPN withdraw VM tenant network IPs through the + chassis hosting the ovn gateway port for the router where the VM is + connected. It ensures traffic destinated to the VM IP stops arriving to + this node. + + +Traffic flow +~~~~~~~~~~~~ + +The next figure shows the N/S traffic flow through the VRF to the VM, +including information regarding the OVS flows on the provider bridge (br-ex), +and the routes on the VRF routing table. + +.. image:: ../../images/evpn_traffic_flow.png + :alt: integration components + :align: center + :width: 100% + + +The IPs of both the router gateway port (cr-lrp, 172.24.1.20), as well as the +IP of the VM itself (20.0.0.241/32) gets added to the dummy device (lo-101) +associated to the vrf (vrf-101) which was used for defining the BGPVPN +(vni 101). That together with the other devices created on the VRF (vxlan-101 +and br-101), and with the FRR reconfiguration ensure the IPs get exposed in +the right EVPN. This allows the traffic to reach the node with the router +gateway port (cr-lrp on ovn). + +However this is not enough as the traffic needs to be redirected to the OVN +Overlay. To do that the VRF is added to the br-ex OVS provider bridge (br-ex), +and two routes are added to the VRF routing table to redirect the traffic +going to the network (20.0.0.0/24) through the CR-LRP port to the br-ex OVS +bridge. +That injects the traffic properly into the OVN overlay, which will redirect +it through the geneve tunnel (by the br-int ovs flows) to the compute node +hosting the VM. The reply from the VM will come back through the same tunnel. +However an extra OVS flow needs to be added to the OVS provider bridge (br-ex) +to ensure the traffic is redirected back to the VRF (vrf-101) if the traffic +is coming from the exposed network (20.0.0.0/24) -- instead of using the +default routing table (action=NORMAL). To that end, the next rule is added: + +.. code-block:: ini + + cookie=0x3e6, duration=4.141s, table=0, n_packets=0, n_bytes=0, priority=1000,ip,in_port="patch-provnet-c",dl_src=fa:16:3e:b7:cc:47,nw_src=20.0.0.0/24 actions=mod_dl_dst:1e:8b:ac:5d:98:4a,output:"vrf-101" + +It matches the traffic coming from the router gateway port (cr-lrp port) from +br-int (in_port="patch-provnet-c"), with the MAC address of the router gateway +port (dl_src=fa:16:3e:b7:cc:47) and from the exposed network (nw_src=20.0.0.0/24). +For that case it changes the MAC by the br-ex device one +(mod_dl_dst:1e:8b:ac:5d:98:4a), and redirect the traffic to the vrf device +(output:"vrf-101"). + + +Agent deployment +~~~~~~~~~~~~~~~~ + +The EVPN mode exposes the VMs on tenant networks (on their respective +EVPN/VXLAN). At OpenStack, with OVN networking, the N/S traffic to the +tenant VMs (without FIPs) needs to go through the networking nodes, more +specifically the one hosting the chassisredirect ovn port (cr-lrp), connecting +the provider network to the OVN virtual router. As a result, there is no need +to deploy the agent in all the nodes. Only the nodes that are able to host +router gateway ports (cr-lrps), i.e., the ones tagged with the +``enable-chassis-gw``. Hence, the VM IPs are advertised through BGP/EVPN in +one of those nodes, and from there it follows the normal path to the OpenStack +compute node where the VM is allocated — the Geneve tunnel. + + +Limitations +----------- + +The following limitations apply: + +- Network traffic is steer by kernel routing (VRF, VXLAN, Bridges), therefore + DPDK, where the kernel space is skipped, is not supported + +- Network traffic is steer by kernel routing (VRF, VXLAN, Bridges), therefore + SRIOV, where the hypervisor is skipped, is not supported. + +- In OpenStack with OVN networking the N/S traffic to the tenant VMs (without + FIPs) needs to go through the networking nodes (the ones hosting the Neutron + Router Gateway Ports, i.e., the chassisredirect cr-lrp ports). Therefore, the + entry point into the OVN overlay need to be one of those networking nodes, + and consequently the VMs are exposed through them. From those nodes the + traffic will follow the normal tunneled path (Geneve tunnel) to the OpenStack + compute node where the VM is allocated. diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst new file mode 100644 index 00000000..865b13ed --- /dev/null +++ b/doc/source/contributor/index.rst @@ -0,0 +1,10 @@ +=========================== + Contributor Documentation +=========================== + + .. toctree:: + :maxdepth: 2 + + bgp_mode_design + evpn_mode_design + diff --git a/doc/source/index.rst b/doc/source/index.rst index f3616222..c03ce5aa 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -13,6 +13,7 @@ Contents: :maxdepth: 2 readme + contributor/index Indices and tables ================== diff --git a/ovn_bgp_agent/agent.py b/ovn_bgp_agent/agent.py new file mode 100644 index 00000000..4a72281a --- /dev/null +++ b/ovn_bgp_agent/agent.py @@ -0,0 +1,77 @@ +# 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 functools +import sys + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_service import periodic_task +from oslo_service import service + +from ovn_bgp_agent import config +from ovn_bgp_agent.drivers import driver_api + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class BGPAgentMeta(type(service.Service), + type(periodic_task.PeriodicTasks)): + pass + + +class BGPAgent(service.Service, periodic_task.PeriodicTasks, + metaclass=BGPAgentMeta): + """BGP OVN Agent.""" + + def __init__(self): + super(BGPAgent, self).__init__() + periodic_task.PeriodicTasks.__init__(self, CONF) + + self.agent_driver = driver_api.AgentDriverBase.get_instance( + CONF.driver) + + def start(self): + LOG.info("Service '%s' starting", self.__class__.__name__) + super(BGPAgent, self).start() + self.agent_driver.start() + + LOG.info("Service '%s' started", self.__class__.__name__) + f = functools.partial(self.run_periodic_tasks, None) + self.tg.add_timer(1, f) + + @periodic_task.periodic_task(spacing=CONF.reconcile_interval, + run_immediately=True) + def sync(self, context): + LOG.info("Running reconciliation loop to ensure routes/rules are " + "in place.") + self.agent_driver.sync() + + def wait(self): + super(BGPAgent, self).wait() + LOG.info("Service '%s' stopped", self.__class__.__name__) + + def stop(self, graceful=False): + LOG.info("Service '%s' stopping", self.__class__.__name__) + super(BGPAgent, self).stop(graceful) + + +def start(): + config.init(sys.argv[1:]) + config.setup_logging() + + bgp_agent_launcher = service.launch(config.CONF, BGPAgent()) + bgp_agent_launcher.wait() diff --git a/ovn_bgp_agent/cmd/__init__.py b/ovn_bgp_agent/cmd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ovn_bgp_agent/cmd/agent.py b/ovn_bgp_agent/cmd/agent.py new file mode 100644 index 00000000..1c779a11 --- /dev/null +++ b/ovn_bgp_agent/cmd/agent.py @@ -0,0 +1,20 @@ +# 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. + +from ovn_bgp_agent import agent + +start = agent.start + +if __name__ == '__main__': + start() diff --git a/ovn_bgp_agent/config.py b/ovn_bgp_agent/config.py new file mode 100644 index 00000000..963774ed --- /dev/null +++ b/ovn_bgp_agent/config.py @@ -0,0 +1,65 @@ +# 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. + +from oslo_config import cfg +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + +agent_opts = [ + cfg.IntOpt('reconcile_interval', + help='Time between re-sync actions.', + default=120), + cfg.BoolOpt('expose_tenant_networks', + help='Expose VM IPs on tenant networks', + default=False), + cfg.StrOpt('driver', + help='Driver to be used', + default='osp_ovn_bgp_driver'), + cfg.StrOpt('ovn_sb_private_key', + default='/etc/pki/tls/private/ovn_controller.key', + help='The PEM file with private key for SSL connection to ' + 'OVN-SB-DB'), + cfg.StrOpt('ovn_sb_certificate', + default='/etc/pki/tls/certs/ovn_controller.crt', + help='The PEM file with certificate that certifies the ' + 'private key specified in ovn_sb_private_key'), + cfg.StrOpt('ovn_sb_ca_cert', + default='/etc/ipa/ca.crt', + help='The PEM file with CA certificate that OVN should use to' + ' verify certificates presented to it by SSL peers'), + cfg.StrOpt('bgp_AS', + default='64999', + help='AS number to be used by the Agent when running in BGP ' + 'mode and configuring the VRF route leaking.'), + cfg.StrOpt('bgp_router_id', + default=None, + help='Router ID to be used by the Agent when running in BGP ' + 'mode and configuring the VRF route leaking.'), +] + +CONF = cfg.CONF +CONF.register_opts(agent_opts) + +logging.register_options(CONF) + + +def init(args, **kwargs): + CONF(args=args, project='bgp-agent', **kwargs) + + +def setup_logging(): + logging.setup(CONF, 'bgp-agent') + logging.set_defaults(default_log_levels=logging.get_default_log_levels()) + LOG.info("Logging enabled!") diff --git a/ovn_bgp_agent/constants.py b/ovn_bgp_agent/constants.py new file mode 100644 index 00000000..2047b00b --- /dev/null +++ b/ovn_bgp_agent/constants.py @@ -0,0 +1,41 @@ +# 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. + +OVN_VIF_PORT_TYPES = ("", "chassisredirect", "virtual") + +OVN_VIRTUAL_VIF_PORT_TYPE = "virtual" +OVN_VM_VIF_PORT_TYPE = "" +OVN_PATCH_VIF_PORT_TYPE = "patch" +OVN_CHASSISREDIRECT_VIF_PORT_TYPE = "chassisredirect" +OVN_LOCALNET_VIF_PORT_TYPE = "localnet" + +OVN_BGP_NIC = "ovn" +OVN_BGP_VRF = "ovn-bgp-vrf" +OVN_BGP_VRF_TABLE = 10 +OVS_CONNECTION_STRING = "unix:/var/run/openvswitch/db.sock" +OVS_RULE_COOKIE = "999" +OVS_VRF_RULE_COOKIE = "998" + +FRR_SOCKET_PATH = "/run/frr/" + +IP_VERSION_6 = 6 +IP_VERSION_4 = 4 + +BGP_MODE = 'BGP' + +OVN_INTEGRATION_BRIDGE = 'br-int' + + +LINK_UP = "up" +LINK_DOWN = "down" \ No newline at end of file diff --git a/ovn_bgp_agent/drivers/__init__.py b/ovn_bgp_agent/drivers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ovn_bgp_agent/drivers/driver_api.py b/ovn_bgp_agent/drivers/driver_api.py new file mode 100644 index 00000000..5a657a0a --- /dev/null +++ b/ovn_bgp_agent/drivers/driver_api.py @@ -0,0 +1,56 @@ +# 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 abc +from stevedore import driver as stevedore_driver + + +class AgentDriverBase(object, metaclass=abc.ABCMeta): + """Base class for agent drivers. + + """ + + @classmethod + def get_instance(cls, specific_driver): + agent_driver = stevedore_driver.DriverManager( + namespace='ovn_bgp_agent.drivers', + name=specific_driver, + invoke_on_load=True + ).driver + + return agent_driver + + @abc.abstractmethod + def expose_ip(self, ip_address): + raise NotImplementedError() + + @abc.abstractmethod + def withdraw_ip(self, ip_address): + raise NotImplementedError() + + @abc.abstractmethod + def expose_remote_ip(self, ip_address): + raise NotImplementedError() + + @abc.abstractmethod + def withdraw_remote_ip(self, ip_address): + raise NotImplementedError() + + @abc.abstractmethod + def expose_subnet(self, subnet): + raise NotImplementedError() + + @abc.abstractmethod + def withdraw_subnet(self, subnet): + raise NotImplementedError() diff --git a/ovn_bgp_agent/drivers/openstack/__init__.py b/ovn_bgp_agent/drivers/openstack/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py b/ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py new file mode 100644 index 00000000..1e46a6bf --- /dev/null +++ b/ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py @@ -0,0 +1,702 @@ +# 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 ipaddress +import pyroute2 + +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__) +# LOG.setLevel(logging.DEBUG) +# logging.basicConfig(level=logging.DEBUG) + +OVN_TABLES = ("Port_Binding", "Chassis", "Datapath_Binding", "Chassis_Private") + + +class OSPOVNBGPDriver(driver_api.AgentDriverBase): + + def __init__(self): + self._expose_tenant_networks = CONF.expose_tenant_networks + self.ovn_routing_tables = {} # {'br-ex': 200} + self.ovn_bridge_mappings = {} # {'public': 'br-ex'} + self.ovn_local_cr_lrps = {} + self.ovn_local_lrps = set([]) + # {'br-ex': [route1, route2]} + self.ovn_routing_tables_routes = collections.defaultdict() + + self.ovs_idl = ovs.OvsIdl() + self.ovs_idl.start(constants.OVS_CONNECTION_STRING) + self.chassis = self.ovs_idl.get_own_chassis_name() + self.ovn_remote = self.ovs_idl.get_ovn_remote() + LOG.debug("Loaded chassis {}.".format(self.chassis)) + + events = () + for event in self._get_events(): + event_class = getattr(watcher, event) + events += (event_class(self),) + + self._sb_idl = ovn.OvnSbIdl( + self.ovn_remote, + chassis=self.chassis, + tables=OVN_TABLES, + events=events) + + def start(self): + # Ensure FRR is configure 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(constants.OVN_BGP_VRF, CONF.bgp_AS, CONF.bgp_router_id) + + # start the subscriptions to the OSP events. This ensures the watcher + # calls the relevant driver methods upon registered events + self.sb_idl = self._sb_idl.start() + + def _get_events(self): + events = set(["PortBindingChassisCreatedEvent", + "PortBindingChassisDeletedEvent", + "FIPSetEvent", + "FIPUnsetEvent", + "ChassisCreateEvent"]) + if self._expose_tenant_networks: + events.update(["SubnetRouterAttachedEvent", + "SubnetRouterDetachedEvent", + "TenantPortCreatedEvent", + "TenantPortDeletedEvent"]) + return events + + @lockutils.synchronized('bgp') + def sync(self): + self.ovn_local_cr_lrps = {} + self.ovn_local_lrps = set([]) + self.ovn_routing_tables_routes = collections.defaultdict() + + LOG.debug("Ensuring VRF configuration for advertising routes") + # Create VRF + linux_net.ensure_vrf(constants.OVN_BGP_VRF, + constants.OVN_BGP_VRF_TABLE) + # Create OVN dummy device + linux_net.ensure_ovn_device(constants.OVN_BGP_NIC, + constants.OVN_BGP_VRF) + + LOG.debug("Configuring br-ex default rule and routing tables for " + "each provider network") + flows_info = {} + # 1) Get bridge mappings: xxxx:br-ex,yyyy:br-ex2 + bridge_mappings = self.ovs_idl.get_ovn_bridge_mappings() + # 2) Get macs for bridge mappings + extra_routes = {} + with pyroute2.NDB() as ndb: + for bridge_mapping in bridge_mappings: + network = bridge_mapping.split(":")[0] + bridge = bridge_mapping.split(":")[1] + self.ovn_bridge_mappings[network] = bridge + if not extra_routes.get(bridge): + extra_routes[bridge] = ( + linux_net.ensure_routing_table_for_bridge( + self.ovn_routing_tables, bridge)) + vlan_tag = self.sb_idl.get_network_vlan_tag_by_network_name( + network) + if vlan_tag: + vlan_tag = vlan_tag[0] + linux_net.ensure_vlan_device_for_network(bridge, + vlan_tag) + + if flows_info.get(bridge): + continue + flows_info[bridge] = { + 'mac': ndb.interfaces[bridge]['address'], + 'in_port': set([])} + # 3) Get in_port for bridge mappings (br-ex, br-ex2) + ovs.get_ovs_flows_info(bridge, flows_info, + constants.OVS_RULE_COOKIE) + # 4) Add/Remove flows for each bridge mappings + ovs.remove_extra_ovs_flows(flows_info, constants.OVS_RULE_COOKIE) + + LOG.debug("Syncing current routes.") + exposed_ips = linux_net.get_exposed_ips(constants.OVN_BGP_NIC) + # get the rules pointing to ovn bridges + ovn_ip_rules = linux_net.get_ovn_ip_rules( + self.ovn_routing_tables.values()) + + # add missing routes/ips for fips/provider VMs + ports = self.sb_idl.get_ports_on_chassis(self.chassis) + for port in ports: + self._ensure_port_exposed(port, exposed_ips, ovn_ip_rules) + + # add missing route/ips for tenant network VMs + if self._expose_tenant_networks: + for cr_lrp_info in self.ovn_local_cr_lrps.values(): + lrp_ports = self.sb_idl.get_lrp_ports_for_router( + cr_lrp_info['router_datapath']) + for lrp in lrp_ports: + if lrp.chassis: + continue + self._ensure_network_exposed( + lrp, cr_lrp_info, exposed_ips, ovn_ip_rules) + + # remove extra routes/ips + # remove all the leftovers on the list of current ips on dev OVN + linux_net.delete_exposed_ips(exposed_ips, constants.OVN_BGP_NIC) + # remove all the leftovers on the list of current ip rules for ovn + # bridges + linux_net.delete_ip_rules(ovn_ip_rules) + + # remove all the extra rules not needed + linux_net.delete_bridge_ip_routes(self.ovn_routing_tables, + self.ovn_routing_tables_routes, + extra_routes) + + def _ensure_port_exposed(self, port, exposed_ips, ovn_ip_rules): + if port.type not in constants.OVN_VIF_PORT_TYPES: + return + if (len(port.mac[0].split(' ')) != 2 and + len(port.mac[0].split(' ')) != 3): + return + port_ips = [port.mac[0].split(' ')[1]] + if len(port.mac[0].split(' ')) == 3: + port_ips.append(port.mac[0].split(' ')[2]) + + fip = self._expose_ip(port_ips, port) + if fip: + if fip in exposed_ips: + exposed_ips.remove(fip) + fip_dst = "{}/32".format(fip) + if fip_dst in ovn_ip_rules.keys(): + del ovn_ip_rules[fip_dst] + + for port_ip in port_ips: + ip_address = port_ip.split("/")[0] + ip_version = linux_net.get_ip_version(port_ip) + if ip_version == constants.IP_VERSION_6: + ip_dst = "{}/128".format(ip_address) + else: + ip_dst = "{}/32".format(ip_address) + if ip_address in exposed_ips: + # remove each ip to add from the list of current ips on dev OVN + exposed_ips.remove(ip_address) + if ip_dst in ovn_ip_rules.keys(): + del ovn_ip_rules[ip_dst] + + def _ensure_network_exposed(self, router_port, gateway, exposed_ips=[], + ovn_ip_rules={}): + gateway_ips = [ip.split('/')[0] for ip in gateway['ips']] + try: + router_port_ip = router_port.mac[0].split(' ')[1] + except IndexError: + return + router_ip = router_port_ip.split('/')[0] + if router_ip in gateway_ips: + return + self.ovn_local_lrps.add(router_port.logical_port) + rule_bridge, vlan_tag = self._get_bridge_for_datapath( + gateway['provider_datapath']) + + linux_net.add_ip_rule(router_port_ip, + self.ovn_routing_tables[rule_bridge], + rule_bridge) + if router_port_ip in ovn_ip_rules.keys(): + del ovn_ip_rules[router_port_ip] + + router_port_ip_version = linux_net.get_ip_version(router_port_ip) + for gateway_ip in gateway_ips: + if linux_net.get_ip_version(gateway_ip) == router_port_ip_version: + linux_net.add_ip_route( + self.ovn_routing_tables_routes, + router_ip, + self.ovn_routing_tables[rule_bridge], + rule_bridge, + vlan=vlan_tag, + mask=router_port_ip.split("/")[1], + via=gateway_ip) + break + + network_port_datapath = self.sb_idl.get_port_datapath( + router_port.options['peer']) + if network_port_datapath: + ports = self.sb_idl.get_ports_on_datapath( + network_port_datapath) + for port in ports: + if ((port.type != constants.OVN_VM_VIF_PORT_TYPE and port.type != constants.OVN_VIRTUAL_VIF_PORT_TYPE) or + (port.type == constants.OVN_VM_VIF_PORT_TYPE and not port.chassis)): + continue + try: + port_ips = [port.mac[0].split(' ')[1]] + except IndexError: + continue + if len(port.mac[0].split(' ')) == 3: + port_ips.append(port.mac[0].split(' ')[2]) + + for port_ip in port_ips: + # Only adding the port ips that match the lrp + # IP version + port_ip_version = linux_net.get_ip_version(port_ip) + if port_ip_version == router_port_ip_version: + linux_net.add_ips_to_dev( + constants.OVN_BGP_NIC, [port_ip]) + if port_ip in exposed_ips: + exposed_ips.remove(port_ip) + if router_port_ip_version == constants.IP_VERSION_6: + ip_dst = "{}/128".format(port_ip) + else: + ip_dst = "{}/32".format(port_ip) + + if ip_dst in ovn_ip_rules.keys(): + del ovn_ip_rules[ip_dst] + + def _remove_network_exposed(self, router_port, gateway): + gateway_ips = [ip.split('/')[0] for ip in gateway['ips']] + try: + router_port_ip = router_port.mac[0].split(' ')[1] + except IndexError: + return + router_ip = router_port_ip.split('/')[0] + if router_ip in gateway_ips: + return + + if router_port.logical_port in self.ovn_local_lrps: + self.ovn_local_lrps.remove(router_port.logical_port) + rule_bridge, vlan_tag = self._get_bridge_for_datapath( + gateway['provider_datapath']) + + linux_net.del_ip_rule(router_port_ip, + self.ovn_routing_tables[rule_bridge], + rule_bridge) + + router_port_ip_version = linux_net.get_ip_version(router_port_ip) + for gateway_ip in gateway_ips: + if linux_net.get_ip_version(gateway_ip) == router_port_ip_version: + linux_net.del_ip_route( + self.ovn_routing_tables_routes, + router_ip, + self.ovn_routing_tables[rule_bridge], + rule_bridge, + vlan=vlan_tag, + mask=router_port_ip.split("/")[1], + via=gateway_ip) + if (linux_net.get_ip_version(gateway_ip) == + constants.IP_VERSION_6): + net = ipaddress.IPv6Network(router_port_ip, strict=False) + else: + net = ipaddress.IPv4Network(router_port_ip, strict=False) + break + # Check if there are VMs on the network + # and if so withdraw the routes + vms_on_net = linux_net.get_exposed_ips_on_network( + constants.OVN_BGP_NIC, net) + linux_net.delete_exposed_ips(vms_on_net, constants.OVN_BGP_NIC) + + def _get_bridge_for_datapath(self, datapath): + network_name, network_tag = self.sb_idl.get_network_name_and_tag( + datapath, self.ovn_bridge_mappings.keys()) + if network_name: + if network_tag: + return self.ovn_bridge_mappings[network_name], network_tag[0] + return self.ovn_bridge_mappings[network_name], None + return None, None + + @lockutils.synchronized('bgp') + def expose_ip(self, ips, row, associated_port=None): + '''Advertice BGP route by adding IP to device. + + This methods ensures BGP advertises the IP of the VM in the provider + network, or the FIP associated to a VM in a tenant networks. + + It relies on Zebra, which creates and advertises a route when an IP + is added to a local interface. + + This method assumes a device named self.ovn_decice exists (inside a + VRF), and adds the IP of either: + - VM IP on the provider network, + - VM FIP, or + - CR-LRP OVN port + ''' + self._expose_ip(ips, row, associated_port) + + def _expose_ip(self, ips, row, associated_port=None): + # VM on provider Network + if ((row.type == constants.OVN_VM_VIF_PORT_TYPE + or row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE) and + self.sb_idl.is_provider_network(row.datapath)): + LOG.info("Add BGP route for logical port with ip %s", ips) + linux_net.add_ips_to_dev(constants.OVN_BGP_NIC, ips) + + rule_bridge, vlan_tag = self._get_bridge_for_datapath(row.datapath) + for ip in ips: + linux_net.add_ip_rule(ip, + self.ovn_routing_tables[rule_bridge], + rule_bridge) + linux_net.add_ip_route( + self.ovn_routing_tables_routes, ip, + self.ovn_routing_tables[rule_bridge], rule_bridge, + vlan=vlan_tag) + + # VM with FIP + elif (row.type == constants.OVN_VM_VIF_PORT_TYPE + or row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE): + # FIPs are only supported with IPv4 + fip_address, fip_datapath = self.sb_idl.get_fip_associated( + row.logical_port) + if fip_address: + LOG.info("Add BGP route for FIP with ip %s", fip_address) + linux_net.add_ips_to_dev(constants.OVN_BGP_NIC, + [fip_address]) + + rule_bridge, vlan_tag = self._get_bridge_for_datapath( + fip_datapath) + linux_net.add_ip_rule(fip_address, + self.ovn_routing_tables[rule_bridge], + rule_bridge) + linux_net.add_ip_route( + self.ovn_routing_tables_routes, fip_address, + self.ovn_routing_tables[rule_bridge], rule_bridge, + vlan=vlan_tag) + return fip_address + else: + ovs.ensure_default_ovs_flows(self.ovn_bridge_mappings.values(), + constants.OVS_RULE_COOKIE) + + # FIP association to VM + elif row.type == constants.OVN_PATCH_VIF_PORT_TYPE: + if (associated_port and self.sb_idl.is_port_on_chassis( + associated_port, self.chassis)): + LOG.info("Add BGP route for FIP with ip %s", ips) + linux_net.add_ips_to_dev(constants.OVN_BGP_NIC, ips) + + rule_bridge, vlan_tag = self._get_bridge_for_datapath( + row.datapath) + for ip in ips: + linux_net.add_ip_rule(ip, + self.ovn_routing_tables[rule_bridge], + rule_bridge) + linux_net.add_ip_route( + self.ovn_routing_tables_routes, ip, + self.ovn_routing_tables[rule_bridge], rule_bridge, + vlan=vlan_tag) + + # CR-LRP Port + elif (row.type == constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE and + row.logical_port.startswith('cr-')): + _, cr_lrp_datapath = self.sb_idl.get_fip_associated( + row.logical_port) + if cr_lrp_datapath: + LOG.info("Add BGP route for CR-LRP Port %s", ips) + # Keeping information about the associated network for + # tenant network advertisement + self.ovn_local_cr_lrps[row.logical_port] = { + 'router_datapath': row.datapath, + 'provider_datapath': cr_lrp_datapath, + 'ips': ips + } + ips_without_mask = [ip.split("/")[0] for ip in ips] + linux_net.add_ips_to_dev(constants.OVN_BGP_NIC, + ips_without_mask) + + rule_bridge, vlan_tag = self._get_bridge_for_datapath( + cr_lrp_datapath) + + for ip in ips: + ip_without_mask = ip.split("/")[0] + linux_net.add_ip_rule( + ip_without_mask, self.ovn_routing_tables[rule_bridge], + rule_bridge, lladdr=row.mac[0].split(' ')[0]) + linux_net.add_ip_route( + self.ovn_routing_tables_routes, ip_without_mask, + self.ovn_routing_tables[rule_bridge], rule_bridge, + vlan=vlan_tag) + # add proxy ndp config for ipv6 + if (linux_net.get_ip_version(ip_without_mask) == + constants.IP_VERSION_6): + linux_net.add_ndp_proxy(ip, rule_bridge, vlan_tag) + + # Check if there are networks attached to the router, + # and if so, add the needed routes/rules + if not self._expose_tenant_networks: + return + lrp_ports = self.sb_idl.get_lrp_ports_for_router( + row.datapath) + for lrp in lrp_ports: + if lrp.chassis: + continue + self._ensure_network_exposed( + lrp, self.ovn_local_cr_lrps[row.logical_port]) + + @lockutils.synchronized('bgp') + def withdraw_ip(self, ips, row, associated_port=None): + '''Withdraw BGP route by removing IP from device. + + This methods ensures BGP withdraw an advertised IP of a VM, either + in the provider network, or the FIP associated to a VM in a tenant + networks. + + It relies on Zebra, which withdraws the advertisement as soon as the + IP is deleted from the local interface. + + This method assumes a device named self.ovn_decice exists (inside a + VRF), and removes the IP of either: + - VM IP on the provider network, + - VM FIP, or + - CR-LRP OVN port + ''' + # VM on provider Network + if ((row.type == constants.OVN_VM_VIF_PORT_TYPE + or row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE) and + self.sb_idl.is_provider_network(row.datapath)): + LOG.info("Delete BGP route for logical port with ip %s", ips) + linux_net.del_ips_from_dev(constants.OVN_BGP_NIC, ips) + + rule_bridge, vlan_tag = self._get_bridge_for_datapath(row.datapath) + for ip in ips: + linux_net.del_ip_rule(ip, + self.ovn_routing_tables[rule_bridge], + rule_bridge) + linux_net.del_ip_route( + self.ovn_routing_tables_routes, ip, + self.ovn_routing_tables[rule_bridge], rule_bridge, + vlan=vlan_tag) + + # VM with FIP + elif (row.type == constants.OVN_VM_VIF_PORT_TYPE + or row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE): + # FIPs are only supported with IPv4 + fip_address, fip_datapath = self.sb_idl.get_fip_associated( + row.logical_port) + if fip_address: + LOG.info("Delete BGP route for FIP with ip %s", fip_address) + linux_net.del_ips_from_dev(constants.OVN_BGP_NIC, + [fip_address]) + + rule_bridge, vlan_tag = self._get_bridge_for_datapath( + fip_datapath) + linux_net.del_ip_rule(fip_address, + self.ovn_routing_tables[rule_bridge], + rule_bridge) + linux_net.del_ip_route( + self.ovn_routing_tables_routes, fip_address, + self.ovn_routing_tables[rule_bridge], rule_bridge, + vlan=vlan_tag) + + # FIP association to VM + elif row.type == constants.OVN_PATCH_VIF_PORT_TYPE: + if (associated_port and ( + self.sb_idl.is_port_on_chassis( + associated_port, self.chassis) or + self.sb_idl.is_port_deleted(associated_port))): + LOG.info("Delete BGP route for FIP with ip %s", ips) + linux_net.del_ips_from_dev(constants.OVN_BGP_NIC, ips) + + rule_bridge, vlan_tag = self._get_bridge_for_datapath( + row.datapath) + for ip in ips: + linux_net.del_ip_rule(ip, + self.ovn_routing_tables[rule_bridge], + rule_bridge) + linux_net.del_ip_route( + self.ovn_routing_tables_routes, ip, + self.ovn_routing_tables[rule_bridge], rule_bridge, + vlan=vlan_tag) + + # CR-LRP Port + elif (row.type == constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE and + row.logical_port.startswith('cr-')): + cr_lrp_datapath = self.ovn_local_cr_lrps.get( + row.logical_port, {}).get('provider_datapath') + if cr_lrp_datapath: + LOG.info("Delete BGP route for CR-LRP Port %s", ips) + # Removing information about the associated network for + # tenant network advertisement + ips_without_mask = [ip.split("/")[0] for ip in ips] + linux_net.del_ips_from_dev(constants.OVN_BGP_NIC, + ips_without_mask) + + rule_bridge, vlan_tag = self._get_bridge_for_datapath( + cr_lrp_datapath) + + for ip in ips_without_mask: + if linux_net.get_ip_version(ip) == constants.IP_VERSION_6: + cr_lrp_ip = '{}/128'.format(ip) + else: + cr_lrp_ip = '{}/32'.format(ip) + linux_net.del_ip_rule( + cr_lrp_ip, self.ovn_routing_tables[rule_bridge], + rule_bridge, lladdr=row.mac[0].split(' ')[0]) + linux_net.del_ip_route( + self.ovn_routing_tables_routes, ip, + self.ovn_routing_tables[rule_bridge], rule_bridge, + vlan=vlan_tag) + # del proxy ndp config for ipv6 + if linux_net.get_ip_version(ip) == constants.IP_VERSION_6: + cr_lrps_on_same_provider = [ + p for p in self.ovn_local_cr_lrps.values() + if p['provider_datapath'] == cr_lrp_datapath] + if (len(cr_lrps_on_same_provider) > 1): + linux_net.del_ndp_proxy(ip, rule_bridge, vlan_tag) + + # Check if there are networks attached to the router, + # and if so, delete the needed routes/rules + lrp_ports = self.sb_idl.get_lrp_ports_for_router( + row.datapath) + for lrp in lrp_ports: + if lrp.chassis: + continue + local_cr_lrp_info = self.ovn_local_cr_lrps.get( + row.logical_port) + if local_cr_lrp_info: + self._remove_network_exposed(lrp, local_cr_lrp_info) + try: + del self.ovn_local_cr_lrps[row.logical_port] + except KeyError: + LOG.debug("Gateway port %s already cleanup from the " + "agent", row.logical_port) + + @lockutils.synchronized('bgp') + def expose_remote_ip(self, ips, row): + if (self.sb_idl.is_provider_network(row.datapath) or + not self._expose_tenant_networks): + return + port_lrp = self.sb_idl.get_lrp_port_for_datapath(row.datapath) + if port_lrp in self.ovn_local_lrps: + LOG.info("Add BGP route for tenant IP %s on chassis %s", + ips, self.chassis) + linux_net.add_ips_to_dev(constants.OVN_BGP_NIC, ips) + + @lockutils.synchronized('bgp') + def withdraw_remote_ip(self, ips, row): + if (self.sb_idl.is_provider_network(row.datapath) or + not self._expose_tenant_networks): + return + port_lrp = self.sb_idl.get_lrp_port_for_datapath(row.datapath) + if port_lrp in self.ovn_local_lrps: + LOG.info("Delete BGP route for tenant IP %s on chassis %s", + ips, self.chassis) + linux_net.del_ips_from_dev(constants.OVN_BGP_NIC, ips) + + @lockutils.synchronized('bgp') + def expose_subnet(self, ip, row): + if not self._expose_tenant_networks: + return + cr_lrp = self.sb_idl.is_router_gateway_on_chassis(row.datapath, + self.chassis) + if cr_lrp: + LOG.info("Add IP Rules for network %s on chassis %s", + ip, self.chassis) + self.ovn_local_lrps.add(row.logical_port) + cr_lrp_info = self.ovn_local_cr_lrps.get(cr_lrp, {}) + cr_lrp_datapath = cr_lrp_info.get('provider_datapath') + if cr_lrp_datapath: + cr_lrp_ips = [ip_address.split('/')[0] + for ip_address in cr_lrp_info.get('ips', [])] + rule_bridge, vlan_tag = self._get_bridge_for_datapath( + cr_lrp_datapath) + linux_net.add_ip_rule(ip, + self.ovn_routing_tables[rule_bridge], + rule_bridge) + + ip_version = linux_net.get_ip_version(ip) + for cr_lrp_ip in cr_lrp_ips: + if linux_net.get_ip_version(cr_lrp_ip) == ip_version: + linux_net.add_ip_route( + self.ovn_routing_tables_routes, + ip.split("/")[0], + self.ovn_routing_tables[rule_bridge], + rule_bridge, + vlan=vlan_tag, + mask=ip.split("/")[1], + via=cr_lrp_ip) + break + + # Check if there are VMs on the network + # and if so expose the route + network_port_datapath = self.sb_idl.get_port_datapath( + row.options['peer']) + if network_port_datapath: + ports = self.sb_idl.get_ports_on_datapath( + network_port_datapath) + for port in ports: + if port.type != constants.OVN_VM_VIF_PORT_TYPE and port.type != constants.OVN_VIRTUAL_VIF_PORT_TYPE: + continue + try: + port_ips = [port.mac[0].split(' ')[1]] + except IndexError: + continue + if len(port.mac[0].split(' ')) == 3: + port_ips.append(port.mac[0].split(' ')[2]) + + for port_ip in port_ips: + # Only adding the port ips that match the lrp + # IP version + port_ip_version = linux_net.get_ip_version(port_ip) + if port_ip_version == ip_version: + linux_net.add_ips_to_dev( + constants.OVN_BGP_NIC, [port_ip]) + + @lockutils.synchronized('bgp') + def withdraw_subnet(self, ip, row): + if not self._expose_tenant_networks: + return + cr_lrp = self.sb_idl.is_router_gateway_on_chassis(row.datapath, + self.chassis) + if cr_lrp: + LOG.info("Delete IP Rules for network %s on chassis %s", + ip, self.chassis) + if row.logical_port in self.ovn_local_lrps: + self.ovn_local_lrps.remove(row.logical_port) + cr_lrp_info = self.ovn_local_cr_lrps.get(cr_lrp, {}) + cr_lrp_datapath = cr_lrp_info.get('provider_datapath') + + if cr_lrp_datapath: + cr_lrp_ips = [ip_address.split('/')[0] + for ip_address in cr_lrp_info.get('ips', [])] + rule_bridge, vlan_tag = self._get_bridge_for_datapath( + cr_lrp_datapath) + linux_net.del_ip_rule(ip, + self.ovn_routing_tables[rule_bridge], + rule_bridge) + + ip_version = linux_net.get_ip_version(ip) + for cr_lrp_ip in cr_lrp_ips: + if linux_net.get_ip_version(cr_lrp_ip) == ip_version: + linux_net.del_ip_route( + self.ovn_routing_tables_routes, + ip.split("/")[0], + self.ovn_routing_tables[rule_bridge], + rule_bridge, + vlan=vlan_tag, + mask=ip.split("/")[1], + via=cr_lrp_ip) + if (linux_net.get_ip_version(cr_lrp_ip) == + constants.IP_VERSION_6): + net = ipaddress.IPv6Network(ip, strict=False) + else: + net = ipaddress.IPv4Network(ip, strict=False) + break + + # Check if there are VMs on the network + # and if so withdraw the routes + vms_on_net = linux_net.get_exposed_ips_on_network( + constants.OVN_BGP_NIC, net) + linux_net.delete_exposed_ips(vms_on_net, + constants.OVN_BGP_NIC) diff --git a/ovn_bgp_agent/drivers/openstack/utils/__init__.py b/ovn_bgp_agent/drivers/openstack/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ovn_bgp_agent/drivers/openstack/utils/frr.py b/ovn_bgp_agent/drivers/openstack/utils/frr.py new file mode 100644 index 00000000..0aba17c2 --- /dev/null +++ b/ovn_bgp_agent/drivers/openstack/utils/frr.py @@ -0,0 +1,144 @@ +# 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 json + +from jinja2 import Template +from oslo_concurrency import processutils +from oslo_log import log as logging + +from ovn_bgp_agent import constants + +LOG = logging.getLogger(__name__) + +ADD_VRF_TEMPLATE = ''' +vrf {{ vrf_name }} + vni {{ vni }} +exit-vrf + +router bgp {{ bgp_as }} vrf {{ vrf_name }} + address-family ipv4 unicast + redistribute connected + exit-address-family + address-family ipv6 unicast + redistribute connected + exit-address-family + address-family l2vpn evpn + advertise ipv4 unicast + advertise ipv6 unicast + exit-address-family + +''' + +DEL_VRF_TEMPLATE = ''' +no vrf {{ vrf_name }} +no router bgp {{ bgp_as }} vrf {{ vrf_name }} + +''' + +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 connected + exit-address-family + + address-family ipv6 unicast + redistribute connected + exit-address-family + +''' + + +def _run_vtysh_config(frr_config_file): + vtysh_command = "copy {} running-config".format(frr_config_file) + full_args = ['/usr/bin/vtysh', '--vty_socket', constants.FRR_SOCKET_PATH, + '-c', vtysh_command] + try: + return processutils.execute(*full_args, run_as_root=True) + except Exception as e: + print("Unable to execute vtysh with {}. Exception: {}".format( + full_args, e)) + raise + + +def _run_vtysh_command(command): + full_args = ['/usr/bin/vtysh', '--vty_socket', constants.FRR_SOCKET_PATH, + '-c', command] + try: + return processutils.execute(*full_args, run_as_root=True)[0] + except Exception as e: + print("Unable to execute vtysh with {}. Exception: {}".format( + full_args, e)) + raise + + +def _get_router_id(bgp_as): + output = _run_vtysh_command(command='show ip bgp summary json') + return json.loads(output).get('ipv4Unicast', {}).get('routerId') + + +def vrf_leak(vrf, bgp_as, bgp_router_id=None): + LOG.info("Add VRF leak for VRF {} on router bgp {}".format(vrf, bgp_as)) + if not bgp_router_id: + bgp_router_id = _get_router_id(bgp_as) + if not bgp_router_id: + LOG.error("Unknown router-id, needed for route leaking") + return + + vrf_template = Template(LEAK_VRF_TEMPLATE) + vrf_config = vrf_template.render(vrf_name=vrf, bgp_as=bgp_as, + bgp_router_id=bgp_router_id) + frr_config_file = "frr-config-vrf-leak-{}".format(vrf) + with open(frr_config_file, 'w') as vrf_config_file: + vrf_config_file.write(vrf_config) + + _run_vtysh_config(frr_config_file) + + +def vrf_reconfigure(evpn_info, action): + LOG.info("FRR reconfiguration (action = {}) for evpn: {}".format( + action, evpn_info)) + frr_config_file = None + if action == "add-vrf": + vrf_template = Template(ADD_VRF_TEMPLATE) + vrf_config = vrf_template.render( + vrf_name="{}{}".format(constants.OVN_EVPN_VRF_PREFIX, + evpn_info['vni']), + bgp_as=evpn_info['bgp_as'], + vni=evpn_info['vni']) + frr_config_file = "frr-config-add-vrf-{}".format(evpn_info['vni']) + elif action == "del-vrf": + vrf_template = Template(DEL_VRF_TEMPLATE) + vrf_config = vrf_template.render( + vrf_name="{}{}".format(constants.OVN_EVPN_VRF_PREFIX, + evpn_info['vni']), + bgp_as=evpn_info['bgp_as']) + frr_config_file = "frr-config-del-vrf-{}".format(evpn_info['vni']) + else: + LOG.error("Unknown FRR reconfiguration action: %s", action) + return + with open(frr_config_file, 'w') as vrf_config_file: + vrf_config_file.write(vrf_config) + + _run_vtysh_config(frr_config_file) diff --git a/ovn_bgp_agent/drivers/openstack/utils/ovn.py b/ovn_bgp_agent/drivers/openstack/utils/ovn.py new file mode 100644 index 00000000..b697ab93 --- /dev/null +++ b/ovn_bgp_agent/drivers/openstack/utils/ovn.py @@ -0,0 +1,249 @@ +# 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. + +from oslo_config import cfg + +from ovs.stream import Stream +from ovsdbapp.backend import ovs_idl +from ovsdbapp.backend.ovs_idl import connection +from ovsdbapp.backend.ovs_idl import idlutils +from ovsdbapp import event +from ovsdbapp.schema.ovn_southbound import impl_idl as sb_impl_idl + +from ovn_bgp_agent import constants + +CONF = cfg.CONF + + +class OvnIdl(connection.OvsdbIdl): + def __init__(self, driver, remote, schema): + super(OvnIdl, self).__init__(remote, schema) + self.driver = driver + self.notify_handler = OvnDbNotifyHandler(driver) + self.event_lock_name = "neutron_ovn_event_lock" + + def notify(self, event, row, updates=None): + if self.is_lock_contended: + return + self.notify_handler.notify(event, row, updates) + + +class OvnDbNotifyHandler(event.RowEventHandler): + def __init__(self, driver): + super(OvnDbNotifyHandler, self).__init__() + self.driver = driver + + +class OvnSbIdl(OvnIdl): + SCHEMA = 'OVN_Southbound' + + def __init__(self, connection_string, chassis=None, events=None, + tables=None): + if connection_string.startswith("ssl"): + self._check_and_set_ssl_files(self.SCHEMA) + helper = self._get_ovsdb_helper(connection_string) + self._events = events + if tables is None: + tables = ('Chassis', 'Encap', 'Port_Binding', 'Datapath_Binding', + 'SB_Global') + for table in tables: + helper.register_table(table) + super(OvnSbIdl, self).__init__( + None, connection_string, helper) + if chassis: + table = ('Chassis_Private' if 'Chassis_Private' in tables + else 'Chassis') + self.tables[table].condition = [['name', '==', chassis]] + + def _get_ovsdb_helper(self, connection_string): + return idlutils.get_schema_helper(connection_string, self.SCHEMA) + + def _check_and_set_ssl_files(self, schema_name): + priv_key_file = CONF.ovn_sb_private_key + cert_file = CONF.ovn_sb_certificate + ca_cert_file = CONF.ovn_sb_ca_cert + + if priv_key_file: + Stream.ssl_set_private_key_file(priv_key_file) + + if cert_file: + Stream.ssl_set_certificate_file(cert_file) + + if ca_cert_file: + Stream.ssl_set_ca_cert_file(ca_cert_file) + + def start(self): + conn = connection.Connection( + self, timeout=180) + ovsdbSbConn = OvsdbSbOvnIdl(conn) + if self._events: + self.notify_handler.watch_events(self._events) + return ovsdbSbConn + + +class Backend(ovs_idl.Backend): + lookup_table = {} + ovsdb_connection = None + + def __init__(self, connection): + self.ovsdb_connection = connection + super(Backend, self).__init__(connection) + + @property + def idl(self): + return self.ovsdb_connection.idl + + @property + def tables(self): + return self.idl.tables + + +class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend): + def __init__(self, connection): + super(OvsdbSbOvnIdl, self).__init__(connection) + self.idl._session.reconnect.set_probe_interval(60000) + + def _get_port_by_name(self, port): + cmd = self.db_find_rows('Port_Binding', ('logical_port', '=', port)) + port_info = cmd.execute(check_error=True) + if port_info: + return port_info[0] + return [] + + def _get_ports_by_datapath(self, datapath, port_type=None): + if port_type: + cmd = self.db_find_rows('Port_Binding', + ('datapath', '=', datapath), + ('type', '=', port_type)) + else: + cmd = self.db_find_rows('Port_Binding', + ('datapath', '=', datapath)) + return cmd.execute(check_error=True) + + def is_provider_network(self, datapath): + cmd = self.db_find_rows('Port_Binding', ('datapath', '=', datapath), + ('type', '=', 'localnet')) + return next(iter(cmd.execute(check_error=True)), None) + + def get_fip_associated(self, port): + cmd = self.db_find_rows('Port_Binding', ('type', '=', 'patch')) + for row in cmd.execute(check_error=True): + for fip in row.nat_addresses: + if port in fip: + return fip.split(" ")[1], row.datapath + return None, None + + def is_port_on_chassis(self, port_name, chassis): + port_info = self._get_port_by_name(port_name) + try: + if (port_info and port_info.type == constants.OVN_VM_VIF_PORT_TYPE and + port_info.chassis[0].name == chassis): + return True + except IndexError: + pass + return False + + def is_port_deleted(self, port_name): + port_info = self._get_port_by_name(port_name) + if port_info: + return False + return True + + def get_ports_on_chassis(self, chassis): + 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_network_name_and_tag(self, datapath, bridge_mappings): + for row in self._get_ports_by_datapath( + datapath, constants.OVN_LOCALNET_VIF_PORT_TYPE): + if (row.options and + row.options.get('network_name') in bridge_mappings): + return row.options.get('network_name'), row.tag + return None, None + + def get_network_vlan_tag_by_network_name(self, network_name): + cmd = self.db_find_rows('Port_Binding', ('type', '=', + constants.OVN_LOCALNET_VIF_PORT_TYPE)) + for row in cmd.execute(check_error=True): + if (row.options and + row.options.get('network_name') == network_name): + return row.tag + return None + + def is_router_gateway_on_chassis(self, datapath, chassis): + port_info = self._get_ports_by_datapath( + datapath, constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE) + try: + if port_info and port_info[0].chassis[0].name == chassis: + return port_info[0].logical_port + except IndexError: + pass + return None + + def get_lrp_port_for_datapath(self, datapath): + for row in self._get_ports_by_datapath( + datapath, constants.OVN_PATCH_VIF_PORT_TYPE): + if row.options: + return row.options['peer'] + return None + + def get_lrp_ports_for_router(self, datapath): + return self._get_ports_by_datapath( + datapath, constants.OVN_PATCH_VIF_PORT_TYPE) + + def get_port_datapath(self, port_name): + port_info = self._get_port_by_name(port_name) + if port_info: + return port_info.datapath + return None + + def get_ports_on_datapath(self, datapath): + return self._get_ports_by_datapath(datapath) + + def get_evpn_info_from_crlrp_port_name(self, port_name): + router_gateway_port_name = port_name.split('cr-lrp-')[1] + return self.get_evpn_info_from_port_name(router_gateway_port_name) + + def get_evpn_info_from_lrp_port_name(self, port_name): + router_interface_port_name = port_name.split('lrp-')[1] + return self.get_evpn_info_from_port_name(router_interface_port_name) + + def get_ip_from_port_peer(self, port): + peer_name = port.options['peer'] + peer_port = self._get_port_by_name(peer_name) + return peer_port.mac[0].split(' ')[1] + + def get_evpn_info_from_port(self, port): + return self.get_evpn_info(port) + + def get_evpn_info_from_port_name(self, port_name): + port = self._get_port_by_name(port_name) + return self.get_evpn_info(port) + + def get_evpn_info(self, port): + try: + evpn_info = { + 'vni': int(port.external_ids[ + constants.OVN_EVPN_VNI_EXT_ID_KEY]), + 'bgp_as': int(port.external_ids[ + constants.OVN_EVPN_AS_EXT_ID_KEY])} + except KeyError: + return {} + return evpn_info + + def get_port_if_local_chassis(self, port_name, chassis): + port = self._get_port_by_name(port_name) + if port.chassis[0].name == chassis: + return port + return None diff --git a/ovn_bgp_agent/drivers/openstack/utils/ovs.py b/ovn_bgp_agent/drivers/openstack/utils/ovs.py new file mode 100644 index 00000000..73128547 --- /dev/null +++ b/ovn_bgp_agent/drivers/openstack/utils/ovs.py @@ -0,0 +1,319 @@ +# 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 pyroute2 + +from oslo_concurrency import processutils +from ovs.db import idl + +from ovn_bgp_agent import constants +from ovn_bgp_agent.utils import linux_net + +from ovsdbapp.backend.ovs_idl import connection +from ovsdbapp.backend.ovs_idl import idlutils +from ovsdbapp.schema.open_vswitch import impl_idl as idl_ovs + + +def ovs_cmd(command, args, timeout=None): + full_args = [command] + if timeout is not None: + full_args += ['--timeout=%s' % timeout] + full_args += args + try: + return processutils.execute(*full_args, run_as_root=True) + except Exception as e: + print("Unable to execute {} {}. Exception: {}".format( + command, full_args, e)) + raise + + +def get_ovs_flows_info(bridge, flows_info, cookie): + ovs_ports = ovs_cmd('ovs-vsctl', + ['list-ports', bridge])[0].rstrip() + if not ovs_ports: + flow = ("cookie={}/-1").format(cookie) + ovs_cmd('ovs-ofctl', ['del-flows', bridge, flow]) + return + for ovs_port in ovs_ports.split("\n"): + ovs_ofport = ovs_cmd( + 'ovs-vsctl', + ['get', 'Interface', ovs_port, 'ofport'])[0].rstrip() + flows_info[bridge]['in_port'].add(ovs_ofport) + + +def remove_extra_ovs_flows(flows_info, cookie): + for bridge, info in flows_info.items(): + for in_port in info.get('in_port'): + flow = ("cookie={},priority=900,ip,in_port={}," + "actions=mod_dl_dst:{},NORMAL".format( + cookie, in_port, info['mac'])) + flow_v6 = ("cookie={},priority=900,ipv6,in_port={}," + "actions=mod_dl_dst:{},NORMAL".format( + cookie, in_port, info['mac'])) + ovs_cmd('ovs-ofctl', ['add-flow', bridge, flow]) + ovs_cmd('ovs-ofctl', ['add-flow', bridge, flow_v6]) + + cookie_id = ("cookie={}/-1").format(cookie) + current_flows = ovs_cmd( + 'ovs-ofctl', ['dump-flows', bridge, cookie_id] + )[0].split('\n')[1:-1] + for flow in current_flows: + agent_flow = False + for port in info.get('in_port'): + in_port = 'in_port={}'.format(port) + if in_port in flow: + agent_flow = True + break + if agent_flow: + continue + in_port = flow.split("in_port=")[1].split(" ")[0] + del_flow = ('{},in_port={}').format(cookie_id, in_port) + ovs_cmd('ovs-ofctl', ['del-flows', bridge, del_flow]) + + +def ensure_evpn_ovs_flow(bridge, cookie, mac, port, net, strip_vlan=False): + ovs_port = None + ovs_ports = ovs_cmd('ovs-vsctl', ['list-ports', bridge])[0].rstrip() + for p in ovs_ports.split('\n'): + if p.startswith('patch-provnet-'): + ovs_port = p + if not ovs_port: + return + ovs_ofport = ovs_cmd( + 'ovs-vsctl', ['get', 'Interface', ovs_port, 'ofport'] + )[0].rstrip() + vrf_ofport = ovs_cmd( + 'ovs-vsctl', ['get', 'Interface', port, 'ofport'] + )[0].rstrip() + + ip_version = linux_net.get_ip_version(net) + if ip_version == constants.IP_VERSION_6: + with pyroute2.NDB() as ndb: + if strip_vlan: + flow = ( + "cookie={},priority=1000,ipv6,in_port={},dl_src:{}," + "ipv6_src={} actions=mod_dl_dst:{},strip_vlan," + "output={}".format( + cookie, ovs_ofport, mac, net, + ndb.interfaces[bridge]['address'], vrf_ofport)) + else: + flow = ( + "cookie={},priority=1000,ipv6,in_port={},dl_src:{}," + "ipv6_src={} actions=mod_dl_dst:{},output={}".format( + cookie, ovs_ofport, mac, net, + ndb.interfaces[bridge]['address'], vrf_ofport)) + else: + with pyroute2.NDB() as ndb: + if strip_vlan: + flow = ( + "cookie={},priority=1000,ip,in_port={},dl_src:{},nw_src={}" + "actions=mod_dl_dst:{},strip_vlan,output={}".format( + cookie, ovs_ofport, mac, net, + ndb.interfaces[bridge]['address'], vrf_ofport)) + else: + flow = ( + "cookie={},priority=1000,ip,in_port={},dl_src:{},nw_src={}" + "actions=mod_dl_dst:{},output={}".format( + cookie, ovs_ofport, mac, net, + ndb.interfaces[bridge]['address'], vrf_ofport)) + ovs_cmd('ovs-ofctl', ['add-flow', bridge, flow]) + + +def remove_evpn_router_ovs_flows(bridge, cookie, mac): + cookie_id = ("cookie={}/-1").format(cookie) + + ovs_port = None + ovs_ports = ovs_cmd('ovs-vsctl', ['list-ports', bridge])[0].rstrip() + for p in ovs_ports.split('\n'): + if p.startswith('patch-provnet-'): + ovs_port = p + if not ovs_port: + return + ovs_ofport = ovs_cmd( + 'ovs-vsctl', ['get', 'Interface', ovs_port, 'ofport'] + )[0].rstrip() + + flow = ("{},ip,in_port={},dl_src:{}".format( + cookie_id, ovs_ofport, mac)) + ovs_cmd('ovs-ofctl', ['del-flows', bridge, flow]) + + flow_v6 = ("{},ipv6,in_port={},dl_src:{}".format(cookie_id, ovs_ofport, + mac)) + ovs_cmd('ovs-ofctl', ['del-flows', bridge, flow_v6]) + + +def remove_evpn_network_ovs_flow(bridge, cookie, mac, net): + cookie_id = ("cookie={}/-1").format(cookie) + + ovs_port = None + ovs_ports = ovs_cmd('ovs-vsctl', ['list-ports', bridge])[0].rstrip() + for p in ovs_ports.split('\n'): + if p.startswith('patch-provnet-'): + ovs_port = p + if not ovs_port: + return + ovs_ofport = ovs_cmd( + 'ovs-vsctl', ['get', 'Interface', ovs_port, 'ofport'] + )[0].rstrip() + + ip_version = linux_net.get_ip_version(net) + if ip_version == constants.IP_VERSION_6: + flow = ("{},ipv6,in_port={},dl_src:{},ipv6_src={}".format( + cookie_id, ovs_ofport, mac, net)) + else: + flow = ("{},ip,in_port={},dl_src:{},nw_src={}".format( + cookie_id, ovs_ofport, mac, net)) + ovs_cmd('ovs-ofctl', ['del-flows', bridge, flow]) + + +def ensure_default_ovs_flows(ovn_bridge_mappings, cookie): + cookie_id = ("cookie={}/-1").format(cookie) + for bridge in ovn_bridge_mappings: + ovs_port = ovs_cmd('ovs-vsctl', ['list-ports', bridge])[0].rstrip() + if not ovs_port: + continue + ovs_ofport = ovs_cmd( + 'ovs-vsctl', ['get', 'Interface', ovs_port, 'ofport'] + )[0].rstrip() + flow_filter = ('{},in_port={}').format(cookie_id, ovs_ofport) + current_flows = ovs_cmd( + 'ovs-ofctl', ['dump-flows', bridge, flow_filter] + )[0].split('\n')[1:-1] + if len(current_flows) == 1: + # assume the rule is the right one as it has the right cookie + # and in_port + continue + + with pyroute2.NDB() as ndb: + flow = ("cookie={},priority=900,ip,in_port={}," + "actions=mod_dl_dst:{},NORMAL".format( + cookie, ovs_ofport, + ndb.interfaces[bridge]['address'])) + flow_v6 = ("cookie={},priority=900,ipv6,in_port={}," + "actions=mod_dl_dst:{},NORMAL".format( + cookie, ovs_ofport, + ndb.interfaces[bridge]['address'])) + ovs_cmd('ovs-ofctl', ['add-flow', bridge, flow]) + ovs_cmd('ovs-ofctl', ['add-flow', bridge, flow_v6]) + + # Remove unneeded flows + port = 'in_port={}'.format(ovs_ofport) + current_flows = ovs_cmd( + 'ovs-ofctl', ['dump-flows', bridge, cookie_id] + )[0].split('\n')[1:-1] + for flow in current_flows: + if not flow or port in flow: + continue + in_port = flow.split("in_port=")[1].split(" ")[0] + del_flow = ('{},in_port={}').format(cookie_id, in_port) + ovs_cmd('ovs-ofctl', ['del-flows', bridge, del_flow]) + + +def add_device_to_ovs_bridge(device, bridge, vlan_tag=None): + if vlan_tag: + tag = "tag={}".format(vlan_tag) + ovs_cmd('ovs-vsctl', ['--may-exist', 'add-port', bridge, device, tag]) + else: + ovs_cmd('ovs-vsctl', ['--may-exist', 'add-port', bridge, device]) + + +def del_device_from_ovs_bridge(device, bridge=None): + if bridge: + ovs_cmd('ovs-vsctl', ['--if-exists', 'del-port', bridge, device]) + else: + ovs_cmd('ovs-vsctl', ['--if-exists', 'del-port', device]) + + +def get_bridge_flows_by_cookie(bridge, cookie): + cookie_id = ("cookie={}/-1").format(cookie) + return ovs_cmd('ovs-ofctl', + ['dump-flows', bridge, cookie_id])[0].split('\n')[1:-1] + + +def get_device_port_at_ovs(device): + return ovs_cmd( + 'ovs-vsctl', ['get', 'Interface', device, 'ofport'])[0].rstrip() + + +def del_flow(flow, bridge, cookie): + cookie_id = ("cookie={}/-1").format(cookie) + f = '{},priority{}'.format( + cookie_id, flow.split(' actions')[0].split(' priority')[1]) + ovs_cmd('ovs-ofctl', ['--strict', 'del-flows', bridge, f]) + + +def get_flow_info(flow): + # example: + # cookie=0x3e7, duration=85.005s, table=0, n_packets=0, + # n_bytes=0, idle_age=65534, priority=1000,ip,in_port=1 + # nw_src=20.0.0.0/24 actions=mod_dl_dst:1a:bd:c3:dc:6a:4c, + # output:5 + flow_mac = flow_port = flow_nw_src = flow_ipv6_src = None + try: + flow_mac = flow.split('dl_src=')[1].split(',')[0] + flow_port = flow.split('output:')[1].split(',')[0] + except (IndexError, TypeError): + pass + flow_nw = flow.split('nw_src=') + if len(flow_nw) == 2: + flow_nw_src = flow_nw[1].split(' ')[0] + flow_ipv6 = flow.split('ipv6_src=') + if len(flow_ipv6) == 2: + flow_ipv6_src = flow_ipv6[1].split(' ')[0] + + return {'mac': flow_mac, 'port': flow_port, 'nw_src': flow_nw_src, + 'ipv6_src': flow_ipv6_src} + + +class OvsIdl(object): + def start(self, connection_string): + helper = idlutils.get_schema_helper(connection_string, + 'Open_vSwitch') + tables = ('Open_vSwitch', 'Bridge', 'Port', 'Interface') + for table in tables: + helper.register_table(table) + ovs_idl = idl.Idl(connection_string, helper) + ovs_idl._session.reconnect.set_probe_interval(60000) + conn = connection.Connection( + ovs_idl, timeout=180) + self.idl_ovs = idl_ovs.OvsdbIdl(conn) + + def get_own_chassis_name(self): + """Return the external_ids:system-id value of the Open_vSwitch table. + + As long as ovn-controller is running on this node, the key is + guaranteed to exist and will include the chassis name. + """ + ext_ids = self.idl_ovs.db_get( + 'Open_vSwitch', '.', 'external_ids').execute() + return ext_ids['system-id'] + + def get_ovn_remote(self): + """Return the external_ids:ovn-remote value of the Open_vSwitch table. + + """ + ext_ids = self.idl_ovs.db_get( + 'Open_vSwitch', '.', 'external_ids').execute() + return ext_ids['ovn-remote'] + + def get_ovn_bridge_mappings(self): + """Return the external_ids:ovn-bridge-mappings value of the Open_vSwitch table. + + """ + ext_ids = self.idl_ovs.db_get( + 'Open_vSwitch', '.', 'external_ids').execute() + try: + return ext_ids['ovn-bridge-mappings'].split(",") + except KeyError: + return [] diff --git a/ovn_bgp_agent/drivers/openstack/watchers/__init__.py b/ovn_bgp_agent/drivers/openstack/watchers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py b/ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py new file mode 100644 index 00000000..836b244e --- /dev/null +++ b/ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py @@ -0,0 +1,270 @@ +# 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. + +from ovn_bgp_agent import constants + +from ovsdbapp.backend.ovs_idl import event as row_event + +from oslo_concurrency import lockutils + +_SYNC_STATE_LOCK = lockutils.ReaderWriterLock() + + +class PortBindingChassisEvent(row_event.RowEvent): + def __init__(self, bgp_agent, events): + self.agent = bgp_agent + table = 'Port_Binding' + super(PortBindingChassisEvent, self).__init__( + events, table, None) + self.event_name = self.__class__.__name__ + + +class PortBindingChassisCreatedEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_UPDATE,) + super(PortBindingChassisCreatedEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + # single and dual-stack format + if (len(row.mac[0].split(' ')) != 2 and + len(row.mac[0].split(' ')) != 3): + return False + return (row.chassis[0].name == self.agent.chassis and + not old.chassis) + except (IndexError, AttributeError): + return False + + def run(self, event, row, old): + if row.type not in constants.OVN_VIF_PORT_TYPES: + return + with _SYNC_STATE_LOCK.read_lock(): + ips = [row.mac[0].split(' ')[1]] + # for dual-stack + if len(row.mac[0].split(' ')) == 3: + ips.append(row.mac[0].split(' ')[2]) + self.agent.expose_ip(ips, row) + + +class PortBindingChassisDeletedEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_UPDATE, self.ROW_DELETE,) + super(PortBindingChassisDeletedEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + # single and dual-stack format + if (len(row.mac[0].split(' ')) != 2 and + len(row.mac[0].split(' ')) != 3): + return False + if event == self.ROW_UPDATE: + return (old.chassis[0].name == self.agent.chassis and + not row.chassis) + else: + if row.chassis[0].name == self.agent.chassis: + return True + except (IndexError, AttributeError): + return False + + def run(self, event, row, old): + if row.type not in constants.OVN_VIF_PORT_TYPES: + return + with _SYNC_STATE_LOCK.read_lock(): + ips = [row.mac[0].split(' ')[1]] + # for dual-stack + if len(row.mac[0].split(' ')) == 3: + ips.append(row.mac[0].split(' ')[2]) + self.agent.withdraw_ip(ips, row) + + +class FIPSetEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_UPDATE,) + super(FIPSetEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + return (not row.chassis and + row.nat_addresses != old.nat_addresses and + not row.logical_port.startswith('lrp-')) + except (AttributeError): + return False + + def run(self, event, row, old): + if row.type != 'patch': + return + with _SYNC_STATE_LOCK.read_lock(): + for nat in row.nat_addresses: + if nat not in old.nat_addresses: + ip = nat.split(" ")[1] + port = nat.split(" ")[2].split("\"")[1] + self.agent.expose_ip([ip], row, associated_port=port) + + +class FIPUnsetEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_UPDATE,) + super(FIPUnsetEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + return (not row.chassis and + row.nat_addresses != old.nat_addresses and + not row.logical_port.startswith('lrp-')) + except (AttributeError): + return False + + def run(self, event, row, old): + if row.type != 'patch': + return + with _SYNC_STATE_LOCK.read_lock(): + for nat in old.nat_addresses: + if nat not in row.nat_addresses: + ip = nat.split(" ")[1] + port = nat.split(" ")[2].split("\"")[1] + self.agent.withdraw_ip([ip], row, associated_port=port) + + +class SubnetRouterAttachedEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_CREATE,) + super(SubnetRouterAttachedEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + # single and dual-stack format + if (len(row.mac[0].split(' ')) != 2 and + len(row.mac[0].split(' ')) != 3): + return False + return (not row.chassis and row.logical_port.startswith('lrp-')) + except (IndexError, AttributeError): + return False + + def run(self, event, row, old): + if row.type != 'patch': + return + with _SYNC_STATE_LOCK.read_lock(): + ip_address = row.mac[0].split(' ')[1] + self.agent.expose_subnet(ip_address, row) + + +class SubnetRouterDetachedEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_DELETE,) + super(SubnetRouterDetachedEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + # single and dual-stack format + if (len(row.mac[0].split(' ')) != 2 and + len(row.mac[0].split(' ')) != 3): + return False + return (not row.chassis and row.logical_port.startswith('lrp-')) + except (IndexError, AttributeError): + return False + + def run(self, event, row, old): + if row.type != 'patch': + return + with _SYNC_STATE_LOCK.read_lock(): + ip_address = row.mac[0].split(' ')[1] + self.agent.withdraw_subnet(ip_address, row) + + +class TenantPortCreatedEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_UPDATE,) + super(TenantPortCreatedEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + # single and dual-stack format + if (len(row.mac[0].split(' ')) != 2 and + len(row.mac[0].split(' ')) != 3): + return False + return (not old.chassis and + self.agent.ovn_local_lrps != []) + except (IndexError, AttributeError): + return False + + def run(self, event, row, old): + if row.type != constants.OVN_VM_VIF_PORT_TYPE and row.type != constants.OVN_VIRTUAL_VIF_PORT_TYPE: + return + with _SYNC_STATE_LOCK.read_lock(): + ips = [row.mac[0].split(' ')[1]] + # for dual-stack + if len(row.mac[0].split(' ')) == 3: + ips.append(row.mac[0].split(' ')[2]) + self.agent.expose_remote_ip(ips, row) + + +class TenantPortDeletedEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_DELETE,) + super(TenantPortDeletedEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + # single and dual-stack format + if (len(row.mac[0].split(' ')) != 2 and + len(row.mac[0].split(' ')) != 3): + return False + return (self.agent.ovn_local_lrps != []) + except (IndexError, AttributeError): + return False + + def run(self, event, row, old): + if row.type != constants.OVN_VM_VIF_PORT_TYPE and row.type != constants.OVN_VIRTUAL_VIF_PORT_TYPE: + return + with _SYNC_STATE_LOCK.read_lock(): + ips = [row.mac[0].split(' ')[1]] + # for dual-stack + if len(row.mac[0].split(' ')) == 3: + ips.append(row.mac[0].split(' ')[2]) + self.agent.withdraw_remote_ip(ips, row) + + +class ChassisCreateEventBase(row_event.RowEvent): + table = None + + def __init__(self, bgp_agent): + self.agent = bgp_agent + self.first_time = True + events = (self.ROW_CREATE,) + super(ChassisCreateEventBase, self).__init__( + events, self.table, (('name', '=', self.agent.chassis),)) + self.event_name = self.__class__.__name__ + + def run(self, event, row, old): + if self.first_time: + self.first_time = False + else: + print("Connection to OVSDB established, doing a full sync") + self.agent.sync() + + +class ChassisCreateEvent(ChassisCreateEventBase): + table = 'Chassis' + + +class ChassisPrivateCreateEvent(ChassisCreateEventBase): + table = 'Chassis_Private' diff --git a/ovn_bgp_agent/tests/unit/__init__.py b/ovn_bgp_agent/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ovn_bgp_agent/tests/unit/cmd/__init__.py b/ovn_bgp_agent/tests/unit/cmd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ovn_bgp_agent/tests/unit/cmd/test_agent.py b/ovn_bgp_agent/tests/unit/cmd/test_agent.py new file mode 100644 index 00000000..e7acf5b7 --- /dev/null +++ b/ovn_bgp_agent/tests/unit/cmd/test_agent.py @@ -0,0 +1,27 @@ +# 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. + + +from unittest import mock + +from ovn_bgp_agent.tests import base as test_base + + +class TestAgentCmd(test_base.TestCase): + @mock.patch('ovn_bgp_agent.agent.start') + def test_start(self, m_start): + from ovn_bgp_agent.cmd import agent # To make it import a mock. + agent.start() + + m_start.assert_called() diff --git a/ovn_bgp_agent/tests/unit/test_agent.py b/ovn_bgp_agent/tests/unit/test_agent.py new file mode 100644 index 00000000..55c4e81c --- /dev/null +++ b/ovn_bgp_agent/tests/unit/test_agent.py @@ -0,0 +1,38 @@ +# 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. + +from unittest import mock + +from ovn_bgp_agent import agent +from ovn_bgp_agent.tests import base as test_base + + +class TestAgent(test_base.TestCase): + + @mock.patch('oslo_service.service.launch') + @mock.patch('ovn_bgp_agent.config.init') + @mock.patch('ovn_bgp_agent.config.setup_logging') + @mock.patch('ovn_bgp_agent.agent.BGPAgent') + def test_start(self, m_agent, m_setup_logging, + m_config_init, m_oslo_launch): + m_launcher = mock.Mock() + m_oslo_launch.return_value = m_launcher + + agent.start() + + m_config_init.assert_called() + m_setup_logging.assert_called() + m_agent.assert_called() + m_oslo_launch.assert_called() + m_launcher.wait.assert_called() diff --git a/ovn_bgp_agent/utils/__init__.py b/ovn_bgp_agent/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ovn_bgp_agent/utils/linux_net.py b/ovn_bgp_agent/utils/linux_net.py new file mode 100644 index 00000000..bed841cc --- /dev/null +++ b/ovn_bgp_agent/utils/linux_net.py @@ -0,0 +1,684 @@ +# 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 ipaddress +import pyroute2 +import random +import re +import sys + +from pyroute2.netlink.rtnl import ndmsg +from socket import AF_INET +from socket import AF_INET6 + +from oslo_concurrency import processutils +from oslo_log import log as logging + +from ovn_bgp_agent import constants + +LOG = logging.getLogger(__name__) + + +def get_ip_version(ip): + return ipaddress.ip_address(ip.split('/')[0]).version + + +def get_interfaces(filter_out=[]): + with pyroute2.NDB() as ndb: + return [iface.ifname for iface in ndb.interfaces + if iface.ifname not in filter_out] + + +def get_interface_index(nic): + with pyroute2.NDB() as ndb: + return ndb.interfaces[nic]['index'] + + +def ensure_vrf(vrf_name, vrf_table): + with pyroute2.NDB() as ndb: + try: + with ndb.interfaces[vrf_name] as vrf: + if vrf['state'] != constants.LINK_UP: + vrf['state'] = constants.LINK_UP + except KeyError: + ndb.interfaces.create( + kind="vrf", ifname=vrf_name, vrf_table=int(vrf_table)).set( + 'state', constants.LINK_UP).commit() + + +def ensure_bridge(bridge_name): + with pyroute2.NDB() as ndb: + try: + with ndb.interfaces[bridge_name] as bridge: + if bridge['state'] != constants.LINK_UP: + bridge['state'] = constants.LINK_UP + except KeyError: + ndb.interfaces.create( + kind="bridge", ifname=bridge_name, br_stp_state=0).set( + 'state', constants.LINK_UP).commit() + + +def ensure_vxlan(vxlan_name, vni, lo_ip): + with pyroute2.NDB() as ndb: + try: + with ndb.interfaces[vxlan_name] as vxlan: + if vxlan['state'] != constants.LINK_UP: + vxlan['state'] = constants.LINK_UP + except KeyError: + # FIXME: Perhaps we need to set neigh_suppress on + ndb.interfaces.create( + kind="vxlan", ifname=vxlan_name, vxlan_id=int(vni), + vxlan_port=4789, vxlan_local=lo_ip, vxlan_learning=False).set( + 'state', constants.LINK_UP).commit() + + +def set_master_for_device(device, master): + with pyroute2.NDB() as ndb: + # Check if already associated to the master, and associate it if not + if (ndb.interfaces[device].get('master') != + ndb.interfaces[master]['index']): + with ndb.interfaces[device] as iface: + iface.set('master', ndb.interfaces[master]['index']) + + +def ensure_dummy_device(device): + with pyroute2.NDB() as ndb: + try: + with ndb.interfaces[device] as iface: + if iface['state'] != constants.LINK_UP: + iface['state'] = constants.LINK_UP + except KeyError: + ndb.interfaces.create( + kind="dummy", ifname=device).set('state', + constants.LINK_UP).commit() + + +def ensure_ovn_device(ovn_ifname, vrf_name): + ensure_dummy_device(ovn_ifname) + set_master_for_device(ovn_ifname, vrf_name) + + +def delete_device(device): + try: + with pyroute2.NDB() as ndb: + ndb.interfaces[device].remove().commit() + except KeyError: + LOG.debug("Interfaces %s already deleted.", device) + + +def ensure_routing_table_for_bridge(ovn_routing_tables, bridge): + # check a routing table with the bridge name exists on + # /etc/iproute2/rt_tables + regex = r'^[0-9]*[\s]*{}$'.format(bridge) + matching_table = [line.replace('\t', ' ') + for line in open('/etc/iproute2/rt_tables') + if re.findall(regex, line)] + if matching_table: + table_info = matching_table[0].strip().split() + ovn_routing_tables[table_info[1]] = int(table_info[0]) + LOG.debug("Found routing table for %s with: %s", bridge, + table_info) + # if not configured, add random number for the table + else: + LOG.debug("Routing table for bridge %s not configured " + "at /etc/iproute2/rt_tables", bridge) + regex = r'^[0-9]+[\s]*' + existing_routes = [int(line.replace('\t', ' ').split(' ')[0]) + for line in open('/etc/iproute2/rt_tables') + if re.findall(regex, line)] + # pick a number between 1 and 252 + try: + table_number = random.choice(list( + set([x for x in range(1, 253)]).difference( + set(existing_routes)))) + except IndexError: + LOG.error("No more routing tables available for bridge %s " + "at /etc/iproute2/rt_tables", bridge) + sys.exit() + + with open('/etc/iproute2/rt_tables', 'a') as rt_tables: + rt_tables.write('{} {}\n'.format(table_number, bridge)) + + ovn_routing_tables[bridge] = int(table_number) + LOG.debug("Added routing table for %s with number: %s", bridge, + table_number) + + # add default route on that table if it does not exist + extra_routes = [] + + with pyroute2.NDB() as ndb: + table_route_dsts = set([r.dst for r in ndb.routes.summary().filter( + table=ovn_routing_tables[bridge])]) + if not table_route_dsts: + ndb.routes.create(dst='default', + oif=ndb.interfaces[bridge]['index'], + table=ovn_routing_tables[bridge], + scope=253, + proto=3).commit() + ndb.routes.create(dst='default', + oif=ndb.interfaces[bridge]['index'], + table=ovn_routing_tables[bridge], + family=AF_INET6, + proto=3).commit() + else: + route_missing = True + route6_missing = True + for dst in table_route_dsts: + if not dst: # default route + try: + route = ndb.routes[ + {'table': ovn_routing_tables[bridge], + 'dst': '', + 'family': AF_INET}] + if (bridge == + ndb.interfaces[{'index': route['oif']}][ + 'ifname']): + route_missing = False + else: + extra_routes.append(route) + except KeyError: + pass # no ipv4 default rule + try: + route_6 = ndb.routes[ + {'table': ovn_routing_tables[bridge], + 'dst': '', + 'family': AF_INET6}] + if (bridge == + ndb.interfaces[{'index': route_6['oif']}][ + 'ifname']): + route6_missing = False + else: + extra_routes.append(route_6) + except KeyError: + pass # no ipv6 default rule + else: + extra_routes.append( + ndb.routes[{'table': ovn_routing_tables[bridge], + 'dst': dst}] + ) + + if route_missing: + ndb.routes.create(dst='default', + oif=ndb.interfaces[bridge]['index'], + table=ovn_routing_tables[bridge], + scope=253, + proto=3).commit() + if route6_missing: + ndb.routes.create(dst='default', + oif=ndb.interfaces[bridge]['index'], + table=ovn_routing_tables[bridge], + family=AF_INET6, + proto=3).commit() + return extra_routes + + +def ensure_vlan_device_for_network(bridge, vlan_tag): + vlan_device_name = '{}.{}'.format(bridge, vlan_tag) + + with pyroute2.NDB() as ndb: + try: + with ndb.interfaces[vlan_device_name] as iface: + if iface['state'] != constants.LINK_UP: + iface['state'] = constants.LINK_UP + except KeyError: + ndb.interfaces.create( + kind="vlan", ifname=vlan_device_name, vlan_id=vlan_tag, + link=ndb.interfaces[bridge]['index']).set('state', + constants.LINK_UP).commit() + + ipv4_flag = "net.ipv4.conf.{}/{}.proxy_arp".format(bridge, vlan_tag) + _set_kernel_flag(ipv4_flag, 1) + ipv6_flag = "net.ipv6.conf.{}/{}.proxy_ndp".format(bridge, vlan_tag) + _set_kernel_flag(ipv6_flag, 1) + + +def delete_vlan_device_for_network(bridge, vlan_tag): + vlan_device_name = '{}.{}'.format(bridge, vlan_tag) + delete_device(vlan_device_name) + + +def _set_kernel_flag(flag, value): + command = ["sysctl", "-w", "{}={}".format(flag, value)] + try: + return processutils.execute(*command, run_as_root=True) + except Exception as e: + LOG.error("Unable to execute %s. Exception: %s", command, e) + raise + + +def get_exposed_ips(nic): + exposed_ips = [] + with pyroute2.NDB() as ndb: + exposed_ips = [ip.address + for ip in ndb.interfaces[nic].ipaddr.summary() + if ip.prefixlen == 32 or ip.prefixlen == 128] + return exposed_ips + + +def get_nic_ip(nic, ip_version): + prefix = 32 + if ip_version == constants.IP_VERSION_6: + prefix = 128 + exposed_ips = [] + with pyroute2.NDB() as ndb: + exposed_ips = [ip.address + for ip in ndb.interfaces[nic].ipaddr.summary().filter( + prefixlen=prefix)] + return exposed_ips + + +def get_exposed_ips_on_network(nic, network): + exposed_ips = [] + with pyroute2.NDB() as ndb: + exposed_ips = [ip.address + for ip in ndb.interfaces[nic].ipaddr.summary() + if ((ip.prefixlen == 32 or ip.prefixlen == 128) and + ipaddress.ip_address(ip.address) in network)] + return exposed_ips + + +def get_ovn_ip_rules(routing_table): + # get the rules pointing to ovn bridges + ovn_ip_rules = {} + with pyroute2.NDB() as ndb: + rules_info = [(rule.table, + "{}/{}".format(rule.dst, rule.dst_len), + rule.family) for rule in ndb.rules.dump() + if rule.table in routing_table] + for table, dst, family in rules_info: + ovn_ip_rules[dst] = {'table': table, 'family': family} + return ovn_ip_rules + + +def delete_exposed_ips(ips, nic): + with pyroute2.NDB() as ndb: + for ip in ips: + address = '{}/32'.format(ip) + if get_ip_version(ip) == constants.IP_VERSION_6: + address = '{}/128'.format(ip) + try: + ndb.interfaces[nic].ipaddr[address].remove().commit() + except KeyError: + LOG.debug("IP address {} already removed from nic {}.".format( + ip, nic)) + + +def delete_ip_rules(ip_rules): + with pyroute2.NDB() as ndb: + for rule_ip, rule_info in ip_rules.items(): + rule = {'dst': rule_ip.split("/")[0], + 'dst_len': rule_ip.split("/")[1], + 'table': rule_info['table'], + 'family': rule_info['family']} + try: + with ndb.rules[rule] as r: + r.remove() + except KeyError: + LOG.debug("Rule {} already deleted".format(rule)) + except pyroute2.netlink.exceptions.NetlinkError: + # FIXME: There is a issue with NDB and ip rules deletion: + # https://github.com/svinota/pyroute2/issues/771 + LOG.debug("This should not happen, skipping: NetlinkError " + "deleting rule %s", rule) + + +def delete_bridge_ip_routes(routing_tables, routing_tables_routes, + extra_routes): + with pyroute2.NDB() as ndb: + for bridge, routes_info in routing_tables_routes.items(): + if not extra_routes[bridge]: + continue + for route_info in routes_info: + oif = ndb.interfaces[bridge]['index'] + if route_info['vlan']: + vlan_device_name = '{}.{}'.format(bridge, + route_info['vlan']) + oif = ndb.interfaces[vlan_device_name]['index'] + if 'gateway' in route_info['route'].keys(): # subnet route + possible_matchings = [ + r for r in extra_routes[bridge] + if (r['dst'] == route_info['route']['dst'] and + r['dst_len'] == route_info['route']['dst_len'] and + r['gateway'] == route_info['route']['gateway'])] + else: # cr-lrp + possible_matchings = [ + r for r in extra_routes[bridge] + if (r['dst'] == route_info['route']['dst'] and + r['dst_len'] == route_info['route']['dst_len'] and + r['oif'] == oif)] + for r in possible_matchings: + extra_routes[bridge].remove(r) + + for bridge, routes in extra_routes.items(): + for route in routes: + r_info = {'dst': route['dst'], + 'dst_len': route['dst_len'], + 'family': route['family'], + 'oif': route['oif'], + 'gateway': route['gateway'], + 'table': routing_tables[bridge]} + try: + with ndb.routes[r_info] as r: + r.remove() + except KeyError: + LOG.debug("Route already deleted: {}".format(route)) + + +def delete_routes_from_table(table): + with pyroute2.NDB() as ndb: + # FIXME: problem in pyroute2 removing routes with local (254) scope + table_routes = [r for r in ndb.routes.dump().filter(table=table) + if r.scope != 254 and r.proto != 186] + for route in table_routes: + try: + with ndb.routes[route] as r: + r.remove() + except KeyError: + LOG.debug("Route already deleted: %s", route) + + +def get_routes_on_tables(table_ids): + 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.proto != 186] + + +def delete_ip_routes(routes): + with pyroute2.NDB() as ndb: + for route in routes: + r_info = {'dst': route['dst'], + 'dst_len': route['dst_len'], + 'family': route['family'], + 'oif': route['oif'], + 'gateway': route['gateway'], + 'table': route['table']} + try: + with ndb.routes[r_info] as r: + r.remove() + except KeyError: + LOG.debug("Route already deleted: %s", route) + + +def add_ndp_proxy(ip, dev, vlan=None): + # FIXME(ltomasbo): This should use pyroute instead but I didn't find + # out how + net_ip = str(ipaddress.IPv6Network(ip, strict=False).network_address) + dev_name = dev + if vlan: + dev_name = "{}.{}".format(dev, vlan) + command = ["ip", "-6", "nei", "add", "proxy", net_ip, "dev", dev_name] + try: + return processutils.execute(*command, run_as_root=True) + except Exception as e: + LOG.error("Unable to execute %s. Exception: %s", command, e) + raise + + +def del_ndp_proxy(ip, dev, vlan=None): + # FIXME(ltomasbo): This should use pyroute instead but I didn't find + # out how + net_ip = str(ipaddress.IPv6Network(ip, strict=False).network_address) + dev_name = dev + if vlan: + dev_name = "{}.{}".format(dev, vlan) + command = ["ip", "-6", "nei", "del", "proxy", net_ip, "dev", dev_name] + try: + return processutils.execute(*command, run_as_root=True) + except Exception as e: + if "No such file or directory" in e.stderr: + # Already deleted + return + LOG.error("Unable to execute %s. Exception: %s", command, e) + raise + + +def add_ips_to_dev(nic, ips, clear_local_route_at_table=False): + with pyroute2.NDB() as ndb: + try: + with ndb.interfaces[nic] as iface: + for ip in ips: + address = '{}/32'.format(ip) + if get_ip_version(ip) == constants.IP_VERSION_6: + address = '{}/128'.format(ip) + iface.add_ip(address) + except KeyError: + # NDB raises KeyError: 'object exists' + # if the ip is already added + pass + + if clear_local_route_at_table: + with pyroute2.NDB() as ndb: + for ip in ips: + route = {'table': clear_local_route_at_table, + 'proto': 2, + 'scope': 254, + 'dst': ip} + try: + with ndb.routes[route] as r: + r.remove() + except (KeyError, ValueError): + LOG.debug("Local route already deleted: %s", route) + + +def del_ips_from_dev(nic, ips): + with pyroute2.NDB() as ndb: + with ndb.interfaces[nic] as iface: + for ip in ips: + address = '{}/32'.format(ip) + if get_ip_version(ip) == constants.IP_VERSION_6: + address = '{}/128'.format(ip) + iface.del_ip(address) + + +def add_ip_rule(ip, table, dev=None, lladdr=None): + ip_version = get_ip_version(ip) + ip_info = ip.split("/") + + if len(ip_info) == 1: + rule = {'dst': ip_info[0], 'table': table, 'dst_len': 32} + if ip_version == constants.IP_VERSION_6: + rule['dst_len'] = 128 + rule['family'] = AF_INET6 + elif len(ip_info) == 2: + rule = {'dst': ip_info[0], 'table': table, 'dst_len': int(ip_info[1])} + if ip_version == constants.IP_VERSION_6: + rule['family'] = AF_INET6 + else: + LOG.error("Invalid ip: %s", ip) + return + + with pyroute2.NDB() as ndb: + try: + ndb.rules[rule] + except KeyError: + LOG.debug("Creating ip rule with: %s", rule) + ndb.rules.create(rule).commit() + + # FIXME: There is no support for creating neighbours in NDB + # So we are using iproute here + if lladdr: + ip_version = get_ip_version(ip) + with pyroute2.IPRoute() as iproute: + # This is doing something like: + # sudo ip nei replace 172.24.4.69 + # lladdr fa:16:3e:d3:5d:7b dev br-ex nud permanent + network_bridge_if = iproute.link_lookup(ifname=dev)[0] + if ip_version == constants.IP_VERSION_6: + iproute.neigh('set', + dst=ip, + lladdr=lladdr, + family=AF_INET6, + ifindex=network_bridge_if, + state=ndmsg.states['permanent']) + else: + iproute.neigh('set', + dst=ip, + lladdr=lladdr, + ifindex=network_bridge_if, + state=ndmsg.states['permanent']) + + +def del_ip_rule(ip, table, dev=None, lladdr=None): + ip_version = get_ip_version(ip) + ip_info = ip.split("/") + + if len(ip_info) == 1: + rule = {'dst': ip_info[0], 'table': table, 'dst_len': 32} + if ip_version == constants.IP_VERSION_6: + rule['dst_len'] = 128 + rule['family'] = AF_INET6 + elif len(ip_info) == 2: + rule = {'dst': ip_info[0], 'table': table, 'dst_len': int(ip_info[1])} + if ip_version == constants.IP_VERSION_6: + rule['family'] = AF_INET6 + else: + LOG.error("Invalid ip: {}".format(ip)) + return + with pyroute2.NDB() as ndb: + try: + ndb.rules[rule].remove().commit() + LOG.debug("Deleting ip rule with: %s", rule) + except KeyError: + LOG.debug("Rule already deleted: %s", rule) + + # FIXME: There is no support for deleting neighbours in NDB + # So we are using iproute here + if lladdr: + ip_version = get_ip_version(ip) + with pyroute2.IPRoute() as iproute: + # This is doing something like: + # sudo ip nei del 172.24.4.69 + # lladdr fa:16:3e:d3:5d:7b dev br-ex nud permanent + network_bridge_if = iproute.link_lookup( + ifname=dev)[0] + if ip_version == constants.IP_VERSION_6: + iproute.neigh('del', + dst=ip.split("/")[0], + lladdr=lladdr, + family=AF_INET6, + ifindex=network_bridge_if, + state=ndmsg.states['permanent']) + else: + iproute.neigh('del', + dst=ip.split("/")[0], + lladdr=lladdr, + ifindex=network_bridge_if, + state=ndmsg.states['permanent']) + + +def add_unreachable_route(vrf_name): + # FIXME: This should use pyroute instead but I didn't find + # out how + for ip_version in [-4, -6]: + command = ["ip", ip_version, "route", "add", "vrf", vrf_name, + "unreachable", "default", "metric", "4278198272"] + try: + return processutils.execute(*command, run_as_root=True) + except Exception as e: + if "RTNETLINK answers: File exists" in e.stderr: + continue + LOG.error("Unable to execute %s. Exception: %s", command, e) + raise + + +def add_ip_route(ovn_routing_tables_routes, ip_address, route_table, dev, + vlan=None, mask=None, via=None): + net_ip = ip_address + if not mask: # default /32 or /128 + if get_ip_version(ip_address) == constants.IP_VERSION_6: + mask = 128 + else: + mask = 32 + else: + ip = '{}/{}'.format(ip_address, mask) + if get_ip_version(ip_address) == constants.IP_VERSION_6: + net_ip = '{}'.format(ipaddress.IPv6Network( + ip, strict=False).network_address) + else: + net_ip = '{}'.format(ipaddress.IPv4Network( + ip, strict=False).network_address) + + with pyroute2.NDB() as ndb: + if vlan: + oif_name = '{}.{}'.format(dev, vlan) + oif = ndb.interfaces[oif_name]['index'] + else: + oif = ndb.interfaces[dev]['index'] + + route = {'dst': net_ip, 'dst_len': int(mask), 'oif': oif, + 'table': int(route_table), 'proto': 3} + if via: + route['gateway'] = via + route['scope'] = 0 + else: + route['scope'] = 253 + if get_ip_version(net_ip) == constants.IP_VERSION_6: + route['family'] = AF_INET6 + del route['scope'] + + with pyroute2.NDB() as ndb: + try: + with ndb.routes[route] as r: + LOG.debug("Route already existing: %s", r) + except KeyError: + ndb.routes.create(route).commit() + LOG.debug("Route created at table %s: %s", route_table, route) + route_info = {'vlan': vlan, 'route': route} + ovn_routing_tables_routes.setdefault(dev, []).append(route_info) + + +def del_ip_route(ovn_routing_tables_routes, ip_address, route_table, dev, + vlan=None, mask=None, via=None): + net_ip = ip_address + if not mask: # default /32 or /128 + if get_ip_version(ip_address) == constants.IP_VERSION_6: + mask = 128 + else: + mask = 32 + else: + ip = '{}/{}'.format(ip_address, mask) + if get_ip_version(ip_address) == constants.IP_VERSION_6: + net_ip = '{}'.format(ipaddress.IPv6Network( + ip, strict=False).network_address) + else: + net_ip = '{}'.format(ipaddress.IPv4Network( + ip, strict=False).network_address) + + with pyroute2.NDB() as ndb: + if vlan: + oif_name = '{}.{}'.format(dev, vlan) + oif = ndb.interfaces[oif_name]['index'] + else: + oif = ndb.interfaces[dev]['index'] + + route = {'dst': net_ip, 'dst_len': int(mask), 'oif': oif, + 'table': int(route_table), 'proto': 3} + if via: + route['gateway'] = via + route['scope'] = 0 + else: + route['scope'] = 253 + if get_ip_version(net_ip) == constants.IP_VERSION_6: + route['family'] = AF_INET6 + + with pyroute2.NDB() as ndb: + try: + with ndb.routes[route] as r: + r.remove() + LOG.debug("Route deleted at table %s: %s", route_table, route) + route_info = {'vlan': vlan, 'route': route} + ovn_routing_tables_routes[dev].remove(route_info) + except (KeyError, ValueError): + LOG.debug("Route already deleted: %s", route) diff --git a/requirements.txt b/requirements.txt index 1d18dd3d..b6ab2212 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,14 @@ # process, which may cause wedges in the gate later. pbr>=2.0 # Apache-2.0 + +Jinja2>=2.10 # BSD License (3 clause) +oslo.concurrency>=3.26.0 # Apache-2.0 +oslo.config>=6.1.0 # Apache-2.0 +oslo.log>=3.36.0 # Apache-2.0 +oslo.service>=1.40.2 # Apache-2.0 +ovs>=2.8.0 # Apache-2.0 +ovsdbapp>=1.4.0 # Apache-2.0 +pyroute2>=0.6.4;sys_platform!='win32' # Apache-2.0 (+ dual licensed GPL2) +stevedore>=1.20.0 # Apache-2.0 + diff --git a/setup.cfg b/setup.cfg index 25bab596..c21a2cef 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,3 +23,10 @@ classifier = [files] packages = ovn_bgp_agent + +[entry_points] +console_scripts = + bgp-agent = ovn_bgp_agent.cmd.agent:start + +ovn_bgp_agent.drivers = + osp_ovn_bgp_driver = ovn_bgp_agent.drivers.openstack.ovn_bgp_driver:OSPOVNBGPDriver diff --git a/tox.ini b/tox.ini index 634f1ae8..9406555e 100644 --- a/tox.ini +++ b/tox.ini @@ -53,6 +53,6 @@ commands = oslo_debug_helper {posargs} # E123, E125 skipped as they are invalid PEP-8. show-source = True -ignore = E123,E125 +ignore = E123,E125,W504 builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build