Add support for l3vpn with NB driver
Creates VRF/VXLAN per VNI, exposed through FRR with kernel routes Vlan interfaces are added to the appropriate VNI, configured per bgpvpn config options on the logical switch. Related-bug: #2051105 Change-Id: I097c4629922d787827aba7761164f4004ed1305a
This commit is contained in:
parent
bdbf573ae4
commit
b3ca890f47
3
doc/images/evpn-flow-l3vpn.svg
Normal file
3
doc/images/evpn-flow-l3vpn.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 55 KiB |
1
doc/images/src/evpn-flow-l3vpn.drawio
Normal file
1
doc/images/src/evpn-flow-l3vpn.drawio
Normal file
File diff suppressed because one or more lines are too long
@ -53,8 +53,8 @@ OVS-DPDK and hardware offload (HWOL) is supported.
|
|||||||
| L2VNI | Extends the L2 segment on a given VNI. | No need to expose it, automatic with the | Ingress: vxlan + bridge device | N/A | No | No |
|
| L2VNI | Extends the L2 segment on a given VNI. | No need to expose it, automatic with the | Ingress: vxlan + bridge device | N/A | No | No |
|
||||||
| | | FRR configuration and the wiring. | Egress: nothing | | | |
|
| | | FRR configuration and the wiring. | Egress: nothing | | | |
|
||||||
+-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+--------------------------+-----------------------+---------------+
|
+-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+--------------------------+-----------------------+---------------+
|
||||||
| VRF | Expose IPs on a given VRF (vni id). | Add IPs to dummy NIC associated to the | Ingress: vxlan + bridge device | Yes | No | No |
|
| VRF | Expose IPs on the routing table of a given VRF | Add routes to the routing table of the | Ingress: vxlan + bridge device | Yes | No | Yes |
|
||||||
| | | VRF device (lo_VNI_ID). | Egress: flow to redirect to VRF device | (Not implemented) | | |
|
| | (vni id), creating L3VNI EVPN functionality. | corresponding VRF (vrf-VNI_ID). | Egress: flow to redirect to VRF device | | | |
|
||||||
+-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+--------------------------+-----------------------+---------------+
|
+-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+--------------------------+-----------------------+---------------+
|
||||||
| Dynamic | Mix of the previous. Depending on annotations it | Mix of the previous three. | Ingress: mix of all the above | Depends on the method | No | No |
|
| Dynamic | Mix of the previous. Depending on annotations it | Mix of the previous three. | Ingress: mix of all the above | Depends on the method | No | No |
|
||||||
| | exposes IPs differently and on different VNIs. | | Egress: mix of all the above | used | | |
|
| | exposes IPs differently and on different VNIs. | | Egress: mix of all the above | used | | |
|
||||||
|
@ -120,6 +120,9 @@ for now you can select:
|
|||||||
- ``underlay``: using kernel routing (what we describe in this document), same
|
- ``underlay``: using kernel routing (what we describe in this document), same
|
||||||
as supported by the driver at :ref:`bgp_driver`.
|
as supported by the driver at :ref:`bgp_driver`.
|
||||||
|
|
||||||
|
- ``vrf``: using kernel routing, similar to the evpn driver, but with some
|
||||||
|
changes, as outlined in :ref:`evpn_wiring`.
|
||||||
|
|
||||||
- ``ovn``: using an extra OVN cluster per node to perform the routing at
|
- ``ovn``: using an extra OVN cluster per node to perform the routing at
|
||||||
OVN/OVS level instead of kernel, enabling datapath acceleration
|
OVN/OVS level instead of kernel, enabling datapath acceleration
|
||||||
(Hardware Offloading and OVS-DPDK). More information about this mechanism
|
(Hardware Offloading and OVS-DPDK). More information about this mechanism
|
||||||
@ -136,8 +139,9 @@ networking accordingly.
|
|||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Linux Kernel Networking is used when the default ``exposing_method``
|
Linux Kernel Networking is used when the default ``exposing_method``
|
||||||
(``underlay``) is used. If ``ovn`` is used instead, OVN routing is
|
(``underlay``) or (``vrf``) is used. If ``ovn`` is used instead, OVN
|
||||||
used instead of Kernel. For more details on this see :ref:`ovn_routing`.
|
routing is used instead of Kernel. For more details on this see
|
||||||
|
:ref:`ovn_routing`.
|
||||||
|
|
||||||
The following events are watched and handled by the BGP watcher:
|
The following events are watched and handled by the BGP watcher:
|
||||||
|
|
||||||
@ -294,6 +298,9 @@ To accomplish the network configuration and advertisement, the driver ensures:
|
|||||||
|
|
||||||
.. include:: ../bgp_advertising.rst
|
.. include:: ../bgp_advertising.rst
|
||||||
|
|
||||||
|
.. _evpn_wiring:
|
||||||
|
.. include:: ../evpn_advertising.rst
|
||||||
|
|
||||||
|
|
||||||
Traffic flow from tenant networks
|
Traffic flow from tenant networks
|
||||||
+++++++++++++++++++++++++++++++++
|
+++++++++++++++++++++++++++++++++
|
||||||
@ -378,6 +385,7 @@ OVN load balancers:
|
|||||||
|
|
||||||
.. include:: ../agent_deployment.rst
|
.. include:: ../agent_deployment.rst
|
||||||
|
|
||||||
|
.. _NB_BGP_driver_limitations:
|
||||||
|
|
||||||
Limitations
|
Limitations
|
||||||
-----------
|
-----------
|
||||||
@ -390,23 +398,25 @@ The following limitations apply:
|
|||||||
- There is no API to decide what to expose, all VMs/LBs on providers or with
|
- There is no API to decide what to expose, all VMs/LBs on providers or with
|
||||||
floating IPs associated with them are exposed. For the VMs in the tenant
|
floating IPs associated with them are exposed. For the VMs in the tenant
|
||||||
networks, use the flag ``address_scopes`` to filter which subnets to expose,
|
networks, use the flag ``address_scopes`` to filter which subnets to expose,
|
||||||
which also prefents having overlapping IPs.
|
which also prevents having overlapping IPs.
|
||||||
|
|
||||||
- In the currently implemented exposing methods (``underlay`` and
|
- In the currently implemented exposing methods (``underlay`` and
|
||||||
``ovn``) there is no support for overlapping CIDRs, so this must be
|
``ovn``) there is no support for overlapping CIDRs, so this must be
|
||||||
avoided, e.g., by using address scopes and subnet pools.
|
avoided, e.g., by using address scopes and subnet pools.
|
||||||
|
|
||||||
- For the default exposing method (``underlay``) the network traffic is steered
|
- For the default exposing method (``underlay``) but also with the ``vrf``
|
||||||
by kernel routing (ip routes and rules), therefore OVS-DPDK, where the kernel
|
exposing method the network traffic is steered by kernel routing (ip
|
||||||
space is skipped, is not supported. With the ``ovn`` exposing method
|
routes and rules), therefore OVS-DPDK, where the kernel space is skipped,
|
||||||
the routing is done at ovn level, so this limitation does not exists.
|
is not supported.
|
||||||
More details in :ref:`ovn_routing`.
|
With the ``ovn`` exposing method the routing is done at ovn level, so this
|
||||||
|
limitation does not exists. More details in :ref:`ovn_routing`.
|
||||||
|
|
||||||
- For the default exposing method (``underlay``) the network traffic is steered
|
- For the default exposing method (``underlay``) but also with the ``vrf``
|
||||||
by kernel routing (ip routes and rules), therefore SRIOV, where the hypervisor
|
exposing method the network traffic is steered by kernel routing (ip
|
||||||
is skipped, is not supported. With the ``ovn`` exposing method
|
routes and rules), therefore SRIOV, where the hypervisor is skipped, is
|
||||||
the routing is done at ovn level, so this limitation does not exists.
|
not supported.
|
||||||
More details in :ref:`ovn_routing`.
|
With the ``ovn`` exposing method the routing is done at ovn level, so this
|
||||||
|
limitation does not exists. More details in :ref:`ovn_routing`.
|
||||||
|
|
||||||
- In OpenStack with OVN networking the N/S traffic to the ovn-octavia VIPs on
|
- In OpenStack with OVN networking the N/S traffic to the ovn-octavia VIPs on
|
||||||
the provider or the FIPs associated with the VIPs on tenant networks needs to
|
the provider or the FIPs associated with the VIPs on tenant networks needs to
|
||||||
|
200
doc/source/contributor/evpn_advertising.rst
Normal file
200
doc/source/contributor/evpn_advertising.rst
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
EVPN Advertisement (expose method ``vrf``)
|
||||||
|
++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
When using expose method ``vrf``, the OVN BGP Agent is in charge of triggering
|
||||||
|
FRR (IP routing protocol suite for Linux which includes protocol daemons for
|
||||||
|
BGP, OSPF, RIP, among others) to advertise/withdraw directly connected and
|
||||||
|
kernel routes via BGP.
|
||||||
|
|
||||||
|
To do that, when the agent starts, it will search for all provider networks
|
||||||
|
and configure them.
|
||||||
|
|
||||||
|
In order to expose a provider network, each provider network must match these
|
||||||
|
criteria:
|
||||||
|
|
||||||
|
- The provider network can be matched to the bridge mappings as defined in the
|
||||||
|
running OpenVSwitch instance (e.g. ``ovn-bridge-mappings="physnet1:br-ex"``)
|
||||||
|
|
||||||
|
- The provider network has been configured by an admin with at least a ``vni``,
|
||||||
|
and the vpn type has been configured too with value ``l3``.
|
||||||
|
|
||||||
|
For example (when using the OVN tools):
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ ovn-nbctl set logical-switch neutron-cd5d6fa7-3ed7-452b-8ce9-1490e2d377c8 external_ids:"neutron_bgpvpn\:type"=l3
|
||||||
|
$ ovn-nbctl set logical-switch neutron-cd5d6fa7-3ed7-452b-8ce9-1490e2d377c8 external_ids:"neutron_bgpvpn\:vni"=100
|
||||||
|
$ ovn-nbctl list logical-switch | less
|
||||||
|
...
|
||||||
|
external_ids : {.. "neutron_bgpvpn:type"=l3, "neutron_bgpvpn:vni"="1001" ..}
|
||||||
|
name : neutron-cd5d6fa7-3ed7-452b-8ce9-1490e2d377c8
|
||||||
|
...
|
||||||
|
|
||||||
|
It is also possible to configure this using the Neutron BGP VPN API.
|
||||||
|
|
||||||
|
Initialization sequence per VRF
|
||||||
|
'''''''''''''''''''''''''''''''
|
||||||
|
|
||||||
|
Once the networks have been initialized, the driver waits for the first ip to be
|
||||||
|
exposed, before actually exposing the VRF on the host.
|
||||||
|
|
||||||
|
Once a VRF is exposed on the host, the following will be done (per VRF):
|
||||||
|
|
||||||
|
1. Create EVPN related devices
|
||||||
|
|
||||||
|
- Create VRF device, using the VNI number as name suffix: vrf-1001
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ 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:: bash
|
||||||
|
|
||||||
|
$ 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:: bash
|
||||||
|
|
||||||
|
$ 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
|
||||||
|
|
||||||
|
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:: jinja
|
||||||
|
|
||||||
|
vrf {{ vrf_name }}
|
||||||
|
vni {{ vni }}
|
||||||
|
exit-vrf
|
||||||
|
router bgp {{ bgp_as }} vrf {{ vrf_name }}
|
||||||
|
bgp router-id {{ bgp_router_id }}
|
||||||
|
address-family ipv4 unicast
|
||||||
|
redistribute connected
|
||||||
|
redistribute kernel
|
||||||
|
exit-address-family
|
||||||
|
|
||||||
|
address-family ipv6 unicast
|
||||||
|
redistribute connected
|
||||||
|
redistribute kernel
|
||||||
|
exit-address-family
|
||||||
|
address-family l2vpn evpn
|
||||||
|
advertise ipv4 unicast
|
||||||
|
advertise ipv6 unicast
|
||||||
|
rd {{ local_ip }}:{{ vni }}
|
||||||
|
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 connect the VRF to the OVS
|
||||||
|
provider bridge:
|
||||||
|
|
||||||
|
- Create a veth device, that will be used for routing between the vrf and
|
||||||
|
OVN, using the uuid of the localnet port in the logical-switch-port table
|
||||||
|
and connect it to ovs (in this example the uuid of the localnet port is
|
||||||
|
``12345678-1234-1234-1234-123456789012``, and the first 11 chars will
|
||||||
|
be used in the interface name):
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ ip link add name vrf12345678-12 type veth peer name ovs12345678-12
|
||||||
|
$ ovs-vsctl add-port br-ex ovs12345678-12
|
||||||
|
$ ip link set up dev ovs12345678-12
|
||||||
|
|
||||||
|
- For EVPN l3 mode (only supported mode currently), it will attach the vrf
|
||||||
|
side to the vrf:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ ip link set vrf12345678-12 master vrf-1001
|
||||||
|
$ ip link set up dev vrf12345678-12
|
||||||
|
|
||||||
|
And it will add routing IPs on the veth interface, so the kernel is able
|
||||||
|
to do L3 routing within the VRF. By default it will add a 169.254.x.x
|
||||||
|
address based on the VNI/VLAN.
|
||||||
|
|
||||||
|
If possible it will use the dhcp options to determine if it can use an
|
||||||
|
actually configured router ip address, in addition to the 169.254.x.x
|
||||||
|
address:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ ip address add 10.0.0.1/32 dev vrf12345678-12 # router option from dhcp opts
|
||||||
|
$ ip address add 169.254.0.123/32 dev vrf12345678-12 # generated 169.254.x.x address for vlan 123
|
||||||
|
$ ip -6 address add fd53:d91e:400:7f17::7b/128 dev vrf12345678-12 # generated ipv6 address for vlan 123
|
||||||
|
|
||||||
|
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:: bash
|
||||||
|
|
||||||
|
$ ovs-ofctl add-flow br-ex cookie=0x3e7,priority=900,ip,in_port=<OVN_PATCH_PORT_ID>,actions=mod_dl_dst:VETH|VLAN_MAC,NORMAL
|
||||||
|
|
||||||
|
5. If ``CONF.anycast_evpn_gateway_mode`` is enabled, it will make sure that the
|
||||||
|
mac address on the vrf12345678-12 interface is equal on all nodes, using the
|
||||||
|
VLAN id and VNI id as an offset while generating a MAC address.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ ip link set address 02:00:03:e7:00:7b dev vrf12345678-12 # generated mac for vni 1001 and vlan 123
|
||||||
|
|
||||||
|
# Replace link local address and update to generated vlan mac (used for ipv6 router advertisements)
|
||||||
|
$ ip -6 address del <some fe80::/10 address> dev vrf12345678-12
|
||||||
|
$ ip -6 address add fe80::200:3e7:65/64 dev vrf12345678-12
|
||||||
|
|
||||||
|
6. If IPv6 subnets are defined (checked in dhcp opts once again), then configure
|
||||||
|
FRR to handle neighbor discovery (and do router advertisements for us)
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
interface {{ vrf_intf }}
|
||||||
|
{% if is_dhcpv6 %}
|
||||||
|
ipv6 nd managed-config-flag
|
||||||
|
{% endif %}
|
||||||
|
{% for server in dns_servers %}
|
||||||
|
ipv6 nd rdnss {{ server }}
|
||||||
|
{% endfor %}
|
||||||
|
ipv6 nd prefix {{ prefix }}
|
||||||
|
no ipv6 nd suppress-ra
|
||||||
|
exit
|
||||||
|
|
||||||
|
7. Then, finally, add the routes to expose to the VRF, since we use full
|
||||||
|
kernel routing in this VRF, we also expose the MAC address that belongs
|
||||||
|
to this route, so we do not rely on ARP proxies in OVN.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ ip route add 10.0.0.5/32 dev vrf12345678-12
|
||||||
|
$ ip route show table 1001 | grep veth
|
||||||
|
local 10.0.0.1 dev vrf12345678-12 proto kernel scope host src 10.0.0.1
|
||||||
|
10.0.0.5 dev vrf12345678-12 scope link
|
||||||
|
local 169.254.0.123 dev vrf12345678-12 proto kernel scope host src 169.254.0.123
|
||||||
|
|
||||||
|
$ ip neigh add 10.0.0.5 dev vrf12345678-12 lladdr fa:16:3e:7d:50:ad nud permanent
|
||||||
|
$ ip neigh show vrf vrf-100 | grep veth
|
||||||
|
10.0.0.5 dev vrf12345678-12 lladdr fa:16:3e:7d:50:ad PERMANENT
|
||||||
|
fe80::f816:3eff:fe7d:50ad dev vrf12345678-12 lladdr fa:16:3e:7d:50:ad STALE
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The VRF is not associated to one OpenStack tenant, but can be mixed with
|
||||||
|
other provider networks too. When using VLAN provider networks, one can
|
||||||
|
connect multiple networks to the same VNI, effectively placing them in the
|
||||||
|
same VRF, routed and handled through kernel and FRR.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
As we also want to be able to expose VM connected to tenant networks
|
||||||
|
(when ``expose_tenant_networks`` or ``expose_ipv6_gua_tenant_networks``
|
||||||
|
configuration options are enabled), there is a need to expose the Neutron
|
||||||
|
router gateway port (cr-lrp on OVN) so that the traffic to VMs in tenant
|
||||||
|
networks is injected into OVN overlay through the node that is hosting
|
||||||
|
that port.
|
8
doc/source/examples/index.rst
Normal file
8
doc/source/examples/index.rst
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
===================
|
||||||
|
Example deployments
|
||||||
|
===================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
nb_evpn_vrf
|
201
doc/source/examples/nb_evpn_vrf.rst
Normal file
201
doc/source/examples/nb_evpn_vrf.rst
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
========================================
|
||||||
|
EVPN L3VNI using OVN NB database
|
||||||
|
========================================
|
||||||
|
|
||||||
|
One example deployment, is to run the OVN BGP Agent and to expose provider
|
||||||
|
networks and tenant networks with pure layer 3 functionality.
|
||||||
|
|
||||||
|
Traffic flow
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The next figure shows the N/S traffic flow through the VRF to the VM,
|
||||||
|
including information the routes in the VRF routing table.
|
||||||
|
|
||||||
|
.. image:: ../../images/evpn-flow-l3vpn.svg
|
||||||
|
:alt: EVPN l3vpn diagram
|
||||||
|
:align: center
|
||||||
|
:width: 100%
|
||||||
|
|
||||||
|
|
||||||
|
On Host-2, the IPs of both the external router gateway port (GW, ``172.16.2.12``),
|
||||||
|
as well as the subnet it exposes (``192.168.0.0/24``) gets added to the
|
||||||
|
routing table of the vrf (``vrf-1001``). Also any instance that is attached
|
||||||
|
directly on a provider network is added to the routing table.
|
||||||
|
|
||||||
|
Then FRR is utilized to expose this vrf through BGP/EVPN. Routes from other nodes
|
||||||
|
are imported and added as an VXLAN route, pointing to the bridge (``br-1001``).
|
||||||
|
|
||||||
|
This allows the external route to reach the internal VM, possibly routed
|
||||||
|
through the host that is hosting the router gateway port.
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
frr-bgpd.conf
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
A typical FRR BGP configuration would look like this (for example for host-1):
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
router bgp 64531
|
||||||
|
bgp router-id 10.100.100.1
|
||||||
|
bgp default l2vpn-evpn
|
||||||
|
|
||||||
|
neighbor rr-peers peer-group
|
||||||
|
neighbor rr-peers remote-as internal
|
||||||
|
neighbor rr-peers bfd
|
||||||
|
|
||||||
|
neighbor upstream-peers peer-group
|
||||||
|
neighbor upstream-peers remote-as internal
|
||||||
|
neighbor upstream-peers bfd
|
||||||
|
|
||||||
|
! Upstream routers (these will most likely expose the default outbound route)
|
||||||
|
neighbor 10.100.250.3 peer-group upstream-peers
|
||||||
|
neighbor 10.100.250.4 peer-group upstream-peers
|
||||||
|
|
||||||
|
! Route reflector peers (used for distributing routes between compute nodes)
|
||||||
|
neighbor 10.100.50.66 peer-group rr-peers
|
||||||
|
neighbor 10.100.51.66 peer-group rr-peers
|
||||||
|
neighbor 10.100.52.66 peer-group rr-peers
|
||||||
|
|
||||||
|
address-family l2vpn evpn
|
||||||
|
neighbor rr-peers soft-reconfiguration inbound
|
||||||
|
neighbor upstream-peers soft-reconfiguration inbound
|
||||||
|
advertise-all-vni
|
||||||
|
exit-address-family
|
||||||
|
exit
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In our best practice we use FRR instances on central nodes to act as route
|
||||||
|
reflector. How to scale your BGP network and what practices you might use
|
||||||
|
is beyond the perview of this example document.
|
||||||
|
|
||||||
|
|
||||||
|
ovn-bgp-agent.conf
|
||||||
|
------------------
|
||||||
|
To run OVN BGP Agent with NB driver and EVPN L3 mode, the following configuration is recommended:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[DEFAULT]
|
||||||
|
# Time (seconds) between re-sync actions.
|
||||||
|
reconcile_interval = 600
|
||||||
|
|
||||||
|
# Time (seconds) between re-sync actions to ensure frr configuration is correct.
|
||||||
|
# NOTE: This function does not do anything in our setup, so this high interval is fine.
|
||||||
|
frr_reconcile_interval = 86400
|
||||||
|
|
||||||
|
# Expose VM IPs on tenant networks.
|
||||||
|
expose_tenant_networks = True
|
||||||
|
|
||||||
|
# The NB driver is capable of advertising the tenant networks either per
|
||||||
|
# host or per subnet. So either per /32 or /128 or per subnet like /24.
|
||||||
|
# Choose "host" as value for this option to advertise per host or choose
|
||||||
|
# "subnet" to announce per subnet prefix.
|
||||||
|
advertisement_method_tenant_networks = subnet
|
||||||
|
|
||||||
|
# Require SNAT on the router port to be disabled before exposing the tenant
|
||||||
|
# networks. Otherwise the exposed tenant networks will be reachable from the
|
||||||
|
# outside, but the connections set up from within the tenant vm will always
|
||||||
|
# be SNAT-ed by the router, thus be the router ip. When SNAT is disabled,
|
||||||
|
# OVN will do pure routing without SNAT
|
||||||
|
require_snat_disabled_for_tenant_networks = True
|
||||||
|
|
||||||
|
# Expose only VM IPv6 IPs on tenant networks if they are GUA.
|
||||||
|
# expose_ipv6_gua_tenant_networks = False
|
||||||
|
|
||||||
|
# Driver to be used.
|
||||||
|
driver = 'nb_ovn_bgp_driver'
|
||||||
|
|
||||||
|
# The connection string for the native OVSDB backend.
|
||||||
|
ovsdb_connection = tcp:127.0.0.1:6640
|
||||||
|
|
||||||
|
# Timeout in seconds for the OVSDB connection transaction.
|
||||||
|
# ovsdb_connection_timeout = 180
|
||||||
|
|
||||||
|
# AS number to be used by the Agent when running in BGP mode.
|
||||||
|
bgp_AS = < CONFIGURE YOUR AS HERE >
|
||||||
|
|
||||||
|
# Router ID to be used by the Agent when running in BGP mode.
|
||||||
|
bgp_router_id = < CONFIGURE YOUR ROUTER ID/IP HERE >
|
||||||
|
|
||||||
|
# IP address of local EVPN VXLAN (tunnel) endpoint.
|
||||||
|
evpn_local_ip = < CONFIGURE YOUR HOST'S EVPN VXLAN IP HERE>
|
||||||
|
|
||||||
|
# If enabled, all routes are removed from the VRF table at startup.
|
||||||
|
clear_vrf_routes_on_startup = False
|
||||||
|
|
||||||
|
# Allows to filter on the address scope (optional, comma separated list of uuids)
|
||||||
|
# address_scopes = 11111111-1111-1111-1111-111111111111,22222222-2222-2222-2222-222222222222
|
||||||
|
|
||||||
|
# The exposing mechanism to be used.
|
||||||
|
exposing_method = 'vrf'
|
||||||
|
|
||||||
|
# When using exposing_method vrf and l3 mode on networks, then one can create
|
||||||
|
# anycast mac addresses, basically using the same mac address on all nodes for
|
||||||
|
# use with routing.
|
||||||
|
anycast_evpn_gateway_mode = True
|
||||||
|
|
||||||
|
[ovn]
|
||||||
|
# The connection string for the OVN_Northbound OVSDB.
|
||||||
|
# Use tcp:IP:PORT for TCP connection.
|
||||||
|
# Use unix:FILE for unix domain socket connection.
|
||||||
|
ovn_nb_connection = < CONNECTION STRING TO NB OVN DB>
|
||||||
|
|
||||||
|
Configure provider networks
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
This section assumes, that you've already configured OVN, and applied the correct bridge-mappings
|
||||||
|
on the hosts themselves, see `Neutron documentation regarding provider networks. <https://docs.openstack.org/neutron/latest/admin/ovn/refarch/provider-networks.html>`_
|
||||||
|
|
||||||
|
First, create your provider network through neutron
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
openstack network create my_network \
|
||||||
|
--provider-network-type vlan \
|
||||||
|
--provider-physical-network physnet1 \
|
||||||
|
--provider-segment 123 \
|
||||||
|
--mtu 1500 \
|
||||||
|
--external \
|
||||||
|
--default
|
||||||
|
|
||||||
|
Then, configure your provider networks through either Neutron BGPVPN API or
|
||||||
|
with ovn commandline:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
ovn-nbctl set logical-switch < UUID > external_ids:"neutron_bgpvpn\:type"="l3"
|
||||||
|
ovn-nbctl set logical-switch < UUID > external_ids:"neutron_bgpvpn\:vni"="1001" # or any other number
|
||||||
|
|
||||||
|
Now use this network to attach routers on it (so update router:external on
|
||||||
|
the provider network) or share your network among tenants (shared = True)
|
||||||
|
|
||||||
|
And create some routers, or add some instances on the provider network, so a
|
||||||
|
host will start exposing the networks and/or ips.
|
||||||
|
|
||||||
|
Current known limitations
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- Only one Flat provider network can be exposed per vni. Recommendation is
|
||||||
|
to use VLAN provider networks.
|
||||||
|
|
||||||
|
- Do not use the same VLAN id twice in the same VNI. A provider network with
|
||||||
|
type flat is considered vlan 0.
|
||||||
|
|
||||||
|
- It is not possible to have a tenant network (which is routed through a
|
||||||
|
gateway) in separate VRF's, make sure to use address scopes and subnet pools
|
||||||
|
to prevent ip overlaps, if you are planning to expose tenant networks.
|
||||||
|
|
||||||
|
- Provider networks of type ``flat`` is supported, but is limited (because of
|
||||||
|
how ``flat`` networks operate) to one provider network per bridge mapping.
|
||||||
|
It is recommended to use provider networks of type ``vlan``. That way it is
|
||||||
|
also easier to create multiple provider networks, without having to create
|
||||||
|
new bridgemappings for every provider network.
|
||||||
|
|
||||||
|
Every provider network can be assigned a separate VNI, so IP overlap is not
|
||||||
|
an issue between provider networks, as long as separate VNI's are used for
|
||||||
|
those provider networks.
|
||||||
|
|
||||||
|
See other known limitations at the NB BGP driver :ref:`NB_BGP_driver_limitations`
|
@ -14,6 +14,7 @@ Contents:
|
|||||||
|
|
||||||
readme
|
readme
|
||||||
contributor/index
|
contributor/index
|
||||||
|
examples/index
|
||||||
bgp_supportability_matrix
|
bgp_supportability_matrix
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
@ -56,6 +56,9 @@ FRR_SOCKET_PATH = "/run/frr/"
|
|||||||
IP_VERSION_6 = 6
|
IP_VERSION_6 = 6
|
||||||
IP_VERSION_4 = 4
|
IP_VERSION_4 = 4
|
||||||
|
|
||||||
|
# initial mac address to generate anycast addresses for (vni and vlan will
|
||||||
|
# be added to this value)
|
||||||
|
MAC_LLADDR_OFFSET = "02:00:00:00:00:00"
|
||||||
ARP_IPV4_PREFIX = "169.254."
|
ARP_IPV4_PREFIX = "169.254."
|
||||||
NDP_IPV6_PREFIX = "fd53:d91e:400:7f17::"
|
NDP_IPV6_PREFIX = "fd53:d91e:400:7f17::"
|
||||||
|
|
||||||
@ -64,8 +67,20 @@ IPV4_OCTET_RANGE = 256
|
|||||||
BGP_MODE = 'BGP'
|
BGP_MODE = 'BGP'
|
||||||
EVPN_MODE = 'EVPN'
|
EVPN_MODE = 'EVPN'
|
||||||
|
|
||||||
|
# NOTE(mnederlof, ltomasbo): for lack of better variables we are using the
|
||||||
|
# neutron_bgpvpn namespace for now. If in the future another API endpoint
|
||||||
|
# is created, we should adapt or maybe move them to another location that
|
||||||
|
# makes sense at that time.
|
||||||
OVN_EVPN_VNI_EXT_ID_KEY = 'neutron_bgpvpn:vni'
|
OVN_EVPN_VNI_EXT_ID_KEY = 'neutron_bgpvpn:vni'
|
||||||
OVN_EVPN_AS_EXT_ID_KEY = 'neutron_bgpvpn:as'
|
OVN_EVPN_AS_EXT_ID_KEY = 'neutron_bgpvpn:as'
|
||||||
|
OVN_EVPN_TYPE_EXT_ID_KEY = 'neutron_bgpvpn:type'
|
||||||
|
OVN_EVPN_ROUTE_TARGETS_EXT_ID_KEY = 'neutron_bgpvpn:rt'
|
||||||
|
OVN_EVPN_ROUTE_DISTINGUISHERS_EXT_ID_KEY = 'neutron_bgpvpn:rd'
|
||||||
|
OVN_EVPN_IMPORT_TARGETS_EXT_ID_KEY = 'neutron_bgpvpn:it'
|
||||||
|
OVN_EVPN_EXPORT_TARGETS_EXT_ID_KEY = 'neutron_bgpvpn:et'
|
||||||
|
|
||||||
|
OVN_EVPN_TYPE_L2 = 'l2'
|
||||||
|
OVN_EVPN_TYPE_L3 = 'l3'
|
||||||
OVN_EVPN_VRF_PREFIX = "vrf-"
|
OVN_EVPN_VRF_PREFIX = "vrf-"
|
||||||
OVN_EVPN_BRIDGE_PREFIX = "br-"
|
OVN_EVPN_BRIDGE_PREFIX = "br-"
|
||||||
OVN_EVPN_VXLAN_PREFIX = "vxlan-"
|
OVN_EVPN_VXLAN_PREFIX = "vxlan-"
|
||||||
@ -77,8 +92,20 @@ OVN_INTEGRATION_BRIDGE = 'br-int'
|
|||||||
OVN_LRP_PORT_NAME_PREFIX = 'lrp-'
|
OVN_LRP_PORT_NAME_PREFIX = 'lrp-'
|
||||||
OVN_CRLRP_PORT_NAME_PREFIX = 'cr-lrp-'
|
OVN_CRLRP_PORT_NAME_PREFIX = 'cr-lrp-'
|
||||||
|
|
||||||
|
# the new prefix will get the first 11 chars of the localnet port prefixed,
|
||||||
|
# neutron-tap style
|
||||||
|
OVN_EVPN_VETH_VRF_UUID_PREFIX = "vrf"
|
||||||
|
OVN_EVPN_VETH_OVS_UUID_PREFIX = "ovs"
|
||||||
|
|
||||||
OVS_PATCH_PROVNET_PORT_PREFIX = 'patch-provnet-'
|
OVS_PATCH_PROVNET_PORT_PREFIX = 'patch-provnet-'
|
||||||
|
|
||||||
|
EVPN_EXT_ID_MAPPING = {
|
||||||
|
'route_targets': OVN_EVPN_ROUTE_TARGETS_EXT_ID_KEY,
|
||||||
|
'route_distinguishers': OVN_EVPN_ROUTE_DISTINGUISHERS_EXT_ID_KEY,
|
||||||
|
'export_targets': OVN_EVPN_EXPORT_TARGETS_EXT_ID_KEY,
|
||||||
|
'import_targets': OVN_EVPN_IMPORT_TARGETS_EXT_ID_KEY,
|
||||||
|
}
|
||||||
|
|
||||||
LINK_UP = "up"
|
LINK_UP = "up"
|
||||||
LINK_DOWN = "down"
|
LINK_DOWN = "down"
|
||||||
|
|
||||||
@ -116,6 +143,7 @@ POLICY_ACTION_TYPES = (POLICY_ACTION_REROUTE)
|
|||||||
LR_POLICY_PRIORITY_MAX = 32767
|
LR_POLICY_PRIORITY_MAX = 32767
|
||||||
|
|
||||||
ROUTE_DISCARD = 'discard'
|
ROUTE_DISCARD = 'discard'
|
||||||
|
ROUTE_TYPE_UNICAST = 1
|
||||||
|
|
||||||
# Family constants
|
# Family constants
|
||||||
AF_INET = socket.AF_INET
|
AF_INET = socket.AF_INET
|
||||||
@ -125,3 +153,5 @@ AF_INET6 = socket.AF_INET6
|
|||||||
ROUTING_TABLES_FILE = '/etc/iproute2/rt_tables'
|
ROUTING_TABLES_FILE = '/etc/iproute2/rt_tables'
|
||||||
ROUTING_TABLE_MIN = 1
|
ROUTING_TABLE_MIN = 1
|
||||||
ROUTING_TABLE_MAX = 252
|
ROUTING_TABLE_MAX = 252
|
||||||
|
|
||||||
|
VLAN_ID_UNTAGGED = 0
|
||||||
|
@ -38,7 +38,7 @@ LOG = logging.getLogger(__name__)
|
|||||||
# logging.basicConfig(level=logging.DEBUG)
|
# logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
OVN_TABLES = ['Logical_Switch_Port', 'NAT', 'Logical_Switch', 'Logical_Router',
|
OVN_TABLES = ['Logical_Switch_Port', 'NAT', 'Logical_Switch', 'Logical_Router',
|
||||||
'Logical_Router_Port', 'Load_Balancer']
|
'Logical_Router_Port', 'Load_Balancer', 'DHCP_Options']
|
||||||
LOCAL_CLUSTER_OVN_TABLES = ['Logical_Switch', 'Logical_Switch_Port',
|
LOCAL_CLUSTER_OVN_TABLES = ['Logical_Switch', 'Logical_Switch_Port',
|
||||||
'Logical_Router', 'Logical_Router_Port',
|
'Logical_Router', 'Logical_Router_Port',
|
||||||
'Logical_Router_Policy',
|
'Logical_Router_Policy',
|
||||||
@ -148,11 +148,18 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
|
|||||||
watcher.LogicalSwitchPortProviderDeleteEvent(self),
|
watcher.LogicalSwitchPortProviderDeleteEvent(self),
|
||||||
watcher.LogicalSwitchPortFIPCreateEvent(self),
|
watcher.LogicalSwitchPortFIPCreateEvent(self),
|
||||||
watcher.LogicalSwitchPortFIPDeleteEvent(self),
|
watcher.LogicalSwitchPortFIPDeleteEvent(self),
|
||||||
watcher.LocalnetCreateDeleteEvent(self),
|
|
||||||
watcher.OVNLBCreateEvent(self),
|
watcher.OVNLBCreateEvent(self),
|
||||||
watcher.OVNLBDeleteEvent(self),
|
watcher.OVNLBDeleteEvent(self),
|
||||||
watcher.OVNPFCreateEvent(self),
|
watcher.OVNPFCreateEvent(self),
|
||||||
watcher.OVNPFDeleteEvent(self)}
|
watcher.OVNPFDeleteEvent(self)}
|
||||||
|
|
||||||
|
if CONF.exposing_method == constants.EXPOSE_METHOD_VRF:
|
||||||
|
# For vrf we require more information on the logical_switch
|
||||||
|
# before performing a sync.
|
||||||
|
events.add(watcher.LogicalSwitchUpdateEvent(self))
|
||||||
|
else:
|
||||||
|
events.add(watcher.LocalnetCreateDeleteEvent(self))
|
||||||
|
|
||||||
if self._expose_tenant_networks:
|
if self._expose_tenant_networks:
|
||||||
events.update({watcher.ChassisRedirectCreateEvent(self),
|
events.update({watcher.ChassisRedirectCreateEvent(self),
|
||||||
watcher.ChassisRedirectDeleteEvent(self),
|
watcher.ChassisRedirectDeleteEvent(self),
|
||||||
@ -279,6 +286,8 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
|
|||||||
self._exposed_ips.setdefault(logical_switch, {}).update(
|
self._exposed_ips.setdefault(logical_switch, {}).update(
|
||||||
{ip: {'bridge_device': bridge_device,
|
{ip: {'bridge_device': bridge_device,
|
||||||
'bridge_vlan': bridge_vlan}})
|
'bridge_vlan': bridge_vlan}})
|
||||||
|
else:
|
||||||
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception("Unexpected exception while wiring provider port: "
|
LOG.exception("Unexpected exception while wiring provider port: "
|
||||||
"%s", e)
|
"%s", e)
|
||||||
@ -622,6 +631,21 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
|
|||||||
def withdraw_remote_ip(self, ips, ips_info):
|
def withdraw_remote_ip(self, ips, ips_info):
|
||||||
self._withdraw_remote_ip(ips, ips_info)
|
self._withdraw_remote_ip(ips, ips_info)
|
||||||
|
|
||||||
|
def _get_exposed_ip(self, exposed_ip):
|
||||||
|
for ls, ip_info in self._exposed_ips.items():
|
||||||
|
if exposed_ip in ip_info:
|
||||||
|
return ls, ip_info[exposed_ip]
|
||||||
|
|
||||||
|
def _get_router_port_info_for_ls(self, ls):
|
||||||
|
# LOG.debug('Searching router port info for ls %s', ls)
|
||||||
|
lrps = list(self.ovn_local_lrps.get(ls, []))
|
||||||
|
if not lrps:
|
||||||
|
LOG.debug('Could not find router gateway port for ls %s', ls)
|
||||||
|
return
|
||||||
|
|
||||||
|
_, cr_lrp_info = self._get_exposed_ip(lrps[0])
|
||||||
|
return cr_lrp_info
|
||||||
|
|
||||||
def _expose_remote_ip(self, ips, ips_info):
|
def _expose_remote_ip(self, ips, ips_info):
|
||||||
if (CONF.advertisement_method_tenant_networks ==
|
if (CONF.advertisement_method_tenant_networks ==
|
||||||
constants.ADVERTISEMENT_METHOD_SUBNET):
|
constants.ADVERTISEMENT_METHOD_SUBNET):
|
||||||
@ -638,10 +662,23 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
|
|||||||
|
|
||||||
LOG.debug("Adding BGP route for tenant IP(s) %s on chassis %s",
|
LOG.debug("Adding BGP route for tenant IP(s) %s on chassis %s",
|
||||||
ips_to_expose, self.chassis)
|
ips_to_expose, self.chassis)
|
||||||
bgp_utils.announce_ips(ips_to_expose)
|
if CONF.exposing_method == constants.EXPOSE_METHOD_VRF:
|
||||||
|
cr_lrp_info = self._get_router_port_info_for_ls(
|
||||||
|
ips_info['logical_switch'])
|
||||||
|
if not cr_lrp_info:
|
||||||
|
LOG.debug('Unable to export routed ips %s: could not find '
|
||||||
|
'router gateway port for ls %s',
|
||||||
|
ips, ips_info['logical_switch'])
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add the bridge_vlan, bridge_device and via to the ips_info
|
||||||
|
ips_info.update(cr_lrp_info)
|
||||||
|
|
||||||
|
bgp_utils.announce_ips(ips_to_expose, ips_info=ips_info)
|
||||||
for ip in ips_to_expose:
|
for ip in ips_to_expose:
|
||||||
self._exposed_ips.setdefault(
|
self._exposed_ips.setdefault(
|
||||||
ips_info['logical_switch'], {}).update({ip: {}})
|
ips_info['logical_switch'], {}).setdefault(ip, {})
|
||||||
|
|
||||||
LOG.debug("Added BGP route for tenant IP(s) %s on chassis %s",
|
LOG.debug("Added BGP route for tenant IP(s) %s on chassis %s",
|
||||||
ips_to_expose, self.chassis)
|
ips_to_expose, self.chassis)
|
||||||
|
|
||||||
@ -660,12 +697,22 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
|
|||||||
|
|
||||||
LOG.debug("Deleting BGP route for tenant IP(s) %s on chassis %s",
|
LOG.debug("Deleting BGP route for tenant IP(s) %s on chassis %s",
|
||||||
ips_to_withdraw, self.chassis)
|
ips_to_withdraw, self.chassis)
|
||||||
bgp_utils.withdraw_ips(ips_to_withdraw)
|
if CONF.exposing_method == constants.EXPOSE_METHOD_VRF:
|
||||||
|
cr_lrp_info = self._get_router_port_info_for_ls(
|
||||||
|
ips_info['logical_switch'])
|
||||||
|
if not cr_lrp_info:
|
||||||
|
LOG.debug('Unable to withdraw routed ips %s: could not find '
|
||||||
|
'router gateway port for ls %s',
|
||||||
|
ips, ips_info['logical_switch'])
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add the bridge_vlan, bridge_device and via to the ips_info
|
||||||
|
ips_info.update(cr_lrp_info)
|
||||||
|
|
||||||
|
bgp_utils.withdraw_ips(ips_to_withdraw, ips_info=ips_info)
|
||||||
for ip in ips_to_withdraw:
|
for ip in ips_to_withdraw:
|
||||||
if self._exposed_ips.get(
|
self._exposed_ips.get(ips_info['logical_switch'], {}).pop(ip, None)
|
||||||
ips_info['logical_switch'], {}).get(ip):
|
|
||||||
self._exposed_ips[
|
|
||||||
ips_info['logical_switch']].pop(ip)
|
|
||||||
LOG.debug("Deleted BGP route for tenant IP(s) %s on chassis %s",
|
LOG.debug("Deleted BGP route for tenant IP(s) %s on chassis %s",
|
||||||
ips_to_withdraw, self.chassis)
|
ips_to_withdraw, self.chassis)
|
||||||
|
|
||||||
@ -805,10 +852,18 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
|
|||||||
if (CONF.advertisement_method_tenant_networks ==
|
if (CONF.advertisement_method_tenant_networks ==
|
||||||
constants.ADVERTISEMENT_METHOD_SUBNET):
|
constants.ADVERTISEMENT_METHOD_SUBNET):
|
||||||
# Fix ips to be the network address, instead of the lrp address
|
# Fix ips to be the network address, instead of the lrp address
|
||||||
# so the cleanup will not remove them, since they match what's
|
# so we can advertise the entire subnet. This way a cleanup of
|
||||||
# in the kernel
|
# EVPN would not remove the incorrect entry as well.
|
||||||
ips = driver_utils.get_prefixes_from_ips(ips)
|
ips = driver_utils.get_prefixes_from_ips(ips)
|
||||||
|
|
||||||
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_VRF:
|
||||||
|
# For evpn, we work with routes and since we are not exposing per
|
||||||
|
# subnet, we need to remove the subnetmask part, so the ips are
|
||||||
|
# exposed as a /32 or /128. Otherwise the cleanup would remove
|
||||||
|
# the exposed route again, since 10.0.0.1/24 (a router ip) would
|
||||||
|
# not match the kernel route of 10.0.0.0/24.
|
||||||
|
ips = [ip.split('/')[0] for ip in ips]
|
||||||
|
|
||||||
ips_to_process = []
|
ips_to_process = []
|
||||||
for ip in ips:
|
for ip in ips:
|
||||||
if not CONF.expose_tenant_networks:
|
if not CONF.expose_tenant_networks:
|
||||||
@ -845,10 +900,11 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
|
|||||||
self._exposed_ips.setdefault(logical_switch, {}).update(
|
self._exposed_ips.setdefault(logical_switch, {}).update(
|
||||||
{ip: {
|
{ip: {
|
||||||
'bridge_device': cr_lrp_info.get('bridge_device'),
|
'bridge_device': cr_lrp_info.get('bridge_device'),
|
||||||
'bridge_vlan': cr_lrp_info.get('bridge_vlan')}})
|
'bridge_vlan': cr_lrp_info.get('bridge_vlan'),
|
||||||
|
'via': cr_lrp_info.get('ips')}})
|
||||||
|
|
||||||
self.ovn_local_lrps.setdefault(
|
self.ovn_local_lrps.setdefault(
|
||||||
subnet_info['network'], []).append(ip)
|
subnet_info['network'], set()).add(ip)
|
||||||
else:
|
else:
|
||||||
error_msg = ("Something happen while exposing the subnet"
|
error_msg = ("Something happen while exposing the subnet"
|
||||||
"and they have not been properly exposed")
|
"and they have not been properly exposed")
|
||||||
@ -875,10 +931,15 @@ class NBOVNBGPDriver(driver_api.AgentDriverBase):
|
|||||||
if (CONF.advertisement_method_tenant_networks ==
|
if (CONF.advertisement_method_tenant_networks ==
|
||||||
constants.ADVERTISEMENT_METHOD_SUBNET):
|
constants.ADVERTISEMENT_METHOD_SUBNET):
|
||||||
# Fix ips to be the network address, instead of the lrp address
|
# Fix ips to be the network address, instead of the lrp address
|
||||||
# so the cleanup will not remove them, since they match what's
|
# so we can withdraw the corrent subnet entry.
|
||||||
# in the kernel
|
|
||||||
ips = driver_utils.get_prefixes_from_ips(ips)
|
ips = driver_utils.get_prefixes_from_ips(ips)
|
||||||
|
|
||||||
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_VRF:
|
||||||
|
# For evpn, we work with routes and since we are not exposing per
|
||||||
|
# subnet, we need to remove the subnetmask part, so the ips are
|
||||||
|
# withdrawn as a /32 or /128.
|
||||||
|
ips = [ip.split('/')[0] for ip in ips]
|
||||||
|
|
||||||
ips_to_process = []
|
ips_to_process = []
|
||||||
for ip in ips:
|
for ip in ips:
|
||||||
if (not CONF.expose_tenant_networks and
|
if (not CONF.expose_tenant_networks and
|
||||||
|
@ -16,6 +16,8 @@ from oslo_config import cfg
|
|||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
|
||||||
from ovn_bgp_agent import constants
|
from ovn_bgp_agent import constants
|
||||||
|
from ovn_bgp_agent.drivers.openstack.utils import driver_utils
|
||||||
|
from ovn_bgp_agent.drivers.openstack.utils import evpn
|
||||||
from ovn_bgp_agent.drivers.openstack.utils import frr
|
from ovn_bgp_agent.drivers.openstack.utils import frr
|
||||||
from ovn_bgp_agent.utils import linux_net
|
from ovn_bgp_agent.utils import linux_net
|
||||||
|
|
||||||
@ -24,11 +26,44 @@ CONF = cfg.CONF
|
|||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def announce_ips(port_ips):
|
def announce_ips(port_ips, ips_info=None):
|
||||||
|
if CONF.exposing_method in [constants.EXPOSE_METHOD_VRF]:
|
||||||
|
if ips_info is None or ips_info.get('bridge_device', None) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Lookup the EVPN vlan dev
|
||||||
|
vlan_dev = evpn.lookup_vlan(ips_info['bridge_device'],
|
||||||
|
ips_info['bridge_vlan'])
|
||||||
|
|
||||||
|
# Split the via ip's per ip version
|
||||||
|
via = driver_utils.ips_per_version(ips_info.get('via', []))
|
||||||
|
|
||||||
|
for ip in port_ips:
|
||||||
|
# Add route for each ip in routing table.
|
||||||
|
if via:
|
||||||
|
ver = linux_net.get_ip_version(ip)
|
||||||
|
vlan_dev.add_route(None, ip, None, via=via.get(ver))
|
||||||
|
else:
|
||||||
|
vlan_dev.add_route(None, ip, ips_info['mac'])
|
||||||
|
return
|
||||||
|
|
||||||
linux_net.add_ips_to_dev(CONF.bgp_nic, port_ips)
|
linux_net.add_ips_to_dev(CONF.bgp_nic, port_ips)
|
||||||
|
|
||||||
|
|
||||||
def withdraw_ips(port_ips):
|
def withdraw_ips(port_ips, ips_info=None):
|
||||||
|
if CONF.exposing_method in [constants.EXPOSE_METHOD_VRF]:
|
||||||
|
if ips_info is None or ips_info.get('bridge_device', None) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Lookup the EVPN vlan dev
|
||||||
|
vlan_dev = evpn.lookup_vlan(ips_info['bridge_device'],
|
||||||
|
ips_info['bridge_vlan'])
|
||||||
|
|
||||||
|
for ip in port_ips:
|
||||||
|
# Add route for each ip in routing table.
|
||||||
|
vlan_dev.del_route(None, ip)
|
||||||
|
return
|
||||||
|
|
||||||
linux_net.del_ips_from_dev(CONF.bgp_nic, port_ips)
|
linux_net.del_ips_from_dev(CONF.bgp_nic, port_ips)
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,6 +85,28 @@ def is_pf_lb(lb):
|
|||||||
return check_name_prefix(lb, constants.OVN_LB_PF_NAME_PREFIX)
|
return check_name_prefix(lb, constants.OVN_LB_PF_NAME_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
|
def ips_per_version(ips: 'list[str]') -> 'dict[int, str]':
|
||||||
|
'''Separate list of ips into ip versions.
|
||||||
|
|
||||||
|
For example, this list ['10.0.0.1/32', 'fe80::1/128'] will be converted
|
||||||
|
to dictionary {
|
||||||
|
4: '10.0.0.1',
|
||||||
|
6: 'fe80::1',
|
||||||
|
}
|
||||||
|
|
||||||
|
If there are more than 1 ip for the same ip version, it will overwrite
|
||||||
|
the previous ip for that ip version.
|
||||||
|
'''
|
||||||
|
ip_list = {constants.IP_VERSION_4: None,
|
||||||
|
constants.IP_VERSION_6: None}
|
||||||
|
|
||||||
|
for ip in ips:
|
||||||
|
ver = linux_net.get_ip_version(ip)
|
||||||
|
ip_list[ver] = ipaddress.ip_address(ip.split('/')[0]).compressed
|
||||||
|
|
||||||
|
return ip_list
|
||||||
|
|
||||||
|
|
||||||
def get_prefixes_from_ips(ips: 'list[str]') -> 'list[str]':
|
def get_prefixes_from_ips(ips: 'list[str]') -> 'list[str]':
|
||||||
'''Return the network address for any given ip (with mask)
|
'''Return the network address for any given ip (with mask)
|
||||||
|
|
||||||
@ -106,3 +128,30 @@ def remove_port_from_ip(ip_address):
|
|||||||
if ip_address[last_colon_index + 1:].isdigit():
|
if ip_address[last_colon_index + 1:].isdigit():
|
||||||
return ip_address[:last_colon_index]
|
return ip_address[:last_colon_index]
|
||||||
return ip_address
|
return ip_address
|
||||||
|
|
||||||
|
|
||||||
|
def get_port_vlan(port):
|
||||||
|
'''Will return the tag of the given logical switch port row as string
|
||||||
|
|
||||||
|
If the vlan tag is not configured, it will return the value of
|
||||||
|
constants.VLAN_ID_UNTAGGED
|
||||||
|
'''
|
||||||
|
if port.tag:
|
||||||
|
return str(port.tag[0])
|
||||||
|
return str(constants.VLAN_ID_UNTAGGED)
|
||||||
|
|
||||||
|
|
||||||
|
def get_port_vrf_settings(port):
|
||||||
|
"""Create a comparable object with the settings of the vrf
|
||||||
|
|
||||||
|
Returns None if the settings are not found in the port, otherwise it
|
||||||
|
will return a string value. Either an empty string or a concatenation
|
||||||
|
of the type and vni, e.g. l3::1001
|
||||||
|
"""
|
||||||
|
if hasattr(port, 'external_ids'):
|
||||||
|
try:
|
||||||
|
return "%s::%s" % (
|
||||||
|
port.external_ids[constants.OVN_EVPN_TYPE_EXT_ID_KEY],
|
||||||
|
port.external_ids[constants.OVN_EVPN_VNI_EXT_ID_KEY])
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return ''
|
||||||
|
541
ovn_bgp_agent/drivers/openstack/utils/evpn.py
Normal file
541
ovn_bgp_agent/drivers/openstack/utils/evpn.py
Normal file
@ -0,0 +1,541 @@
|
|||||||
|
# Copyright 2024 team.blue/nl
|
||||||
|
#
|
||||||
|
# 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 netaddr
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from ovn_bgp_agent import constants
|
||||||
|
from ovn_bgp_agent.drivers.openstack.utils import driver_utils
|
||||||
|
from ovn_bgp_agent.drivers.openstack.utils import frr
|
||||||
|
from ovn_bgp_agent.drivers.openstack.utils import ovs
|
||||||
|
from ovn_bgp_agent import exceptions
|
||||||
|
from ovn_bgp_agent.utils import linux_net
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# dictionary to hold all evpn bridge classes
|
||||||
|
local_bridges: 'dict[str, EvpnBridge]' = {}
|
||||||
|
|
||||||
|
# dictionary to hold all vlandev mappings, based on port uuid
|
||||||
|
local_vlandevs: 'dict[str, VlanDev]' = {}
|
||||||
|
|
||||||
|
|
||||||
|
class EvpnBridge:
|
||||||
|
def __init__(self, ovs_bridge: str, vni: int, evpn_opts: dict,
|
||||||
|
mode=constants.OVN_EVPN_TYPE_L3, ovs_flows: dict = None):
|
||||||
|
|
||||||
|
if not CONF.evpn_local_ip:
|
||||||
|
LOG.error("EVPN device must have an IP associated for the "
|
||||||
|
"VXLAN local ip")
|
||||||
|
raise exceptions.ConfOptionRequired(option='evpn_local_ip')
|
||||||
|
|
||||||
|
self.ovs_bridge = ovs_bridge
|
||||||
|
self.vni = vni
|
||||||
|
self.mode = mode
|
||||||
|
self.ovs_flows = ovs_flows or {}
|
||||||
|
|
||||||
|
self.vrf_name = '%s%s' % (constants.OVN_EVPN_VRF_PREFIX, vni)
|
||||||
|
self.bridge_name = '%s%s' % (constants.OVN_EVPN_BRIDGE_PREFIX, vni)
|
||||||
|
self.vxlan_name = '%s%s' % (constants.OVN_EVPN_VXLAN_PREFIX, vni)
|
||||||
|
self.local_ip = CONF.evpn_local_ip
|
||||||
|
self.evpn_opts = dict(
|
||||||
|
vni=self.vni,
|
||||||
|
local_ip=self.local_ip,
|
||||||
|
vrf_name=self.vrf_name,
|
||||||
|
redistribute=['connected', 'kernel'],
|
||||||
|
)
|
||||||
|
self.evpn_opts.update(evpn_opts)
|
||||||
|
|
||||||
|
self.vlans: dict[str, 'VlanDev'] = {}
|
||||||
|
self._setup_done = False
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
if self._setup_done:
|
||||||
|
return
|
||||||
|
|
||||||
|
LOG.debug('Creating bridge %s', self.bridge_name)
|
||||||
|
linux_net.ensure_bridge(self.bridge_name)
|
||||||
|
|
||||||
|
LOG.debug('Creating vxlan interface %s for vni %s with local ip %s',
|
||||||
|
self.vxlan_name, self.vni, self.local_ip)
|
||||||
|
linux_net.ensure_vxlan(self.vxlan_name, self.vni, self.local_ip,
|
||||||
|
CONF.evpn_udp_dstport)
|
||||||
|
|
||||||
|
LOG.debug('Connect vxlan interface %s to bridge %s',
|
||||||
|
self.vxlan_name, self.bridge_name)
|
||||||
|
linux_net.set_master_for_device(self.vxlan_name, self.bridge_name)
|
||||||
|
|
||||||
|
LOG.debug('Disable learning for vxlan device %s', self.vxlan_name)
|
||||||
|
linux_net.disable_learning_vxlan_intf(self.vxlan_name)
|
||||||
|
|
||||||
|
LOG.debug('Configure FRR VRF (add)')
|
||||||
|
frr.vrf_reconfigure(self.evpn_opts, 'add-vrf')
|
||||||
|
|
||||||
|
if self.mode == constants.OVN_EVPN_TYPE_L3:
|
||||||
|
LOG.debug('Create L3 EVPN devices')
|
||||||
|
linux_net.ensure_vrf(self.vrf_name, self.vni)
|
||||||
|
|
||||||
|
LOG.debug('Attach bridge %s to vrf %s',
|
||||||
|
self.bridge_name, self.vrf_name)
|
||||||
|
linux_net.set_master_for_device(self.bridge_name, self.vrf_name)
|
||||||
|
|
||||||
|
self._setup_done = True
|
||||||
|
|
||||||
|
def _eval_disconnect(self):
|
||||||
|
if not self._setup_done:
|
||||||
|
return
|
||||||
|
|
||||||
|
connected = [1 for v in self.vlans.values() if v._setup_done]
|
||||||
|
if len(connected) == 0:
|
||||||
|
# All vlan interfaces are unprovisioned, proceed with disconnect
|
||||||
|
return self.disconnect()
|
||||||
|
|
||||||
|
LOG.debug('No disconnect needed, there are still %s vlans active',
|
||||||
|
len(connected))
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
LOG.info('Disconnecting evpn bridge %s',
|
||||||
|
self.vrf_name)
|
||||||
|
|
||||||
|
for devname in [self.bridge_name, self.vxlan_name, self.vrf_name]:
|
||||||
|
LOG.info('Delete device %s', devname)
|
||||||
|
linux_net.delete_device(devname)
|
||||||
|
|
||||||
|
# We need to do the frr reconfigure after deleting all devices.
|
||||||
|
# otherwise, frr will throw an error that it can only delete
|
||||||
|
# inactive vrf's
|
||||||
|
LOG.debug('Configure FRR VRF (del)')
|
||||||
|
frr.vrf_reconfigure(self.evpn_opts, action="del-vrf")
|
||||||
|
|
||||||
|
self._setup_done = False
|
||||||
|
|
||||||
|
def connect_vlan(self, port):
|
||||||
|
vlan_tag = driver_utils.get_port_vlan(port)
|
||||||
|
if vlan_tag not in self.vlans:
|
||||||
|
self.vlans[vlan_tag] = VlanDev(self, port)
|
||||||
|
|
||||||
|
return self.vlans[vlan_tag]
|
||||||
|
|
||||||
|
def get_vlan(self, vlan: 'int|str|None') -> 'VlanDev':
|
||||||
|
if vlan is None:
|
||||||
|
vlan = constants.VLAN_ID_UNTAGGED
|
||||||
|
|
||||||
|
return self.vlans[str(vlan)]
|
||||||
|
|
||||||
|
|
||||||
|
class VlanDev:
|
||||||
|
def __init__(self, bridge: 'EvpnBridge', port):
|
||||||
|
self.bridge = bridge
|
||||||
|
self.port = port # localnet port
|
||||||
|
|
||||||
|
uuid = str(port.uuid)[0:11]
|
||||||
|
self.veth_vrf = constants.OVN_EVPN_VETH_VRF_UUID_PREFIX + uuid
|
||||||
|
self.veth_ovs = constants.OVN_EVPN_VETH_OVS_UUID_PREFIX + uuid
|
||||||
|
|
||||||
|
if uuid in local_vlandevs:
|
||||||
|
# It already exists, but probably in another vni
|
||||||
|
local_vlandevs[uuid].teardown()
|
||||||
|
|
||||||
|
local_vlandevs[uuid] = self
|
||||||
|
|
||||||
|
self.vlan_tag = driver_utils.get_port_vlan(port)
|
||||||
|
|
||||||
|
# Will be filled by the @property with the mac address of
|
||||||
|
# the vrf-vlan interface
|
||||||
|
self._lladdr = None
|
||||||
|
|
||||||
|
# boolean to indicate if setup is required for this interface
|
||||||
|
self._setup_done = False
|
||||||
|
self._veth_created = False
|
||||||
|
|
||||||
|
# list of custom addresses to use during setup, before adding the
|
||||||
|
# 169.254.x.x address, making another possibly public ip the
|
||||||
|
# primary ip in traceroutes
|
||||||
|
self._custom_ips = set()
|
||||||
|
|
||||||
|
# list with tuple of (task, args, kwargs) we should execute once the
|
||||||
|
# setup has completed. For example to run frr config for
|
||||||
|
# ipv6 neighbor discovery, or to add ip's to the interface.
|
||||||
|
self._post_setup_tasks = []
|
||||||
|
|
||||||
|
self._agent_routing_tables_routes = collections.defaultdict(list)
|
||||||
|
self._route_table_routes = {}
|
||||||
|
|
||||||
|
def _set_agent_cache(self, routing_tables_routes):
|
||||||
|
if routing_tables_routes is not None:
|
||||||
|
self._agent_routing_tables_routes = (
|
||||||
|
routing_tables_routes[self.veth_vrf])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lladdr(self):
|
||||||
|
if not self._lladdr:
|
||||||
|
if not self._veth_created:
|
||||||
|
self.setup()
|
||||||
|
self._lladdr = linux_net.get_interface_address(self.veth_vrf)
|
||||||
|
return self._lladdr
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
if self._setup_done:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Run the setup of the bridge.
|
||||||
|
self.bridge.setup()
|
||||||
|
|
||||||
|
LOG.debug('Create VLAN veth interface %s <-> %s',
|
||||||
|
self.veth_vrf, self.veth_ovs)
|
||||||
|
linux_net.ensure_veth(self.veth_vrf, self.veth_ovs)
|
||||||
|
self._veth_created = True
|
||||||
|
|
||||||
|
# Connect the veth_ovs to ovs
|
||||||
|
ovs_vlan_tag = self.vlan_tag
|
||||||
|
if self.vlan_tag == '0':
|
||||||
|
ovs_vlan_tag = None
|
||||||
|
|
||||||
|
ovs.add_device_to_ovs_bridge(self.veth_ovs,
|
||||||
|
self.bridge.ovs_bridge,
|
||||||
|
vlan_tag=ovs_vlan_tag)
|
||||||
|
|
||||||
|
# Connect veth to bridge for L2
|
||||||
|
if self.bridge.mode == constants.OVN_EVPN_TYPE_L2:
|
||||||
|
linux_net.set_master_for_device(self.veth_vrf,
|
||||||
|
self.bridge.bridge_name)
|
||||||
|
self._setup_done = True
|
||||||
|
return
|
||||||
|
|
||||||
|
# Connect veth to vrf for L3
|
||||||
|
# Create vrf interface, connect bridge and veth_vrf to it.
|
||||||
|
LOG.debug('Configure L3 for EVPN devices')
|
||||||
|
linux_net.set_master_for_device(self.veth_vrf, self.bridge.vrf_name)
|
||||||
|
|
||||||
|
if self._custom_ips:
|
||||||
|
linux_net.add_ips_to_dev(self.veth_vrf, ips=list(self._custom_ips))
|
||||||
|
|
||||||
|
# Add 169.254.x.x address to veth_vrf for ipv4 and ipv6
|
||||||
|
linux_net.ensure_arp_ndp_enabled_for_bridge(
|
||||||
|
self.veth_vrf, offset=int(self.vlan_tag), vlan_tag=self.vlan_tag
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure mac on the veth interface to be the same on all hosts
|
||||||
|
offset = _offset_for_vni_and_vlan(self.bridge.vni, self.vlan_tag)
|
||||||
|
linux_net.ensure_anycast_mac_for_interface(
|
||||||
|
self.veth_vrf, offset=offset
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure ipv4 and ipv6 forwarding is enabled
|
||||||
|
linux_net.enable_routing_for_interfaces(self.veth_vrf,
|
||||||
|
self.bridge.bridge_name)
|
||||||
|
|
||||||
|
# As long as we use 169.254.x.x addresses, we require proxy arp to be
|
||||||
|
# there for initial router discovery
|
||||||
|
linux_net.enable_proxy_arp(self.veth_vrf)
|
||||||
|
linux_net.enable_proxy_ndp(self.veth_vrf)
|
||||||
|
|
||||||
|
ovs_ok = self._setup_ovs()
|
||||||
|
if ovs_ok is False:
|
||||||
|
LOG.error('Unable to setup ovs, a retry will pick it up.')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Any post-setup tasks to run.
|
||||||
|
for method, a, kw in self._post_setup_tasks:
|
||||||
|
method(*a, **kw)
|
||||||
|
|
||||||
|
self._setup_done = True
|
||||||
|
|
||||||
|
def _setup_ovs(self):
|
||||||
|
try:
|
||||||
|
in_port = ovs.get_ovs_patch_port_ofport(self.port.name)
|
||||||
|
LOG.debug('ovs in-port: %s', in_port)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ovs_flows = self.bridge.ovs_flows
|
||||||
|
ovs_bridge = self.bridge.ovs_bridge
|
||||||
|
|
||||||
|
pmm = ovs_flows[ovs_bridge].setdefault('port-mac-mapping', {})
|
||||||
|
pmm[in_port] = self.lladdr
|
||||||
|
|
||||||
|
ovs.ensure_mac_tweak_flows(ovs_bridge,
|
||||||
|
self.lladdr,
|
||||||
|
[in_port],
|
||||||
|
constants.OVS_RULE_COOKIE)
|
||||||
|
|
||||||
|
ovs.remove_extra_ovs_flows(ovs_flows, ovs_bridge,
|
||||||
|
constants.OVS_RULE_COOKIE)
|
||||||
|
|
||||||
|
def _eval_disconnect(self):
|
||||||
|
if not self._setup_done:
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(self._agent_routing_tables_routes) == 0:
|
||||||
|
return self.disconnect()
|
||||||
|
|
||||||
|
LOG.debug('No disconnect needed, there are still %s announcements',
|
||||||
|
len(self._agent_routing_tables_routes))
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
LOG.info('Disconnecting vlan interface %s.%s',
|
||||||
|
self.bridge.vrf_name, self.vlan_tag)
|
||||||
|
|
||||||
|
LOG.info('Remove device %s from ovs bridge %s', self.veth_ovs,
|
||||||
|
self.bridge.ovs_bridge)
|
||||||
|
ovs.del_device_from_ovs_bridge(self.veth_ovs,
|
||||||
|
self.bridge.ovs_bridge)
|
||||||
|
|
||||||
|
LOG.info('Delete device %s', self.veth_vrf)
|
||||||
|
linux_net.delete_device(self.veth_vrf)
|
||||||
|
|
||||||
|
self._veth_created = False
|
||||||
|
self._setup_done = False
|
||||||
|
self.bridge._eval_disconnect()
|
||||||
|
|
||||||
|
def teardown(self):
|
||||||
|
LOG.info('Running teardown for vlandev %s (vni change)', self.veth_vrf)
|
||||||
|
self.disconnect()
|
||||||
|
del self.bridge.vlans[self.vlan_tag]
|
||||||
|
|
||||||
|
def _run(self, method, *a, **kw):
|
||||||
|
# Run the method if setup is done, otherwise, run them when setup
|
||||||
|
# is called
|
||||||
|
if not self._setup_done:
|
||||||
|
self._post_setup_tasks.append([method, a, kw])
|
||||||
|
else:
|
||||||
|
method(*a, **kw)
|
||||||
|
|
||||||
|
def process_dhcp_opts(self, dhcp_opts):
|
||||||
|
'''Add IP's or router advertisements from configured dhcp options
|
||||||
|
|
||||||
|
For networks that have DHCP enabled (and have lsp with dhcp options),
|
||||||
|
we can add the IP of the gateway on our vlan interface. Then OVN is
|
||||||
|
able to discover the IP.
|
||||||
|
|
||||||
|
Also if IPv6 is configured, we should enable router advertisements
|
||||||
|
through FRR (since it has it built-in anyway), so vm's can then
|
||||||
|
receive the router information from the 'provider' side.
|
||||||
|
'''
|
||||||
|
|
||||||
|
for opt in dhcp_opts:
|
||||||
|
ver = netaddr.IPNetwork(opt.cidr).version
|
||||||
|
|
||||||
|
if opt.options.get('router', False):
|
||||||
|
LOG.debug('Adding IPv%s gateway ip: %s',
|
||||||
|
ver, opt.options['router'])
|
||||||
|
self.add_ips([opt.options['router']])
|
||||||
|
|
||||||
|
if ver == 6 and opt.cidr:
|
||||||
|
LOG.debug('Configure ipv6nd for %s and opts %s',
|
||||||
|
opt.cidr, opt.options)
|
||||||
|
self.configure_nd(opt.cidr, opts=opt.options)
|
||||||
|
|
||||||
|
def configure_nd(self, cidr, opts):
|
||||||
|
self._run(frr.nd_reconfigure, self.veth_vrf, cidr, opts)
|
||||||
|
|
||||||
|
def add_ips(self, ips: list):
|
||||||
|
self._custom_ips.update(ips)
|
||||||
|
self._run(linux_net.add_ips_to_dev, self.veth_vrf, ips=ips)
|
||||||
|
|
||||||
|
def add_route(self, routing_tables_routes: 'dict | None', ip: str,
|
||||||
|
mac: 'str | None', via: 'str | None' = None):
|
||||||
|
'''Will add route to the routing table for this vlan_dev
|
||||||
|
|
||||||
|
Please make sure pass along the routing_tables_routes dictionary at
|
||||||
|
least the first time a route is added (for example when exposing the
|
||||||
|
lrp or lsp). Then with a expose_remote_ip we can re-use the reference
|
||||||
|
from the agent set earlier.
|
||||||
|
'''
|
||||||
|
self.setup() # setup the bridge and vlan, if not already done.
|
||||||
|
self._set_agent_cache(routing_tables_routes)
|
||||||
|
|
||||||
|
if self.bridge.mode != constants.OVN_EVPN_TYPE_L3:
|
||||||
|
return
|
||||||
|
|
||||||
|
mask = None
|
||||||
|
if '/' in ip:
|
||||||
|
ip, mask = ip.split('/')
|
||||||
|
|
||||||
|
self._agent_routing_tables_routes.append({
|
||||||
|
'ip': ip, 'mask': mask, 'mac': mac, 'via': via,
|
||||||
|
})
|
||||||
|
LOG.debug('Add route %s/%s via %s dev %s table %s',
|
||||||
|
ip, mask, via, self.veth_vrf, self.bridge.vni)
|
||||||
|
linux_net.add_ip_route(self._route_table_routes, ip, self.bridge.vni,
|
||||||
|
self.veth_vrf, mask=mask, via=via)
|
||||||
|
|
||||||
|
# When a floating ip is passed along, it is a set of mac
|
||||||
|
# addresses, so ensure we are always processing a list.
|
||||||
|
for lladdr in _ensure_list(mac):
|
||||||
|
LOG.debug('Add neigh %s -> %s dev %s', ip, mac, self.veth_vrf)
|
||||||
|
linux_net.add_ip_nei(ip, lladdr, self.veth_vrf)
|
||||||
|
|
||||||
|
def del_route(self, routing_tables_routes: 'dict | None', ip: str,
|
||||||
|
lladdr: 'str | None' = None):
|
||||||
|
'''Will remove the route from the routing table for this vlan_dev
|
||||||
|
|
||||||
|
Please make sure pass along the routing_tables_routes dictionary at
|
||||||
|
least the first time a route is added (for example when exposing the
|
||||||
|
lrp or lsp). Then with a withdraw_remote_ip we can re-use the reference
|
||||||
|
from the agent set earlier.
|
||||||
|
|
||||||
|
lladdr is optional, as it will be fetched from the internal
|
||||||
|
route table dictionary
|
||||||
|
'''
|
||||||
|
|
||||||
|
if self.bridge.mode != constants.OVN_EVPN_TYPE_L3:
|
||||||
|
return
|
||||||
|
self._set_agent_cache(routing_tables_routes)
|
||||||
|
|
||||||
|
# When a floating ip is passed along, it is a set of mac
|
||||||
|
# addresses, so ensure we are always processing a list.
|
||||||
|
|
||||||
|
mask = None
|
||||||
|
if '/' in ip:
|
||||||
|
ip, mask = ip.split('/')
|
||||||
|
|
||||||
|
route = _find_route_info(self._agent_routing_tables_routes, ip)
|
||||||
|
|
||||||
|
# Remove route from vrf
|
||||||
|
linux_net.del_ip_route(self._route_table_routes, ip, self.bridge.vni,
|
||||||
|
self.veth_vrf, mask=mask or route['mask'],
|
||||||
|
via=route['via'])
|
||||||
|
|
||||||
|
# Remove any neighbor information for route.
|
||||||
|
for mac in _ensure_list(lladdr or route['mac']):
|
||||||
|
linux_net.del_ip_nei(ip, mac, self.veth_vrf)
|
||||||
|
|
||||||
|
if route in self._agent_routing_tables_routes:
|
||||||
|
self._agent_routing_tables_routes.remove(route)
|
||||||
|
|
||||||
|
self._eval_disconnect()
|
||||||
|
|
||||||
|
def cleanup_excessive_routes(self, routing_tables_routes: dict):
|
||||||
|
if not self._setup_done:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._set_agent_cache(routing_tables_routes)
|
||||||
|
|
||||||
|
# Get all routes on host for our vrf and our veth_vrf
|
||||||
|
intf_idx = linux_net.get_interface_index(self.veth_vrf)
|
||||||
|
current_routes = dict([
|
||||||
|
(r.get_attr('RTA_DST'), r)
|
||||||
|
for r in linux_net._get_table_routes(self.bridge.vni)
|
||||||
|
if r.get_attr('RTA_OIF') == intf_idx and
|
||||||
|
r['type'] == constants.ROUTE_TYPE_UNICAST and
|
||||||
|
r.get_attr('RTA_DST') not in ('fe80::') and
|
||||||
|
not r.get_attr('RTA_DST').startswith(
|
||||||
|
constants.NDP_IPV6_PREFIX)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Create set with prefixes currently on host
|
||||||
|
prefixes = {r.get_attr('RTA_DST') for r in current_routes.values()}
|
||||||
|
|
||||||
|
# Create set with prefixes we maintain
|
||||||
|
exposed_prefixes = {r['ip']
|
||||||
|
for r in self._agent_routing_tables_routes}
|
||||||
|
|
||||||
|
if len(prefixes - exposed_prefixes) == 0:
|
||||||
|
LOG.debug('No excessive routes to remove.')
|
||||||
|
|
||||||
|
for ip in prefixes - exposed_prefixes:
|
||||||
|
LOG.info('Remove excessive route %s', ip)
|
||||||
|
kernel_route = current_routes[ip]
|
||||||
|
route = _find_route_info(self._agent_routing_tables_routes, ip)
|
||||||
|
if ((route['mask'] and
|
||||||
|
int(route['mask']) != kernel_route['dst_len']) or
|
||||||
|
route['via'] != kernel_route.get_attr('RTA_GATEWAY')):
|
||||||
|
self._agent_routing_tables_routes.append({
|
||||||
|
'ip': ip, 'mask': kernel_route['dst_len'], 'mac': None,
|
||||||
|
'via': kernel_route.get_attr('RTA_GATEWAY'),
|
||||||
|
})
|
||||||
|
|
||||||
|
self.del_route(routing_tables_routes, ip)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_list(var):
|
||||||
|
if var is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not isinstance(var, (list, tuple, set)):
|
||||||
|
var = [var]
|
||||||
|
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
def _find_route_info(routes: 'list[dict]', ip: str):
|
||||||
|
for r in routes:
|
||||||
|
if r['ip'] == ip:
|
||||||
|
return r
|
||||||
|
|
||||||
|
return {'ip': ip, 'mask': None, 'mac': None, 'via': None}
|
||||||
|
|
||||||
|
|
||||||
|
def _offset_for_vni_and_vlan(vni: int, vlan: str):
|
||||||
|
'''Generate a offset (in numeric system), based on the vni and vlan
|
||||||
|
|
||||||
|
vni has range 1-16777214 (6 bytes)
|
||||||
|
vlan has range 0-4094 (3 bytes)
|
||||||
|
|
||||||
|
It will transform the vni to a 6 digit hex, append the 3 digit vlan hex
|
||||||
|
and transform it back to a integer.
|
||||||
|
'''
|
||||||
|
if vni > 16777214:
|
||||||
|
LOG.warning('Configured vni value %d is too big (range 1-16777214)',
|
||||||
|
vni)
|
||||||
|
vni = vni % 0xffffff # reset vni, to prevent overflow.
|
||||||
|
|
||||||
|
if int(vlan) > 4094:
|
||||||
|
LOG.warning('Configured vlan value %d is too big (range 0-4094)',
|
||||||
|
int(vlan))
|
||||||
|
vlan = int(vlan) % 0xfff # reset vlan, to prevent overflow.
|
||||||
|
|
||||||
|
return int(''.join([
|
||||||
|
('%x' % vni).zfill(6),
|
||||||
|
('%x' % int(vlan)).zfill(4),
|
||||||
|
]), 16)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(ovs_bridge, vni, evpn_opts, mode=constants.OVN_EVPN_TYPE_L3,
|
||||||
|
ovs_flows={}) -> EvpnBridge:
|
||||||
|
# This method will either create the EvpnBridge or return the one that
|
||||||
|
# already exists for the current vni.
|
||||||
|
|
||||||
|
vni = int(vni) # make sure the vni is a int, for lookup purposes
|
||||||
|
|
||||||
|
if local_bridges.get(vni, None) is None:
|
||||||
|
local_bridges[vni] = EvpnBridge(ovs_bridge, vni, evpn_opts,
|
||||||
|
mode=mode, ovs_flows=ovs_flows)
|
||||||
|
else:
|
||||||
|
local_bridges[vni].ovs_flows = ovs_flows
|
||||||
|
|
||||||
|
return local_bridges[vni]
|
||||||
|
|
||||||
|
|
||||||
|
def lookup(ovs_bridge: str, vlan: str) -> EvpnBridge:
|
||||||
|
if vlan is None:
|
||||||
|
vlan = constants.VLAN_ID_UNTAGGED
|
||||||
|
|
||||||
|
for br in local_bridges.values():
|
||||||
|
if br.ovs_bridge == ovs_bridge:
|
||||||
|
if str(vlan) in br.vlans:
|
||||||
|
return br
|
||||||
|
|
||||||
|
raise KeyError('Could not locate EVPN for bridge %s and/or vlan %s' % (
|
||||||
|
ovs_bridge, vlan))
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_vlan(ovs_bridge: str, vlan: str) -> VlanDev:
|
||||||
|
bridge = lookup(ovs_bridge, vlan)
|
||||||
|
return bridge.get_vlan(vlan)
|
@ -16,15 +16,31 @@ import json
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
|
||||||
from ovn_bgp_agent import constants
|
from ovn_bgp_agent import constants
|
||||||
import ovn_bgp_agent.privileged.vtysh
|
import ovn_bgp_agent.privileged.vtysh
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_REDISTRIBUTE = {'connected'}
|
DEFAULT_REDISTRIBUTE = {'connected'}
|
||||||
|
|
||||||
|
CONFIGURE_ND_TEMPLATE = '''
|
||||||
|
interface {{ intf }}
|
||||||
|
{% if is_dhcpv6 %}
|
||||||
|
ipv6 nd managed-config-flag
|
||||||
|
{% endif %}
|
||||||
|
{% for server in dns_servers %}
|
||||||
|
ipv6 nd rdnss {{ server }}
|
||||||
|
{% endfor %}
|
||||||
|
ipv6 nd prefix {{ prefix }}
|
||||||
|
no ipv6 nd suppress-ra
|
||||||
|
exit
|
||||||
|
'''
|
||||||
|
|
||||||
ADD_VRF_TEMPLATE = '''
|
ADD_VRF_TEMPLATE = '''
|
||||||
vrf {{ vrf_name }}
|
vrf {{ vrf_name }}
|
||||||
vni {{ vni }}
|
vni {{ vni }}
|
||||||
@ -44,12 +60,28 @@ router bgp {{ bgp_as }} vrf {{ vrf_name }}
|
|||||||
address-family l2vpn evpn
|
address-family l2vpn evpn
|
||||||
advertise ipv4 unicast
|
advertise ipv4 unicast
|
||||||
advertise ipv6 unicast
|
advertise ipv6 unicast
|
||||||
|
{% if route_distinguishers|length > 0 %}
|
||||||
|
rd {{ route_distinguishers[0] }}
|
||||||
|
{% else %}
|
||||||
|
rd {{ local_ip }}:{{ vni }}
|
||||||
|
{% endif %}
|
||||||
|
{% for route_target in route_targets %}
|
||||||
|
route-target import {{ route_target }}
|
||||||
|
route-target export {{ route_target }}
|
||||||
|
{% endfor %}
|
||||||
|
{% for route_target in export_targets %}
|
||||||
|
route-target export {{ route_target }}
|
||||||
|
{% endfor %}
|
||||||
|
{% for route_target in import_targets %}
|
||||||
|
route-target import {{ route_target }}
|
||||||
|
{% endfor %}
|
||||||
exit-address-family
|
exit-address-family
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
DEL_VRF_TEMPLATE = '''
|
DEL_VRF_TEMPLATE = '''
|
||||||
no vrf {{ vrf_name }}
|
no vrf {{ vrf_name }}
|
||||||
|
no interface veth-{{ vrf_name }}
|
||||||
no router bgp {{ bgp_as }} vrf {{ vrf_name }}
|
no router bgp {{ bgp_as }} vrf {{ vrf_name }}
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@ -118,6 +150,33 @@ def set_default_redistribute(redist_opts):
|
|||||||
DEFAULT_REDISTRIBUTE.update(redist_opts)
|
DEFAULT_REDISTRIBUTE.update(redist_opts)
|
||||||
|
|
||||||
|
|
||||||
|
def nd_reconfigure(interface, prefix, opts):
|
||||||
|
LOG.info('FRR IPv6 ND reconfiguration (intf %s, prefix %s)', interface,
|
||||||
|
prefix)
|
||||||
|
nd_template = Template(CONFIGURE_ND_TEMPLATE)
|
||||||
|
|
||||||
|
# Need to define what setting is for SLAAC
|
||||||
|
if (not opts.get('dhcpv6_stateless', False) or
|
||||||
|
opts.get('dhcpv6_stateless', '') not in ('true', True)):
|
||||||
|
prefix += ' no-autoconfig'
|
||||||
|
|
||||||
|
# Parse dns servers from dhcp options.
|
||||||
|
dns_servers = []
|
||||||
|
if opts.get('dns_server'):
|
||||||
|
dns_servers = [s.strip() for s in opts['dns_server'][1:-1].split(',')]
|
||||||
|
|
||||||
|
is_dhcpv6 = True # Need a better way to define this one.
|
||||||
|
|
||||||
|
nd_config = nd_template.render(
|
||||||
|
intf=interface,
|
||||||
|
prefix=prefix,
|
||||||
|
dns_servers=dns_servers,
|
||||||
|
is_dhcpv6=is_dhcpv6,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_vtysh_config_with_tempfile(nd_config)
|
||||||
|
|
||||||
|
|
||||||
def vrf_leak(vrf, bgp_as, bgp_router_id=None, template=LEAK_VRF_TEMPLATE):
|
def vrf_leak(vrf, bgp_as, bgp_router_id=None, template=LEAK_VRF_TEMPLATE):
|
||||||
LOG.info("Add VRF leak for VRF %s on router bgp %s", vrf, bgp_as)
|
LOG.info("Add VRF leak for VRF %s on router bgp %s", vrf, bgp_as)
|
||||||
if not bgp_router_id:
|
if not bgp_router_id:
|
||||||
@ -136,21 +195,29 @@ def vrf_leak(vrf, bgp_as, bgp_router_id=None, template=LEAK_VRF_TEMPLATE):
|
|||||||
def vrf_reconfigure(evpn_info, action):
|
def vrf_reconfigure(evpn_info, action):
|
||||||
LOG.info("FRR reconfiguration (action = %s) for evpn: %s",
|
LOG.info("FRR reconfiguration (action = %s) for evpn: %s",
|
||||||
action, evpn_info)
|
action, evpn_info)
|
||||||
if action == "add-vrf":
|
|
||||||
vrf_template = Template(ADD_VRF_TEMPLATE)
|
# If we have more actions, we can define them in this list.
|
||||||
vrf_config = vrf_template.render(
|
vrf_templates = {
|
||||||
vrf_name="{}{}".format(constants.OVN_EVPN_VRF_PREFIX,
|
'add-vrf': ADD_VRF_TEMPLATE,
|
||||||
evpn_info['vni']),
|
'del-vrf': DEL_VRF_TEMPLATE,
|
||||||
bgp_as=evpn_info['bgp_as'],
|
}
|
||||||
redistribute=DEFAULT_REDISTRIBUTE,
|
if action not in vrf_templates:
|
||||||
vni=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'])
|
|
||||||
else:
|
|
||||||
LOG.error("Unknown FRR reconfiguration action: %s", action)
|
LOG.error("Unknown FRR reconfiguration action: %s", action)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Set default opts, so all params are available for the templates
|
||||||
|
# Then update them with evpn_info
|
||||||
|
opts = dict(route_targets=[], route_distinguishers=[], export_targets=[],
|
||||||
|
import_targets=[], local_ip=CONF.evpn_local_ip,
|
||||||
|
redistribute=DEFAULT_REDISTRIBUTE,
|
||||||
|
bgp_as=CONF.bgp_AS, vrf_name='', vni=0)
|
||||||
|
opts.update(evpn_info)
|
||||||
|
|
||||||
|
if not opts['vrf_name']:
|
||||||
|
opts['vrf_name'] = "{}{}".format(constants.OVN_EVPN_VRF_PREFIX,
|
||||||
|
evpn_info['vni'])
|
||||||
|
|
||||||
|
vrf_template = Template(vrf_templates.get(action))
|
||||||
|
vrf_config = vrf_template.render(**opts)
|
||||||
|
|
||||||
_run_vtysh_config_with_tempfile(vrf_config)
|
_run_vtysh_config_with_tempfile(vrf_config)
|
||||||
|
@ -391,14 +391,36 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
|
|||||||
|
|
||||||
def get_network_vlan_tag_by_network_name(self, network_name):
|
def get_network_vlan_tag_by_network_name(self, network_name):
|
||||||
tags = []
|
tags = []
|
||||||
cmd = self.db_find_rows('Logical_Switch_Port', ('type', '=',
|
for row in self.get_localnet_ports_by_network_name(network_name):
|
||||||
constants.OVN_LOCALNET_VIF_PORT_TYPE))
|
if row.tag:
|
||||||
for row in cmd.execute(check_error=True):
|
|
||||||
if (row.tag and row.options and
|
|
||||||
row.options.get('network_name') == network_name):
|
|
||||||
tags.append(row.tag[0])
|
tags.append(row.tag[0])
|
||||||
return tags
|
return tags
|
||||||
|
|
||||||
|
def get_localnet_ports_by_network_name(self, network_name):
|
||||||
|
conditions = (
|
||||||
|
('options', '=', {'network_name': network_name}),
|
||||||
|
('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE),
|
||||||
|
)
|
||||||
|
cmd = self.db_find_rows('Logical_Switch_Port', *conditions)
|
||||||
|
for row in cmd.execute(check_error=True):
|
||||||
|
yield row
|
||||||
|
|
||||||
|
def get_bgpvpn_networks_for_ports(self, ports,
|
||||||
|
vpn_type=constants.OVN_EVPN_TYPE_L3):
|
||||||
|
cmd = self.db_find_rows(
|
||||||
|
'Logical_Switch',
|
||||||
|
('external_ids', '=', {constants.OVN_EVPN_TYPE_EXT_ID_KEY:
|
||||||
|
vpn_type}))
|
||||||
|
networks = []
|
||||||
|
for row in cmd.execute(check_error=True):
|
||||||
|
if not row.ports:
|
||||||
|
continue
|
||||||
|
|
||||||
|
localnet_ports = [p for p in row.ports if p in ports]
|
||||||
|
if localnet_ports:
|
||||||
|
networks.append(row)
|
||||||
|
return networks
|
||||||
|
|
||||||
def ls_has_virtual_ports(self, logical_switch):
|
def ls_has_virtual_ports(self, logical_switch):
|
||||||
ls = self.lookup('Logical_Switch', logical_switch)
|
ls = self.lookup('Logical_Switch', logical_switch)
|
||||||
for port in ls.ports:
|
for port in ls.ports:
|
||||||
|
@ -124,12 +124,19 @@ def ensure_mac_tweak_flows(bridge, mac, ports, cookie):
|
|||||||
def remove_extra_ovs_flows(ovs_flows, bridge, cookie):
|
def remove_extra_ovs_flows(ovs_flows, bridge, cookie):
|
||||||
expected_flows = []
|
expected_flows = []
|
||||||
for port in ovs_flows[bridge].get('in_port'):
|
for port in ovs_flows[bridge].get('in_port'):
|
||||||
flow = ("=900,ip,in_port={} actions=mod_dl_dst:{},NORMAL".format(
|
pmm = ovs_flows[bridge].get('port-mac-mapping', {})
|
||||||
port, ovs_flows[bridge]['mac']))
|
lladdr = pmm.get(port, ovs_flows[bridge]['mac'])
|
||||||
expected_flows.append(flow)
|
|
||||||
flow_v6 = ("=900,ipv6,in_port={} actions=mod_dl_dst:{},NORMAL".format(
|
for flow in [
|
||||||
port, ovs_flows[bridge]['mac']))
|
# Add ipv4 flow in 'normal' and OpenFlow13 format
|
||||||
expected_flows.append(flow_v6)
|
"=900,ip,in_port={} actions=mod_dl_dst:{},NORMAL",
|
||||||
|
"=900,ip,in_port={} actions=set_field:{}->eth_dst,NORMAL",
|
||||||
|
|
||||||
|
# Add ipv6 flow in 'normal' and OpenFlow13 format
|
||||||
|
"=900,ipv6,in_port={} actions=mod_dl_dst:{},NORMAL",
|
||||||
|
"=900,ipv6,in_port={} actions=set_field:{}->eth_dst,NORMAL",
|
||||||
|
]:
|
||||||
|
expected_flows.append(flow.format(port, lladdr))
|
||||||
|
|
||||||
cookie_id = "cookie={}/-1".format(cookie)
|
cookie_id = "cookie={}/-1".format(cookie)
|
||||||
current_flows = get_bridge_flows(bridge, cookie_id)
|
current_flows = get_bridge_flows(bridge, cookie_id)
|
||||||
|
@ -12,10 +12,15 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import ast
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
|
||||||
from ovn_bgp_agent import constants
|
from ovn_bgp_agent import constants
|
||||||
|
from ovn_bgp_agent.drivers.openstack.utils import driver_utils
|
||||||
|
from ovn_bgp_agent.drivers.openstack.utils import evpn
|
||||||
|
from ovn_bgp_agent.drivers.openstack.utils import ovn
|
||||||
from ovn_bgp_agent.drivers.openstack.utils import ovs
|
from ovn_bgp_agent.drivers.openstack.utils import ovs
|
||||||
from ovn_bgp_agent import exceptions as agent_exc
|
from ovn_bgp_agent import exceptions as agent_exc
|
||||||
from ovn_bgp_agent.utils import helpers
|
from ovn_bgp_agent.utils import helpers
|
||||||
@ -30,9 +35,13 @@ def ensure_base_wiring_config(idl, ovs_idl, ovn_idl=None, routing_tables={}):
|
|||||||
if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY:
|
if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY:
|
||||||
return _ensure_base_wiring_config_underlay(idl, ovs_idl,
|
return _ensure_base_wiring_config_underlay(idl, ovs_idl,
|
||||||
routing_tables)
|
routing_tables)
|
||||||
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_VRF: # Type 5 evpn
|
||||||
|
return _ensure_base_wiring_config_evpn(idl, ovs_idl)
|
||||||
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
|
||||||
return _ensure_base_wiring_config_ovn(ovs_idl, ovn_idl)
|
return _ensure_base_wiring_config_ovn(ovs_idl, ovn_idl)
|
||||||
|
|
||||||
|
raise agent_exc.UnsupportedWiringConfig(method=CONF.exposing_method)
|
||||||
|
|
||||||
|
|
||||||
def _ensure_base_wiring_config_underlay(idl, ovs_idl, routing_tables):
|
def _ensure_base_wiring_config_underlay(idl, ovs_idl, routing_tables):
|
||||||
# Get bridge mappings: xxxx:br-ex,yyyy:br-ex2
|
# Get bridge mappings: xxxx:br-ex,yyyy:br-ex2
|
||||||
@ -68,6 +77,105 @@ def _ensure_base_wiring_config_underlay(idl, ovs_idl, routing_tables):
|
|||||||
return ovn_bridge_mappings, flows_info
|
return ovn_bridge_mappings, flows_info
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_base_wiring_config_evpn(idl: 'ovn.OvsdbNbOvnIdl|ovn.OvsdbSbOvnIdl',
|
||||||
|
ovs_idl: 'ovs.OvsIdl',
|
||||||
|
mode=constants.OVN_EVPN_TYPE_L3):
|
||||||
|
# This method will create the bridge mappings and make sure the
|
||||||
|
# vrf bridge and vlans are provisioned
|
||||||
|
|
||||||
|
# Get bridge mappings: xxxx:br-ex,yyyy:br-ex2
|
||||||
|
bridge_mappings = ovs_idl.get_ovn_bridge_mappings()
|
||||||
|
|
||||||
|
ovn_bridge_mappings = {}
|
||||||
|
flows_info = {} # dictionary to use for mappings with vrf's
|
||||||
|
for bridge_mapping in bridge_mappings:
|
||||||
|
try:
|
||||||
|
# for example: physnet1, br-ex
|
||||||
|
network, bridge = bridge_mapping.split(":", 1)
|
||||||
|
except ValueError:
|
||||||
|
LOG.debug('Invalid bridge mapping: %s', bridge_mapping)
|
||||||
|
continue
|
||||||
|
|
||||||
|
ovn_bridge_mappings[network] = bridge
|
||||||
|
LOG.debug('Setup EVPN base wiring for network %s on bridge %s',
|
||||||
|
network, bridge)
|
||||||
|
|
||||||
|
# Make sure the bridge exists
|
||||||
|
ovs_idl.idl_ovs.add_br(bridge).execute(check_error=True)
|
||||||
|
|
||||||
|
if bridge not in flows_info:
|
||||||
|
flows_info[bridge] = {
|
||||||
|
'mac': linux_net.get_interface_address(bridge),
|
||||||
|
'in_port': ovs.get_ovs_patch_ports_info(bridge),
|
||||||
|
'evpn': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find all provider networks, and create the vrf's
|
||||||
|
localnet_ports = list(idl.get_localnet_ports_by_network_name(network))
|
||||||
|
if not localnet_ports:
|
||||||
|
LOG.debug('No localnet ports found for network %s', network)
|
||||||
|
continue
|
||||||
|
|
||||||
|
provnets = idl.get_bgpvpn_networks_for_ports(localnet_ports,
|
||||||
|
vpn_type=mode)
|
||||||
|
if not provnets:
|
||||||
|
LOG.debug('No provider networks found for %s %s',
|
||||||
|
constants.OVN_EVPN_TYPE_EXT_ID_KEY, mode)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for ls in provnets:
|
||||||
|
LOG.info('Network %s (settings: %s)', ls.name, ls.external_ids)
|
||||||
|
|
||||||
|
if constants.OVN_EVPN_VNI_EXT_ID_KEY not in ls.external_ids:
|
||||||
|
LOG.warning('Skipped, VNI required for EVPN VRF setup')
|
||||||
|
continue
|
||||||
|
|
||||||
|
evpn_opts = {}
|
||||||
|
for opt, ext_id_key in constants.EVPN_EXT_ID_MAPPING.items():
|
||||||
|
evpn_opts[opt] = ast.literal_eval(
|
||||||
|
ls.external_ids.get(ext_id_key, '[]')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create or return the EVPN bridge
|
||||||
|
evpn_bridge = evpn.setup(
|
||||||
|
ovs_bridge=bridge,
|
||||||
|
vni=ls.external_ids[constants.OVN_EVPN_VNI_EXT_ID_KEY],
|
||||||
|
evpn_opts=evpn_opts,
|
||||||
|
mode=mode,
|
||||||
|
ovs_flows=flows_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Connect all VLAN interfaces to this VRF and gather dhcp
|
||||||
|
# options to be configured for l3 mode.
|
||||||
|
evpn_dev, dhcp_opts = _ensure_evpn_vlan_dev(ls, localnet_ports,
|
||||||
|
evpn_bridge,
|
||||||
|
flows_info, bridge)
|
||||||
|
|
||||||
|
if dhcp_opts and evpn_dev:
|
||||||
|
evpn_dev.process_dhcp_opts(dhcp_opts)
|
||||||
|
|
||||||
|
return ovn_bridge_mappings, flows_info
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_evpn_vlan_dev(ls, localnet_ports, evpn_bridge, flows_info, bridge):
|
||||||
|
evpn_dev = None
|
||||||
|
dhcp_opts = set()
|
||||||
|
for port in ls.ports:
|
||||||
|
if port not in localnet_ports:
|
||||||
|
if port.dhcpv4_options:
|
||||||
|
dhcp_opts.update(port.dhcpv4_options)
|
||||||
|
if port.dhcpv6_options:
|
||||||
|
dhcp_opts.update(port.dhcpv6_options)
|
||||||
|
continue
|
||||||
|
|
||||||
|
LOG.info('VLAN tag %s', driver_utils.get_port_vlan(port))
|
||||||
|
evpn_dev = evpn_bridge.connect_vlan(port)
|
||||||
|
|
||||||
|
flows_info[bridge]['evpn'][str(evpn_dev.vlan_tag)] = evpn_bridge
|
||||||
|
|
||||||
|
return evpn_dev, dhcp_opts
|
||||||
|
|
||||||
|
|
||||||
def _ensure_base_wiring_config_ovn(ovs_idl, ovn_idl):
|
def _ensure_base_wiring_config_ovn(ovs_idl, ovn_idl):
|
||||||
"""Base configuration for extra OVN cluster instead of kernel networking
|
"""Base configuration for extra OVN cluster instead of kernel networking
|
||||||
|
|
||||||
@ -376,6 +484,8 @@ def cleanup_wiring(idl, bridge_mappings, ovs_flows, exposed_ips,
|
|||||||
return _cleanup_wiring_underlay(idl, bridge_mappings, ovs_flows,
|
return _cleanup_wiring_underlay(idl, bridge_mappings, ovs_flows,
|
||||||
exposed_ips, routing_tables,
|
exposed_ips, routing_tables,
|
||||||
routing_tables_routes)
|
routing_tables_routes)
|
||||||
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_VRF:
|
||||||
|
return _cleanup_wiring_evpn(ovs_flows, routing_tables_routes)
|
||||||
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
|
||||||
# TODO(ltomasbo): clean up old policies, routes and proxy_arps cidrs
|
# TODO(ltomasbo): clean up old policies, routes and proxy_arps cidrs
|
||||||
return True
|
return True
|
||||||
@ -432,6 +542,17 @@ def delete_vlan_devices_leftovers(idl, bridge_mappings):
|
|||||||
linux_net.delete_vlan_device_for_network(ovs_device, vlan)
|
linux_net.delete_vlan_device_for_network(ovs_device, vlan)
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup_wiring_evpn(ovs_flows, routing_tables_routes):
|
||||||
|
for flow_conf in ovs_flows.values():
|
||||||
|
for vlan, evpn_bridge in flow_conf.get('evpn', {}).items():
|
||||||
|
LOG.debug('Running cleanup for vrf %s vlan %s',
|
||||||
|
evpn_bridge.vrf_name, vlan)
|
||||||
|
evpn_bridge.get_vlan(vlan).cleanup_excessive_routes(
|
||||||
|
routing_tables_routes
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def wire_provider_port(routing_tables_routes, ovs_flows, port_ips,
|
def wire_provider_port(routing_tables_routes, ovs_flows, port_ips,
|
||||||
bridge_device, bridge_vlan, localnet, routing_table,
|
bridge_device, bridge_vlan, localnet, routing_table,
|
||||||
proxy_cidrs, lladdr=None, mac=None, ovn_idl=None):
|
proxy_cidrs, lladdr=None, mac=None, ovn_idl=None):
|
||||||
@ -441,6 +562,12 @@ def wire_provider_port(routing_tables_routes, ovs_flows, port_ips,
|
|||||||
bridge_vlan, localnet,
|
bridge_vlan, localnet,
|
||||||
routing_table, proxy_cidrs,
|
routing_table, proxy_cidrs,
|
||||||
lladdr=lladdr)
|
lladdr=lladdr)
|
||||||
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_VRF:
|
||||||
|
return _wire_provider_port_evpn(routing_tables_routes, ovs_flows,
|
||||||
|
port_ips, bridge_device,
|
||||||
|
bridge_vlan, localnet,
|
||||||
|
proxy_cidrs,
|
||||||
|
mac=mac)
|
||||||
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
|
||||||
# We need to add a static mac binding due to proxy-arp issue in
|
# We need to add a static mac binding due to proxy-arp issue in
|
||||||
# core ovn that would reply on the incomming traffic from the LR,
|
# core ovn that would reply on the incomming traffic from the LR,
|
||||||
@ -456,6 +583,10 @@ def unwire_provider_port(routing_tables_routes, port_ips, bridge_device,
|
|||||||
bridge_device, bridge_vlan,
|
bridge_device, bridge_vlan,
|
||||||
routing_table, proxy_cidrs,
|
routing_table, proxy_cidrs,
|
||||||
lladdr=lladdr)
|
lladdr=lladdr)
|
||||||
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_VRF:
|
||||||
|
return _unwire_provider_port_evpn(routing_tables_routes, port_ips,
|
||||||
|
bridge_device, bridge_vlan,
|
||||||
|
lladdr)
|
||||||
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
|
||||||
# We need to remove thestatic mac binding added due to proxy-arp issue
|
# We need to remove thestatic mac binding added due to proxy-arp issue
|
||||||
# in core ovn that would reply on the incomming traffic from the LR,
|
# in core ovn that would reply on the incomming traffic from the LR,
|
||||||
@ -512,6 +643,27 @@ def _wire_provider_port_underlay(routing_tables_routes, ovs_flows, port_ips,
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _wire_provider_port_evpn(routing_tables_routes, ovs_flows, port_ips,
|
||||||
|
bridge_device, bridge_vlan, localnet,
|
||||||
|
proxy_cidrs, mac=None, via=None):
|
||||||
|
try:
|
||||||
|
evpn_dev = evpn.lookup_vlan(bridge_device, bridge_vlan)
|
||||||
|
except KeyError:
|
||||||
|
msg = ('EVPN has not been setup for bridge %s with vlan device %s. '
|
||||||
|
'Either the network has not been configured, or something '
|
||||||
|
'went wrong in the base wiring method.')
|
||||||
|
LOG.warning(msg, bridge_device, bridge_vlan)
|
||||||
|
return
|
||||||
|
|
||||||
|
via = via or {} # make sure it is at least a empty dictionary
|
||||||
|
|
||||||
|
for ip in port_ips:
|
||||||
|
ver = linux_net.get_ip_version(ip)
|
||||||
|
evpn_dev.add_route(routing_tables_routes, ip, mac, via=via.get(ver))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _wire_provider_port_ovn(ovn_idl, port_ips, mac):
|
def _wire_provider_port_ovn(ovn_idl, port_ips, mac):
|
||||||
cmds = []
|
cmds = []
|
||||||
port = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER)
|
port = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER)
|
||||||
@ -561,6 +713,24 @@ def _unwire_provider_port_underlay(routing_tables_routes, port_ips,
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _unwire_provider_port_evpn(routing_tables_routes, port_ips,
|
||||||
|
bridge_device, bridge_vlan, lladdr):
|
||||||
|
# locate the evpn_dev, based on bridge and vlan
|
||||||
|
try:
|
||||||
|
evpn_dev = evpn.lookup_vlan(bridge_device, bridge_vlan)
|
||||||
|
except KeyError:
|
||||||
|
msg = ('EVPN has not been setup for bridge %s with vlan device %s. '
|
||||||
|
'Either the network has not been configured, or something '
|
||||||
|
'went wrong in the base wiring method.')
|
||||||
|
LOG.warning(msg, bridge_device, bridge_vlan)
|
||||||
|
return
|
||||||
|
|
||||||
|
for ip in port_ips:
|
||||||
|
evpn_dev.del_route(routing_tables_routes, ip, lladdr)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _unwire_provider_port_ovn(ovn_idl, port_ips):
|
def _unwire_provider_port_ovn(ovn_idl, port_ips):
|
||||||
cmds = []
|
cmds = []
|
||||||
port = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER)
|
port = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER)
|
||||||
@ -579,6 +749,9 @@ def wire_lrp_port(routing_tables_routes, ip, bridge_device, bridge_vlan,
|
|||||||
return _wire_lrp_port_underlay(routing_tables_routes, ip,
|
return _wire_lrp_port_underlay(routing_tables_routes, ip,
|
||||||
bridge_device, bridge_vlan,
|
bridge_device, bridge_vlan,
|
||||||
routing_tables, cr_lrp_ips)
|
routing_tables, cr_lrp_ips)
|
||||||
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_VRF:
|
||||||
|
return _wire_lrp_port_evpn(routing_tables_routes, ip, bridge_device,
|
||||||
|
bridge_vlan, cr_lrp_ips)
|
||||||
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
|
||||||
# TODO(ltomasbo): Add flow on br-ex(-X)
|
# TODO(ltomasbo): Add flow on br-ex(-X)
|
||||||
# ovs-ofctl add-flow br-ex
|
# ovs-ofctl add-flow br-ex
|
||||||
@ -622,12 +795,35 @@ def _wire_lrp_port_underlay(routing_tables_routes, ip, bridge_device,
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _wire_lrp_port_evpn(routing_tables_routes, ip, bridge_device,
|
||||||
|
bridge_vlan, cr_lrp_ips):
|
||||||
|
|
||||||
|
# Generate the via addresses
|
||||||
|
via = driver_utils.ips_per_version(cr_lrp_ips)
|
||||||
|
|
||||||
|
try:
|
||||||
|
evpn_dev = evpn.lookup_vlan(bridge_device, bridge_vlan)
|
||||||
|
except KeyError:
|
||||||
|
msg = ('EVPN has not been setup for bridge %s with vlan device %s. '
|
||||||
|
'Either the network has not been configured, or something '
|
||||||
|
'went wrong in the base wiring method.')
|
||||||
|
LOG.warning(msg, bridge_device, bridge_vlan)
|
||||||
|
return
|
||||||
|
|
||||||
|
ver = linux_net.get_ip_version(ip)
|
||||||
|
evpn_dev.add_route(routing_tables_routes, ip, None, via=via.get(ver))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def unwire_lrp_port(routing_tables_routes, ip, bridge_device, bridge_vlan,
|
def unwire_lrp_port(routing_tables_routes, ip, bridge_device, bridge_vlan,
|
||||||
routing_tables, cr_lrp_ips):
|
routing_tables, cr_lrp_ips):
|
||||||
if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY:
|
if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY:
|
||||||
return _unwire_lrp_port_underlay(routing_tables_routes, ip,
|
return _unwire_lrp_port_underlay(routing_tables_routes, ip,
|
||||||
bridge_device, bridge_vlan,
|
bridge_device, bridge_vlan,
|
||||||
routing_tables, cr_lrp_ips)
|
routing_tables, cr_lrp_ips)
|
||||||
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_VRF:
|
||||||
|
return _unwire_lrp_port_evpn(routing_tables_routes, ip,
|
||||||
|
bridge_device, bridge_vlan)
|
||||||
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
|
elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN:
|
||||||
# TODO(ltomasbo): Remove flow(s) and router route
|
# TODO(ltomasbo): Remove flow(s) and router route
|
||||||
return
|
return
|
||||||
@ -660,3 +856,20 @@ def _unwire_lrp_port_underlay(routing_tables_routes, ip, bridge_device,
|
|||||||
via=cr_lrp_ip)
|
via=cr_lrp_ip)
|
||||||
LOG.debug("Deleted IP Routes for network %s", ip)
|
LOG.debug("Deleted IP Routes for network %s", ip)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _unwire_lrp_port_evpn(routing_tables_routes, ip, bridge_device,
|
||||||
|
bridge_vlan):
|
||||||
|
# locate the evpn_dev, based on bridge and vlan
|
||||||
|
try:
|
||||||
|
evpn_dev = evpn.lookup_vlan(bridge_device, bridge_vlan)
|
||||||
|
except KeyError:
|
||||||
|
msg = ('EVPN has not been setup for bridge %s with vlan device %s. '
|
||||||
|
'Either the network has not been configured, or something '
|
||||||
|
'went wrong in the base wiring method.')
|
||||||
|
LOG.warning(msg, bridge_device, bridge_vlan)
|
||||||
|
return
|
||||||
|
|
||||||
|
evpn_dev.del_route(routing_tables_routes, ip)
|
||||||
|
|
||||||
|
return True
|
||||||
|
@ -85,6 +85,15 @@ class OVNLBEvent(Event):
|
|||||||
constants.OVN_LB_VIP_FIP_EXT_ID_KEY)
|
constants.OVN_LB_VIP_FIP_EXT_ID_KEY)
|
||||||
|
|
||||||
|
|
||||||
|
class LogicalSwitchChassisEvent(Event):
|
||||||
|
def __init__(self, bgp_agent, events):
|
||||||
|
self.agent = bgp_agent
|
||||||
|
table = 'Logical_Switch'
|
||||||
|
super(LogicalSwitchChassisEvent, self).__init__(
|
||||||
|
events, table, None)
|
||||||
|
self.event_name = self.__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
class LSPChassisEvent(Event):
|
class LSPChassisEvent(Event):
|
||||||
def __init__(self, bgp_agent, events):
|
def __init__(self, bgp_agent, events):
|
||||||
self.agent = bgp_agent
|
self.agent = bgp_agent
|
||||||
|
@ -311,6 +311,43 @@ class LogicalSwitchPortFIPDeleteEvent(base_watcher.LSPChassisEvent):
|
|||||||
self.agent.withdraw_fip(fip, row)
|
self.agent.withdraw_fip(fip, row)
|
||||||
|
|
||||||
|
|
||||||
|
class LogicalSwitchUpdateEvent(base_watcher.LogicalSwitchChassisEvent):
|
||||||
|
'''Event to trigger on logical switch vrf config updates'''
|
||||||
|
def __init__(self, bgp_agent):
|
||||||
|
events = (self.ROW_UPDATE, self.ROW_DELETE)
|
||||||
|
super(LogicalSwitchUpdateEvent, self).__init__(
|
||||||
|
bgp_agent, events)
|
||||||
|
|
||||||
|
def match_fn(self, event, row, old):
|
||||||
|
'''Match updates for vrf configuration
|
||||||
|
|
||||||
|
Will trigger whenever external_ids[neutron_bgpvpn:vni] and
|
||||||
|
external_ids[neutron_bgpvpn:type] have been set and either one has
|
||||||
|
been updated
|
||||||
|
'''
|
||||||
|
|
||||||
|
settings = driver_utils.get_port_vrf_settings(row)
|
||||||
|
if settings and event == self.ROW_DELETE:
|
||||||
|
# Always run sync method if we are deleting this network (and it
|
||||||
|
# had settings applied)
|
||||||
|
return True
|
||||||
|
|
||||||
|
old_settings = driver_utils.get_port_vrf_settings(old)
|
||||||
|
if old_settings is None:
|
||||||
|
# it was not provided in old, so do not process this update
|
||||||
|
return False
|
||||||
|
|
||||||
|
return settings != old_settings
|
||||||
|
|
||||||
|
def _run(self, event, row, old):
|
||||||
|
with _SYNC_STATE_LOCK.read_lock():
|
||||||
|
# NOTE(mnederlof): For now it makes sense to run the sync method
|
||||||
|
# as this is triggered with a configured interval anyway and it
|
||||||
|
# will add/remove the triggered logical switch.
|
||||||
|
# It might make sense in the future to optimize this behaviour.
|
||||||
|
self.agent.sync()
|
||||||
|
|
||||||
|
|
||||||
class LocalnetCreateDeleteEvent(base_watcher.LSPChassisEvent):
|
class LocalnetCreateDeleteEvent(base_watcher.LSPChassisEvent):
|
||||||
def __init__(self, bgp_agent):
|
def __init__(self, bgp_agent):
|
||||||
events = (self.ROW_CREATE, self.ROW_DELETE,)
|
events = (self.ROW_CREATE, self.ROW_DELETE,)
|
||||||
|
@ -33,6 +33,24 @@ class OVNBGPAgentException(Exception):
|
|||||||
return self.msg
|
return self.msg
|
||||||
|
|
||||||
|
|
||||||
|
class ConfOptionRequired(OVNBGPAgentException):
|
||||||
|
"""Configuration option is required for this driver to function properly
|
||||||
|
|
||||||
|
:param option: The configuration option required
|
||||||
|
"""
|
||||||
|
|
||||||
|
message = _("Required driver conf option missing: CONF.%(option)s")
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedWiringConfig(OVNBGPAgentException):
|
||||||
|
"""Wiring config called with unknown/unsupported exposing method
|
||||||
|
|
||||||
|
:param method: The (unsupported) exposing method
|
||||||
|
"""
|
||||||
|
|
||||||
|
message = _("No wiring support for exposing_method %(method)s.")
|
||||||
|
|
||||||
|
|
||||||
class InvalidPortIP(OVNBGPAgentException):
|
class InvalidPortIP(OVNBGPAgentException):
|
||||||
"""OVN Port has Invalid IP.
|
"""OVN Port has Invalid IP.
|
||||||
|
|
||||||
|
@ -518,6 +518,15 @@ def _run_iproute_neigh(command, device, **kwargs):
|
|||||||
command, device)
|
command, device)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_iproute_brport(command, ifname, **kwargs):
|
||||||
|
try:
|
||||||
|
with iproute.IPRoute() as ip:
|
||||||
|
idx = _get_link_id(ifname)
|
||||||
|
return ip.brport(command, index=idx, **kwargs)
|
||||||
|
except netlink_exceptions.NetlinkError as e:
|
||||||
|
_translate_ip_device_exception(e, ifname)
|
||||||
|
|
||||||
|
|
||||||
@ovn_bgp_agent.privileged.default.entrypoint
|
@ovn_bgp_agent.privileged.default.entrypoint
|
||||||
def create_interface(ifname, kind, **kwargs):
|
def create_interface(ifname, kind, **kwargs):
|
||||||
ifname = ifname[:15]
|
ifname = ifname[:15]
|
||||||
@ -545,11 +554,17 @@ def set_link_attribute(ifname, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@ovn_bgp_agent.privileged.default.entrypoint
|
@ovn_bgp_agent.privileged.default.entrypoint
|
||||||
def add_ip_address(ip_address, ifname):
|
def set_brport_attribute(ifname, **kwargs):
|
||||||
|
_run_iproute_brport("set", ifname, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ovn_bgp_agent.privileged.default.entrypoint
|
||||||
|
def add_ip_address(ip_address, ifname, prefixlen=None, **kwargs):
|
||||||
ifname = ifname[:15]
|
ifname = ifname[:15]
|
||||||
net = netaddr.IPNetwork(ip_address)
|
net = netaddr.IPNetwork(ip_address)
|
||||||
ip_version = l_net.get_ip_version(ip_address)
|
ip_version = l_net.get_ip_version(ip_address)
|
||||||
address = str(net.ip)
|
address = str(net.ip)
|
||||||
|
if not prefixlen:
|
||||||
prefixlen = 32 if ip_version == 4 else 128
|
prefixlen = 32 if ip_version == 4 else 128
|
||||||
family = common_utils.IP_VERSION_FAMILY_MAP[ip_version]
|
family = common_utils.IP_VERSION_FAMILY_MAP[ip_version]
|
||||||
_run_iproute_addr('add',
|
_run_iproute_addr('add',
|
||||||
@ -560,18 +575,20 @@ def add_ip_address(ip_address, ifname):
|
|||||||
|
|
||||||
|
|
||||||
@ovn_bgp_agent.privileged.default.entrypoint
|
@ovn_bgp_agent.privileged.default.entrypoint
|
||||||
def delete_ip_address(ip_address, ifname):
|
def delete_ip_address(ip_address, ifname, prefixlen=None, **kwargs):
|
||||||
ifname = ifname[:15]
|
ifname = ifname[:15]
|
||||||
net = netaddr.IPNetwork(ip_address)
|
net = netaddr.IPNetwork(ip_address)
|
||||||
ip_version = l_net.get_ip_version(ip_address)
|
ip_version = l_net.get_ip_version(ip_address)
|
||||||
address = str(net.ip)
|
address = str(net.ip)
|
||||||
|
if not prefixlen:
|
||||||
prefixlen = 32 if ip_version == 4 else 128
|
prefixlen = 32 if ip_version == 4 else 128
|
||||||
family = common_utils.IP_VERSION_FAMILY_MAP[ip_version]
|
family = common_utils.IP_VERSION_FAMILY_MAP[ip_version]
|
||||||
_run_iproute_addr("delete",
|
_run_iproute_addr("delete",
|
||||||
ifname,
|
ifname,
|
||||||
address=address,
|
address=address,
|
||||||
mask=prefixlen,
|
mask=prefixlen,
|
||||||
family=family)
|
family=family,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
|
||||||
@ovn_bgp_agent.privileged.default.entrypoint
|
@ovn_bgp_agent.privileged.default.entrypoint
|
||||||
|
@ -833,7 +833,32 @@ class TestNBOVNBGPDriver(test_base.TestCase):
|
|||||||
}
|
}
|
||||||
self.nb_bgp_driver.expose_remote_ip(ips, ips_info)
|
self.nb_bgp_driver.expose_remote_ip(ips, ips_info)
|
||||||
|
|
||||||
m_announce_ips.assert_called_once_with(ips)
|
m_announce_ips.assert_called_once_with(ips, ips_info=ips_info)
|
||||||
|
|
||||||
|
@mock.patch.object(bgp_utils, 'announce_ips')
|
||||||
|
def test_expose_remote_ip_vrf(self, m_announce_ips):
|
||||||
|
CONF.set_override('exposing_method', constants.EXPOSE_METHOD_VRF)
|
||||||
|
self.addCleanup(CONF.clear_override, 'exposing_method')
|
||||||
|
|
||||||
|
mock__get_router_port_info_for_ls = mock.patch.object(
|
||||||
|
self.nb_bgp_driver, '_get_router_port_info_for_ls'
|
||||||
|
).start()
|
||||||
|
|
||||||
|
mock__get_router_port_info_for_ls.return_value = {
|
||||||
|
'bridge_device': self.bridge, 'bridge_vlan': None,
|
||||||
|
'via': self.router1_info['ips'],
|
||||||
|
}
|
||||||
|
|
||||||
|
ips = [self.ipv4, self.ipv6]
|
||||||
|
ips_info = {
|
||||||
|
'mac': 'fake-mac',
|
||||||
|
'cidrs': [],
|
||||||
|
'type': constants.OVN_VM_VIF_PORT_TYPE,
|
||||||
|
'logical_switch': 'test-ls'
|
||||||
|
}
|
||||||
|
self.nb_bgp_driver.expose_remote_ip(ips, ips_info)
|
||||||
|
|
||||||
|
m_announce_ips.assert_called_once_with(ips, ips_info=ips_info)
|
||||||
|
|
||||||
@mock.patch.object(driver_utils, 'is_ipv6_gua')
|
@mock.patch.object(driver_utils, 'is_ipv6_gua')
|
||||||
@mock.patch.object(bgp_utils, 'announce_ips')
|
@mock.patch.object(bgp_utils, 'announce_ips')
|
||||||
@ -852,7 +877,32 @@ class TestNBOVNBGPDriver(test_base.TestCase):
|
|||||||
m_gua.side_effect = [False, True]
|
m_gua.side_effect = [False, True]
|
||||||
self.nb_bgp_driver.expose_remote_ip(ips, ips_info)
|
self.nb_bgp_driver.expose_remote_ip(ips, ips_info)
|
||||||
|
|
||||||
m_announce_ips.assert_called_once_with([self.ipv6])
|
m_announce_ips.assert_called_once_with([self.ipv6], ips_info=ips_info)
|
||||||
|
|
||||||
|
def test__get_exposed_ip(self):
|
||||||
|
self.nb_bgp_driver._exposed_ips = {
|
||||||
|
'provider-ls': {'vip': {'bridge_device': self.bridge,
|
||||||
|
'bridge_vlan': None}}}
|
||||||
|
|
||||||
|
ls, info = self.nb_bgp_driver._get_exposed_ip('vip')
|
||||||
|
self.assertEqual('provider-ls', ls)
|
||||||
|
self.assertDictEqual({'bridge_device': self.bridge,
|
||||||
|
'bridge_vlan': None}, info)
|
||||||
|
|
||||||
|
def test__get_router_port_info_for_ls(self):
|
||||||
|
ls = 'provider-ls'
|
||||||
|
self.nb_bgp_driver._exposed_ips = {
|
||||||
|
ls: {'vip': {'bridge_device': self.bridge, 'bridge_vlan': None}}
|
||||||
|
}
|
||||||
|
|
||||||
|
tenant_ls = 'tenant_ls'
|
||||||
|
self.nb_bgp_driver.ovn_local_lrps[tenant_ls] = {'vip'}
|
||||||
|
|
||||||
|
info = self.nb_bgp_driver._get_router_port_info_for_ls(tenant_ls)
|
||||||
|
self.assertDictEqual({'bridge_device': self.bridge,
|
||||||
|
'bridge_vlan': None}, info)
|
||||||
|
self.assertIsNone(self.nb_bgp_driver._get_router_port_info_for_ls(
|
||||||
|
'other_ls'))
|
||||||
|
|
||||||
@mock.patch.object(bgp_utils, 'withdraw_ips')
|
@mock.patch.object(bgp_utils, 'withdraw_ips')
|
||||||
def test_withdraw_remote_ip(self, m_withdraw_ips):
|
def test_withdraw_remote_ip(self, m_withdraw_ips):
|
||||||
@ -865,7 +915,32 @@ class TestNBOVNBGPDriver(test_base.TestCase):
|
|||||||
}
|
}
|
||||||
self.nb_bgp_driver.withdraw_remote_ip(ips, ips_info)
|
self.nb_bgp_driver.withdraw_remote_ip(ips, ips_info)
|
||||||
|
|
||||||
m_withdraw_ips.assert_called_once_with(ips)
|
m_withdraw_ips.assert_called_once_with(ips, ips_info=ips_info)
|
||||||
|
|
||||||
|
@mock.patch.object(bgp_utils, 'withdraw_ips')
|
||||||
|
def test_withdraw_remote_ip_vrf(self, m_withdraw_ips):
|
||||||
|
CONF.set_override('exposing_method', constants.EXPOSE_METHOD_VRF)
|
||||||
|
self.addCleanup(CONF.clear_override, 'exposing_method')
|
||||||
|
|
||||||
|
mock__get_router_port_info_for_ls = mock.patch.object(
|
||||||
|
self.nb_bgp_driver, '_get_router_port_info_for_ls'
|
||||||
|
).start()
|
||||||
|
|
||||||
|
mock__get_router_port_info_for_ls.return_value = {
|
||||||
|
'bridge_device': self.bridge, 'bridge_vlan': None,
|
||||||
|
'via': self.router1_info['ips'],
|
||||||
|
}
|
||||||
|
|
||||||
|
ips = [self.ipv4, self.ipv6]
|
||||||
|
ips_info = {
|
||||||
|
'mac': 'fake-mac',
|
||||||
|
'cidrs': [],
|
||||||
|
'type': constants.OVN_VM_VIF_PORT_TYPE,
|
||||||
|
'logical_switch': 'test-ls'
|
||||||
|
}
|
||||||
|
self.nb_bgp_driver.withdraw_remote_ip(ips, ips_info)
|
||||||
|
|
||||||
|
m_withdraw_ips.assert_called_once_with(ips, ips_info=ips_info)
|
||||||
|
|
||||||
@mock.patch.object(driver_utils, 'is_ipv6_gua')
|
@mock.patch.object(driver_utils, 'is_ipv6_gua')
|
||||||
@mock.patch.object(bgp_utils, 'withdraw_ips')
|
@mock.patch.object(bgp_utils, 'withdraw_ips')
|
||||||
@ -884,7 +959,7 @@ class TestNBOVNBGPDriver(test_base.TestCase):
|
|||||||
m_gua.side_effect = [False, True]
|
m_gua.side_effect = [False, True]
|
||||||
self.nb_bgp_driver.withdraw_remote_ip(ips, ips_info)
|
self.nb_bgp_driver.withdraw_remote_ip(ips, ips_info)
|
||||||
|
|
||||||
m_withdraw_ips.assert_called_once_with([self.ipv6])
|
m_withdraw_ips.assert_called_once_with([self.ipv6], ips_info=ips_info)
|
||||||
|
|
||||||
def test_expose_subnet(self):
|
def test_expose_subnet(self):
|
||||||
ips = ['10.0.0.1/24']
|
ips = ['10.0.0.1/24']
|
||||||
|
122
ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_bgp.py
Normal file
122
ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_bgp.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
# Copyright 2024 team.blue/nl
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from ovn_bgp_agent import constants
|
||||||
|
from ovn_bgp_agent.drivers.openstack.utils import bgp as bgp_utils
|
||||||
|
from ovn_bgp_agent.tests import base as test_base
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class TestEVPN(test_base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestEVPN, self).setUp()
|
||||||
|
|
||||||
|
self.mock_frr = mock.patch.object(bgp_utils, 'frr').start()
|
||||||
|
self.mock_linux_net = mock.patch.object(bgp_utils, 'linux_net').start()
|
||||||
|
|
||||||
|
def _set_exposing_method(self, exposing_method):
|
||||||
|
CONF.set_override('exposing_method', exposing_method)
|
||||||
|
self.addCleanup(CONF.clear_override, 'exposing_method')
|
||||||
|
|
||||||
|
def _test_announce_ips(self, exposing_method):
|
||||||
|
ips = ['10.10.10.1', '10.20.10.1']
|
||||||
|
self._set_exposing_method(exposing_method)
|
||||||
|
|
||||||
|
bgp_utils.announce_ips(list(ips))
|
||||||
|
|
||||||
|
if exposing_method in [constants.EXPOSE_METHOD_VRF]:
|
||||||
|
self.mock_linux_net.add_ips_to_dev.assert_not_called()
|
||||||
|
else:
|
||||||
|
self.mock_linux_net.add_ips_to_dev.assert_called_once_with(
|
||||||
|
CONF.bgp_nic, ips)
|
||||||
|
|
||||||
|
def test_announce_ips_underlay(self):
|
||||||
|
self._test_announce_ips('underlay')
|
||||||
|
|
||||||
|
def test_announce_ips_dynamic(self):
|
||||||
|
self._test_announce_ips('dynamic')
|
||||||
|
|
||||||
|
def test_announce_ips_ovn(self):
|
||||||
|
self._test_announce_ips('ovn')
|
||||||
|
|
||||||
|
def test_announce_ips_vrf(self):
|
||||||
|
self._test_announce_ips('vrf')
|
||||||
|
|
||||||
|
def test_announce_ips_l2vni(self):
|
||||||
|
self._test_announce_ips('l2vni')
|
||||||
|
|
||||||
|
def _test_withdraw_ips(self, exposing_method):
|
||||||
|
ips = ['10.10.10.1', '10.20.10.1']
|
||||||
|
self._set_exposing_method(exposing_method)
|
||||||
|
|
||||||
|
bgp_utils.withdraw_ips(list(ips))
|
||||||
|
|
||||||
|
if exposing_method in [constants.EXPOSE_METHOD_VRF]:
|
||||||
|
self.mock_linux_net.del_ips_from_dev.assert_not_called()
|
||||||
|
else:
|
||||||
|
self.mock_linux_net.del_ips_from_dev.assert_called_once_with(
|
||||||
|
CONF.bgp_nic, ips)
|
||||||
|
|
||||||
|
def test_withdraw_ips_underlay(self):
|
||||||
|
self._test_withdraw_ips('underlay')
|
||||||
|
|
||||||
|
def test_withdraw_ips_dynamic(self):
|
||||||
|
self._test_withdraw_ips('dynamic')
|
||||||
|
|
||||||
|
def test_withdraw_ips_ovn(self):
|
||||||
|
self._test_withdraw_ips('ovn')
|
||||||
|
|
||||||
|
def test_withdraw_ips_vrf(self):
|
||||||
|
self._test_withdraw_ips('vrf')
|
||||||
|
|
||||||
|
def test_withdraw_ips_l2vni(self):
|
||||||
|
self._test_withdraw_ips('l2vni')
|
||||||
|
|
||||||
|
def _test_ensure_base_bgp_configuration(self, exposing_method):
|
||||||
|
self._set_exposing_method(exposing_method)
|
||||||
|
|
||||||
|
bgp_utils.ensure_base_bgp_configuration()
|
||||||
|
|
||||||
|
if exposing_method not in [constants.EXPOSE_METHOD_UNDERLAY,
|
||||||
|
constants.EXPOSE_METHOD_DYNAMIC,
|
||||||
|
constants.EXPOSE_METHOD_OVN]:
|
||||||
|
self.mock_frr.vrf_leak.assert_not_called()
|
||||||
|
self.mock_linux_net.ensure_vrf.assert_not_called()
|
||||||
|
self.mock_linux_net.ensure_ovn_device.assert_not_called()
|
||||||
|
else:
|
||||||
|
self.mock_frr.vrf_leak.assert_called_once()
|
||||||
|
self.mock_linux_net.ensure_vrf.assert_called_once()
|
||||||
|
self.mock_linux_net.ensure_ovn_device.assert_called_once()
|
||||||
|
|
||||||
|
def test_ensure_base_bgp_configuration_underlay(self):
|
||||||
|
self._test_ensure_base_bgp_configuration('underlay')
|
||||||
|
|
||||||
|
def test_ensure_base_bgp_configuration_dynamic(self):
|
||||||
|
self._test_ensure_base_bgp_configuration('dynamic')
|
||||||
|
|
||||||
|
def test_ensure_base_bgp_configuration_ovn(self):
|
||||||
|
self._test_ensure_base_bgp_configuration('ovn')
|
||||||
|
|
||||||
|
def test_ensure_base_bgp_configuration_vrf(self):
|
||||||
|
self._test_ensure_base_bgp_configuration('vrf')
|
||||||
|
|
||||||
|
def test_ensure_base_bgp_configuration_l2vni(self):
|
||||||
|
self._test_ensure_base_bgp_configuration('l2vni')
|
@ -121,6 +121,34 @@ class TestDriverUtils(test_base.TestCase):
|
|||||||
lb = utils.create_row(name='lb-someuuid')
|
lb = utils.create_row(name='lb-someuuid')
|
||||||
self.assertFalse(driver_utils.is_pf_lb(lb))
|
self.assertFalse(driver_utils.is_pf_lb(lb))
|
||||||
|
|
||||||
|
def test_ips_per_version(self):
|
||||||
|
ips = ['10.0.0.1/32', 'fe80::1/128']
|
||||||
|
self.assertDictEqual(driver_utils.ips_per_version(ips), {
|
||||||
|
constants.IP_VERSION_4: '10.0.0.1',
|
||||||
|
constants.IP_VERSION_6: 'fe80::1',
|
||||||
|
})
|
||||||
|
|
||||||
|
# More than 1 ip per version and without prefix
|
||||||
|
ips = ['10.0.0.1/32', '10.0.0.254', 'fe80::1', 'fc00::1/128']
|
||||||
|
self.assertDictEqual(driver_utils.ips_per_version(ips), {
|
||||||
|
constants.IP_VERSION_4: '10.0.0.254',
|
||||||
|
constants.IP_VERSION_6: 'fc00::1',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Only IPv4
|
||||||
|
ips = ['10.0.0.1/32']
|
||||||
|
self.assertDictEqual(driver_utils.ips_per_version(ips), {
|
||||||
|
constants.IP_VERSION_4: '10.0.0.1',
|
||||||
|
constants.IP_VERSION_6: None,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Only IPv6
|
||||||
|
ips = ['fc00::1/128']
|
||||||
|
self.assertDictEqual(driver_utils.ips_per_version(ips), {
|
||||||
|
constants.IP_VERSION_4: None,
|
||||||
|
constants.IP_VERSION_6: 'fc00::1',
|
||||||
|
})
|
||||||
|
|
||||||
def test_get_prefixes_from_ips(self):
|
def test_get_prefixes_from_ips(self):
|
||||||
# IPv4
|
# IPv4
|
||||||
ips = ['192.168.0.1/24', '192.168.0.244/28', '172.13.37.59/27']
|
ips = ['192.168.0.1/24', '192.168.0.244/28', '172.13.37.59/27']
|
||||||
@ -137,3 +165,37 @@ class TestDriverUtils(test_base.TestCase):
|
|||||||
ips = ['172.13.37.59/27', 'ff00::13:37/112']
|
ips = ['172.13.37.59/27', 'ff00::13:37/112']
|
||||||
self.assertListEqual(driver_utils.get_prefixes_from_ips(ips),
|
self.assertListEqual(driver_utils.get_prefixes_from_ips(ips),
|
||||||
['172.13.37.32/27', 'ff00::13:0/112'])
|
['172.13.37.32/27', 'ff00::13:0/112'])
|
||||||
|
|
||||||
|
def test_get_port_vlan_untagged(self):
|
||||||
|
port = utils.create_row(tag=[])
|
||||||
|
self.assertEqual('0', driver_utils.get_port_vlan(port))
|
||||||
|
|
||||||
|
def test_get_port_vlan_tagged(self):
|
||||||
|
port = utils.create_row(tag=[100])
|
||||||
|
self.assertEqual('100', driver_utils.get_port_vlan(port))
|
||||||
|
|
||||||
|
port = utils.create_row(tag=[100, 123])
|
||||||
|
self.assertEqual('100', driver_utils.get_port_vlan(port))
|
||||||
|
|
||||||
|
def test_get_port_vrf_settings(self):
|
||||||
|
row = utils.create_row(external_ids={
|
||||||
|
constants.OVN_EVPN_TYPE_EXT_ID_KEY: constants.OVN_EVPN_TYPE_L3,
|
||||||
|
constants.OVN_EVPN_VNI_EXT_ID_KEY: 1001,
|
||||||
|
})
|
||||||
|
self.assertEqual('l3::1001', driver_utils.get_port_vrf_settings(row))
|
||||||
|
|
||||||
|
def test_get_port_vrf_settings_no_vni(self):
|
||||||
|
row = utils.create_row(external_ids={
|
||||||
|
constants.OVN_EVPN_TYPE_EXT_ID_KEY: constants.OVN_EVPN_TYPE_L3,
|
||||||
|
})
|
||||||
|
self.assertEqual('', driver_utils.get_port_vrf_settings(row))
|
||||||
|
|
||||||
|
def test_get_port_vrf_settings_no_type(self):
|
||||||
|
row = utils.create_row(external_ids={
|
||||||
|
constants.OVN_EVPN_VNI_EXT_ID_KEY: 1001,
|
||||||
|
})
|
||||||
|
self.assertEqual('', driver_utils.get_port_vrf_settings(row))
|
||||||
|
|
||||||
|
def test_get_port_vrf_settings_not_provided(self):
|
||||||
|
row = utils.create_row()
|
||||||
|
self.assertIsNone(driver_utils.get_port_vrf_settings(row))
|
||||||
|
670
ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_evpn.py
Normal file
670
ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_evpn.py
Normal file
@ -0,0 +1,670 @@
|
|||||||
|
# Copyright 2024 team.blue/nl
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from ovn_bgp_agent import constants
|
||||||
|
from ovn_bgp_agent.drivers.openstack.utils import evpn
|
||||||
|
from ovn_bgp_agent import exceptions
|
||||||
|
from ovn_bgp_agent.tests import base as test_base
|
||||||
|
from ovn_bgp_agent.tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class TestEVPN(test_base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestEVPN, self).setUp()
|
||||||
|
CONF.set_override('evpn_local_ip', '127.0.0.1')
|
||||||
|
|
||||||
|
self.mock_ovs = mock.patch.object(evpn, 'ovs').start()
|
||||||
|
self.mock_ovs.get_ovs_patch_port_ofport.return_value = 12
|
||||||
|
|
||||||
|
self.mock_frr = mock.patch.object(evpn, 'frr').start()
|
||||||
|
self.mock_linux_net = mock.patch.object(evpn, 'linux_net').start()
|
||||||
|
|
||||||
|
self.fake_mac = fake_mac = 'fe:12:34:56:89:90'
|
||||||
|
self.mock_linux_net.get_interface_address.return_value = fake_mac
|
||||||
|
|
||||||
|
self._bridge_args = {
|
||||||
|
'ovs_bridge': 'br-ex',
|
||||||
|
'vni': 100,
|
||||||
|
'evpn_opts': {},
|
||||||
|
'mode': constants.OVN_EVPN_TYPE_L3,
|
||||||
|
'ovs_flows': {'br-ex': {}},
|
||||||
|
}
|
||||||
|
self.vrf_name = 'vrf-100'
|
||||||
|
self.vxlan_name = 'vxlan-100'
|
||||||
|
self.bridge_name = 'br-100'
|
||||||
|
|
||||||
|
self.veth_vrf = '_to_be_set_by__create_bridge_and_vlan'
|
||||||
|
self.veth_ovs = '_to_be_set_by__create_bridge_and_vlan'
|
||||||
|
|
||||||
|
# evpn.local_bridges = {}
|
||||||
|
def _reset_evpn_local_bridges(self):
|
||||||
|
evpn.local_bridges = {}
|
||||||
|
|
||||||
|
def _create_bridge(self, **override_args) -> evpn.EvpnBridge:
|
||||||
|
self.addCleanup(self._reset_evpn_local_bridges)
|
||||||
|
args = dict(**self._bridge_args)
|
||||||
|
args.update(**override_args)
|
||||||
|
return evpn.setup(**args)
|
||||||
|
|
||||||
|
def _create_bridge_and_vlan(
|
||||||
|
self, vlan_tag=4094, **bridge_args
|
||||||
|
) -> 'tuple[object, evpn.EvpnBridge, evpn.VlanDev]':
|
||||||
|
port = utils.create_row(
|
||||||
|
name='fake-port-name',
|
||||||
|
tag=[vlan_tag]
|
||||||
|
)
|
||||||
|
|
||||||
|
uuid = str(port.uuid)[0:11]
|
||||||
|
self.veth_vrf = constants.OVN_EVPN_VETH_VRF_UUID_PREFIX + uuid
|
||||||
|
self.veth_ovs = constants.OVN_EVPN_VETH_OVS_UUID_PREFIX + uuid
|
||||||
|
|
||||||
|
evpn_bridge = self._create_bridge(**bridge_args)
|
||||||
|
return port, evpn_bridge, evpn_bridge.connect_vlan(port)
|
||||||
|
|
||||||
|
def test_setup_no_ip(self):
|
||||||
|
CONF.set_override('evpn_local_ip', None)
|
||||||
|
self.assertRaises(exceptions.ConfOptionRequired, self._create_bridge)
|
||||||
|
|
||||||
|
def test_setup(self):
|
||||||
|
evpn_bridge = self._create_bridge()
|
||||||
|
|
||||||
|
self.assertIsInstance(evpn_bridge, evpn.EvpnBridge)
|
||||||
|
self.assertEqual(evpn_bridge.ovs_bridge, 'br-ex')
|
||||||
|
self.assertEqual(evpn_bridge.vni, 100)
|
||||||
|
self.assertEqual(evpn_bridge.vrf_name, self.vrf_name)
|
||||||
|
self.assertEqual(evpn_bridge.bridge_name, self.bridge_name)
|
||||||
|
self.assertEqual(evpn_bridge.vxlan_name, self.vxlan_name)
|
||||||
|
|
||||||
|
other_bridge = self._create_bridge()
|
||||||
|
self.assertEqual(evpn_bridge, other_bridge)
|
||||||
|
|
||||||
|
def test_lookup_vlan_int(self):
|
||||||
|
port = utils.create_row(
|
||||||
|
tag=[4094]
|
||||||
|
)
|
||||||
|
evpn_bridge = self._create_bridge()
|
||||||
|
evpn_bridge.connect_vlan(port)
|
||||||
|
|
||||||
|
bridge = evpn.lookup(self._bridge_args['ovs_bridge'], 4094)
|
||||||
|
self.assertIsInstance(bridge, evpn.EvpnBridge)
|
||||||
|
|
||||||
|
def test_lookup_vlan_str(self):
|
||||||
|
port = utils.create_row(
|
||||||
|
tag=[4094]
|
||||||
|
)
|
||||||
|
evpn_bridge = self._create_bridge()
|
||||||
|
evpn_bridge.connect_vlan(port)
|
||||||
|
|
||||||
|
bridge = evpn.lookup(self._bridge_args['ovs_bridge'], '4094')
|
||||||
|
self.assertIsInstance(bridge, evpn.EvpnBridge)
|
||||||
|
|
||||||
|
def test_lookup_vlan_None(self):
|
||||||
|
port = utils.create_row(
|
||||||
|
tag=[]
|
||||||
|
)
|
||||||
|
evpn_bridge = self._create_bridge()
|
||||||
|
evpn_bridge.connect_vlan(port)
|
||||||
|
|
||||||
|
bridge = evpn.lookup(self._bridge_args['ovs_bridge'], None)
|
||||||
|
self.assertIsInstance(bridge, evpn.EvpnBridge)
|
||||||
|
|
||||||
|
def test_lookup_vlan_unknown(self):
|
||||||
|
self._create_bridge_and_vlan(vlan_tag=123)
|
||||||
|
self._create_bridge_and_vlan(vlan_tag=123, vni=123, ovs_bridge='foo')
|
||||||
|
self._create_bridge_and_vlan(vlan_tag=4094, vni=123, ovs_bridge='foo')
|
||||||
|
self.assertRaises(KeyError, evpn.lookup,
|
||||||
|
self._bridge_args['ovs_bridge'], '4094')
|
||||||
|
|
||||||
|
def test_evpnbridge_setup_l3(self):
|
||||||
|
bridge = self._create_bridge()
|
||||||
|
bridge.setup()
|
||||||
|
|
||||||
|
self.assertTrue(bridge._setup_done)
|
||||||
|
|
||||||
|
# Create pointer for shorter lines.
|
||||||
|
linux_net = self.mock_linux_net
|
||||||
|
|
||||||
|
linux_net.ensure_bridge.assert_called_once_with(self.bridge_name)
|
||||||
|
linux_net.ensure_vxlan.assert_called_once_with(self.vxlan_name, 100,
|
||||||
|
'127.0.0.1', 4789)
|
||||||
|
linux_net.set_master_for_device.assert_has_calls([
|
||||||
|
mock.call(self.vxlan_name, self.bridge_name),
|
||||||
|
mock.call(self.bridge_name, self.vrf_name),
|
||||||
|
])
|
||||||
|
linux_net.disable_learning_vxlan_intf.assert_called_once_with(
|
||||||
|
self.vxlan_name)
|
||||||
|
linux_net.ensure_vrf.assert_called_once_with(self.vrf_name, 100)
|
||||||
|
|
||||||
|
frr = self.mock_frr
|
||||||
|
frr.vrf_reconfigure.assert_called_once_with(mock.ANY, 'add-vrf')
|
||||||
|
|
||||||
|
def test_evpnbridge_setup_l2(self):
|
||||||
|
bridge = self._create_bridge(mode=constants.OVN_EVPN_TYPE_L2)
|
||||||
|
bridge.setup()
|
||||||
|
|
||||||
|
self.assertTrue(bridge._setup_done)
|
||||||
|
|
||||||
|
# Create pointer for shorter lines.
|
||||||
|
linux_net = self.mock_linux_net
|
||||||
|
|
||||||
|
linux_net.ensure_bridge.assert_called_once_with(self.bridge_name)
|
||||||
|
linux_net.ensure_vxlan.assert_called_once_with(self.vxlan_name, 100,
|
||||||
|
'127.0.0.1', 4789)
|
||||||
|
linux_net.set_master_for_device.assert_has_calls([
|
||||||
|
mock.call(self.vxlan_name, self.bridge_name),
|
||||||
|
])
|
||||||
|
linux_net.disable_learning_vxlan_intf.assert_called_once_with(
|
||||||
|
self.vxlan_name)
|
||||||
|
linux_net.ensure_vrf.assert_not_called()
|
||||||
|
|
||||||
|
frr = self.mock_frr
|
||||||
|
frr.vrf_reconfigure.assert_called_once_with(mock.ANY, 'add-vrf')
|
||||||
|
|
||||||
|
def test_evpnbridge_setup_done(self):
|
||||||
|
bridge = self._create_bridge()
|
||||||
|
bridge._setup_done = True
|
||||||
|
bridge.setup()
|
||||||
|
|
||||||
|
self.mock_linux_net.ensure_bridge.assert_not_called()
|
||||||
|
|
||||||
|
def test_evpnbridge_eval_disconnect(self):
|
||||||
|
_, bridge, evpn_vlan = self._create_bridge_and_vlan()
|
||||||
|
|
||||||
|
bridge_disconnect = mock.patch.object(bridge, 'disconnect').start()
|
||||||
|
|
||||||
|
bridge._eval_disconnect()
|
||||||
|
bridge_disconnect.assert_not_called()
|
||||||
|
|
||||||
|
bridge._setup_done = True
|
||||||
|
evpn_vlan._setup_done = True
|
||||||
|
bridge._eval_disconnect()
|
||||||
|
|
||||||
|
bridge_disconnect.assert_not_called()
|
||||||
|
|
||||||
|
evpn_vlan._setup_done = False
|
||||||
|
|
||||||
|
bridge._eval_disconnect()
|
||||||
|
bridge_disconnect.assert_called_once()
|
||||||
|
|
||||||
|
def test_evpnbridge_disconnect(self):
|
||||||
|
bridge = self._create_bridge()
|
||||||
|
bridge._setup_done = True
|
||||||
|
bridge.disconnect()
|
||||||
|
|
||||||
|
calls = [mock.call('br-100'),
|
||||||
|
mock.call('vxlan-100'),
|
||||||
|
mock.call('vrf-100')]
|
||||||
|
self.mock_linux_net.delete_device.assert_has_calls(calls)
|
||||||
|
self.mock_frr.vrf_reconfigure.assert_called_once_with(mock.ANY,
|
||||||
|
action='del-vrf')
|
||||||
|
|
||||||
|
self.assertFalse(bridge._setup_done)
|
||||||
|
|
||||||
|
def test_evpnbridge_connect_vlan_again(self):
|
||||||
|
port, bridge, evpn_vlan = self._create_bridge_and_vlan()
|
||||||
|
|
||||||
|
vlan = bridge.connect_vlan(port)
|
||||||
|
self.assertEqual(vlan, evpn_vlan)
|
||||||
|
|
||||||
|
def test_evpnbridge_get_vlan(self):
|
||||||
|
_, bridge, evpn_vlan = self._create_bridge_and_vlan()
|
||||||
|
self.assertEqual(bridge.get_vlan(4094), evpn_vlan)
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_lladdr_property_calls_setup(self):
|
||||||
|
_, bridge, evpn_vlan = self._create_bridge_and_vlan()
|
||||||
|
|
||||||
|
evpn_vlan_setup = mock.patch.object(evpn_vlan, 'setup').start()
|
||||||
|
self.assertEqual(evpn_vlan.lladdr, self.fake_mac)
|
||||||
|
evpn_vlan_setup.assert_called_once()
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_setup_l2(self):
|
||||||
|
vlan_tag = 4094
|
||||||
|
vlan_tag_str = '4094'
|
||||||
|
_, evpn_bridge, vlan_dev = self._create_bridge_and_vlan(vlan_tag,
|
||||||
|
mode='l2')
|
||||||
|
|
||||||
|
evpn_setup = mock.patch.object(evpn_bridge, 'setup').start()
|
||||||
|
|
||||||
|
vlan_dev.setup()
|
||||||
|
|
||||||
|
evpn_setup.assert_called_once()
|
||||||
|
|
||||||
|
linux_net = self.mock_linux_net
|
||||||
|
linux_net.ensure_veth.assert_called_once_with(self.veth_vrf,
|
||||||
|
self.veth_ovs)
|
||||||
|
self.mock_ovs.add_device_to_ovs_bridge(self.veth_ovs, 'br-ex',
|
||||||
|
vlan_tag=vlan_tag_str)
|
||||||
|
linux_net.set_master_for_device.assert_called_once_with(self.veth_vrf,
|
||||||
|
'br-100')
|
||||||
|
|
||||||
|
linux_net.ensure_arp_ndp_enabled_for_bridge.assert_not_called()
|
||||||
|
linux_net.enable_routing_for_interfaces.assert_not_called()
|
||||||
|
linux_net.enable_proxy_arp.assert_not_called()
|
||||||
|
linux_net.enable_proxy_ndp.assert_not_called()
|
||||||
|
|
||||||
|
self.mock_ovs.ensure_mac_tweak_flows.assert_not_called()
|
||||||
|
self.mock_ovs.remove_extra_ovs_flows.assert_not_called()
|
||||||
|
|
||||||
|
self.assertTrue(vlan_dev._veth_created)
|
||||||
|
self.assertTrue(vlan_dev._setup_done)
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_setup_l3(self, custom_ips=[]):
|
||||||
|
vlan_tag = 4094
|
||||||
|
vlan_tag_str = '4094'
|
||||||
|
_, evpn_bridge, vlan_dev = self._create_bridge_and_vlan(vlan_tag)
|
||||||
|
|
||||||
|
evpn_setup = mock.patch.object(evpn_bridge, 'setup').start()
|
||||||
|
|
||||||
|
if custom_ips:
|
||||||
|
vlan_dev.add_ips(list(custom_ips))
|
||||||
|
|
||||||
|
vlan_dev.setup()
|
||||||
|
|
||||||
|
evpn_setup.assert_called_once()
|
||||||
|
|
||||||
|
linux_net = self.mock_linux_net
|
||||||
|
linux_net.ensure_veth.assert_called_once_with(self.veth_vrf,
|
||||||
|
self.veth_ovs)
|
||||||
|
self.mock_ovs.add_device_to_ovs_bridge(self.veth_ovs, 'br-ex',
|
||||||
|
vlan_tag=vlan_tag_str)
|
||||||
|
linux_net.set_master_for_device.assert_called_once_with(self.veth_vrf,
|
||||||
|
self.vrf_name)
|
||||||
|
linux_net.ensure_arp_ndp_enabled_for_bridge.assert_called_once_with(
|
||||||
|
self.veth_vrf, offset=vlan_tag, vlan_tag=vlan_tag_str)
|
||||||
|
|
||||||
|
linux_net.enable_routing_for_interfaces.assert_called_once_with(
|
||||||
|
self.veth_vrf, 'br-100')
|
||||||
|
|
||||||
|
linux_net.enable_proxy_arp.assert_called_once_with(self.veth_vrf)
|
||||||
|
linux_net.enable_proxy_ndp.assert_called_once_with(self.veth_vrf)
|
||||||
|
|
||||||
|
self.mock_ovs.ensure_mac_tweak_flows.assert_called_once_with(
|
||||||
|
'br-ex', self.fake_mac, [12], constants.OVS_RULE_COOKIE)
|
||||||
|
self.mock_ovs.remove_extra_ovs_flows.assert_called_once_with(
|
||||||
|
mock.ANY, 'br-ex', constants.OVS_RULE_COOKIE)
|
||||||
|
|
||||||
|
self.assertTrue(vlan_dev._veth_created)
|
||||||
|
self.assertTrue(vlan_dev._setup_done)
|
||||||
|
|
||||||
|
if custom_ips:
|
||||||
|
linux_net.add_ips_to_dev.assert_has_calls([
|
||||||
|
mock.call(self.veth_vrf, ips=custom_ips),
|
||||||
|
])
|
||||||
|
|
||||||
|
linux_net.ensure_anycast_mac_for_interface.assert_called_once_with(
|
||||||
|
self.veth_vrf, offset=6557694)
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_setup_l3_with_custom_ips(self):
|
||||||
|
self.test_evpnbridge_vlan_setup_l3(custom_ips=['10.10.10.1'])
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_setup_l3_failed_ovs_call(self):
|
||||||
|
vlan_tag = 4094
|
||||||
|
_, evpn_bridge, vlan_dev = self._create_bridge_and_vlan(vlan_tag)
|
||||||
|
|
||||||
|
mock.patch.object(evpn_bridge, 'setup').start()
|
||||||
|
|
||||||
|
self.mock_ovs.get_ovs_patch_port_ofport.side_effect = KeyError
|
||||||
|
|
||||||
|
vlan_dev.setup()
|
||||||
|
self.assertTrue(vlan_dev._veth_created)
|
||||||
|
self.assertFalse(vlan_dev._setup_done)
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan__eval_disconnect(self):
|
||||||
|
_, _, vlan_dev = self._create_bridge_and_vlan()
|
||||||
|
|
||||||
|
vlan_dev_disconnect = mock.patch.object(vlan_dev, 'disconnect').start()
|
||||||
|
|
||||||
|
vlan_dev._setup_done = True
|
||||||
|
|
||||||
|
vlan_dev._agent_routing_tables_routes = ['entry']
|
||||||
|
vlan_dev._eval_disconnect()
|
||||||
|
vlan_dev_disconnect.assert_not_called()
|
||||||
|
|
||||||
|
vlan_dev._agent_routing_tables_routes = []
|
||||||
|
vlan_dev._eval_disconnect()
|
||||||
|
vlan_dev_disconnect.assert_called_once()
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan__eval_disconnect_not_setup_yet(self):
|
||||||
|
_, _, vlan_dev = self._create_bridge_and_vlan()
|
||||||
|
|
||||||
|
vlan_dev_disconnect = mock.patch.object(vlan_dev, 'disconnect').start()
|
||||||
|
|
||||||
|
vlan_dev._agent_routing_tables_routes = []
|
||||||
|
vlan_dev._eval_disconnect()
|
||||||
|
|
||||||
|
vlan_dev_disconnect.assert_not_called()
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_disconnect(self):
|
||||||
|
_, evpn_bridge, vlan_dev = self._create_bridge_and_vlan()
|
||||||
|
|
||||||
|
evpn_bridge__eval_disconnect = mock.patch.object(
|
||||||
|
evpn_bridge, '_eval_disconnect').start()
|
||||||
|
|
||||||
|
vlan_dev.disconnect()
|
||||||
|
|
||||||
|
evpn_bridge__eval_disconnect.assert_called_once()
|
||||||
|
|
||||||
|
self.mock_ovs.del_device_from_ovs_bridge.assert_called_once_with(
|
||||||
|
self.veth_ovs, 'br-ex')
|
||||||
|
self.mock_linux_net.delete_device.assert_called_once_with(
|
||||||
|
self.veth_vrf)
|
||||||
|
|
||||||
|
self.assertFalse(vlan_dev._veth_created)
|
||||||
|
self.assertFalse(vlan_dev._setup_done)
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_teardown(self):
|
||||||
|
_, evpn_bridge, vlan_dev = self._create_bridge_and_vlan()
|
||||||
|
|
||||||
|
vlan_dev_disconnect = mock.patch.object(
|
||||||
|
vlan_dev, 'disconnect').start()
|
||||||
|
|
||||||
|
vlan_dev.teardown()
|
||||||
|
|
||||||
|
vlan_dev_disconnect.assert_called_once()
|
||||||
|
self.assertNotIn(vlan_dev.vlan_tag, evpn_bridge.vlans)
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_process_dhcp_opts(self):
|
||||||
|
_, _, vlan_dev = self._create_bridge_and_vlan()
|
||||||
|
dhcp_opts = [
|
||||||
|
utils.create_row(cidr='10.10.10.0/24',
|
||||||
|
options={'router': '10.10.10.1'}),
|
||||||
|
utils.create_row(cidr='fe00::/64', options={}),
|
||||||
|
]
|
||||||
|
vlan_dev._setup_done = True
|
||||||
|
vlan_dev.process_dhcp_opts(dhcp_opts)
|
||||||
|
|
||||||
|
ips = {'10.10.10.1'}
|
||||||
|
self.assertSetEqual(vlan_dev._custom_ips, ips)
|
||||||
|
self.mock_linux_net.add_ips_to_dev.assert_called_once_with(
|
||||||
|
self.veth_vrf, ips=list(ips))
|
||||||
|
self.mock_frr.nd_reconfigure.assert_called_once_with(
|
||||||
|
self.veth_vrf, 'fe00::/64', {})
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_process_dhcp_opts_multiple_subnets(self):
|
||||||
|
_, _, vlan_dev = self._create_bridge_and_vlan()
|
||||||
|
dhcp_opts = [
|
||||||
|
utils.create_row(cidr='10.10.10.0/24',
|
||||||
|
options={'router': '10.10.10.1'}),
|
||||||
|
utils.create_row(cidr='10.10.20.0/24',
|
||||||
|
options={'router': '10.10.20.1'}),
|
||||||
|
]
|
||||||
|
vlan_dev._setup_done = True
|
||||||
|
vlan_dev.process_dhcp_opts(dhcp_opts)
|
||||||
|
|
||||||
|
ips = {'10.10.10.1', '10.10.20.1'}
|
||||||
|
self.assertSetEqual(vlan_dev._custom_ips, ips)
|
||||||
|
|
||||||
|
calls = [
|
||||||
|
mock.call(self.veth_vrf, ips=['10.10.10.1']),
|
||||||
|
mock.call(self.veth_vrf, ips=['10.10.20.1']),
|
||||||
|
]
|
||||||
|
self.mock_linux_net.add_ips_to_dev.assert_has_calls(calls)
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_add_route(self, ip='10.10.10.10'):
|
||||||
|
_, _, vlan_dev = self._create_bridge_and_vlan()
|
||||||
|
vlan_dev._setup_done = True
|
||||||
|
|
||||||
|
routing_tables_routes = {self.veth_vrf: []}
|
||||||
|
addr = ip.split('/')[0]
|
||||||
|
mask = None if '/' not in ip else ip.split('/')[1]
|
||||||
|
mac = 'fe:12:34:56:89:12'
|
||||||
|
via = None
|
||||||
|
|
||||||
|
vlan_dev.add_route(routing_tables_routes, ip, mac, via)
|
||||||
|
|
||||||
|
self.assertDictEqual(routing_tables_routes, {self.veth_vrf: [{
|
||||||
|
'ip': '10.10.10.10',
|
||||||
|
'mask': mask,
|
||||||
|
'mac': 'fe:12:34:56:89:12',
|
||||||
|
'via': None,
|
||||||
|
}]})
|
||||||
|
|
||||||
|
self.mock_linux_net.add_ip_route.assert_called_once_with(
|
||||||
|
mock.ANY, addr, 100, self.veth_vrf, mask=mask, via=None)
|
||||||
|
|
||||||
|
self.mock_linux_net.add_ip_nei.assert_called_once_with(
|
||||||
|
addr, 'fe:12:34:56:89:12', self.veth_vrf)
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_add_route_with_prefix(self):
|
||||||
|
self.test_evpnbridge_vlan_add_route(ip='10.10.10.10/32')
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_add_route_l2(self):
|
||||||
|
_, _, vlan_dev = self._create_bridge_and_vlan(mode='l2')
|
||||||
|
vlan_dev._setup_done = True
|
||||||
|
|
||||||
|
routing_tables_routes = {self.veth_vrf: []}
|
||||||
|
ip = '10.10.10.10/32'
|
||||||
|
mac = 'fe:12:34:56:89:12'
|
||||||
|
via = None
|
||||||
|
|
||||||
|
vlan_dev.add_route(routing_tables_routes, ip, mac, via)
|
||||||
|
|
||||||
|
self.mock_linux_net.add_ip_route.assert_not_called()
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_del_route(self, ip='10.10.10.10'):
|
||||||
|
_, _, vlan_dev = self._create_bridge_and_vlan()
|
||||||
|
vlan_dev._setup_done = True
|
||||||
|
|
||||||
|
addr = ip.split('/')[0]
|
||||||
|
routing_tables_routes = {self.veth_vrf: [{
|
||||||
|
'ip': addr,
|
||||||
|
'mask': '32',
|
||||||
|
'mac': 'fe:12:34:56:89:12',
|
||||||
|
'via': '10.10.20.10',
|
||||||
|
}, {
|
||||||
|
'ip': '10.10.10.11',
|
||||||
|
'mask': '32',
|
||||||
|
'mac': 'fe:12:34:56:89:12',
|
||||||
|
'via': '10.10.20.10',
|
||||||
|
}]}
|
||||||
|
mac = 'fe:12:34:56:89:12'
|
||||||
|
vlan_dev__eval_disconnect = mock.patch.object(
|
||||||
|
vlan_dev, '_eval_disconnect').start()
|
||||||
|
|
||||||
|
vlan_dev.del_route(routing_tables_routes, ip, mac)
|
||||||
|
|
||||||
|
self.assertDictEqual(routing_tables_routes, {self.veth_vrf: [{
|
||||||
|
'ip': '10.10.10.11',
|
||||||
|
'mask': '32',
|
||||||
|
'mac': 'fe:12:34:56:89:12',
|
||||||
|
'via': '10.10.20.10',
|
||||||
|
}]})
|
||||||
|
|
||||||
|
self.mock_linux_net.del_ip_route.assert_called_once_with(
|
||||||
|
mock.ANY, addr, 100, self.veth_vrf, mask='32', via='10.10.20.10')
|
||||||
|
|
||||||
|
self.mock_linux_net.del_ip_nei.assert_called_once_with(
|
||||||
|
addr, 'fe:12:34:56:89:12', self.veth_vrf)
|
||||||
|
|
||||||
|
vlan_dev__eval_disconnect.assert_called()
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_del_route_with_prefix(self):
|
||||||
|
self.test_evpnbridge_vlan_del_route('10.10.10.10/32')
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_del_route_no_route_table(self):
|
||||||
|
_, _, vlan_dev = self._create_bridge_and_vlan()
|
||||||
|
vlan_dev._setup_done = True
|
||||||
|
|
||||||
|
addr = '10.10.10.10'
|
||||||
|
routing_tables_routes = {
|
||||||
|
self.veth_vrf: [{
|
||||||
|
'ip': '10.10.10.11',
|
||||||
|
'mask': '32',
|
||||||
|
'mac': 'fe:12:34:56:89:12',
|
||||||
|
'via': '10.10.20.10',
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
mac = 'fe:12:34:56:89:12'
|
||||||
|
vlan_dev__eval_disconnect = mock.patch.object(
|
||||||
|
vlan_dev, '_eval_disconnect').start()
|
||||||
|
|
||||||
|
vlan_dev.del_route(routing_tables_routes, addr, mac)
|
||||||
|
|
||||||
|
self.assertDictEqual(routing_tables_routes, {self.veth_vrf: [{
|
||||||
|
'ip': '10.10.10.11',
|
||||||
|
'mask': '32',
|
||||||
|
'mac': 'fe:12:34:56:89:12',
|
||||||
|
'via': '10.10.20.10',
|
||||||
|
}]})
|
||||||
|
|
||||||
|
self.mock_linux_net.del_ip_route.assert_called_once_with(
|
||||||
|
mock.ANY, addr, 100, self.veth_vrf, mask=None, via=None)
|
||||||
|
|
||||||
|
self.mock_linux_net.del_ip_nei.assert_called_once_with(
|
||||||
|
addr, 'fe:12:34:56:89:12', self.veth_vrf)
|
||||||
|
|
||||||
|
vlan_dev__eval_disconnect.assert_called()
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_del_route_l2(self):
|
||||||
|
_, _, vlan_dev = self._create_bridge_and_vlan(mode='l2')
|
||||||
|
vlan_dev._setup_done = True
|
||||||
|
|
||||||
|
routing_tables_routes = {self.veth_vrf: [{
|
||||||
|
'ip': '10.10.10.10',
|
||||||
|
'mask': '32',
|
||||||
|
'mac': 'fe:12:34:56:89:12',
|
||||||
|
'via': '10.10.20.10',
|
||||||
|
}]}
|
||||||
|
ip = '10.10.10.10'
|
||||||
|
mac = 'fe:12:34:56:89:12'
|
||||||
|
|
||||||
|
vlan_dev.del_route(routing_tables_routes, ip, mac)
|
||||||
|
|
||||||
|
self.assertDictEqual(routing_tables_routes, {self.veth_vrf: [{
|
||||||
|
'ip': '10.10.10.10',
|
||||||
|
'mask': '32',
|
||||||
|
'mac': 'fe:12:34:56:89:12',
|
||||||
|
'via': '10.10.20.10',
|
||||||
|
}]})
|
||||||
|
|
||||||
|
self.mock_linux_net.del_ip_route.assert_not_called()
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_cleanup_excessive_routes(self):
|
||||||
|
_, _, vlan_dev = self._create_bridge_and_vlan()
|
||||||
|
vlan_dev._setup_done = True
|
||||||
|
|
||||||
|
intf_idx = 1337
|
||||||
|
self.mock_linux_net.get_interface_index.return_value = intf_idx
|
||||||
|
|
||||||
|
routes = utils.create_linux_routes([{
|
||||||
|
'_attrs': [
|
||||||
|
('RTA_DST', '198.51.100.0'), ('RTA_OIF', intf_idx),
|
||||||
|
('RTA_GATEWAY', '100.64.0.102')
|
||||||
|
],
|
||||||
|
'dst_len': 28, 'type': 1,
|
||||||
|
}, {
|
||||||
|
'attrs': [('RTA_DST', '198.51.100.136'), ('RTA_OIF', intf_idx)],
|
||||||
|
'dst_len': 32, 'type': 1,
|
||||||
|
}, {
|
||||||
|
'attrs': [('RTA_DST', '198.51.100.158'), ('RTA_OIF', intf_idx)],
|
||||||
|
'dst_len': 32, 'type': 1,
|
||||||
|
}])
|
||||||
|
self.mock_linux_net._get_table_routes.return_value = routes
|
||||||
|
|
||||||
|
routing_tables_routes = {self.veth_vrf: [{
|
||||||
|
'ip': '198.51.100.0',
|
||||||
|
'mask': '28',
|
||||||
|
'mac': 'fe:12:34:56:89:12',
|
||||||
|
'via': '100.64.0.102',
|
||||||
|
}]}
|
||||||
|
del_route = mock.patch.object(vlan_dev, 'del_route').start()
|
||||||
|
|
||||||
|
vlan_dev.cleanup_excessive_routes(routing_tables_routes)
|
||||||
|
|
||||||
|
calls = [
|
||||||
|
mock.call(mock.ANY, '198.51.100.136'),
|
||||||
|
mock.call(mock.ANY, '198.51.100.158'),
|
||||||
|
]
|
||||||
|
del_route.assert_has_calls(calls, any_order=True)
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_cleanup_excessive_routes_in_sync(self):
|
||||||
|
_, _, vlan_dev = self._create_bridge_and_vlan()
|
||||||
|
vlan_dev._setup_done = True
|
||||||
|
|
||||||
|
intf_idx = 1337
|
||||||
|
self.mock_linux_net.get_interface_index.return_value = intf_idx
|
||||||
|
|
||||||
|
routes = utils.create_linux_routes([{
|
||||||
|
'_attrs': [
|
||||||
|
('RTA_DST', '198.51.100.0'), ('RTA_OIF', intf_idx),
|
||||||
|
('RTA_GATEWAY', '100.64.0.102')
|
||||||
|
],
|
||||||
|
'dst_len': 28, 'type': 1,
|
||||||
|
}])
|
||||||
|
self.mock_linux_net._get_table_routes.return_value = routes
|
||||||
|
|
||||||
|
routing_tables_routes = {self.veth_vrf: [{
|
||||||
|
'ip': '198.51.100.0',
|
||||||
|
'mask': '28',
|
||||||
|
'mac': 'fe:12:34:56:89:12',
|
||||||
|
'via': '100.64.0.102',
|
||||||
|
}]}
|
||||||
|
del_route = mock.patch.object(vlan_dev, 'del_route').start()
|
||||||
|
|
||||||
|
vlan_dev.cleanup_excessive_routes(routing_tables_routes)
|
||||||
|
del_route.assert_not_called()
|
||||||
|
|
||||||
|
def test_evpnbridge_vlan_cleanup_excessive_routes_not_setup_yet(self):
|
||||||
|
_, _, vlan_dev = self._create_bridge_and_vlan()
|
||||||
|
vlan_dev.cleanup_excessive_routes({})
|
||||||
|
self.mock_linux_net._get_table_routes.assert_not_called()
|
||||||
|
|
||||||
|
def test_evpn__find_route_info(self):
|
||||||
|
result = evpn._find_route_info([
|
||||||
|
{'ip': '198.51.100.136', 'mask': '32', 'mac': None, 'via': None},
|
||||||
|
{'ip': '198.51.100.158', 'mask': '24', 'mac': None, 'via': None},
|
||||||
|
{'ip': '127.0.0.1', 'mask': '8', 'mac': None, 'via': None},
|
||||||
|
{'ip': '198.51.100.0', 'mask': '28', 'mac': None, 'via': None},
|
||||||
|
], '127.0.0.1')
|
||||||
|
self.assertDictEqual(result, {'ip': '127.0.0.1', 'mask': '8',
|
||||||
|
'mac': None, 'via': None})
|
||||||
|
|
||||||
|
def test_evpn__find_route_info_not_found(self):
|
||||||
|
result = evpn._find_route_info([], '127.0.0.1')
|
||||||
|
self.assertDictEqual(result, {'ip': '127.0.0.1', 'mask': None,
|
||||||
|
'mac': None, 'via': None})
|
||||||
|
|
||||||
|
def test_evpn__ensure_list(self):
|
||||||
|
self.assertListEqual(evpn._ensure_list(None), [])
|
||||||
|
self.assertListEqual(evpn._ensure_list('aa'), ['aa'])
|
||||||
|
self.assertListEqual(evpn._ensure_list(['aa']), ['aa'])
|
||||||
|
self.assertSetEqual(evpn._ensure_list({'aa'}), {'aa'})
|
||||||
|
self.assertTupleEqual(evpn._ensure_list(('a', 'b',)), ('a', 'b'))
|
||||||
|
|
||||||
|
def test__offset_for_vni_and_vlan(self):
|
||||||
|
vni = 100
|
||||||
|
vlan = 100
|
||||||
|
exp = int(('%x' % vni).zfill(6) + ('%x' % vlan).zfill(4), 16)
|
||||||
|
self.assertEqual(exp, evpn._offset_for_vni_and_vlan(vni, vlan))
|
||||||
|
|
||||||
|
vni = 16777214
|
||||||
|
vlan = 4094
|
||||||
|
exp = int(('%x' % vni).zfill(6) + ('%x' % vlan).zfill(4), 16)
|
||||||
|
self.assertEqual(exp, evpn._offset_for_vni_and_vlan(vni, vlan))
|
||||||
|
|
||||||
|
# Below value is 1 too much, so it is being modulo'd, and should be
|
||||||
|
# calculated to 0
|
||||||
|
vni = 16777215
|
||||||
|
vlan = 4095
|
||||||
|
exp = int(('%x' % vni).zfill(6) + ('%x' % vlan).zfill(4), 16)
|
||||||
|
self.assertNotEqual(exp, evpn._offset_for_vni_and_vlan(vni, vlan))
|
||||||
|
self.assertEqual(0, evpn._offset_for_vni_and_vlan(vni, vlan))
|
@ -93,6 +93,37 @@ class TestFrr(test_base.TestCase):
|
|||||||
self._test_vrf_reconfigure(add_vrf=False)
|
self._test_vrf_reconfigure(add_vrf=False)
|
||||||
|
|
||||||
def test_vrf_reconfigure_unknown_action(self):
|
def test_vrf_reconfigure_unknown_action(self):
|
||||||
frr_utils.vrf_reconfigure('fake-evpn-info', 'non-existing-action')
|
frr_utils.vrf_reconfigure({'fake': 'evpn-info'}, 'non-existing-action')
|
||||||
# Assert run_vtysh_command() wasn't called
|
# Assert run_vtysh_command() wasn't called
|
||||||
self.assertFalse(self.mock_vtysh.run_vtysh_config.called)
|
self.assertFalse(self.mock_vtysh.run_vtysh_config.called)
|
||||||
|
|
||||||
|
@mock.patch.object(tempfile, 'NamedTemporaryFile')
|
||||||
|
def _test_nd_reconfigure(self, mock_tf, stateless=False):
|
||||||
|
interface = 'veth-vrf-100'
|
||||||
|
prefix = '2a04::/64'
|
||||||
|
opts = {
|
||||||
|
'dhcpv6_stateless': str(stateless),
|
||||||
|
'dns_server': '{2001:4860:4860::8888, 2001:4860:4860::8844}',
|
||||||
|
}
|
||||||
|
|
||||||
|
frr_utils.nd_reconfigure(interface, prefix, opts)
|
||||||
|
|
||||||
|
write_arg = mock_tf.return_value.write.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
self.assertIn("ipv6 nd managed-config-flag", write_arg)
|
||||||
|
self.assertIn("ipv6 nd rdnss 2001:4860:4860::8888", write_arg)
|
||||||
|
self.assertIn("ipv6 nd rdnss 2001:4860:4860::8844", write_arg)
|
||||||
|
self.assertIn("ipv6 nd prefix 2a04::/64", write_arg)
|
||||||
|
if not stateless:
|
||||||
|
self.assertIn('ipv6 nd prefix 2a04::/64 no-autoconfig', write_arg)
|
||||||
|
|
||||||
|
self.mock_vtysh.run_vtysh_config.assert_called_once_with(
|
||||||
|
mock_tf.return_value.name)
|
||||||
|
# Assert the file was closed
|
||||||
|
mock_tf.return_value.close.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_nd_reconfigure_statefull(self):
|
||||||
|
self._test_nd_reconfigure()
|
||||||
|
|
||||||
|
def test_nd_reconfigure_stateless(self):
|
||||||
|
self._test_nd_reconfigure(stateless=True)
|
||||||
|
@ -66,6 +66,7 @@ class TestOvsdbNbOvnIdl(test_base.TestCase):
|
|||||||
self.assertEqual(tag, ret)
|
self.assertEqual(tag, ret)
|
||||||
self.nb_idl.db_find_rows.assert_called_once_with(
|
self.nb_idl.db_find_rows.assert_called_once_with(
|
||||||
'Logical_Switch_Port',
|
'Logical_Switch_Port',
|
||||||
|
('options', '=', {'network_name': network_name}),
|
||||||
('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE))
|
('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE))
|
||||||
|
|
||||||
def test_ls_has_virtual_ports(self):
|
def test_ls_has_virtual_ports(self):
|
||||||
|
@ -18,11 +18,13 @@ from unittest import mock
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from ovn_bgp_agent import constants
|
from ovn_bgp_agent import constants
|
||||||
|
from ovn_bgp_agent.drivers.openstack.utils import evpn as evpn_utils
|
||||||
from ovn_bgp_agent.drivers.openstack.utils import ovn as ovn_utils
|
from ovn_bgp_agent.drivers.openstack.utils import ovn as ovn_utils
|
||||||
from ovn_bgp_agent.drivers.openstack.utils import ovs as ovs_utils
|
from ovn_bgp_agent.drivers.openstack.utils import ovs as ovs_utils
|
||||||
from ovn_bgp_agent.drivers.openstack.utils import wire
|
from ovn_bgp_agent.drivers.openstack.utils import wire
|
||||||
from ovn_bgp_agent import exceptions as agent_exc
|
from ovn_bgp_agent import exceptions as agent_exc
|
||||||
from ovn_bgp_agent.tests import base as test_base
|
from ovn_bgp_agent.tests import base as test_base
|
||||||
|
from ovn_bgp_agent.tests import utils as test_utils
|
||||||
from ovn_bgp_agent.utils import linux_net
|
from ovn_bgp_agent.utils import linux_net
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -37,7 +39,10 @@ class TestWire(test_base.TestCase):
|
|||||||
self.ovs_idl = mock.Mock()
|
self.ovs_idl = mock.Mock()
|
||||||
|
|
||||||
# Helper variables that are used across multiple methods
|
# Helper variables that are used across multiple methods
|
||||||
self.bridge_mappings = 'datacentre:br-ex'
|
self.bridge_mappings = ['datacentre:br-ex']
|
||||||
|
|
||||||
|
self.ovs_idl.get_ovn_bridge_mappings.return_value = (
|
||||||
|
self.bridge_mappings)
|
||||||
|
|
||||||
# Monkey-patch parent class methods
|
# Monkey-patch parent class methods
|
||||||
self.nb_idl.ls_add = mock.Mock()
|
self.nb_idl.ls_add = mock.Mock()
|
||||||
@ -63,21 +68,82 @@ class TestWire(test_base.TestCase):
|
|||||||
ovn_idl=self.nb_idl)
|
ovn_idl=self.nb_idl)
|
||||||
mock_ovn.assert_called_once_with(self.ovs_idl, self.nb_idl)
|
mock_ovn.assert_called_once_with(self.ovs_idl, self.nb_idl)
|
||||||
|
|
||||||
@mock.patch.object(wire, '_ensure_base_wiring_config_underlay')
|
@mock.patch.object(wire, '_ensure_base_wiring_config_evpn')
|
||||||
@mock.patch.object(wire, '_ensure_base_wiring_config_ovn')
|
def test_ensure_base_wiring_config_evpn(self, mock_evpn):
|
||||||
def test_ensure_base_wiring_config_not_implemeneted(self, mock_ovn,
|
|
||||||
mock_underlay):
|
|
||||||
CONF.set_override('exposing_method', 'vrf')
|
CONF.set_override('exposing_method', 'vrf')
|
||||||
self.addCleanup(CONF.clear_override, 'exposing_method')
|
self.addCleanup(CONF.clear_override, 'exposing_method')
|
||||||
|
|
||||||
wire.ensure_base_wiring_config(self.sb_idl, self.ovs_idl,
|
wire.ensure_base_wiring_config(self.nb_idl, self.ovs_idl,
|
||||||
ovn_idl=self.nb_idl)
|
ovn_idl=self.nb_idl)
|
||||||
|
mock_evpn.assert_called_once_with(self.nb_idl, self.ovs_idl)
|
||||||
|
|
||||||
|
@mock.patch.object(wire, '_ensure_base_wiring_config_underlay')
|
||||||
|
@mock.patch.object(wire, '_ensure_base_wiring_config_ovn')
|
||||||
|
def test_ensure_base_wiring_config_not_implemented(self, mock_ovn,
|
||||||
|
mock_underlay):
|
||||||
|
CONF.set_override('exposing_method', 'dynamic')
|
||||||
|
self.addCleanup(CONF.clear_override, 'exposing_method')
|
||||||
|
|
||||||
|
self.assertRaises(agent_exc.UnsupportedWiringConfig,
|
||||||
|
wire.ensure_base_wiring_config,
|
||||||
|
self.sb_idl, self.ovs_idl, ovn_idl=self.nb_idl)
|
||||||
mock_ovn.assert_not_called()
|
mock_ovn.assert_not_called()
|
||||||
mock_underlay.assert_not_called()
|
mock_underlay.assert_not_called()
|
||||||
|
|
||||||
def test__ensure_base_wiring_config_ovn(self):
|
def test__ensure_base_wiring_config_ovn(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@mock.patch.object(linux_net, 'get_interface_address')
|
||||||
|
@mock.patch.object(ovs_utils, 'get_ovs_patch_ports_info')
|
||||||
|
def test__ensure_base_wiring_config_evpn(self, m_get_ovs_patch_ports_info,
|
||||||
|
m_get_interface_address):
|
||||||
|
localnet_ports = [test_utils.create_row(
|
||||||
|
tag=[4096],
|
||||||
|
)]
|
||||||
|
ports = localnet_ports + [test_utils.create_row(
|
||||||
|
dhcpv4_options=[test_utils.create_row()],
|
||||||
|
dhcpv6_options=[],
|
||||||
|
)]
|
||||||
|
get_localnet_ports_by_network_name = mock.patch.object(
|
||||||
|
self.nb_idl, 'get_localnet_ports_by_network_name').start()
|
||||||
|
get_localnet_ports_by_network_name.return_value = localnet_ports
|
||||||
|
|
||||||
|
provnets = [test_utils.create_row(
|
||||||
|
name='fake-provnet',
|
||||||
|
external_ids={
|
||||||
|
constants.OVN_EVPN_VNI_EXT_ID_KEY: 100,
|
||||||
|
},
|
||||||
|
ports=ports,
|
||||||
|
)]
|
||||||
|
get_bgpvpn_networks_for_ports = mock.patch.object(
|
||||||
|
self.nb_idl, 'get_bgpvpn_networks_for_ports').start()
|
||||||
|
get_bgpvpn_networks_for_ports.return_value = provnets
|
||||||
|
|
||||||
|
CONF.set_override('evpn_local_ip', '127.0.0.1')
|
||||||
|
self.addCleanup(CONF.clear_override, 'evpn_local_ip')
|
||||||
|
|
||||||
|
vlan_dev = mock.MagicMock()
|
||||||
|
|
||||||
|
evpn_bridge = mock.MagicMock()
|
||||||
|
evpn_bridge.connect_vlan.return_value = vlan_dev
|
||||||
|
|
||||||
|
evpn_setup = mock.patch.object(evpn_utils, 'setup').start()
|
||||||
|
evpn_setup.return_value = evpn_bridge
|
||||||
|
|
||||||
|
wire._ensure_base_wiring_config_evpn(self.nb_idl, self.ovs_idl)
|
||||||
|
|
||||||
|
evpn_setup.assert_called_with(ovs_bridge='br-ex',
|
||||||
|
vni=100,
|
||||||
|
evpn_opts={'route_targets': [],
|
||||||
|
'route_distinguishers': [],
|
||||||
|
'export_targets': [],
|
||||||
|
'import_targets': []},
|
||||||
|
mode=constants.OVN_EVPN_TYPE_L3,
|
||||||
|
ovs_flows=mock.ANY)
|
||||||
|
|
||||||
|
evpn_bridge.connect_vlan.assert_called_with(ports[0])
|
||||||
|
vlan_dev.process_dhcp_opts.assert_called()
|
||||||
|
|
||||||
def test__ensure_ovn_router(self):
|
def test__ensure_ovn_router(self):
|
||||||
wire._ensure_ovn_router(self.nb_idl)
|
wire._ensure_ovn_router(self.nb_idl)
|
||||||
self.nb_idl.lr_add.assert_called_once_with(
|
self.nb_idl.lr_add.assert_called_once_with(
|
||||||
@ -272,9 +338,34 @@ class TestWire(test_base.TestCase):
|
|||||||
routing_tables_routes)
|
routing_tables_routes)
|
||||||
self.assertTrue(ret)
|
self.assertTrue(ret)
|
||||||
|
|
||||||
|
def test_cleanup_wiring_evpn(self):
|
||||||
|
CONF.set_override('exposing_method', 'vrf')
|
||||||
|
self.addCleanup(CONF.clear_override, 'exposing_method')
|
||||||
|
|
||||||
|
vlan_dev = mock.MagicMock()
|
||||||
|
evpn_bridge = mock.MagicMock()
|
||||||
|
evpn_bridge.get_vlan.return_value = vlan_dev
|
||||||
|
ovs_flows = {
|
||||||
|
'foo': {
|
||||||
|
'evpn': {
|
||||||
|
'4096': evpn_bridge,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exposed_ips = {}
|
||||||
|
routing_tables = {}
|
||||||
|
routing_tables_routes = {}
|
||||||
|
ret = wire.cleanup_wiring(self.sb_idl, self.bridge_mappings, ovs_flows,
|
||||||
|
exposed_ips, routing_tables,
|
||||||
|
routing_tables_routes)
|
||||||
|
|
||||||
|
evpn_bridge.get_vlan.assert_called_with('4096')
|
||||||
|
vlan_dev.cleanup_excessive_routes.assert_called()
|
||||||
|
self.assertTrue(ret)
|
||||||
|
|
||||||
@mock.patch.object(wire, '_cleanup_wiring_underlay')
|
@mock.patch.object(wire, '_cleanup_wiring_underlay')
|
||||||
def test_cleanup_wiring_not_implemeneted(self, mock_underlay):
|
def test_cleanup_wiring_not_implemeneted(self, mock_underlay):
|
||||||
CONF.set_override('exposing_method', 'vrf')
|
CONF.set_override('exposing_method', 'dynamic')
|
||||||
self.addCleanup(CONF.clear_override, 'exposing_method')
|
self.addCleanup(CONF.clear_override, 'exposing_method')
|
||||||
|
|
||||||
ovs_flows = {}
|
ovs_flows = {}
|
||||||
@ -325,10 +416,68 @@ class TestWire(test_base.TestCase):
|
|||||||
ovn_idl=self.nb_idl)
|
ovn_idl=self.nb_idl)
|
||||||
mock_ovn.assert_called_once_with(self.nb_idl, port_ips, mac)
|
mock_ovn.assert_called_once_with(self.nb_idl, port_ips, mac)
|
||||||
|
|
||||||
|
def test_wire_provider_port_evpn(self):
|
||||||
|
CONF.set_override('exposing_method', 'vrf')
|
||||||
|
self.addCleanup(CONF.clear_override, 'exposing_method')
|
||||||
|
|
||||||
|
routing_tables_routes = {}
|
||||||
|
ovs_flows = {}
|
||||||
|
port_ips = ['10.10.10.1']
|
||||||
|
bridge_device = 'fake-bridge'
|
||||||
|
bridge_vlan = '101'
|
||||||
|
localnet = 'fake-localnet'
|
||||||
|
routing_table = 5
|
||||||
|
proxy_cidrs = []
|
||||||
|
mac = 'fake-mac'
|
||||||
|
|
||||||
|
vlan_dev = mock.MagicMock()
|
||||||
|
|
||||||
|
evpn_bridge = mock.MagicMock()
|
||||||
|
evpn_bridge.get_vlan.return_value = vlan_dev
|
||||||
|
|
||||||
|
evpn_lookup = mock.patch.object(evpn_utils, 'lookup').start()
|
||||||
|
evpn_lookup.return_value = evpn_bridge
|
||||||
|
|
||||||
|
ret = wire.wire_provider_port(routing_tables_routes, ovs_flows,
|
||||||
|
port_ips, bridge_device, bridge_vlan,
|
||||||
|
localnet, routing_table, proxy_cidrs,
|
||||||
|
mac=mac, ovn_idl=self.nb_idl)
|
||||||
|
self.assertTrue(ret)
|
||||||
|
|
||||||
|
evpn_lookup.assert_called_once_with(bridge_device, bridge_vlan)
|
||||||
|
evpn_bridge.get_vlan.assert_called_once_with(bridge_vlan)
|
||||||
|
vlan_dev.add_route.assert_called_with(routing_tables_routes,
|
||||||
|
port_ips[0], mac, via=None)
|
||||||
|
|
||||||
|
def test_wire_provider_port_evpn_unconfigured(self):
|
||||||
|
CONF.set_override('exposing_method', 'vrf')
|
||||||
|
self.addCleanup(CONF.clear_override, 'exposing_method')
|
||||||
|
|
||||||
|
routing_tables_routes = {}
|
||||||
|
ovs_flows = {}
|
||||||
|
port_ips = ['10.10.10.1']
|
||||||
|
bridge_device = 'fake-bridge'
|
||||||
|
bridge_vlan = '101'
|
||||||
|
localnet = 'fake-localnet'
|
||||||
|
routing_table = 5
|
||||||
|
proxy_cidrs = []
|
||||||
|
mac = 'fake-mac'
|
||||||
|
|
||||||
|
evpn_lookup = mock.patch.object(evpn_utils, 'lookup').start()
|
||||||
|
evpn_lookup.side_effect = KeyError
|
||||||
|
|
||||||
|
ret = wire.wire_provider_port(routing_tables_routes, ovs_flows,
|
||||||
|
port_ips, bridge_device, bridge_vlan,
|
||||||
|
localnet, routing_table, proxy_cidrs,
|
||||||
|
mac=mac, ovn_idl=self.nb_idl)
|
||||||
|
self.assertIsNone(ret)
|
||||||
|
|
||||||
@mock.patch.object(wire, '_wire_provider_port_underlay')
|
@mock.patch.object(wire, '_wire_provider_port_underlay')
|
||||||
@mock.patch.object(wire, '_wire_provider_port_ovn')
|
@mock.patch.object(wire, '_wire_provider_port_ovn')
|
||||||
def test_wire_provider_port_not_implemented(self, mock_ovn, mock_underlay):
|
@mock.patch.object(wire, '_wire_provider_port_evpn')
|
||||||
CONF.set_override('exposing_method', 'vrf')
|
def test_wire_provider_port_not_implemented(self, mock_evpn, mock_ovn,
|
||||||
|
mock_underlay):
|
||||||
|
CONF.set_override('exposing_method', 'dynamic')
|
||||||
self.addCleanup(CONF.clear_override, 'exposing_method')
|
self.addCleanup(CONF.clear_override, 'exposing_method')
|
||||||
|
|
||||||
routing_tables_routes = {}
|
routing_tables_routes = {}
|
||||||
@ -344,6 +493,7 @@ class TestWire(test_base.TestCase):
|
|||||||
bridge_device, bridge_vlan, localnet,
|
bridge_device, bridge_vlan, localnet,
|
||||||
routing_table, proxy_cidrs)
|
routing_table, proxy_cidrs)
|
||||||
|
|
||||||
|
mock_evpn.assert_not_called()
|
||||||
mock_ovn.assert_not_called()
|
mock_ovn.assert_not_called()
|
||||||
mock_underlay.assert_not_called()
|
mock_underlay.assert_not_called()
|
||||||
|
|
||||||
@ -410,10 +560,8 @@ class TestWire(test_base.TestCase):
|
|||||||
proxy_cidrs, ovn_idl=self.nb_idl)
|
proxy_cidrs, ovn_idl=self.nb_idl)
|
||||||
mock_ovn.assert_called_once_with(self.nb_idl, port_ips)
|
mock_ovn.assert_called_once_with(self.nb_idl, port_ips)
|
||||||
|
|
||||||
@mock.patch.object(wire, '_unwire_provider_port_underlay')
|
@mock.patch.object(wire, '_unwire_provider_port_evpn')
|
||||||
@mock.patch.object(wire, '_unwire_provider_port_ovn')
|
def test_unwire_provider_port_evpn(self, mock_evpn):
|
||||||
def test_unwire_provider_port_not_implemented(self, mock_ovn,
|
|
||||||
mock_underlay):
|
|
||||||
CONF.set_override('exposing_method', 'vrf')
|
CONF.set_override('exposing_method', 'vrf')
|
||||||
self.addCleanup(CONF.clear_override, 'exposing_method')
|
self.addCleanup(CONF.clear_override, 'exposing_method')
|
||||||
|
|
||||||
@ -424,12 +572,72 @@ class TestWire(test_base.TestCase):
|
|||||||
routing_table = 5
|
routing_table = 5
|
||||||
proxy_cidrs = []
|
proxy_cidrs = []
|
||||||
|
|
||||||
|
wire.unwire_provider_port(routing_tables_routes, port_ips,
|
||||||
|
bridge_device, bridge_vlan, routing_table,
|
||||||
|
proxy_cidrs, lladdr='boo')
|
||||||
|
mock_evpn.assert_called_once_with(routing_tables_routes, port_ips,
|
||||||
|
bridge_device, bridge_vlan, 'boo')
|
||||||
|
|
||||||
|
@mock.patch.object(wire, '_unwire_provider_port_underlay')
|
||||||
|
@mock.patch.object(wire, '_unwire_provider_port_ovn')
|
||||||
|
@mock.patch.object(wire, '_unwire_provider_port_evpn')
|
||||||
|
def test_unwire_provider_port_not_implemented(self, mock_evpn, mock_ovn,
|
||||||
|
mock_underlay):
|
||||||
|
CONF.set_override('exposing_method', 'dynamic')
|
||||||
|
self.addCleanup(CONF.clear_override, 'exposing_method')
|
||||||
|
|
||||||
|
routing_tables_routes = {}
|
||||||
|
port_ips = []
|
||||||
|
bridge_device = 'fake-bridge'
|
||||||
|
bridge_vlan = '101'
|
||||||
|
routing_table = 5
|
||||||
|
proxy_cidrs = []
|
||||||
|
|
||||||
wire.unwire_provider_port(routing_tables_routes, port_ips,
|
wire.unwire_provider_port(routing_tables_routes, port_ips,
|
||||||
bridge_device, bridge_vlan, routing_table,
|
bridge_device, bridge_vlan, routing_table,
|
||||||
proxy_cidrs)
|
proxy_cidrs)
|
||||||
|
mock_evpn.assert_not_called()
|
||||||
mock_ovn.assert_not_called()
|
mock_ovn.assert_not_called()
|
||||||
mock_underlay.assert_not_called()
|
mock_underlay.assert_not_called()
|
||||||
|
|
||||||
|
def test__unwire_provider_port_evpn(self):
|
||||||
|
routing_tables_routes = {}
|
||||||
|
port_ips = ['10.10.10.1']
|
||||||
|
bridge_device = 'fake-bridge'
|
||||||
|
bridge_vlan = '101'
|
||||||
|
lladdr = 'boo'
|
||||||
|
|
||||||
|
vlan_dev = mock.MagicMock()
|
||||||
|
|
||||||
|
evpn_bridge = mock.MagicMock()
|
||||||
|
evpn_bridge.get_vlan.return_value = vlan_dev
|
||||||
|
|
||||||
|
evpn_lookup = mock.patch.object(evpn_utils, 'lookup').start()
|
||||||
|
evpn_lookup.return_value = evpn_bridge
|
||||||
|
|
||||||
|
ret = wire._unwire_provider_port_evpn(routing_tables_routes, port_ips,
|
||||||
|
bridge_device, bridge_vlan,
|
||||||
|
lladdr)
|
||||||
|
self.assertTrue(ret)
|
||||||
|
|
||||||
|
vlan_dev.del_route.assert_called_with(routing_tables_routes,
|
||||||
|
port_ips[0], lladdr)
|
||||||
|
|
||||||
|
def test__unwire_provider_port_evpn_unconfigured(self):
|
||||||
|
routing_tables_routes = {}
|
||||||
|
port_ips = ['10.10.10.1']
|
||||||
|
bridge_device = 'fake-bridge'
|
||||||
|
bridge_vlan = '101'
|
||||||
|
lladdr = 'boo'
|
||||||
|
|
||||||
|
evpn_lookup = mock.patch.object(evpn_utils, 'lookup').start()
|
||||||
|
evpn_lookup.side_effect = KeyError
|
||||||
|
|
||||||
|
ret = wire._unwire_provider_port_evpn(routing_tables_routes, port_ips,
|
||||||
|
bridge_device, bridge_vlan,
|
||||||
|
lladdr)
|
||||||
|
self.assertIsNone(ret)
|
||||||
|
|
||||||
@mock.patch.object(wire, '_execute_commands')
|
@mock.patch.object(wire, '_execute_commands')
|
||||||
def test__unwire_provider_port_ovn(self, m_cmds):
|
def test__unwire_provider_port_ovn(self, m_cmds):
|
||||||
port_ips = ['1.1.1.1']
|
port_ips = ['1.1.1.1']
|
||||||
|
@ -103,6 +103,11 @@ class TestOVNLBEvent(test_base.TestCase):
|
|||||||
['192.168.1.50', '172.24.4.5'])
|
['192.168.1.50', '172.24.4.5'])
|
||||||
|
|
||||||
|
|
||||||
|
class FakeLogicalSwitchChassisEvent(base_watcher.LogicalSwitchChassisEvent):
|
||||||
|
def run(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FakeLSPChassisEvent(base_watcher.LSPChassisEvent):
|
class FakeLSPChassisEvent(base_watcher.LSPChassisEvent):
|
||||||
def run(self):
|
def run(self):
|
||||||
pass
|
pass
|
||||||
|
@ -579,6 +579,60 @@ class TestLogicalSwitchPortFIPDeleteEvent(test_base.TestCase):
|
|||||||
self.agent.withdraw_fip.assert_not_called()
|
self.agent.withdraw_fip.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
class TestLogicalSwitchUpdateEvent(test_base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestLogicalSwitchUpdateEvent, self).setUp()
|
||||||
|
self.agent = mock.Mock()
|
||||||
|
self.event = nb_bgp_watcher.LogicalSwitchUpdateEvent(
|
||||||
|
self.agent)
|
||||||
|
self.row = utils.create_row(external_ids={
|
||||||
|
constants.OVN_EVPN_TYPE_EXT_ID_KEY: constants.OVN_EVPN_TYPE_L3,
|
||||||
|
constants.OVN_EVPN_VNI_EXT_ID_KEY: 1001,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_match_fn(self):
|
||||||
|
# ls had no conf in external_ids, does have it now.
|
||||||
|
old = utils.create_row(external_ids={})
|
||||||
|
self.assertTrue(self.event.match_fn(None, self.row, old))
|
||||||
|
|
||||||
|
def test_match_fn_changed_vni(self):
|
||||||
|
old = utils.create_row(external_ids={
|
||||||
|
constants.OVN_EVPN_TYPE_EXT_ID_KEY: constants.OVN_EVPN_TYPE_L3,
|
||||||
|
constants.OVN_EVPN_VNI_EXT_ID_KEY: 1002,
|
||||||
|
})
|
||||||
|
self.assertTrue(self.event.match_fn(None, self.row, old))
|
||||||
|
|
||||||
|
def test_match_fn_deleted_ls(self):
|
||||||
|
self.assertTrue(self.event.match_fn(self.event.ROW_DELETE, self.row,
|
||||||
|
None))
|
||||||
|
|
||||||
|
def test_match_fn_no_match(self):
|
||||||
|
# no vrf update in old, should return False
|
||||||
|
old = utils.create_row()
|
||||||
|
self.assertFalse(self.event.match_fn(None, self.row, old))
|
||||||
|
|
||||||
|
def test_match_fn_no_match_same_vni(self):
|
||||||
|
# same update in old, should return False
|
||||||
|
self.assertFalse(self.event.match_fn(None, self.row, self.row))
|
||||||
|
|
||||||
|
def test_match_fn_no_match_incomplete_row(self):
|
||||||
|
# incomplete configuration, should return False
|
||||||
|
row = utils.create_row(external_ids={
|
||||||
|
constants.OVN_EVPN_TYPE_EXT_ID_KEY: constants.OVN_EVPN_TYPE_L3,
|
||||||
|
})
|
||||||
|
self.assertFalse(self.event.match_fn(None, row, None))
|
||||||
|
|
||||||
|
row = utils.create_row(external_ids={
|
||||||
|
constants.OVN_EVPN_VNI_EXT_ID_KEY: 1001,
|
||||||
|
})
|
||||||
|
self.assertFalse(self.event.match_fn(None, row, None))
|
||||||
|
|
||||||
|
def test_run(self):
|
||||||
|
self.event.run(None, None, None)
|
||||||
|
self.agent.sync.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
class TestLocalnetCreateDeleteEvent(test_base.TestCase):
|
class TestLocalnetCreateDeleteEvent(test_base.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import eventlet
|
import eventlet
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
class WaitTimeout(Exception):
|
class WaitTimeout(Exception):
|
||||||
@ -21,7 +22,22 @@ class WaitTimeout(Exception):
|
|||||||
|
|
||||||
|
|
||||||
def create_row(**kwargs):
|
def create_row(**kwargs):
|
||||||
return type('FakeRow', (object,), kwargs)
|
row = type('FakeRow', (object,), kwargs)
|
||||||
|
if not hasattr(row, 'uuid'):
|
||||||
|
row.uuid = uuid.uuid4()
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
class FakeLinuxRoute(dict):
|
||||||
|
def get_attr(self, key, default=None):
|
||||||
|
for k, v in self.get('attrs', []):
|
||||||
|
if k == key:
|
||||||
|
return v
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def create_linux_routes(route_info) -> 'list[FakeLinuxRoute]':
|
||||||
|
return [FakeLinuxRoute(r) for r in route_info]
|
||||||
|
|
||||||
|
|
||||||
def wait_until_true(predicate, timeout=60, sleep=1, exception=None):
|
def wait_until_true(predicate, timeout=60, sleep=1, exception=None):
|
||||||
|
@ -156,6 +156,65 @@ def ensure_arp_ndp_enabled_for_bridge(bridge, offset, vlan_tag=None):
|
|||||||
enable_proxy_ndp(bridge)
|
enable_proxy_ndp(bridge)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_anycast_mac_for_interface(intf, offset):
|
||||||
|
# Make pointer to module, to shorten amount of chars to call module.
|
||||||
|
priv = ovn_bgp_agent.privileged.linux_net
|
||||||
|
|
||||||
|
# The intf acting as L3 GW (needs to have same mac address everywhere)
|
||||||
|
# So generate the mac address based on the given offset and add the
|
||||||
|
# configured MAC_LLADR_OFFSET to it.
|
||||||
|
mac_int = int(constants.MAC_LLADDR_OFFSET.replace(':', ''), 16)
|
||||||
|
ll = ('%x' % int(mac_int + offset)).zfill(12)
|
||||||
|
lladdr = ":".join([ll[i:i + 2] for i in range(0, len(ll), 2)])
|
||||||
|
|
||||||
|
# Check what the mac address currently is.
|
||||||
|
dev = priv.get_link_device(intf)
|
||||||
|
curr_lladdr = priv.get_attr(dev, 'IFLA_ADDRESS')
|
||||||
|
|
||||||
|
if lladdr != curr_lladdr:
|
||||||
|
LOG.info("Updating mac address for intf %s to address %s",
|
||||||
|
intf, lladdr)
|
||||||
|
priv.set_link_attribute(intf, address=lladdr)
|
||||||
|
|
||||||
|
# Also update the 'scope link' address on the interface.
|
||||||
|
ll = lladdr.replace(':', '')
|
||||||
|
ll_ip_address = ipaddress.IPv6Address(
|
||||||
|
f'fe80::{ll[0:4]}:{ll[4:8]}:{ll[8:12]}')
|
||||||
|
ll_net = ipaddress.IPv6Network('fe80::/10')
|
||||||
|
|
||||||
|
# Fetch all ipv6 addresses and check if we already configured the
|
||||||
|
# link-local address.
|
||||||
|
addresses = priv.get_ip_addresses(
|
||||||
|
index=dev['index'], family=constants.AF_INET6)
|
||||||
|
for addr in addresses:
|
||||||
|
ip_addr = ipaddress.IPv6Address(
|
||||||
|
priv.get_attr(addr, 'IFA_ADDRESS'))
|
||||||
|
|
||||||
|
if (addr['scope'] != 0 and
|
||||||
|
ip_addr in ll_net and
|
||||||
|
ip_addr != ll_ip_address):
|
||||||
|
|
||||||
|
LOG.info('Update scope link address on intf %s from %s to %s',
|
||||||
|
intf, ip_addr, ll_ip_address)
|
||||||
|
|
||||||
|
# Delete the old link local ip address from the interface
|
||||||
|
priv.delete_ip_address(ip_addr.compressed, intf,
|
||||||
|
prefixlen=addr['prefixlen'],
|
||||||
|
scope=addr['scope'])
|
||||||
|
|
||||||
|
# Attach our anycast link local address to the interface
|
||||||
|
priv.add_ip_address(ll_ip_address.compressed, intf,
|
||||||
|
prefixlen=addr['prefixlen'],
|
||||||
|
scope=addr['scope'])
|
||||||
|
|
||||||
|
|
||||||
|
def disable_learning_vxlan_intf(intf):
|
||||||
|
'''ip link set vni200 type bridge_slave neigh_suppress on learning off'''
|
||||||
|
ovn_bgp_agent.privileged.linux_net.set_brport_attribute(
|
||||||
|
intf, neigh_suppress=True, learning=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def ensure_routing_table_for_bridge(ovn_routing_tables, bridge, vrf_table):
|
def ensure_routing_table_for_bridge(ovn_routing_tables, bridge, vrf_table):
|
||||||
# check a routing table with the bridge name exists on
|
# check a routing table with the bridge name exists on
|
||||||
# /etc/iproute2/rt_tables
|
# /etc/iproute2/rt_tables
|
||||||
@ -368,6 +427,23 @@ def enable_proxy_arp(device):
|
|||||||
ovn_bgp_agent.privileged.linux_net.set_kernel_flag(flag, 1)
|
ovn_bgp_agent.privileged.linux_net.set_kernel_flag(flag, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_routing_for_interfaces(*interfaces):
|
||||||
|
# Configure sysctl
|
||||||
|
keys = [
|
||||||
|
('net.ipv4.ip_forward', 1),
|
||||||
|
('net.ipv4.conf.all.forwarding', 1),
|
||||||
|
('net.ipv6.conf.all.forwarding', 1),
|
||||||
|
]
|
||||||
|
for intf in interfaces:
|
||||||
|
intf_key = intf.replace('.', '/')
|
||||||
|
keys.append((f'net.ipv4.conf.{intf_key}.forwarding', 1))
|
||||||
|
keys.append((f'net.ipv6.conf.{intf_key}.forwarding', 1))
|
||||||
|
|
||||||
|
for k, v in keys:
|
||||||
|
LOG.debug('Configure sysctl %s=%s', k, v)
|
||||||
|
ovn_bgp_agent.privileged.linux_net.set_kernel_flag(k, v)
|
||||||
|
|
||||||
|
|
||||||
@tenacity.retry(
|
@tenacity.retry(
|
||||||
retry=tenacity.retry_if_exception_type(
|
retry=tenacity.retry_if_exception_type(
|
||||||
netlink_exceptions.NetlinkDumpInterrupted),
|
netlink_exceptions.NetlinkDumpInterrupted),
|
||||||
|
Loading…
Reference in New Issue
Block a user