pax_global_header00006660000000000000000000000064146032736760014530gustar00rootroot0000000000000052 comment=c54729d31718a26dcb868f8ca47893dd9ffb1a72 ovn-bgp-agent-2.0.1/000077500000000000000000000000001460327367600141745ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/.coveragerc000066400000000000000000000002761460327367600163220ustar00rootroot00000000000000[run] branch = True source = ovn_bgp_agent omit = ovn_bgp_agent/tests/*, ovn_bgp_agent/privileged/linux_net.py, ovn_bgp_agent/utils/linux_net.py [report] ignore_errors = True ovn-bgp-agent-2.0.1/.mailmap000066400000000000000000000001311460327367600156100ustar00rootroot00000000000000# Format is: # # ovn-bgp-agent-2.0.1/.stestr.conf000066400000000000000000000000651460327367600164460ustar00rootroot00000000000000[DEFAULT] test_path=./ovn_bgp_agent/tests top_dir=./ ovn-bgp-agent-2.0.1/AUTHORS000066400000000000000000000014071460327367600152460ustar00rootroot00000000000000Daniel Alvarez Sanchez Dmitriy Rabotyagov Eduardo Olivares Fernando Royo Ihtisham ul Haq Jakub Libosvar Justin Lamp Luca Czesla Lucas Alvares Gomes Luis Tomas Bolivar Michel Nederlof Michel Nederlof OpenStack Release Bot Rodolfo Alonso Hernandez Takashi Kajinami Thomas Goirand Trygve Vea elajkat maliangyi ovn-bgp-agent-2.0.1/CONTRIBUTING.rst000066400000000000000000000012141460327367600166330ustar00rootroot00000000000000The source repository for this project can be found at: https://opendev.org/openstack/ovn-bgp-agent Pull requests submitted through GitHub are not monitored. To start contributing to OpenStack, follow the steps in the contribution guide to set up and use Gerrit: https://docs.openstack.org/contributors/code-and-documentation/quick-start.html Bugs should be filed on Launchpad: https://bugs.launchpad.net/ovn-bgp-agent For more specific information about contributing to this repository, see the replace with the service it implements contributor guide: https://docs.openstack.org/ovn-bgp-agent/latest/contributor/contributing.html ovn-bgp-agent-2.0.1/ChangeLog000066400000000000000000000274421460327367600157570ustar00rootroot00000000000000CHANGES ======= 2.0.1 ----- * Fix placement of lsp when external\_ids not in sync * Update TOX\_CONSTRAINTS\_FILE for stable/2024.1 * Update .gitreview for stable/2024.1 2.0.0.0rc1 ---------- * [NB watcher] Prevent lsp events for remote tenant events * Change default to NB DB Driver * Fix typo for linux util function used at evpn driver * Bump OVS version (branch 3.3) for devstack local.conf sample * Fix OVN LB Delete events for NB driver * Fix address scope test and add address scope unit tests * Add feature to check if SNAT disabled before exposing tenant networks * Fix backward compatibility for setups that export subnets per host * Update NB driver to re-use new methods and make code cleaner * Fix event handling for LSP and prefer the options.requested-chassis info * Disable exposing remote\_ips, when only the lrp prefix is sufficient * Trimm interface name consistently * Fixes at the documentation * Add bgp tempest job * tox: Drop envdir * Address the Load\_Balancer's datapath\_group column deprecation * Add documentation about NB DB driver * Check for networks on router port in match\_fn * Add support to PF OVN LBs for NB Driver * Use .coveragerc to omit directories * Fix startup if the hostname is not configured in OVS * Fix FRR 9 mgmtd crashes when applying config * Refactor ensure\_routing\_table\_for\_bridge * Remove copy&paste code from ensure\_arp\_ndp\_enabled\_for\_bridge * Use netaddr IPNetwork instead of parsing IP strings * Consolidate use of IP\_VERSION\_TO\_FAMILY conversion * Add netaddr as a requirement * Remove obtaining event classes from strings, vol 2 * Remove core OVN backward compatible code * Generate config file using oslo-config-generator * Remove obtaining event classes from strings * Remove re-typing lists to sets at initialization * Use common function to (un)install packages * devstack: Install vrf kernel module if needed * Drop lower-constraints.txt * Fix watcher logic for exposing OVN LB with the NB DB driver * Ensure NDB Proxy gets added for provider IPs too * Python 3.12: use importlib instead of imp * Ensure proxy arp and ndp is configure for flat provider networks * Avoid race when deleting VM with FIP * Ensure withdrawn events are only processed in relevant nodes * Group the ovn DBs configs together * Add support at NB BGP Driver for exposing OVN LBs * Add support at NB BGP Driver for exposing tenant IPs * Add support at NB BGP Driver for exposing subnets * Fix typo on listing config options * Add support at NB BGP Driver for exposing CR-LRPs * Add initial support for local OVN cluster instead of kernel-networking * Devstack plugin * Fix [EVPN-Driver] Cannot remove routes * Update master for stable/2023.2 * Fix spelling errors * [UT] Fix the \`\`TestLinuxNet\`\` wrong assert calls 1.0.0 ----- * Fix issue with virtual ports not being exposed on time * Make debug log less chatty 1.0.0.0b1 --------- * [functional testing] enhance \_get\_device aux function * Ensure agent is protected again wrong/missing bridge mappings * Explicitly define the MAC address when creating the interface * Avoid vlan device leaking * Reshape code around ip neigh * CI: add periodic weekly and experimental queue * Config register\_opts for tests in base class * Minor code improvements around move from NDB to IPRoute * Ensure FIPs are exposed as part of cr-lrp binding events * Avoid usage of NDB linux\_net utils * Avoid usage of NDB in ovn\_bgp\_driver, and ovs and wire utils * Add new privileged rule methods implementations * Add more new privileged method implementations * Add new privileged method implementations * Retry get\_ovs\_patch\_port\_ofport if empty port * Ensure PortBindingChassis Events consider the port up status * Minor code improvements on priviledged linux\_net functions * [NB Driver] Ensure proper processing of LSP ports creation updates * Ensure watchers do not crash * Add more pyroute2 protection * Give more time for patch ports to be created * Publish docs at docs.openstack.org * Handle DatapathNotFound in l2 driver 0.4.0 ----- * Use logical\_port instead of name for port binding (localnet) entries * Optimize the patch port ofport retrieval * Avoid race condition when adding ovs-flows * Ensure options are registered for unit testing * Fix IndexError when no VLAN tag is defined * Add ls\_get\_localnet\_port function * Revert "Bump ovsdbapp dependency" * Ensure all vlan tags are processed * Bump ovsdbapp dependency * Add frr\_sync function to other drivers * Ensure the reconcile intervals configuration option is enforced * Set more reasonable default syncronization period * Ensure ovn-lbs on the provider withdrawn works with cascade deletion * Ensure the needed mac tweak flows are created as part of the wiring * Split creation of base ovs flows from removal of extra leftovers * Ensure FIPs events are properly handled * Replace storyboard links with launchpad ones * Fix error in NB DB watcher that triggered wrong events * Add protection from pyroute crashed * Add initial wiring configuration to a common function * Ensure cover information is recreated upon coverage failure * Better protect from FRR restarts * Ensure permanent mac entry are deleted from the right device * Add init\_var function to simplify code * Ensure no wiring is added for OVN exposing method * Move base bgp configurations to a common bgp utils * Add supportability matrix * Change x/ovn-bgp-agent references to openstack/ovn-bgp-agent * Add a new driver that uses NB DB instead of SB DB * Change default IPv4 associated to provider bridges * Add vrf\_leak call to sync * Improve OVNLBMemberCreateDeleteEvent watching event * Do not process external network that are not provider * Ensure localnet port events are processed * Ensure proper handling of missing bridge\_device at wiring * Disable only connecting to SB leader * Add ChassisPrivateCreateEvent to the events * Split the wiring actions to a different file * Split the BGP expose/withdraw to a common utils file * Add protection from tailing spaces on mac field * Ensure proper order during expose/withdraw * Add protection from DatapathNotFound exception * Expose ovn lb on provider network if tenant network is disabled * Add protection for races around expose\_subnet * More protection from router deletion races * Avoid collision between vrf and ovs bridges routing tables * Minor rename of stretched l2 driver to be in sync with docs * Ensure permanent mac entry is added for the right device * Minor fix to avoid unneeded calls for FIP Set and Unset events * Ensure exceptions on the sync function don't kill the agent * Ensure OVN LB on provider are only exposed in the right node * Ensure exposed ovn lb IPs are not removed upon re-sync * Remove dead code * Avoid race condition on is\_provider\_network checkings * Avoid race between withdraw\_subnet and cr-lrp association * Minor code fix to reuse cr\_lrp\_info * Avoid race condition at \_process\_lrp\_port * Add retry mechanism for some pyroute actions * Remove unneed functions * Ensure ovn loadbalancers are properly managed * Support for AddressScopes as an API of what to expose * Publish docs to readthedocs * Add releasenotes job * Fix tox -e releasenotes * Only process provider lbs associated with local cr-lrps * Ensure tenant IPs are exposed when using configdrive * Properly handle virtual port chassis updates * Ensure FIP to VIP association is not lost on re-sync * Add some information and VRF configuration on start up * Make return types consistent * Add new driver ovn\_stretched\_l2\_bgp\_driver * Ensure ndp proxy is created for ipv6 amphoras on provider networks * Adapt to the new fields on Load\_Balancer SB DB * Fix gates due to tox bump to tox4 * Fix AttributeError exception due to missing old.chassis * More removal of duplicated code * Code reshape to avoid code duplication * Fix problem exposing ovn-lb tenant IPs * Add support to only expose tenant IPv6 GUA IPs * Ensure cleanup upon router removal without gateway unset * Fix delete\_routes\_from\_table * Fix unit tests run individually * Fix loadbalancer cascade deletion * Fix KeyError at add\_ip\_route for provider vlan networks * Avoid KeyError exception * Avoid rules recreation at ensure\_default\_ovs\_flows function * Ensure only the provider network port is used * Add list\_opts() function and define entry point 0.3.0 ----- * Ensure no IP leftovers upon restart * Fix typo when calling \_ensure\_network\_exposed method * Allow the user to set the VRF settings * Code improvements for clarity and avoiding extra calculations * Variable renamed for clarity * Fix lookup in ensure\_routing\_table\_for\_bridge * Add support for IPv6 multiprefix * Ensure ovn-lb VIPs on tenant networks are exposed * Fix rst typos at bgp\_mode\_design documentation * Add initial BGP Mode documentation * Use log.debug instead of log.info for ovn driver * Ensure IP family is used to obtain the existing routes * Ensure VM ports on tenant networks are withdraw when VM is removed * Ensure arp/ndp proxy is enabled on the needed bridges * Minor fixing on EVPN documentation * Remove events for BGP mode with expose\_tenant\_networks disabled * Add unittest for OvnSbIdl() * Add unittests to ovn\_evpn\_agent.py * Ensure ovn-lb deletion event is also handled * Add set\_device\_status() to utils/linux\_net.py * Ensure ovn-lb on provider networks only get exposed in one chassis * Add unittests to ovn\_bgp\_agent.py * Add specific info about when to remove pyroute2 workaround * Protect from NDB issue 967 * Ensure scope is not used for ip6 routes removal * Add support for ovn-lb on the provider network * Fix pyroute2 import issue * Account for DVR when exposing FIPs 0.2.0 ----- * Ensure handling of already added arp/ndp IPs * Move create\_routing\_table\_for\_bridge() to privleged/ * Increase unittest coverage for privileged linux\_net * Add unittests to evpn\_watcher.py * Ensure bridge devices has ARP/NDP proxy enabled * Add unittests to base\_watcher.py and bgp\_watcher.py * Ensure netlink commands are privsep executed * Avoid KeyError exception * Fix typo at config options. Remove "\_" * Add support for specifying ovsdbconnection string * Add oslo.rootwrap requirements * Add unittests to privileged/ovs\_vsctl * Add unittests to privileged/vtysh * Add unittests to privileged/linux\_net * Add unittests to linux\_net 0.1.0 ----- * Add privsep rootwrap configuration support and filters * Avoid raising KeyError * Ensure sync task exposes all NATed IPs on the cr-lrp ports * Ensure all IPs on nat\_addresses are processed * Make is\_port\_on\_chassis not specific to a port type * Ensure default driver match the setup.cfg option * Lower the neutron-lib version requirement * Add wait event for ovn agents (bgp/evpn) sb\_idl * Add support for older OVN versions without Chassis\_Private * Revert "Remove Chassis\_Private table dependency" * Ensure ovs-ofctl command works with different flow formats * Change the entrypoint name to ovn-bgp-agent * Remove Chassis\_Private table dependency * Update EVPN Mode documentation * Fix typo on error handling * Ensure ovs-ofctl commands work with Devstack * Add option to specify the EVPN VXLAN local IPs * [EVPN] Connect VRF to OVS through veth or vlan * Simplify some methods from OvsIdl and add unittests * Fix port regex in ensure\_default\_ovs\_flows() * Refactor parts of ovs.py and add unittests * Coverage to omit the test files * Fix string matching bug in ensure\_default\_ovs\_flows() * Ensure permanent nei entry is added for cr-lrp ports * Refactor parts of frr.py and add unittests * Remove OVSDB lock-related code * Refactor some methods from OvsdbSbOvnIdl + unittests * Add privsep support * Ensure existing env variables are not removed * Fix \_check\_single\_dual\_stack\_format function definition * Clean-up OVN utils * Create base watcher class * Ensure InvalidIP exceptions are handled properly * Add initial support for EVPN * Ensure stderr error handling works on non-english systems * Enable basic gate jobs * Initial support for BGP * Initial commit * Added .gitreview ovn-bgp-agent-2.0.1/HACKING.rst000066400000000000000000000002431460327367600157710ustar00rootroot00000000000000ovn-bgp-agent Style Commandments =============================================== Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ ovn-bgp-agent-2.0.1/LICENSE000066400000000000000000000236371460327367600152140ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ovn-bgp-agent-2.0.1/PKG-INFO000066400000000000000000000027601460327367600152760ustar00rootroot00000000000000Metadata-Version: 1.2 Name: ovn-bgp-agent Version: 2.0.1 Summary: The OVN BGP Agent allows to expose VMs/Containers/Networks through BGP on OVN Home-page: https://www.openstack.org/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: ============= OVN BGP Agent ============= The OVN BGP Agent allows to expose VMs/Containers through BGP on OVN * Free software: Apache license * Documentation: https://docs.openstack.org/ovn-bgp-agent * Source: https://opendev.org/openstack/ovn-bgp-agent * Bugs: https://bugs.launchpad.net/ovn-bgp-agent Features -------- * Expose VMs with FIPs or on Provider Networks through BGP on OVN environments. * Expose VMs on Tenant Networks through EVPN on OVN environments. Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.6 ovn-bgp-agent-2.0.1/README.rst000066400000000000000000000007321460327367600156650ustar00rootroot00000000000000============= OVN BGP Agent ============= The OVN BGP Agent allows to expose VMs/Containers through BGP on OVN * Free software: Apache license * Documentation: https://docs.openstack.org/ovn-bgp-agent * Source: https://opendev.org/openstack/ovn-bgp-agent * Bugs: https://bugs.launchpad.net/ovn-bgp-agent Features -------- * Expose VMs with FIPs or on Provider Networks through BGP on OVN environments. * Expose VMs on Tenant Networks through EVPN on OVN environments. ovn-bgp-agent-2.0.1/devstack/000077500000000000000000000000001460327367600160005ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/devstack/lib/000077500000000000000000000000001460327367600165465ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/devstack/lib/ovn-bgp-agent000066400000000000000000000126301460327367600211370ustar00rootroot00000000000000#!/bin/bash # # 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. # ``stack.sh`` calls the entry points in this order: # # - install_frr # - configure_frr # - init_frr # - install_ovn_bgp_agent # - configure_ovn_bgp_agent # - init_ovn_bgp_agent # - start_ovn_bgp_agent # - stop_ovn_bgp_agent # - cleanup_ovn_bgp_agent function install_frr { echo_summary "Installing FRR" setup_develop $OVN_BGP_AGENT_DIR install_package frr } function configure_frr { echo_summary "Configuring FRR" # Create the configuration dir sudo install -d -o $STACK_USER $FRR_CONF_DIR # Configure frr daemons sudo install -o root -g root -m 644 $OVN_BGP_AGENT_DIR/etc/frr/* $FRR_CONF_DIR/ } function init_frr { echo_summary "Initializing (restart) FRR" sudo systemctl restart $FRR_SYSTEMD_SERVICE } function start_frr { echo_summary "Starting FRR" start_service $FRR_SYSTEMD_SERVICE } function stop_frr { echo_summary "Stopping FRR" stop_service $FRR_SYSTEMD_SERVICE } function cleanup_frr { echo_summary "Cleaning FRR" # Remove FRR disable_service $$FRR_SYSTEMD_SERVICE uninstall_package frr # Clean the FRRt configuration dir sudo rm -rf $FRR_CONF_DIR } function install_vrf_kernel_module_if_needed { # If the kernel vrf was compiled separately as a module if [ "$(grep CONFIG_NET_VRF /boot/config-$(uname -r) | cut -d = -f 2)" == "m" ]; then if is_ubuntu; then install_package linux-modules-extra-$(uname -r) fi if is_fedora; then install_package kernel-modules-core-$(uname -r) fi sudo modprobe vrf fi } function install_ovn_bgp_agent { echo_summary "Installing OVN BGP Agent" setup_develop $OVN_BGP_AGENT_DIR # Create the systemd unit file local cmd cmd=$(which ovn-bgp-agent) cmd+=" --config-dir $OVN_BGP_AGENT_CONF_DIR" write_user_unit_file $OVN_BGP_AGENT_SYSTEMD_SERVICE "$cmd" "" "root" $SYSTEMCTL daemon-reload enable_service $OVN_BGP_AGENT_SYSTEMD_SERVICE install_vrf_kernel_module_if_needed } function configure_ovn_bgp_agent { echo_summary "Configuring OVN BGP Agent" # Create the configuration dir sudo install -d -o $STACK_USER $OVN_BGP_AGENT_CONF_DIR if ! is_service_enabled tls-proxy; then die $LINENO "OVN BGP Agent requires TLS to be enabled. Please set ENABLE_TLS=True and enable tls-proxy in your local.conf" fi if [[ $OVN_BGP_AGENT_DRIVER != "ovn_bgp_driver" && $OVN_BGP_AGENT_DRIVER != "nb_ovn_bgp_driver" ]]; then die $LINENO "\"ovn_bgp_driver\" or \"nb_ovn_bgp_driver\" are the only supported drivers at the moment" fi iniset $OVN_BGP_AGENT_CONF_FILE DEFAULT driver $OVN_BGP_AGENT_DRIVER iniset $OVN_BGP_AGENT_CONF_FILE DEFAULT debug $OVN_BGP_AGENT_DEBUG iniset $OVN_BGP_AGENT_CONF_FILE DEFAULT expose_tenant_networks $OVN_BGP_AGENT_TENANT iniset $OVN_BGP_AGENT_CONF_FILE DEFAULT ovsdb_connection $OVN_BGP_AGENT_OVS_DB # Configure TLS/SSL iniset $OVN_BGP_AGENT_CONF_FILE ovn ovn_sb_ca_cert "$INT_CA_DIR/ca-chain.pem" iniset $OVN_BGP_AGENT_CONF_FILE ovn ovn_sb_certificate "$INT_CA_DIR/$DEVSTACK_CERT_NAME.crt" iniset $OVN_BGP_AGENT_CONF_FILE ovn ovn_sb_private_key "$INT_CA_DIR/private/$DEVSTACK_CERT_NAME.key" iniset $OVN_BGP_AGENT_CONF_FILE ovn ovn_sb_connection $OVN_BGP_AGENT_OVN_SB_DB iniset $OVN_BGP_AGENT_CONF_FILE ovn ovn_nb_ca_cert "$INT_CA_DIR/ca-chain.pem" iniset $OVN_BGP_AGENT_CONF_FILE ovn ovn_nb_certificate "$INT_CA_DIR/$DEVSTACK_CERT_NAME.crt" iniset $OVN_BGP_AGENT_CONF_FILE ovn ovn_nb_private_key "$INT_CA_DIR/private/$DEVSTACK_CERT_NAME.key" iniset $OVN_BGP_AGENT_CONF_FILE ovn ovn_nb_connection $OVN_BGP_AGENT_OVN_NB_DB # Configure rootwrap sudo install -d -o root -g root -m 755 $OVN_BGP_AGENT_CONF_DIR/rootwrap.d sudo install -o root -g root -m 644 $OVN_BGP_AGENT_DIR/etc/ovn-bgp-agent/rootwrap.d/*.filters $OVN_BGP_AGENT_CONF_DIR/rootwrap.d sudo install -o root -g root -m 644 $OVN_BGP_AGENT_DIR/etc/ovn-bgp-agent/rootwrap.conf $OVN_BGP_AGENT_CONF_DIR iniset $OVN_BGP_AGENT_CONF_FILE agent root_helper "$OVN_BGP_AGENT_ROOTWRAP_COMMAND" iniset $OVN_BGP_AGENT_CONF_FILE agent root_helper_daemon "$OVN_BGP_AGENT_ROOTWRAP_DAEMON" } function init_ovn_bgp_agent { echo_summary "Initializing OVN BGP Agent" } function start_ovn_bgp_agent { echo_summary "Starting OVN BGP Agent" start_service $OVN_BGP_AGENT_SYSTEMD_SERVICE } function stop_ovn_bgp_agent { echo_summary "Stopping OVN BGP Agent" stop_service $OVN_BGP_AGENT_SYSTEMD_SERVICE } function cleanup_ovn_bgp_agent { echo_summary "Cleaning OVN BGP Agent" # Clean the OVN BGP Agent systemd unit disable_service $OVN_BGP_AGENT_SYSTEMD_SERVICE local unitfile="$SYSTEMD_DIR/$OVN_BGP_AGENT_SYSTEMD_SERVICE" sudo rm -f $unitfile $SYSTEMCTL daemon-reload # Clean the OVN BGP Agent configuration dir sudo rm -rf $OVN_BGP_AGENT_CONF_DIR } ovn-bgp-agent-2.0.1/devstack/local.conf.sample000066400000000000000000000052741460327367600212310ustar00rootroot00000000000000# # Sample DevStack local.conf. # # This sample file is intended to be used for your typical DevStack environment # that's running all of OpenStack on a single host. This can also be used as # the first host of a multi-host test environment. # # No changes to this sample configuration are required for this to work. # [[local|localrc]] DATABASE_PASSWORD=password RABBIT_PASSWORD=password SERVICE_PASSWORD=password SERVICE_TOKEN=password ADMIN_PASSWORD=password Q_AGENT=ovn Q_ML2_PLUGIN_MECHANISM_DRIVERS=ovn,logger Q_ML2_PLUGIN_TYPE_DRIVERS=local,flat,vlan,geneve Q_ML2_TENANT_NETWORK_TYPE="geneve" # Enable devstack spawn logging LOGFILE=$DEST/logs/stack.sh.log enable_service ovn-northd enable_service ovn-controller enable_service q-ovn-metadata-agent # Use Neutron enable_service q-svc # Disable Neutron agents not used with OVN. disable_service q-agt disable_service q-l3 disable_service q-dhcp disable_service q-meta # Enable services, these services depend on neutron plugin. enable_plugin neutron https://opendev.org/openstack/neutron enable_service q-trunk enable_service q-dns enable_service q-port-forwarding enable_service q-qos enable_service neutron-segments enable_service q-log # Horizon (the web UI) is enabled by default. You may want to disable # it here to speed up DevStack a bit. #enable_service horizon disable_service horizon # Cinder (OpenStack Block Storage) is disabled by default to speed up # DevStack a bit. You may enable it here if you would like to use it. disable_service cinder c-sch c-api c-vol #enable_service cinder c-sch c-api c-vol # Enable SSL/TLS ENABLE_TLS=True enable_service tls-proxy # Enable ovn-bgp-agent enable_plugin ovn-bgp-agent https://opendev.org/openstack/ovn-bgp-agent # Whether or not to build custom openvswitch kernel modules from the ovs git # tree. This is disabled by default. This is required unless your distro kernel # includes ovs+conntrack support. This support was first released in Linux 4.3, # and will likely be backported by some distros. # NOTE(mjozefcz): We need to compile the module for Ubuntu Bionic, because default # shipped kernel module doesn't openflow meter action support. OVN_BUILD_MODULES=True OVN_BUILD_FROM_SOURCE=true OVN_BRANCH=main OVS_BRANCH=branch-3.3 # If the admin wants to enable this chassis to host gateway routers for # external connectivity, then set ENABLE_CHASSIS_AS_GW to True. # Then devstack will set ovn-cms-options with enable-chassis-as-gw # in Open_vSwitch table's external_ids column. # If this option is not set on any chassis, all the of them with bridge # mappings configured will be eligible to host a gateway. ENABLE_CHASSIS_AS_GW=True [[post-config|$NOVA_CONF]] [scheduler] discover_hosts_in_cells_interval = 2 ovn-bgp-agent-2.0.1/devstack/plugin.sh000066400000000000000000000025371460327367600176410ustar00rootroot00000000000000#!/bin/bash # # 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. # Save trace setting _XTRACE_OVN_BGP_AGENT_PLUGIN=$(set +o | grep xtrace) set +o xtrace source $DEST/ovn-bgp-agent/devstack/lib/ovn-bgp-agent # Main loop if is_service_enabled q-svc ovn-controller; then # Stack if [[ "$1" == "stack" && "$2" == "install" ]]; then install_frr configure_frr init_frr install_ovn_bgp_agent configure_ovn_bgp_agent init_ovn_bgp_agent elif [[ "$1" == "stack" && "$2" == "extra" ]]; then start_ovn_bgp_agent start_frr fi # Unstack if [[ "$1" == "unstack" ]]; then stop_ovn_bgp_agent stop_frr fi # Clean if [[ "$1" == "clean" ]]; then cleanup_ovn_bgp_agent cleanup_frr fi fi # Restore xtrace $_XTRACE_OVN_BGP_AGENT_PLUGIN ovn-bgp-agent-2.0.1/devstack/settings000066400000000000000000000021131460327367600175600ustar00rootroot00000000000000# Configurations OVN_BGP_AGENT_DRIVER=${OVN_BGP_AGENT_DRIVER:-nb_ovn_bgp_driver} OVN_BGP_AGENT_CONF_DIR=${OVN_BGP_AGENT_CONF_DIR:-/etc/ovn-bgp-agent} OVN_BGP_AGENT_DEBUG=$(trueorfalse True OVN_BGP_AGENT_DEBUG) OVN_BGP_AGENT_TENANT=$(trueorfalse False OVN_BGP_AGENT_TENANT) OVN_BGP_AGENT_OVS_DB=${OVN_BGP_AGENT_OVS_DB:-tcp:127.0.0.1:6640} OVN_BGP_AGENT_OVN_SB_DB=${OVN_BGP_AGENT_OVN_SB_DB:-ssl:127.0.0.1:6642} OVN_BGP_AGENT_OVN_NB_DB=${OVN_BGP_AGENT_OVN_NB_DB:-ssl:127.0.0.1:6641} # FRR configurations FRR_CONF_DIR=${FRR_CONF_DIR:-/etc/frr} FRR_SYSTEMD_SERVICE="frr.service" FRR_CONF_FILE=$FRR_CONF_DIR/frr.conf FRR_DAEMON_CONF_FILE=$FRR_CONF_DIR/daemons # Defaults OVN_BGP_AGENT_DIR=$DEST/ovn-bgp-agent OVN_BGP_AGENT_SYSTEMD_SERVICE="devstack@ovn-bgp-agent.service" OVN_BGP_AGENT_CONF_FILE=$OVN_BGP_AGENT_CONF_DIR/bgp-agent.conf OVN_BGP_AGENT_ROOTWRAP=$(get_rootwrap_location ovn-bgp-agent) OVN_BGP_AGENT_ROOTWRAP_COMMAND="sudo $OVN_BGP_AGENT_ROOTWRAP $OVN_BGP_AGENT_CONF_DIR/rootwrap.conf" OVN_BGP_AGENT_ROOTWRAP_DAEMON="sudo $OVN_BGP_AGENT_ROOTWRAP-daemon $OVN_BGP_AGENT_CONF_DIR/rootwrap.conf" ovn-bgp-agent-2.0.1/doc/000077500000000000000000000000001460327367600147415ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/doc/images/000077500000000000000000000000001460327367600162065ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/doc/images/evpn_traffic_flow.png000066400000000000000000003165351460327367600224260ustar00rootroot00000000000000PNG  IHDR?P,IDATx^XGQbDQ)j4vM4$X@ {رbQ,(&wgۙٽٝwY(B!B8B!P !B 4!B!6@&B!(ЄB!B!@B!bhB!BlM!B P !B 4!B!6@&B!(ЄB!B!@B!bhB!BlM!B P !B 4!B!6@&B!(ЄB!B!@B!bhB!BlM!B P !B 4!B!6@&B!(ЄB!B!@B!bhB!BlM!B P !B 4!B!6@&B!(ЄB!B!@B!bhB!BlM!B P !B 4!B!6@&B!(ЄB!B!@B!bhB!BlM!B P !B 4!B!6@&B!(ЄB!B!@B!bhB!BlM!B P !B 4!B!6@&B!(ЄB!B!p~Amݻ'B!^cƍbgϞ :ߏi~wݻj)>|(jO>lA ,Y2hР/_ڵk2̙33f8p _ѣG_}|Q̈U$Οe&Eւ4yȮ5mZP!"ĂC?d#+W_ʕc:uJl:ujx,Z_`wyqs ̄5RJuY5~رj7o8l?W H2{ jӧ'yG߾}s ԯ__n۩S'l2.4Za1SF u/ŋǎSC ,եK5.&8ٕGF?RD u_7nܹsG %$v@$O\B1,b^Z#$kv)Р|1 }h(o}s:Zh;;;AN-ޱc{F$b'wmݺuB6χ .]:0ıwީnrڵkіhjxm\&۷oRK.~… NZٲeqf`UkM}a b'2dP xxx7+VPRUVq>-[qFl3gN5_LѳgO-) T >>>^ldÆ K) }%1JJ3?J\|"(Є$AKmj<$9kDzWd5khG0`oF4ڕ3gh($Xp 35.Ș18gpA^D mV}7tB͛q1A6F t=8XtR 4@aqyU 49@&$ Ξ=ŋh#[xСpkZ &}m4(W܋/4F#tdɜL5۶mִiS%*~@ NP"hB2ACzԸDDDaC !rh] t5ul!Я^5kV%j֭"J;<>@ NP"hB2DϜ9#- g/+@uKÃHZ裏]v˗/tƌ̝;7pBI56o Mɓ'ȍ="'2wzshw MpU'qh:yϴի(䞸s).P3fА!CG@֭ [J;v!n\rH4[lիWP,Nb'W.mKl7o|Z/]rd[ni%L28@2Ѹk?G~0F*UdZj^&R;v(AYGޔw0rYL@} unFqTb@ULp$Mp"ӦM+8¿!C?#]tb?9ثW/8[z"%WEAdJ[[)Rׯ߮].]6UEpK,Q6db֬Yڨ؍6'ڤ {" 1ݤIvWg"ogdEi)LݢƉQ, qh ;^'NU탹Q@: 4Ўqe*[-@{fΜ k9nȑ'B>zAr+]yWf͔O?T*="Zbn 4U~eJ+~^j(W#eQZԻᆱ WRHA ɩQ4mNu+")晁[n@; gSt&XbHWò@lRN4I{ɓ'E K6տyH`e=<0ԢR? ҬP I8@xdԨQbWlٲFI 2ԬYs&[zs?H5,SVZy;Z˗CjԨ!+mK9ѣGѓ!/pmQr9sDլY3|J,)cb92 2O:F1q-ٸqvj ,s.h9WxI*W,iq/ܗJ^rg`"QcW\ټy3DN)h'G@={VJ3ƍ"YL8,Q`T)yԩSE-?3R޽{";)Ӡ(zyQ EIݺuEh( 4!I&aaaQə3vL#H+z 19VR92jr NFSi}AU#G"g0zc_l\rEO"sO0E/תUKBڴid,S%NCN`8UoXv;SQ49 Rj |JQ6LRj#Fŋ%K|w: Ν;Ů*T W@JI6cIJ@[@L"R͕Y^"[(ܽ 4!Ę&QI3$ti!BX( c,WV= oIBp&@?x@5~M(P zZ9C8M42yʕ+Q(f@M=XƂ18@ˢ 5jF@Gnn '@]h\js}9xҒ!CP:00P כ6\˗">mj4H64ʔ)-g1'&E&" |֭e˖"ڵk>tB "AҥQY;@O7ωd߾}LaG={ؕ= 5s=tPnNs$8I6lD}ryFqM!ybWd "juXhO09b-_'àj3#Gyu 4!$$B~a ɓ(Θ1CMAo2ܝx8 yM EM(MKfn&(Ôp]ϟdzL 9 ԎpGӎpc3!~NoZ8)[? 282d49"ٜ@KTzt֠@ XJ,ir8)S&F}-iӚ,I, s}5N)R0m]#(R !  tz b,4VIA4͜eZ}8c{(^v"ܻ!rV2sǩžssDi]|)xyy@ĊV-4[}"8qr 񰯫XTϋpmGcF9h ӥ@f-  @HH @K(|wjuXhY|,qsNxsFi|RNl>ګ7[}v'O/.`ۋ(xzAp +779s(\{%vww?tlڵk/Q(uh91Z'ѣY _hΝr kdɒds/]vݹs'$$K؊e2('-R̙3\nݺIKb'8ٸqcɓ'-s0*-UCq, s/8<}4K,256Ua2L #fgK<Çh0c GRzu ^((g>ۼyM$bg X???knVo""NiY}$ G_Nj׀ 栗ݻezNcZrcpU9Rb] jhg3Ml1 tm'_Rc'QчSF tgZy& $b@www2uL7&>:*@G(C{ 'ړ(P_L],Y8qexo Γ'O@@5Jt(lp,1C.]:yp݊PpkCD,x,['.믿U]v6 )/^t2&I+pɒ%}L eoG: 4x_>U-.\R6vs777m_Nڄ)(SLq(zyyiogJ,9sL b-W9^ cҥˉѣGV( "{"( !hP?~``Ǐ! 4NR+ЦXݘÇnщS5;wp.K9v9QLB&ϟwƍjA!$Rܹ#^Veٳ' ӳ%!tܹ'NܷoɻZ($C!$Q3zhZ86-hBMX%NPI"' `˖-j"$jvV C&$6zcǎAHR֭[۷J@bw(ЄĆׯ޽;jRp`(Є 4!!00pjy"$OuVRp`(Є 4!aܹVR!$ f͚竕C&P Æ ۷oZ!I#GC&P >>>/^T!$ @ 4!vMHlpss Q!$ p jP ;hBbC۶mDIjP ;hBbCV"""DIP+Mݡ@ ѯW* 4I̴jСZ 1СC;rIth#nmLP}ƍ .8Uhy]rp%a$>IMK<_z`9snݺUЈ?pذa߳q.|v̑ݥK)F%\vȐ!"?̙3'NSժUKoŊSHvZD;v,}իSNϓ'OVРAsԩS1bʙ3gڴi!{9+U\|߿HtR=֭*B~$f,%%-Xm߲rasf*?GSH|zGd&˭f&v݃ j۶CLeA>ޝدo_~%>ߑ.iᐺxy=D9}!/G9W~mXsc?ׯ4BӤIk׮'Nܼy3>"N{Eo DI K(M6V^]jʕ+¼ U:u` }}}ZjݱZvm|o!.W PIbƲX^ Jn|D^ϯB7|l:8bՌg6oӧwA+ qey~k{ʕwQbWpHiqCC7,_ѯ˙#z.)R!Ŋ,VZr1cƈcǎ]vE%K{Ç'O*[ ( WcAOyeO2{mۆ~aXX, .[, ,V^*_%@&!7nY5|ە3]4ABOjwU]xNOM]"E~]}mM&V3@߯oߎ7TSMѱcӧ]tIcׯc4uȎ;!G\:jco셫tɔss{{z@t&|m[%vN=9&@#P|#+tYl>v^WkƜ>}Z={Y܉7>#b|ءC]e3Z^pHW2p;Rc]RR:z)c_:۰H jMp䨏Ϛ9] ^U=H&w%vN=9&:<<<{NofݡC`͚5v54i"A5D{>|(_|9gΜ[qd pB' l~Eܹs)S&E ,d'WGUh/nN=_"ͣ#Wo* =1*6vVzNF_jXn5jGZ{}ݽ[@}ٽ{˱+w|鐺tRCu,:ma 5,n$C'OV[?xs,_|Z깝 zc)K˗; r]S,gCuª@aǎN6l*|O׫W0$Zxvt :44T<9rdݺu jǎEb8`|[v)V=K~$Zg?sGŽiQgr(z62%ЈRP7>hBR@Ն[phfB(#r==W\ymbÇK,rVn9{dͨ=pHK.ٲ XtW2N" F!ϮOX=/u*}ZpJR%PS,gA۶mEodx-Zq9 xÎ-sgta ǏW ۷C=8d8.a;fk86R}ƍC7v$""b…/`!kfw*ЪJԛ[H.9c^wp klYZ[@fH!}@,xC~mXΜ09C2`[4 WX~*UEDvQ"́WD\/§M&V:~xݻwꫯ6miQjrԩX]f6 !BJM}Am3ѭ~iV <ʔ@+pH޺ RXM&Oյ!ϟߩS{?^@6m֬Y{ݑwA+޻W]v.2ޏUE1R8=[m-'1PY@ ̓+.7uHo;qH |6z$e:"M i5>X,N=9/_6=r>B~$*N 1Ԧ7h[ͻw?[*?-ܳuAK iy,]8x𠋋vKɐ)["chrT r)pGfYd}7J(q\CN=!$)_%@&?cСÇme Ê.A ]̳ǥ ɓ'ݼ:ՉIwI"yZ.ֱG]|>ey1n5wZt|ħ@'LّիW=zTh9c +,T)[5$bch,z6q2-b2뇆!jZ2HٱuU oe]N=!$)_%@& ˗/{zz\իWJe2f2e  Ks؉e%Cyuom,W7/iҦ~Zލq1j^~}~~~jPYL>qݻkpة?{wBϖM*ʑ S 1^PȖ@7X<,uyПDwC*(2;~)͡_۩9$($aiӦN:>}Z0`?d1jFDDW 53nܸѧOlӢGZ@S9sI W PIBӧcǎׯ߃Ը7$j={k׮wQښNrww_Ԉ7$ ئyhN=!$)_%@& 7ơ7Q4$ 9sm㙲[޽[А .`avAs93VZ^Pބ P[xa= *5k:u|Uʷ!Zɽ{̙reȐdɒڜ )SWn:W\ٲekР'V+W,Xɓj1BJM{quu=paD[M~*t 5g*F"""Ν㻋lb<N=1sۧO5Ԉw}SNjt :T __̙3O:u͚50uRHHZjӦMP0cƌdɒn !"jȑ84,^XDUT@ ,%C˧c ;wnMy昘DJM/^9sfnBCC8SnݪӡC{ěۣF޽{e+-)V,-cks fּTCv'GIKN=h#G+iҤA-$Voݺ4hPTH,m۶q@s=<nݺJ*2Jr1Ç_ϦFx /N* 4I0{ҤIϞ=ScٴiիՈ81E.ҥK]tA'Çj[%?1ѯgv)S,T0\BÆ 裏rU`&MC۴iӾ}y(QgϞVZ>wwɗ/_L,Xk(xۙ3gDHUVMdݺu OSlYp 3#Dcǎo8 p+V,YRX@/_*aҥ|  ѳeI>=Rbݺu{XBͩ=m^iN<==HDl5{l Eyd|5Nŋ)V_%@& 'NnݺU)r%| 5.VPYx9߿= ?aѯgaƌ2dhԨЩ]Mvܸqׯ5k|RJ"%:EHvڀ$@ϟ /eb"ž}*sW_}(9RO IΝ:u ૗,Y9;;:uJMΝ;h7o.S B} E)RX1Ք)S%K7E  UV`|}}E9s$O4iRl@8ljc^ڧODB!-B#F9U_|iP3g{apzv8:u  7|SLy)W\R޽+;kJ(.]ÇQSN=eL϶Eܥ/;{u|V.diP 9qi~mxC8:t{Q^8 kGM#5ݣG,Yo߆ [.V!҆tM 1.ۓw|O> l_y[&Uqk=T*vx-|j&ݺu7f͚4ih">-^Q^*ÇaUeʔ *ܮ]ɓ'7h{w^~ja˖-?V]]vΏˣg W PCѣ!Cxg16H .^^^'N Sl!Fڨo^ب ?}`ɋzM^}uhfh{uu5aOs|k; ƳF//0`@ٲe%K&!]tİڥKFjrʐ){ի˭LoϜ96.k@_xQBd her%mGϩTvp21,@/^>Tr(L@g!3gΌ./]4: 6mf4hGd|{m}H) 4  EF5 q{\Thѯ@ի_1h%16Hԧhht\4@ {~x7%A4 zvXbzY %Krʅ"/lܸqÆ ݿ˗VJ.U6Wռ뷸+,mGn-ZTD@bs'Z]]|-ZDQܣ-\p C#w+Wn%@#ڷo>| B2e( UypjՂc|ɤ4gm FW&WD̙S;Rݺu?Ǐa9b5.رY4!&Я@eÆ JΞ=FB R-+}knc@ {.~Ѱ[?x(c%[6q< ?˜/'N5|r5%k; Ƴ ͛ӧoڴѯ_?-/ؽ{7{) 4hz#PNׯ/WMk~!yE_YfM1b7;_r&O,7ٹsQ+BڑAې`(>oNqj:,,Ld5>^XbK*hvZ\;9:uꈈuuE͐!>|zڵk>ҡC49zr071~8W@@*5Fbl7n}Xb?~C:YdR@-[ b 9#D^|̱QFJhDM6b\rh"|˂ Dr&_|溿)<(ъ(-dpONr?UhXQ;fyg;16HիWbZK-~~lwӥM3|GЦ g.NK"O So@̑5K挃vZpwU+O^ Ӎ;7m@9 AȎ(h43gҥv-ʟЯԃ ,[l71-Z43C˔)Sti .DB>}4iB& POٳg…79LSLA LT7Ꙝ.&M8h׮]ҧO/@#%FWXQvYE՗6mZ84~;2 ~vحYj.|dң?H#Y͚5ӦMW`HO?!KǍ'Y`… 4ݻ79k֬ByQE|^.d#Ne"EDu! Rzzzb{/Od˚օMJ ˜25fO*>I*c!dۺ)2֗hu?>kسޥM 4|p;wq5?-!_۩1B`x,s/~XlٵkO⎲?ǥ'>C~^N N.{+\ܴi2+?,]ER4^^^sN %Pk; ˗8z@ >>>|$UhwSLո8ra3gj6]gbUzXף6M񢯟7,Cfu@ [û 6sy >zc2fHx{ny{/aa%\k; ___cͱg7?~m=nnnڧDJM2ǎsuuݾ}azwܹеk׳gϪqh9 țxw!?|@[twhC.\0`/r5OKvAb= ԩvf -#F3fLfm* 4yk|u55N$...+W47J{Lt<9\uj|6mj1|Y.6N?wl@W\?}ZɟЯԃzyW PѣG 5jT^1h%l$~~~4>X4>?!^ `2}SH^gbV7G M,YգCݻw8qbd㟖8ЯԃzyW P[@TD))OVLz_%@&_5zV$ݛ6mcDH%@?x`ԩN8vw﯉,ᆱM-)>OGY~"zG}5w܂ :sΩvbuɞ={|ܴ=yF2S!hM[H,Y`+mܹs]5Ҿ$44m۶9 Mεk6n܈<5GQ\9= d˖^z'NPS$($@ h"bJh {ԩQRYK7o߿?:VuϯxtXw=5N-)O{͗Ś N=У4iaj=8~xƌׯvZ==zhscݺui֬Y7=ZDӧO]ajՂKn߾=VgϞjժ+-ZTzÆ *hҥ^KUΝ;tʔ)8*Y&zmxzzf͚uƌW\;C}MBJMT(GQ#tF)1>|x^(,%4K .47kר)lٞ=^6ҌkK_'Vji9zG} ޺u~ݼy2e ބ aϟ_Æ ˑ#\@ 3d0p@`oٳgO.._&MG?PM Sdɖ-[&V EcSN-_6\.OW PӦMѣ65NhSN@[ Gaa|o!װaBCDQKJx3gj5s8z>}J*;v[nüy֩SGXBU@#>ܹs!dg?u/Dݹsٳg͚;ٵkYFlۇt͛fqϞC Y={6ŋrZjBJMt u@VbA"`h(,9uоeh7d9o8i߬EC(^\Ma;H=i[۩--^`AP.YêVgM"W[jUʕafBՓ)mHDu {۶moݺuL ׯqvv^lճ}ɓCF>},Yk.p[T~tRxq%\VZըQ#""BRJ}'eʔ8___1 KwL('>tU|`O/ҤkKvZ-ZT ]>~׸qP8kƌ#D"9sf!"{܈իWΜ9]FoG -]$tfY\Jƍկ_?$0G I:W P^9rDՈxrA+5sL43#_,9>/wfr4^ =XxCN=9Bb JEpM4J=>Jիgjy5xEFq!߼yq_|5jTԩڥKBd2GX#l+eʔBql.\g@CR r 8HK/_'BPBڨ;X&t\CUB"+!bljBF ThX~lڶm+w`D!#  @GF ~"E9pU] JHAJM˗/-Z{u5.~ %eݻ7bĈчhk$v\V@u~ky_۩3G%Jԩ)LÃ~iBΪTiQi<6yb@3k׮xyyV)~#G\|Qa4B7|#ϟL2`rŋcݻwZwܹ3>`ҙ3goäMtIժUYL%U @WPAK޲e >l߾]FL2u-p}t94R]믵+ț7oK":iFI* 4'Νsww_nޯ R~@ Q޶u#el=cB[e;8 ߢ}]rrƲ3*E;кa9sb-Q_|EUV%K2dZ0a>k޾}[(. j_x188x)BBB߿߿ہ 6i=cƌrh(~|b/0:s `2 ,z-Bֿ?#1eѣӤI8wa Ѕ ֆ)Ri^E@;7K#G4:}zɒ%ɓiӦi:fbpv>ѯ@c^v- ՜ %AP纸(wD"hyr.O.iҤB`Y]aI,K+Z9Cܩ]|+K_Nq6:8eTo6-lnryqK<~mXΜX 4:uX=rHʔ)g<ȗ/\ *رu۵ !o9:0 3ЉfDr+//,YϪ ||uJʖ-f趼{9(unJ.݀DԞ={~T)M&TFDD>}ڵk"J 6o,VPHEQc8 1mnԨ@䛡8MYf5+c&s=z88s΍*_%@&vɓ'F4hУGԸ)-|+Hm Aoe؛谫[6xt;8[Yr,!#f98֚%[?U7W/A3\l]9k;rZӤI#,[nr\vZhE!ϟ,YR 6k֬O>uԁz D)W½sȡ,ܹso_$EV2 *_|L " |B xb>ضm[1&*G5@ܑ-=9~iIarJ;?u6neA׮]/B"B?v'O!gw`U,}SlAF vw*H( `"b6&ݝ`XWz^?|/gd\9XrgN2;coOVTTJlԨQh~6mKk[ahӦMŠ!ۈ드4q͛C\e8r>ȿ~"%Fi{]?N9+vv?8*mgcA|KvhP*VlyQELw/_4ƌ3;7Ln\?Nmu_Z|tk^gn :j:υC4uTss¾f>  eZKdgAЂrwœ'O~ WhT R oz˚pP/u/N47 zrJI`qchK}\[&z.N.D3V⸒Ay!U#{xq vQ s~\QtEM[ǧ`L+zwTW a/}bwLTݬt$r\|(g~J.qZ]KKk_IIggL@/_USS9-(ZPNˉ'.[c r!%''{yy} @7ml~$K͘zj!2Z,5C?fn\: 78pgW+mtu;@[-N4m^ĨP37_1gT_3{9:B>vy@jeK$㘃=6ґdswNxx8w Y)88 -}$l$<ה)SPMB5L5@ ʶ.^tI֣)gRPbb"ɑ )t]mZԮ,j wsf *V(D;ݒAOo6)7X";7ƌ{IwSXGcƈ%cًl0=/kI ,wN)|.AJk }uݺu ?}+*ѣG鮳r @)SO?` ̵N\/.2vT@I{Exz=7)~=@?/hضe]XS|ڷ @Ϛ2Fs3,dLax8O\>!}'$$F{.\:X=22ŋls>!°ŠQ";-[ݹsZ# b n`l_jbR·~L9(4ț۴|z!ë17@舟.&F#24{G-hyjXNKtrX[#j┅E\>aZsT_#Pׯˉ{rwW*z={RSSقȦnݺBc= JZI&=}4[{JhjjOA,WJ.rYlMVC~q}sЇw8zDn5S*NzgK55|y'wR\qُ[tiTsw|.©7n<{ S8=dmqաC&O㽵qΝ;߿zիWӧOnQrp fަ%(2mԁY*&rvF#<tUL9U'L&g~x]1kvIJkUڼRSoqRNí ;sxpǎ'Nџwlْsr|px ޽kff/mϟ/^],NNNvwwu{M2%))-_BCC  pѣG+Vٗ9o_i֤Rd@u W;ޱ˦{h)¢$YL#d؁tu5.V661۟(b kii/Wj":<7.'Y웎&؅.]_=Wkմe<牳pܻsízmPF:Z.ːy{HJ?9rdxxx KSS'؅M ЌСC[l޽;Kʗ/߶m988I3g4>^=yp9A=((zm6},c|AEK5% -ڷo_LL [&@[7֯(;©7nܹPXrzI4S5۳-y,kkkAtUqФkڀ9S*<`g4bvvXJ4Χ!~ˬԂy:?$B}sqǧNZiCdvP&ӇVRtVq*A޽pf$]M6^> ܘUTTmm9 -QAe_ɇp޾}`}UUU9B\NII13ͫwENɸq /uC A-,,չss'ZYY֜@7,vuu[C0[2ua''WEJy!B:uYH.@r'z~"nmx\c֖fU$ MDRj"$[ujev\|Hn$%% 6Zju qښUTW^h_z: ֨Qcرo޽kcc=z8{,رo߾{m޼9,--lB^~ ]6M6]b[nf̘A|Ԛrzڴi***z ұcGr)@GG;y(Ν }SNeD`0}TTAL5% |0J޿?j(8?!{@@wX&RQΝ;]\\쪨ԓjj;2^~td4qM%) ;y`oo΋˗/-,,V\xas?^$ebbR~ 6 |Ntuuϟ?%Khkkm޼e˖mx]w1b޷oH85 $xQp;1rw[;#t[ZZZD͛7ԩ&L:99LOc\ÇG$Q/kJ@n|^xdŊ_|aq'T) 4i<=SÎ5oZǖj={|.@mWs*.'O5߿ON)qLP/6lxaf 耀P#߼y$q |eqܢE V|ӦM;q=Jq >t(#@׫W]vUD'OF$]T)!P^E,Lsmڴ) h "CBr*t1%E,]M O/ $%@:vCd|P޽+UD)!88Z(@미޽v=QFF1( ]_# tIgn?f_߹s]&'xHp'z3M؅86jefs PofOȺ+! r\P+lmmɌ^ٳg9s&+ٳgf͸LLLD2Tr&ؘ@KDgVpZ$Y7CEE9UJM6YuHρF.qܽ{AQ,}yLᡬ|eN5@ѓ'O/^|a# Of8Bkf֪zv8;;߿(Af]xѣGxHWHyl ޽{x?uc^K׮  K}34$HUڽ)2rH]MwrrڵkqL7L9~8N~7pyt鞔8(\5@\Ųׯk׮$h$E+VwmԨQٲeܹC,j"[GqzJB , )@b6;D?G/,؉F@h9 1cưys*I~GCw^;]hΚ7oNI?C߿߾};ZOƫ(5^hA:D#/$"} T2p@A;w\\9OOω'ttBP!IFieeNILMM>|(EY誆17,C @$|FzAfa۶ms޽eQf-J_89$ _kBsE<(["DFk#?ZauBfC?k/fQ%cccY<҈,#Nn>}x Sl(X4" Qp-pỲ2R͛#qƾ}v֭[r"t Gf͢kJ9rΎ%7oƒCBBYty@j )]%zӦMht[ c؊?c\j֌1Z95n2f6q-"Iۈ!H+.Z(** طo۷͈\~ՃOZU8"(Z&MԪUrtR###mmmh =<<В-TZ>c*lNc ^`` R?n z$HPZs =/͚yH:˴o~ʥ3kV.r9I2j*?d,BPuC;[i>r?&fq8{Y^ k#k.MM*Ux{{ըQCKK+!!?lbbI&UTTV r劍M ʔ)Ќ~q=@?QQQSL)[ *" ^^^.J{<_ vH&]ttv׏Jg.IҴϰB׮^\hOIʪYHƪUc;c.Ң44SM__;P.E AZd48Ȩzt/^ԩSԔ!0`%nnnJϥ---PgcAUt:11Iz OB|wĭ[_uܾynD˻3gCHҶ-kF"y nNyLk*d8*HZ\fMg/(8w\֋ A_# xFDDpś"O %RܕKbه?X[3c\0n[+Y9ѣ/^d™}5;z'M<{t׏WTqtvs`t?JҸqc{ooGhuyWgi>ÇjI3qd;"N>z$Hk#M6CJr2{"itNsf;"N>z$H#H6@Ϙ1#Gd׼4LغuJz]P痔JVnPԔqm;2@@>::]*%%yhGq\OL@sC߼ mAUˣr$ȹ% Vx`F`]S޼fdsaZA$(k#|r/ ׂn.-44 vIN޽R|@,ب1ǶNI 1_Լ!g9}D0R a&FJiMt3& GDD]^|s&_w7"\iH&x9clɓ^L߱]$x:G"<0@/*r]ZBi}1C vD|(OEA[TzX=[|rjԨ1uTE>cXzuFJ.111w:|p۶m˖-[jU???lyڵkͅ>|H?իWF*g=2 dbnne=$rqqAV())IGG͛Gr$ݻnee]'wQ|&$8z!XV`?h8\›40ET1.VٰKGA˗97SΐN:8ep+cAJGX"(%߿y@c|]Z=.*2ưN$ڶyY%ifHS3I ̚NXtc.->>{]jW³}'ʓzQP̎T;vfc###i|||tt\[~WBBBpp )CǏ6l={'H6lU3O;&"/һ'Qo6m Xi&xI#x #FHl!&CSNmH]*U𫎍qG+W?~KhdK*9Sp@ooF3m,^^-|S~}8sJ{ݿw¹c>rpl|{7#㑤S$/,Fus|,%TxzrP-`G_ɇ^QV{M2lٲ?l֖TӦM;uDO郞㟹'oߞ#9 )~Y6o{2eʀ dtfff+W+V̙StirЙJh9".]$PrPPPrQCCcȑtR@3/$ܹsC3@CЌSr1YwAɮ~_~RʪU@ׯ_nVf4{MG3hbbAϋ8 D6h\IDAT;͛HسgOdddRd \]xjŊ rRsTQQ!PC%|Ӑ޽tԩS~yH.޽;** YvuuIf?#B?#z͝;FPi5jFDx=ɓ'$3$;jՊ'NHH E$cm۶nd& ; L6} :4ull,[ߟ̱ ФfW1(cH4FPӷoh`L1A.P(vjUS5@q!h>})b%򝝝1 ~ikU}F7ٽ83fVZPs^3J14uyqXE]E%#_q*N>$^+h[}$`7ne˖h˗֢E 6)o}0[J8}A  T2;BDA- >%㷤:::DF@]HL )IdeeE 'JZwy3ZV-d%0 2#GgiZzA:t 9,h1b^ҷo_DMċ$ iȬB h"ipUo_ @S1}Y%2bΝ>}z.?9[5@1ЪU0ZRSS]\\@Ϟ:m¹FЂ H3q\>r_zK j*N>$^|||*VZE"ڵks-3gMk`Gr HP>q\]]  gtlٲ jiiQNJJ"KJG8իl##ϟpG7y#k{˰e˖ +WHh&L@jĞ)@(z-.q޺u]W͚5Lׯ_ӣ3Xr 5lؐhԑ{QpPh_# tI ߠLb ьI":`ZՓkQVVXх]3ߎx -% ء=A7|=6Dɮ{{jժV'w-ZZ@6h?~ ׯ )JVZ---߽{GP&‹?{JJRf6mb@", "hT 2BIj.WNBCpШQ#& [nMHH׆ US⼡TǏgʰG 4Y6X@=) ;t0=<SZjƈ4h38dY(U74y'1 /z"Eî1 }x k.)~; ;c/ g3S}&F JU8>_vYThT_˖d{۽,㮩 Pd*%gϞ͚5zٙAuIT`` @~}̋ABf"vޝCED!s!t:ڀ+H'x $.$@~W3whB{bFȦ.w=O8A(g rg@駖:ujݺ5954;D\;wE,$wDA\ā+`dd? MgߵkRB(xWt(sF׷jվJҾ k) 3;؋g{yy- Wʹ"ZR7~ta&9IFvk{-yLOϨZ¾]k Ĩ3 f;z9x 7S:8jժ%_)@ *ڊ2~, qʕ+pHNRtt4Гq < C"MMM;wL"thтd)hڵ^Liժ4333#>|0ӕΜ9g/^De˖/2k9S3gNo߾MNSk=85j4zhrLL靈QM4iذ(}t)f_z4W N8=w9tI nܸ!X.ގ;w~}6lT)Ko̳r?C7rڰᏋEG!???(j#`r08ٽ Ah Hέ~Fڌw;Y47ʥFw2E$F`X| _~`ڵkkeeUlYwwI&Y+QFի\; `mmm=eʔQF5'.K tp/9M|f͚dir#H3{n8@@eʔi֭֬}9%nj^(' @4hPtiXUV1@ odd?@O ѣGRtRI):u*)'oaÆ:\ @㉪[. .j5 |X_#P7 cƈ {W(>SVew&:u2SX؟|C ?N}‘MҥKYkҟ,!<܏u{v6e8~,}×w!"@ފ+ Եk` IьޫW/0Ad"ϼyۧOgϞ;K'?~/_f;\ǎ~zb+U/wERR ^i鄧/^H%_ԉ$]]];u4p@T7nk`zIj߸qmV^ LGl˷lB=vYvZLL y  fڷotSN!Y֭[Q3jXp! ܁qx8q-wl *~UGOQSB("7"WH cLD}M< 믈{x01"¨.Rx_#Pwq,ft˔C.fΝoܐv?gBB۽&]Hߎq_-,>Jft'4lH?xxx`lz -op$N,v$"CE*<BQԫO`UVm/_߿WQ~/}W|suvҹ733ifl^=c G/_+QM6ZP=zȿvJCm~?]GŦ,CE⊎'(#п&^H!k;ڦ8^DФǁ.Ǐ=ߑ#֯/VU6P9ZrwAOOOW.Ϛ5k޽p>}ڵkYTDK= &ȲGDK]FkoRVqlE;6xk3u;PdY A\YEgx@Fګ`6L%''ĸ(P <`kMלv퐵::kܙ'yj7o8::~(KXXe/J_ AYk׷#Zi4"v#dNMUkMLq`aZ^IIi5K~5FV\'; jo}'ʓ, $+"O'Ld Q)C+W{z`y;v,XJ={6Y,tH@GZf}Y ot4"p/7kn[181<ɲ A(R v:T+Z/[F1S@B6py˗/7n`= ~:|7j\?D]x k6dcZ8z^tixx/tH@hFdNB.@Vøl_)XY'OmլV%bIעJkm-Y}vBBkRvZvܕs~̬\r]v1a!ѠA, ͛7Wf˔)J9s G5kVFXJhe@RGfϞMN?1cFPQ91c_zUlY7o`1T(n:Ҳ_~߽{wF}￳FW'QpC'CvH9QYhOiJd.h^ͫ9gz?w. znVCJvt颮nffַo_d*EXXXƍ LMM5mkkkoobY&wkn.@߾}w4111662d\h5IJ uݼyzٮ];zӣGClаQF >o>>22_ Џ=\2 TbSNR^ iZff*Up͛7#655ׯ2"hР۷c WllljU/BCCEޱcC¯^ZEE僲ZhQ1wD:fD"kŊ'yÆ 5^ ={FniB2~ @(nݺlBS%Pqqq(+ vƌCCbbeea|rYThN0Yp!F;cǎE-MYq4ԣnݺ^Gܝ }v,a@-[eˊCBXhZX`( 4|׾}ӷ;$A(@on3̗wudNBA3ޥR9uUQCv p-Yh$7)ǏQ/P/Mz e˖x @ӬDZШz St1C@ ku7NWZ%&`pHn1G)J]viiiqhDzAKgkԨH(L  ˊ+D:22G/244AjiDR9A2?qȠ.0Z'NXrݻ7ݻoffؾ}ׯwT1={Zv wM~FCqÆ?9TȆx,q2?VSSǏG,w?YHn$HPvW4V>Ҕ,](м:ىTP|Hnyfp o߾ .@O6^Btzp9w!C(I|6g555dVmi"r˕hfvu&M.L/R "ZjϞ=IЗ.]AQ>Ĉ^4hM#BgΝw'o9CHVkS˕+GO3WRKҥhT(ׂѣ9nժ7#)@QFvpp.3\ƀaРA8!|@  /VVV(=lKKjhhp-[Vw_#PpbO/XW}ǺĩmN~|9˗q/^ٗI"'~U5 ^>9@%N>$^ @?K>J~`FBmff&7M&R UWWx" P#Л7o*T@b>2Q3tr t^o\\y-2 Y۾}{j}!fG(NV 0z֭;y摻Qn]brW9r$l1+! 9EK*E479h\k)KJvDnuԔ̆ ՐkFfH僇GUULNRnh C/8w|@ԩ?yJa.ϛs~Y ԕ+WVބQ29^ *r3~ߒ}R}Jp{y,܈ɮdbk4 իW\9$$LӠA+mll  Xh`MӦM8uҤIUVEllxb$%$v9h---}}}___t @@:!APP0GNQ;jkkw`%w]v 8pe@x4~$GN:.][ H,X@ƍr PM=7nv9hT%W^^^ HC@dEt XPdI>GGG555QaDIU=z(=]]]FP/< eE2W5߿==szx(H!=z$n^lh(~Z?vssèAnrrrz1S *r?|w稑#GllFx;Xw>[ɇFȬdY_@({]m۶5jT\\M-yc 4))\}⅟ ؍aرcwMaÆ&L:H04'a܈pgNO6>FB)!BիYtyݺu p\$ځK H*zaWgΜvvhf*<((7l`hh3w޳'=[Y @Oyo) ۷oGݦr;$AF I|֭[NQ\Ykqё#GZyʊ>>΁JOfAKWRRR)Sdkc .eK.9rppPUUݼy3WZp!Od߿=zoߨQF/M:%ՖLz G#/gg7oֈ"! $@+| }vtEDD̛7d,H$Y-tM: ͑kժUX# z8i⩫Gvqw_]q0A`hY_ƍ[.ZZϟ?wqqIJJb=B%CT$C%^ 7dh_?ߓWJhpMͯ 7.mjCVof=rW^a||E#$tH=!.A9+Wr~|ԋ A& =zoJgpw_]5j7~*.ǝ;fqk|||agWo߾uww?s w:$AO\7lw7@,7UX],N::Ѕ_%^ 7dhF;Olkk?xh޼y5zYڱ熅իbqBBœ9sX? C$ nKɯY#>:g)@1tY  ]ȕҵkו+WVu떵3gp^IJteD 6N6 nӹs޽o߾hzҥK/Yd׮]ڵ3446B5~xZejd7ԵkTTTP .VB*UZ3gϞ;wfEM5eKKe ]Ĕ$669cDq-T~,gf_'OLa$HP!x'G5:Ո pz& ]UxeŊwޅ*Ur @[XX 66k֌;[}}}///bG <'''?~X[[;""x-[6888="+W5kbw^]^Υ w^$z}/^|Isr͚5kݺu\sN׮]KcC^mA5jI^Z qR޽{iӆR7Hg:EN6D }PnGA 6lijk###ѠA߿O}oߎT2 bwqq%5 ̘1M4A4h0y_-Sϝ;U!,ݻw_f ݑ9{rrqpk)'o޼W^UTѣ ײeKP#dstNfΜ # xHh\a5jҥKk׮d Yxxĉ ȩH*C?`Pgx4N8qt:t)D25j &W5hmYfrmˉx+|fZ Ԥ󧫩zkwTUSx&]p![]CSK7,zpԙl`01N+:DUTTw_^?z@ [5K'g4Ba6ZjG! T^ȏOme5i1ieY*5!t!zpz:PٳA655$ rss$>}H2lٲVVV۶maR݃|`npҥKHp<|8MFc$>2~:<~\Bؗ4 N"" {"p@BCCG5N]]P+RI1>|UXK\Pn&L') H50H2ANQ&ݧO⑫SsNAKK `eN:u*A^Px*)6@i\ŘM|XVE.mK-@)޷+miʗK~{ Gc- J&#Zw9z|z)U͟}6^b4둙?4Ih() AEQ\nz;~JJTTpZg NeU]8VRVˋ^"tz! @C$hx͛7zQ=z4@իW믿\\\DNyO&^EpܹsdlرX1Sr@"b‾@Ă"G aaa G!D\V'//^!p-֭[e˖ܼW^4!y{4!-4;'N ֭w-@v޽i06 FT!+2` ?q9}!,4_܃){¢K.8(#~UF NM$y3˗S6@g5}gQRZ5.@J]QJ3h--Mz =wӺmKrض.*f&jj&S[Yﰍ XZ˗Yfgz *4?%tWMWjSFi}wl8@fɮ#?`Q:@;wzQYȑ#ׯ_ƍEN"ePǎ;t Wϝ;^%W @ܹS)#@oܸP\I)˗dҿ)Z澧TϠb<d*\[n]D8f̘(I񷾾>B:!%@skJq+WPe lf%ښ<{ 8]K.IR$jٲD611=zg8 KdV} @Ӊ1`n.\,1@z+>x>X  IHp;rttZ*=bӧkڴi?h/R ͛7`4/3fL,e*HP@㫣oVk{4~Q(Ē]/ t@g 6$/:uD^*(2e̞=׷o_bh&dgϞG& &3 $&&*e\ Yt)^۷֭=b$Wf͟Aӕ{f511DNUTTA~zzzL4^fT"Ђ+txx8=ݺu+vNtt4tѤI:WP#\:?w󑌬ŋ7E:|X\9۷oATR:D.l%h???bG^@xeq;FNw؁HpwdCƍwNJJ׮]EqAƔ{M^ԯu򴲲Blllh~M-Td4M#Gpm۶:&&^tǏ^"HGtV;޺lvLu+)H%[it}td%N;Tb\Ť~d;tm]?/=>7范OjVݬfNvD[;^ ~ n'yʕמB]QHb g[j^&:e[t[[2}Q(Ē]/AC [XX= hN:uG`hӦMd4G/w/ ΣPD} mmN:!KcccJ>Qx_t 611iѢYM$.i4jժEOKK HsР=}}}yxI6$3Դ{ؠ"$$Y$IhQCvAaO=QMZ -AX)СCG^ZÁ@CͷnJ6l HztHC9-`h!T01 8<2$^h81 nȐ!+VcرW01Z>_4 O`KB_H|ሄNY#}$CN84\I iR/:b>Ydڵ[Ώ"M^1:,h0zNqh<|.^@ ]8q"HZ8h Dݯ9DP堀z۩+eǘphܧ s3C/Zs}BbW) >Ld)Нgo]NAE2[79h&aaaU_$^ KϜ9YNkJŴiӄ}Ac# }i\| 48vWy}]V@AyxTف"wݡn:sLwwwh4x0d&]6XTYUuMf='s&nnn?f %Fy!_6\0:uаGE})+WQE@!˖-c9)` )x֬Yӯ_?6T9( NNN>v4wq?8!Rz}vܸqh q _zp(L@xF}N\uy4Gq/Ze~q8/N#.pǏ){Nyxx֨h"f`E8r䈙y!DT^'(J>!!!̫Ifam!|F1Sq5lq0\9JǏ4o9cl̙3goD~tذa}6M>}:`9sw^ G!GILe޹V7 |>]Dlq0\9˅ ܬj͡KJJb>}n/K.a;tXX6sHd!Ŕ-]1 J4ezBw}B~o ,Nfp()uSZHUO,_T:pI?"a_~G?Q"hrժU۷o~jժuY7?~r$۸qanDrf#Z}fF e43IȌ3;@l@@@\9Jǭ[`hORШ2e 5((|kJA@$ȀR[&޽ڵ+9T>J @wEb֭[F ԩS'))իgvʞ)(sk.#л;$=QW d @c*YѰzWb"иqE@Q m&h º;wdӣTvfҥlDV0kժ?fzڴiUT#CGEEQx%'_)'DD@3NA&ˎoWՓPxaÆ zp((`wa#bC <~U>D@zÆ GuBW|d;W\A]|1NU.B@s@hJڧAgI #Х-&{h L@k==/WP \|ӧO8B}"MM:%.?_oݺuDݻwwZvmKKKׯ_#p˖--Z@8"i;v6ׯM`O֬Ysz]vHU>eʔFUT e (I+8ׯ 4dMDGGc/\ x"ڶmH 0QSSSyِ`l{.I_ZZv- ***s2l0,R˖-cM6EU+?v@xj֬v(vtt,S m*uv1\Q88e~~~4ѣ$پ}HB'~F>CZj5LLLΟ?OǏBcQQ.%{FF-J6[@w,[~{zmffyA@1jRJQ\>@wAڷPJKHF[97.{bEZ #CNhZZZp7o@/P JǮ&-j21/vfHu$ +. A+Iw^>SBaƍ!_h֚>}:R9[?\nnnQzaaaD}(~ j-n@^ݿ_"}T(@55UV4Fvvv~XXnD"ubZ#>]Fkm;vo! sFa%Mƍ3Ce/b *,9.իWh:t-  .d ˰.49nJ*͛7#YGp*yaKF\޽{zÎuvNۼycudW,DE[ܢlh!e Q4ni]]]&$سgDZ/" - jҌ@j@lC`z_Dv._CTzü zM@؃yh</иal>|`o"YA!49HWU($Bm۶Y~VJB7EhҽJeM44h]Q / _(N`~3f̀_'J J.MyBə})D(066nڴ ɷC )8v.T0nܸɓ'gN 8-Z2eD/QFʫ,D _-_"vI~{zѢEH!Cl@: rʕ4RbERd 0^@Kz$Mä]/͛7(K~ @`rgKK;aHMѰaCf͚qpp`>v !˂w܁z阘?uꔂB.^ #GUyE9KLCD߾}VShh(jf͚!n:}tMdǏWhɐs &O Жlj)d\\\psYO_UUS+x-%x83gnܸң@ݻWqf޳gbI>M̘1C"OTDpi;tTVd/jE_|ao"+[pTDmP>DAt,X :u$| e D*d&..g"а|%J`Zp8@}'<ӧOCvQ.#F 2hTiit~)WfJݻaM6>aYH e}MHrlaǎ4 +p(>=r mVXI۷]vX45j)*{t\=ڥ3ÇQ"͛| > ³]@s SN=z(>|mڔT)ժz&(pQh0vX,80$$ǧbŊzzz1jjj(\PըQ|dgшB-Оr!0}`r-1]67M1BVzQCy>!9H GQlL U&*ӧOD|}}K,INz).X]KKJCO?4hW!@GDD`M߿2AdN믿 ,"J_iAL^D! b8(=@R/\Bo...sYltb!=tPװORpuԡ\WVmȑ$3:%O p)SG2ܪ|^nr qUVI 29ywH"mY]ACSTn""?z(? tD**-VF1SZwk93y9pH> b{1 t 5;?$GA199͋LK@@@xx(N–-_տ9ĉlt_Z ={޾}f"4d&И {[k\Ĭk۹NEK+qo:88^~kP>Ep8JݻNNNʞ 3gΌg##¢g# t>)F*=Zhݎ;觳8"3n<.P9Ew܉Ѹ]xǏٛhH:o8pbCCCׯK. Fqy067%=<}tʕVVV?|07iR@ͰUfSkEKq=;1<<<Њ"Ep8E .yH.Ł8;;ّdHv```sN4fP%gϞ]d+2'::!wE~n"3V|R6Q݄{ w/epCc>66w"{s(y),p%Q9:o077',n$%%7爈.j)SܸqPV>nݺ'''KKK1SH:pjYdJghwZ,Xȑ#?vW@C޸qcmmzyxxHިQ#]] ?p=z Yݺu3F7{{*UTPaĈr٤Ilm۶h211mݝ:u*]4BƌQis8"'GTT _~ܹ ׮]sss+_p~$zX?S)V+zy@ϰLجfZ.a(G ȏj隚p*UܜDEEE(QbΜ9@3gΤ_;_w˗,YdIc;wlѢ\`ݙ3gwO8Acϝ;Wl٤$Î߿ۄשS򭢢RV-a #$;.9Ж-[o6" 79#5Kr{թZ_M[jFb"M,4KW.Y!Q5۷o<<}B0<\2Ě 4GWq..dv ܹsƌ2,TRR7o9B>?6YEUEɸtCӏu۩RBi\,kgϞ988Ԯ]سgO_~̈7o|288 VZ MHH~:fh̙3K.M)۷:t(]NJv$IeAZj%{D4O[[v ڵ+0~tR2h SSSggg.9_q5o>BnLLL``>p2B(f'?h2m]:  4&ȯm۶QFdd޽{w߽{ُRHp kѳg(H-BੂTXqԩt۷*** .$I[ 2- ǎï?~@555oݺ}4ȳ/Ő}}D._q: P)/l9aaafReÇ{{{oٲ%ǃX^Crssߙť<|ҷoСl @[59hPVch%G~ }vɓ''OZliƌp߭[b+W͛73!mmm>---&7nXJ {GGGqY0#+@g ؈<C]('<~ ʔ)SR%ԨQ#66I5ߚ2e;wfX[**)8@8O*ln"@7FxE\5[ P@Qꪪh"k.\0Qfƍz* !z9s0.++Ш%5j$ ;v 'ϟ!;wDy^>i$''贈:thRRDM6666$ 4G>Wq΂]*U*11E޲ePIEOҥQXdj|Й3gPMarʇ=z| RͰN rܮtRQUԢ}KhhC+'RQEX.J*CS\n(#Hpr#FFFFҵ?4?3 YbB?~LCB}-YD$SVZUbllܥKa$J=|0 Yf B޽5kݻ{j$00DĞAPPEջCe˖ }6ɿ t@jժ~lS('K:tH_&O"5}&--.͡.dSp84:i9k.isNVnm x\C3pWPx"gs_Cuuuc0Cya?O>H둮]+WNiիiȔ)ST"H~ 0.9sO?$HʪUgϞѐuBIR 8.f._-W!Q zWVMKK 3[l&pWqEȑ#}PJ^zaQ{iSSS4Q1ńDe:::%KČp-_p!gǎ@E ~A ?{ZG' 6Ñ"Yf9Ӹ; 0`# -Tg.ͽ{RwaW\QĪP0`7o&+LtٲeS=ZOOOҭ[={o|Փ=zO7nE[[۪U†.YESS3(($C='ׯ_yTFFF%]SaQ!H?v._q: hgjjjGYhѢ͛P{I4EA |(ׯX"f|OԩZ]~5O}277'-|gUC,YŸaV^~& [T)IҪ>}̜9EժU5˘1c`d?Xz*1CH~5k*8kkiӦ+WnȐ!$бϝ;ѣ~u֕+WyD*&)) %*;fϗ/_NI͑8ߊ;.Y _ AҲeKh1i4 d3EiHLL\2thtPԿW)8Nz^6ȕttQ&$JuWpӅҜ%ԪYP߿_zi~̙C̙3Ne.] d 9ƍ/>}kkСw/_FKhڵkܻwo___lذ./666׮]H MyRhnʞ={vs8Lk&#0&&kR:PP vP\WX?̷L戒~m~{=hp ~òeS{=DjںZeKh$eZMސ5  XPG>|+@g|@lŊc"CBBH_u޼ycǎm߾}2eH5]RQQر'O8c#_|a3Eر)%J|s NG,mPksu~lڭKV~ MlRf*ջuw X5_Gq\| VGo,/dgsssL'NHСäI-[l ׯnjU 㯨H~}ƍl(Νڴ7ݛp=b@ TTKÄ7Y)RQmM,0111a7 |rMq:A.~ p#'x/Б >͛e ?Jpndܷo_wuu%J]拒ꚬ*Ѓ|Fs8q&mJ| .~5c}ٳ'QNNINN~}--- \g85Ñ tI@Ì555[nMxCIG,f D޽{"8te5wpp(wgɋ/6wl\qWU8>C&-|k(̣YnM^|y`++t^̃g85Ñ ttݺudsD; w}{c:uhhhtҶmۺu릫drŋZZZ!ق^C ,HNNN7֭[P@wb.yBOoNHHȰ{%Q!쉝]bb oxww,U| e˖L6mʕѨ,޽o߲y'S[ջ@4cBUogӉ˺Jǁf74y՝;wΜ9mŊNRV^@p߸qc۶mfffB XYY-]^b|"3EOc٫W/ELLLЬ~I=`YxqӦM~zڵlhzvUti6T1lҺu%K֯_חFݽҧbccQt1 ~F":{lٲeu&Z2k֬z۶m1SL122bCesx**0ŋat6yyDDu@@#Gr< "S+ ᣐiNzС399o߾?^KKK^pf"潆6v >IGѦM64+7n3?H$r)S M߲eK6Tuuu)QDuuu&o+LҨ(6H0#6T"qppPSSQUAмpBDF@2ُAG(\x:/؟1c@*U#Fu=!?+###%x$h̐J*rӧO8.\# QFpYz!IO|xٲe!$WC3>}X6{l2.QC/^!Q8 Ƴg$i}C;v CsW= 9[Pti\3pV'-%d)c`@6%WpeB۵kk.\HJaÆ9QXQ1c"阿dd8C&ɰ38] -\bb"nLgsƝf jHGٸ;%>}2!Nhs@oW֒LEsx$Ca0TAD?^c}r˩S`dTtrM:8ׯJ7oSapP1c4~ n IU$ ,:t ig&#"o"$,̰x}ִi˗/߸q3pDkR!25۷oCh l-GDKX}$ŋ@9]8(@#[H#A{5b0][Hh(4$R@zH#W,eҥyD!!۷/65GYÇѨbPPD8}ADڲEl|ذadWqǑbnи{Er2|D:)cɒ%ǎcsFyf d#Wo_η>>>(_zd:ؙ:d9|Q+Z߲eɓ!)a:_͛ٻ,XpeHy>ÌCCC455qDI&lR>T_ݻwv*V(Ih UVIO1T&%M_5;y$6znڿ(I$'Iecc@T\YҶm[̙ԏw {8!ۺu!!!!Ps& ƨQTUUe-[6&&hT@d5j+ )ShJu&ettt``EiC>}:4=]8G__3C A{Rג@KW;+m4pH#r_|y[[[fud>N=}@A n1`8v6;? llaj(o<a777(%4m~,gRDԊP'N2{pܸqsA X̘1cJ(prrPhN:+d.0C SΜ9:.Ih(Dmp7R"siD2`$i c#[#MEy׮]" *!$ tB @h}*.P#""hhȽkz:'3r jjjl.]eN=;.`rQ%GQ( d5k&An.@ V }W>X=z]MȜ6]Ɖ ۷[4o߾3r"#gh~v4*0JA:O+=cx;v GcgC`4[6Ch6*wqญ\pq@jmBgZI wPEEE]zUɇ$???(\ʗ/(t{R$00ٳ S)^D@#Df`?UVڵD29rD&дSofJY֣@V\f Qh)e)^^^TaVZ).HC_H{)ܸqhWHr$ ,\%@?Fs~)&C@GB 6LHH yfPU+RN2<&L6M[[$\r~C"mN!a"~]N̨Vfl"L Ww<'\+V4i H-CoŒ@g'Zq4aB>z.F;6oׯ_'f([,5ulm6~!D[o%\ h=yME]8 ܮh[WgqPH 6BIhӆ—ŋ٨4ؼ+Nܿvb]؋NEXN!?6lp _v?i}ZCrS:DZI,WԔ ϣdNnD$( {^3TR >}N\\\<<dĴiܹcc *pBcǎ!ȃjxXá,Y0rdӧOIٳg c;XD@wR3$N۷oc$SEڿ̙30^z-Z͟geYf c~:~Tv Yla͛]W^^^2NTH& ~E߆paaev y.]s~h)E G$ h,[ 4n8ynG݆:qq޽{Xx6buU0`!\Q-µt)mdd$ M:7nġ|uHW^Ms!O:Ҷ\9.} vZ5 ol}}jY ) FeauIϿ߈:#=%kr&qVVXܼy3:: \?5k֦Mp g ׭[JXY StshܣȆ*DU|k(C#ƍZ [uG>e8N\\6޿i\&Y6e_X*}fIYE?)=x hkk+8L6m͚5gϞ}*C6vvvNHH`o~FLEeþ(MҠ#ds~\9Ǐ]\\R?lR$+˗ vZń dC/ߗpUY;Sv'a6՟m溝 /UUdSO8FxY qrr 9v^pP$ߠ !:$AzŮs$h;88dt@!]?UǏЊaq'K>sxIYY SM*TGx1Pk]F*PDױhN>](Blll/^H, +Z9ąq^81hN{K5*M6EDD \ ǭ N̩{֯)_#1ѣy>ԏ8t˗/nɬ罥wuuv [[[eF p81hNW__ߘ6B9C>\ MY&kx|iWGݡ)b7nDGGC y游8H6~ޱcǎe˖"C\9ϻw=b#/zzzfk:S1)*nǧS_rl!VJ~/^,]@su;o9::>{- q>#).]ꪴ/,Xয়~bC3666dHb_gυ5%G>&ݻ==9E4Ǐ:;;#e@ Y=[n]v-[(%<:/9#11q۶mlhvx-RӧO[nɵkf&66ܩS':0s;20ڵk!e׮]dŋXF.]WK)b.Мbx]'^~mkk{6"#Ο?[?.v}VN<9O@'%%:߼y738e„ x vfɒ%'OdKC󒷴k.;PWmۦN7!11Q6>qDV*T) hkkӏ^:**@^WWw***?&\")DG۷l(و$''߷`i~ا0 ceOLIKڌ7%ٸrJeqIDATA QC---WϦM._XNPP[((K||1cjժURƍe6ҥK[hQr&Md{ f͚E?vXm޼yu733LN6na֭X3իWGb'^f͚>}`gLLL/^L7ŋݻT>}]VR[ t~Qǎ 4pppH30Q\`˝:uBnԫWŅc׮]ۣGy.\8j($$$ >-ԩS4 0lذ[n=G]Da###bŊ.\ hо}-[渙ES>|5e+0z::8ZB&88<^DRR7"|RpBsss9&h4bbb 7666de>}:| K LVVV?f%CyH͒nttpDЯ 6])ս ---[[[UdIДEDkСCXXBHټyCa# 6lٲ%i#` 81Z)Fx$pv$b" l<8q3tDD7o޴i`ժU{9rdR.^իE&M×Hyݹs/ĝ#UTTǾ!vݻk׮ NF!ݻ/WN,gnݺEtttf̘AZP{ v%b.М%99900x6p};G(kWlǏ?% yc6c?U;nxe~07@(7ld Ν;iHIR zG"QQQiO\}L[nMʔ)C+Fak {ա͚5CcwV*L#WA~qB(󱖊ҥKiJ:ZӧO'"##PQr#`'h ɦ[Nyh}DQZ A$(&?F";v|!m+ϾG駟tz[j!#ٝR7"c|tG >d1c9i7& 9|Qy07@\\\rAhssJ*^{~n h!Qyʕn^SSVBHҥuؑ6HEx~zUUU\*Cڵk_28+777ׯ_m߾]([zu& :cd&вmxŊÇѣGq81cƐg"ƍ"䒇Y Fqgy@re˖}M ]P@sj(?F"VV'fcSR&N($44сYƖZ|qvԍȸ r0-{ ř?[c# ,c@QAM$V`%2B)u#2.ȧL˰g.\ܩވ\펶+ympL3zk{9JT[,W~Hp`e#CH;v(777aݫW/4Ȩ̄$h z߾}\bu떽=r [6nx޽#.xРAB' f̘-`LJY͛7^^^Ķ)PψѣGwʕ+4 { %*ݣQY!KBB {7nZ"}[XA@}UPsE C^|ӧ2 `d]||<S\9׳ȋ)USW7}a$n!T B ot&VL_ :Ã٨.y:6/>(;Rw:$`0ۙ{>(C+rs^6l@{"l(Sɿ 4'|m+W)D,״0X*ՔS"a!5M!z5D*[.웬)gR?3]vg6vW\**Ǝ%'=q#=,=kOe LPBrs^釣shC\993Q=R?|@`zRM>%Ҿ[WDh"ԵBzZ,3@dtdEO9x?m޽ +++r kSSJ!6334yQۻ;+NҊ(װr<% ܜ p ws={'''ȩS) 6laleȓ'O9|ɓ'e`"ЯKW>ﴬg9s}EVp8J\9~6񣣣Ç"1Y/T)է]l)سEKdw#)u#2[S&Y~ӧ#""\]]ɵ*ҥK:4c"ʱ"8WZQDoh^Q< ߍ[޿oooOUUy9sׯ_S@[FSCMC]mH;#lI fd'0ѫ 6G\&qrsrrzŋf̘annN.Q'.X`޽o6hN7u `R̛7ѣ)T ?sem+̗+k(ZXX̝;w۶mW^pEulM\_iEqգyEhNrJ6y򥝝N$y`^Ӕ'a(gu\R[~ۛ:H6F)8kĮv];ÊNe8Njӻby]w&H7n^=U|G[ gcSTf>ڻHȽ(bk4[u?H*,~LzK]XYKk.\L 1!ηz4s^>|XV-Paŋ =ztݺud^HΝӧBJ|J|ŏؤOڵ+ˏ `~g6B"yeGNQ'n|.МΝc# ۷OyΣ)SVjbfmB'b#'$60¸lJIN Oeӕ{"?=θi-~7eITdSjFRoSmQ4]?QV/Hhq4Y)SGK=##VKq  B`JG3~diӦlP̙3nG>Un߿?6+pLf,YR$x3ɓ;wq"H\9yӧO^~F*>}rqq-4oߔU 4:KAg[B&x(ϘסU-Qpt.OhPyeE;)9>`hToݣeVQt!H fVSS@:!W`Gg{TOa\s8ѼByyVTCCQV%ɢܥK((xժUCBB$}2e;(r6/݊+"Xz("HSbrV{^ zwߝzY]؁e7}x2o2d2efիWZ5 |6 a777CC <NŋӧիW{97mtڵ({Yz-Z@nԩS8RΝQilrʔ)yꕣ#|0=z4fbp\tH 6B2,^ǏOU,$\çMdX"66dU[ϟ9$QIKM(mkMKyMC}+3 Q.zvM:3Oeqt^xe@|eБܷpz3\ ǎ\&σ [%[Qe)z2dƭ[FgφMY`nΟ?4 :4ǎ{# ֭,WrD={6lºogg06g̘67o,~^?#IsNNH… ѣ *rʭZCSABvClY7666(1h}}:uꠋ!'X_$dKKKthWWWaT]5| TJ֗/_<<<>|HF+쟎^4!40NynxoeKS?mp۷oOKK#)xK^x^J9PhDdMYjlZf?7`W2#M_f82GʑgtŠJszߢt  E'VmY/p)޾}67o(69ЀFŹ́޷o_~ 4ҥtc޿_,`%xOe7 ???dMt8(l)SDABRRlĠ|ɓ'MkI(%uHri{zq! .t<Rq )@S)_p?ɈB>2@}إ9DZ&FتTbXs2# g{ř\> C= ܥC`h!6L~yng}߆,.㑮 Q& kk]xTg+V c;;spz;ػ[Vh&'@b'Cc#̜hv69N%),p5'l)@K=ʳ^>|`ddwyn޼)[P,h===+++;%%{ G/_ @cC0{qww+oܸbѼ퀀[[[s Xhvu"{nG39w:ϟs(rDN&U3+Ǚfݢ{8O+ы{X%O89;w.,--qwtzz#`dɒiFY&x֭[=cDz7@pq6@׋OXVZ0R5D4\&3x`8nFQď޽\CaBQKOJcΜ9#& ,|.fy|.`hU]Mnhk1}>&rU#!Ν1at' K Ǐ8#gqVkikkפAu=]C(ϾnWGs$!fL6ϣp=٣Ff=}J4q&|> r+VmS/PE WZ5V_G?"GGݺuEix Ez޼yӮ c[كe8^*h2ga2rHuttD6D?^$$i e˖E)@<9rD$ /$SSSQJRkRRRȈB>b ~cGEx|z6թst)c"oMdq:.@h5+٥EKx;uoD HÇK",..͛@Oxӳ\jzjЎƎw6,SK Gy)ȁtȋRʒ|wE:9"lDpsĈȣ(@s_=A7nxiӦs3lh\?nڔ]4bbGGG4]D~0\ŬYv؁fbrhypJX$9/^|"j$MMM]'о=<8hwsS )@S .̘1۷odD u|.oRDOZZZtk0YJz:Odc#!6]mɻ"(?6 kW\fUŏW7Νz)\'6@falsoFśC~E:4h8(#Yݪ#JV\Sd=64κ }j^8ڒ@|};w\?A(q\\`ާOp-w::"@#I{N~E/ڶm~'O$_|VX4zTtgϊyp޽I0!KSV6l ,u\/5zW5G~~''_KeCgN| ioЧ'r3#'N]pT&:E:.tQp=g:跳Xu[\L tڒQ hMk׮ݪU+_ER-ZD&-\}$*p 4JLL4iRFFQ"K^^PKcvMe/"]eK3u006|<}9" ]#{9qM)ϖZM9Ŕc_d/T(U[2 H&xPk CXhQff&QX"K^^7f52fr?.\ժw++^n4mzCtOϿgsV-9յ -|rluc)-ad n͡rNWI[B.">kjjZVMzyy%''rp 4UadDa),y]xa5̾L3k(Ѕe43a+ \xˑ#$m[fbɯ&Nv˗g֎=& K5:Ԭx<8t>y(@+єGffeKsC&vED"t36HӿG ?&NR!&HO1iҤD2PMamȐ-}a:M_,sN>1zGB }5_2ke6Z]]m7;k>`!s_S@ю 򿁞docT4ߟ-\^d PUBE S*|?nr/^M{3{3g-5/.6uf9+]_bx ԉ'b$Q+)CQ%Qڠt6@H&6yki%Gr=S!&HOPaÆp+4g:~cv֔kkD}+6:0 G1>nH/@s*{܈,-A9ۼrж11mEH3SĄhHMS&@'E03U vn !-R٨l8YaVluvx B^஁|3adDzeJhCⵁv|BnR!&HOP۷3f\pX3egj??Qj6pF{7?MGԒ4fގ=<==Kz̲f3/ Y!JQe D->sL }nݺLz#G,X.%\çMUJIIqss{=!4gBs߇]i=ͩei\VgCyNJ,G+K؟?Qhd=IF&lEV^^#nžݚUbZZPƏD 0b3E;WNRypaXXXƍðsذa8ZvXE5j<Ǐ\xJHHCCʆ /^h֬RuQUV<ŵkgMĒ/zyyծ]CW\ɹdɒ4)a;vILAnٲd4Go߾ [ݻwp@t޽{?rHGGG)^S*3eΜ9?~ #S=z#Bg`h i>xMq鮯 TJ9zͮ)3KČ1ELlv' C_\.[w]uAj/`$= nЫkK%]_HG+qVYAa )wqq\2NӧOJtU`Gsܹ>B ͛ רQp?pך5k hpB2.Ξ=kll ᨨ Cxɒ%111v hDCCCGPvppXl :ާOSjiiq~ʔ)9fѣ )kF;w$)ϞI=D#Z|B/ o0#8 P3EĄ:"fY΋TgoH~=r ڬv%'n$o@3 ӵ([0|bI?_WiҶ) SNrT]v-0( ?צMx:u; onٲgϞ˗zDB&MҸOLLc+W܁pytӧbuK&$$\o)(3gM»v킱/.m۶9;;Cѡñ aDg%U05k\̛7ccc9{n4%%rkҤI!53ܥX"gϞriӦǏa,7;wV Pw'O7ŋҥKuС'0)U5| TE?d0g*)\6Lx9aQ"4gG3]La ߐzte\ϦUぃ `$jGuPdVO`(@,K.W, 2dEwi>|Deʔ}0XRMZZ`:w]~pvƍ[600@}}ݺu8^l <̘1Zjb @C;,, Tv>GGǏoٲȑ#a\!&.@߻w-bɨ(Yf{) h"]1 O8=(}((oooR ีk׆K'_~= t!!!m۶ݳg#d89%\çMUd20/_$gTt):-4좨X01Z ":E\^zͰ,|V]htEv dYHbx7}|)@ "9 D*@-[655yN:G!R@ zjƍCTT)XxwDbjS@_t^zAM6E3..Lʕ9s ^gJJ Jf577ޛ4p*a42A~ T<}ԨQȹuV0-" @wÇGEEM<k ϟA|||dDd]5HׯGa8%T@ҁyQ1-- 0l1҇*UgCi̟?JO( K+왊0ҙT}~:|ۂE8T0Z$z_fi\Ə ly5ЀIח~O:=ҙQzƤ^::q&|BAjK 8p 4h \=x~C.҄p˷oBǏnM???reiiyAy&hX0l!LLL006@ĉgϞ-L8FoIʔ)TF ___x޽ڵ=( &ddKjܹ 而8[q\z*Ν;` ZH@0z7oދ/b`wuYdx(N:a!T1.p &4| )@S~1w\xdʖ=S&3b8{hlnv]7juH8VBE:d){*ЪlDj"//kЦ&F@SELdeW* w磐 "U[rTرu'~ +Cm7޺u+UT Ҫ^]8{H}Fb  ĕG@Q;v@K.!ٳ'(xbr ,P8paÆWw8L2mڴիWb').|-.@'''hEW2 cc㤤>ǂQQܿh{{YfXWZ[0iS*bAvssKF(UrLZx ^fLe I|ge5L3S !cE$ŏWƳ 3'G?(z1)Q)7|sU }M\R4|do~IsbN`~> Y> b P%gEaYG^zݛ4i:55O\qV UTX+[£GFa4譧XpNעE `UH]t߼y'| =ЩSի+ 0tV bu5=zT\9?~wPgbbrd>~E *W FŒ.]U"j:u;-('N*>hŋ ʓ=S&3䚗%]_*es7~yIgeP]8p[15p5a aL?ں1YNu,Pڒ@ڹsggŊzzz#G#% Dqöo!hT.{!IhccCMA}}}tQF!V|rժUZjs9K r@p>}蠹 sέ]iĚ7o/bŜr"֬Y} '=eʔ G_QY[[ٓ= W1uFp_ d Cz op[%3! 'LPZ],hA%~F9޽J*P>>>pRsX5| T*ד^IΞX Y䚋=9v3U<ּ ZZLza%dbbDj"//)V0}Zd_iQ[-9 ԩSćQJܽ{G'OQ@*d ȐΝ;XǹG|={8Ys(yv=? xΝp] P2[l!S)jLLƍ;ogϞ7KXOT<|07Tݻw2z  ǂ/w*u8rAnݺlyfbXpc@Jbm> 6%KSR ;<%IΞX Y |vohgQ^nKAUAX΍fmXƢ3w4XSfFEf?}Siɲm l}= bDj"//Yf|0ܝI\M&+:G!Dl @T\ISR=|ĉ0 &#!M0IRW^h4r0X!az621$FVa<0->.g~\hmm~ݛ ئ.\, V䞲(/ۯ< Pmb*,:#f7[%[B.**\CFkTH\paff&Q`iB$gVykl3دQ.@o:|&VAXh%j'=rvX! _.O%J9LTrv$V~TYՀU!KOJdt&LK%Wyi ~|.`EQ/,پPkłSswExyR*F)9{vF8Ή9hZWW{/];6TY.FEeA}!rJ]p%OEUSR-%?~Q0iBϔ<\A7G[7E?[EMi hfR@Ck <^HwZ_*N#g{?^:Z''@;r_?<3<ơC'{Z7.NcBӧĿe~ I]p%OEUSR9ݺu"# M虲k2U!:b|@szT9ӳ}әlnݢvjGgeM#&ԁ]Z;5Y>lB}( g'Gg*%um,ʒ<UJOJiӦիWHz&OOO ?[@d7yQlI1VL6@F:[fBW? O1 L\UQ"SjX؊PmkSQkTQ߾}=<_iHЛWd2yù̬cOY~s;adGl{cܸ{eH\$ʜ3yҠ7Kgƌazر^[?V"\?,kSQkTTnnn޽##% vMm\ ̟8 d:z@ϝ,2)%^^djbm1gD:7f2C2q3  `+>dI(WwDFG% yLQQkTW'O5kť!=-@O o\nX;в v8vVBSCH|o!tbP%vvvϞ=#תU.\\rS ׏*Ky>HFH>Dzs֭[J%\çM ٱcU\yLеjT4mľ= *[2|i Vľގ!'رLMq}oS^-^U|LEFF.[˗/b қSkݺ5饢Rk )@SOOO{ *ϞI=TdYwk~ã|X]Z@_wpq Ќǵ5E)=cۼ@#tF7n܋/Z,:}JMw`?|؊Ֆ?M4P5| TGyxxd"gR"վy(0i8mwူ=9kṫFv|{g/'ytWjWгř={1Ĉˆ,EGGXfaf}?c?ng_[-[9S|t钟DW\_:{{pp0bk.@zjժUo߾ŻPQkvXO|3 /MMRQl&2O(B9sj./EMY7n\SW YpVnߞi`ЦTfvAVjVo߾Ѵi͛7c3ڶmۿ 9 l2** RW )@ScܹǏ'#<=HN˼KRy"VWɠ|4TFt`QK؆͗Eζ(n:"TСYƿid+BJ^ԣG;v옑h iq6@߹sQF[nʼnX5| TCiiinnnIIId|g*JLLLV:OrbVɠ|4T3zYSXfFud/%)hD!h]pƏLVg6{7Z?LUVj  ޻wX5k466ܹ3bk**p 4UѥKO?rH VQSing3}-~243L)E SBʕglE%hPtJ`fnnSQk֭[Gz<=Sq&3!ȕb? kvWbtFR­\ƏC9s_~Mh V~TY h{7mڄSQkM:dD^g*dF+Vmy VW%Sp5Aedd۷5ZDB~baՖOMM3f̐!Cttt 0a߿?lYM4 9eXLGٲeϟ?6U5| TL/^8qǏ30 :'6ysáwNO -bDL9C(hP8qm:99ȚbL=ڒ]iii8:v?z;w+ |… (pY>}ZJO ,Ȉ%gRa2ĸ7)ɰl).ݿ$Qia"1MyQ&Tk'OF؜UZ4fbaՖ<UJO)33sҥ##r&LFp[`.^4v{}m\9yUzm[j-1KyN Wcǎ!9UZ4➛:U[ZTTE(>hbO>M4)!!E3a2'GU,_۩Ak+څVӉӽPٳSE#um,=Rג*B )@SWݾ}"#Iz&D`>z8(|cH$z|.:Y֝7?CdKp'wL~è҈B6VmkSQklFzIz&D`t3j~Oô\ × gKM1EGXU_\\[ȒX]1d\1{<UA$\çMU۷3g={Iz&_t3VZ4XONDעBRV3_G_;2kdžeK\%bjea*i~hp3> } -P2U[RK S*z۷oɈ҄ 7!g7\@}zofhٍjU MMĸٽ>.l&<RSΝ&Y2ۤ4X=Çх(qz2U[RK S*:}YNFp ='h v#L<"5>@#q th6c=1n'-"D hC3>Rg^^^dtaIjcwti_cbȸb"QeIjSQQD5| Tꠕ+Wn۶r =ݻwx8GMmH]. mdh`E ^g2 _RVE}9~+&U<UA$\çM׽{Ȉlwϔg`qt-EirhPll,ɓ'Cх"%]'Oȸb"QeIjɫM8$  BCC!9EݻK/_FEsN2N#ݻ}^%\çM&'ϟTgʷ,X0gΜϟ}9EzŜaGy h,\ 1%bH ߾}ĉdtX>?z%tiqa}%\?,K^t̙F\ԥKuΏ?FDD#bŊDTqI/G0m֬ٴivѪU7smRu(!C7_h/JMM~OIOJ}{˗gffšgʟΝ;i`=ٹ{(@+lEL[ @wvIeoߎǧH^Bטߺt!G%~ɫד^nݺU# ZjׯsFʫEzÆ 8*22gϞ7w={t->h*я?ϟQ28LÇQQQaagZxU*z4EDQΧ2nHqQ2Zx@={6Uê-~s-=<<o.X 55+Vxڵk'Mt 88Hn͚50*C߇^޽CN@2x~r_pBb@.(((%%E /_={69 111p&Lؼy3V @ŹAyN(|)0O<ÇO8qԩ9e˖WRM9~*F@ː/ĹsjiYDVDuGg9Qg  vgdn Lj4[CPhb P%@re˖!g߾}c0:pݝ;wFʕ+߹smb"6l߾}[,y ̇Hzzz>|i~h=j(dw4*@T GGyӧ7oRհaCTr4RzT 6t 6a׾}{F_7n@pwA/wG1c sgmnn7SRCEGGøA,Udwb WAHI$'@Lx9K58EnEПwg2d\]?Du֡dPSdȔ+Şj1U6^b P%@sh<ʲ Aab{5jXetKe…'380c@ׯ_o)S<QA'NKz(% 1" h*5iӦ]v {p2ŋ[ںuknNVOlc)jl&<jJ9\q49݊ zLG<_&%%AR^rE\ | l sy/ʼ.U[ >T\9 З.]BN=ӃΝkggw#xlrnsppĽ{#B/_/.XY!^jJk4i[hxծ]{b--rAo¥=j(*Up #:uڵٳgֈa؀=zt <# 2(n9>>>PV-_5| T)xm"w*eu>|*te,EmnGp5Ҿ[E-h%K>xR٨_Pb P%@*WLD"Eرcuů\~ ӧ+VcA? З/_ƋN%}1@:9Zh ׆ÿ6S޽ S322~ws>@O4  @Ca:AYKK*@C0{;v=Pq׮]E\`ll, ոC@ab;ܹs(k99)\OJ'ã Q Ӧ%Gtҙ3gD^&RfW@?^< rtuX_2C~^Ķ쨚͛VުCBdggw<j J$\çM1W6/]Fv\0PҡIπs{}0@5+$^hϫΐxg\ܿiӦ~/(UY8y./ ?(As תU1PԎpJpo!-asjTs2ڴ^GG2kUh?Οe]~\r.I3{٥0BPTmrPQik_[n%gvYjiinX kV(Yv`~&thy% z4ic^6{Z7Yp'\\I6tZZ?Rd0Nils3ه#`@_F];6B 숾Bƕ!{bχ{Y:s0ڌZ>@c-ӱcǢ]^^:G﬙U3,ecVh]ϴeKS|T*{AGp{kkw8O%jJMjTʵ#3TZ]ް;+`3r|* )@S}߅c[hgf-Ta mˋl @4A5| T!.[6u͛z/^][H#:%/Y~kd/SfE |c}{g{o`ӓU4]>'+Ç!%3^G3s1=Z0eK1[0@/yyP,fV11bd}랞=hٷ[3 ?-\^d+HUBE Se% V|i@dS#l@ 'b0zϯ]> C=<dWDTh4~qzvPD {IsۦUw@& 4Tcqhd{bpӯU! Ӈ$Z4 (gk }9v@OU^4ׄ9 x+*U[\ TT >h*PYpleKtnWsa)7338  #QC x+*U[\ TT >h*PYplh-Y-9Ҫm=ò%$;Fz5q@d_&?>/`m~8͊g-`MTdY @ZCD1@C6Ԭ^MI46b{D,EP#VF ½ g e$a]+ GB.**Mp 4FgŞ통D0oRS[[ +hx* РqMiӡy8tZ 9<i}jT>@VP}/2j ʕlTµjTZ/՜'@C m?OY޿tlSe{{ۖhpU]ӈYox$\?,rPQik4B,>Fj@yXV]:4ڸ.<{rYCFvYaVCrs9x؎l ; }>@gbܿ~"^:9_,~ؕd0ѿ5dעΘԋ/_DB_.b7;nF6# ; w%7{G`K#:/02-mΉ9tBV^! ׏*K… QٳgcǎUVÆ W^QQQ81b_xUV&&&-Z8s r?ncc=kB[f v?¢f͚FΧO5 ԨQ#84rqNڵk8b>RH5| T!e*gnR}Vc>ē5'ԯ` ~+$QeIٵkH$pFF̙3߽{# 'N?%%nuxbo֭w)1322rȐ!VVV:X xmff|h[l{nDDDCǏM4?߿_zs΁?99dγg=zT, %RQ)]5| T!U?>eǻ rhߑzяBR- TmS _-t)kooL#zlٲ hVZڵk7N:{lFa_b\tQF=ѣGM99^#+Q!**E%\çM߅}֗gЦ xhPCt+N<~[^!ڒ@*u fmmyfgժU}m۶~:8 @{{{Ϝ9ϟ?߼ysСгgP 򏍍z-[6h eʔ#AE< )@Sip2yOڔrl&RWM"zX݊\RSW[< d˖-;wȐ ǎ311yieeqFu+W<%%'O|:۴iciܸq餗JSArvrD좲rvr \_Re3TsF\Wa~(UXr?_% #./ ')lG%[Pv۷o%?"$ѣGc=~L2o޼AM4ٺu+mll K&[M8CbI@@ppp֭7{λwmĿ}SQ)W5| T!=:+jl&<TE1}J.>h*Imǻ&5Mz1~gVxs޽tׯ+Tp=d۴isA 6D9?|P,Yx֭ݑ`_@6dcJ )@SifΜZ\#gAXZĈDL_5Z0===ɚ^ʒ܏).@%תUkL2̉x.]nzʔ)8J/֭[؇---!MN8ԩɩ9pYfq-h7j|$\?,ic$\çMҨiݺu Vsjh598^pX`ԥ GʒF= G5| T!޽{7ydsz9bZfqSkd KʒF= G5| T!M޽koozGWjhsXd QG%M{LQQk4B3ةZ82ח~tP%$\?,icJh )@Si4g w`6SZQh%0Y+TE$QeI3STTJOJ#=Ν;ܹݙԔP 7x n-_;YTE'QeIcSTTIOJ#D{ϯ|̧=%T@= )-"pƏgϒޢɓ'邒*W^-\W5| T!5׻kfG@C1XbؽmƍG|%\?,:vXǎkժ/\3f055<<'=yW^+ohڵiӦ #r*##cʔ)kn߾KR(׮];8k׮L(޲eK>}ՠAҫlݻwoرFžׯnݺ8ӬY[R}VZA={ŽD,TRۀ8z5 ԩS?~W\Av*Kw%Sp )@Si4H?|}O뤐v#{t9|d ;wk!nlrr2d#={QQQQ8eBBlٲ%<ٱ޽;l׮]z7[0ׯXPhPn7n܀}6ޅЁ 3g tRzn,gҥG (@ZZZ݈#^xjeeN8O 6 Nܭ`Io~%\çM3i6]>:=.fI橐wۼ94ENHS:%&((ӧdYSlG%[)eMH*TŞ={Z;pBeFFFnWLRc%CM49tXjbŊ97v…xw8sp~bb"\S @ڵ o>gϞP۱c¡v`}h MGGa̘1b ])5lϱD0RDĒ_.* @HH:.. 666%J,e+7[.r5jW@hTަMXE#%?@mfii&@2D5ŋ;(5k޼y:djj YfAXeL2K~ #" ЎΗp 4FHΞX4 ׏*K<]r۷OIaM=ztn/VŒy*T@( UdIKMVV %غu+%"[@ʕܹ3,;xPX-,DK=\F0@HG)(E"`, (J("1X#;syr͙88Gy3;;3gٙǍJl90`60A]]] TBhq^^%7nh۶k(QԩS;wx"rͺ46ǃ YF`ĈQ* 4L>c%$$W\Aq5| ]a&>>>.Wdر([lR#${*w&hB ^Xp! 烖I_t}{{ 6%Vq">&o_YY)3O4 v#ԩSѣGahwwK!g|lrCW 4~}%(/_8;;~۴iRdbboF:x=z500YL^hի~<${kL_hB 嗩+W4o|rkL> -luٲe.lһwoQPPk=}-Ynǿ kooͻ׹st#GZ[[A yZ--'\,ZH1]]TT|eddH!)΋B1b46d͉m۶AmOi8p} V&@עE KKK)&ߴi?ꫯ A=˲V$wW)ah={V_o3_`7|LnXJ0vXn׮ V8qBJ`cf3s@?ጩd_h່閖⏌>(qݤt$98}4 d]@sx/P~g"@J}T]e Ԏ*> 4^@whB L\'& >@M(AsQuA)P;${ݙH %h>.2EjGsx/; 4G]Bh@tgz &AU|hT<3\<{͛/_.--L3<<|۶뱬qs[egKsůqؗG1qdv gyqvA]c5T\;bhP7I !XBwYő H#yyy ׬Y( w S>j,WɌ鞞6lO=</^ȇKKK6yK8}4ƍHvTT?{Ai\zi>zބXaust[vMQAAwr2!!71$%t,?fe)(`ӧ_\|A)>epLڎYgGBڐBEḾ.u  4A4;;;a&B4jԈ 9:v숭l oof͚͙3gѢE͛7777@r&Mdff7j(AFqm۶VBa&mWYf}!!!,VݢE $o̙H?g[YYf6--M[[{ƌ, ;pԩ#Hvo9th Q^^KvŞOKS]\[~HK;^A6 Fr@^# Cl;w" ^1H B@DpuuUI"˗Éy橮3fܹseI`{a233ZYYׯJ%?cN` 4fIQ=ڴiSsPPO>500b={AWX!'֖FFF}>q/?xW^WfU͛qq~~+Wnr,62sQ};ubyA/yDN_yG cE@.chZQ6n(B777K.-AM b? 2}#G8p6vܙŋfffO}pQw}uO?T́p~P=z;v,,)um}EU  DbbbjjXQVV} lѢEttV [q{f7m4jԨO*h'N&߄yÇ۶m?K p㮏&ğ;Ν;;v7SZZ;:8D&&V+ʥ͛6_Ϟ 7ٸqС]tuǔz'L66nʿM0eh>hgΝS<J#"0%A)6pzz:~fuC|ƍ^OhT~W7… mmm۵k'?eIڴ{޽{jדN:KGk5E?>[+ D^^^TTXuS@[[[K7EDbJ}!jjdd4zhvb .`]@3T贴4--;wb}֭m[°0[MZB 99ɟêq^%\kgc}=;[ 5D_hzx@u۷PTE_|B.3f`yyc==ORrsAUʮ]p :55Kncc B{^zufa/2^vj`Æ {#ׯJFFơCpFTjX/M I&}zhРA4Eݫ>۷o n5>NhҤ !yʔ)x=q^o`15+Ņ@>n[4{ZYaɋ:ϏE@ co&orAPz(CqssE@.c(#FHQ)X'!hرCOO#Bڸq#vlJ\:MMMYpp0[/++Cc!C̚5 m o=`ׯw}'166^d [@8 H࣏:W3#-Fړ_+YSN CڷoK߷o_`[&hilUF:2_e [`>᣽|n޽{ԩH'D? 4 $PΞ=(-066nҤ*O03i)y@~>}8!,XI:t&75 4tٙmEs`/ԧp8p Gw5߅a{wPBQp\(v''', >~?>'mg&Mb|#lF'9rD:fqqq]>O˓3g?|it~ĉ t]㌌&їU\P([Ww 4|ֈt=ذ/^ԩ$EF5]x|\p8Fd@pxjh%ٳ֭[ {ҵkW={X5 &T orJ%Zo@mllp(%iB~787B6oz(mڴ Cȍ`oyB۶mȨQ#63/#̧(:t`3ϟ?_x*x-x'QEqkܸsd2 uuu0`bСCMLL춮-72 ƇhHϞ=UV >\ɓ'Ν;YD@@Mj_\: ysݻMճgϖ6'kR"iHZĻɊe˞=|KB@@Co}A AAeءׂwUӧUE ︸={#{QvC/??_z^ +++adJ  * GGGgffHaRRR||4Dvv+AH B]ĩql4(-[4z׮]8c nNڵc Veر\rL'Њ̝;7]j|}1Ty*a~WoQ}||!A4$lmmf(-=!VD}v҅ߑXgs06o|A>~~~ߩ>F9Ŏj&~7nLJJJKKC␘(%C@!&;ݻJV; 4%y&L<:u222gC]]˗''' S>N35k ĉ{GSEFkhQQzM1~~Ғ)/?mۆU֔0aۅҒ{++,|&9Y zx@?Fpw.믿&&&ZYYAsss k"''{˖-FNMj Ƌ/lmm/]$ֳZPtYYօڀa=:{fϭ4Z/III,_5̙C_Ǒll>tA||֭_Nn2O>1--=8s„ƛV?|u&&Mo.Xu1cF 9ж-[[:w?Fh³:v;uaÞ+H|'Je+6bAAA'OURSS2H B@:)//wttT#*]QQu֧-K<@H޽;~xDsrr SyuꚉEhiI ү߳)):ڱتeˊGhP[xMV?쮽GQvܣkWE7/> h`B>8vaƃ U{ pssގy^ë;" \Dq8ƾ}p9sW3A 2[RqHv„ 'N]bFӨw}c/p@NJs66#"*ڡK㌌WXYG绗Oa]͵C ӭQUƎU>(*b cJݥĸ,[t1U\Pb(7%)|bըɢ&+1$H B?&ݨU^%)faSƍ٤wX?rqƵlْl---e["=o/"/˗/FݻWOOoҥldwȑ:8K@@@.]:4hQM7oӧgΜ U ŋ6/m6 G!gߔĖ-[,Y4 333SSSXqLR_}-W**w^}=;[QU\zżiӄhoS<:}))>ho͈QR@K˭l}=}Xoצ͕Lz9s+_P>(%J Sg 4PQ:n~WwwhQ_~3Hnh$~?QT.FFF ,֭dϏmDHf&Mh"SzDqoݺuJk֬A%  hP1!tnͦFر4 Cς>Ԕ?x}obgg7d#N'/Аf81)4WmVmΝZɿq飑={zyyIv,=w.6*:2 71*?_ ,@T}{8JJN:u̘G/?l1qlêUX@fWf۷m[7:8X|. 2Aɠ|PJ(+AFkvYYYh I'& @MܡLk׮C qv.]:{l$۷o׳f͂>oƎ8 Ȩ{Etb͛7Ϝ9#}D1 Օϝ;Hao6# خ];(8˄P˗sQN`kk+u&O[l/f@|L]:uݻ,ׯ@\~=ȑm666!>>)%))C;w#>ƃ͚V^a!CL8w4mD_OG׮ut>Gq t~\^ǎ7lBdzUVV{";J@33kfV\y юU ??sC  4Ah n]\\jGԋ׏Qnhh(mS322-ȿSׯ_Ey533>}:EM6^]]-4X3f 1Ѿ.]d4y |AyyMRcinnmiqbcϧ1[v77?_ϲQNt;V%K33 үw|Q@_LMixThpۊ/O!ӹ{`@^# CSFQb(%993GNTc8~8=&7 4Ahϟ{{{Ӡ}Wݻ'3(r̙>hʔ)Ǐ^?>++ A 8xӦM'iiimڴ&xիW|]Fjꎭ[C|==]V6_|޼y֖n~7n ò7$$94Hddð[W+۲3?ӳgo|fN)(cVy ɹ'CB"7zx8Y[\lٳ-XT!mBBf pUڢ ZcÀE/V//,1'$H MP^^ncc{Gi%ן0ao8**JGGGȺ; M7@?{u֊/!n]/%Kسg+RYY)%+W:u^v/رcRL;;::6o<>>Ao޼yҜ'KH344<$$:nWUl47_:G}֭qp4Gq#GpRi^U/j;}3=ܹ311QbՀgSދI B,&&7Ë/bccqWNKK$P?9sƍ|] ~׵kWif͚v011ꫯ:UV&+**jѢ/aT{cǎ lf+**{yy!),TRT73נ7޽{C M̒hP;$F:tH&F9'ھd hx ٳgmosqqLMM=}kn޼I<BBZrҡm߾Qo{{,єkOD}X 4Aԁ͛7;:::88,[L|DD}ABBBCECu{]5j;==]TqagggODx &H DFF&'' 0?A ++ Uj  @ii ח F)jX 4AAԁ~Rݠ-,,Wc h8wE=܋$AQ7D /111^^^ jz*V]AMAu_͑#GD;.\Aԗ PSh 3EEE٢בOD}yjXi 4AAԇ5k֨>"{챷wAԋj$h {DD[f=[ZZ*ܛ :QUUZ)VWuCMA鴴4++{\;?AԋӧOؠ>{$A@^+111s ¹s缽]\\*++ʩ1H gΜF[YYmذ!>>ìkDZcǰkaaEcnDC|2*WDD[qqy,AMAjÇGC;;;Cn<<aÆiӦI Bglll0Zjծ].\ >m^zuĉѣGGDD޽ݻT!իǏuЉ" Ÿw޸87777o|;)ALzz: P;wLMM 5jTPPʕ+O:K>C%]b֭͛M$ѣG!;::.\jOGOH@9Pu:tѣ###7mt$ Bܻw^JJJ``EN}v~idtRRRQB yѪUccc LǏϙ3^kSRR\v-P L3g΄^r̅K.; n߾_-AW\ 'N3 aF6n?Y f...7n,n޼={WHEÇM BӧOΝOݝ 0rՐпޅ'O̞=; ҥKD=zgCppBNj /8;C N (""Ç!kCgCsMwwY B֭[]]]/]guSΝ;cǎMMM寄 PMCe U6 ŋ]\\vT -?3B$njyfPa_Ο?g$BP/Q 9]]]M/˗' !א6H@>=<< ̕[z{{7w܁xR-탼c}9M6PǍ}I^h BӧO}||3A7)))&8رcysuKgMppsfb}D;s-[B}! /_9:9yT6*QXbrTsbϘϝ/ Bܹs.\H ̋ 6E@___jЌ' s0yO3~e}|xﱮgΜ^:aM|+oxWZwJ^b +kp.Llݺ|v$z1cLjٳg/^?Q\uf,3RUE`g }!|9,TdeQ3G9:iջtڵk 0Sv_[l4iMd\ RєEΝ;=p6mg3fа W "hM{yi^ޒ.deeLQ ݱc_"K.\x??\X8r RhNNX#c|f3pNMI='8pS@pk.|$¬9}kv;vϞ=իWHgM,cY/#oܸ){nOOϧOY(lڴ7r, xvЅG{da,X %%/HBBZf҄_Ȣl1l=MllE,gBBB_*"Uo2S@j*yٳ'Tx!'۶mKNN^nUUyy=&]pVO&*lfԨQߟiӦK~W_ӫT iii|^;w.СC|Zx׮]š6m ^z6/p`}`̙3g˖-|`x&MuVyT;Z{ՉS`͐ˌ?軍B^AŬ¢EO2ŭPw*H@:k֬3\$7 P޷o<|&Ml@ѲH8p`r'\b[!}vQ+777܄ %Klt6B 7"&ܹD]S@g Ruǎ%*Y'L@7kL>X~mASjFP8rAW wuu7+Wb0F01z!㉶̵kb@Γug:ޜ[j<#0^;vڇN%$՚Ça{{JVKtu>F۴i 3(T^'8ta=z|a>v옔UaU%.oz*69w!3[oc h\PJ;W_}Œ8ݿX._<{8}ҥKÝ`#GGp߮_> :4vR ;Lw 4oG|yW>}>K%$՚43a4T*K B׏عq-} _1`8wz5Ѕ_~EU'v hj{0w~!zlllgv?B b[1pe:Leۧ'~A=|0:A6kLP8lƑ:@c۷o9rʕ+3eZUnݺ(f*K,ooog†0x M@!JBv}9t`` e*] ߽{7_^1J$՚tg{峥0J?WcW }&˖T:t9-`F$))X>"DTvzY&K(F+#5Й"rЙoAVJa_"v߿SN17N#(po% wD''=ᎽK,UBX,lf]vppDv+P@I StʿNQ—zeƌf hz5kxl߾}̙|(qU>A*J.V1['/x+TGb!M@ll, M@8p 74ӧ gXXXG~gPа]'Ftп;17 Ŝ#SV>qDy YԩS@6jР:?κ M4C٣ 7ZjWf@۷Nk׮~l"+w ϔ//Nj/&DQ@zjs:@w&y;Ǯ񥘎l?/VT ;2bZ wBlmmՆo߆<p,8"x{};+ac V_9Hz~qnF*Xu H@Z(<صk~j&:3g,]tʔ)~mRR7g^f>Hpye T 8!M)SNظSSܹsp,Y=78!)((hYA!N|-L:+޽{ IgΜ Z\V]'4(׭[:y ʕ+yAVZ't~?3l#{&qSyL3'R74Q1WMkHSZC@wl,KroyJI-V|oٳg֬Y |6CgNuF͛7ѣLȢ0VTiaSDA: "%,}ER hB+|||:%gԩ8 z{,?s9Ο?ŗ#--mٲeQ sG *1jyKclfz@j,ReK wjfa;ը\'jz[:qqqLᣵ!!!a͚5|WWW $o0ON¢Eܽ8^g_IȹN== f Ѕwww=sA֏Yfl5jy׋,ׯ_3f _^菠 M#CIhL@ Pw[i>-_N5ac:Z}7Ctٳg=<9[fJ}ڴi ɹu=&-QB5cz #G;ɸUݺuù(Gw}' 52f#?' W켁@X[beK[J~{&!f" 'z{{YTd k׮q*(";[#~oSh\6Q0K4/ߵM79rRJX6n4;#ڵk?N:e˷m۶kԨ~b4X0 hӓqqqo6u@n/ i h0/꡿o몯s`Wb;/-v(nQ{ЋiUsZ9ՕϢ\0Ӭ7yӼ%m?ah^k~ojÇ|Y)׽{wX-Vرc6lǣ h`ĉ,Rsmii?_[={l7:o  ity˪ee^UݛC+{ P@?O =$eusCޫ%Gg\سg@ ѳv\| Hpq|ɛ, hE|K٣y-zȑ#Xk׎gzzz&bU87{tt40tr]T)5@.]| hPFnaaѹsg Z*׮]M6ͫ_~FXh{i޼9{[ׯxGelOvڅIʞܭ[78+ sܹs[-[泷Ì [V2 QW@Gƿq:qBaO*;P4Bf#)$J7E8]^&8 텙RTaOh!0ߧNyKD)|K)y-z,vbcc>}:9~xLņ: (at!EY zmSNŔd{~ 4b\sT'SL% ֭˔yF !555Sٲ^4yݻsҥ 3d1Wzj<  , 7nj K.͔np7*HmWP4I~L9B oT eINA|{u0_(4&x0wڧ]WhX zӈ淝R E][XL<k4c̟?S3``{Mp5,,m15)))ScyfT _rп v1baPQF ]]]1֭[af Bא--8H۶malٲpΰPƵkO*U9 hv qM&z%(2kF}4ʪMcq(#t@a( $# oz"G٢bު%|}q^='I4yO@гܡZ,T bc3(_Als[ 4o; h`-A&延ؙ8q"Vg@>Y`U']xqvL@_p;TԩST/QFqzԩʎm4!> rǎay-^N ^'2ezŊ, h8 .;37O B9BBjuqrUReaNM|;F^"mմ@\m,)_VA1Pv@ |򅸊y:OJ gi~I@(o 0Y4/ߵEάYʋ23Ll{U"I֤I֪U [1 =k.agϞa WիW_  Uݻsȑ]q*T{.^/Y#ϟ?NL>qa1l0j;o # 2]6r( ~4Gz”)rr Įh]?=J=.VH+:HKܜ 詳*QЭiov&J[ Lwmc}v efvS_١3""2C.322:u$()ci 4]/WNꑉZPM*uӧ{C59#X3)З/_f1rݦMA)wEm !F+Ԧ_r:Я>Zl9s&#F,1'>Ve9 gyIB9e+jiv&J[ LwmcΆHII'15v(S̍7rJ*P@l+![@>'l* ~ hAُYPuݒ?mѹ`L_5ЎwAu޾}{Kɭ#jS/G`9QW@?" t-n7-bVch 1L@60ۈ9JK 4c4o; h`-A&延IJJ dsZZիmmmql JDazA䩙*b w6jԈmz߾}8ǰaݻw~ J)|LX{+֑pppO||-&yHvf` }P,# h@){}s_bj 2)j+.~ JaNU@職6b[(71hݞ?m7 *6?gxkέ[BsTv 6'Ny:8ުUګ4[Kb16~F?wm1D{!C0=ZF kkksAER6lIrgjj*n%30ȑ#r=lقijgƦbA9cdd$nԨGwuu4hʕ+իxvʶE=jccCt߿Y*N[sڵ lf͛7g֬Y,?~G3S Gh`9QM@}m-XzzCæoFz*KED2Tzf824G4oI+%%t3 ǔn% /Peee0; 6P.NAdy؍O~zBŎtëL-F6#F҈( x\Gm4Aʜe4?ۡj ĴCmũUX!IRbVOSpXqBZZ)5WN<9s̹sn߾… Ϟ=? UpBΝ;!nM m4#c}eߌn6S8j:ZbǼwޱc֭[S9KHvr\őFDaWEJE'eߩ\(H ѣSNٳO>ͣi@I>}:::2s>ȌPPMFhLjj*R>V ;'7,٣/3j<صN3ʪָ/|N}8 'RV>Rզ/1[/7j)Qcџ[(M UT/_9""tTTHlwjDt$$$EުFčT.ߦLv7nh˗/_vyX# 9qÅm P~˂~XӻwW-׌o@+sH&j/S{֪,[TqPCQ#I!8BL"Jm&ApP+ J+5nŜ7,҂6&`ժa"<|KK.Fdgi]H.#pgg :Zܪ!cΞ=ߩ\(X"""=}…'7l&Etb}j͌.[Y1rICMpy&kJ鯉1й)sL98Jp*:ݛ/"Cn+ hʯ{}ctvi&C\e% >Olfi{NJlX40۶[~?)VN#Ojj`1,XzeJZY܅ۉU-^2 hnfuĩJ[Zz@cwFУ*Y)e:?,-d`%6 W!Lٳi'Okh!A /錗/_N;4tyA(w @@gee߽{O|Y2zɠ,4#b*}[WE ҩ)/.0ރݦU; z\ҭ[@C?4cY <5p9]-jrWml޽{'#ǏioN111K.}!V{M49R:Xg74puu}hy|DƏڠ3l}~=^.~c8nlFuq1!_jݫ hqW Ud5P@Pop'ɖ5iMUz/v2^ѱgϞݺuꫯf̘qy>N]ƻqwwG} >MԲ,qȄb?-"2b|ho,9۞tPSiiiNN^&+2};+F'}0;lѣG_~}M*PoMvUY7+%q[MMC@(/SoJmmReshRLF4$(?I翫{V` ڕ_*TgU cθܨfgzS@gamnpժ~:H4SBD>:wΝ;'ZrTϣFC&Bzjƛ7pS'MiR/i t6m oW}OC @@߹s DP۷}||~7Nך ʅ^ST!7jDUihjp.o?bЫ6?4C d&Iݝ✚bdk3YFڼҷ#>])2 sGI_+NN>낞7ćn{ɗ/_~]Nݺu 6z>~u?#M.TX6BKv؁tF^(}űcjS;7OOO#G0ߚwZ3B2 n\SjE@s>OQJ?l(vrUIҰ>"T]-8GG~ hj@\@LٮaA9Vt2'$B zES&/^z|ab.zA@_zOОG>L:oFG7trs\0~{9)7ϗ`ۘоoXw!vu`g~hݫ =^7J;7mڔ*3gdx͚5çrC[ncׯ_LL˗/)))ܹs¢SN7nD1c W^lm۶ϗ_~݃Ohhh $5jHƍ߾}-[Q@8pO#L tB/~ŋ? ^^^Ǐ.yc=bD8m>:KsmXa|$) B׏=W_aK%o0s0'/1]\  D%$}OJ"w+![Mh/o5Ӫ ]>c:b4hS>4ݕNfWtЫǵ`?䘎[7ZZK^v->???>Mx .]tjkP\:tWK.M&LIɒ %JHMM%Kl;Ν;l ޽ =;vܤIvgZ`'<~L0zlPj0!omlllQ#Lvd!aNO>uuuK'NL<ҥK⦆kLe>Ty`6Ш0kQ9 BN|PF)@JF  嘾->F\7.)[NɌXY0#ͮI/Ꮢ hU$SlV2ʪKJ@&BPN>|?ծ"**̙ 669bsL巰*Uԩ6Ο?]r%x6nha!I_~|…Gg߿yf޽aAׯ_[xx8 ذax`N:uƍşl8oԨo>SV^Xw T(p9  _z?x 602ݻɀ`RJl0 ~ر̃ 8F-g^ҨQ# .]$1{lTذa 興>0I P`޽{n_iԠÇegD5>eIao}'rhB ^gJ7z}"0m,yNOLC3'ijUz ĩmTXy0 ۆ-ZԮ]G Ā-[ QKKe#ߍ7l~ < q<ٶm%J~!sJ_;w. uiyrVϏ:MBhbya;ovpr: L@Vv2k hX.5Y!M4djK>s挛|!(n׮]CCC^b@ kb:w СCSU322̙ӼT"g͚vXYYV:u;w4j<򰰰5jNWqF0qΝ,&%%4`٦ Tu+ g;3;nmRb|R1\N'BA }AayG(!"O~С"d h#(ɏݠjK̜ rZZѠ+''sdh}9td4^ye@?4x!U'""e˖ix]AlggM I:zf;tI& usqp9 ,ϟpuvsbNHXX؊+N:uر;wnٲeͰ-ϟC~33EmMNkk`3PɹVK&FS8HSp~Q84!s%]4lʧ,cq $dUKg$ŋ*qZo]B?ln/>?|9XiPeJ^fG˺?ܦ  U|Mnm3)&Nz|ZN ?+@_[?tB/^~Ǐ^x/G [[Yfٳx>NEtzoGc32 !v??;I$Y۶ hjlmv򥥷:UJkWkJ-őŀT}&69o(ػ|?RwiV VuyV ^2ݻgccP;\_Q ۫4+_pg p9 (%$1;wa>ZB[Ů똵O*PDPhdUfC[VUdS4w=Hh:6yQ_0cߓ4}\MZ-C7 &{D~8t)Ǐddddx9T5/N~:3Im u֪UPaÆ&-[o㓒._P[e++xxx$''={hڬM Y$'Z.~umvո$ݴvY6 ء:x,K_m h0%k,IO/L gp9 BW@8q,11qҥf`…...{fɲ} oS{"ɪʆ]3jEq \(fyz;\Y @CI3~r2]UrćnLȖ-[4 b7R5#7eU ;zeYM!ê1jw[6hɒp9 hPNo>}ΐo?.\{k2b6?JSηjlhu홹jǀSgѨ8s%]yc1Z5tc:V ^23GKB'˘uqQ_Ռw?@yEgꥯָX^{ڹ}LD6^Q@O2(P?M?f1UE!Vccݭ&Vub:N"u#+3|>~X}V(SJi&=_mUjQZ5K&tX\i9qsjFл wSJɋՔ*4c ^Np9 $V@[[[O_$`U/ 6#!)OUYiV=ZTf$0%q5,R0aB7S؂&|YOP&?OoZ p1m;Ǯ/3m %8 ho`Wi=]ӪTSJ'ŋ nc:֪,N6[Є(}||>|E6'tN W%{nSNT0ZaRGɆ`Pn0ϘuAr[}ذ|+&:7G5HѪAyXU6/64]WI@ CѣGoݺӧSL7p l#{_E$j3&Mj)x0a"S *Hډ|f:?OVD6Jȥ#7 ʱl_k{fflQW`Iwcjd/Ѝ%KNJJT0ZatRef; eJjZ֍arc_gJNFtzKaNfﲲVQǥ&Mh.ѣG1c1aBBBppmǁxRXZutJR&Lto?[%zW@<,}i׫q)y63'ijd/ЍT0ZaΩҗl1}0 gqvan>̳d4 jݷJrЄ"^!efg}d,=;oX(5Gr`?$U0Z,ֱ k1Ƿ[LgZ5K&tˑ#G*4 qwTVl.VL`صCXecNI}S@}WѩiEI@6aaay hÇ|Gҁm)+D/Z--%MV@Ǭj(?2V|XLJOS UZSI28!ʞͲҬG!j&_;=úU✚Jy*jwI@BD#S@A EB xXX CFaekioF 2kFZyELgZ5K&t %E)&&OS kB/Qz-XP]J磥ѣ~{Fl˔%$ !m䑩t R5nLljy( SgIa$U0ZW4x!UdBOz|:VLee0hΩ*wC[[-WTq6=j^u&)Vj|WR/>eHʕ+UVû7Ӭ`6X-LgN&JJ R@,K .RrOuo>%Tn'wlW$U0ZW4x!UdB[222Q@?OV&byd:tB/6QT:P`z4ICW)LmhU#y3|♆VM m9{,gggg>-0)#.zT4VƍiӦ'OiJ/_q֭,s䑩tt瞒2fhJ¬b h]:pfO<Ӑժ^2-Pr <2r:LB@?{,>>~ҤIOd%'O HMM_UQyd*(=w$i' 3,, ;F֌o\nZG}Z 7w1,,fO<Ӑժ^2-Cf>-PZp9 /τ3f:t(J VMd#L#%Y<ܞO=4G:$)$uu^ꥥT} q(T7|♆VM mB}q>-PZp9 ,_z2~Ds۷7n۶m7l^䑩tzĈb81 ?48&/=o `:t8\O7]V>6VM ~5,(Ji$l^zPX퀠fC=C@i_#3x9N#PW9DMЀ=d=?*###00ҥK_~]PPL Uc9k &zHFE3 |64Tؾg3g8YǨeh4C>y6 kl `M!ӇPH~ ?1%γӏka d%ݭ_*jS W333W^%%JQ}0ȸgG7sfHE!t 9Xx'uaY_ٷ-}G4<e>OH}hoQl+ UjtN44d J:')9r䈟͛_x!qsqq17o rrrBBBΟ?/={ecǎuAf! sNVa9j9aOLS KRSM {=ZV:*F=. пwxj)|Z8BW'Ś2=Z?}Ea>99a={Ҽcf,7ȧfj6 $>Tajg耺h XWIG+׬jGY{nTFDDq4 8ޙg@8|9=@)~ I4~ gvJpfxa Eȝ١ &h F3zO?Uzh`fv}GrhUǏ%Bа8dOМSnn.za+As;Z :q6BU,5U 6{DzlZh :qԼll(m"RN(cMUt&Ҳ+Ms Lld ?ShhhJJ۷ő+P{ 14ٳݹs:::֭[N*e@3B'95={ ^ѿAzzzBK&RG-QbA ڑ#G8.[ȗ3@/eި?2}v'w=R+g93AgGM9=na/8T}_Z={6;DU3H~1lu]KTD}YG3VL =W!0~B'tNH<Ν;]bْm@:pQh:=Æ 'eL 0$zjlllbb͛7pw(P)6l؀d2" 88???,,Շ~[hssC8:QKvUO> '{.]zi&ZfT2-Ɓ{VDX54o?_sȐ2Zf0!%v\(4紴4Uq=E0i@[bbb {͈Jѣdw'(2fff[lA?O<9t۷mVʡϟ?/,,sxxF+Q#jGYuiTɸg-e@_.vha9l+9L/CW(>"f|gaÖ.] @ʺw^rnvE&Q#)}*++C%J=Μ9o z 5v`;&_LөS7^23{aUׯNzzzj TD9’pqHJ'åⵌW6 p |Y__F=Ӆ:77744Tr?O?| }ĬPWF"8kC ;v߿!TTTnJ [l{Kre. t#vX޽{=ݻo]-2Fz|ҧq_ej|N~: m[{j+}4L%Yٺ,^n-ZNF{RdN&NU@J WB70?iaaaO$LZ~GH -P)a@wee%ǩiE\ŋ4ggg--Ν;ߺu+99yРAǏwrrWWWEAAA=600w$PyYJJ_(,,,Pѻr Vz666B8:ꎈӣGffI;tO2MCo$@GgvևdlǢS/RNb^;m&/û.]VZZM #//OV}t'\;77700[EfCjcގ/-;wnK$x\;}!3}] ݻw~SQ kTqkǩ ݭ[R" ĸ8\HUNNN uGL~> 0zbs7@%C?>`߾Ă: pWgR>dN6h@s$殧OX+IN555'O ˗/JfƍUϒw޾}{׏?NMMi)O$qՎr O eɓ~K/tm߾yZČbk5s0fBYOu2g|9.?D [/ta?Cw@15H#27@F%ҢziK1FOuu*Vnj7<:t~^R!Jh9N@u6kO4xydN6 @C;z}q~k׮dI(T]GE6Td'ݻ/^ CkdccMffu릪:tP m۶AQ%>}[ZZ;vHI~b㲒W;J,hmbhzQ...uĉ٘Ij*cF5wЄHgVfXH gL.Zڭd9de|񅎺# FmKMd$1Yh`Q+Q($`3ڈgl+ MMD!ÅHݻ觙(%/A^x1o\z޽7oɈq_~#nr DVGv Bܕjm)Wx>ѳgO`h sRc<==ɈA)swwis&&&#qՎr iQDC  ۵k) èߚzx'ٮ$LQUQQWSy3+w;mEfe~q֝ipe@kgvL蠉c/M֚/rYABAI?~ZaȑrXKPӦU3 @b- (ҫDG ™ס(D3S?m=Ȝ1h8@_|9!!022o"[[/^ر#<ͦ`td_*Z_ f.`:]Ob7}?%EmzӾ&<524{D*{ :1RӆmxhlB#lC`$B'^VV=dϯ>}zFO/$&WSK -+ۥ5;G^zQ5,K1x(O񍦢zBy9Xv4UˁGj>} ZRRD t onnn}5//ǏCsΕ+Ẃ9O?1gx 5'NpqqΆ3y a_ABCCݻQQQryJBj%d'y xZZZl;u|ɽC1qYIY%ֳgЛ}E$BBL?y$犁wmt/%O<8x(YVgp`͌UU(=YT_/OŽ;i1D7ډֿk+jh[_E8(Mshk@*( i DY=I"$:Yt@.](bVQ3*- .JtE!>3x9źB@; Uh/*?**Դ̜1h @eee Pt/tJvzu@@Çs1@޾}6|}}_hPiiiFFϷoncc/,,д@ PAٷn݂@x4r!@AAA/^t Y -~Q2zzzzZt)4{UQQYd N(H/D [###t@PָS޹Վ ST6#j %(&犁wׂ(6PH&5B !mx,I?,׹[{0 khgUz($x˷)+7gkW`b><\Oz;mڒCVaaѩ;QgHI(t/^U{zVv?8_VݪhMo9zS3:>f M'˩~ ϥ:ѤԞN/Pc)p%6Bg= jyB0q9|4h @梮\ؾ-[spt^^ޫWBCC?ڵk47tЩO<@/\KȀ01h 8P2@sff=2FGGC4Q׊ /ehhS2L-e.]ƏԀ3(zj*&C:t8Jexx8_ hᥔ[!EĬe%v[PrQ MLL$jq4Q$Xj/O8 D:ZiVQD;<WywF; MzOxz@Uy:t耾:}͚5 ,077/..~AE_g!!!Çd:N f=.+ɤPvw1,.aѝU,ثrJ\lɣHoJT}JC1fw##f>Jj Xy.zέ*o+w' ;QyM/ F#{ Аy:eYO!aͬ:ˁ~oi) Dމɯѽ{էNU؁:cjPchƼ МdҒ)X22SCgϞqƝ:ugϞP}^7mߟ9sի%ٳg( @m rzbR#;w{;[靌_d$۷ ,ׄyܽsV od4Ciqa13& g_&aSig)]Q:@?͞[E7[+- DaCDV{{z63 ? VJ἞%8<515_I,>7>cTlMO>544DU97Ln>}رc;N ^ajjjom5?^XXKM"YJvlkrF7pz~o}rqqA^ɸ+ȸt@\Ic =' &dQ/0(!j" t_&7tA/x"T iY+ȉsGvl6[+t@Ou9T^1h[d@!hj_gd 3珦!;4 :5!^s÷Z#ĉ䝨Jd9ϘhUcZ2ѣGQULqjf Nɓ''Lpar7`GhY}ڵLIĬeW;@F?&x0%p)flB/^%Y O p6>!"B|4Ex!@N h7MuƼfYsL{ط ,gO#\W<@ck0믉z&zr2$=[iPCy?%s Xvn$jkЧ{-L $ *.Y|3ZAL1mذ$?~V\bEEE(gwW@[l+_80ƍd:N )f=.+5ڹs玭)2A[6Wjx/Ϝ}S*zPuiw`6Zn@j8<`X6t. t4gNR\NBfQh=gB\;sԡBoO[L'&:׮?ұcP"hkzxx򁩭YΟ3#LP% eFswtЭԤ34dE@EZOW%t}Ƽ@+֒)D9u[y۶mݪU͛7;w6ƎH''/^མuVPP 4:YJ v|FbZn'\u)]fFvD X}%pB|I4eX>5e/hj4'㩥^x>`]^`_0~ {(T6h.K u@ Icc;Y;3" /Dgx$I~z[-Q2޹{x@9٣&%TɎFY%t}Ƽ@+֒)QÚ􂶳۷oGsoܸq%>}<{ 攔rg1ڷo  ww/8)Ԁjoq1d5bd;h^.~:J t\ֵ)'^lʩ[p4.[k,Nm,${F kuF!A#\[iҧlp>bǑ(4 3NJ".sVZDGYQ)f"r~~>ruue[{,rlh1hgc& FPuW%/?!*bZDBk R xdzvpj@KLLNN&8r֭׵k?ܹsFFF߾}W\yȑǏ?xW^ , wfFq}I1ŬeT;ޞ.&.f"sT*/W`?: Wh-CyBV~R3.j:@>7 -KS=x_: ws2,V4_Ϸ5׫A(x֝v<|$߿%,7eМIoX{*+H3$.YH:b2]dd- OvQ2x9s[7mѢE? AٳO---''jQ`rc(ӧDĶڹ~>}/i 1˻65b`V'b*BO2 QF+%@kV;81s!ُ&/C|ArqV/@#8:q .DTQQm3EFkYLZ*'=^W%/?9lPl=jXe"zGg% cTl[2ThW^E!SqN7oÇ=<_2)Ah1kZS]|=PBBNUT1@5}`sp4d kS~!?@4@9cyX'hdo[1ܼy3* p(fff(j߾}(:{꘣~87F;t28sZ]W%/?۔YCFEMy?-]' :ubEgG1o *-B 2$]PPj(2'P 2׮]파 ^}UTT&Ogb_N>A?v-scqR|1qYm]NJQc&l0 @aq6ZCU{lK.cԊ e Р6ZjxX"8kF¡&'fk%F999T;v젇pGGGKϟo+ʰ4 r2C  rXmiuS[[HJd-@XS}O,Ҡ4Q8YHHFƼ@+ضd %Tڲe MPFFFvMWWwʕ4iM`` 6l>S3QJ|Վ՝,ݓ ?ȫw>n5pHhr{~E"` ^3Khh"& E} 7֛OS,g& CQ@gX:W03 I[ʜȢ ^YYIK(Aerש}ll-D{( ֬!C.7u'eH|tC6vd9o4j!JPB*hȖu1E%fHy;8VPYd uHO[Nw2Ϩe%>jGWG?A~m+Q4gH` bT 5G(`K$-X!z/rg"גt$JRߔѡ?On;wҒ+~jU՟뛫K~%] CE!KM.\ҧ {^+ė72ZAŶ%k?~#GTTTdeeEEE^XYY-,,]^ޡ1SRro//ۚx7[r#[Æ*q%gdÑãc,/8_w 9|*rݻw曭[B#""\\\LLLz!aѱqp ʷl((ں/,ZUUũ ]}?qIv`#%\]ܾ \dק3ܸqcA*mC;CZZZpp=\5 .=BV3 UmCh +޹mJ*9ypuqpCvvpMen:zx1Xz\Vⳬv0OD8' ^dӡ#RC;@Ӈ_@L f g*\{@ӗ6Y u`PoC&ѡ:U^9`BҠp4q_iGeB=ZJOj/cc2K~%] k1I-`Xj{fE+SԘhۖLJ]tav:::vv^~Rs7[0yz\{ 9I-񵱵UׄqCnMNOPl["l MͩT~iv'ay\G, LLxC"5z\Vⳬv0hO{a&l&4a,bFs3uoELh0`\|L-NxjhhL&־ nvuuEoE!P4RmFOz߾}zu&%.YwAV&K}S8mad:5u%3G,3ZAŶ%'X曜\??C##k[[/_؄;*eA d\^ZJB!a>~@pE>~Y9pMwޭYgieena!ONXiWJcКZZcO7p_:3;''/=ik2bOmQs `{nMppr644t:yg+TtGw+ Udӡtɣs-]^繵BAĸ f:8 zszi@Cz6NOcqo8ªiݘ7Ç:::/Om *Z^0U:`mK;vyxYXFojX_o# ULKƻ\)\/\{{:w\Xdw`w'M&?pp>x>E%[ٞCs\=Rn(?@&nodkpA$8,B|F=.+YV;TNǙ̍~_QB!;wD!-[j*9K~%]Ӆ@Pۯ,3xCUTx>damKFMJQXlPmNHGG'!#X?[D~Ѧ}EȘX8BLrz^92\fvN >Yl)D BFK{Ii;&p̬lx@T&{ \lHDԭzz\Vⳬv0h3R*a _o`~3z49K~%]4ח0 R(fqF{.[nEƼz9x.=`h^tApGgwݻ`MY 0x̬l_BzDrkVl*}\644DRYY~ZXX4nK_Jtɲ}/KݬL+i#cQ@+XdQ!Q]rl9 Rx&0vTi<Ի ޠdic+[zK #="EEEE&$3 6/?֬T;U@7rT/QHIII{b@)W%+88b&DdfD8hLAPeim3$JOOd~މIA sw]2ܣ!yF\Ox֬T;U@7'{C?ɚ>P7~hYTE~%]vȄȖlýCgZAŪ%S$$weNG242՜ ШuY%gL@KA>3t,*>]24 "fۡ,)E&$9_n*lTqTKtt4*۷o򥳳3YTTT(4SEUW% Nmaa1.o3#[|2(TZ2HljhmTP<7:/pp}&fe]X9JO#s+ߠwqL5ұstnȓ_{[YK&©=+ lӮ!afV|Η[*U@7WKpѶÇxz3k5rc,@2uJ[U^^*Di^8VXj !106&)RAQ[}q'Vά3.W/85̤ ~ 濓l{z ,%IԐ}Cc'AVQɶW50,3xX/fU673,1c]Kj]`t'E}KOt.}n⍆d|Ǡ[ )gFZ&ˍ7Pb~^0??1-I@h_I޼yⓔ1tn"}ŠUKFGB62KO!cs ye Eߺ^ yƢ3 +}#3h]o#04025 *(hy9ޠ:goKJ۴coJV$X5]2 dIElo9PPDˁK ˄K.^,xcRQ.܂v%yp @ݵͩ9ypV‘ | tYx$`\9_n*lTH48uhzQ8sqۨp; m@\c(@?z`COOw?䐷he_S3AUK >$g|[TKXk[@`+W/@X0k;1&t7@u:!'R¾ p4tX8>΅y]lY% %80 G7 M : N:fjtD~syKgfuLx Qp8'?f r9rV\0\]2zrkVYeJ}#ysrܙԨ&tګ5O?_;a ig;08J]+CaC]MܺIՊ'N@ϧ000s"J)Y@_ *V-_ʙU-ޟl%q>'="^kVYeJ7_I??ҕ'nz' !Ӧp|>vr>"`;Zڏѓ\=@"Fff&y?Z8Vf N4UHqbՒIf`;w0)YA+gVfb`+ -c&覸0F8'(K0 Fn~D2go7 !fl%O>Zɟh:xҟyp5r,8{u]m;]ӘG`k|.fU6+ۻDpc4BU/dh :@BɰuWn+w}/>vɶ~_:((q3["ƍq`[UKƗ>jW9[mܥf7f,C_fT+gV[92yĩ<1ǵǙ,EK>wJT ~jg Гu GNB0c.H>ܾ}5S-P^MVئ62 ܪuإ >U:ha'[n`ᥔkVYeJnS˳Gt@+PZ(ـn쪭wbb!ҌFK](:#7hU\\-@öh/5|`z 2h=G=gofl+gVdDWh={wGEU4Vb!>a({È#54m# |'n ,TjXZi{`xOp>Ģd'@kig|~b}aq]%7^6!N0@eKYZ2@O9aᶁuDSyBȦXFd&NC!A@cԢ:)ռf,7Wנz%mn7#O:~D^ݱKWр Џ2g}N 6g$5)ӚO B&GQh%SRR;тrիjuZРBL@+Xd|1yo䘞Ox: g|"^adoPt w9a4$|v=p_}-]52j #6~^rHpq{o>(Vc*6`ΑwppKTvN@wg/P?k2?i.>h%ݢ(d6r7Cbd ;z"ԜKۄYQ5#L@󄃆ago2BRʙUl)>T^kʌ]/ɗz(d**/}z8i·x/Ѐ}A **hC.(.=zh9<.$$3vlʯx hh/^8rHݚQb*9)hF(3Y FwXY=e.ѺZu/?%sA8k$K3xiP?2(?..-^@+LS% 9VPjɤ dԣ@4ƫ}h2X>?754D _ZNe0P=mjd +5Ɓ n8PrfJE P&1'v ~|Vع[^a{ V W |1-^29M;mfۉ Fx³)< 7j۾â()󙙙ȮWFl`e D5_uM'Kt_o+W{ -@4[h/5|ltQ hhB!=y:>QcxkQ } .(@ݪM[JZe%lx5}Zo8 R%SZwo X111Gy! -K& y@ ? MMHhYZÇmIw/˗/꣡d͛W^o= B)))^^^vvvfff|Y^1˖MʼƘZtu N8VBTR6opE *ܒI#ASx"> Ј<2ng|-iނQ @B@ǮGUSW'r :/ؐ,};oF_E H+--ԩSdjƻx =1Sg?PACxBnFO5FQ\o'}J.Y}ԧ dn߾}%8_]QQQZZ^{H8}:::|>8qh )T|1 jK @ (5(!ZA-&K^zSMAM!=uZc#Dڸ^ P4N^hr18ڱ4؇+֠|noNIp ,=27pH9-ŅRXƲ۸q˗dsT뇐%wkw섦ɗz؇CO~Ez>dkjIN8UqSg1| ZTKp1c'=+ -Q4YHz#'Р :v(5u q;yh*1>Tn(Ah@7<*$'dOcൠ>vG7x<<< yB/ ! YbhYc>}z`eM+Ĭ۷WVVh PԣFTgoH)xڏzM <]9 h kO KY};ey)t=pf Bᙛ'} ޡ4Up2h5#F 2_ SĀMMBѢ'̚OD Ye_5#3$$$!!!==7\t g ٓ'O ^|֭÷2??-X@+80_A7 0;w:tfG :'2(2rhd)?;$6 a6X9J> Cbp;d255iN^s=Gktafp;v x`mllX Ly]v㏀ŏ=z9Y}Iag273Ĩ(|ӋɛR2hN /P&c̪OH[nv O۷=<<իhhK aQzx5q lov\iGǣ? Em?{ı5`nz{hw^{/rb;LCI !?z &<^JFee|ifv;~ o7ya{GyU0n qvCS<.IIIq/(( ABZMQdbÖJYjjuqaU>е1`oܸSk׮mݺ#""ȟux"W. ;d)LG5SbO/JoG9ƥɿ"#SDupbpsI >E`ǏCwPJg2a[C,5TvT(4`RRR@s\z@*zqa#+ee=793ĥ fmgAK6jPfUFF /+h(.ަi\yˬ@eWU#]*u踸8C*Ć-o *SuXZ^^^lEGG?|%SNA=[Rq3G}}]c#[+/vHX l&?3ϸM0e㊹_7=0;zUhؒ)dM 4#zGСC\C >CXRRB' [>jh%V<#52a- s"8x믿NJJ 7CyyARG`S۫)&o&ガUbG=sϛ+.^+Q"""{bmm}@ZMQGGG$ۓĆ-o j >U..ʏيuJ} nEAC)KΏf(Xݯ#3wl +[zr 1;3'[1ەY@Mn.M*ne5-Х ?N_cx9z48Ǔk-L2p@SˋIbÖOx}Z]\G9T®]Ї]t(^DB^XC/ֲ@mNr4̀.ª07ߒ-ݠ :ľMq)H|fTwZ2KY7}qd 4p؜:u,&^%/vt$KZePAPPq (^817ml/tWg(:(B H]MtLLq ׆}`4䯖5cpunF,ᗴL[_mաju uFhzI6{b`Uرc(CgΜ@B *zߙCQRDWZftS,诙 #c=wo ÖQ4 2@:z.4p=>o!K#=x)ƿWEi_h^plO .jyAвMŒ nI Ox׬#?wiVܹfܣ1}nEpS ?#:\@yg-=}ᓧgo\QZ]|c87k՚Y8 !ql4[R:tpGBY!/BkYO'U &*c"֍X8H:ߜp,WaڗC}͛*aoOΙ0#Hl"d?( 7go<_:r-[O?'W yy`0p̱#7F2} tVRW;Qh%9|g/ ibbyp&u!VЋ,wV>S1 1"i?fn5"Ӿ6tA۷o :88(Z xSkϯ% reP*ac0} hmcSfWPE 4йgLWR!Z0w7py2a 2ѾsWHlն=Bbb{YoبF_, ĔX3aUk'C*zqx;3+ O}2"ۍӱE7֯@{N @?'~IJ|jL-i_hYi\>3`m#pH?q*U\.QptEAPA:>Iz%Y pV7mڄS999B L VЋ/Aֵ13o6xU xq35ǬC{Oks ҹq6i.~cEœ`~󀣖Br5777\|,/Ӿ *XnN6)yUMB@b:{Y@t7wi6Z N?'+Ћ<~CMQ^rX_yx2NX̵h7rEju u{:;۴%ժuMG.pQARXC/־@?8Q&fTf U H33~g@AۋNgo}8T)ߦMJbCP$>K0kݪ]v܏ޟ8._Bӏ x']9[%g.!7m*dMN"i0pi}~Ĥy4bccHPA:'Bءu0| f|/_nu~[[ۯ*-- ~_ xhіPkݺ $T=.O?_www%Y^ۓzӾ *x9>|lU<ʇHG)>ޓMBdi)T=(_zFc'f"@h3}\_"AӚh)l\0PA:wG6s ~F"itL:ij9J8t萓,!VЋE^ YҥK.?3YЯS0MQX0 c1U2\@6 K:YZ UO?'!Ἶo ?’3e%Hxc11E̱¹TWV (_I;%-禷lZ4΋Qz6mڴ~z|⒝MF9lO}Z>?Brxyyֿ~%%;9Ӿ *駟x6B|h@OqŃ@)pd ]8 &zFeCӏ\f9D8sXI!&?:|-[v-x믿τ]U=s Cᄈ;wBPXX q/7;jn"2IX[x%';vɓ'du 4hڼ~ΦoH|hUDXa#(|P 1+VCG t4 ݨIc&z',T$ЭڶȎf텪S@C0*MZ5uVhz\z dP$s,;)0m5B?9pe6;GeΟ>}vfݰa!Uos Dž7|:t``{j0%m n.W\!kGcePh˗/%I7UC UO?^V WANx+=̈Fu_~!&Üy>ACPԩS666- >YBEڒ#`ڗC!OPPЉ'Umh:YԌ YeUbmce ;ju |#+cXښrCw/]DLVO&ͦ ?qI׵:ľMa28rEEEd {xV&J':/dMih*~egg&O4 $!)iJGò4)/#(&155퓞&ԩ %kD 4yD؉_zUGBT5E#[ujYoƠ,bo،Q=-Yǥj:.#|3S\r&V*T)°~\I_@Jx&eKSRT=Cܻw/$440":t5BVGl N\1e4-2eRb: ,ة!^mff ͮf G&oQ? Yw^"/!sNN΅ tDPMEIޞdMih*(,,tss~:8޸qK,d1 !5(*C)z Fu>|`w%KM_UAPcQNqqGAk ؘgȔLTƀ/|Z`T{@` |x 94vhď˗/}}}{vAjYa2$_֎[EcnӴN&`\;ZZ6)XWqmօﷄ1hݤtJ(xq$m`?E#Aŝ-5EDDU/C'%%Yhb%*z]@nP`\qa|kmֹw/TI/uN¢N*ibWQ0PMSN*O7n۷/==5040tIF`Rvm@~؋0怐00-wwwS_k"͛pILLtppM[Uj4/>-$ni@H+KeK5lllD"qus X$4|=KkE}N)ΝC鱳!5@UN]i|*@wY$>2zXW,@fYh_Lvqbgff&YsN֡pR=֞+++lZlե2a=]\5 $Z7%Tq!60ic%w-ʴ𪶠a*M۷GFFݻwO>k.`;{{gWȘK3RTJllh_P|rpLbPD 2xCPP Z|ҥK`˗/x (u4л塙kRm>85?1# .9(:!0,88%K7mtĉ۷o;f/] v?8"*4nIHB*,rQ'ág.QRBS_W0h۶m#[>8X9aX5Bކ>!Pdt B m]#]FF1cV"̍=,OQ-dQ\H͗TzFJLcȪ|.ȃC[r 4.V),qzbߦX:{~SKym-烥Eq}ܥ Ԑ8XT5L%8T)aÆx%r#:+V?(8$*&1줐8FYA_ N[vC< $ $m-qtt󏍋-턭m)+))zSٓhoo 'JI K`4Hj{B Ǩ0[iE}bSc/ O rHbbcAҎ9r* F`w```a7+x{zW*ү\ T [U [./Bs`pq4n8~|+_*R4jhׯ'|enݺߛ7ot׆`ԗvJkɦL41$3!OF%;?2^km݄l^SC1o?q3g~:޺F<5Q%j2Ab-}\1> \lȅ:b_4w IĨhm2pd{De]/!6@؂d 4Ep޼yrʄE۩ |%PÇ([n]!d%66 OO @'Xecb`=6\->> >>>A˃ 嫯&eժUIIIAAA age:b`Xu7bJg]vM`>TTTsrr,Y@NCV]7? j̄Umjapp@ u}68y 78::Z Vz.g6$F@̑; t@G3w{oɳHX֍V-ӄB^>>lQeBA α3vnUS^^_!P=c|7(vZsyި65>*s޽dukS(((."ւ@[7Ѷ>? hdo"ڼak!xo5a+. ͥIc bZ"J6@oқkpB?.) dF/4loŋ'UGJ(,,xzf>'4?s, 8Z wh^'1} 33fV$Ibf~ǫOƧUO evkA*O?ݰ 2] 1q(M,ﴁ͈=0j E0lA3DӦeZ~U@O>ש(gϞ="y,4'OZ9N832[EԮK.8iámi=ܨ~]E#[=X7b4hH쬼'H6߇!urnb2s]{[-|ԁΧT/8kHsX*ي'R_ESœRaV#%9tX  6"=f"pw8nWD>pfuϷWTA W9w0 0llL35Y'~z߾}۷oՏQ(ƹs)4Uh{KgyMXXn[x+T c_CZsl#s## !T\[3q8$s5p…vf*`)ւ@iӇ`e'wA o /Z66G:MtLSz&B;P!֠a*0믲&MH޽,VP+ଜ}ȑjPj#7otq4t YB;@Ä 433dV˗/{lUа $NgWhk\݉!Lԋk?Htoɚ>ׯ_ɱ>Nb-4.>M=}o:> c v2|8K6}\5wUXDT}Ȩ̬L h=ʕ+p޿Lk@@!d!M&4O^j@zz:BRR Tٳg#C=]F_>pmQcfFF2KrEsv?tPK<''J Ctt4kAA9Շhw@S ? ~7``gNAΕw[q)pgp" b!Z S (ĉd ?i-77dӤP M ĂB3MRyOXt5)))Z2+ :qG_׉:+x8$1FQ\9] ׯ_kMxsBnQ6m a[SɆ_YX0ܳ'P68޼y'{MܹsgVVVDD>>BP@M 4'OBdl 8x n_pQ>G# a[S>JcPP(gϼXC.P٘oNf+X?E:N|}!l ҃%MG ulB* Eye\\\,˃\>z~bAO}i/<}!l ґgϞ} ?Ŷm/&Sozz.*CB?k׮%&yNqwTn],韢0텧4mA5իWwYKM~Vr֑k:޽pT)s MV XSׯ_'D,韢0텧4mA5%K'n'2 !lWUT)sq8(|\ֲzjܣ7oސي z1L{# a[P (z ~3%YF<4v}@S(~;vX&M.Pk)))ǼӧOي z1L{# a[P (}駾Ԟ1&.__ 4B1, f po2[bAO=i/<}T/> qL{9ӠԺԏ|k|4p =i\mŋn OcGDmgպԏ_|ᒜƗHC^I陙DEQP(%\\\ػrZɓ'Q={FfC?E-7mA5%Љ%ҐQF//@S(ٳd~͛pٵkגOc@ a[PM Dbb3_" 9LWm'* 4B1 pb*j/W\{.C?Eц@M4Quf} 6x|&SҨ[׃+ҿi&uamCb#&o +5_5?2zf2oX_!l )15СFN%* 4B1^|N)wX"cٲeK,!x{1 4h1ÄM^b`Jݸ@|ĨbRV9wE1%hVx +}R/mA5%>X8@S(Cŋhd믿^_p̮#@h{?ǰ)@1qҹ٤A=CP swXmp'yOƍi)&Fquu:Ml_!l [@Znh b(_UrΝduV@yCrOchڧRͅ56bn$;g43ԟƜ_' b{dwAaū'X[7b@6a=MR;mA|c@ MP ׯ_J޺u_O/ 韢.Wz%O۴#Л=Ǵ7_׹!0ۑ]po/,};ٚ-olh?T)ApuHDzCp]]]r=Su:D<_%WD`a >S=Uꅰ-olh?T)SPPK paeepU2 4Eil^^xMkשe= Gֆ:ƶc-.27Kރ''b>7kA?q6~HܹE#[a;wHtJՀ &kJV=PAB?(ϟ' 5k`=DFF򟤤MQc鶉|<7姫1np~V*DB֔z-7 4Bso2ې())J믉\*% U%Ґ*$.*) [@Znh 8p1++3<ك!H *%)صmS\Drl]\PP@֔zPMD{ghǢuda[Ds:vK;[4 Z,&7֊&i]tDE+ |d' c+swzgaAB9111O& fPginhܽ{Ѻ$# 3*\ZȚU 2Ӡ((R9mb" Ek&"0Y\d{R`wdFFbb2Y9)MP8s666ϟ?' MMQy˂ᩤTBFm[7u|chԙ @M6ݣac1\_ \EW#Wƒd #>/KNb)1xe)g)MPG,&''yʛ7o"##Z֬YæS(ϓ'O1p;/>**#[@s;H@"SoڸR^&<ȕ1ܳ]+.;R4cv,{ -jw]fnKѓ&UY{* EoFS& WbXYYݹs@ST J2"xFGAPdh تa:'_dL[#ji)K)*LRɤ" ӻr (ob,Z27|#E.CFx-yA3/u kSAB-ϟ?AMmdffC'&&b h䂃k-L Ģ #gz !Q>sm":zLkv==tS MP˗/yK,JII!OB"A{x5`zPSt#JͭT<K~rf~K:4nD) z@Aҧ̒[%* EC#'(<޽Ν#.SeƬ@$K|cT蜭ж1LcW=bJcɉAaYpq,B1e[MICDBo޼H$ׯ_')Xv-XHHHII y2PtO_3&&ڙʤf[@Lh oܺu ]d6EϞ=JK#OnWjj%{oWj?|c@ MP;w_̣(h(**"JM lnߏ'~V=PAB(/^$(Ī'JMr,=Cd|c@ MP:99|̦(*WΝ;)BQ8rD}{Br2BUTpPP(zž}Psrr|/==̣ Vo޼ikk5yĉʧu Hߤ$2C[@Znh ?$$$LQ@\YJ}ڵkD"pYTH2|bɓ'`ޡP=~pe㯕+_={]&ߟ,Ԕ@G;6\ortj!* 4BrrrGQC@_|Ν;Gfϗ]zhCEhhFF--_ΜJ]CԔ@~]DO% 0&Eh | %d6E9 4rJ2ݻwhga+,&:&kC+*/g"5(Я^JLLd ?<4&8&*#* Ex"^@@GQE]XX5|A2CAA/!xdop(s__OWɢzD 4sphfiaC"N-"D ;NEe@S(O"y̦(X@GzS4>uxU[x'fZy̴v9ܬ\T}317,k}@J|[&0m5fAꖬnCMÆmlSd C]xz:h RG#(P@cUgeeR /1q12w{t2626meA4MW0lb0#&XC5DGGyս;+/$2[]v\,;GT)˫W:thƍK.\P(+++ݻ… oS4}/5'b`:ud\zX1)4"L>ie9$hϯ۴yv>@3=0L @Sɓ'KHHppp\~Ϝ9?ߺu1?ʮ3|l())%ᅦ -[$%%999oڴʕ+_&AmFEcK"jHsh}fC"}4MBLk'L1ew GtkV]R@ [[@s'@?}ZV.s;v^T)'Nƺ]ѣG/A`cVR~6 !,,,&&{IE]![ݻ~m}.^!H>3nSX X|95@?&oݺ.VQ;QFroW_}Eֈ@>vDqS0냅L'UxIlF61+bGL]\*/g$3CK+zd/*yᑔ7~ٽ{w-Nޞ)SdǏW^ރu%C.k֬8[ҥKrJ΢x)d7nwU: \?L<9//AAA(spGi۷o@Fgff޹sl! %mӦMXjDv"b>JȀ ҫ4 [CP4,b x3]~ z퟼" t׊o5 Z2K ^T)͛'O:ggg_rl{ gϞVVV3f077;vX|IDATǔiӦlׯ///`FS,X@hWWW,Ӹq9s -,,0eĈUOW>}&_57 Q\\_H$5k[Ta'ܬziV,M עYs|/6'NŔ#lj-OV/G ]z?3A -8@ST޽{QR~'Zڵk'oaA2zjHyA})(uD,_ZY|a,V>re7߾}o߾駟V^B8tng54j\ZZWWh`p6SKP^>sy'''A`kDckЉܲH(! \Wa#HiܴYxc"@gvK$m¶T "#= Pm.X`x1Ms^^igϞ%+Z@PIuhj":7lܸOD(/ŔEa/-6h HiҤ v 4лwb\(hvgΜᦃL2%66'3glժ{v|iF B|ݻC؅}=|_ Ο?ϝ;7Zw}miiYnqƝ}:uT"}ŮD `7qH55֭[RlEJ7ߠ@C_v kD[w(#cc en=D[; e?b@f[ph|ah +2*7 8}?7Hb}=Y5pz ҞE؋ 4E)={O ʅ5+-g~@ZjŜP--B.] \hpΝ;Ë \rj.))~أG&*sePa(ٵk׃^x1,, ׼xb, f}sYWށ`˭1Թ#`۷ouDZARSSq%=)x\\=y$upp`W} \BB@eBܾ}lK:R#=b :z]ޤEKHlآAcss YYKbP ?Qp&TŧWC@)|\/*Д#oooi!=cĉxj,,,$8ۡCwU^Ѭ@Cݻ_Ϛ5j.**qƑy’@`bjj{=l0}Eϟ)v”ݻwrWF鏸H5olW-Х@O>vؑ]. yl7gϞuwwE7 (`h :ŤzsB7 5i),$\BÙ:N6bPyw`C;8:*7c8@S“'O|||ƭ_,\(;e~wd^i[\1c{{˗/yߟ]+fdd  ԫWrSɌ̝ˌ}۰aCnbzz:(@/Xs0700Sߏ)`ҥ>O.UKu&n*v3H]J?`VZ]ɽAFX>|0dӦM2v,3OrE`K49-- V,"cIp a4i"˭(ܣC |3,I|:fOӃ3WPhHhX_|c 9#RRcL*вk֐hB@u|GkӦ C ,X l ŋsILLӧ8Hݻ׾}{Y]]]q3o<\K.8ۉ'Ӈ [R\\ +kٳgc#GlG>@$A~…R`s cp%qqq7n܀MJ|ݺuһvjll}w};@-["D*P-nnnp9J4]BmE&VV򝃆kkʷ ŒVN ]}QC3yƦ?wbgl㒸:xGE ehIbzN2]7MË/@θ} OZjI ĽgR̙Cd}7rY1]дiS 0yM_.] q۷/|;ݻwSg׿)/,X߽{wE|||p͛GGGoܸ͖ !`ᚡT~X'רyjٱc]_$;9hhPIII_uuWP)x%d4̈3=>k!֚7hYOUfuzĤӭ'*n=1p [JP.7}t݀ 4E7o.f&Ofy&lIէǏՃhCb#r_nҍ74'O8;;ծ䋊$ɝ;w3h?#\w|v%wMђ@{D4jȨБ:^ Mܣؐqh7 8 +pf5MZ4535u+;d{NR MZP.>Çt݀ 4͠:oPBusΡFx{{y:333&3h%ߢh&==]Ah〆eohccㅮ>D\GOph;astx{zAٳܠؤyoZ6S7(b(BÖ$b*&0OP\lV5kP 6l@<?vwwd 4m ۼ<ꄋ={ɓ*={yxcȸ,^MNfuR+uI 9N1,{v 5-ETGd2]7M ^!(Jʱc<ݠdvm`۶m[n%nh÷h`)..S2rrr.]D,] CɧOyEEvgAXy@nߡ#mbfomM[ig_6S<$u*ДJl޼yǎd;Pt/.)i۷d4}1$ngoD~NlTJjlGի֭srrt9aaa_|ܛ ݃c,bVXZEHQ_\am@J:u:uUQ&{C--u.5|4Y A_P!ָ( t>@_JT)|~dvLyyyxb@'22ʕ+d45|k3HҢuŒ ~I #8#wٚC{?f.!f-- nܣ Ҍx7??[&::ϺU?o-Zթ[ΆY&e Bldl읰 ZBmКX;eo_!w,GLw}L @S*8{lRRو)O&j6m"[#?&c/v͚Y55*hK.ٕ뱌cǎ?yt]r]*PeuN&&Z/22j٦=߰Qg-`1v<Yh%Yʼ?q*۝zw瞲0E@  4 lek:x ; 3|tJ)#־aLd4 M5Cy׈%m]q,JGܹs666\׊>r@ c,`rp!,[x[l4llѢu[3cYXc6b* 29P(,,  [03=uvINNpk͵@yk>AmUĽN:u롧"fu̱sc A"HyloXx,6 /^CNt--eY]*ЃFOѸ0 `Z~E=`TjUߵkD")We8889s\^|fonʛR TP;1-w`R IU7> !VX )#CT)2?nRt[nvvvEEEdvm#??޽d#i4} S ?KMʃ1Qf{0*;ٰe("湱~s=wm¥ֵI2`0~" HUQ[IW>p.c{T-3m!ػ6RF(@JF7j<Gdٲe[ѣGeJP9,.]਍!π'O]Pa YK *cwTbmթ _5B%.`к hm۶lngt h6]AiiipHW柔;U`e~Q.p`>}d21]:~H͟PVڣT#Uo="瀴hɠcUZzE4; "$$Őh1Rk2n3`ZQQAγgs'6/*EY?Y/נQ;aMc,0Todm6hH8:j"ЁyG&#/=3x(3nҬ9d2(~ZUU!dT1ƹISѠQ*/rMiױ +6PSD  $Qt h[cƍ|?q& }඄zuAPL+Jfh5"v1v@D{]뤨hhԷ!)yz4o|ᣐFR׎1\ڳa#'pd,YaCU9{&Pm,13"`l{1,l^щ Ύnnn#M-eXZ K e%J2\278: hnHaȂILb.pQDӻ +>J演pZn6,!ؓW :ޥB77wS)36S-1S µ^nsSZZaVǃTLfp%rQ@(mY4{bZg} (6S`CUٰж h¥bBvz.t" +_Lr,; e `4좸c 2 6NMrNoّNɓ'8\DMH l&")U^hQQQcP7/4"A},6݃iE@d0za]KY5f:#Ʀ)ȝD0A#ƠhT{uR8g!*1Gz#}hP4f]fI^^p=L\r5)-b38|qPd$ }ˆ%o3R@3#'M:v⬏]ap|0asn%;ibt,̀mH1SgAKF_Cڌ2sjOݪ6s!ªחn3``cSԈf9:;889ì٪]{?|"Gm;tM)Rvo@i6M%~LW Ŧ;[ \1n,.O&+I&SѦۅ>wO@Nuը@=MiH@ץw٧k~mwNC@B:ZX#V ! y P"JNqnR~[fƞR 68DXEDG4I H-\``cSԈ3xU6k 4l*bg6*9qBiH.-4kNScm(tm%"CO/NHI4*$px Pz0XX{aH@h^fUU|)Fc3d!zEtcՠq3]Qݬl̝ hZa֫k/^tW*ܰ$=zr*ȥ5> vZ hClLmիI|" ?BJD7$Gȑ hC A#Ɛ<{G],kEET݌7oR (X@c4XgD YEj_=e&J4r,Jaİ۪m;0 {{{ІC٘ZǏ?ݺ]${ꉪavs052$g.[e> qfd_BW6ihAu]Mɹq!6еׯ׽;U=-X@-Y``cSX_@!A 6k Yn6:gG:Xߞ $\5DiD%\5٪.R_ZZ rt g $ `CxoFM8;wddd0]v}||ʕxH i]J]pa>>>G[Mϝ31qб']ZW,XXDjU|_H'"CU-t^޺Y&4cM߸09|oU\5$~<Æ MB,XsT}6yt2(9d!lp0Wqqq1bpgϞR.*G֩Pxݣ^ }<{ ԳRO?rhCyy9h'%%˗/S,1YY]NoّN1Vj}<LBQ5cS3gh?#ٷoȏЌ42. JY"kk׮rB033SnܸAN=~xAAHU͛7WTTz3),,|Qt uzxx4Ŧ%ɖ-[Gc`xKہaQ[ h˘ b{#fc,HoOBQ5$&c'd+ãRۡǏ_tv-j ʣxfOꥷ!z^zu \)fWnذ_,ٳBxH$Bj|X@c4~]jv4gxxAo6L? Ж1:_<+̎B#x{|d^(QG`O>3j;j68Xv=租~p@@߿F-[ܹQW;'NlذATb,1v`-°QDFg84cN Axj$fjQxٶm! z;AgIm t#lҬY -KE0GGhInn=ak~%htxN1tuVV:6$jYUCJɸ\QuRXbv~Oa`RӭMv=3GMNN~4hہ QH]N;٫i;4n=+^O踌~d)3ZX@ z܌3d:FG|P HdtbOJK@1`s]hRJFۯQ1W^-߇RPI <㨢_Qi hhEjx#txffOn߹vZ(F@jPDcT KڑKWCt|AO@F%?`pSHiޢD$m zmGܟwo4]\CīW FYY04Fl|^hֹKeGFϛ5w,/1|q;ĩĠ/tGT5"G=P<4m{ +yL+@)Эڵ׮3$>J;q(§V#zh耴>Y|M|5;:ͮ'+*_r Tq#\5r={k5N_#~y;~aÆƵ@o ֓,K7'Ht׭=]tc3#QKc^3! l}Tu6]\CHgjAo6W6n7cf|a#R@FBat[Y hA_@ jVвM;HХ*M,Hsl@&9V%W}Q}.ڟޢUH֧vbh+A;mhd薀@xkv4#t]sNn~!'\5aqN΍i:4eUïjTE2x2wK-ZQdR:_. C p7+|1&˗/|>v`Ѡ;1!4l~yJAXN0p2Cڼ` 7sVSfб!E^6nAYZ|z=MLmw ? @k5 9$:88x)#P _R0GQwd2 _|5;:ͮݷ?ÓUhj ?rfbR~m=wըw#`JY!ݽimڵoXՋ}tȱ-Pm'lؐ=|pD%AOzi\Z!v=S?… [lŞmT+4Flr11|i^ob0APn޶m[yy95 h^P miњxlGDh" OBf,լҶC k~xHSo;m6z4?U ps⾫a#'n` *S&>h#,.e xX@y}dlfP.QM#DwƮyltυQ74ݗ۴mtWM܌PB 4<CKM9vn"JJJrrrvYTTtM6Q?cX@c4΁IOؙpGQkFcP>S|()FDDrjs1 h0"{ɹѲMJ9 ͘tҍTtn}UU!@RbN7nlHF^@Kc3:u'RH@ç\,*+! U?$7Ҧ}2> }#jDKয়~6w#`u@Y{)4_=ԓHd":~R];kZES'WfG0{Lj_P:D|2++K(.Ty]vi`L h |H;:ړoWhS-)4NhIn={V*Í8W77CH+X7cбGMOֳ0,aƒU#&N;mrEF&J]L$~i^-sĺ嗐=߅9f,<}㳴s5 xWR Zhk0Ahjl:ik0}q8@D \54IHw@#'' snc/6u2KSd12U#..ĉU #}o?[cͿV.h:)[ E) tF=zo0wt9q/@7]Ϡ!͛ ?SsEؾ}$5ǎUˡCI5G?NM5+X@c4Ag[c;sc\m '~Gȃ8w h|ǎ\.w֭_l/6- e'I:4;Z <<^BIX[K/еc&(R_)6hؐ#H׊5^'B"l24zu?;Z [|^z/U#RR@N_r 7+Z O~r///a:tO:w% tp|H$y=8t+|F@rѺ hz)C$?zi!/_k\@?~ hہ xD4d C$urrdtT/P[m)t56SLA:МR)++KB\5?E$-r4lԭO?H;JCQ gBP)hKҐ㍫~Y,МG^i&SfKdѽslZɲأ+ou0iPԡm<oYi8mVOWjcU"6nh)TO.|w?_>w.`mܹ֭ҥKd Eu>{ŋA訟o8t c/>2d"4… O:Ȝ`Ѡ́o7D |;;h(3[ftWȣ>O{cX؋Ü^=Z@sv&UZ̝1 >XKmڵn߁ v@IL}DZrn;"?9ɹ5 jiOZ|Cеv=:C_e/!E@wha6|r4DOxbT^6iBC>}[PQ!U+bŴe˖7n@)))͚"&OrAjɣ  z;A:Y?wB''O6lė lƙoxߟzAgv2[r'FըpKݓF 䨢7mw988H/ ;uEI šmCFo伊vՈŮ+ޮGgi+$$z9J@6ѣGrip!ϟwHxlN:cǎ[n}%Az -?P{ztid1~($=OPyT-X  lpyR "wr7o\ctxrtE!6>LU@gv24-p GU;2Y^ߤYV/]{Eݥ*Qd9 9-rԯ1Mh>W"~~2-+9FG`z!D*6tdd$YٙG6"C3/ZZd/ٳg)3f̀8::JPKD6|4aA'u޽TX| j^]n!fDǦbhL΍  љAF@\Yy6i GG<F^/e4l"SGy*2-7>~Æ 2gHiܸqff`0P%7o~%C]Xx:vGjj*TrQ=[QQa`ѣC8pAo6L? .zaS`mj򖗗yB聢j, xZk?ibұ[S?" ?.Rѱ1+U3Wzް-cLo3hxeXXXRR~B1B@cǎWуLD'kOY˗~,F"32޽ɥKaG@ h lp3^=:{x7b h+PŋI) &[(֍QMUvbg}ڸi3EJʚ8g=/(9aB R)vհYlpz||Mȱw-{iLDD/s%)j`<5k֤I,Yh"ԌꤨpHܶm'@Jll,RZZ gφj-Z/_ W,Xp2&'NY  z;AgIx7"رc< X޻woæOy!Ӌ# ܑq@Z%:vѶC3u، ^>QmإW)*%>~rn:a}P(l6VEEEoߦVyw+ h 8RJ`l{k5E1VR3j-.F͎j< TjZz^彔)As`Yz8tGAOwٻa#W |UFTt4bWZ ;{y@bsNTO?8q"///44T,e`>>>Gf hk͝ϟݻmذɓ'le 'jFŸѢXxRBr*᥌Dݥ*hRnKH)ؓװQ9+?^\ʃ $R) 쪁1D>|X.A&z,10w%` ܞbf۶mbm?څ᳣q *v㧛B:FH5+4n 6l֢o\ntndU|@j~OX#gٹUS1tx1%--*gh,1,7wa.^~ujܹVY*ʠxgG+bA쫯&\5<<_W W{hw%O ޾aF ]+R&Э/&U bXQQpC]50uC J@e9sxJbIIɡCh0YOx}UЃ1jYPzX otRRjUCJ]֖LU|_eZ2ڶf@$"-K\x¬QhѪ 럔# >Ν;ݻGmS1h? ]N߿s}YYYr.uff& f;!<~% ˱c*e=(.̎A[Ԯ^Q5՛M[=&͚w/!?96H[#! 6ur %,e 4+hnx:\PF`0uw*Jjy왧X,χ)oX@c4>wb`.!,,,ի{.L]Ġ!K}3gDEEQ/,1;1Ul@W_Q0+kJ@.Z!up=l޲U f9BE0UC^F$c97i:h^ۻIպ{}ګo#''Kɹ>Q'LyHFNH@]5(999US0tHOO߷oi߿z`@**44ʕ+< =Jee٬x8;Zw =U#:E[7iZjѪr?h1d -tc˓rQV.XFԃ#UjGD|7US?1tqG3RZZp`z`ιcx=fcWV Zbh\SW\S jďhбg,YL(3)RP9bG w~;{3dHZ+ b;v`W L=Lj!ڵkO8A c0qRhfĘÇ׮] ÇyjOOR݈߿e6P4zf-ZکiҬbOW}0aڼױc븁P1*G! p6j4}JiXZ%;;7D^pZ#Ν;'~w%~ s'Ƽ\|900088;ZRj!͎ˏ;t޳E60DIg;6h KPݝ[jѲ5H(Kb6r2)lw{3mV.4-pK(fXG6F^:uhooիرj80M7nP/X@c4s'rTTT:tՁg֡~vjZѳ&g2,/88: 5 SUNHYogg m Uv ѓ湫<4%8PR$J"xBX]500}x18YYY fC, T5lxxx_!mFZ T9w .P/hehpZB $55SD0ēE3}v4#?ÓY쯎ӬR{Ҡa9+ݵ32P̠AX4wmبѐQ׈䠹E°D .eQyeZ@"3Y/R¢Cĵk@R=zϞ=~v%33?ɓ'6mDnݢ^2h j2۶m#tyXbّ&WaQUE@=Wf${p{lgo?tDHе{Q^p,R6HIsŤI@79rV/X Ky<ޡC\k_ F\\܉'@ SÇw霟oQ/>h >[OԌΝ;nnnMͮXzv{Q% !\5(wD2\2a΍8m鍜dk0 ؐ ?M:j\5Ban^ S5V"=z$ɲ d׮]SRRm0 6?~zy?w^(UPPp}5,1)`COs%(fZ0;˽_|!Hl/Q5mb0,?ylD:88,p%t9 4^x~γc,"BX غukRR5Ւ|7?:/ظr cPpz$\.7<<|ӦM.]P¼,1Y~@_/ǎ(fZ3;"^zEjD"W oTPFO3%Bz@G3FԕK(f0YaaU1k7nP(=FQ^^rj#6(`W\Z@h8|0_vÇyPcR.i2̎[.fsbUCW"Wo>Q6o ~L/T$ E"om۶aW ,Xg Ϝ9#T*աCLqcA:GFF2L+RTT44,  X@=*((أCWRj3_|ž>R_GU5<΍4mrȱCNlߩè)3}Y? 1 Xtׯ_Ol6;&&f޽/_~gv(pʕofƍaaa1[nlٲr> ׯS`рMQhhJ)[+K=͍/EE=zfW^}BDT''QDM39 =9B͡aa_}U]Y,1D?p⠠ wwwH&:::22RPxzz}]z" k!n߾  yVí .X@NS51p'UF%)jgBy|Oh[,vygGY_bbhe4Er_`"Lp )5'N@W\k+f?;.., prAX@c4`m<~߿<:JKKToһ6UƂ<5YfǗ/_?ppՠUPSH} &333,y'f"l B~ ffˁ4Fж 趰0Ry%jy20H SJfy|PQ2;z~1"}Ŧ(!!_}@ B2D27nظqP(H$루Hϟ?~ɓ'wb"##a׈s h,m#GjشSWW]OJkQQ7;ojԮ߽STTDm(:ϟvQ-4FY4fyƍy<ޞ={̲4; t3.LEJJ ҫq3w v[r/oȚtJ-[iܸ@zR>E6TMf4;Ç]]ݤ~2""Q5W "df\vu?t @OɡvQ-4Fc#ܼy322R.BC 0!tugȂQJAh'RJ:jf9(e&] +yRJftgSOGFyx2}},! \5BBjTh cu6mڴgjX@c4 0ɉ'KJJyv(A?Y7 lHCFG%<2 1zZBQDW6ů,fDgzjn^ۋ%j"9WDDvU*.^@m::EEE.%h0B`lO~\.wytXB@O2H7 LVܸI-ѮOtc!ôZBF4pH:9vWD}\mvY>|{(@+BixZ%+;FU<~X*RX\^RRB[ hSyfttL&;<5]XB@Wيp,dv7 yk حG!{S;=qBn`nfwvϟ>}:2:ELҭt#j3Г'OW FYY|0z"}+L4F;5Vs XjJЃFBֵW_(yX++Ю2v,X{D9~\Pa܌Lj:˵kQ5ApHY'&@ ;wޥvf7F ˗/+ jX@c4T 0uPZ7orva =f,PU޺} *<`V4oٺK>hw܌9ޑy6%d/tjՎ7wnM<|p/|DՐ[UCvʾzׯd_a0kpƍΉ1hxzϭ[bbbDu^];} ,0qT=)}8k$ BAL7kђؓ7mǺUvtlн,Mgy3gHW -{.V/ߚQ ׮] -`0V%66￧vN4F mL?ɓ'bqJJݻwyZXB@˓rlNgg];Qz~X;wqҸi.=+iq\ׯ88"Q5WUӧNkXǏS  *m9ߨcc``vQ^^uV. *Kh{~ aoo߮cgO^n|8YFN]{~l '^H HEyxL/F$| &3#3ʕ+pըoU`0V vK4FZ /I*谐3nҬ)3t4wqD_αoGusBc*(D-o^b_7wkxF`P]5>:%%U`0V`n=h0]kb]{9}D"IJJ_-'V}{hU-ס")mۡS6!z5ykښh9vln#LɗL/\THHv|u\5?z7Xϟ 0ttt ytر%ZT@# H%ALt B;0x0X|JnؕM(%jgdXU8P `,ٳgc``pxT h4*Ã#\T "Ctĉ?kb>_5Cbl,1LךX@Μ9#H례HYYlfЛoܸ - h7Xp8/_R;$&ttm͛ϟw޽iӦ̈U2# |rO#1)ܹs/ m#G$%%Q. blݺ5??1  kM,m۷o_pĉزeKVVV|||PPX,vwwgR4<<<--pǎ?߽{ h0;0B7Pg l"@yAJpeD"իWی`,ӧOy<~}4FZ Ν;}m۶奤 a2 CBBsrr@@'Ox"?O-Hdr4M!s 6ܼyzYظ@m ޽;== 16  kjCnVS(Y~NyT n bWN vzq߿σ>޵kLr D"R7o޻wѣG[L_O~ hUz%ϤWo̓HHL:{,rը 'χ Ƭy{{߻w 16  kj4ϧ+[}0%-삲qD(6,G1G5O<}۠w?o߾-[UTbr@1gdd_~Ν^ZRRBS/S5F͖+<#uԇ =#""M`0fmh0]kV#%ӟ= kw):>Yrdz-/]tC(6`! @1( ?´fXMoĜIst| 4J/" -zWQB@~:((d4 LPr?N~ hӵf5OƠ}J9L$//z d\*:̓4FZz}x{{B4-Jy P )Am_ ~=`{Ըs5~۷/^x/r˖-999!!!bӓd>NII m6}]qq;w9lP@d耔<7OEr^ѡ ]":uz 1X@[K讽jwatGF@{w}p|jmخz]폵^ݶnHy PUz/$biͬKTNb'EAR<'3Kɓ85f}<< {o]=^VUۛt!yAQh h;/' ?p%WG'N0N\玀#x45;_MGGFʙ~5n{:Ŀ<55Epi^*/-( Л`6}7]7{J"!®ǿ y 觗LifzsEwL /= 688.o+.((@@# &M@bSY~u q}G@K{%?}A{9k't: ̞6L>O^QP 7,zY|ۢ-~=O~  &o7C7~eDƀ~1A/Q3'y耾'r.0NpZ2 fWsQn>n>/]ڃ'G {@k OṬu[f>FXSNn۶מՄF@M@@+[nYsw~ѲOO N=4n%-H/0Fʪk~?7oworO/n1V^u[nTT&S]UU:zhzzΝ;#zk&O(\kv/#/L^nݺuF1>ʞ={Zm[[l|4BoMZ}aC̬lyOO5,BavX,~y@hޚX֔,̈Mz  SSS7ot:"Iwwh|7FGGu zkY>_gq(p],&FyOO}}atꄄ>H` \:Nw^yy@]hޚ*m۶M9VUYhNT~ǏW?syHII<{*:4BoMZ<O1?!#wfMF3su Z[[u:뻻nedd|gbJxϜ9#?+V-@iVkfffvvnR"+V<<,#r:;;Ĥ-++7^<ahYC~0x"0"A9bZ.]*~(--nllO:;v˻71ML<1jjjĄLKK*..~Ͻϟ1"z!? "`>tvb6WZ%:uRtt|n nb盘xbY,>Ãߌ"wM6y󠪪E^ ߯p0@Qh***V|2n0\P:2өjů@h %PQ[[˧erssP166駟 `N;&/6иÇq"7n42(oT___RRrcϞ=ccc(otbr5ájP,Wr9 ɓ'SRR8 /-G~(+0 MMM(or{=9|>Q;v쐗@hHnnM<\ .p8RRRxԊl^|fiZ{#fW744qƌ F@#hN_reWW\@lIIIf=И ֖P^^-Iv{QQQNNNOOTjD@cEFLv_vm6[^^^ff Py}j-[tvv\.5

-tEXtCreation TimeMon 18 Dec 2023 01:36:09 PM CET IDATxwXWґ^J EA4bAĨHl$Xb[,Il1؍1b%(4"a"e|) yxdw=3.g"`1B!ȤB!|(%B!DB!QB! JD !B/(%B!DB!QB! h"""|z?Cʜ:u WĨQjUfڵw^={6jUfĉ˫WPUUϞ=Ü9sUg˖-믿֪˗m۶z۽{wL>>h۶-ҴP" LтSBɓ'K|hizh!B%B!B!^P"J!BxA(!B%B!B!^P"J!BxAGll,m۶FF?yyyXXXCSHCCC!48JD{[n!==yyyPUUТEJ˔ <<ЪU+jժeffJ$WHLLDzz: 1;wڵkWeo{5߿011vݻwѽ{w@jj*xcǎ`ffw/ P```P|ԩ?;BipzD8tVZV|||\b F(.]q .?TTT35ko뇠jQTTGGG۷ѹs՝͕۾};&LPI;v 'OSٳgcݺu9"B! %^rss1zh>}@YR ())A("$$ ?qcܸq10{l\|1111c@YY>ؿ?FյkWx洵akk["DFFLMMZa֭[7x|BQRg2d.^Ŋ+вe aʔ)㡨>>~x,\عs'VZ9W_}Ua&2331sLu=fAJJd6l VHX[[6n܈6vhBFͿkkkQ/%?#vQi GHHw]~@Y(NLكׯ_۷\]]ѱc njssscܹ;F;w <</^qLDFF֪̻Ν;>jT&''Ghh(![&|nݺ1HTr7n`h"m!!!ܶ'NT{Ǐs۷Ob۔)S6}tvn˗/Wzn۷o<cL(ro^rb_f?3366#e˗YΝ%8883EEE&//nݺŕ~:g3;;;c0PigΜaWSScsayyyʬZ`ffff>}222ԩSD_~,,,B/2yyyD"۽{7344(omm͂*<rbDRRRlРA,!!]t3uuucW^'7nܨ%%%L__˳UVUn„ lȑOhh(!ҤP"J̙3SNժ'|0ss ĉЀ=ٳ %Q???ccƌaU}'---e b@ `c}7osrr?V({E&++-[2___6ydfaaddd_ŕ]v{>55U☛7oHƍǼ2XllDݻwse,--/6mswwwww%1Tfnn0%%%6j(xb6uTfbb0v9r/w-[0ޞ?1Kedd%絭*y0زe˸:۷oϦNƍ444۸q#4442 8qbٳ\RX~'OGq?/_;$BJDI|Z#ND*K&--͒*-_\\ttt6w ʼn/c,ab_a>Qv̙ GݻwE"ln[II h1{!D4""Klg8V=B6Bo>gJlkDtС kݺ5{ĶB6`jՊsʷηhтKMKKc\hyu}m:y3999}嗬ۖmڵ SUU ^~*N!5A(Ç3¢eO>vĶ\KVZȑ#\kSLLLo'NNNEDDHG"jff/M6ZBCC:++..ZjN>III1?L 0쯿/WZ>㞯kXh}c STTdYYY=~IKKWHDB!ݻBTۅBޅ+:wN_ţŔwU >}̬Fu?={Dqq1&MX~_bccSСC+GJJ +Wp߼y@ټ={PNVV'88YѣG###ܤغu+N:U弫F(*ŋD>>>߿۷ok!~@]]Bmͭ2do ˗/O?@!UDԉT[e˗]|[\2?~U}Z/U׶*9nս_tttHDff&#GV7!%ζl)))bĈ(((vsÇWܹǏԩSj?H2k׮.7&&&\ǏWOII O>~zrվ'Otiՠ \`񀳪l۶ W˨;ɓ'+\Y:w̙Jt3 qˍ7*] Ç8vߏx{{WقL!ϬQXh7bӧYqq>iiilʕ*=,%%Fӧ\]]PӻlʈW f Gt֭ڟU cO<5300`X.]$cÆ D  撜9sDϟ?VquuOף@"2UUUO>Db{cNh/ yyy͍;n[yD+,:uĀU˫kX<"""9H'O,&&&mr'U455vJ%ڢDxvQ`HնlM~ 0[[[͹u={Vc?~+ ]ej2X߾}%_D&?SLʊD".f>>>ll}cѣ5dCe>>>e˖L[[*e`m۶e_~%:t(ACC=z?((Hbs粙3gr/­%%%ž~X&iii\ʕ+7|ínݺ%Q|":o<&##ýF7YYYvUymJDz16}t\بQ'SPP`TU"c?w SSw~$D+W#Gr?SN;JDD0a֮III1gggsN֚x5kݺ5YFF;&})STTlD%9WZ$ɉUY3gOj7|pbcc'%d-ˀXTTTl"(֦Mo>ƘdK6h X&1&NȔ$bcC aʔODee?敮nX_۪Ѻce+9]qettt؜9sX^^rN0--[r?B-c<.1ӄ\zc---lڴLj>L1$$$ٳg(..,--whMc HKKڴiS //Obp(Rہ]HIIڶm[x!󡯯33 T7ec(..Fdd$Z͛pqqkXRR(dffBWW@:ymy-''16l3`nnJ=}HHH`ĉ%Θ1NNN 1SN0 W/([;]S-11:::pvvƶm*lѣG Oݻ!%%E5h1f;v }ȑ#xlٲPQQҥK_|v܉3gիZh{aƍ(,, ƍǕ7n1|!佣D",X.\ $ww~ɇk֭PRR¶m۰cǎ zyyaǎPPP_=2e VXh1B>Ř1cpe$$$ ++ ԩ<)chhe˖h:}\JZZ6lk׮!99Ӄ+ڴiS̄ бcG())W^U:E!E(!(999ۗ0֭[7ZZZB!B!^Эh֭HOO;.F|rԀw(7(}B!JKKannw(B 4H@(R"JHBh;ֆ gwJPQB! JDI+!Bݚ'M͛7k.t ]tA.] ''wXBy(%MRNP\\Pؾ};tggg888G(((@QQ -+//EEE(((@VVH !|(%M&  %%7oDhh(_UUU]v|Az$䠰K8 QZZZcIKKCAAKPCCCZj3"!D4y[7PܼyϟjSXX.[RRPPPyyy.^1qIǙ@aa!@VVKJ۵!?JDPԩS0113t邖-[*o|G!""999Cv}}}ޮ444`mm-|nn.RSS7nȑ#Ѐ=amm MMM^b&x(%<@ ??KJwڅ]vs\R*--wEFF\G!22@Ϟ=ѱcGbTTTKKK <BQQQQ\\vڡm۶pss6!Biall ooobm 2ݣEիzꅌ .)ݼy3m'ԩߡIRR^+W@FF666ڵ+ѢE ë3x p%;w=z'|CCCCmF SSS\PH5!ҤP"1y`CsbHLL9]v \Rڶm[C}\r/_,(**vvv͛7qM =z􀹹9a690tXYYqR _ IDATShg JDׯ_ŋШ0;c PSSL/_aa!k.77ֆTZtFFF'O488ܭ{###CC\r7n܀1|||RkߜO>ٳ'BBB/k׮ѣ:vwM˗/QRR-- JJJPuu[ZZ ())AYY}g+SϭnΜ9|@Hּfh׮:w6m`Ϟ=ܶk׮cǎ-9N:?^}ի3%%5>X[[OO?}4 /@#رce̝;h"9sS[V‹/0n8̛7ݻwhdddн{w̛7ƍË/j*[O>;&FB`iiKwrϞ= @$AWW۷oG׮]###,\"zׯ_333DRR}۶m077LMMѷo_nF~nBJDC͛HLLĔ)S0o<&L+p-qqq055P_tt4^˗/s>SlذG-|}}q9,^4puu+rssPܹ;whC 6wNkעo߾w) DÖcOx/^xt]8yLLL3&O#Grq :Tb>(%yDy7 @ FQQ`„ 0s).x.)|2455VҺvSOŸ޽{c\wk۶-ڴi3gܹsȑ#l˗/#00IIIaccQFU}~ex?9993D>l۟W+Vɓ ._~?>fϞ]z !%ML||<7T~~>233Ѻuk.KLLZ,"""P\\,Q>-- mea͚5{~~~:u*dee1i$\zZ囉 LLL0|pDDD 44ׯ_G`` Zn͵x͛ѲeKƦϠyСCM6aٰ;4ŋgϞcǎ}/-bЪU+>Ξ=+޻wHIIh͚5&&&麗۱l2.sҼ`&f֭\; --^zX~=իWC^^16n,ܵk}+1c8|0n޼ׯ_7iqlll0qD`ӦMxQ_sa̙عs' d̞=&@Ybyeo?~P3==D"bbb 8v޽{wQqի z 7oF=вeK_prrBrr2JJJ Zl)q|nBjm"JJJ @ @NN~gn?| 7'5kp>_&&&(((5{V[˗/1i$t!!!ضm7mwww}i+RwX`A祤~z ,, 'OD||7 cbUۣG|w>|8Ցgggܺu ]tкuklݺ;F]>!%MDݹC"-- :::-ݺuÝ;wPTTyyy"77W"a~y󐕕}}wNNollg";;p:v숴u]TTпEEEСCؾ};:uaÆ!++ Bjj*VZp$''#??] . ??(,,č7о}{8;;#;;O)6oތ?mې ///a˖-hѢ&LۚݺuΝ;ڵ+lقK8KbÆ 3f H$‚ _mڴ}W/ǹ[%o)Ξ= lcnݺ¨w3gNJBH,_@ J8ejtϟCKKrrrÚ5k#Gq hAAz *.l?{xyfC#|(}#''?W֢7'''ܾ}N0#F)#::KAQQ)))Uܤ˗B!?!C066,N2+EUzDͭI 8uMxbUf̘kkke fΜ @UUUT|pp0V+0k{ehtt4/_{aܜox>}:1b~7nƍrrr011+|AAAZ,++<<M7KreעgϞIJe )))DFFb߾} ºu<:qpp'  S~ ?%%% 6 ݊bҐ(%SSSbĈxBCCq={5,j4?b}={ j=NC Fpp0ի͛%:::|2~G?777ёs+qȔJфիPSSÈ#-@!D|pڷo/۷Ǐȑ#҆/6ċ4||mmmlܸ6l@FFD"* ʒvIӦD?! я _k~=z4>|P\t O1uHIIe%@G_ǎѱcGL0ta:t 'UUzcll06`H*,,Djj*"B!%|M2pك={KJZG:J))) vԎx"Zv,!!P"!)**gϞٳ'233[lm۸[iIOdCJJ #F;Zy9АH>,/kBHàD#B-5 xxx 99KJ_rIuׇ&?t޽H$‹/$-**BVV444ju 00SSSeMsss!%%%%ڝ{chjjRh-!!PXh7{G}Xv-~",, ˖-ìYp!$$$TZV^^p޽FСCd EEEW_}&&&6mW^^|Ν;Ajj*YЙ3gM6pwwG6mpB.y&tttp!N}dd$BCCSNM6\ իWaoo1ѣ6m999|y&кukšXn.\JKK%&/ɓ'> sPإK###Ġ_~000ɓ!''R?~/߸qh?tEnP\\=.DG+_vvväIANо}{n <==aK.W_(K(>3fpMMM1l0?~ .S=;wČ3`ddlQFØL:Ue}%4j 'wuuĉgرcp͟>}#G`B!v-q @ #}vzW\}0qDtڕ8DiѦgϞ7nY{{{ڢ{2dOl3f`h۶- vz5`РAصk! xbk׮A ???^… 8|0Ə={CsB- `g!څ k._4]p߿ 6}~~>^x]]Jn"33zzzeffBIIIlAA^x---(**6hU ӧ1j( 8ǏcٲeXz5JEyo(-Z;Ɖ'pӓgϞٳ>|||0dCj6D" !++ YYY!4³0;v })յkpYhjj;w;$B!@(!M@~~>;@tXTM(̙3 ?V///a|B!uB(!MHXX=W^apqq;&-$$PVVưa>VPwwwߟP!Nh7^*aM6uڵñcǰo>ܹs;wMH$­[ǏtUAII +lGII Ԫ=Fzz:444jPVVJ"PSSχV$4/퍷GӨr—-Z`̘1;w.bʕFVVRQ IDAT&++ Xr%Ocܹ3fGcРA044.\]]!  P;v,TUUann~a000P(bС͡YfUO҂tuuѣGnуL0h׮TUU1j(.] ggg,]Z>,--ހW֭òe˸HC"ID{(P7{{{L6 +W=\+Vܤ8>|+V+W`oo+Wbڴi;<>8{,~?DR@JJ %|D( ''-[w/_|gGHKKCYY BHII &...HOOǃ# ZZZ :vwz"""#B~~~Сj@FFc\JJ`չx?yyy(((p'$K.ݻɓ'b gggŋCNNzzzXr% G z pez 8p@r/_ ???ddd@$v킣#ߏ\7|K.ە" GWWpwwGaa!޽;wq'񏡡! u*,,Drr2eu:T(Zll㄄n0R֭/=,,1>}*8))U` !::Z⹘@6mCqq1:uN:aѢE6l6m%%%%-JG&,..ׯ1h ?xfϞ-[૯B\\PZZ iiie-#۷o{wWSվHJ%-` R WhadF4va5Xd"m*I{|]nGq>}|o8x bccѵkW|X~}mfdd`2e ~Gc2dⴲ^:3ueMBHLLر'N-[ x%/q(NOO[nEoAQQe>|󑟟< //OH6K ykhh@]]PUU˫N:%ɉP l޼PVV7|SSSzZh۷o#11Ճ;oヤ$Hf`ff'O"''044zbRRR%K~@zz:H!VceMB^ $_ χc94kL8Fzl.SéBUUbB~wԩSG\\oߎqI\WybffV6UTT$f¿^zW^J'7"VcKw-)K$; t" \\\$7k ۶mC޽%z^{YfСCXx1&O~MFd>_|;BGGϞ=ȑ#7߈T-]Dp͚5`#996l@qq1ݻ _?~GwOd(3 1o,߹s'мys},%K[n_`nnkkkٳ5?F>}`jjvD t޼y9s&N< uuu\r+i֬섯M &DDǏ www###C  ,˗,̚5 HJJBjj*0k,!==&&&.=%%%ܸq=Brr2={[[[a)97nԐ$W\ܹs1a enSLDj2 ̞=[(CVV6mW_}%l;v@YY 4@\\xbzjmJJJPQQСCq}ݻwq y]vؾ}{|fj2bbb`ii)=.4h";;hܸp^EENNNիWyfDGG#//Ϟ=CAAAmC]]]8VSS>W3f̀~<~XHVnc"Zpו@AAAXWN:㧟~ŋ={ǧ6dTiB:i$ϟ>ψnj"lll#xB({acc_~ALL :[[[|whڴ)YfKKK|'|:|pDT{08逈*jȑPPP_zB͛=== 8uӧOspȑ6ԺukksAvv6 225‰'} %ڋh5IDTQعs'֬Y B__:mmm9sGy_>~Gl۶ VVV8q"LLL`aasssl۶ `kk Ǵo>5BΝ1~x IQQQe cD)N: ???xx J\{"33011Ƃիx աHHH˗/cǎ uH􄧧pܸqc>}Ϟ=CNN%>C郧OXH`"ZMq}(w]nf򪤤%%JŤ==2iiiUn"ϟ#!!A8VUUE&MDI3 }ĤRtG EEEEDDDuLjׯcתUDc"ZMqQѣG _.];$jEtawi[N8nРV^-bD;TYJ9頨Htʛtٜt@D<<<$BM>;v1"cD9N: ""ڊ扈HLDHLDHLDHLDHLDHLDHLDHLDHLDHLDHLDHbP]E±Qmckݺ5Zn-vDDTS(DDDD$%""""Q0%""""Q0%""""Q0%""""Q0%""""Q0%""""Q0%""""QpA2<~YYYZZZ޽{(**Tvvv>>h߾}Ylrss+ *aj+V=W^ƍ+nY{:SNU &wg͚5HJJTK.{_]韭6o\{|JۦMVǏСCjÆ =+޽[v;jժBW׭[BUUOHH{ .=.]BpppׯƍW{ۇ .TI&K.6y""""Q""""Q""""BIIIAQQ""""Q""""Q""""%" 2}\̛Dzi"ڳgO텇ѣG2kj ={0b 2AAAUަ[Ϗ?Ğ={0`*&Du?^.hy^OB]\\D*n()"h)MF<NjКMII WQQ!Q D3eZ)*J`"JDD#eZ;())I LD(y&LBk,1%䑌2 ]#JDDdIhf"Q" &DT%(IAAA☉(UQ" dIhGDKaa!RRR0==CJJ ^|YnyyyU&D$E2$vd%ꥨ]tCeee.]¶m`kk-[H]{nb۶mYQ"PdIhQEMM GFppԹG+++l߾];vҲc- Q"!(кQ&D7nΞ=JcܸqVZ!==ׯ_y!bbb`oo/ϐLDLIF|4OT5\q֭ģ7nݻ=zPV\\#FHܹ...())+h''' >NNNbC5$LB޾}C|pرcapp0 }}}\]]`׮]puuT,Z$7b@5Xi2:b?AAA9&u%SNa߾}v~)fϞGW^ ={> yy DީdIhGDs-_ݻwnݺu)++ ;v@vv6,,,бc2uuuŞ={/jo(GD|L$ny"HJJB`` Zl OOO憓'O~{ :׮]ӧEQ"zo2 'vD5ڋ/uuuL4IfgϞ8uvQu8p rss-?BXӤꠕ+Wի`cc՚n´iDNvBCC֤扈$88.]e|4_bccŀd֖+Q#G/G}$v8*veDFFYfbRkݽ{Wŋضm NDTNjӘdժUb |AUmcIJ`v3 0qD[edd[0j=1?;/-ꈉ(Q51p@CLW0VD411? LgLDMrG;w4h!:5&~TPQ@AA|}}"j,gUk"22066;j(ݻgΜĉѪU+éq8;;GbS1%"""ׯcٳMVvӑ.VUFGGh׮ء$$$ 00[W_}Au=J䞈&''#((7oDz`ff&Vp! .bccez^PPP4nXMrr2mۆ3gΈ U@$''~vﯫ[Uukbڴi: cccxyyJۉ˜1cpȑ*mʶuV\r&M5y&tuu٤իSS*m#==aaa+&8t=cǢCbS5}ׯ/&E.vDDDTΝ;]vaРA߿H\=z#&&ɕ YK$0 IDATB`` :uWWWé%%%amm GGG5NNNܹ3?f͚#LxyyqСЦM8p@ĉ022‚ $Ǝ+JDD$4I&v܉x{{EbS+ɓ'={=ؾ};BBB1fL>ׯGTT\]]+WT+ePUӧO󊊊022 Ϟ=? )) K՝"$'' о}{̘1˖-ԘQ""#Gk׮bSkԸDTQQ ,_sνsy,YWP077Gpp0rssJp=Zcg+*Jwn+((HH]555XUUU⼯/lW___8** HMMErrr.[ """Yz*зo_ 2Dpjv’%K0wܷ^5`R,--Dɓ'Reiii1,7n,sXسg< 6`…8q"*˗CSSzzz(**q""8m۶pww;ZF}݊+eavyz)2q,URR .aÆ011:u8xԹ۷ԩSqBB|}}1|p8;;cժU|r͘711_|$$$7oݻw=""{rrrL}*tF|FFN(<|>>>xQO_~֭C\\bccxbL:Νb\v χ/:TkDD$HOOǤI)v8RMD`ǦMR5[.m&uΝ;?~UY YXX.]}_1zh̜9VRRž={0h ̝;m۶6n܈ٳg~z:u ˖-C ϟ bĉXܪQFa՘9s&ͱk.L2˗/竬m""6oތk׮aҤICHj)++cʕpvvl:aΜ9!C˗/CsԲխ[7ƌE!99FFFӓd]i&">>011X*R֯_7oެp?8wV^ t]tAӦM,;?www Ijt" :t 6o\5… 1uT(++CSSSL`ҥXlpOm۶!C:::ёyh֬*ZZZ߿?X!)=z(7n,$bJDDș3gg|۷z5*mѢ>|(Ul2$O>fԨQ5j233QXX(ٳ1{*ոqc4ncƌK۷cpttD.]бcG$""ݹsk׮E.]_NPQY;QǎѱcGddd ""Xr%tuu^Ҫ\&M;:%D  A!::8}4ФIt]v-w!""=eee&r&DlRt{SդI4icǎ`SNܹ3H`` !vT:uBNs!""+V@ G\ƃؾ};.\)SԨI\ҥ ͱo>y6+Pdee}|Xd ̙m7|رc;L"رc?US/W_}Ueu@S9r5j:u$v8uLj86l+W`aa yPRSS#w;$-ZE?~KeK.ҥ ڴi#vTDžH0x"^Z%uGGGʙCVdջF~0x`O=upp?/ܻwaaaʙ{癈ڵ+v'OIٳgadd.]s033-ƃb޽GFjLDTsmڴ up%Fnƍ'׶I( *FD2ѰaC 6 Æ í[p9>|@˖-TUUUqݿaaa5n/^ !!ՃQ׽| 033+w7%%%!77ƨW^eff"''o˗HNN&'III QTT ((({]||<*HFF222|;^7`jj@k:hժ+lܸ 66l[D %%%yppp-Ǐ ׬Xz*о}{\|uڵ hٲ%aee#FHmej VVVhٲ%T@qq10c yhӦ 6mJGK.h۶-q) .{ѦM[>͛7W^ CCCDGG 666h߾=,,,$6177Ǟ={ CCC;V/ ¤I.6|LDdDUUz˗c… 1sL8pONJ{naӧ[-.."x{{iӦDZcо}{,Z.\߿077^۰an޼ɓ': USSS8;;ׯ_ǹso>ݻ[&9)*V߂iiiqY&[P/a``K{8pLNMMN<4l-ٳg'O@AAK,q1BLSߖZ/66/_˖-[޽{ ;wТE Ѩ֮]^.]عsЃ4bdeea֬Y8t\\\!M6bbbP\\ EEE((( 66.]5޽{XI233/ CNNOCxܾ}׮]׼ys̙3G8ŰapE|ر#455?r/=۷0aZn]a(QkӦ qFŋXv-&L͛7lӷ={6ڵk'NGƍ1g?~\FQQصk~^}@xx8̙K._~q9cQTT ;wFDD~wXZZÉ'dlmmz7uqD!CޫtܹsݺuzٳgOzJ۞3g>|(\שS'JլY3! }3ׯWlTq 8ڵ+G+~*q\Yz?Ծ}0l0[lPC ihho߾۷/sI$py [n#l8}4aoo/cbbpi9ӧOj*===JJJ½ i1k,_ѡC2d(((m*((ѻ^z]xk;T֣FCCCיH/]gΜձw^$''y=jaɘ1cp=lذ{5z#FQ^SS]SqQQ%KJJ<+{yzK]]e=zXfD?&M^T{}ʳgݻw\ۦwc"ZJTU|!T~ӧOe^666011͛7%ʭ+OݥknСlll$zʫR4/~aouOYU(+-oF]eo~5nupcҤI8rñsNl޼>>>{Y߇UVUQRR+**BQQQwy-wUTTYeN򢧧SM1b{1@HH/OĈHիhԨz*1###H/C; uNzx77n\f"ZV?꒕ѣG˴&< zӅוGΕKb Y899!>>G3$7M$߼_V"##`W\H,UsxO>SDHNܹP\rz eK79UTT,'"uw_ޯ?R]ܺu #n8w]vTZjpBSSS8w1g eee~KV7W=c yC ݻP077ĉѣG}I_8CBBd#""#Gp͚5V`BT>TTTBCC? ի\.}uQ4j9={3g@v.yߺmmm;v}All,-Zvڕ9?DFg!==]&%zI> 9?F~~>LMM}TWW/˗ɓ'xLLL$*))I0a<~e&8|7n ..EEEhܸ$&{{ ZǬO,>DID!%%EXV6ֆ :v(v($GGhh(.]SSS&Jw7YnJ455xbUU֭[1yd,^ .D6mfH Ʒ~+x25554mT*((שהaGrݴICCbA"{"#((m۶E=ШQ#yP>}8o|/&Lmo| W=̄rׯ/vDT 5=/D 67n܀+U# 8y$ѳg۷oGHH>c@||<ƌӧcXp!ׯÇCQQ}ŋ#&&6l5Yy%㡥%vn  Exx8ttt0zh 4IKD`|8w;w_š5kzj! sss#770qDhhh{"##ŋqUڗtصk \cIJJBhh(N8mmm5  DDDrVB`׮]Xd Ν]2'gĹݻw>5 - ˗/c̙ӧ={Kjjjpuu8Ǐ~/0h &DDD"qh+VGpvvF˖-˽.;;W{׽tR;wڍ-))Abb"`aaQ#䒒COOZZZUϑ}}}2l;w/^̙3+k-ZΝ;4MNNFXX;---1 JIDDDVc0c L6 ^gdd<<}ugΜAXXXJII .]Ѷm[ԩS5.\!݋ÇM6Xn;{.>3X[[666ׯn޼)\O?'Ow044|զMb۶m0aD >߆ KIIAPPk?#F@`` $(L<شiSE֭m۶Isϟ(**}d+XEEEԩSqYlܸ999/CUUUQTTݻw#//˗/G>}Zorr2>3<~;w۷t 2O<3fԦbΜ9ڵ+<<<ڎ  Jm~跦L)))غu+O0%""jyPVVʕ+,h)aΜ9!C˗/Clٲ vc333/}/qͨQ0j(dffPj3g$fYPWWWH v)www 6 IIIBͥ@BBDO,ZNNN6ly777<{L8vtt? qMjj*\]]q-lݺfff"99=°a0h hjjVN"""ʂ!TXYKH8PEEE2eeeCCrzG177PPP@TT^xUUU_osC_}(mmmbѢEZWњ(77999Vjj*W3!!!e333C||>>o},q2qrrb"JDDT5mӦ +Wb}4J-V\ 777l0abbb+;ϟ?.QΝ;ѭ[7BBBₔ _|$^2/`ʔ)Bف䄤$5]]]qq$''jSNgϞyĚSLwJ\oll ?Ϟ=nݺڵkhӦP>}tJ=)jٲ%F)ݿ~ĉ!44W^u6lĄҫWO>8vڷo/طoо}{ 0BѫW/_ptt'ܹu8::o߾XhPvmt ԩPh"lݺUb~|Gѣ.]*ݽ{;wơCu`ɒ%ظq#% ݃-oW(_`VX!Dm۶ptt|@8QfO0ΈŃJYXXiӦ/w: ;vď?(U>n8;w.$;tPYnnnА(SWW/Zh!Q޺ukJ1bmK닖-[JWj9/gggJ㰷(oٲ%|}}^$ZIhi/_zhݺDY-+ذaÐ'UGVСC2;;;̝;WjYgggdeeI1o<lmm1w2_ˬYꠚ sn.5p2`񁃃D%Ν+aʧ~ :&M$)|}}֊ӧOCݼ>GLLLʬW^RO`ĉRfffݻw/3C5]vEˬQFeTo~@ӦMgI1w\X[[KzTy %ovӐL` ;ZiժUPVVDž кuk dmј6m5kVEQQQXjv!v(RFVzcVs㰺Ă PEBCC&πDaRPp&r$&&W^xءHΜ9ǏU38y$u&v(5QrX[[DKDuϧ~;wUC={|2nLDdtKe&9rF; Z[|^Qja }}TTTC5?j,X_|lmmFc"JT$5 K.Z*UyTN^I0o .Dk㭅X/RNdM!!!"fTuӪUC)#CNNN| ha<<< RբSN&^eT۷o4ܹsvڵ+WT*mB1Kƅ(cYC5'ִtւ媪*2i|%%%bXƷob13~|hGVV?5c #1ff~~~;F#H0yd`L4Ɗ+0l0iѸm"o)Zumڍ7ݻwӕǔ)SN1JH}5*V V+v^cyyy /cߘ1cK/;Z%.Dc̴Z-RRR$v*5d\2ƘY1f.\2f!Μ9l;VTc1\2Vr8q<\\\N灅a޽-I>1u+:tBBB0ydsكiii(,,C=_7dee wttf͚~Iccʂ\.ϊNeYYY1bQڱc}Q`LBBJi͉'닕+WK. Ŕ)SPRRSbԨQu`sb񰳳ke˖aر1h \xQHJJ ~-|}}y~ˍիة|k2t ~XF:ACCCI.N4ϯFz޹sJ%5,z?QUUEJ222ξyyyT*R<2V^-vfj͍|||(##ݻwS6mgշ撕͛7ϠJ"{{{?ˋFiз}Qrttr3#Et___9rAAA8L|{ŴiN,CbӦM4x}̘1xw$@qq1Э[7ryyyݶ BBBPPPk׮={aؕ0kӧOiӦNɢL&m۶N#ZM&L ;;;JKKJ ֻx"[n4{lRՔO/"YYYQZZZ9 2Ο?O'Oht*.. \.'GDD/2988Ј#ʕ+TPPיGTTuQyy9ݺuONT*Mc׮]-ZDEEET\\L BADwG@PiTPP@Էo_޽;5jZw ) XscW}._Lh=z TZZZ-]{b Q 1sLA!zYSEEEdccCvFC"##hԮ];~ɒ%dgg?O:u?y$]vՙGhh(DA? 8:tH' @PV:}AG}<.\@[lBOLL$kR/SUUE>>>4e"_!,zj`ܸq|jj:uc٧(44TߖLlllhذapBJHH0XB4))ݩ_~Fxjb#>-YnjӦMÚ5k0hР N!C!qy̙3foG(FGGcҤIpttԯׯ_?mV\=p5_􄫫+@&<y%ՓjyqA `kΝuѧOtvBFFJKKqMwfeJ' XA?777>}ZVv s:{ƹsp 8pG?DLLɓ'ѯ_?r^^J%뇭[}z2+Ϛp\ZJ2<gggڵkO>ׯbbb0e:u ׮]_|!XG&N09rD߾akgooRڵ.dվ}{H$X%%%u摐 &ÇqGEEʐH:v(Xsc m6LiXxH4d%GΝ; 2D?#̙^xǎlg֬YeC5i<<ׯ_^;尵Ehh(~zܾ}[zzz⭷jwZՄիЪ*2<{-AqMۦON#663g4yA. w(NCrrrr^x=zj{1؎NCJJJAD|2F!_ih ~#bD"Avvh94mII ,X vfFnh~GGZGRU*d2ڴiӨ֋ Q #d Ö-[/jo:vQFaժUP*A&aBEE6n܈DL>O}?>z F;Z |8{>1^^^8x nݺ۷oC||<$ Lc޼yz*ϟ|TTT 66Ɔ L|t暴nM~~~hמ<֭ۖ1l0˸p6l؀QQQ/3f@vO~+WSNaΜ9ؾ};|Mtk(.D-?ƌ30uTسg<<<}gΜӧOcȐ!f=yt !!!?%e899!66۷/z GGG5"88={L&Cxx Vm dff⡇L&CLL 6oތSbժU2eJ8q"DEEYp޼yxM|^|E[nXhIHk\[ brСCppp y,XGFbbiA=zcЯ_?gϞ4h~W|XjH \?kB[/+1b57n܀vލ1cƘ%fii)T*ڷoo0)t:dddU?{2.]O?EEE1۷oJ%t:J%\\\jUSEEJ%ڷo/TRRL&]+jUUUHOOD"\.ӄ(++}o)/.W/!v_t:`cc;{Pee%QTTWWWp8c=&[;dccooo/Jѽ{wDSg*{{V{:ݜ,iDt̘1x饗ϋؘX[[Tfʸ\5ѣd=zr;n߾O>_Z))) ;FaIǓÇ}&c<=ŋc„ 8t1vXL8Q0}ر)h9SB]t̮Ĕ|/v j\ZLZ_0KD8Si|?U?KɓbXbIǓ(J`͌"""b Q aIOB1~Dd[71(++Cll,/@FFbccE}O?hӰx{ũSHCkɓɓ' ֺbԩ8x ѣ:u*jșϑ#G|Sitj~޽6miX7|}ckx2e)0 cXO;f4":|pShB7/2&}j5Z}hj}]!--Md.Ss8qDTTT ##CPTUUATBThARAT9T``  9Bܚ>W)_~~>T*A'OֹߩT* hZ_}vmT 7o| QjV?wwwx{{ #F ۹s'R):RgΜ:߿AN=z@N_t___鰆0rHYgΜT*ڵkR Ji&]vEZZ`pwwG׮]ѵkWtAJ6m\.]k׮زegI̥9* F+v ]VЧ/''R <==}/_q3z쉷~!!!DTTT>8y$:xwvmۿՓY;/bEtIMKK#LFǏL}6رd2W_}EҥKu{9ܹ3UVVR\\Ҿ}d4{:s {{{ th44c ׯQxx8d2ڶmt:R4a4"君KF"RTRRB999K>>|JJJ觟~"ATXXH˗/'O?R$qF""ڲe WѮ]-Z{=@ III_~8@ tSj4+Wcǎ=PWVVR~O>tYϧ_޽;_ (==K`|vkIDATݻw'""Zt)ׯS^^Q>}hҤI~RT lhƌDdx PTHcƌLj6mPdd>3q!X[hY[[Sff_'[[[*++W&TTTDDD}`ߟԤφB4""R)bUUUպ}&>>5XKLLD.]!h0`q899a񈉉ѿcb֬Y $XvqqAAA@ܹstӧ znnnظq#Tt: 2Drϟopu:t`911`Ϧ6km۶`TKll,$ %>k:%%%X`h}Xϧwu׬Yog, ~!M}#G{+++hZdddԺ}&>5X+,,A{v̙31i$xУGz2 w_~e˖CBBuVy0Ρ<_oHu7~EEMcc$%%!22;ֺ}sCDDfv?}4FowwwFr7ԌϦ.(,,4[picq!XsttDffA{nnu;v,\]]8p֯_oҶ̙^xA\=`4{s`񦹹ݻIyF"-J!jfuNu1~xlڴɤ훓?25>?x`\zU,3_d!js lwww$''`XÆ 3KLKŧō7 Hg @FFɓh׮z hӦ BBB;v!!!&mwwwA*J߷O<лwo?Ļ~:n޼y 0&ԩS Apq%&|n9.?{lYsjgSAAR/=_1-1kܰ0!<<ZwAll,bbbmΜ9IIIذa&MdQ_~m۶EXXnܸl̙3Ǐ/d2°e/ZoGGGk G:;w.._>(..ŋs6h[իW1|磢L>>>R)ܹs}ݻW^=… 3fW9۷wNuܙvܩ^yj۶-iӆP>}&{nj۶-W_}U?_"?ښ$ uЁ͚'"oC}h"$"s搓C5-sܾyׯ_aÆyLF˗/omٸq# RIDwoԱcG@={$kζogS׬y"ǏS>}T*Sҭ[}j>3 cBR % H$tSPTh߾dS nnn~ڑtb$Bzz:$ r9ڴis?iߗXL:F9c@Z-it0o͛7ѱc0ghP\\N:'< Q b r^,R6nZ))) ;kQ aeeT?~P(Fg2V777.BkŲ H'v*- mg >;ƘH~iDGGkf ___Si\֖OvQVV"`…;-ۻw^1Q k:Z w;Fs%֭[)vZ\ZeBPITcDP͛bq!j!Q!22׮];cHdքSi5GD|MUV<1}YSixQu B4c"Zr%R)*v*J\ZXG|%%%bXƧ-g!""bcg3tj!ʘEPP;&v*- Qݸq{;cUBBԼFDʤpvvFDD|||N1&1c`֭bWWW( 6LTZ<.D-;T?Y|d%'+=B\AJѣG ={/^4S8233qΝCjjjj,瑒b4FNNMVd1mh .… Fch4A[vvHJJ2CV rrrHII/9r#F0Yoti\rEzYY F4G3g˂{T*t=11.]2hoNǺz-O,BJJk @v]B?8͘1CЖE(..N"hSo.h_ns ڴZ-A 9r$M b >8m8_Qpp͛_~%;wGE<-??Ж-[ǺrLDž(c O7o)J:rADJMM5C T*) gϞ5\1rrrmF sQrrق,1Ο?O.\0CjIIId4Zi41.\@ϟ7#++KЖm4Frr2;whL SN˗mOnSBBB8}4]tIVVVF񔛛+hȠG8s ]xѠ9qHx c1L>]xb ckDc1Ƙ(xD1Ƙɮ_[[["c>\2c1QWc1& .Dc1Ƙ(e1cB1c Qc1& .Dc1Ƙ(e1ckqqq˽z’%KD̈1cGD-D'TVV c1]\ZkkkrUUH0cŧ-DBGDct(//NCii)z!vZ Q Q<2Ě5k_5c Q Q%"2a1 Q #1knd%c57\Ze1XsÅY1knḐc1&6.D-2ck{FTTi0c(c11cL\2c1QHc1D#1cL\2c1Qp!c1D(c11cL\2c1Qp!c1D(c11cL\2c1Qp!c1DlةIENDB`ovn-bgp-agent-2.0.1/doc/requirements.txt000066400000000000000000000001531460327367600202240ustar00rootroot00000000000000sphinx>=2.0.0,!=2.1.0 # BSD openstackdocstheme>=2.2.1 # Apache-2.0 # releasenotes reno>=3.1.0 # Apache-2.0 ovn-bgp-agent-2.0.1/doc/source/000077500000000000000000000000001460327367600162415ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/doc/source/bgp_supportability_matrix.rst000066400000000000000000000247361460327367600243150ustar00rootroot00000000000000.. This work is licensed under a Creative Commons Attribution 3.0 Unported License. http://creativecommons.org/licenses/by/3.0/legalcode Convention for heading levels in Neutron devref: ======= Heading 0 (reserved for the title in a document) ------- Heading 1 ~~~~~~~ Heading 2 +++++++ Heading 3 ''''''' Heading 4 (Avoid deeper levels because they do not render well.) ========================= BGP Supportability Matrix ========================= The next sections highlight the options and features supported by each driver BGP Driver (SB) --------------- +-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+--------------------------+--------------------+-----------------------+-----------+ | Exposing Method | Description | Expose with | Wired with | Expose Tenants | Expose only GUA | OVS-DPDK/HWOL Support | Supported | +=================+=====================================================+==========================================+==========================================+==========================+====================+=======================+===========+ | Underlay | Expose IPs on the default underlay network. | Adding IP to dummy NIC isolated in a VRF | Ingress: ip rules, and ip routes on the | Yes | Yes | No | Yes | | | | | routing table associated with OVS | | (expose_ipv6_gua | | | | | | | Egress: OVS flow to change MAC | (expose_tenant_networks) | _tenant_networks) | | | +-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+--------------------------+--------------------+-----------------------+-----------+ BGP Driver (NB) --------------- OVN version 23.09 is required to expose tenant networks and ovn Load Balancers, because Distributed Gateway port (cr-lrp) chassis information in the NB DB is only available in that version (https://bugzilla.redhat.com/show_bug.cgi?id=2107515). The following table lists the various methods you can use to expose the networks/IPS, how they expose the IPs and the tenant networks, and whether OVS-DPDK and hardware offload (HWOL) is supported. +-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+--------------------------+-----------------------+---------------+ | Exposing Method | Description | Expose with | Wired with | Expose Tenants or GUA | OVS-DPDK/HWOL Support | Supported | +=================+=====================================================+==========================================+==========================================+==========================+=======================+===============+ | Underlay | Expose IPs on the default underlay network. | Adding IP to dummy NIC isolated in a VRF.| Ingress: ip rules, and ip routes on the | Yes | No | Yes | | | | | routing table associated to OVS | | | | | | | | Egress: OVS-flow to change MAC | (expose_tenant_networks) | | | +-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+--------------------------+-----------------------+---------------+ | 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 | | | | +-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+--------------------------+-----------------------+---------------+ | 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 device (lo_VNI_ID). | Egress: flow to redirect to VRF device | (Not implemented) | | | +-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+--------------------------+-----------------------+---------------+ | 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 | | | +-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+--------------------------+-----------------------+---------------+ | OVN | Make use of an extra OVN cluster (per node) instead | Adding IP to dummy NIC isolated in a VRF | Ingress: OVN routes, OVS flow (MAC tweak)| Yes | Yes | Yes. Only for | | | of kernel routing -- exposing the IPs with BGP is | (as it only supports the underlay | Egress: OVN routes and policies, | (Not implemented) | | ipv4 and flat | | | the same as before. | option). | and OVS flow (MAC tweak) | | | provider | | | | | | | | networks | +-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+--------------------------+-----------------------+---------------+ BGP Stretched Driver (SB) ------------------------- +-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+----------------+--------------------+-----------------------+-----------+ | Exposing Method | Description | Expose with | Wired with | Expose Tenants | Expose only GUA | OVS-DPDK/HWOL Support | Supported | +=================+=====================================================+==========================================+==========================================+================+====================+=======================+===========+ | Underlay | Expose IPs on the default underlay network. | Adding IP routes to default VRF table. | Ingress: ip rules, and ip routes on the | Yes | No | No | Yes | | | | | routing table associated to OVS | | | | | | | | | Egress: OVS-flow to change MAC | | | | | +-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+----------------+--------------------+-----------------------+-----------+ EVPN Driver (SB) ---------------- +-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+----------------+--------------------+-----------------------+-----------+ | Exposing Method | Description | Expose with | Wired with | Expose Tenants | Expose only GUA | OVS-DPDK/HWOL Support | Supported | +=================+=====================================================+==========================================+==========================================+================+====================+=======================+===========+ | VRF | Expose IPs on a given VRF (vni id) -- requires | Add IPs to dummy NIC associated to the | Ingress: vxlan + bridge device | Yes | No | No | No | | | newtorking-bgpvpn or manual NB DB inputs. | VRF device (lo_VNI_ID). | Egress: flow to redirect to VRF device | | | | | +-----------------+-----------------------------------------------------+------------------------------------------+------------------------------------------+----------------+--------------------+-----------------------+-----------+ ovn-bgp-agent-2.0.1/doc/source/conf.py000077500000000000000000000051341460327367600175460ustar00rootroot00000000000000# -*- coding: utf-8 -*- # 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 os import sys sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'openstackdocstheme', #'sphinx.ext.intersphinx', ] # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = u'ovn-bgp-agent' copyright = u'2017, OpenStack Developers' # openstackdocstheme options openstackdocs_repo_name = 'openstack/ovn-bgp-agent' openstackdocs_bug_project = 'ovn-bgp-agent' openstackdocs_bug_tag = '' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ["."] # html_theme = '_theme' # html_static_path = ['static'] html_theme = 'openstackdocs' # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', '%s.tex' % project, u'%s Documentation' % project, u'OpenStack Developers', 'manual'), ] # Example configuration for intersphinx: refer to the Python standard library. #intersphinx_mapping = {'http://docs.python.org/': None} ovn-bgp-agent-2.0.1/doc/source/contributor/000077500000000000000000000000001460327367600206135ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/doc/source/contributor/agent_deployment.rst000066400000000000000000000137261460327367600247140ustar00rootroot00000000000000Agent deployment ~~~~~~~~~~~~~~~~ The BGP mode (for both NB and SB drivers) exposes the VMs and LBs in provider networks or with FIPs, as well as VMs on tenant networks if ``expose_tenant_networks`` or ``expose_ipv6_gua_tenant_networks`` configuration options are enabled. There is a need to deploy the agent in all the nodes where VMs can be created as well as in the networker nodes (i.e., where OVN router gateway ports can be allocated): - For VMs and Amphora load balancers on provider networks or with FIPs, the IP is exposed on the node where the VM (or amphora) is deployed. Therefore the agent needs to be running on the compute nodes. - For VMs on tenant networks (with ``expose_tenant_networks`` or ``expose_ipv6_gua_tenant_networks`` configuration options enabled), the agent needs to be running on the networker nodes. In OpenStack, with OVN networking, the N/S traffic to the tenant VMs (without FIPs) needs to go through the networking nodes, more specifically the one hosting the Distributed Gateway Port (chassisredirect OVN port (cr-lrp)), connecting the provider network to the OVN virtual router. Hence, the VM IPs are advertised through BGP in that node, and from there it follows the normal path to the OpenStack compute node where the VM is located — through the tunnel. - Similarly, for OVN load balancer the IPs are exposed on the networker node. In this case the ARP request for the VIP is replied by the OVN router gateway port, therefore the traffic needs to be injected into OVN overlay at that point too. Therefore the agent needs to be running on the networker nodes for OVN load balancers. As an example of how to start the OVN BGP Agent on the nodes, see the commands below: .. code-block:: ini $ python setup.py install $ cat bgp-agent.conf # sample configuration that can be adapted based on needs [DEFAULT] debug=True reconcile_interval=120 expose_tenant_networks=True # expose_ipv6_gua_tenant_networks=True # for SB DB driver driver=ovn_bgp_driver # for NB DB driver #driver=nb_ovn_bgp_driver bgp_AS=64999 bgp_nic=bgp-nic bgp_vrf=bgp-vrf bgp_vrf_table_id=10 ovsdb_connection=tcp:127.0.0.1:6640 address_scopes=2237917c7b12489a84de4ef384a2bcae [ovn] ovn_nb_connection = tcp:172.17.0.30:6641 ovn_sb_connection = tcp:172.17.0.30:6642 [agent] root_helper=sudo ovn-bgp-agent-rootwrap /etc/ovn-bgp-agent/rootwrap.conf root_helper_daemon=sudo ovn-bgp-agent-rootwrap-daemon /etc/ovn-bgp-agent/rootwrap.conf $ sudo bgp-agent --config-dir bgp-agent.conf Starting BGP Agent... Loaded chassis 51c8480f-c573-4c1c-b96e-582f9ca21e70. BGP Agent Started... Ensuring VRF configuration for advertising routes Configuring br-ex default rule and routing tables for each provider network Found routing table for br-ex with: ['201', 'br-ex'] Sync current routes. Add BGP route for logical port with ip 172.24.4.226 Add BGP route for FIP with ip 172.24.4.199 Add BGP route for CR-LRP Port 172.24.4.221 .... .. note:: If you only want to expose the IPv6 GUA tenant IPs, then remove the option ``expose_tenant_networks`` and add ``expose_ipv6_gua_tenant_networks=True`` instead. .. note:: If you want to filter the tenant networks to be exposed by some specific address scopes, add the list of address scopes to ``address_scope=XXX`` section. If no filtering should be applied, just remove the line. Note that the OVN BGP Agent operates under the next assumptions: - A dynamic routing solution, in this case FRR, is deployed and advertises/withdraws routes added/deleted to/from certain local interface, in this case the ones associated to the VRF created to that end. As only VM and load balancer IPs need to be advertised, FRR needs to be configure with the proper filtering so that only /32 (or /128 for IPv6) IPs are advertised. A sample config for FRR is: .. code-block:: ini frr version 7.5 frr defaults traditional hostname cmp-1-0 log file /var/log/frr/frr.log debugging log timestamp precision 3 service integrated-vtysh-config line vty router bgp 64999 bgp router-id 172.30.1.1 bgp log-neighbor-changes bgp graceful-shutdown no bgp default ipv4-unicast no bgp ebgp-requires-policy neighbor uplink peer-group neighbor uplink remote-as internal neighbor uplink password foobar neighbor enp2s0 interface peer-group uplink neighbor enp3s0 interface peer-group uplink address-family ipv4 unicast redistribute connected neighbor uplink activate neighbor uplink allowas-in origin neighbor uplink prefix-list only-host-prefixes out exit-address-family address-family ipv6 unicast redistribute connected neighbor uplink activate neighbor uplink allowas-in origin neighbor uplink prefix-list only-host-prefixes out exit-address-family ip prefix-list only-default permit 0.0.0.0/0 ip prefix-list only-host-prefixes permit 0.0.0.0/0 ge 32 route-map rm-only-default permit 10 match ip address prefix-list only-default set src 172.30.1.1 ip protocol bgp route-map rm-only-default ipv6 prefix-list only-default permit ::/0 ipv6 prefix-list only-host-prefixes permit ::/0 ge 128 route-map rm-only-default permit 11 match ipv6 address prefix-list only-default set src f00d:f00d:f00d:f00d:f00d:f00d:f00d:0004 ipv6 protocol bgp route-map rm-only-default ip nht resolve-via-default - The relevant provider OVS bridges are created and configured with a loopback IP address (eg. 1.1.1.1/32 for IPv4), and proxy ARP/NDP is enabled on their kernel interface. ovn-bgp-agent-2.0.1/doc/source/contributor/bgp_advertising.rst000066400000000000000000000045741460327367600245260ustar00rootroot00000000000000BGP Advertisement +++++++++++++++++ The OVN BGP Agent (both SB and NB drivers) is in charge of triggering FRR (IP routing protocol suite for Linux which includes protocol daemons for BGP, OSPF, RIP, among others) to advertise/withdraw directly connected routes via BGP. To do that, when the agent starts, it ensures that: - FRR local instance is reconfigured to leak routes for a new VRF. To do that it uses ``vtysh shell``. It connects to the existsing FRR socket ( ``--vty_socket`` option) and executes the next commands, passing them through a file (``-c FILE_NAME`` option): .. code-block:: ini router bgp {{ bgp_as }} address-family ipv4 unicast import vrf {{ vrf_name }} exit-address-family address-family ipv6 unicast import vrf {{ vrf_name }} exit-address-family router bgp {{ bgp_as }} vrf {{ vrf_name }} bgp router-id {{ bgp_router_id }} address-family ipv4 unicast redistribute connected exit-address-family address-family ipv6 unicast redistribute connected exit-address-family - There is a VRF created (the one leaked in the previous step), by default with name ``bgp-vrf``. - There is a dummy interface type (by default named ``bgp-nic``), associated to the previously created VRF device. - Ensure ARP/NDP is enabled at OVS provider bridges by adding an IP to it. Then, to expose the VMs/LB IPs as they are created (or upon initialization or re-sync), since the FRR configuration has the ``redistribute connected`` option enabled, the only action needed to expose it (or withdraw it) is to add it (or remove it) from the ``bgp-nic`` dummy interface. Then it relies on Zebra to do the BGP advertisement, as Zebra detects the addition/deletion of the IP on the local interface and advertises/withdraws the route: .. code-block:: ini $ ip addr add IPv4/32 dev bgp-nic $ ip addr add IPv6/128 dev bgp-nic .. 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.ovn-bgp-agent-2.0.1/doc/source/contributor/bgp_traffic_redirection.rst000066400000000000000000000056671460327367600262200ustar00rootroot00000000000000Traffic Redirection to/from OVN +++++++++++++++++++++++++++++++ Besides the VM/LB IP being exposed in a specific node (either the one hosting the VM/LB or the one with the OVN router gateway port), the OVN BGP Agent is in charge of configuring the linux kernel networking and OVS so that the traffic can be injected into the OVN overlay, and vice versa. To do that, when the agent starts, it ensures that: - ARP/NDP is enabled on OVS provider bridges by adding an IP to it - There is a routing table associated to each OVS provider bridge (adds entry at /etc/iproute2/rt_tables) - If the provider network is a VLAN network, a VLAN device connected to the bridge is created, and it has ARP and NDP enabled. - Cleans up extra OVS flows at the OVS provider bridges Then, either upon events or due to (re)sync (regularly or during start up), it: - Adds an IP rule to apply specific routing table routes, in this case the one associated to the OVS provider bridge: .. code-block:: ini $ ip rule 0: from all lookup local 1000: from all lookup [l3mdev-table] *32000: from all to IP lookup br-ex* # br-ex is the OVS provider bridge *32000: from all to CIDR lookup br-ex* # for VMs in tenant networks 32766: from all lookup main 32767: from all lookup default - Adds an IP route at the OVS provider bridge routing table so that the traffic is routed to the OVS provider bridge device: .. code-block:: ini $ ip route show table br-ex default dev br-ex scope link *CIDR via CR-LRP_IP dev br-ex* # for VMs in tenant networks *CR-LRP_IP dev br-ex scope link* # for the VM in tenant network redirection *IP dev br-ex scope link* # IPs on provider or FIPs - Adds a static ARP entry for the OVN Distributed Gateway Ports (cr-lrps) so that the traffic is steered to OVN via br-int -- this is because OVN does not reply to ARP requests outside its L2 network: .. code-block:: ini $ ip neigh ... CR-LRP_IP dev br-ex lladdr CR-LRP_MAC PERMANENT ... - For IPv6, instead of the static ARP entry, an NDP proxy is added, same reasoning: .. code-block:: ini $ ip -6 neigh add proxy CR-LRP_IP dev br-ex - Finally, in order for properly send the traffic out from the OVN overlay to kernel networking to be sent out of the node, the OVN BGP Agent needs to add a new flow at the OVS provider bridges so that the destination MAC address is changed to the MAC address of the OVS provider bridge (``actions=mod_dl_dst:OVN_PROVIDER_BRIDGE_MAC,NORMAL``): .. code-block:: ini $ sudo ovs-ofctl dump-flows br-ex cookie=0x3e7, duration=77.949s, table=0, n_packets=0, n_bytes=0, priority=900,ip,in_port="patch-provnet-1" actions=mod_dl_dst:3a:f7:e9:54:e8:4d,NORMAL cookie=0x3e7, duration=77.937s, table=0, n_packets=0, n_bytes=0, priority=900,ipv6,in_port="patch-provnet-1" actions=mod_dl_dst:3a:f7:e9:54:e8:4d,NORMAL ovn-bgp-agent-2.0.1/doc/source/contributor/drivers/000077500000000000000000000000001460327367600222715ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/doc/source/contributor/drivers/bgp_mode_design.rst000066400000000000000000000332721460327367600261370ustar00rootroot00000000000000.. _bgp_driver: =================================================================== [SB DB] OVN BGP Agent: Design of the BGP Driver with kernel routing =================================================================== Purpose ------- The addition of a BGP driver enables the OVN BGP agent to expose virtual machine (VMs) and load balancer (LBs) IP addresses through the BGP dynamic protocol when these IP addresses are either associated with a floating IP (FIP) or are booted or created on a provider network. The same functionality is available on project networks, when a special flag is set. This document presents the design decision behind the BGP Driver for the Networking OVN BGP agent. Overview -------- With the growing popularity of virtualized and containerized workloads, it is common to use pure Layer 3 spine and leaf network deployments in data centers. The benefits of this practice reduce scaling complexities, failure domains, and broadcast traffic limits. The Southbound driver for OVN BGP Agent is a Python-based daemon that runs on each OpenStack Controller and Compute node. The agent monitors the Open Virtual Network (OVN) southbound database for certain VM and floating IP (FIP) events. When these events occur, the agent notifies the FRR BGP daemon (bgpd) to advertise the IP address or FIP associated with the VM. The agent also triggers actions that route the external traffic to the OVN overlay. Because the agent uses a multi-driver implementation, you can configure the agent for the specific infrastructure that runs on top of OVN, such as OSP or Kubernetes and OpenShift. .. note:: Note it is only intended for the N/S traffic, the E/W traffic will work exactly the same as before, i.e., VMs are connected through geneve tunnels. This design simplicity enables the agent to implement different drivers, depending on what OVN SB DB events are being watched (watchers examples at ``ovn_bgp_agent/drivers/openstack/watchers/``), and what actions are triggered in reaction to them (drivers examples at ``ovn_bgp_agent/drivers/openstack/XXXX_driver.py``, implementing the ``ovn_bgp_agent/drivers/driver_api.py``). A driver implements the support for BGP capabilities. It ensures that both VMs and LBs on provider networks or associated floating IPs are exposed through BGP. In addition, VMs on tenant networks can be also exposed if the ``expose_tenant_network`` configuration option is enabled. To control what tenant networks are exposed another flag can be used: ``address_scopes``. If not set, all the tenant networks will be exposed, while if it is configured with a (set of) address_scopes, only the tenant networks whose address_scope matches will be exposed. A common driver API is defined exposing the these methods: - ``expose_ip`` and ``withdraw_ip``: exposes or withdraws IPs for local OVN ports. - ``expose_remote_ip`` and ``withdraw_remote_ip``: exposes or withdraws IPs through another node when the VM or pods are running on a different node. For example, use for VMs on tenant networks where the traffic needs to be injected through the OVN router gateway port. - ``expose_subnet`` and ``withdraw_subnet``: exposes or withdraws subnets through the local node. Proposed Solution ----------------- To support BGP functionality the OVN BGP Agent includes a driver that performs the extra steps required for exposing the IPs through BGP on the correct nodes and steering the traffic to/from the node from/to the OVN overlay. To configure the OVN BGP agent to use the BGP driver set the ``driver`` configuration option in the ``bgp-agent.conf`` file to ``ovn_bgp_driver``. The BGP driver requires a watcher to react to the BGP-related events. In this case, BGP actions are triggered by events related to ``Port_Binding`` and ``Load_Balancer`` OVN SB DB tables. The information in these tables is modified when VMs and LBs are created and deleted, and when FIPs for them are associated and disassociated. Then, the agent performs some actions in order to ensure those VMs are reachable through BGP: - Traffic between nodes or BGP Advertisement: These are the actions needed to expose the BGP routes and make sure all the nodes know how to reach the VM/LB IP on the nodes. - Traffic within a node or redirecting traffic to/from OVN overlay: These are the actions needed to redirect the traffic to/from a VM to the OVN Neutron networks, when traffic reaches the node where the VM is or in their way out of the node. The code for the BGP driver is located at ``ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py``, and its associated watcher can be found at ``ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py``. OVN SB DB Events ~~~~~~~~~~~~~~~~ The watcher associated with the BGP driver detects the relevant events on the OVN SB DB to call the driver functions to configure BGP and linux kernel networking accordingly. The following events are watched and handled by the BGP watcher: - VMs or LBs created/deleted on provider networks - FIPs association/disassociation to VMs or LBs - VMs or LBs created/deleted on tenant networks (if the ``expose_tenant_networks`` configuration option is enabled, or if the ``expose_ipv6_gua_tenant_networks`` for only exposing IPv6 GUA ranges) .. note:: If ``expose_tenant_networks`` flag is enabled, it does not matter the status of ``expose_ipv6_gua_tenant_networks``, as all the tenant IPs are advertised. It creates new event classes named ``PortBindingChassisEvent`` and ``OVNLBEvent``, that all the events watched for BGP use as the base (inherit from). The BGP watcher reacts to the following events: - ``PortBindingChassisCreatedEvent``: Detects when a port of type ``""`` (empty double-quotes), ``virtual``, or ``chassisredirect`` gets attached to the OVN chassis where the agent is running. This is the case for VM or amphora LB ports on the provider networks, VM or amphora LB ports on tenant networks with a FIP associated, and neutron gateway router ports (cr-lrps). It calls ``expose_ip`` driver method to perform the needed actions to expose it. - ``PortBindingChassisDeletedEvent``: Detects when a port of type ``""`` (empty double-quotes), ``virtual``, or ``chassisredirect`` gets detached from the OVN chassis where the agent is running. This is the case for VM or amphora LB ports on the provider networks, VM or amphora LB ports on tenant networks with a FIP associated, and neutron gateway router ports (cr-lrps). It calls ``withdraw_ip`` driver method to perform the needed actions to withdraw the exposed BGP route. - ``FIPSetEvent``: Detects when a Port_Binding entry of type ``patch`` gets its ``nat_addresses`` field updated (e.g., action related to FIPs NATing). When true, and the associated VM port is on the local chassis, the event is processed by the agent and the required IP rule gets created and its IP is (BGP) exposed. It calls the ``expose_ip`` driver method, including the associated_port information, to perform the required actions. - ``FIPUnsetEvent``: Same as previous, but when the ``nat_addresses`` field get an IP deleted. It calls the ``withdraw_ip`` driver method to perform the required actions. - ``SubnetRouterAttachedEvent``: Detects when a Port_Binding entry of type ``patch`` port gets created. This means a subnet is attached to a router. In the ``expose_tenant_network`` case, if the chassis is the one having the cr-lrp port for that router where the port is getting created, then the event is processed by the agent and the needed actions (ip rules and routes, and ovs rules) for exposing the IPs on that network are performed. This event calls the driver API ``expose_subnet``. The same happens if ``expose_ipv6_gua_tenant_networks`` is used, but then, the IPs are only exposed if they are IPv6 global. - ``SubnetRouterDetachedEvent``: Same as ``SubnetRouterAttachedEvent``, but for the deletion of the port. It calls ``withdraw_subnet``. - ``TenantPortCreateEvent``: Detects when a port of type ``""`` (empty double-quotes) or ``virtual`` gets updated. If that port is not on a provider network, and the chassis where the event is processed has the ``LogicalRouterPort`` for the network and the OVN router gateway port where the network is connected to, then the event is processed and the actions to expose it through BGP are triggered. It calls the ``expose_remote_ip`` because in this case the IPs are exposed through the node with the OVN router gateway port, instead of the node where the VM is located. - ``TenantPortDeleteEvent``: Same as ``TenantPortCreateEvent``, but for the deletion of the port. It calls ``withdraw_remote_ip``. - ``OVNLBMemberUpdateEvent``: This event is required to handle the OVN load balancers created on the provider networks. It detects when new datapaths are added/removed to/from the ``Load_Balancer`` entries. This happens when members are added/removed which triggers the addition/deletion of their datapaths into the ``Load_Balancer`` table entry. The event is only processed in the nodes with the relevant OVN router gateway ports, because it is where it needs to get exposed to be injected into OVN overlay. ``OVNLBMemberUpdateEvent`` calls ``expose_ovn_lb_on_provider`` only when the second datapath is added. The first datapath belongs to the VIP for the provider network, while the second one belongs to the load balancer member. ``OVNLBMemberUpdateEvent`` calls ``withdraw_ovn_lb_on_provider`` when the second datapath is deleted, or the entire load balancer is deleted (event type is ``ROW_DELETE``). .. note:: All the load balancer members are expected to be connected through the same router to the provider network. Driver Logic ~~~~~~~~~~~~ The BGP driver is in charge of the networking configuration ensuring that VMs and LBs on provider networks or with FIPs can be reached through BGP (N/S traffic). In addition, if the ``expose_tenant_networks`` flag is enabled, VMs in tenant networks should be reachable too -- although instead of directly in the node they are created, through one of the network gateway chassis nodes. The same happens with ``expose_ipv6_gua_tenant_networks`` but only for IPv6 GUA ranges. In addition, if the config option ``address_scopes`` is set, only the tenant networks with matching corresponding ``address_scope`` will be exposed. To accomplish the network configuration and advertisement, the driver ensures: - VM and LBs IPs can be advertised in a node where the traffic could be injected into the OVN overlay, in this case either the node hosting the VM or the node where the router gateway port is scheduled (see limitations subsection). - Once the traffic reaches the specific node, the traffic is redirected to the OVN overlay by leveraging kernel networking. .. include:: ../bgp_advertising.rst .. include:: ../bgp_traffic_redirection.rst Driver API ++++++++++ The BGP driver needs to implement the ``driver_api.py`` interface with the following functions: - ``expose_ip``: creates all the IP rules and routes, and OVS flows needed to redirect the traffic to the OVN overlay. It also ensure FRR exposes through BGP the required IP. - ``withdraw_ip``: removes the above configuration to withdraw the exposed IP. - ``expose_subnet``: add kernel networking configuration (IP rules and route) to ensure traffic can go from the node to the OVN overlay, and vice versa, for IPs within the tenant subnet CIDR. - ``withdraw_subnet``: removes the above kernel networking configuration. - ``expose_remote_ip``: BGP exposes VM tenant network IPs through the chassis hosting the OVN gateway port for the router where the VM is connected. It ensures traffic destinated to the VM IP arrives to this node by exposing the IP through BGP locally. The previous steps in ``expose_subnet`` ensure the traffic is redirected to the OVN overlay once on the node. - ``withdraw_remote_ip``: removes the above steps to stop advertising the IP through BGP from the node. The driver API implements these additional methods for OVN load balancers on provider networks: - ``expose_ovn_lb_on_provider``: adds kernel networking configuration to ensure traffic is forwarded from the node to the OVN overlay and to expose the VIP through BGP. - ``withdraw_ovn_lb_on_provider``: removes the above steps to stop advertising the load balancer VIP. .. include:: ../agent_deployment.rst Limitations ----------- The following limitations apply: - There is no API to decide what to expose, all VMs/LBs on providers or with floating IPs associated with them will get exposed. For the VMs in the tenant networks, the flag ``address_scopes`` should be used for filtering what subnets to expose -- which should be also used to ensure no overlapping IPs. - There is no support for overlapping CIDRs, so this must be avoided, e.g., by using address scopes and subnet pools. - Network traffic is steered by kernel routing (IP routes and rules), therefore OVS-DPDK, where the kernel space is skipped, is not supported. - Network traffic is steered by kernel routing (IP routes and rules), therefore SR-IOV, where the hypervisor is skipped, is not supported. - In OpenStack with OVN networking the N/S traffic to the ovn-octavia VIPs on the provider or the FIPs associated to the VIPs on tenant networks needs to go through the networking nodes (the ones hosting the Distributed Router Gateway Ports, i.e., the chassisredirect cr-lrp ports, for the router connecting the load balancer members to the provider network). Therefore, the entry point into the OVN overlay needs to be one of those networking nodes, and consequently the VIPs (or FIPs to VIPs) are exposed through them. From those nodes the traffic follows the normal tunneled path (Geneve tunnel) to the OpenStack compute node where the selected member is located. ovn-bgp-agent-2.0.1/doc/source/contributor/drivers/bgp_mode_stretched_l2_design.rst000066400000000000000000000262571460327367600306060ustar00rootroot00000000000000.. This work is licensed under a Creative Commons Attribution 3.0 Unported License. http://creativecommons.org/licenses/by/3.0/legalcode Convention for heading levels in Neutron devref: ======= Heading 0 (reserved for the title in a document) ------- Heading 1 ~~~~~~~ Heading 2 +++++++ Heading 3 ''''''' Heading 4 (Avoid deeper levels because they do not render well.) ==================================================== OVN BGP Agent: Design of the stretched L2 BGP Driver ==================================================== Purpose ------- The main reason for adding the L2 BGP driver is to announce networks via BGP that are not masqueraded (SNAT disabled) and can communicate directly via routing. The driver requires that all routers to be announced are in a L2 provider network and that the BGP Neighbor and Speaker are also part of this network. The whole tenant networks are announced via the external gateway IP of the router, instead of as /32 (or /128 for IPv6). This means that the tenant networks can be routed directly via the router IP and failover can run completely via BFD in OVN. No additional BGP announcements are needed incase of failover of routers, but only ARP/GARP in the respective L2 network. The resulting routes are the same on all instances of the ovn-bgp-agent and are not bound to the machine the agent is running on. For redundancy reasons it is recommended to run multiple instances. Overview -------- The OVN BGP Agent is a Python-based daemon that can run on any node. However, it is recommended to run the L2 BGP driver on the gateway node since all requirements are already met there, e.g. connectivity to the L2 provider network. The driver connects to the OVN SouthBound database (OVN SB DB) for all information and responds to events via it. It uses a VRF to create the routes locally and FRR to announce them to the BGP peer. The VRF is completely isolated and is not used for anything else than announcing routes via FRR. The tenant routers/networks cannot be reached from the VRF either. .. note:: Note it is only intended for the N/S traffic, the E/W traffic will work exactly the same as before, i.e., VMs are connected through geneve tunnels. The agent provides a multi-driver implementation that allows you to configure it for specific infrastructure running on top of OVN, for instance OpenStack or Kubernetes/OpenShift. This simple design allows the agent to implement different drivers, depending on what OVN SB DB events are being watched (watchers examples at ``ovn_bgp_agent/drivers/openstack/watchers/``), and what actions are triggered in reaction to them (drivers examples at ``ovn_bgp_agent/drivers/openstack/XXXX_driver.py``, implementing the ``ovn_bgp_agent/drivers/driver_api.py``). A common driver API is defined exposing the next methods: - ``expose_ip`` and ``withdraw_ip``: used to expose/withdraw IPs/Networks for OVN ports. - ``expose_subnet``, ``update_subnet`` and ``withdraw_subnet``: used to expose/withdraw subnets through the external router gateway ip. OVN SB DB Events ~~~~~~~~~~~~~~~~ The watcher associated to this BGP driver detect the relevant events on the OVN SB DB to call the driver functions to configure BGP and linux kernel networking accordingly. The BGP watcher detects OVN Southbound Database events at the ``Port_Binding`` and ``Load_Balancer`` tables. It creates new event classes named ``PortBindingChassisEvent`` and ``OVNLBEvent``, that all the events watched for BGP use as the base (inherit from). The driver react specifically to the following events: - ``PortBindingChassisCreatedEvent``: Detects when a port of type ``""`` (empty double-qoutes), ``virtual``, or ``chassisredirect`` gets attached to the OVN chassis where the agent is running. This is the case for VM or amphora LB ports on the provider networks, VM or amphora LB ports on tenant networks with a FIP associated, and neutron gateway router ports (cr-lrps). It calls ``expose_ip`` driver method to perform the needed actions to expose it. - ``PortBindingChassisDeletedEvent``: Detects when a port of type ``""`` (empty double-quotes), ``virtual``, or ``chassisredirect`` gets detached from the OVN chassis where the agent is running. This is the case for VM or amphora LB ports on the provider networks, VM or amphora LB ports on tenant networks with a FIP associated, and neutron gateway router ports (cr-lrps). It calls ``withdraw_ip`` driver method to perform the needed actions to withdraw the exposed BGP route. - ``SubnetRouterAttachedEvent``: Detects when a patch port gets created. This means a subnet is attached to a router. If this port is associated to a cr-lrp port, the subnet will get announced. - ``SubnetRouterDetachedEvent``: Same as previous one, but for the deletion of the port. It calls ``withdraw_subnet``. - ``SubnetRouterUpdateEvent``: Detects when a subnet/IP is added to an existing patch port. This can happen when multiple subnets are generated from an address pool and added to the same router. It calls ``update_subnet``. Driver Logic ~~~~~~~~~~~~ The stretched L2 BGP driver is responsible for announcing all tenant networks that match the corresponding address scope (if used for filtering subnets). If the config option ``address_scopes`` is not set, all tenant networks will be announced via the corresponding provider network router IP. BGP Advertisement +++++++++++++++++ The OVN BGP Agent is in charge of triggering FRR (IP routing protocol suite for Linux which includes protocol daemons for BGP, OSPF, RIP, among others) to advertise/withdraw directly connected routes via BGP. To do that, when the agent starts, it ensures that: - FRR local instance is reconfigured to leak routes for a new VRF. To do that it uses ``vtysh shell``. It connects to the existing FRR socket ( ``--vty_socket`` option) and executes the next commands, passing them through a file (``-c FILE_NAME`` option): .. code-block:: ini LEAK_VRF_TEMPLATE = ''' router bgp {{ bgp_as }} address-family ipv4 unicast import vrf {{ vrf_name }} exit-address-family address-family ipv6 unicast import vrf {{ vrf_name }} exit-address-family router bgp {{ bgp_as }} vrf {{ vrf_name }} bgp router-id {{ bgp_router_id }} address-family ipv4 unicast redistribute kernel exit-address-family address-family ipv6 unicast redistribute kernel exit-address-family ''' - There is a VRF created (the one leaked in the previous step) by default with name ``bgp_vrf``. - There is a dummy interface type (by default named ``bgp-nic``), associated to the previously created VRF device. Then, to expose the tenant networks as they are created (or upon initialization or re-sync), since the FRR configuration has the ``redistribute kernel`` option enabled, the only action needed to expose/withdraw the tenant networks is to add/remove the routes in the ``bgp_vrf_table_id`` table. Then it relies on Zebra to do the BGP advertisement, as Zebra detects the addition/deletion of the routes in the table and advertises/withdraw the route. In order to add these routes we have to make the Linux kernel believe that it can reach the respective router IPs. For this we use link-local routes pointing to the interface of the VRF. If we use the provider network ``111.111.111.0/24``, a router with the IP ``111.111.111.17/24`` on the gateway port and the tenant subnet ``192.168.0.0/24``, the route would be added like this (same logic applies to IPv6): .. code-block:: ini $ ip route add 111.111.111.0/24 dev bgp-nic table 10 $ ip route add 192.168.0.0/24 via 111.111.111.17 table 10 .. note:: The link-local route for the provider network is also announced and is only removed when no router to be announced has a gateway port on the network. Since all BGP peers should also be on this network, the BGP neighbor will prefer its connected route over the announced link-local route. On the BGP neighbor side, the route should look like this: .. code-block:: ini $ ip route show 192.168.0.0/24 via 111.111.111.17 Driver API ++++++++++ The BGP driver needs to implement the ``driver_api.py`` interface with the following functions: - ``expose_ip``: Creates the routes for all tenant networks and announces them via FRR. If no subnets are connected to this port, nothing is announced. - ``withdraw_ip``: Removes all routes for the tenant networks and withdraws them from FRR. - ``expose_subnet``: Announces the tenant network via the router IP if this router has an external gateway port. - ``withdraw_subnet``: Withdraws the tenant network if this router has an external gateway port. - ``update_subnet``: Does the same as ``expose_subnet`` / ``withdraw_subnet`` and is called when a subnet is added or removed from the port. Agent deployment ~~~~~~~~~~~~~~~~ The agent can be deployed anywhere as long as it is in the respective L2 network that is to be announced. In addition, OVS agent must be installed on the machine (from which it reads SB DB address) and it must be possible to connect to the Southbound Database. The L2 network can be filtered via the address scope, so it is not necessary that the agent has access to all L2 provider networks, but only the one in which it is to peer. Unlike the ``ovn_bgp_driver``, it announces all routes regardless of which chassis they are on. As an example of how to start the OVN BGP Agent on the nodes, see the commands below: .. code-block:: ini $ python setup.py install $ cat bgp-agent.conf # sample configuration that can be adapted based on needs [DEFAULT] debug=True reconcile_interval=120 driver=ovn_stretched_l2_bgp_driver address_scopes=2237917c7b12489a84de4ef384a2bcae $ sudo bgp-agent --config-dir bgp-agent.conf .... Note that the OVN BGP Agent operates under the next assumptions: - A dynamic routing solution, in this case FRR, is deployed and advertises/withdraws routes added/deleted to/from the vrf routing table. A sample config for FRR is: .. code-block:: ini frr version 7.0 frr defaults traditional hostname cmp-1-0 log file /var/log/frr/frr.log debugging log timestamp precision 3 service integrated-vtysh-config line vty debug bgp neighbor-events debug bgp updates router bgp 64999 bgp router-id 172.30.1.1 neighbor pg peer-group neighbor 172.30.1.2 remote-as 64998 address-family ipv6 unicast redistribute kernel neighbor pg activate neighbor pg route-map IMPORT in neighbor pg route-map EXPORT out exit-address-family address-family ipv4 unicast redistribute kernel neighbor pg activate neighbor pg route-map IMPORT in neighbor pg route-map EXPORT out exit-address-family route-map EXPORT deny 100 route-map EXPORT permit 1 match interface bgp-nic route-map IMPORT deny 1 line vty Limitations ----------- - TBDovn-bgp-agent-2.0.1/doc/source/contributor/drivers/evpn_mode_design.rst000066400000000000000000000511471460327367600263400ustar00rootroot00000000000000.. This work is licensed under a Creative Commons Attribution 3.0 Unported License. http://creativecommons.org/licenses/by/3.0/legalcode Convention for heading levels in Neutron devref: ======= Heading 0 (reserved for the title in a document) ------- Heading 1 ~~~~~~~ Heading 2 +++++++ Heading 3 ''''''' Heading 4 (Avoid deeper levels because they do not render well.) ========================================================= Design of OVN BGP Agent with EVPN Driver (kernel routing) ========================================================= Purpose ------- The purpose of this document is to present the design decision behind the EVPN Driver for the Networking OVN BGP agent. The main purpose of adding support for EVPN is to be able to provide multitenancy aspects by using BGP in conjunction with EVPN/VXLAN. It allows tenants to have connectivity between VMs running in different clouds, with overlapping subnet CIDRs among tenants. Overview -------- The OVN BGP Agent is a Python based daemon that runs on each node (e.g., OpenStack controllers and/or compute nodes). It connects to the OVN Southbound DataBase (OVN SB DB) to detect the specific events it needs to react to, and then leverages FRR to expose the routes towards the VMs, and kernel networking capabilities to redirect the traffic once on the nodes to the OVN overlay. This simple design allows the agent to implement different drivers, depending on what OVN SB DB events are being watched (watchers examples at ``ovn_bgp_agent/drivers/openstack/watchers/``), and what actions are triggered in reaction to them (drivers examples at ``ovn_bgp_agent/drivers/openstack/XXXX_driver.py``, implementing the ``ovn_bgp_agent/drivers/driver_api.py``). A new driver implements the support for EVPN capabilities with multitenancy (overlapping CIDRs), by leveraging VRFs and EVPN Type-5 Routes. The API used is the ``networking_bgpvpn`` upstream project, and a new watcher is created to react to the information being added by it into the OVN SB DB (using the ``external-ids`` field). Proposed Solution ----------------- To support EVPN the functionality of the OVN BGP Agent needs to be extended with a new driver that performs the extra steps required for the EVPN configuration and steering the traffic to/from the node from/to the OVN overlay. The only configuration needed is to enable the specific driver on the ``bgp-agent.conf`` file. This new driver will also require a new watcher to react to the EVPN-related events. In this case, the EVPN events will be triggered by the addition of EVPN/VNI information into the relevant OVN ``logical_switch_ports`` at the OVN NB DB, which gets translated into external-ids at ``port_binding`` table at the OVN SB DB. This information is added into OVN DBs by the ``networking-bgpvpn`` projects. The admin and user API to leverage the EVPN functionality is provided by extending the ``networking-bgpvpn`` upstream project with a new service plugin for ML2/OVN. This plugin will annotate the needed information regarding VNI ids into the OVN DBs by using the ``external-ids`` field. BGPVPN API ~~~~~~~~~~ To allow users to expose their tenant networks through EVPN, without worring about overlapping CIDRs from other tenants, the ``networking-bgpvpn`` upstream project is leveraged as the API. It fits nicely as it has: - An Admin API to define the BGPVPN properties, such as the VNI or the BGP AS to be used, and to associate it to a given tenant. - A Tenant API to allow users to associate the BGPVPN to a router or to a network. This provides an API that allows users to expose their tenant networks, and admins to provide the needed EVPN/VNI information. Then, we need to enhance ``networking-bgpvpn`` with ML2/OVN support so that the provided information is stored on the OVN SB DB and consumed by the new driver (when the watcher detects it). The overall arquitecture and integration between the ``networking-bgpvpn`` and the ``networking-bgp-ovn`` agent are shown in the next figure: .. image:: ../../../images/networking-bgpvpn_integration.png :alt: integration components :align: center :width: 100% There are 3 main components: - ``BGPVPN API``: This is the component that enables the association of RT/VNIs to tenant network/routers. It creates a couple of extra DBs on Neutron to keep the information. This is the component we leverage, restricting some of the APIs. - ``OVN Service Plugin Driver``: (for ml2/ovs, the equivalent is the bagpipe driver) This is the component in charge of triggering the extra actions to notify the backend driver about the changes needed (RPCs for the ml2/ovs bagpipe driver). In our case it is a simple driver that just integrates with OVN (OVN NB DB) to ensure the information gets propagated to the corresponding OVN resource in the OVN Southbound database — by adding the information into the external_ids field. The Neutron ML2/OVN driver already copies the external_ids information of the ports from the ``Logical_Switch_Port`` table at the OVN NB DB into the ``Port_Binding`` table at the OVN SB DB. Thus the new OVN service plugin driver only needs to annotate the relevant ports at the ``Logical_Switch_Port`` table with the required EVPN information (BGP AS number and VNI number) on the ``external_ids`` field. Then, it gets automatically translated into the OVN SB DB at the ``Port_Binding`` table, ``external_ids`` field, and the OVN BGP Agent can react to it. - ``Backend driver``, i.e., the networking-bgp-ovn with the EVPN driver: (for ml2/ovs, the equivalent is the bagpipe-bgp project) This is the backend driver running on the nodes, in charge of configuring the networking layer based on the needs. In this case, the agent continues to consume information from the OVN SB DB (reading the extra information at external_ids, instead of relying on RPC as in the bagpipe-bgp case), and adds the needed kernel routing and FRR configuration, as well as OVS flows to steer the traffic to/from OVN overlay. As regards to the API actions implemented, the user can: - Associate the BGPVPN to a network: The OVN service plugin driver annotates the information into the ``external_ids`` field of the ``Logical_Switch_Port`` associated to the network router interface port (OVN patch port). Additionally, the router where the network is connected also gets the ``Logical_Switch_Port`` associated to the router gateway port annotated (OVN patch port). - Associate the BGPVPN to a router: The OVN service plugin driver performs the same actions as before, but annotating all the router interface ports connected to the router (i.e., all the subnets attached to the router). OVN SB DB Events ~~~~~~~~~~~~~~~~ The networking-bgp-ovn watcher that the EVPN driver uses need to detect the relevant events on the OVN SB DB to call the driver functions to configure EVPN. When the VNI information is added/updated/delete to either a router gateway port (patch port on the Port_Binding table) or a router interface port (also a patch port on the Port_Binding table), it is clear that some actions need to be trigger. However there are other events that should be processed such as: - VM creation on a exposed network/router - Router exposed being attached/detached from the provider network - Subnet exposed being attached/detached from the router The EVPN watcher detects OVN SB DB events of ``RowEvent`` type at the ``Port_Binding`` table. It creates a new event class named ``PortBindingChassisEvent``, that all the rest extend. The EVPN watcher reacts to the same type of events as the BGP watcher, but with some differences. Also, it does not react to FIPs related events as EVPN is only used for tenant networks. The specific defined events to react to are: - ``PortBindingChassisCreatedEvent`` (set gateway port for router): Detects when a port of type ``chassisredirect`` gets attached to the OVN chassis where the agent is running. This is the case for neutron gateway router ports (cr-lrps). It calls ``expose_ip`` driver method to decide if it needs to expose it through EVPN (in case it has related EVPN info annotated). - ``PortBindingChassisDeletedEvent`` (unset gateway port for router): Detects when a port of type ``chassisredirect`` gets detached from the OVN chassis where teh agent is running. This is the case for neutron gateway router ports (cr-lrps). It calls ``withdraw_ip`` driver method to decide if it needs to withdraw the exposed EVPN route (in case it had EVPN info annotated). - ``SubnetRouterAttachedEvent`` (add BGPVPN to router/network or attach subnet to router): Detects when a port of type ``patch`` gets created/updated with EVPN information (VNI and BGP_AS). These type of ports can be of 2 types: 1) related to the router gateway port and therefore calling the ``expose_ip`` method, as in the ``PortBindingChassisCreateEvent``. The different is that in ``PortBindingChassisCreateEvent`` event the port was being created as a result of attaching the router to the provider network, while in the ``SubnetRouterAttachedEvent`` event the port was already there but information related to EVPN was added, i.e., the router was exposed by associating it a BGPVPN. 2) related to the router interface port and therefore calling the ``expose_subnet`` method. This method will check if the associated gateway port is on the local chassis (where the agent runs) to proceed with the configuration steps to redirect the traffic to/from OVN overlay. - ``SubnetRouterDetachedEvent`` (remove BGPVPN from router/network or detach subnet from router): Detects when a port of type ``patch`` gets either updated (removal of EVPN information) or directly deleted. The same 2 type of ports as in the previous event can be found, and the method ``withdraw_ip`` or ``withdraw_subnet`` are called for router gateway and router interface ports, respectively. - ``TenantPortCreatedEvent`` (VM created): Detects when a port of type ``""`` or ``virtual`` gets updated (chassis added). It calls the method ``expose_remote_ip``. The method checks if the port is not on a provider network and the chassis where the agent is running has the gateway port for the router the VM is connected to. - ``TenantPortDeletedEvent`` (VM deleted): Detects when a port of type ``""`` or ``virtual`` gets updated (chassis deleted) or deleted. It calls the method ``withdraw_remote_ip``. The method checks if the port is not on a provider network and the chassis where the agent is running has the gateway port for the router the VM is connected to. Driver Logic ~~~~~~~~~~~~ The EVPN driver is in charge of the networking configuration ensuring that VMs on tenant networks can be reached through EVPN (N/S traffic). To acomplish this, it needs to ensure that: - VM IPs can be advertized in a node where the traffic could be injected into OVN overlay, in this case the node where the router gateway port is scheduled (see limitations subsection). - Once the traffic reaches the specific node, the traffic is redirected to the OVN overlay. To do that it needs to: 1. Create the EVPN related devices when a router gets attached to the provider network and/or gets a BGPVPN assigned to it. - Create the VRF device, using the VNI number as the routing table number associated to it, as well as for the name suffix: vrf-1001 for vni 1001 .. code-block:: ini ip link add vrf-1001 type vrf table 1001 - Create the VXLAN device, using the VNI number as the vxlan id, as well as for the name suffix: vxlan-1001 .. code-block:: ini ip link add vxlan-1001 type vxlan id 1001 dstport 4789 local LOOPBACK_IP nolearning - Create the Bridge device, where the vxlan device is connected, and associate it to the created vrf, also using the VNI number as name suffix: br-1001 .. code-block:: ini ip link add name br-1001 type bridge stp_state 0 ip link set br-1001 master vrf-1001 ip link set vxlan-1001 master br-1001 - Create a dummy device, where the IPs to be exposed will be added. It is associated to the created vrf, and also using the VNI number as name suffix: lo-1001 .. code-block:: ini ip link add name lo-1001 type dummy ip link set lo-1001 master vrf-1001 .. note:: The VRF is not associated to an OpenStack tenant but to a router gateway ports, meaning that if a tenant has several Neutron routers connected to the provider network, it will have a different VRFs, one associated with each one of them. 2. Reconfigure local FRR instance (``frr.conf``) to ensure the new VRF is exposed. To do that it uses ``vtysh shell``. It connects to the existing FRR socket (--vty_socket option) and executes the next commands, passing them through a file (-c FILE_NAME option): .. code-block:: ini ADD_VRF_TEMPLATE = ''' vrf {{ vrf_name }} vni {{ vni }} exit-vrf router bgp {{ bgp_as }} vrf {{ vrf_name }} address-family ipv4 unicast redistribute connected exit-address-family address-family ipv6 unicast redistribute connected exit-address-family address-family l2vpn evpn advertise ipv4 unicast advertise ipv6 unicast exit-address-family ''' 3. Connect EVPN to OVN overlay so that traffic can be redirected from the node to the OVN virtual networking. It needs to connect the VRF to the OVS provider bridge: - Create veth device and attach one end to the OVS provider bridge, and the other to the vrf: .. code-block:: ini ip link add veth-vrf type veth peer name veth-ovs ovs-vsctl add-port br-ex veth-ovs ip link set veth-vrf master vrf-1001 ip link set up dev veth-ovs ip link set up dev veth-vrf - Or the equivalent steps (vlan device) for the vlan provider network cases: .. code-block:: ini ovs-vsctl add-port br-vlan br-vlan-1001 tag=ID -- set interface br-vlan-1001 type=internal ip link set br-vlan-1001 up ip link set br-vlan-1001 master vrf-1001 - Add route on the VRF routing table for both the router gateway port IP and the subnet CIDR so that the traffic is redirected to the OVS provider bridge (e.g., br-ex) through the veth/vlan device .. code-block:: ini $ ip route show vrf vrf-1001 10.0.0.0/26 via 172.24.4.146 dev veth-vrf-1001|br-vlan-1001 172.24.4.146 dev veth-vrf-1001|br-vlan-1001 scope link 4. Add needed OVS flows into the OVS provider bridge (e.g., br-ex) to redirect the traffic back from OVN to the proper VRF, based on the subnet CIDR and the router gateway port MAC address. .. code-block:: ini $ ovs-ofctl add-flow br-ex cookie=0x3e7,priority=1000,ip,in_port=1,dl_src:ROUTER_GATEWAY_PORT_MAC,nw_src=SUBNET_CIDR, actions=mod_dl_dst:VETH|VLAN_MAC,output=VETH|VLAN_PORT 5. Add IPs to expose to VRF associated dummy device. This interface is only used for the purpose of exposing the IPs, but not meant to receive the traffic. Thus, the local route being automatically added pointing to the dummy interface on the VRF for that (VM) IP is removed so that the traffic can get redirected properly to the OVN overlay. .. code-block:: ini $ ip addr add 10.0.0.5/32 dev lo-1001 $ ip route show vrf table 1001 | grep local 10.0.0.5 dev lo-1001 $ ip route delete local 10.0.0.5 dev 1001 table 1001 Driver API ++++++++++ The EVPN driver needs to implement the ``driver_api.py`` interface. It implements the next functions: - ``expose_ip``: Creates all the VRF/VXLAN configuration (devices and its connection to the OVN overlay) as well as the VRF configuration at FRR (steps 1 to 3). It also checks if there are subnets and VMs connected to the OVN gateway router port that must be exposed through EVPN (steps 4-5). - ``withdraw_ip``: removes the above configuration (devices and FRR configuration). - ``expose_subnet``: add kernel and ovs networking configuration to ensure traffic can go from the node to the OVN overlay, and viceversa, for IPs within the subnet CIDR and on the right VRF -- step 4. - ``withdraw_subnet``: removes the above kernel and ovs networking configuration. - ``expose_remote_ip``: EVPN expose VM tenant network IPs through the chassis hosting the OVN gateway port for the router where the VM is connected. It ensures traffic destinated to the VM IP arrives to this node (step 5). The previous steps ensure the traffic is redirected to the OVN overlay once on the node. - ``withdraw_remote_ip``: EVPN withdraw VM tenant network IPs through the chassis hosting the OVN gateway port for the router where the VM is connected. It ensures traffic destinated to the VM IP stops arriving to this node. Traffic flow ~~~~~~~~~~~~ The next figure shows the N/S traffic flow through the VRF to the VM, including information regarding the OVS flows on the provider bridge (br-ex), and the routes on the VRF routing table. .. image:: ../../../images/evpn_traffic_flow.png :alt: integration components :align: center :width: 100% The IPs of both the router gateway port (cr-lrp, 172.24.1.20), as well as the IP of the VM itself (20.0.0.241/32) gets added to the dummy device (lo-101) associated to the vrf (vrf-101) which was used for defining the BGPVPN (vni 101). That together with the other devices created on the VRF (vxlan-101 and br-101), and with the FRR reconfiguration ensure the IPs get exposed in the right EVPN. This allows the traffic to reach the node with the router gateway port (cr-lrp on OVN). However this is not enough as the traffic needs to be redirected to the OVN Overlay. To do that the VRF is added to the br-ex OVS provider bridge (br-ex), and two routes are added to the VRF routing table to redirect the traffic going to the network (20.0.0.0/24) through the cr-lrp port to the br-ex OVS bridge. That injects the traffic properly into the OVN overlay, which will redirect it through the geneve tunnel (by the br-int ovs flows) to the compute node hosting the VM. The reply from the VM will come back through the same tunnel. However an extra OVS flow needs to be added to the OVS provider bridge (br-ex) to ensure the traffic is redirected back to the VRF (vrf-101) if the traffic is coming from the exposed network (20.0.0.0/24) -- instead of using the default routing table (action=NORMAL). To that end, the next rule is added: .. code-block:: ini cookie=0x3e6, duration=4.141s, table=0, n_packets=0, n_bytes=0, priority=1000,ip,in_port="patch-provnet-c",dl_src=fa:16:3e:b7:cc:47,nw_src=20.0.0.0/24 actions=mod_dl_dst:1e:8b:ac:5d:98:4a,output:"veth-ovs-101" It matches the traffic coming from the router gateway port (cr-lrp port) from br-int (in_port="patch-provnet-c"), with the MAC address of the router gateway port (dl_src=fa:16:3e:b7:cc:47) and from the exposed network (nw_src=20.0.0.0/24). For that case it changes the MAC by the veth-vrf-101 device one (mod_dl_dst:1e:8b:ac:5d:98:4a), and redirect the traffic to the vrf device through the veth/vlan device (output:"veth-ovs-101"). Agent deployment ~~~~~~~~~~~~~~~~ The EVPN mode exposes the VMs on tenant networks (on their respective EVPN/VXLAN). At OpenStack, with OVN networking, the N/S traffic to the tenant VMs (without FIPs) needs to go through the networking nodes, more specifically the one hosting the chassisredirect OVN port (cr-lrp), connecting the provider network to the OVN virtual router. As a result, there is no need to deploy the agent in all the nodes. Only the nodes that are able to host router gateway ports (cr-lrps), i.e., the ones tagged with the ``enable-chassis-gw``. Hence, the VM IPs are advertised through BGP/EVPN in one of those nodes, and from there it follows the normal path to the OpenStack compute node where the VM is allocated — the Geneve tunnel. Limitations ----------- The following limitations apply: - Network traffic is steer by kernel routing (VRF, VXLAN, Bridges), therefore DPDK, where the kernel space is skipped, is not supported - Network traffic is steer by kernel routing (VRF, VXLAN, Bridges), therefore SRIOV, where the hypervisor is skipped, is not supported. - In OpenStack with OVN networking the N/S traffic to the tenant VMs (without FIPs) needs to go through the networking nodes (the ones hosting the Neutron Router Gateway Ports, i.e., the chassisredirect cr-lrp ports). Therefore, the entry point into the OVN overlay need to be one of those networking nodes, and consequently the VMs are exposed through them. From those nodes the traffic will follow the normal tunneled path (Geneve tunnel) to the OpenStack compute node where the VM is allocated. ovn-bgp-agent-2.0.1/doc/source/contributor/drivers/index.rst000066400000000000000000000003531460327367600241330ustar00rootroot00000000000000========================== BGP Drivers Documentation ========================== .. toctree:: :maxdepth: 1 bgp_mode_design nb_bgp_mode_design ovn_bgp_mode_design evpn_mode_design bgp_mode_stretched_l2_design ovn-bgp-agent-2.0.1/doc/source/contributor/drivers/nb_bgp_mode_design.rst000066400000000000000000000445031460327367600266150ustar00rootroot00000000000000.. _nb_bgp_driver: ====================================================================== [NB DB] NB OVN BGP Agent: Design of the BGP Driver with kernel routing ====================================================================== Purpose ------- The addition of a BGP driver enables the OVN BGP agent to expose virtual machine (VMs) and load balancer (LBs) IP addresses through the BGP dynamic protocol when these IP addresses are either associated with a floating IP (FIP) or are booted or created on a provider network. The same functionality is available on project networks, when a special flag is set. This document presents the design decision behind the NB BGP Driver for the Networking OVN BGP agent. Overview -------- With the growing popularity of virtualized and containerized workloads, it is common to use pure Layer 3 spine and leaf network deployments in data centers. The benefits of this practice reduce scaling complexities, failure domains, and broadcast traffic limits The Northbound driver for OVN BGP agent is a Python-based daemon that runs on each OpenStack Controller and Compute node. The agent monitors the Open Virtual Network (OVN) northbound database for certain VM and floating IP (FIP) events. When these events occur, the agent notifies the FRR BGP daemon (bgpd) to advertise the IP address or FIP associated with the VM. The agent also triggers actions that route the external traffic to the OVN overlay. Unlike its predecessor, the Southbound driver for OVN BGP agent, the Northbound driver uses the northbound database API which is more stable than the southbound database API because the former is isolated from internal changes to core OVN. .. note:: Note northbound OVN BGP agent driver is only intended for the N/S traffic, the E/W traffic will work exactly the same as before, i.e., VMs are connected through geneve tunnels. The agent provides a multi-driver implementation that allows you to configure it for specific infrastructure running on top of OVN, for instance OpenStack or Kubernetes/OpenShift. This design simplicity enables the agent to implement different drivers, depending on what OVN NB DB events are being watched (watchers examples at ``ovn_bgp_agent/drivers/openstack/watchers/``), and what actions are triggered in reaction to them (drivers examples at ``ovn_bgp_agent/drivers/openstack/XXXX_driver.py``, implementing the ``ovn_bgp_agent/drivers/driver_api.py``). A driver implements the support for BGP capabilities. It ensures that both VMs and LBs on provider networks or associated Floating IPs are exposed through BGP. In addition, VMs on tenant networks can be also exposed if the ``expose_tenant_network`` configuration option is enabled. To control what tenant networks are exposed another flag can be used: ``address_scopes``. If not set, all the tenant networks will be exposed, while if it is configured with a (set of) address_scopes, only the tenant networks whose address_scope matches will be exposed. A common driver API is defined exposing the these methods: - ``expose_ip`` and ``withdraw_ip``: exposes or withdraws IPs for local OVN ports. - ``expose_remote_ip`` and ``withdraw_remote_ip``: exposes or withdraws IPs through another node when the VM or pods are running on a different node. For example, use for VMs on tenant networks where the traffic needs to be injected through the OVN router gateway port. - ``expose_subnet`` and ``withdraw_subnet``: exposes or withdraws subnets through the local node. Proposed Solution ----------------- To support BGP functionality the NB OVN BGP Agent includes a new driver that performs the steps required for exposing the IPs through BGP on the correct nodes and steering the traffic to/from the node from/to the OVN overlay. To configure the OVN BGP agent to use the northbound OVN BGP driver, in the ``bgp-agent.conf`` file, set the value of ``driver`` to ``nb_ovn_bgp_driver``. This driver requires a watcher to react to the BGP-related events. In this case, BGP actions are triggered by events related to ``Logical_Switch_Port``, ``Logical_Router_Port``and ``Load_Balancer`` on OVN NB DB tables. The information in these tables is modified when VMs and LBs are created and deleted, and when FIPs for them are associated and disassociated. Then, the agent performs these actions to ensure the VMs are reachable through BGP: - Traffic between nodes or BGP Advertisement: These are the actions needed to expose the BGP routes and make sure all the nodes know how to reach the VM/LB IP on the nodes. This is exactly the same as in the initial OVN BGP Driver (see :ref:`bgp_driver`) - Traffic within a node or redirecting traffic to/from OVN overlay (wiring): These are the actions needed to redirect the traffic to/from a VM to the OVN neutron networks, when traffic reaches the node where the VM is or in their way out of the node. The code for the NB BGP driver is located at ``ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py``, and its associated watcher can be found at ``ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py``. Note this new driver also allows different ways of wiring the node to the OVN overlay. These are configurable through the option ``exposing_method``, where for now you can select: - ``underlay``: using kernel routing (what we describe in this document), same as supported by the driver at :ref:`bgp_driver`. - ``ovn``: using an extra OVN cluster per node to perform the routing at OVN/OVS level instead of kernel, enabling datapath acceleration (Hardware Offloading and OVS-DPDK). More information about this mechanism at :ref:`bgp_driver`. OVN NB DB Events ~~~~~~~~~~~~~~~~ The watcher associated with the BGP driver detects the relevant events on the OVN NB DB to call the driver functions to configure BGP and linux kernel networking accordingly. .. note:: Linux Kernel Networking is used when the default ``exposing_method`` (``underlay``) is used. If ``ovn`` is used instead, OVN 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: - VMs or LBs created/deleted on provider networks - FIPs association/disassociation to VMs or LBs - VMs or LBs created/deleted on tenant networks (if the ``expose_tenant_networks`` configuration option is enabled, or if the ``expose_ipv6_gua_tenant_networks`` for only exposing IPv6 GUA ranges) .. note:: If ``expose_tenant_networks`` flag is enabled, it does not matter the status of ``expose_ipv6_gua_tenant_networks``, as all the tenant IPs are advertised. The NB BGP watcher reacts to the following events: - ``Logical_Switch_Port`` - ``Logical_Router_Port`` - ``Load_Balancer`` Besides the previously existing ``OVNLBEvent`` class, the NB BGP watcher has new event classes named ``LSPChassisEvent`` and ``LRPChassisEvent`` that all the events watched for NB BGP driver use as the base (inherit from). The specific defined events to react to are: - ``LogicalSwitchPortProviderCreateEvent``: Detects when a VM or an amphora LB port, logical switch ports of type ``""`` (empty double-qoutes) or ``virtual``, comes up or gets attached to the OVN chassis where the agent is running. If the ports are on a provider network, then the driver calls the ``expose_ip`` driver method to perform the needed actions to expose the port (wire and advertise). If the port is on a tenant network, the driver dismisses the event. - ``LogicalSwitchPortProviderDeleteEvent``: Detects when a VM or an amphora LB port, logical switch ports of type "" (empty double-qoutes) or ``virtual``, goes down or gets detached from the OVN chassis where the agent is running. If the ports are on a provider network, then the driver calls the ``withdraw_ip`` driver method to perform the needed actions to withdraw the port (withdraw and unwire). If the port is on a tenant network, the driver dismisses the event. - ``LogicalSwitchPortFIPCreateEvent``: Similar to ``LogicalSwitchPortProviderCreateEvent`` but focusing on the changes on the FIP information on the Logical Switch Port external_ids. It calls ``expose_fip`` driver method to perform the needed actions to expose the floating IP (wire and advertize). - ``LogicalSwitchPortFIPDeleteEvent``: Same as previous one but for withdrawing FIPs. In this case it is similar to ``LogicalSwitchPortProviderDeleteEvent`` but instaed calls the ``withdraw_fip`` driver method to perform the needed actions to withdraw the floating IP (Withdraw and unwire). - ``LocalnetCreateDeleteEvent``: Detects creation/deletion of OVN localnet ports, which indicates the creation/deletion of provider networks. This triggers a resync (``sync`` method) action to perform the base configuration needed for the provider networks, such as OVS flows or arp/ndp configurations. - ``ChassisRedirectCreateEvent``: Similar to ``LogicalSwitchPortProviderCreateEvent`` but with the focus on logical router ports, such as the Distributed Router Ports (cr-lrps), instead of logical switch ports. The driver calls ``expose_ip`` which performs additional steps to also expose IPs related to the cr-lrps, such as the ovn-lb or IPs in tenant networks. The watcher ``match`` checks the chassis information in the ``status`` field, which must be ovn23.09 or later. - ``ChassisRedirectDeleteEvent``: Similar to ``LogicalSwitchPortProviderDeleteEvent`` but with the focus on logical router ports, such as the Distributed Router Ports (cr-lrps), instead of logical switch ports. The driver calls ``withdraw_ip`` which performs additional steps to also withdraw IPs related to the cr-lrps, such as the ovn-lb or IPs in tenant networks. The watcher ``match`` checks the chassis information in the ``status`` field, which must be ovn23.09 or later. - ``LogicalSwitchPortSubnetAttachEvent``: Detects Logical Switch Ports of type ``router`` (connecting Logical Switch to Logical Router) and checks if the associated router is associated to the local chassis, i.e., if the cr-lrp of the router is located in the local chassis. If that is the case, the ``expose_subnet`` driver method is called which is in charge of the wiring needed for the IPs on that subnet (set of IP routes and rules). - ``LogicalSwitchPortSubnetDetachEvent``: Similar to ``LogicalSwitchPortSubnetAttachEvent`` but for unwiring the subnet, so it is calling the``withdraw_subnet`` driver method. - ``LogicalSwitchPortTenantCreateEvent``: Detects when a logical switch port of type ``""`` (empty double-qoutes) or ``virtual``, similar to ``LogicalSwitchPortProviderCreateEvent``. It checks if the network associated to the VM is exposed in the local chassis (meaning its cr-lrp is also local). If that is the case, it calls ``expose_remote_ip``, which manages the advertising of the IP -- there is no need for wiring, as that is done when the subnet is exposed by ``LogicalSwitchPortSubnetAttachEvent`` event. - ``LogicalSwitchPortTenantDeleteEvent``: Similar to ``LogicalSwitchPortTenantCreateEvent`` but for withdrawing IPs. Calling ``withdraw_remote_ips``. - ``OVNLBCreateEvent``: Detects Load_Balancer events and processes them only if the Load_Balancer entry has associated VIPs and the router is local to the chassis. If the VIP or router is added to a provider network, the driver calls ``expose_ovn_lb_vip`` to expose and wire the VIP or router. If the VIP or router is added to a tenant network, the driver calls ``expose_ovn_lb_vip`` to only expose the VIP or router. If a floating IP is added, then the driver calls ``expose_ovn_lb_fip`` to expose and wire the FIP. - ``OVNLBDeleteEvent``: If the VIP or router is removed from a provider network, the driver calls ``withdraw_ovn_lb_vip`` to withdraw and unwire the VIP or router. If the VIP or router is removed to a tenant network, the driver calls ``withdraw_ovn_lb_vip`` to only withdraw the VIP or router. If a floating IP is removed, then the driver calls ``withdraw_ovn_lb_fip`` to withdraw and unwire the FIP. Driver Logic ~~~~~~~~~~~~ The NB BGP driver is in charge of the networking configuration ensuring that VMs and LBs on provider networks or with FIPs can be reached through BGP (N/S traffic). In addition, if the ``expose_tenant_networks`` flag is enabled, VMs in tenant networks should be reachable too -- although instead of directly in the node they are created, through one of the network gateway chassis nodes. The same happens with ``expose_ipv6_gua_tenant_networks`` but only for IPv6 GUA ranges. In addition, if the config option ``address_scopes`` is set, only the tenant networks with matching corresponding ``address_scope`` will be exposed. .. note:: To be able to expose tenant networks a OVN version OVN 23.09 or newer is required. To accomplish the network configuration and advertisement, the driver ensures: - VM and LBs IPs can be advertised in a node where the traffic can be injected into the OVN overlay: either in the node that hosts the VM or in the node where the router gateway port is scheduled. (See the "limitations" subsection.). - After the traffic reaches the specific node, kernel networking redirects the traffic to the OVN overlay, if the default ``underlay`` exposing method is used. .. include:: ../bgp_advertising.rst Traffic flow from tenant networks +++++++++++++++++++++++++++++++++ By default neutron enables SNAT on routers (because that is typically what you'd use the routers for). This has some side effects that might not be all that convenient; for one, all connections initiated from VMs in tenant networks will be externally identified with the IP of the cr-lrp. The VMs in the tenant networks are reachable through their own ip and return traffic will flow as expected as well, but it is just not really what one would expect. To prevent tenant networks from being exposed if SNAT is enabled, one can set the configuration option ``require_snat_disabled_for_tenant_networks`` to ``True`` This will check if the cr-lrp has SNAT disabled for that subnet, and prevent announcement of those tenant networks. .. note:: Neutron will add IPv6 subnets are without NAT, so even though the IPv4 of those tenant networks might have NAT enabled, the IPv6 subnet might still be exposed, as this has no NAT enabled. To disable the SNAT on a neutron router, one could simply run this command: .. code-block:: ini $ openstack router set --disable-snat --external-gateway .. include:: ../bgp_traffic_redirection.rst Driver API ++++++++++ The NB BGP driver implements the ``driver_api.py`` interface with the following functions: - ``expose_ip``: creates all the IP rules and routes, and OVS flows needed to redirect the traffic to OVN overlay. It also ensures that FRR exposes the required IP by using BGP. - ``withdraw_ip``: removes the configuration (IP rules/routes, OVS flows) from ``expose_ip`` method to withdraw the exposed IP. - ``expose_subnet``: adds kernel networking configuration (IP rules and route) to ensure traffic can go from the node to the OVN overlay (and back) for IPs within the tenant subnet CIDR. - ``withdraw_subnet``: removes kernel networking configuration added by ``expose_subnet``. - ``expose_remote_ip``: BGP expose VM tenant network IPs through the chassis hosting the OVN gateway port for the router where the VM is connected. It ensures traffic directed to the VM IP arrives at this node by exposing the IP through BGP locally. The previous steps in ``expose_subnet`` ensure the traffic is redirected to the OVN overlay after it arrives on the node. - ``withdraw_remote_ip``: removes the configuration added by ``expose_remote_ip``. And in addition, the driver also implements extra methods for the FIPs and the OVN load balancers: - ``expose_fip`` and ``withdraw_fip`` which are equivalent to ``expose_ip`` and ``withdraw_ip`` but for FIPs. - ``expose_ovn_lb_vip``: adds kernel networking configuration to ensure traffic is forwarded from the node with the associated cr-lrp to the OVN overlay, as well as to expose the VIP through BGP in that node. - ``withdraw_ovn_lb_vip``: removes the above steps to stop advertising the load balancer VIP. - ``expose_ovn_lb_fip`` and ``withdraw_ovn_lb_fip``: for exposing the FIPs associated to ovn loadbalancers. This is similar to ``expose_fip/withdraw_fip`` but taking into account that it must be exposed on the node with the cr-lrp for the router associated to the loadbalancer. .. include:: ../agent_deployment.rst Limitations ----------- The following limitations apply: - OVN 23.09 or later is needed to support exposing tenant networks IPs and OVN loadbalancers. - 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 networks, use the flag ``address_scopes`` to filter which subnets to expose, which also prefents having overlapping IPs. - In the currently implemented exposing methods (``underlay`` and ``ovn``) there is no support for overlapping CIDRs, so this must be avoided, e.g., by using address scopes and subnet pools. - For the default exposing method (``underlay``) the network traffic is steered by kernel routing (ip routes and rules), therefore OVS-DPDK, where the kernel space is skipped, is not supported. 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 by kernel routing (ip routes and rules), therefore SRIOV, where the hypervisor is skipped, is not supported. 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 the provider or the FIPs associated with the VIPs on tenant networks needs to go through the networking nodes (the ones hosting the Neutron Router Gateway Ports, i.e., the chassisredirect cr-lrp ports, for the router connecting the load balancer members to the provider network). Therefore, the entry point into the OVN overlay needs to be one of those networking nodes, and consequently the VIPs (or FIPs to VIPs) are exposed through them. From those nodes the traffic will follow the normal tunneled path (Geneve tunnel) to the OpenStack compute node where the selected member is located. ovn-bgp-agent-2.0.1/doc/source/contributor/drivers/ovn_bgp_mode_design.rst000066400000000000000000000250761460327367600270240ustar00rootroot00000000000000.. _ovn_routing: =================================================================== [NB DB] NB OVN BGP Agent: Design of the BGP Driver with OVN routing =================================================================== This is an extension of the NB OVN BGP Driver which adds a new ``exposing_method`` named ``ovn`` to make use of OVN routing, instead of relying on Kernel routing. Purpose ------- This document presents the design decision behind the extensions on the NB OVN BGP Driver to support OVN routing instead of kernel routing, and therefore enabling datapath acceleartion. Overview -------- The main goal is to make the BGP capabilities of OVN BGP Agent compliant with OVS-DPDK and HWOL. To do that we need to move to OVN/OVS what the OVN BGP Agent is currently doing with Kernel networking -- redirect traffic to/from the OpenStack OVN Overlay. To accomplish this goal, the following is required: - Ensure that incoming traffic gets redirected from the physical NICs to the OVS integration bridge (br-int) though one or more OVS provider bridges (br-ex) without using kernel routes and rules. - Ensure the outgoing traffic gets redirected to the physical NICs without using the default kernel routes. - Expose the IPs in the same way as we did before. The third point is simple as it is already being done, but for the first two points OVN virtual routing capabilities are needed, ensuring the traffic gets routed from the NICS to the OpenStack Overlay and vice versa. Proposed Solution ----------------- To avoid placing kernel networking in the middle of the datapath and blocking acceleration, the proposed solution mandates locating a separate OVN cluster on each node that manages the needed virtual infrastructure between the OpenStack networking overlay and the physical network. Because routing occurs at OVN/OVS level, this proposal makes it is possible to support hardware offloading (HWOL) and OVS-DPDK. The next figure shows the proposed cluster required to manage the OVN virtual networking infrastructure on each node. .. image:: ../../../images/ovn-cluster-overview.png :alt: OVN Routing integration :align: center :width: 100% In a standard deployment ``br-int`` is directly connected to the OVS external bridge (``br-ex``) where the physical NICs are attached. By contrast, in the default BGP driver solution (see :ref:`nb_bgp_driver`), the physical NICs are not directly attached to br-ex, but rely on kernel networking (ip routes and ip rules) to redirect the traffic to ``br-ex``. The OVN routing architecture proposes the following mapping: - ``br-int`` connects to an external (from the OpenStack perspective) OVS bridge (``br-osp``). - ``br-osp`` does not have any physical resources attached, just patch ports connecting them to ``br-int`` and ``br-bgp``. - ``br-bgp`` is the integration bridge managed by the extra OVN cluster deployed per node. This is where the virtual OVN resources are be created (routers and switches). It creates mappings to ``br-osp`` and ``br-ex`` (patch ports). - ``br-ex`` keeps being the external bridge, where the physical NICs are attached (as in default environments without BGP). But instead of being directly connected to ``br-int``, is connected to ``br-bgp``. Note for ECMP purposes, each nic is attached to a different ``br-ex`` device (``br-ex`` and ``br-ex-2``). The virtual OVN resources requires the following: - Logical Router (``bgp-router``): manages the routing that was previously done in the kernel networking layer between both networks (physical and OpenStack OVN overlay). It has two connections (i.e., Logical Router Ports) towards the ``bgp-ex-X`` Logical Switches to add support for ECMP (only one switch is required but you must have several in case of ECMP), and one connection to the ``bgp-osp`` Logical Switch to ensure traffic to/from the OpenStack networking overlay. - Logical Switch (``bgp-ex``): is connected to the ``bgp-router``, and has a localnet to connect it to ``br-ex`` and therefore the physical NICs. There is one Logical Switch per NIC (``bgp-ex`` and ``bgp-ex-2``). - Logical Switch (``bgp-osp``): is connected to the ``bgp-router``, and has a localnet to connect it to ``br-osp`` to enable it to send traffic to and from the OpenStack OVN overlay. The following OVS flows are required on both OVS bridges: - ``br-ex-X`` bridges: require a flow to ensure only the traffic targetted for OpenStack provider networks is redirected to the OVN cluster. .. code-block:: ini cookie=0x3e7, duration=942003.114s, table=0, n_packets=1825, n_bytes=178850, priority=1000,ip,in_port=eth1,nw_dst=172.16.0.0/16 actions=mod_dl_dst:52:54:00:30:93:ea,output:"patch-bgp-ex-lo" - ``br-osp`` bridge: require a flow for each OpenStack provider network to change the MAC by the one on the router port in the OVN cluster and to properly manage traffic that is routed to the OVN cluster. .. code-block:: ini cookie=0x3e7, duration=942011.971s, table=0, n_packets=8644, n_bytes=767152, priority=1000,ip,in_port="patch-provnet-0" actions=mod_dl_dst:40:44:00:00:00:06,NORMAL OVN NB DB Events ~~~~~~~~~~~~~~~~ The OVN northbound database events that the driver monitors are the same as the ones for the NB DB driver with the ``underlay`` exposing mode. See :ref:`nb_bgp_driver`. The main difference between the two drivers is that the wiring actions are simplified for the OVN routing driver. Driver Logic ~~~~~~~~~~~~ As with the other BGP drivers or ``exposing modes`` (:ref:`bgp_driver`, :ref:`nb_bgp_driver`) the NB DB Driver with the ``ovn`` exposing mode enabled (i.e., enabling ``OVN routing`` instead of rely on ``Kernel networking``) is in charge of exposing the IPs with BGP and of the networking configuration to ensure that VMs abd LBs on provider networks or with FIPs can be reached through BGP (N/S traffic). Similarly, if ``expose_tenant_networks`` flag is enabled, VMs in tenant networks should be reachable too -- although instead of directly in the node they are created, through one of the network gateway chassis nodes. The same happens with ``expose_ipv6_gua_tenant_networks`` but only for IPv6 GUA ranges. In addition, if the config option ``address_scopes`` is set only the tenant networks with matching corresponding address_scope will be exposed. To accomplish this, it needs to configure the extra per node ovn cluster to ensure that: - VM and LBs IPs can be advertized in a node where the traffic could be injected into the OVN overlay through the extra ovn cluster (instead of the Kernel routing) -- either in the node hosting the VM or the node where the router gateway port is scheduled. - Once the traffic reaches the specific node, the traffic is redirected to the OVN overlay by using the extra ovn cluster per node with the proper OVN configuration. To do this it needs to create Logical Switches, Logical Routers and the routing configuration between them (routes and policies). .. include:: ../bgp_advertising.rst Traffic Redirection to/from OVN +++++++++++++++++++++++++++++++ As explained before, the main idea of this exposing mode is to leverage OVN routing instead of kernel routing. For the traffic going out the steps are the next: - If (OpenStack) OVN cluster knows about the destination MAC then that works as in deployment without BGP or OVN cluster support (no arp needed, MAC directly used). If the MAC is unknown but on the same provider network(s) range, the ARP gets replied by the Logical Switch Port on the ``bgp-osp`` LS thanks to enabling arp_proxy on it. And if it is a different range, it will reply due to the router having default routes to the outside. The flow at ``br-osp`` is in charge of changing the destination MAC by the one on the Logical Router Port on ``bgp-router`` LR. - The previous step takes the traffic to the extra OVN cluster per node, where the default (ECMP) routes are used to send the traffic to the external Logical Switch and from there to the physical nics attached to the external OVS bridge(s) (``br-ex``, ``br-ex-2``). In case of known MAC by OpenStack, instead of the default routes, a Logical Route Policy gets applied so that traffic is forced to be redirected out (through the LRPs connected to the external LS) when comming through the internal LRP (the one connected to OpenStack). And for the traffic comming in: - The flow hits the ovs flow added at the ``br-ex-X`` bridge(s) to redirect the traffic to the per node OVN cluster, changing the destination MAC by the one at the related ``br-ex`` device, which are the same used for the OVN cluster Logical Router Ports. This takes the traffic to the OVN router. - After that, thanks to having the arp_proxy enabled on the LSP on ``bgp-osp`` the traffic will be redirected to there. And due to a limitation in the functionality of arp_proxy, there is a need of adding an extra static mac binding entry in the cluster so that the VM MAC is used for destination instead of the own LSP MAC, which would lead to droping the traffic on the LS pipeline. .. code-block:: ini _uuid : 6e1626b3-832c-4ee6-9311-69ebc15cb14d ip : "172.16.201.219" logical_port : bgp-router-openstack mac : "fa:16:3e:82:ee:19" override_dynamic_mac: true Driver API ++++++++++ This is the very same as in the NB DB driver with the ``underlay`` exposing mode. See :ref:`nb_bgp_driver`. Agent deployment ~~~~~~~~~~~~~~~~ The deployment is similar to the NB DB driver with the ``underlay`` exposing method but with some extra configuration. See :ref:`nb_bgp_driver` for the base. It is needed to state the exposing method in the DEFAULT section and the extra configuration for the local ovn cluster that performs the routing, including the range for the provider networks to expose/handle: .. code-block:: ini [DEFAULT] exposing_method=ovn [local_ovn_cluster] ovn_nb_connection=unix:/run/ovn/ovnnb_db.sock ovn_sb_connection=unix:/run/ovn/ovnsb_db.sock external_nics=eth1,eth2 peer_ips=100.64.1.5,100.65.1.5 provider_networks_pool_prefixes=172.16.0.0/16 Limitations ----------- The following limitations apply: - OVN 23.06 or later is required - Tenant networks, subnet and OVN load balancers are not yet supported, and will require OVN vesion 23.09 or newer. - IPv6 not yet supported - ECMP not properly working as there is no support for BFD at the ovn-cluster, which means if one of the routes goes away the OVN cluster won't react to it and there will be traffic disruption. - There is no support for overlapping CIDRs, so this must be avoided, e.g., by using address scopes and subnet pools. ovn-bgp-agent-2.0.1/doc/source/contributor/index.rst000066400000000000000000000003131460327367600224510ustar00rootroot00000000000000=========================== Contributor Documentation =========================== .. toctree:: :maxdepth: 2 drivers/index agent_deployment bgp_advertising bgp_traffic_redirection ovn-bgp-agent-2.0.1/doc/source/index.rst000066400000000000000000000010541460327367600201020ustar00rootroot00000000000000.. ovn-bgp-agent documentation master file, created by sphinx-quickstart on Tue Jul 9 22:26:36 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. ============================================= Welcome to the documentation of OVN BGP Agent ============================================= Contents: .. toctree:: :maxdepth: 3 readme contributor/index bgp_supportability_matrix Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ovn-bgp-agent-2.0.1/doc/source/readme.rst000066400000000000000000000000361460327367600202270ustar00rootroot00000000000000.. include:: ../../README.rst ovn-bgp-agent-2.0.1/etc/000077500000000000000000000000001460327367600147475ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/etc/frr/000077500000000000000000000000001460327367600155405ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/etc/frr/daemons000066400000000000000000000016251460327367600171150ustar00rootroot00000000000000bgpd=yes ospfd=no ospf6d=no ripd=no ripngd=no isisd=no pimd=no ldpd=no nhrpd=no eigrpd=no babeld=no sharpd=no pbrd=no bfdd=no fabricd=no vrrpd=no pathd=no # # If this option is set the /etc/init.d/frr script automatically loads # the config via "vtysh -b" when the servers are started. # Check /etc/pam.d/frr if you intend to use "vtysh"! # vtysh_enable=yes zebra_options=" -A 127.0.0.1 -s 90000000" bgpd_options=" -A 127.0.0.1" ospfd_options=" -A 127.0.0.1" ospf6d_options=" -A ::1" ripd_options=" -A 127.0.0.1" ripngd_options=" -A ::1" isisd_options=" -A 127.0.0.1" pimd_options=" -A 127.0.0.1" ldpd_options=" -A 127.0.0.1" nhrpd_options=" -A 127.0.0.1" eigrpd_options=" -A 127.0.0.1" babeld_options=" -A 127.0.0.1" sharpd_options=" -A 127.0.0.1" pbrd_options=" -A 127.0.0.1" staticd_options="-A 127.0.0.1" bfdd_options=" -A 127.0.0.1" fabricd_options="-A 127.0.0.1" vrrpd_options=" -A 127.0.0.1" ovn-bgp-agent-2.0.1/etc/frr/frr.conf000066400000000000000000000025711460327367600172050ustar00rootroot00000000000000frr version 7.0 frr defaults traditional hostname devstack log file /var/log/frr/frr.log informational log timestamp precision 3 service integrated-vtysh-config line vty router bgp 64999 bgp router-id 172.24.4.1 bgp log-neighbor-changes bgp graceful-shutdown no bgp default ipv4-unicast no bgp ebgp-requires-policy neighbor uplink peer-group neighbor uplink remote-as internal neighbor uplink password f00barZ neighbor br-ex interface peer-group uplink address-family ipv4 unicast redistribute connected neighbor uplink activate neighbor uplink allowas-in origin neighbor uplink prefix-list only-host-prefixes out exit-address-family address-family ipv6 unicast redistribute connected neighbor uplink activate neighbor uplink allowas-in origin neighbor uplink prefix-list only-host-prefixes out exit-address-family ip prefix-list only-default permit 0.0.0.0/0 ip prefix-list only-host-prefixes permit 0.0.0.0/0 ge 32 route-map rm-only-default permit 10 match ip address prefix-list only-default set src 172.24.4.1 ip protocol bgp route-map rm-only-default ipv6 prefix-list only-default permit ::/0 ipv6 prefix-list only-host-prefixes permit ::/0 ge 128 route-map rm-only-default permit 11 match ipv6 address prefix-list only-default set src 2001:db8::2 ipv6 protocol bgp route-map rm-only-default ip nht resolve-via-default ovn-bgp-agent-2.0.1/etc/oslo-config-generator/000077500000000000000000000000001460327367600211525ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/etc/oslo-config-generator/bgp-agent.conf000066400000000000000000000002641460327367600236670ustar00rootroot00000000000000[DEFAULT] output_file = etc/ovn-bgp-agent/bgp-agent.conf.sample wrap_width = 79 namespace = oslo.concurrency namespace = oslo.log namespace = oslo.privsep namespace = ovnbgpagent ovn-bgp-agent-2.0.1/etc/ovn-bgp-agent/000077500000000000000000000000001460327367600174135ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/etc/ovn-bgp-agent/rootwrap.conf000066400000000000000000000017421460327367600221430ustar00rootroot00000000000000# Configuration for ovn-bgp-agent-rootwrap # This file should be owned by (and only-writeable by) the root user [DEFAULT] # List of directories to load filter definitions from (separated by ','). # These directories MUST all be only writeable by root ! filters_path=/etc/ovn-bgp-agent/rootwrap.d,/usr/share/ovn-bgp-agent/rootwrap # List of directories to search executables in, in case filters do not # explicitely specify a full path (separated by ',') # If not specified, defaults to system PATH environment variable. # These directories MUST all be only writeable by root ! exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin,/usr/local/bin,/usr/local/sbin # Enable logging to syslog # Default value is False use_syslog=False # Which syslog facility to use. # Valid values include auth, authpriv, syslog, local0, local1... # Default value is 'syslog' syslog_log_facility=syslog # Which messages to log. # INFO means log all usage # ERROR means only log unsuccessful attempts syslog_log_level=ERROR ovn-bgp-agent-2.0.1/etc/ovn-bgp-agent/rootwrap.d/000077500000000000000000000000001460327367600215125ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/etc/ovn-bgp-agent/rootwrap.d/rootwrap.filters000066400000000000000000000011751460327367600247650ustar00rootroot00000000000000# ovn-bgp-agent-rootwrap command filters for scripts # This file should be owned by (and only-writable by) the root user [Filters] # privileged/__init__.py: priv_context.PrivContext(default) # This line ties the superuser privs with the config files, context name, # and (implicitly) the actual python code invoked. privsep-rootwrap: RegExpFilter, privsep-helper, root, privsep-helper, --config-file, /etc/(?!\.\.).*, --privsep_context, ovn_bgp_agent.privileged.default, --privsep_sock_path, /tmp/.* ovs-vsctl: CommandFilter, ovs-vsctl, root sysctl: CommandFilter, sysctl, root ip: IpFilter, ip, root vtysh: CommandFilter, vtysh, root ovn-bgp-agent-2.0.1/ovn_bgp_agent.egg-info/000077500000000000000000000000001460327367600204765ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent.egg-info/PKG-INFO000066400000000000000000000027601460327367600216000ustar00rootroot00000000000000Metadata-Version: 1.2 Name: ovn-bgp-agent Version: 2.0.1 Summary: The OVN BGP Agent allows to expose VMs/Containers/Networks through BGP on OVN Home-page: https://www.openstack.org/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: ============= OVN BGP Agent ============= The OVN BGP Agent allows to expose VMs/Containers through BGP on OVN * Free software: Apache license * Documentation: https://docs.openstack.org/ovn-bgp-agent * Source: https://opendev.org/openstack/ovn-bgp-agent * Bugs: https://bugs.launchpad.net/ovn-bgp-agent Features -------- * Expose VMs with FIPs or on Provider Networks through BGP on OVN environments. * Expose VMs on Tenant Networks through EVPN on OVN environments. Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.6 ovn-bgp-agent-2.0.1/ovn_bgp_agent.egg-info/SOURCES.txt000066400000000000000000000122061460327367600223630ustar00rootroot00000000000000.coveragerc .mailmap .stestr.conf AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE README.rst requirements.txt setup.cfg setup.py test-requirements.txt tox.ini devstack/local.conf.sample devstack/plugin.sh devstack/settings devstack/lib/ovn-bgp-agent doc/requirements.txt doc/images/evpn_traffic_flow.png doc/images/networking-bgpvpn_integration.png doc/images/ovn-cluster-overview.png doc/source/bgp_supportability_matrix.rst doc/source/conf.py doc/source/index.rst doc/source/readme.rst doc/source/contributor/agent_deployment.rst doc/source/contributor/bgp_advertising.rst doc/source/contributor/bgp_traffic_redirection.rst doc/source/contributor/index.rst doc/source/contributor/drivers/bgp_mode_design.rst doc/source/contributor/drivers/bgp_mode_stretched_l2_design.rst doc/source/contributor/drivers/evpn_mode_design.rst doc/source/contributor/drivers/index.rst doc/source/contributor/drivers/nb_bgp_mode_design.rst doc/source/contributor/drivers/ovn_bgp_mode_design.rst etc/frr/daemons etc/frr/frr.conf etc/oslo-config-generator/bgp-agent.conf etc/ovn-bgp-agent/rootwrap.conf etc/ovn-bgp-agent/rootwrap.d/rootwrap.filters ovn_bgp_agent/__init__.py ovn_bgp_agent/agent.py ovn_bgp_agent/config.py ovn_bgp_agent/constants.py ovn_bgp_agent/exceptions.py ovn_bgp_agent.egg-info/PKG-INFO ovn_bgp_agent.egg-info/SOURCES.txt ovn_bgp_agent.egg-info/dependency_links.txt ovn_bgp_agent.egg-info/entry_points.txt ovn_bgp_agent.egg-info/not-zip-safe ovn_bgp_agent.egg-info/pbr.json ovn_bgp_agent.egg-info/requires.txt ovn_bgp_agent.egg-info/top_level.txt ovn_bgp_agent/cmd/__init__.py ovn_bgp_agent/cmd/agent.py ovn_bgp_agent/drivers/__init__.py ovn_bgp_agent/drivers/driver_api.py ovn_bgp_agent/drivers/openstack/__init__.py ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py ovn_bgp_agent/drivers/openstack/utils/__init__.py ovn_bgp_agent/drivers/openstack/utils/bgp.py ovn_bgp_agent/drivers/openstack/utils/driver_utils.py ovn_bgp_agent/drivers/openstack/utils/frr.py ovn_bgp_agent/drivers/openstack/utils/ovn.py ovn_bgp_agent/drivers/openstack/utils/ovs.py ovn_bgp_agent/drivers/openstack/utils/wire.py ovn_bgp_agent/drivers/openstack/watchers/__init__.py ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py ovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py ovn_bgp_agent/privileged/__init__.py ovn_bgp_agent/privileged/linux_net.py ovn_bgp_agent/privileged/ovs_vsctl.py ovn_bgp_agent/privileged/vtysh.py ovn_bgp_agent/tests/__init__.py ovn_bgp_agent/tests/base.py ovn_bgp_agent/tests/test_ovn_bgp_agent.py ovn_bgp_agent/tests/utils.py ovn_bgp_agent/tests/functional/__init__.py ovn_bgp_agent/tests/functional/base.py ovn_bgp_agent/tests/functional/privileged/__init__.py ovn_bgp_agent/tests/functional/privileged/test_linux_net.py ovn_bgp_agent/tests/functional/utils/__init__.py ovn_bgp_agent/tests/functional/utils/test_linux_net.py ovn_bgp_agent/tests/unit/__init__.py ovn_bgp_agent/tests/unit/fakes.py ovn_bgp_agent/tests/unit/test_agent.py ovn_bgp_agent/tests/unit/cmd/__init__.py ovn_bgp_agent/tests/unit/cmd/test_agent.py ovn_bgp_agent/tests/unit/drivers/__init__.py ovn_bgp_agent/tests/unit/drivers/openstack/__init__.py ovn_bgp_agent/tests/unit/drivers/openstack/test_nb_ovn_bgp_driver.py ovn_bgp_agent/tests/unit/drivers/openstack/test_ovn_bgp_driver.py ovn_bgp_agent/tests/unit/drivers/openstack/test_ovn_evpn_driver.py ovn_bgp_agent/tests/unit/drivers/openstack/test_ovn_stretched_l2_bgp_driver.py ovn_bgp_agent/tests/unit/drivers/openstack/utils/__init__.py ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_driver_utils.py ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_frr.py ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovn.py ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovs.py ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_wire.py ovn_bgp_agent/tests/unit/drivers/openstack/watchers/__init__.py ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_base_watcher.py ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_bgp_watcher.py ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_evpn_watcher.py ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_nb_bgp_watcher.py ovn_bgp_agent/tests/unit/privileged/__init__.py ovn_bgp_agent/tests/unit/privileged/test_linux_net.py ovn_bgp_agent/tests/unit/privileged/test_ovs_vsctl.py ovn_bgp_agent/tests/unit/privileged/test_vtysh.py ovn_bgp_agent/tests/unit/utils/__init__.py ovn_bgp_agent/tests/unit/utils/test_helpers.py ovn_bgp_agent/tests/unit/utils/test_linux_net.py ovn_bgp_agent/utils/__init__.py ovn_bgp_agent/utils/common.py ovn_bgp_agent/utils/helpers.py ovn_bgp_agent/utils/linux_net.py releasenotes/notes/.placeholder releasenotes/notes/nb_driver-cc7098183fcedb0a.yaml releasenotes/source/2023.2.rst releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/unreleased.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder zuul.d/project.yaml zuul.d/tempest-singlenode.yamlovn-bgp-agent-2.0.1/ovn_bgp_agent.egg-info/dependency_links.txt000066400000000000000000000000011460327367600245440ustar00rootroot00000000000000 ovn-bgp-agent-2.0.1/ovn_bgp_agent.egg-info/entry_points.txt000066400000000000000000000011501460327367600237710ustar00rootroot00000000000000[console_scripts] ovn-bgp-agent = ovn_bgp_agent.cmd.agent:start ovn-bgp-agent-rootwrap = oslo_rootwrap.cmd:main ovn-bgp-agent-rootwrap-daemon = oslo_rootwrap.cmd:daemon [oslo.config.opts] ovnbgpagent = ovn_bgp_agent.config:list_opts [ovn_bgp_agent.drivers] nb_ovn_bgp_driver = ovn_bgp_agent.drivers.openstack.nb_ovn_bgp_driver:NBOVNBGPDriver ovn_bgp_driver = ovn_bgp_agent.drivers.openstack.ovn_bgp_driver:OVNBGPDriver ovn_evpn_driver = ovn_bgp_agent.drivers.openstack.ovn_evpn_driver:OVNEVPNDriver ovn_stretched_l2_bgp_driver = ovn_bgp_agent.drivers.openstack.ovn_stretched_l2_bgp_driver:OVNBGPStretchedL2Driver ovn-bgp-agent-2.0.1/ovn_bgp_agent.egg-info/not-zip-safe000066400000000000000000000000011460327367600227240ustar00rootroot00000000000000 ovn-bgp-agent-2.0.1/ovn_bgp_agent.egg-info/pbr.json000066400000000000000000000000561460327367600221550ustar00rootroot00000000000000{"git_version": "047d261", "is_release": true}ovn-bgp-agent-2.0.1/ovn_bgp_agent.egg-info/requires.txt000066400000000000000000000004401460327367600230740ustar00rootroot00000000000000Jinja2>=2.10 netaddr>=0.7.18 neutron-lib>=2.10.1 oslo.concurrency>=3.26.0 oslo.config>=6.1.0 oslo.log>=3.36.0 oslo.privsep>=2.3.0 oslo.rootwrap>=5.15.0 oslo.service>=1.40.2 ovs>=2.8.0 ovsdbapp>=1.16.0 pbr>=2.0 stevedore>=1.20.0 tenacity>=6.0.0 [:(sys_platform!='win32')] pyroute2>=0.6.6 ovn-bgp-agent-2.0.1/ovn_bgp_agent.egg-info/top_level.txt000066400000000000000000000000161460327367600232250ustar00rootroot00000000000000ovn_bgp_agent ovn-bgp-agent-2.0.1/ovn_bgp_agent/000077500000000000000000000000001460327367600170045ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/__init__.py000066400000000000000000000012351460327367600211160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # 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 pbr.version __version__ = pbr.version.VersionInfo( 'ovn-bgp-agent').version_string() ovn-bgp-agent-2.0.1/ovn_bgp_agent/agent.py000066400000000000000000000051651460327367600204630ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys from oslo_config import cfg from oslo_log import log as logging from oslo_service import loopingcall from oslo_service import service from ovn_bgp_agent import config from ovn_bgp_agent.drivers import driver_api CONF = cfg.CONF LOG = logging.getLogger(__name__) class BGPAgent(service.Service): """BGP OVN Agent.""" def __init__(self): super(BGPAgent, self).__init__() self.agent_driver = driver_api.AgentDriverBase.get_instance( CONF.driver) def start(self): LOG.info("Service '%s' starting", self.__class__.__name__) super(BGPAgent, self).start() self.agent_driver.start() LOG.info("Service '%s' started", self.__class__.__name__) sync_routes = loopingcall.FixedIntervalLoopingCall(self.sync) sync_routes.start(interval=CONF.reconcile_interval) sync_frr = loopingcall.FixedIntervalLoopingCall(self.frr_sync) sync_frr.start(interval=CONF.frr_reconcile_interval) def sync(self): LOG.info("Running reconciliation loop to ensure routes/rules are " "in place.") try: self.agent_driver.sync() except Exception as e: LOG.exception("Unexpected exception while running the sync: %s", e) def frr_sync(self): LOG.info("Running reconciliation loop to ensure frr configuration is " "in place.") try: self.agent_driver.frr_sync() except Exception as e: LOG.exception("Unexpected exception while running the frr sync: " "%s", e) def wait(self): super(BGPAgent, self).wait() LOG.info("Service '%s' stopped", self.__class__.__name__) def stop(self, graceful=False): LOG.info("Service '%s' stopping", self.__class__.__name__) super(BGPAgent, self).stop(graceful) def start(): config.register_opts() config.init(sys.argv[1:]) config.setup_logging() config.setup_privsep() bgp_agent_launcher = service.launch(config.CONF, BGPAgent()) bgp_agent_launcher.wait() ovn-bgp-agent-2.0.1/ovn_bgp_agent/cmd/000077500000000000000000000000001460327367600175475ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/cmd/__init__.py000066400000000000000000000000001460327367600216460ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/cmd/agent.py000066400000000000000000000012351460327367600212200ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from ovn_bgp_agent import agent start = agent.start if __name__ == '__main__': start() ovn-bgp-agent-2.0.1/ovn_bgp_agent/config.py000066400000000000000000000310101460327367600206160ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import shlex from oslo_config import cfg from oslo_log import log as logging from oslo_privsep import priv_context from ovn_bgp_agent import constants LOG = logging.getLogger(__name__) agent_opts = [ cfg.IntOpt('reconcile_interval', help='Time (seconds) between re-sync actions.', default=300), cfg.IntOpt('frr_reconcile_interval', help='Time (seconds) between re-sync actions to ensure frr ' 'configuration is correct, in case frr is restart.', default=15), cfg.BoolOpt('expose_tenant_networks', help='Expose VM IPs on tenant networks. ' 'If this flag is enabled, it takes precedence over ' 'expose_ipv6_gua_tenant_networks flag and all tenant ' 'network IPs will be exposed.', default=False), cfg.StrOpt('advertisement_method_tenant_networks', help='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.', default=constants.ADVERTISEMENT_METHOD_HOST, choices=[constants.ADVERTISEMENT_METHOD_HOST, constants.ADVERTISEMENT_METHOD_SUBNET]), cfg.BoolOpt('require_snat_disabled_for_tenant_networks', help='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', default=False), cfg.BoolOpt('expose_ipv6_gua_tenant_networks', help='Expose only VM IPv6 IPs on tenant networks if they are ' 'GUA. The expose_tenant_networks parameter takes ' 'precedence over this one. So if it is set, all the ' 'tenant network IPs will be exposed and not only the ' 'IPv6 GUA IPs.', default=False), cfg.StrOpt('driver', help='Driver to be used', choices=('ovn_bgp_driver', 'ovn_evpn_driver', 'ovn_stretched_l2_bgp_driver', 'nb_ovn_bgp_driver'), default='ovn_bgp_driver'), cfg.StrOpt('ovsdb_connection', default='unix:/usr/local/var/run/openvswitch/db.sock', help='The connection string for the native OVSDB backend.\n' 'Use tcp:IP:PORT for TCP connection.\n' 'Use unix:FILE for unix domain socket connection.'), cfg.IntOpt('ovsdb_connection_timeout', default=180, help='Timeout in seconds for the OVSDB connection transaction'), cfg.StrOpt('bgp_AS', default='64999', help='AS number to be used by the Agent when running in BGP ' 'mode and configuring the VRF route leaking.'), cfg.StrOpt('bgp_router_id', default=None, help='Router ID to be used by the Agent when running in BGP ' 'mode and configuring the VRF route leaking.'), cfg.IPOpt('evpn_local_ip', default=None, help='IP address of local EVPN VXLAN (tunnel) endpoint. ' 'This option can be used instead of the evpn_nic one. ' 'If none specified, it will take the one from the ' 'loopback device.'), cfg.StrOpt('evpn_nic', default=None, help='NIC with the IP address to use for the local EVPN ' 'VXLAN (tunnel) endpoint. This option can be used ' 'instead of the evpn_local_ip one. If none specified, ' 'it will take the one from the loopback device.'), cfg.PortOpt('evpn_udp_dstport', default=4789, help='The UDP port used for EVPN VXLAN communication. By ' 'default 4789 is being used.'), cfg.BoolOpt('clear_vrf_routes_on_startup', help='If enabled, all routes are removed from the VRF table' '(specified by bgp_vrf_table_id option) at startup.', default=False), cfg.StrOpt('bgp_nic', default='bgp-nic', help='The name of the interface used within the VRF ' '(bgp_vrf option) to expose the IPs and/or Networks.'), cfg.StrOpt('bgp_vrf', default='bgp-vrf', help='The name of the VRF to be used to expose the IPs ' 'and/or Networks through BGP.'), cfg.IntOpt('bgp_vrf_table_id', default=10, help='The Routing Table ID that the VRF (bgp_vrf option) ' 'should use. If it does not exist, this table will be ' 'created.'), cfg.ListOpt('address_scopes', default=None, help='Allows to filter on the address scope. Only networks' ' with the same address scope on the provider and' ' internal interface are announced.'), cfg.StrOpt('exposing_method', default='underlay', choices=('underlay', 'l2vni', 'vrf', 'dynamic', 'ovn'), help='The exposing mechanism to be used. underlay is the ' 'default and simply expose it on the default/plain ' 'network.' 'With l2vni the l2 is extended over the l3 infrastructure ' 'and it is exposed on a given VNI (Type-2).' 'With vrf the routes are exposed in different VRFs/VNIs ' '(Type-5).' 'With dynamic, a mix between underlay, l2vni and vrf is ' 'used, depending on the information annotated on the ' 'ports. ' 'Finally, with ovn, instead of using kernel networking a ' 'dedicated ovn cluster per node is used for the traffic ' 'redirection'), ] root_helper_opts = [ cfg.StrOpt('root_helper', default='sudo', help=("Root helper application. " "Use 'sudo ovn-bgp-agent-rootwrap " "/etc/ovn-bgp-agent/rootwrap.conf' to use the real " "root filter facility. Change to 'sudo' to skip the " "filtering and just run the command directly.")), cfg.BoolOpt('use_helper_for_ns_read', default=True, help=("Use the root helper when listing the namespaces on a " "system. This may not be required depending on the " "security configuration. If the root helper is " "not required, set this to False for a performance " "improvement.")), # We can't just use root_helper=sudo ovn-bgp-agent-rootwrap-daemon $cfg # because it isn't appropriate for long-lived processes spawned with # create_process. Having a bool use_rootwrap_daemon option precludes # specifying the rootwrap daemon command, which may be necessary for Xen? cfg.StrOpt('root_helper_daemon', help=(""" Root helper daemon application to use when possible. Use 'sudo ovn-bgp-agent-rootwrap-daemon /etc/ovn-bgp-agent/rootwrap.conf' to run rootwrap in "daemon mode" which has been reported to improve performance at scale. For more information on running rootwrap in "daemon mode", see: https://docs.openstack.org/oslo.rootwrap/latest/user/usage.html#daemon-mode """)), ] ovn_opts = [ cfg.StrOpt('ovn_sb_private_key', default='/etc/pki/tls/private/ovn_bgp_agent.key', deprecated_group='DEFAULT', help='The PEM file with private key for SSL connection to ' 'OVN-SB-DB'), cfg.StrOpt('ovn_sb_certificate', default='/etc/pki/tls/certs/ovn_bgp_agent.crt', deprecated_group='DEFAULT', help='The PEM file with certificate that certifies the ' 'private key specified in ovn_sb_private_key'), cfg.StrOpt('ovn_sb_ca_cert', default='/etc/ipa/ca.crt', deprecated_group='DEFAULT', help='The PEM file with CA certificate that OVN should use to' ' verify certificates presented to it by SSL peers'), cfg.StrOpt('ovn_sb_connection', default='', deprecated_group='DEFAULT', help='The connection string for the OVN_Southbound OVSDB.\n' 'Use tcp:IP:PORT for TCP connection.\n' 'Use unix:FILE for unix domain socket connection.'), cfg.StrOpt('ovn_nb_private_key', default='/etc/pki/tls/private/ovn_bgp_agent.key', deprecated_group='DEFAULT', help='The PEM file with private key for SSL connection to ' 'OVN-NB-DB'), cfg.StrOpt('ovn_nb_certificate', default='/etc/pki/tls/certs/ovn_bgp_agent.crt', deprecated_group='DEFAULT', help='The PEM file with certificate that certifies the ' 'private key specified in ovn_nb_private_key'), cfg.StrOpt('ovn_nb_ca_cert', default='/etc/ipa/ca.crt', deprecated_group='DEFAULT', help='The PEM file with CA certificate that OVN should use to' ' verify certificates presented to it by SSL peers'), cfg.StrOpt('ovn_nb_connection', default='', deprecated_group='DEFAULT', help='The connection string for the OVN_Northbound OVSDB.\n' 'Use tcp:IP:PORT for TCP connection.\n' 'Use unix:FILE for unix domain socket connection.'), ] local_ovn_cluster_opts = [ cfg.StrOpt('ovn_nb_connection', default='unix:/var/run/ovn/ovnnb_db.sock', help='The connection string for the OVN_Northbound OVSDB.\n' 'Use tcp:IP:PORT for TCP connection.\n' 'Use unix:FILE for unix domain socket connection.'), cfg.ListOpt('external_nics', default=[], help='List of NICS that the local OVN cluster needs to be ' 'connected to for the external connectivity.'), cfg.ListOpt('peer_ips', default=[], help='List of peer IPs used for redirecting the outgoing ' 'traffic (ECMP supported).'), cfg.ListOpt('provider_networks_pool_prefixes', default=['192.168.0.0/16'], help='List of prefixes for provider networks'), ] CONF = cfg.CONF EXTRA_LOG_LEVEL_DEFAULTS = [ 'oslo.privsep.daemon=INFO' ] logging.register_options(CONF) def register_opts(): CONF.register_opts(agent_opts) CONF.register_opts(root_helper_opts, "agent") CONF.register_opts(ovn_opts, "ovn") CONF.register_opts(local_ovn_cluster_opts, "local_ovn_cluster") def init(args, **kwargs): CONF(args=args, project='bgp-agent', **kwargs) def setup_logging(): logging.set_defaults(default_log_levels=logging.get_default_log_levels() + EXTRA_LOG_LEVEL_DEFAULTS) logging.setup(CONF, 'bgp-agent') LOG.info("Logging enabled!") def get_root_helper(conf): return conf.agent.root_helper def setup_privsep(): priv_context.init(root_helper=shlex.split(get_root_helper(cfg.CONF))) def list_opts(): return [ ("DEFAULT", agent_opts), ("agent", root_helper_opts), ("ovn", ovn_opts), ("local_ovn_cluster", local_ovn_cluster_opts), ] ovn-bgp-agent-2.0.1/ovn_bgp_agent/constants.py000066400000000000000000000072051460327367600213760ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import socket OVN_VIF_PORT_TYPES = ("", "chassisredirect", "virtual") OVN_VIRTUAL_VIF_PORT_TYPE = "virtual" OVN_VM_VIF_PORT_TYPE = "" OVN_PATCH_VIF_PORT_TYPE = "patch" OVN_ROUTER_PORT_TYPE = "router" OVN_CHASSISREDIRECT_VIF_PORT_TYPE = "chassisredirect" OVN_LOCALNET_VIF_PORT_TYPE = "localnet" OVN_SNAT = "snat" OVN_DNAT_AND_SNAT = "dnat_and_snat" OVN_CR_LRP_PORT_TYPE = 'crlrp' OVN_ROUTER_INTERFACE = 'network:router_interface' OVN_CIDRS_EXT_ID_KEY = 'neutron:cidrs' OVN_PORT_NAME_EXT_ID_KEY = 'neutron:port_name' OVN_LS_NAME_EXT_ID_KEY = 'neutron:network_name' OVN_LR_NAME_EXT_ID_KEY = 'neutron:router_name' OVN_DEVICE_ID_EXT_ID_KEY = 'neutron:device_id' OVN_DEVICE_OWNER_EXT_ID_KEY = 'neutron:device_owner' OVN_FIP_EXT_ID_KEY = 'neutron:port_fip' OVN_FIP_NET_EXT_ID_KEY = 'neutron:fip_network_id' LB_VIP_PORT_PREFIX = "ovn-lb-vip-" OVN_LB_PF_NAME_PREFIX = "pf-floatingip-" OVN_LB_VIP_IP_EXT_ID_KEY = 'neutron:vip' OVN_LB_VIP_FIP_EXT_ID_KEY = 'neutron:vip_fip' OVN_LB_VIP_PORT_EXT_ID_KEY = 'neutron:vip_port_id' OVN_LB_LR_REF_EXT_ID_KEY = 'lr_ref' OVN_LB_EXT_ID_ROUTER_KEY = [ OVN_LB_LR_REF_EXT_ID_KEY, OVN_LR_NAME_EXT_ID_KEY ] OVS_RULE_COOKIE = "999" OVS_VRF_RULE_COOKIE = "998" FRR_SOCKET_PATH = "/run/frr/" IP_VERSION_6 = 6 IP_VERSION_4 = 4 ARP_IPV4_PREFIX = "169.254." NDP_IPV6_PREFIX = "fd53:d91e:400:7f17::" IPV4_OCTET_RANGE = 256 BGP_MODE = 'BGP' EVPN_MODE = 'EVPN' OVN_EVPN_VNI_EXT_ID_KEY = 'neutron_bgpvpn:vni' OVN_EVPN_AS_EXT_ID_KEY = 'neutron_bgpvpn:as' OVN_EVPN_VRF_PREFIX = "vrf-" OVN_EVPN_BRIDGE_PREFIX = "br-" OVN_EVPN_VXLAN_PREFIX = "vxlan-" OVN_EVPN_VLAN_PREFIX = "vlan-" OVN_EVPN_LO_PREFIX = "lo-" OVN_EVPN_VETH_VRF_PREFIX = "veth-vrf-" OVN_EVPN_VETH_OVS_PREFIX = "veth-ovs-" OVN_INTEGRATION_BRIDGE = 'br-int' OVN_LRP_PORT_NAME_PREFIX = 'lrp-' OVN_CRLRP_PORT_NAME_PREFIX = 'cr-lrp-' OVS_PATCH_PROVNET_PORT_PREFIX = 'patch-provnet-' LINK_UP = "up" LINK_DOWN = "down" SUBNET_POOL_ADDR_SCOPE4 = "neutron:subnet_pool_addr_scope4" SUBNET_POOL_ADDR_SCOPE6 = "neutron:subnet_pool_addr_scope6" EXPOSE = "expose" WITHDRAW = "withdraw" OVN_REQUESTED_CHASSIS = "requested-chassis" OVN_STATUS_CHASSIS = "hosting-chassis" OVN_HOST_ID_EXT_ID_KEY = "neutron:host_id" OVN_CHASSIS_AT_OPTIONS = "options" OVN_CHASSIS_AT_EXT_IDS = "external-ids" # Exposing method names EXPOSE_METHOD_UNDERLAY = 'underlay' EXPOSE_METHOD_L2VNI = 'l2vni' EXPOSE_METHOD_VRF = 'vrf' EXPOSE_METHOD_OVN = 'ovn' EXPOSE_METHOD_DYNAMIC = 'dynamic' # Advertisement method names for tenant networks ADVERTISEMENT_METHOD_HOST = 'host' ADVERTISEMENT_METHOD_SUBNET = 'subnet' # OVN Cluster related constants OVN_CLUSTER_BRIDGE = 'bgp' OVN_CLUSTER_ROUTER = 'bgp-router' OVN_CLUSTER_ROUTER_INTERNAL_MAC = '40:44:00:00:00:06' # FIXME(ltomasbo): This can be removed once ovsdbapp version is >=1.13.0 POLICY_ACTION_REROUTE = 'reroute' POLICY_ACTION_TYPES = (POLICY_ACTION_REROUTE) LR_POLICY_PRIORITY_MAX = 32767 ROUTE_DISCARD = 'discard' # Family constants AF_INET = socket.AF_INET AF_INET6 = socket.AF_INET6 # Path to file containing routing tables ROUTING_TABLES_FILE = '/etc/iproute2/rt_tables' ROUTING_TABLE_MIN = 1 ROUTING_TABLE_MAX = 252 ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/000077500000000000000000000000001460327367600204625ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/__init__.py000066400000000000000000000000001460327367600225610ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/driver_api.py000066400000000000000000000031261460327367600231620ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc from stevedore import driver as stevedore_driver class AgentDriverBase(object, metaclass=abc.ABCMeta): """Base class for agent drivers. """ @classmethod def get_instance(cls, specific_driver): agent_driver = stevedore_driver.DriverManager( namespace='ovn_bgp_agent.drivers', name=specific_driver, invoke_on_load=True ).driver return agent_driver @abc.abstractmethod def expose_ip(self, ip_address): raise NotImplementedError() @abc.abstractmethod def withdraw_ip(self, ip_address): raise NotImplementedError() @abc.abstractmethod def expose_remote_ip(self, ip_address): raise NotImplementedError() @abc.abstractmethod def withdraw_remote_ip(self, ip_address): raise NotImplementedError() @abc.abstractmethod def expose_subnet(self, subnet): raise NotImplementedError() @abc.abstractmethod def withdraw_subnet(self, subnet): raise NotImplementedError() ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/000077500000000000000000000000001460327367600224515ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/__init__.py000066400000000000000000000000001460327367600245500ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py000066400000000000000000001273751460327367600265260ustar00rootroot00000000000000# Copyright 2023 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import collections import ipaddress import threading from oslo_concurrency import lockutils from oslo_config import cfg from oslo_log import log as logging from ovn_bgp_agent import constants from ovn_bgp_agent.drivers import driver_api from ovn_bgp_agent.drivers.openstack.utils import bgp as bgp_utils from ovn_bgp_agent.drivers.openstack.utils import driver_utils 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 wire as wire_utils from ovn_bgp_agent.drivers.openstack.watchers import nb_bgp_watcher as watcher from ovn_bgp_agent import exceptions from ovn_bgp_agent.utils import linux_net CONF = cfg.CONF LOG = logging.getLogger(__name__) # LOG.setLevel(logging.DEBUG) # logging.basicConfig(level=logging.DEBUG) OVN_TABLES = ['Logical_Switch_Port', 'NAT', 'Logical_Switch', 'Logical_Router', 'Logical_Router_Port', 'Load_Balancer'] LOCAL_CLUSTER_OVN_TABLES = ['Logical_Switch', 'Logical_Switch_Port', 'Logical_Router', 'Logical_Router_Port', 'Logical_Router_Policy', 'Logical_Router_Static_Route', 'Gateway_Chassis', 'Static_MAC_Binding'] class NBOVNBGPDriver(driver_api.AgentDriverBase): def __init__(self): self.allowed_address_scopes = set(CONF.address_scopes or []) self._init_vars() self._nb_idl = None self._local_nb_idl = None self._post_start_event = threading.Event() @property def _expose_tenant_networks(self): return (CONF.expose_tenant_networks or CONF.expose_ipv6_gua_tenant_networks) @property def nb_idl(self) -> ovn.OvsdbNbOvnIdl: if not self._nb_idl: self._post_start_event.wait() return self._nb_idl @property def local_nb_idl(self): if not self._local_nb_idl: self._post_start_event.wait() return self._local_nb_idl @nb_idl.setter def nb_idl(self, val): self._nb_idl = val @local_nb_idl.setter def local_nb_idl(self, val): self._local_nb_idl = val def _init_vars(self): self.ovn_bridge_mappings = {} # {'public': 'br-ex'} self.ovs_flows = {} self.ovn_routing_tables = {} # {'br-ex': 200} # {'br-ex': [route1, route2]} self.ovn_routing_tables_routes = collections.defaultdict(list) self.ovn_local_cr_lrps = {} self.ovn_local_lrps = {} # {'ls_name': ['ip': {'bridge_device': X, 'bridge_vlan': Y}]} self._exposed_ips = {} self._ovs_flows = collections.defaultdict() self.ovn_provider_ls = {} # dict instead of list to speed up look ups self.ovn_tenant_ls = {} # {'ls_name': True} def start(self): self.ovs_idl = ovs.OvsIdl() self.ovs_idl.start(CONF.ovsdb_connection) self.chassis = self.ovs_idl.get_own_chassis_name() self.chassis_id = self.ovs_idl.get_own_chassis_id() # NOTE(ltomasbo): remote should point to NB DB port instead of SB DB, # so changing 6642 by 6641 self.ovn_remote = self.ovs_idl.get_ovn_remote(nb=True) LOG.info("Loaded chassis %s.", self.chassis) LOG.info("Starting VRF configuration for advertising routes") # Base BGP configuration bgp_utils.ensure_base_bgp_configuration() # Clear vrf routing table if CONF.clear_vrf_routes_on_startup: linux_net.delete_routes_from_table(CONF.bgp_vrf_table_id) LOG.info("VRF configuration for advertising routes completed") if self._expose_tenant_networks and self.allowed_address_scopes: LOG.info("Configured allowed address scopes: %s", ", ".join(self.allowed_address_scopes)) self._post_start_event.clear() events = self._get_events() self.nb_idl = ovn.OvnNbIdl( self.ovn_remote, tables=OVN_TABLES, events=events).start() # if local OVN cluster, gets an idl for it if CONF.exposing_method == constants.EXPOSE_METHOD_OVN: self.local_nb_idl = ovn.OvnNbIdl( CONF.local_ovn_cluster.ovn_nb_connection, tables=LOCAL_CLUSTER_OVN_TABLES, events=[], leader_only=True).start() # Now IDL connections can be safely used self._post_start_event.set() def _get_events(self): events = {watcher.LogicalSwitchPortProviderCreateEvent(self), watcher.LogicalSwitchPortProviderDeleteEvent(self), watcher.LogicalSwitchPortFIPCreateEvent(self), watcher.LogicalSwitchPortFIPDeleteEvent(self), watcher.LocalnetCreateDeleteEvent(self), watcher.OVNLBCreateEvent(self), watcher.OVNLBDeleteEvent(self), watcher.OVNPFCreateEvent(self), watcher.OVNPFDeleteEvent(self)} if self._expose_tenant_networks: events.update({watcher.ChassisRedirectCreateEvent(self), watcher.ChassisRedirectDeleteEvent(self), watcher.LogicalSwitchPortSubnetAttachEvent(self), watcher.LogicalSwitchPortSubnetDetachEvent(self)}) if CONF.advertisement_method_tenant_networks == 'host': events.update({ watcher.LogicalSwitchPortTenantCreateEvent(self), watcher.LogicalSwitchPortTenantDeleteEvent(self) }) return events @lockutils.synchronized('nbbgp') def frr_sync(self): LOG.debug("Ensuring VRF configuration for advertising routes") # Base BGP configuration bgp_utils.ensure_base_bgp_configuration() @lockutils.synchronized('nbbgp') def sync(self): self._init_vars() LOG.debug("Configuring default wiring for each provider network") # Apply base configuration for each bridge self.ovn_bridge_mappings, self.ovs_flows = ( wire_utils.ensure_base_wiring_config( self.nb_idl, self.ovs_idl, ovn_idl=self.local_nb_idl, routing_tables=self.ovn_routing_tables)) LOG.debug("Syncing current routes.") # add missing routes/ips for OVN router gateway ports ports = self.nb_idl.get_active_cr_lrp_on_chassis(self.chassis_id) for port in ports: self._ensure_crlrp_exposed(port) # add missing routes/ips for subnets connected to local gateway ports ports = self.nb_idl.get_active_local_lrps( self.ovn_local_cr_lrps.keys()) for port in ports: ips = port.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "").split() subnet_info = { 'associated_router': port.external_ids.get( constants.OVN_DEVICE_ID_EXT_ID_KEY), 'network': port.external_ids.get( constants.OVN_LS_NAME_EXT_ID_KEY), 'address_scopes': driver_utils.get_addr_scopes(port)} self._expose_subnet(ips, subnet_info) # add missing routes/ips for IPs on provider network ports = self.nb_idl.get_active_lsp_on_chassis(self.chassis) for port in ports: if port.type not in [constants.OVN_VM_VIF_PORT_TYPE, constants.OVN_VIRTUAL_VIF_PORT_TYPE]: continue self._ensure_lsp_exposed(port) # add missing routes/ips for OVN loadbalancers self._expose_lbs(self.ovn_local_cr_lrps.keys()) # remove extra wiring leftovers wire_utils.cleanup_wiring(self.nb_idl, self.ovn_bridge_mappings, self.ovs_flows, self._exposed_ips, self.ovn_routing_tables, self.ovn_routing_tables_routes) def _ensure_lsp_exposed(self, port): port_fip = port.external_ids.get(constants.OVN_FIP_EXT_ID_KEY) if port_fip: external_ip, external_mac, ls_name = ( self.get_port_external_ip_and_ls(port.name)) if not external_ip or not ls_name: return return self._expose_fip(external_ip, external_mac, ls_name, port) logical_switch = port.external_ids.get( constants.OVN_LS_NAME_EXT_ID_KEY) if not self.is_ls_provider(logical_switch): return _, bridge_device, bridge_vlan = self._get_provider_ls_info( logical_switch) ips = port.addresses[0].strip().split(' ')[1:] mac = port.addresses[0].strip().split(' ')[0] self._expose_ip(ips, mac, logical_switch, bridge_device, bridge_vlan, port.type, port.external_ids.get( constants.OVN_CIDRS_EXT_ID_KEY, "").split()) def _ensure_crlrp_exposed(self, port): if not port.networks: return logical_switch = port.external_ids.get( constants.OVN_LS_NAME_EXT_ID_KEY) if not self.is_ls_provider(logical_switch): return _, bridge_device, bridge_vlan = self._get_provider_ls_info( logical_switch) ips = [net.split("/")[0] for net in port.networks] router = port.external_ids.get(constants.OVN_LR_NAME_EXT_ID_KEY) self._expose_ip(ips, port.mac, logical_switch, bridge_device, bridge_vlan, constants.OVN_CR_LRP_PORT_TYPE, port.networks, router=router) def _expose_provider_port(self, port_ips, mac, logical_switch, bridge_device, bridge_vlan, localnet, proxy_cidrs=None): if proxy_cidrs is None: proxy_cidrs = [] # Connect to OVN try: if wire_utils.wire_provider_port( self.ovn_routing_tables_routes, self.ovs_flows, port_ips, bridge_device, bridge_vlan, localnet, self.ovn_routing_tables, proxy_cidrs, mac=mac, ovn_idl=self.local_nb_idl): # Expose the IP now that it is connected bgp_utils.announce_ips(port_ips) for ip in port_ips: self._exposed_ips.setdefault(logical_switch, {}).update( {ip: {'bridge_device': bridge_device, 'bridge_vlan': bridge_vlan}}) except Exception as e: LOG.exception("Unexpected exception while wiring provider port: " "%s", e) return False return True def _withdraw_provider_port(self, port_ips, logical_switch, bridge_device, bridge_vlan, proxy_cidrs=None): if proxy_cidrs is None: proxy_cidrs = [] # Withdraw IP before disconnecting it bgp_utils.withdraw_ips(port_ips) # Disconnect IP from OVN try: wire_utils.unwire_provider_port( self.ovn_routing_tables_routes, port_ips, bridge_device, bridge_vlan, self.ovn_routing_tables, proxy_cidrs, ovn_idl=self.local_nb_idl) except Exception as e: LOG.exception("Unexpected exception while unwiring provider port: " "%s", e) for ip in port_ips: if self._exposed_ips.get(logical_switch, {}).get(ip): self._exposed_ips[logical_switch].pop(ip) def _get_bridge_for_localnet_port(self, localnet): bridge_device = None bridge_vlan = None network_name = localnet.options.get('network_name') if network_name: bridge_device = self.ovn_bridge_mappings.get(network_name) if localnet.tag: bridge_vlan = localnet.tag[0] return bridge_device, bridge_vlan def _expose_lbs(self, router_list): lbs = self.nb_idl.get_active_local_lbs(router_list) for lb in lbs: if driver_utils.is_pf_lb(lb): self._expose_ovn_pf_lb_fip(lb) else: self._expose_ovn_lb_vip(lb) # if vip-fip expose fip too if lb.external_ids.get(constants.OVN_LB_VIP_FIP_EXT_ID_KEY): self._expose_ovn_lb_fip(lb) def _withdraw_lbs(self, router_list): lbs = self.nb_idl.get_active_local_lbs(router_list) for lb in lbs: if driver_utils.is_pf_lb(lb): self._withdraw_ovn_pf_lb_fip(lb) else: self._withdraw_ovn_lb_vip(lb) # if vip-fip withdraw fip too if lb.external_ids.get(constants.OVN_LB_VIP_FIP_EXT_ID_KEY): self._withdraw_ovn_lb_fip(lb) def is_ls_provider(self, logical_switch): '''Check if given logical switch is a provider network on this host It will also validate that the provider network actually has been exposed by the driver. ''' if logical_switch is None: self.ovn_tenant_ls[logical_switch] = True # just for caching return False # Check if the ls has already been identified as a tenant network if self.ovn_tenant_ls.get(logical_switch, None) is True: return False # Check the bridge device from provider network _, bridge_device, _ = self._get_provider_ls_info(logical_switch) # Check if the bridge device has been exposed by the wiring methods if bridge_device not in self.ovn_bridge_mappings.values(): return False return bridge_device is not None def is_ip_exposed(self, logical_switch, ips): '''Check if the ip(s) from given logical_switch is exported. So basically, check if the ips are listed in self._exposed_ips. If it is in there, it should be exposed by ovn-bgp-agent. This helps a lot in evaluating events. ''' # Ip may be a list if not isinstance(ips, (list, tuple, set)): ips = [ips] for ip in ips: if ip in self._exposed_ips.get(logical_switch, {}): return True return False @lockutils.synchronized('nbbgp') def expose_ip(self, ips, ips_info): '''Advertice BGP route by adding IP to device. This methods ensures BGP advertises the IP of the VM in the provider network. It relies on Zebra, which creates and advertises a route when an IP is added to a local interface. This method assumes a device named self.ovn_device exists (inside a VRF), and adds the IP of: - VM IP on the provider network ''' logical_switch = ips_info.get('logical_switch') if not self.is_ls_provider(logical_switch): return False _, bridge_device, bridge_vlan = self._get_provider_ls_info( logical_switch) mac = ips_info.get('mac') return self._expose_ip(ips, mac, logical_switch, bridge_device, bridge_vlan, port_type=ips_info['type'], cidrs=ips_info['cidrs'], router=ips_info.get('router')) def _expose_ip(self, ips, mac, logical_switch, bridge_device, bridge_vlan, port_type, cidrs, router=None): LOG.debug("Adding BGP route for logical port with ip %s", ips) localnet = self.ovn_provider_ls[logical_switch]['localnet'] if not self._expose_provider_port(ips, mac, logical_switch, bridge_device, bridge_vlan, localnet, cidrs): return [] if router and port_type == constants.OVN_CR_LRP_PORT_TYPE: # Store information about local CR-LRPs that will later be used # to expose networks self.ovn_local_cr_lrps[router] = { 'bridge_device': bridge_device, 'bridge_vlan': bridge_vlan, 'provider_switch': logical_switch, 'ips': ips, } # Expose associated subnets ports = self.nb_idl.get_active_local_lrps([router]) for port in ports: ips = port.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "").split() subnet_info = { 'associated_router': port.external_ids.get( constants.OVN_DEVICE_ID_EXT_ID_KEY), 'network': port.external_ids.get( constants.OVN_LS_NAME_EXT_ID_KEY), 'address_scopes': driver_utils.get_addr_scopes(port)} self._expose_subnet(ips, subnet_info) # add missing routes/ips for OVN loadbalancers self._expose_lbs([router]) LOG.debug("Added BGP route for logical port with ip %s", ips) return ips @lockutils.synchronized('nbbgp') def withdraw_ip(self, ips, ips_info): '''Withdraw BGP route by removing IP from device. This methods ensures BGP withdraw an advertised IP of a VM, either in the provider network. It relies on Zebra, which withdraws the advertisement as soon as the IP is deleted from the local interface. This method assumes a device named self.ovn_decice exists (inside a VRF), and removes the IP of: - VM IP on the provider network ''' logical_switch = ips_info.get('logical_switch') if not logical_switch: return _, bridge_device, bridge_vlan = self._get_ls_localnet_info( logical_switch) if not bridge_device: # This means it is not a provider network return proxy_cidr = [] if ips_info['cidrs']: if not (self.nb_idl.ls_has_virtual_ports(logical_switch) or self.nb_idl.get_active_lsp_on_chassis(self.chassis)): for n_cidr in ips_info['cidrs']: if (linux_net.get_ip_version(n_cidr) == constants.IP_VERSION_6): proxy_cidr.append(n_cidr) LOG.debug("Deleting BGP route for logical port with ip %s", ips) self._withdraw_provider_port(ips, logical_switch, bridge_device, bridge_vlan, proxy_cidr) if ips_info.get('router'): # It is a Logical Router Port (CR-LRP) # Withdraw associated subnets ports = self.nb_idl.get_active_local_lrps([ips_info['router']]) for port in ports: ips = port.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "").split() subnet_info = { 'associated_router': port.external_ids.get( constants.OVN_DEVICE_ID_EXT_ID_KEY), 'network': port.external_ids.get( constants.OVN_LS_NAME_EXT_ID_KEY), 'address_scopes': driver_utils.get_addr_scopes(port)} self._withdraw_subnet(ips, subnet_info) # withdraw routes/ips for OVN loadbalancers self._withdraw_lbs([ips_info['router']]) try: del self.ovn_local_cr_lrps[ips_info['router']] except KeyError: LOG.debug("Gateway port for router %s already cleanup.", ips_info['router']) LOG.debug("Deleted BGP route for logical port with ip %s", ips) def _get_provider_ls_info(self, logical_switch): '''Helper method for _get_ls_localnet_info It returns the information from cached self.ovn_provider_ls dictionary or calls the method and populates the dictionary for given logical_switch. It returns a tuple of localnet, bridge_device and bridge_vlan for compatibilty with _get_ls_localnet_info ''' if logical_switch is None: return None, None, None if logical_switch not in self.ovn_provider_ls: localnet, bridge_dev, bridge_vlan = self._get_ls_localnet_info( logical_switch) self.ovn_provider_ls[logical_switch] = { 'bridge_device': bridge_dev, 'bridge_vlan': bridge_vlan, 'localnet': localnet } ls = self.ovn_provider_ls[logical_switch] return ls['localnet'], ls['bridge_device'], ls['bridge_vlan'] def _get_ls_localnet_info(self, logical_switch): localnet_ports = self.nb_idl.ls_get_localnet_ports( logical_switch, if_exists=True).execute(check_error=True) if not localnet_ports: # means it is not a provider network, so no need to expose the IP return None, None, None bridge_device, bridge_vlan = self._get_bridge_for_localnet_port( localnet_ports[0]) # NOTE: assuming only one localnet per LS exists return localnet_ports[0].name, bridge_device, bridge_vlan def get_port_external_ip_and_ls(self, port): nat_entry = self.nb_idl.get_nat_by_logical_port(port) if not nat_entry: return None, None, None net_id = nat_entry.external_ids.get(constants.OVN_FIP_NET_EXT_ID_KEY) if not net_id: return nat_entry.external_ip, nat_entry.external_mac, None else: ls_name = "neutron-{}".format(net_id) return nat_entry.external_ip, nat_entry.external_mac, ls_name @lockutils.synchronized('nbbgp') def expose_fip(self, ip, mac, logical_switch, row): '''Advertice BGP route by adding IP to device. This methods ensures BGP advertises the FIP associated to a VM in a tenant networks. It relies on Zebra, which creates and advertises a route when an IP is added to a local interface. This method assumes a device named self.ovn_device exists (inside a VRF), and adds the IP of: - VM FIP ''' return self._expose_fip(ip, mac, logical_switch, row) def _expose_fip(self, ip, mac, logical_switch, row): if not self.is_ls_provider(logical_switch): return False localnet, bridge_device, bridge_vlan = self._get_provider_ls_info( logical_switch) tenant_logical_switch = row.external_ids.get( constants.OVN_LS_NAME_EXT_ID_KEY) if not tenant_logical_switch: return self.ovn_tenant_ls[tenant_logical_switch] = True LOG.debug("Adding BGP route for FIP with ip %s", ip) if not self._expose_provider_port([ip], mac, tenant_logical_switch, bridge_device, bridge_vlan, localnet): return False LOG.debug("Added BGP route for FIP with ip %s", ip) return True @lockutils.synchronized('nbbgp') def withdraw_fip(self, ip, row): '''Withdraw BGP route by removing IP from device. This methods ensures BGP withdraw an advertised the FIP associated to a VM in a tenant networks. It relies on Zebra, which withdraws the advertisement as soon as the IP is deleted from the local interface. This method assumes a device named self.ovn_decice exists (inside a VRF), and removes the IP of: - VM FIP ''' tenant_logical_switch = row.external_ids.get( constants.OVN_LS_NAME_EXT_ID_KEY) if not tenant_logical_switch: return fip_info = self._exposed_ips.get(tenant_logical_switch, {}).get(ip) if not fip_info: # No information to withdraw the FIP return bridge_device = fip_info['bridge_device'] bridge_vlan = fip_info['bridge_vlan'] LOG.debug("Deleting BGP route for FIP with ip %s", ip) self._withdraw_provider_port([ip], tenant_logical_switch, bridge_device, bridge_vlan) LOG.debug("Deleted BGP route for FIP with ip %s", ip) @lockutils.synchronized('nbbgp') def expose_remote_ip(self, ips, ips_info): self._expose_remote_ip(ips, ips_info) @lockutils.synchronized('nbbgp') def withdraw_remote_ip(self, ips, ips_info): self._withdraw_remote_ip(ips, ips_info) def _expose_remote_ip(self, ips, ips_info): if (CONF.advertisement_method_tenant_networks == constants.ADVERTISEMENT_METHOD_SUBNET): # Ip should already be exported via cr-lrp subnet announcement. return ips_to_expose = ips if not CONF.expose_tenant_networks: # This means CONF.expose_ipv6_gua_tenant_networks is enabled gua_ips = [ip for ip in ips if driver_utils.is_ipv6_gua(ip)] if not gua_ips: return ips_to_expose = gua_ips LOG.debug("Adding BGP route for tenant IP(s) %s on chassis %s", ips_to_expose, self.chassis) bgp_utils.announce_ips(ips_to_expose) for ip in ips_to_expose: self._exposed_ips.setdefault( ips_info['logical_switch'], {}).update({ip: {}}) LOG.debug("Added BGP route for tenant IP(s) %s on chassis %s", ips_to_expose, self.chassis) def _withdraw_remote_ip(self, ips, ips_info): if (CONF.advertisement_method_tenant_networks == constants.ADVERTISEMENT_METHOD_SUBNET): return ips_to_withdraw = ips if not CONF.expose_tenant_networks: # This means CONF.expose_ipv6_gua_tenant_networks is enabled gua_ips = [ip for ip in ips if driver_utils.is_ipv6_gua(ip)] if not gua_ips: return ips_to_withdraw = gua_ips LOG.debug("Deleting BGP route for tenant IP(s) %s on chassis %s", ips_to_withdraw, self.chassis) bgp_utils.withdraw_ips(ips_to_withdraw) for ip in ips_to_withdraw: if self._exposed_ips.get( 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", ips_to_withdraw, self.chassis) @lockutils.synchronized('nbbgp') def expose_subnet(self, ips, subnet_info): return self._expose_subnet(ips, subnet_info) @lockutils.synchronized('nbbgp') def withdraw_subnet(self, ips, subnet_info): return self._withdraw_subnet(ips, subnet_info) def _expose_subnet(self, ips, subnet_info): gateway_router = subnet_info['associated_router'] if not gateway_router: LOG.debug("Subnet CIDRs %s not exposed as there is no associated " "router", ips) return cr_lrp_info = self.ovn_local_cr_lrps.get(gateway_router) if not cr_lrp_info: LOG.debug("Subnet CIDRs %s not exposed as there is no local " "cr-lrp matching %s", ips, gateway_router) return if CONF.require_snat_disabled_for_tenant_networks: # Check if there is a SNAT entry for this LRP router = self.nb_idl.get_router(gateway_router) ips_without_snat = set(ips) for nat in router.nat: if nat.type == constants.OVN_SNAT: net = ipaddress.ip_network(nat.logical_ip, strict=False) for ip in list(ips_without_snat): if ipaddress.ip_address(ip.split('/')[0]) in net: ips_without_snat.discard(ip) if len(ips_without_snat) == 0: LOG.info('All ips (%s) were removed due to SNAT requirement ' 'when exposing subnet %s for router %s', ips, subnet_info['network'], gateway_router) return if len(set(ips)) != len(ips_without_snat): LOG.info('When exposing subnet %s for router %s, these ips ' 'were removed for SNAT: %s', subnet_info['network'], gateway_router, set(ips) - ips_without_snat) ips = list(ips_without_snat) try: self._expose_router_lsp(ips, subnet_info, cr_lrp_info) except (exceptions.ExposeDeniedForAddressScope, exceptions.WireFailure) as e: LOG.debug("Not exposing subnet CIDR's %s: %s", ips, e) return if (CONF.advertisement_method_tenant_networks == constants.ADVERTISEMENT_METHOD_SUBNET): # Networks have been exposed via self._expose_router_lsp return ports = self.nb_idl.get_active_lsp(subnet_info['network']) for port in ports: # Check if the ip's on this port match the address scope. As the # port can be dual-stack, it could be that v4 is not allowed, but # v6 is allowed, so then only v6 address should be exposed. ips = self._ips_in_address_scope(port.addresses[0].split(' ')[1:], subnet_info['address_scopes']) if not ips: # All ip's have been removed due to address scope requirement continue mac = port.addresses[0].strip().split(' ')[0] ips_info = { 'mac': mac, 'cidrs': port.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "").split(), 'type': port.type, 'logical_switch': port.external_ids.get( constants.OVN_LS_NAME_EXT_ID_KEY) } self._expose_remote_ip(ips, ips_info) def _withdraw_subnet(self, ips, subnet_info): gateway_router = subnet_info['associated_router'] if not gateway_router: LOG.debug("Subnet CIDRs %s not withdrawn as there is no associated" " router", ips) return cr_lrp_info = self.ovn_local_cr_lrps.get(gateway_router) if not cr_lrp_info: # NOTE(ltomasbo) there is a chance the cr-lrp just got moved # to this node but was not yet processed. In that case there # is no need to withdraw the network as it was not exposed here LOG.debug("Subnet CIDRs %s not withdrawn as there is no local " "cr-lrp matching %s", ips, gateway_router) return try: self._withdraw_router_lsp(ips, subnet_info, cr_lrp_info) except (exceptions.ExposeDeniedForAddressScope, exceptions.UnwireFailure) as e: # Log a message, but silently continue, to make sure we have # it all withdrawn LOG.debug("Withdraw router lsp failure for CIDR's %s: %s", ips, e) if (CONF.advertisement_method_tenant_networks == constants.ADVERTISEMENT_METHOD_SUBNET): # Expose the routes per prefix, rather than per port. return ports = self.nb_idl.get_active_lsp(subnet_info['network']) for port in ports: ips = port.addresses[0].split(' ')[1:] mac = port.addresses[0].strip().split(' ')[0] ips_info = { 'mac': mac, 'cidrs': port.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "").split(), 'type': port.type, 'logical_switch': port.external_ids.get( constants.OVN_LS_NAME_EXT_ID_KEY) } self._withdraw_remote_ip(ips, ips_info) def _expose_router_lsp(self, ips, subnet_info, cr_lrp_info): '''Expose the tenant router ip address (cidr) for given router Will raise WireException if wire_lrp_port raises an exception or if it returns False Will raise ExposeDeniedForAddressScope if configured address scopes do not match the ones in configuration (if configured) (and execution should stop) ''' if not self._expose_tenant_networks: return True if (CONF.advertisement_method_tenant_networks == constants.ADVERTISEMENT_METHOD_SUBNET): # Fix ips to be the network address, instead of the lrp address # so the cleanup will not remove them, since they match what's # in the kernel ips = driver_utils.get_prefixes_from_ips(ips) ips_to_process = [] for ip in ips: if not CONF.expose_tenant_networks: # This means CONF.expose_ipv6_gua_tenant_networks is enabled if not driver_utils.is_ipv6_gua(ip): continue ips_to_process.append(ip) if not ips_to_process: # Silently return, since there are no ip's left to process and # the address_scope has nothing to do with it. return True ips = self._ips_in_address_scope(ips_to_process, subnet_info['address_scopes']) if not ips: # All ip's failed address scope test, so stop processing this lsp raise exceptions.ExposeDeniedForAddressScope( addresses=','.join(ips_to_process), address_scopes=subnet_info['address_scopes'], configured_scopes=self.allowed_address_scopes, ) for ip in ips: try: if wire_utils.wire_lrp_port( self.ovn_routing_tables_routes, ip, cr_lrp_info.get('bridge_device'), cr_lrp_info.get('bridge_vlan'), self.ovn_routing_tables, cr_lrp_info.get('ips')): logical_switch = cr_lrp_info['provider_switch'] self._exposed_ips.setdefault(logical_switch, {}).update( {ip: { 'bridge_device': cr_lrp_info.get('bridge_device'), 'bridge_vlan': cr_lrp_info.get('bridge_vlan')}}) self.ovn_local_lrps.setdefault( subnet_info['network'], []).append(ip) else: error_msg = ("Something happen while exposing the subnet" "and they have not been properly exposed") raise exceptions.WireFailure(cidr=ip, message=error_msg) except Exception as e: raise exceptions.WireFailure(cidr=ip, message=str(e)) from e return True def _withdraw_router_lsp(self, ips, subnet_info, cr_lrp_info): '''Withdraw the tenant router ip address (cidr) for given router Will raise UnwireException if wire_lrp_port raises an exception or if it returns False Will raise ExposeDeniedForAddressScope if configured address scopes do not match the ones in configuration (if configured) (and execution should stop) ''' if not self._expose_tenant_networks: return True if (CONF.advertisement_method_tenant_networks == constants.ADVERTISEMENT_METHOD_SUBNET): # Fix ips to be the network address, instead of the lrp address # so the cleanup will not remove them, since they match what's # in the kernel ips = driver_utils.get_prefixes_from_ips(ips) ips_to_process = [] for ip in ips: if (not CONF.expose_tenant_networks and not driver_utils.is_ipv6_gua(ip)): # This means CONF.expose_ipv6_gua_tenant_networks is enabled continue ips_to_process.append(ip) if not ips_to_process: # Silently return, since there are no ip's left to process and # the address_scope has nothing to do with it. return True ips = self._ips_in_address_scope(ips_to_process, subnet_info['address_scopes']) if not ips: # All ip's failed address scope test, so stop processing this lsp raise exceptions.ExposeDeniedForAddressScope( addresses=','.join(ips_to_process), address_scopes=subnet_info['address_scopes'], configured_scopes=self.allowed_address_scopes, ) for ip in ips: try: if wire_utils.unwire_lrp_port( self.ovn_routing_tables_routes, ip, cr_lrp_info.get('bridge_device'), cr_lrp_info.get('bridge_vlan'), self.ovn_routing_tables, cr_lrp_info.get('ips')): logical_switch = cr_lrp_info['provider_switch'] self._exposed_ips.get(logical_switch, {}).pop(ip, None) else: error_msg = ("Something happened while withdrawing subnet" "and they have not been properly removed") raise exceptions.UnwireFailure(cidr=ip, message=error_msg) except Exception as e: raise exceptions.UnwireFailure(cidr=ip, message=str(e)) from e try: del self.ovn_local_lrps[subnet_info['network']] except KeyError: # Router port for subnet already cleanup pass return True @lockutils.synchronized('nbbgp') def expose_ovn_lb_vip(self, lb): self._expose_ovn_lb_vip(lb) def _expose_ovn_lb_vip(self, lb): vip_port = lb.external_ids.get(constants.OVN_LB_VIP_PORT_EXT_ID_KEY) vip_ip = lb.external_ids.get(constants.OVN_LB_VIP_IP_EXT_ID_KEY) vip_router = lb.external_ids[ constants.OVN_LB_LR_REF_EXT_ID_KEY].replace('neutron-', "", 1) vip_lsp = self.nb_idl.lsp_get(vip_port).execute(check_error=True) if not vip_lsp: LOG.debug("Something went wrong, VIP port %s not found", vip_port) return vip_net = vip_lsp.external_ids.get(constants.OVN_LS_NAME_EXT_ID_KEY) if vip_net in self.ovn_local_lrps.keys(): # It is a VIP on a tenant network # NOTE: the LB is exposed through the cr-lrp, so we add the # vip_router instead of the logical switch ips_info = {'logical_switch': vip_router} self._expose_remote_ip([vip_ip], ips_info) else: # It is a VIP on a provider network localnet, bridge_device, bridge_vlan = self._get_provider_ls_info( vip_net) self._expose_provider_port([vip_ip], None, vip_net, bridge_device, bridge_vlan, localnet) @lockutils.synchronized('nbbgp') def withdraw_ovn_lb_vip(self, lb): self._withdraw_ovn_lb_vip(lb) def _withdraw_ovn_lb_vip(self, lb): vip_ip = lb.external_ids.get(constants.OVN_LB_VIP_IP_EXT_ID_KEY) vip_router = lb.external_ids[ constants.OVN_LB_LR_REF_EXT_ID_KEY].replace('neutron-', "", 1) cr_lrp_info = self.ovn_local_cr_lrps.get(vip_router) if not cr_lrp_info: return provider_ls = cr_lrp_info['provider_switch'] if self._exposed_ips.get(provider_ls, {}).get(vip_ip): # VIP is on provider network self._withdraw_provider_port([vip_ip], cr_lrp_info['provider_switch'], cr_lrp_info['bridge_device'], cr_lrp_info['bridge_vlan']) else: # VIP is on tenant network ips_info = {'logical_switch': vip_router} self._withdraw_remote_ip([vip_ip], ips_info) @lockutils.synchronized('nbbgp') def expose_ovn_lb_fip(self, lb): self._expose_ovn_lb_fip(lb) def _expose_ovn_lb_fip(self, lb): vip_port = lb.external_ids.get(constants.OVN_LB_VIP_PORT_EXT_ID_KEY) vip_lsp = self.nb_idl.lsp_get(vip_port).execute(check_error=True) if not vip_lsp: LOG.debug("Something went wrong, VIP port %s not found", vip_port) return external_ip, external_mac, ls_name = ( self.get_port_external_ip_and_ls(vip_lsp.name)) if not external_ip or not ls_name: LOG.debug("Something went wrong, no NAT entry for the VIP %s", vip_port) return self._expose_fip(external_ip, external_mac, ls_name, vip_lsp) def _get_parameters_from_lb(self, lb, include_mac_and_localnet=False): for fipport in lb.vips.keys(): fip, port = fipport.split(':') break else: return router = lb.external_ids.get( constants.OVN_LR_NAME_EXT_ID_KEY, '').replace('neutron-', "", 1) if not router: return cr_lrp_info = self.ovn_local_cr_lrps.get(router) if not cr_lrp_info: return net, bridge_device, bridge_vlan = self._get_ls_localnet_info( cr_lrp_info['provider_switch']) kwargs = { 'port_ips': [fip], 'logical_switch': cr_lrp_info['provider_switch'], 'bridge_device': bridge_device, 'bridge_vlan': bridge_vlan} if include_mac_and_localnet: kwargs['mac'] = None kwargs['localnet'] = net return kwargs @lockutils.synchronized('nbbgp') def expose_ovn_pf_lb_fip(self, lb): self._expose_ovn_pf_lb_fip(lb) @lockutils.synchronized('nbbgp') def withdraw_ovn_pf_lb_fip(self, lb): self._withdraw_ovn_pf_lb_fip(lb) def _withdraw_ovn_pf_lb_fip(self, lb): kwargs = self._get_parameters_from_lb(lb) self._withdraw_provider_port(**kwargs) if kwargs else None def _expose_ovn_pf_lb_fip(self, lb): kwargs = self._get_parameters_from_lb(lb, True) self._expose_provider_port(**kwargs) if kwargs else None @lockutils.synchronized('nbbgp') def withdraw_ovn_lb_fip(self, lb): self._withdraw_ovn_lb_fip(lb) def _withdraw_ovn_lb_fip(self, lb): vip_fip = lb.external_ids.get(constants.OVN_LB_VIP_FIP_EXT_ID_KEY) # OVN loadbalancers ARPs are replied by router port vip_router = lb.external_ids.get( constants.OVN_LB_LR_REF_EXT_ID_KEY, "").replace('neutron-', "", 1) if not vip_router: return cr_lrp_info = self.ovn_local_cr_lrps.get(vip_router) if not cr_lrp_info: return self._withdraw_provider_port([vip_fip], cr_lrp_info['provider_switch'], cr_lrp_info['bridge_device'], cr_lrp_info['bridge_vlan']) def _ips_in_address_scope(self, ips, address_scopes): return [ip for ip in ips if self._address_scope_allowed(ip, address_scopes)] def _address_scope_allowed(self, ip, address_scopes): if not self.allowed_address_scopes: # No address scopes to filter on => announce everything return True # if we should filter on address scopes and this port has no # address scopes set we do not need to expose it if not any(address_scopes.values()): return False # if address scope does not match, no need to expose it ip_version = linux_net.get_ip_version(ip) return address_scopes[ip_version] in self.allowed_address_scopes ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py000066400000000000000000001460651460327367600260440ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import collections import ipaddress import threading from oslo_concurrency import lockutils from oslo_config import cfg from oslo_log import log as logging from ovn_bgp_agent import constants from ovn_bgp_agent.drivers import driver_api from ovn_bgp_agent.drivers.openstack.utils import bgp as bgp_utils from ovn_bgp_agent.drivers.openstack.utils import driver_utils 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 wire as wire_utils from ovn_bgp_agent.drivers.openstack.watchers import bgp_watcher as watcher from ovn_bgp_agent import exceptions as agent_exc from ovn_bgp_agent.utils import helpers from ovn_bgp_agent.utils import linux_net CONF = cfg.CONF LOG = logging.getLogger(__name__) # LOG.setLevel(logging.DEBUG) # logging.basicConfig(level=logging.DEBUG) OVN_TABLES = ["Port_Binding", "Chassis", "Datapath_Binding", "Load_Balancer", "Chassis_Private", "Logical_DP_Group"] class OVNBGPDriver(driver_api.AgentDriverBase): def __init__(self): self._expose_tenant_networks = (CONF.expose_tenant_networks or CONF.expose_ipv6_gua_tenant_networks) self.allowed_address_scopes = set(CONF.address_scopes or []) self.ovn_routing_tables = {} # {'br-ex': 200} self.ovn_bridge_mappings = {} # {'public': 'br-ex'} self.ovs_flows = {} self.ovn_local_cr_lrps = {} self.ovn_local_lrps = {} # {'br-ex': [route1, route2]} self.ovn_routing_tables_routes = collections.defaultdict() # {ovn_lb: {'ips': [VIP1, VIP2], 'gateway_port': cr-lrpX} self.provider_ovn_lbs = collections.defaultdict() # {datapath: localnet_port_name} self.ovn_provider_datapath = {} self._sb_idl = None self._post_fork_event = threading.Event() @property def sb_idl(self): if not self._sb_idl: self._post_fork_event.wait() return self._sb_idl @sb_idl.setter def sb_idl(self, val): self._sb_idl = val def start(self): self.ovs_idl = ovs.OvsIdl() self.ovs_idl.start(CONF.ovsdb_connection) self.chassis = self.ovs_idl.get_own_chassis_id() self.ovn_remote = self.ovs_idl.get_ovn_remote() LOG.info("Loaded chassis %s.", self.chassis) LOG.info("Starting VRF configuration for advertising routes") # Base BGP configuration bgp_utils.ensure_base_bgp_configuration() # Clear vrf routing table if CONF.clear_vrf_routes_on_startup: linux_net.delete_routes_from_table(CONF.bgp_vrf_table_id) LOG.info("VRF configuration for advertising routes completed") if self._expose_tenant_networks and self.allowed_address_scopes: LOG.info("Configured allowed address scopes: %s", ", ".join(self.allowed_address_scopes)) self._post_fork_event.clear() events = self._get_events() self.sb_idl = ovn.OvnSbIdl( self.ovn_remote, chassis=self.chassis, tables=OVN_TABLES, events=events).start() # Now IDL connections can be safely used self._post_fork_event.set() def _get_events(self): events = {watcher.PortBindingChassisCreatedEvent(self), watcher.PortBindingChassisDeletedEvent(self), watcher.FIPSetEvent(self), watcher.FIPUnsetEvent(self), watcher.OVNLBMemberCreateEvent(self), watcher.OVNLBMemberDeleteEvent(self), watcher.ChassisCreateEvent(self), watcher.ChassisPrivateCreateEvent(self), watcher.LocalnetCreateDeleteEvent(self)} if self._expose_tenant_networks: events.update({watcher.SubnetRouterAttachedEvent(self), watcher.SubnetRouterDetachedEvent(self), watcher.TenantPortCreatedEvent(self), watcher.TenantPortDeletedEvent(self), watcher.OVNLBVIPPortEvent(self)}) return events @lockutils.synchronized('bgp') def frr_sync(self): LOG.debug("Ensuring VRF configuration for advertising routes") # Base BGP configuration bgp_utils.ensure_base_bgp_configuration() @lockutils.synchronized('bgp') def sync(self): self._expose_tenant_networks = (CONF.expose_tenant_networks or CONF.expose_ipv6_gua_tenant_networks) self.ovn_routing_tables = {} self.ovn_bridge_mappings = {} self.ovn_local_cr_lrps = {} self.ovn_local_lrps = {} self.ovn_routing_tables_routes = collections.defaultdict() self.provider_ovn_lbs = collections.defaultdict() self.ovs_flows = {} LOG.debug("Configuring br-ex default rule and routing tables for " "each provider network") # 1) Get bridge mappings: xxxx:br-ex,yyyy:br-ex2 bridge_mappings = self.ovs_idl.get_ovn_bridge_mappings() # 2) Get macs for bridge mappings extra_routes = {} for bridge_index, bridge_mapping in enumerate(bridge_mappings, 1): network, bridge = helpers.parse_bridge_mapping(bridge_mapping) if not network: continue self.ovn_bridge_mappings[network] = bridge if not extra_routes.get(bridge): extra_routes[bridge] = ( linux_net.ensure_routing_table_for_bridge( self.ovn_routing_tables, bridge, CONF.bgp_vrf_table_id)) vlan_tags = self.sb_idl.get_network_vlan_tag_by_network_name( network) for vlan_tag in vlan_tags: linux_net.ensure_vlan_device_for_network(bridge, vlan_tag) linux_net.ensure_arp_ndp_enabled_for_bridge(bridge, bridge_index, vlan_tags) if self.ovs_flows.get(bridge): continue mac = linux_net.get_interface_address(bridge) self.ovs_flows[bridge] = { 'mac': mac, 'in_port': set([])} # 3) Get in_port for bridge mappings (br-ex, br-ex2) self.ovs_flows[bridge]['in_port'] = ( ovs.get_ovs_patch_ports_info(bridge)) # 4) Add/Remove flows for each bridge mappings ovs.ensure_mac_tweak_flows(bridge, self.ovs_flows[bridge]['mac'], self.ovs_flows[bridge]['in_port'], constants.OVS_RULE_COOKIE) ovs.remove_extra_ovs_flows(self.ovs_flows, bridge, constants.OVS_RULE_COOKIE) LOG.debug("Syncing current routes.") exposed_ips = linux_net.get_exposed_ips(CONF.bgp_nic) # get the rules pointing to ovn bridges ovn_ip_rules = linux_net.get_ovn_ip_rules( self.ovn_routing_tables.values()) # add missing routes/ips for IPs on provider network ports = self.sb_idl.get_ports_on_chassis(self.chassis) for port in ports: self._ensure_port_exposed(port, exposed_ips, ovn_ip_rules) # this information is only available when there are cr-lrps add # missing routes/ips for FIPs associated to VMs/LBs on the chassis cr_lrp_ports = self.sb_idl.get_cr_lrp_ports_on_chassis( self.chassis) for cr_lrp_port in cr_lrp_ports: self._ensure_cr_lrp_associated_ports_exposed( cr_lrp_port, exposed_ips, ovn_ip_rules) for cr_lrp_port, cr_lrp_info in self.ovn_local_cr_lrps.items(): lrp_ports = self.sb_idl.get_lrp_ports_for_router( cr_lrp_info['router_datapath']) for lrp in lrp_ports: self._process_lrp_port(lrp, cr_lrp_port, exposed_ips, ovn_ip_rules) # add missing routes/ips related to ovn-octavia loadbalancers # on the provider networks provider_ovn_lbs = self.sb_idl.get_provider_ovn_lbs_on_cr_lrp( cr_lrp_info['provider_datapath'], cr_lrp_info['router_datapath']) for ovn_lb, ovn_lb_ip in provider_ovn_lbs.items(): self._expose_ovn_lb_on_provider(ovn_lb_ip, ovn_lb, cr_lrp_port, exposed_ips, ovn_ip_rules) # remove extra routes/ips # remove all the leftovers on the list of current ips on dev OVN linux_net.delete_exposed_ips(exposed_ips, CONF.bgp_nic) # remove all the leftovers on the list of current ip rules for ovn # bridges linux_net.delete_ip_rules(ovn_ip_rules) # remove all the extra rules not needed linux_net.delete_bridge_ip_routes(self.ovn_routing_tables, self.ovn_routing_tables_routes, extra_routes) wire_utils.delete_vlan_devices_leftovers(self.sb_idl, self.ovn_bridge_mappings) def _ensure_cr_lrp_associated_ports_exposed(self, cr_lrp_port, exposed_ips, ovn_ip_rules): ips, patch_port_row = self.sb_idl.get_cr_lrp_nat_addresses_info( cr_lrp_port, self.chassis, self.sb_idl) if not ips: return ips_adv = self._expose_ip(ips, patch_port_row, associated_port=cr_lrp_port) for ip in ips_adv: if exposed_ips and ip in exposed_ips: exposed_ips.remove(ip) if ovn_ip_rules: ip_version = linux_net.get_ip_version(ip) if ip_version == constants.IP_VERSION_6: ip_dst = "{}/128".format(ip) else: ip_dst = "{}/32".format(ip) ovn_ip_rules.pop(ip_dst, None) def _ensure_port_exposed(self, port, exposed_ips, ovn_ip_rules): if port.type not in constants.OVN_VIF_PORT_TYPES or not port.mac: return port_ips = [] if port.mac == ['unknown']: # For FIPs associated to VM ports we don't need the port IP, so # we can check if it is a VM on the provider and trigger the # expose_ip without passing any port_ips try: if ((port.type != constants.OVN_VM_VIF_PORT_TYPE and port.type != constants.OVN_VIRTUAL_VIF_PORT_TYPE) or self.sb_idl.is_provider_network(port.datapath)): return except agent_exc.DatapathNotFound: # There is no need to expose anything related to a removed # datapath LOG.debug("Port %s not being exposed as its datapath %s was " "removed", port.logical_port, port.datapath) return else: if len(port.mac[0].strip().split(' ')) < 2: return port_ips = port.mac[0].strip().split(' ')[1:] ips_adv = self._expose_ip(port_ips, port) for port_ip in ips_adv: ip_address = port_ip.split("/")[0] if exposed_ips and ip_address in exposed_ips: # remove each ip to add from the list of current ips on dev OVN exposed_ips.remove(ip_address) if ovn_ip_rules: ip_version = linux_net.get_ip_version(port_ip) if ip_version == constants.IP_VERSION_6: ip_dst = "{}/128".format(ip_address) else: ip_dst = "{}/32".format(ip_address) ovn_ip_rules.pop(ip_dst, None) def _expose_provider_port(self, port_ips, provider_datapath, bridge_device=None, bridge_vlan=None, lladdr=None, proxy_cidrs=None): if proxy_cidrs is None: proxy_cidrs = [] if not bridge_device and not bridge_vlan: bridge_device, bridge_vlan = self._get_bridge_for_datapath( provider_datapath) if (not bridge_device or bridge_device not in self.ovn_bridge_mappings.values()): return False localnet = self.ovn_provider_datapath.get(provider_datapath) if not localnet: try: localnet = self.sb_idl.get_localnet_for_datapath( provider_datapath) if localnet: self.ovn_provider_datapath[provider_datapath] = localnet else: LOG.warning("%s is not a provider network as it does not" "have a localnet port, no need to expose the" "ips %s", provider_datapath, port_ips) return False except agent_exc.DatapathNotFound: LOG.exception("Provider network not found, no need to expose " "ips %s", port_ips) return False # Connect to OVN try: if wire_utils.wire_provider_port( self.ovn_routing_tables_routes, self.ovs_flows, port_ips, bridge_device, bridge_vlan, localnet, self.ovn_routing_tables, proxy_cidrs, lladdr=lladdr): # Expose the IP now that it is connected bgp_utils.announce_ips(port_ips) return True return False except Exception as e: LOG.exception("Unexpected exception while wiring provider port: " "%s", e) return False def _expose_tenant_port(self, port, ip_version, exposed_ips=None, ovn_ip_rules=None): # specific case for ovn-lb vips on tenant networks if not port.mac and not port.chassis and not port.up[0]: ext_n_cidr = port.external_ids.get( constants.OVN_CIDRS_EXT_ID_KEY, "") if ext_n_cidr: ovn_lb_ip = ext_n_cidr.split(" ")[0].split("/")[0] bgp_utils.announce_ips([ovn_lb_ip]) if exposed_ips and ovn_lb_ip in exposed_ips: exposed_ips.remove(ovn_lb_ip) if ovn_ip_rules: ovn_ip_rules.pop(ext_n_cidr.split(" ")[0], None) return elif (not port.mac or port.type not in ( constants.OVN_VM_VIF_PORT_TYPE, constants.OVN_VIRTUAL_VIF_PORT_TYPE) or (port.type == constants.OVN_VM_VIF_PORT_TYPE and not port.chassis)): return try: if port.mac == ['unknown']: # Handling the case for unknown MACs when configdrive is used # instead of dhcp n_cidrs = port.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "") port_ips = [ip.split("/")[0] for ip in n_cidrs.split(" ")] else: port_ips = port.mac[0].strip().split(' ')[1:] except IndexError: return for port_ip in port_ips: # Only adding the port ips that match the lrp # IP version port_ip_version = linux_net.get_ip_version(port_ip) if port_ip_version == ip_version: bgp_utils.announce_ips([port_ip]) if exposed_ips and port_ip in exposed_ips: exposed_ips.remove(port_ip) if ovn_ip_rules: if port_ip_version == constants.IP_VERSION_6: ip_dst = "{}/128".format(port_ip) else: ip_dst = "{}/32".format(port_ip) ovn_ip_rules.pop(ip_dst, None) def _withdraw_provider_port(self, port_ips, provider_datapath, bridge_device=None, bridge_vlan=None, lladdr=None, proxy_cidrs=None): if proxy_cidrs is None: proxy_cidrs = [] # Withdraw IP before disconnecting it bgp_utils.withdraw_ips(port_ips) # Disconnect IP from OVN # assuming either you pass both or none if not bridge_device and not bridge_vlan: bridge_device, bridge_vlan = self._get_bridge_for_datapath( provider_datapath) if not bridge_device: return False try: return wire_utils.unwire_provider_port( self.ovn_routing_tables_routes, port_ips, bridge_device, bridge_vlan, self.ovn_routing_tables, proxy_cidrs, lladdr=lladdr) except Exception as e: LOG.exception("Unexpected exception while unwiring provider port: " "%s", e) return False def _get_bridge_for_datapath(self, datapath): network_name, network_tag = self.sb_idl.get_network_name_and_tag( datapath, self.ovn_bridge_mappings.keys()) if network_name: if network_tag: return self.ovn_bridge_mappings[network_name], network_tag[0] return self.ovn_bridge_mappings[network_name], None return None, None @lockutils.synchronized('bgp') def expose_ovn_lb(self, ip, row): self._process_ovn_lb(ip, row, constants.EXPOSE) @lockutils.synchronized('bgp') def withdraw_ovn_lb(self, ip, row): self._process_ovn_lb(ip, row, constants.WITHDRAW) def _process_ovn_lb(self, ip, row, action): try: if (not self._expose_tenant_networks or self.sb_idl.is_provider_network(row.datapath)): return except agent_exc.DatapathNotFound: # There is no need to expose anything related to a removed # datapath LOG.debug("LoadBalancer with VIP %s not being exposed/withdraw as" " its associated datapath %s was removed", ip, row.datapath) return if action == constants.EXPOSE: return self._expose_remote_ip([ip], row) if action == constants.WITHDRAW: return self._withdraw_remote_ip([ip], row) # if unknown action return return @lockutils.synchronized('bgp') def expose_ovn_lb_on_provider(self, ip, lb_name, cr_lrp_port): self._expose_ovn_lb_on_provider(ip, lb_name, cr_lrp_port) @lockutils.synchronized('bgp') def withdraw_ovn_lb_on_provider(self, lb_name, cr_lrp_port): self._withdraw_ovn_lb_on_provider(lb_name, cr_lrp_port) def _expose_ovn_lb_on_provider(self, ip, lb_name, cr_lrp, exposed_ips=None, ovn_ip_rules=None): LOG.debug("Adding BGP route for loadbalancer VIP %s", ip) try: bridge_device = self.ovn_local_cr_lrps[cr_lrp]['bridge_device'] bridge_vlan = self.ovn_local_cr_lrps[cr_lrp]['bridge_vlan'] except KeyError: LOG.debug("Failure adding BGP route for loadbalancer VIP %s", ip) return False self.ovn_local_cr_lrps[cr_lrp]['provider_ovn_lbs'].append(lb_name) if self.provider_ovn_lbs.get(lb_name): self.provider_ovn_lbs[lb_name]['ips'].append(ip) else: self.provider_ovn_lbs[lb_name] = {'ips': [ip], 'gateway_port': cr_lrp} if not self._expose_provider_port( [ip], self.ovn_local_cr_lrps[cr_lrp]['provider_datapath'], bridge_device=bridge_device, bridge_vlan=bridge_vlan): LOG.debug("Failure adding BGP route for loadbalancer VIP %s", ip) return False LOG.debug("Added BGP route for loadbalancer VIP %s", ip) if exposed_ips and ip in exposed_ips: exposed_ips.remove(ip) if ovn_ip_rules: ip_version = linux_net.get_ip_version(ip) if ip_version == constants.IP_VERSION_6: ip_dst = "{}/128".format(ip) else: ip_dst = "{}/32".format(ip) ovn_ip_rules.pop(ip_dst, None) return True def _withdraw_ovn_lb_on_provider(self, lb_name, cr_lrp): try: bridge_device = self.ovn_local_cr_lrps[cr_lrp]['bridge_device'] bridge_vlan = self.ovn_local_cr_lrps[cr_lrp]['bridge_vlan'] except KeyError: LOG.debug("Failure deleting BGP routes for loadbalancer VIPs " "%s", self.provider_ovn_lbs[lb_name].get('ips')) return False for ip in self.provider_ovn_lbs[lb_name].get('ips').copy(): LOG.debug("Deleting BGP route for loadbalancer VIP %s", ip) if not self._withdraw_provider_port( [ip], None, bridge_device=bridge_device, bridge_vlan=bridge_vlan): LOG.debug("Failure deleting BGP route for loadbalancer VIP " "%s", ip) return False if ip in self.provider_ovn_lbs[lb_name].get('ips', []): self.provider_ovn_lbs[lb_name]['ips'].remove(ip) LOG.debug("Deleted BGP route for loadbalancer VIP %s", ip) if lb_name in self.ovn_local_cr_lrps[cr_lrp]['provider_ovn_lbs']: self.ovn_local_cr_lrps[cr_lrp]['provider_ovn_lbs'].remove( lb_name) return True @lockutils.synchronized('bgp') def expose_ip(self, ips, row, associated_port=None): '''Advertice BGP route by adding IP to device. This methods ensures BGP advertises the IP of the VM in the provider network, or the FIP associated to a VM in a tenant networks. It relies on Zebra, which creates and advertises a route when an IP is added to a local interface. This method assumes a device named self.ovn_device exists (inside a VRF), and adds the IP of either: - VM IP on the provider network, - VM FIP, or - CR-LRP OVN port ''' self._expose_ip(ips, row, associated_port) def _expose_ip(self, ips, row, associated_port=None): if (row.type == constants.OVN_VM_VIF_PORT_TYPE or row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE): try: provider_network = self.sb_idl.is_provider_network( row.datapath) except agent_exc.DatapathNotFound: # There is no need to expose anything related to a removed # datapath LOG.debug("Port %s not being exposed as its associated " "datapath %s was removed", row.logical_port, row.datapath) return [] # VM on provider Network if provider_network: exposed_port = False LOG.debug("Adding BGP route for logical port with ip %s", ips) if row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE: # NOTE: For Amphora Load Balancer with IPv6 VIP on the # provider network, we need a NDP Proxy so that the # traffic from the amphora can properly be redirected back bridge_device, bridge_vlan = self._get_bridge_for_datapath( row.datapath) # NOTE: This is neutron specific as we need the provider # prefix to add the ndp proxy n_cidr = row.external_ids.get( constants.OVN_CIDRS_EXT_ID_KEY, "").split() exposed_port = self._expose_provider_port( ips, row.datapath, bridge_device, bridge_vlan, None, n_cidr) else: exposed_port = self._expose_provider_port(ips, row.datapath) if not exposed_port: LOG.debug("Failure adding BGP route for logical port with " "ip %s", ips) return [] LOG.debug("Added BGP route for logical port with ip %s", ips) return ips # VM with FIP else: # FIPs are only supported with IPv4 fip_address, fip_datapath = self.sb_idl.get_fip_associated( row.logical_port) if not fip_address: return [] if not self.sb_idl.is_provider_network(fip_datapath): # Only exposing IPs if the associated network is a # provider network return [] LOG.debug("Adding BGP route for FIP with ip %s", fip_address) if self._expose_provider_port([fip_address], fip_datapath): LOG.debug("Added BGP route for FIP with ip %s", fip_address) return [fip_address] LOG.debug("Failure adding BGP route for FIP with ip %s", fip_address) return [] # FIP association to VM elif row.type == constants.OVN_PATCH_VIF_PORT_TYPE: if (associated_port and self.sb_idl.is_port_on_chassis( associated_port, self.chassis)): if not self.sb_idl.is_provider_network(row.datapath): # Only exposing IPs if the associated network is a # provider network return [] LOG.debug("Adding BGP route for FIP with ip %s", ips) if self._expose_provider_port(ips, row.datapath): LOG.debug("Added BGP route for FIP with ip %s", ips) return ips LOG.debug("Failure adding BGP route for FIP with ip %s", ips) return [] # CR-LRP Port elif (row.type == constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE and row.logical_port.startswith('cr-')): cr_lrp_datapath = self.sb_idl.get_provider_datapath_from_cr_lrp( row.logical_port) if not cr_lrp_datapath: return [] if not self.sb_idl.is_provider_network(cr_lrp_datapath): # Only exposing IPs if the associated network is a # provider network return [] bridge_device, bridge_vlan = self._get_bridge_for_datapath( cr_lrp_datapath) mac = row.mac[0].strip().split(' ')[0] # Keeping information about the associated network for # tenant network advertisement self.ovn_local_cr_lrps[row.logical_port] = { 'router_datapath': row.datapath, 'provider_datapath': cr_lrp_datapath, 'ips': ips, 'mac': mac, 'subnets_datapath': {}, 'subnets_cidr': [], 'provider_ovn_lbs': [], 'bridge_vlan': bridge_vlan, 'bridge_device': bridge_device } if self._expose_cr_lrp_port(ips, mac, bridge_device, bridge_vlan, router_datapath=row.datapath, provider_datapath=cr_lrp_datapath, cr_lrp_port=row.logical_port): return ips return [] @lockutils.synchronized('bgp') def withdraw_ip(self, ips, row, associated_port=None): '''Withdraw BGP route by removing IP from device. This methods ensures BGP withdraw an advertised IP of a VM, either in the provider network, or the FIP associated to a VM in a tenant networks. It relies on Zebra, which withdraws the advertisement as soon as the IP is deleted from the local interface. This method assumes a device named self.ovn_device exists (inside a VRF), and removes the IP of either: - VM IP on the provider network, - VM FIP, or - CR-LRP OVN port ''' if (row.type == constants.OVN_VM_VIF_PORT_TYPE or row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE): try: provider_network = self.sb_idl.is_provider_network( row.datapath) except agent_exc.DatapathNotFound: # NOTE(ltomasbo): Datapath has been deleted. This means that: # - If it was a provider network we need to withdraw it # - It it was a VM with a FIP, the removal would be handled # by the FIP dissassociation even (FIP removal) that must # happen before removing the subnet from the router, and # before being able to remove the subnet # This means we only need to process the "provider_network" # case provider_network = True LOG.debug("Port %s belongs to a removed datapath %s. " "Assuming it was a provider network to avoid " "leaks.", row.logical_port, row.datapath) # VM on provider Network if provider_network: LOG.debug("Deleting BGP route for logical port with ip %s", ips) n_cidr = None withdrawn_port = False if row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE: virtual_provider_ports = ( self.sb_idl.get_virtual_ports_on_datapath_by_chassis( row.datapath, self.chassis)) if not virtual_provider_ports: cr_lrps_on_same_provider = [ p for p in self.ovn_local_cr_lrps.values() if p['provider_datapath'] == row.datapath] if not cr_lrps_on_same_provider: bridge_device, bridge_vlan = ( self._get_bridge_for_datapath(row.datapath)) # NOTE: This is neutron specific as we need the # provider prefix to add the ndp proxy n_cidr = row.external_ids.get( constants.OVN_CIDRS_EXT_ID_KEY, "").split() if n_cidr: withdrawn_port = self._withdraw_provider_port( ips, row.datapath, bridge_device, bridge_vlan, None, n_cidr) else: withdrawn_port = self._withdraw_provider_port(ips, row.datapath) if not withdrawn_port: LOG.debug("Failure deleting BGP route for logical port " "with ip %s", ips) return LOG.debug("Deleted BGP route for logical port with ip %s", ips) return # VM with FIP else: # FIPs are only supported with IPv4 fip_address, fip_datapath = self.sb_idl.get_fip_associated( row.logical_port) if not fip_address: return if not self.sb_idl.is_provider_network(fip_datapath): # Only exposing IPs if the associated network is a # provider network return LOG.debug("Deleting BGP route for FIP with ip %s", fip_address) if not self._withdraw_provider_port([fip_address], fip_datapath): LOG.debug("Failure deleting BGP route for FIP with ip %s", fip_address) return LOG.debug("Deleted BGP route for FIP with ip %s", fip_address) return # FIP disassociation to VM elif row.type == constants.OVN_PATCH_VIF_PORT_TYPE: if (associated_port and ( self.sb_idl.is_port_on_chassis( associated_port, self.chassis) or self.sb_idl.is_port_without_chassis(associated_port) or self.sb_idl.is_port_deleted(associated_port))): if not self.sb_idl.is_provider_network(row.datapath): # Only exposing IPs if the associated network is a # provider network return LOG.debug("Deleting BGP route for FIP with ip %s", ips) if not self._withdraw_provider_port(ips, row.datapath): LOG.debug("Failure deleting BGP route for FIP with ip %s", ips) return LOG.debug("Deleted BGP route for FIP with ip %s", ips) return # CR-LRP Port elif (row.type == constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE and row.logical_port.startswith('cr-')): cr_lrp_datapath = self.ovn_local_cr_lrps.get( row.logical_port, {}).get('provider_datapath') if not cr_lrp_datapath: return bridge_vlan = self.ovn_local_cr_lrps[row.logical_port].get( 'bridge_vlan') bridge_device = self.ovn_local_cr_lrps[row.logical_port].get( 'bridge_device') mac = row.mac[0].strip().split(' ')[0] self._withdraw_cr_lrp_port(ips, mac, bridge_device, bridge_vlan, provider_datapath=cr_lrp_datapath, cr_lrp_port=row.logical_port) @lockutils.synchronized('bgp') def expose_remote_ip(self, ips, row): self._expose_remote_ip(ips, row) def _expose_remote_ip(self, ips, row): try: if (self.sb_idl.is_provider_network(row.datapath) or not self._expose_tenant_networks): return except agent_exc.DatapathNotFound: # There is no need to expose anything related to a removed # datapath LOG.debug("Port %s not being exposed as its datapath %s was " "removed", row.logical_port, row.datapath) return if not CONF.expose_tenant_networks: # This means CONF.expose_ipv6_gua_tenant_networks is enabled gua_ips = [] for ip in ips: if driver_utils.is_ipv6_gua(ip): gua_ips.append(ip) if not gua_ips: return ips = gua_ips ips_to_expose = [] for ip in ips: if self._address_scope_allowed(ip, None, row): ips_to_expose.append(ip) if not ips_to_expose: return port_lrps = self.sb_idl.get_lrps_for_datapath(row.datapath) for port_lrp in port_lrps: if port_lrp in self.ovn_local_lrps.keys(): LOG.debug("Adding BGP route for tenant IP %s on chassis %s", ips_to_expose, self.chassis) bgp_utils.announce_ips(ips_to_expose) LOG.debug("Added BGP route for tenant IP %s on chassis %s", ips_to_expose, self.chassis) break @lockutils.synchronized('bgp') def withdraw_remote_ip(self, ips, row, chassis=None): self._withdraw_remote_ip(ips, row, chassis) def _withdraw_remote_ip(self, ips, row, chassis=None): try: if (self.sb_idl.is_provider_network(row.datapath) or not self._expose_tenant_networks): return except agent_exc.DatapathNotFound: # There is no need to continue as the subnet removal (patch port # removal) will trigger a withdraw_subnet event that will remove # the associated IPs LOG.debug("Port %s not being withdrawn as its datapath %s was " "removed. The subnet withdraw action will take care of " "the withdrawal.", row.logical_port, row.datapath) return if not CONF.expose_tenant_networks: # This means CONF.expose_ipv6_gua_tenant_networks is enabled gua_ips = [] for ip in ips: if driver_utils.is_ipv6_gua(ip): gua_ips.append(ip) if not gua_ips: return ips = gua_ips ips_to_withdraw = [] for ip in ips: if self._address_scope_allowed(ip, None, row): ips_to_withdraw.append(ip) if not ips_to_withdraw: return port_lrps = self.sb_idl.get_lrps_for_datapath(row.datapath) for port_lrp in port_lrps: if port_lrp in self.ovn_local_lrps.keys(): LOG.debug("Deleting BGP route for tenant IP %s on chassis %s", ips_to_withdraw, self.chassis) bgp_utils.withdraw_ips(ips_to_withdraw) LOG.debug("Deleted BGP route for tenant IP %s on chassis %s", ips_to_withdraw, self.chassis) break def _process_lrp_port(self, lrp, associated_cr_lrp, exposed_ips=None, ovn_ip_rules=None): if (lrp.chassis or not lrp.logical_port.startswith('lrp-') or "chassis-redirect-port" in lrp.options.keys() or associated_cr_lrp.strip('cr-') == lrp.logical_port): return # add missing route/ips for tenant network VMs if self._expose_tenant_networks: try: lrp_ip = lrp.mac[0].strip().split(' ')[1] except IndexError: # This should not happen: subnet without CIDR return if not lrp.options.get('peer'): # if there is no peer associated to the port we need to # 1) creation: wait for another re-sync to expose it # 2) deletion: no need to add it as it being removed return if not self._address_scope_allowed(lrp_ip, lrp.options['peer']): return subnet_datapath = self.sb_idl.get_port_datapath( lrp.options['peer']) self._expose_lrp_port(lrp_ip, lrp.logical_port, associated_cr_lrp, subnet_datapath, exposed_ips=exposed_ips, ovn_ip_rules=ovn_ip_rules) def _expose_cr_lrp_port(self, ips, mac, bridge_device, bridge_vlan, router_datapath, provider_datapath, cr_lrp_port): LOG.debug("Adding BGP route for CR-LRP Port %s", ips) ips_without_mask = [ip.split("/")[0] for ip in ips] if not self._expose_provider_port(ips_without_mask, provider_datapath, bridge_device, bridge_vlan, lladdr=mac, proxy_cidrs=ips): LOG.debug("Failure adding BGP route for CR-LRP Port %s", ips) return False LOG.debug("Added BGP route for CR-LRP Port %s", ips) # Expose FIPS # This is needed in case the router get disabled and enabled # In that case there may be FIPs already associated to VMs fips, patch_port_row = self.sb_idl.get_cr_lrp_nat_addresses_info( cr_lrp_port, self.chassis, self.sb_idl) fips = [ip for ip in fips if ip not in ips_without_mask] if fips: self._expose_ip(fips, patch_port_row, associated_port=cr_lrp_port) # Check if there are networks attached to the router, # and if so, add the needed routes/rules lrp_ports = self.sb_idl.get_lrp_ports_for_router(router_datapath) for lrp in lrp_ports: self._process_lrp_port(lrp, cr_lrp_port) cr_lrp_provider_dp = self.ovn_local_cr_lrps[cr_lrp_port][ 'provider_datapath'] cr_lrp_router_dp = self.ovn_local_cr_lrps[cr_lrp_port][ 'router_datapath'] provider_ovn_lbs = self.sb_idl.get_provider_ovn_lbs_on_cr_lrp( cr_lrp_provider_dp, cr_lrp_router_dp) for ovn_lb, ovn_lb_ip in provider_ovn_lbs.items(): self._expose_ovn_lb_on_provider(ovn_lb_ip, ovn_lb, cr_lrp_port) return True def _withdraw_cr_lrp_port(self, ips, mac, bridge_device, bridge_vlan, provider_datapath, cr_lrp_port): LOG.debug("Deleting BGP route for CR-LRP Port %s", ips) # Removing information about the associated network for # tenant network advertisement ips_without_mask = [ip.split("/")[0] for ip in ips] # del proxy ndp config for ipv6 proxy_cidrs = [] for ip in ips_without_mask: if linux_net.get_ip_version(ip) == constants.IP_VERSION_6: cr_lrps_on_same_provider = [ p for p in self.ovn_local_cr_lrps.values() if p['provider_datapath'] == provider_datapath] # if no other cr-lrp port on the same provider # delete the ndp proxy if (len(cr_lrps_on_same_provider) <= 1): proxy_cidrs.append(ip) if not self._withdraw_provider_port( ips_without_mask, provider_datapath, bridge_device=bridge_device, bridge_vlan=bridge_vlan, lladdr=mac, proxy_cidrs=proxy_cidrs): LOG.debug("Failure deleting BGP route for CR-LRP Port %s", ips) return False LOG.debug("Deleted BGP route for CR-LRP Port %s", ips) # Check if there are networks attached to the router, # and if so delete the needed routes/rules local_cr_lrp_info = self.ovn_local_cr_lrps.get(cr_lrp_port) for subnet_cidr in local_cr_lrp_info['subnets_cidr']: self._withdraw_lrp_port(subnet_cidr, None, cr_lrp_port) # check if there are loadbalancers associated to the router, # and if so delete the needed routes/rules provider_ovn_lbs = self.ovn_local_cr_lrps[cr_lrp_port][ 'provider_ovn_lbs'].copy() for provider_ovn_lb in provider_ovn_lbs: self._withdraw_ovn_lb_on_provider(provider_ovn_lb, cr_lrp_port) try: del self.ovn_local_cr_lrps[cr_lrp_port] except KeyError: LOG.debug("Gateway port %s already cleanup from the agent.", cr_lrp_port) return True def _expose_lrp_port(self, ip, lrp, associated_cr_lrp, subnet_datapath, exposed_ips=None, ovn_ip_rules=None): if not self._expose_tenant_networks: return if not CONF.expose_tenant_networks: # This means CONF.expose_ipv6_gua_tenant_networks is enabled if not driver_utils.is_ipv6_gua(ip): return cr_lrp_info = self.ovn_local_cr_lrps.get(associated_cr_lrp, {}) cr_lrp_ips = [ip_address.split('/')[0] for ip_address in cr_lrp_info.get('ips', [])] # this is the router gateway port if ip.split('/')[0] in cr_lrp_ips: return cr_lrp_datapath = cr_lrp_info.get('provider_datapath') if not cr_lrp_datapath: return bridge_device = cr_lrp_info.get('bridge_device') bridge_vlan = cr_lrp_info.get('bridge_vlan') # update information needed for the loadbalancers cr_lrp_info['subnets_datapath'].update({lrp: subnet_datapath}) cr_lrp_info['subnets_cidr'].append(ip) self.ovn_local_lrps.update({lrp: associated_cr_lrp}) try: if not wire_utils.wire_lrp_port( self.ovn_routing_tables_routes, ip, bridge_device, bridge_vlan, self.ovn_routing_tables, cr_lrp_ips): LOG.warning("Not able to expose subnet with IP %s", ip) return except Exception as e: LOG.exception("Unexpected exception while wiring lrp port: %s", e) return if ovn_ip_rules: ovn_ip_rules.pop(ip, None) # Check if there are VMs on the network # and if so expose the route ports = self.sb_idl.get_ports_on_datapath(subnet_datapath) ip_version = linux_net.get_ip_version(ip) for port in ports: self._expose_tenant_port(port, ip_version=ip_version, exposed_ips=exposed_ips, ovn_ip_rules=ovn_ip_rules) def _withdraw_lrp_port(self, ip, lrp, associated_cr_lrp): if not self._expose_tenant_networks: return if not CONF.expose_tenant_networks: # This means CONF.expose_ipv6_gua_tenant_networks is enabled if not driver_utils.is_ipv6_gua(ip): return cr_lrp_info = self.ovn_local_cr_lrps.get(associated_cr_lrp, {}) exposed_lrp = False if lrp: if lrp in self.ovn_local_lrps.keys(): exposed_lrp = True self.ovn_local_lrps.pop(lrp) else: for subnet_lp in cr_lrp_info['subnets_datapath'].keys(): if subnet_lp in self.ovn_local_lrps.keys(): exposed_lrp = True self.ovn_local_lrps.pop(subnet_lp) break cr_lrp_info['subnets_datapath'].pop(lrp, None) if not exposed_lrp: return cr_lrp_ips = [ip_address.split('/')[0] for ip_address in cr_lrp_info.get('ips', [])] bridge_device = cr_lrp_info.get('bridge_device') bridge_vlan = cr_lrp_info.get('bridge_vlan') ip_version = linux_net.get_ip_version(ip) for cr_lrp_ip in cr_lrp_ips: cr_lrp_ip_version = linux_net.get_ip_version(cr_lrp_ip) if cr_lrp_ip_version != ip_version: continue if cr_lrp_ip_version == constants.IP_VERSION_6: net = ipaddress.IPv6Network(ip, strict=False) else: net = ipaddress.IPv4Network(ip, strict=False) break # Check if there are VMs on the network # and if so withdraw the routes if net: vms_on_net = linux_net.get_exposed_ips_on_network( CONF.bgp_nic, net) linux_net.delete_exposed_ips(vms_on_net, CONF.bgp_nic) # Disconnect the network to OVN try: wire_utils.unwire_lrp_port( self.ovn_routing_tables_routes, ip, bridge_device, bridge_vlan, self.ovn_routing_tables, cr_lrp_ips) except Exception as e: LOG.exception("Unexpected exception while unwiring lrp port: %s", e) @lockutils.synchronized('bgp') def expose_subnet(self, ip, row): try: cr_lrp = self.sb_idl.is_router_gateway_on_chassis( row.datapath, self.chassis) except agent_exc.DatapathNotFound: # It seems it may also happen that router gets deleted before the # subnet attachment to it gets processed, and in that case there # is no need to expose anything return if not row.options.get('peer'): # if there is no peer associated to the port we need to # 1) creation: wait for another re-sync to expose it # 2) deletion: no need to add it as it being removed return subnet_datapath = self.sb_idl.get_port_datapath( row.options['peer']) if not cr_lrp or not self.ovn_local_cr_lrps.get(cr_lrp): return if not self._address_scope_allowed(ip, row.options['peer']): return self._expose_lrp_port(ip, row.logical_port, cr_lrp, subnet_datapath) @lockutils.synchronized('bgp') def withdraw_subnet(self, ip, row): try: cr_lrp = self.sb_idl.is_router_gateway_on_chassis( row.datapath, self.chassis) except agent_exc.DatapathNotFound: # NOTE(ltomasbo): This happens when the router (datapath) gets # deleted at the same time as subnets are detached from it. # Usually this will be hit when router is deleted without # removing its gateway. In that case we don't need to withdraw # the subnet as it is not exposed, just the cr-lrp which is # handle in a different event/method (withdraw_ip) LOG.debug("Router is being deleted, so it's datapath does " "not exists any more. Checking if port %s belongs " "to chassis redirect and skip in that case.", row.logical_port) cr_lrp = [cr_lrp_name for cr_lrp_name in self.ovn_local_cr_lrps.keys() if row.logical_port in cr_lrp_name] # if cr_lrp exists, this means the lrp port is for the router # gateway, so there is no need to proceed if cr_lrp: LOG.debug("Port %s is related to chassis redirect, so " "there is no need to do further actions for " "subnet withdrawal, as this port was not " "triggering a subnet exposure.", row.logical_port) return if not cr_lrp or not self.ovn_local_cr_lrps.get(cr_lrp): # NOTE(ltomasbo) there is a chance the cr-lrp just got moved # to this node but was not yet processed. In that case there # is no need to withdraw the network as it was not exposed here return self._withdraw_lrp_port(ip, row.logical_port, cr_lrp) def _address_scope_allowed(self, ip, port_name, sb_port=None): if not self.allowed_address_scopes: # No address scopes to filter on => announce everything return True if not sb_port: sb_port = self.sb_idl.get_port_by_name(port_name) if not sb_port: LOG.error("Port %s missing, skipping.", port_name) return False address_scopes = driver_utils.get_addr_scopes(sb_port) # if we should filter on address scopes and this port has no # address scopes set we do not need to expose it if not any(address_scopes.values()): return False # if address scope does not match, no need to expose it ip_version = linux_net.get_ip_version(ip) return address_scopes[ip_version] in self.allowed_address_scopes ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py000066400000000000000000001064201460327367600262330ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import collections import ipaddress import threading from oslo_concurrency import lockutils from oslo_config import cfg from oslo_log import log as logging from ovn_bgp_agent import constants from ovn_bgp_agent.drivers import driver_api from ovn_bgp_agent.drivers.openstack.utils import frr from ovn_bgp_agent.drivers.openstack.utils import ovn from ovn_bgp_agent.drivers.openstack.utils import ovs from ovn_bgp_agent.drivers.openstack.watchers import evpn_watcher as \ watcher from ovn_bgp_agent.utils import helpers from ovn_bgp_agent.utils import linux_net CONF = cfg.CONF LOG = logging.getLogger(__name__) # LOG.setLevel(logging.DEBUG) # logging.basicConfig(level=logging.DEBUG) OVN_TABLES = ["Port_Binding", "Chassis", "Datapath_Binding", "Chassis_Private"] EVPN_INFO = collections.namedtuple( 'EVPNInfo', ['vrf_name', 'lo_name', 'bridge_name', 'vxlan_name', 'veth_vrf', 'veth_ovs', 'vlan_name']) class OVNEVPNDriver(driver_api.AgentDriverBase): def __init__(self): self.ovn_bridge_mappings = {} # {'public': 'br-ex'} self.ovn_local_cr_lrps = {} self.ovn_local_lrps = {} # {'br-ex': [route1, route2]} self._ovn_routing_tables_routes = collections.defaultdict() self._ovn_exposed_evpn_ips = collections.defaultdict() self._sb_idl = None self._post_fork_event = threading.Event() @property def sb_idl(self): if not self._sb_idl: self._post_fork_event.wait() return self._sb_idl @sb_idl.setter def sb_idl(self, val): self._sb_idl = val def start(self): self.ovs_idl = ovs.OvsIdl() self.ovs_idl.start(CONF.ovsdb_connection) self.chassis = self.ovs_idl.get_own_chassis_id() self.ovn_remote = self.ovs_idl.get_ovn_remote() LOG.debug("Loaded chassis %s.", self.chassis) self._post_fork_event.clear() events = self._get_events() self.sb_idl = ovn.OvnSbIdl( self.ovn_remote, chassis=self.chassis, tables=OVN_TABLES, events=events).start() # Now IDL connections can be safely used self._post_fork_event.set() def _get_events(self): return {watcher.PortBindingChassisCreatedEvent(self), watcher.PortBindingChassisDeletedEvent(self), watcher.SubnetRouterAttachedEvent(self), watcher.SubnetRouterDetachedEvent(self), watcher.TenantPortCreatedEvent(self), watcher.TenantPortDeletedEvent(self), watcher.ChassisCreateEvent(self), watcher.ChassisPrivateCreateEvent(self), watcher.LocalnetCreateDeleteEvent(self)} @lockutils.synchronized('evpn') def frr_sync(self): # Note(ltomasbo): There is no need for resync on this as there is # no base configuration to be made, but one added when subnets are # exposed, so the sync action takes care of it pass @lockutils.synchronized('evpn') def sync(self): self.ovn_local_cr_lrps = {} self.ovn_local_lrps = {} self._ovn_routing_tables_routes = collections.defaultdict() self._ovn_exposed_evpn_ips = collections.defaultdict() # 1) Get bridge mappings: xxxx:br-ex,yyyy:br-ex2 bridge_mappings = self.ovs_idl.get_ovn_bridge_mappings() # 2) Get macs for bridge mappings for bridge_index, bridge_mapping in enumerate(bridge_mappings, 1): network, bridge = helpers.parse_bridge_mapping(bridge_mapping) if not network: continue self.ovn_bridge_mappings[network] = bridge linux_net.ensure_arp_ndp_enabled_for_bridge(bridge, bridge_index) # TO DO # add missing routes/ips for fips/provider VMs ports = self.sb_idl.get_ports_on_chassis(self.chassis) for port in ports: if port.type != constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE: continue self._expose_ip(port, cr_lrp=True) self._remove_extra_exposed_ips() self._remove_extra_routes() self._remove_extra_ovs_flows() self._remove_extra_vrfs() def _ensure_network_exposed(self, router_port, gateway): evpn_info = self.sb_idl.get_evpn_info_from_port_name( router_port.logical_port) if not evpn_info: LOG.debug("No EVPN information for LRP Port %s. " "Not exposing it.", router_port) return gateway_ips = [ip.split('/')[0] for ip in gateway['ips']] try: router_port_ip = router_port.mac[0].strip().split(' ')[1] except IndexError: return router_ip = router_port_ip.split('/')[0] if router_ip in gateway_ips: return self.ovn_local_lrps[router_port.logical_port] = { 'datapath': router_port.datapath, 'ip': router_port_ip } datapath_bridge, vlan_tag = self._get_bridge_for_datapath( gateway['provider_datapath']) network_datapath = self.sb_idl.get_port_datapath( router_port.options['peer']) self._expose_subnet(router_port_ip, gateway_ips, gateway, datapath_bridge, vlan_tag, network_datapath) def _get_bridge_for_datapath(self, datapath): network_name, network_tag = self.sb_idl.get_network_name_and_tag( datapath, self.ovn_bridge_mappings.keys()) if network_name: if network_tag: return self.ovn_bridge_mappings[network_name], network_tag[0] return self.ovn_bridge_mappings[network_name], None return None, None @lockutils.synchronized('evpn') def expose_ip(self, row, cr_lrp=False): '''Advertice BGP route through EVPN. This methods ensures BGP advertises the IP through the required VRF/Tenant by using the specified VNI/VXLAN id. It relies on Zebra, which creates and advertises a route when an IP is added to a interface in the related VRF. ''' self._expose_ip(row, cr_lrp) def _expose_ip(self, row, cr_lrp=False): if cr_lrp: cr_lrp_port_name = row.logical_port cr_lrp_port = row else: cr_lrp_port_name = 'cr-lrp-' + row.logical_port cr_lrp_port = self.sb_idl.get_port_if_local_chassis( cr_lrp_port_name, self.chassis) if not cr_lrp_port: # Not in local chassis, no need to proccess return _, cr_lrp_datapath = self.sb_idl.get_fip_associated( cr_lrp_port_name) if not cr_lrp_datapath: return if len(cr_lrp_port.mac[0].strip().split(' ')) < 2: return ips = cr_lrp_port.mac[0].strip().split(' ')[1:] if cr_lrp: evpn_info = self.sb_idl.get_evpn_info_from_port_name( cr_lrp_port_name) else: evpn_info = self.sb_idl.get_evpn_info(row) if not evpn_info: LOG.debug("No EVPN information for CR-LRP Port with IPs %s. " "Not exposing it.", ips) return datapath_bridge, vlan_tag = self._get_bridge_for_datapath( cr_lrp_datapath) LOG.info("Adding BGP route for CR-LRP Port %s on AS %s and " "VNI %s", ips, evpn_info['bgp_as'], evpn_info['vni']) evpn_devices = self._ensure_evpn_devices(datapath_bridge, evpn_info['vni'], vlan_tag) if not evpn_devices.vrf_name or not evpn_devices.lo_name: return self.ovn_local_cr_lrps[cr_lrp_port_name] = { 'router_datapath': cr_lrp_port.datapath, 'provider_datapath': cr_lrp_datapath, 'ips': ips, 'mac': cr_lrp_port.mac[0].strip().split(' ')[0], 'vni': int(evpn_info['vni']), 'bgp_as': evpn_info['bgp_as'], 'lo': evpn_devices.lo_name, 'bridge': evpn_devices.bridge_name, 'vxlan': evpn_devices.vxlan_name, 'vrf': evpn_devices.vrf_name, 'veth_vrf': evpn_devices.veth_vrf, 'veth_ovs': evpn_devices.veth_ovs, 'vlan': evpn_devices.vlan_name } frr.vrf_reconfigure(evpn_info, action="add-vrf") self._connect_evpn_to_ovn(evpn_devices.vrf_name, evpn_devices.veth_vrf, evpn_devices.veth_ovs, ips, datapath_bridge, evpn_info['vni'], evpn_devices.vlan_name, vlan_tag) ips_without_mask = [ip.split("/")[0] for ip in ips] nei_dev = evpn_devices.vlan_name if vlan_tag else evpn_devices.veth_vrf for ip in ips_without_mask: linux_net.add_ip_nei( ip, self.ovn_local_cr_lrps[cr_lrp_port_name]['mac'], nei_dev) # Check if there are networks attached to the router, # and if so, add the needed routes/rules lrp_ports = self.sb_idl.get_lrp_ports_for_router( cr_lrp_port.datapath) for lrp in lrp_ports: if lrp.chassis or "chassis-redirect-port" in lrp.options.keys(): continue self._ensure_network_exposed( lrp, self.ovn_local_cr_lrps[cr_lrp_port_name]) @lockutils.synchronized('evpn') def withdraw_ip(self, row, cr_lrp=False): '''Withdraw BGP route through EVPN. This methods ensures BGP withdraw the IP advertised through the required VRF/Tenant by using the specified VNI/VXLAN id. It relies on Zebra, which cwithdraws the advertisement as son as the IP is deleted from the interface in the related VRF. ''' if cr_lrp: cr_lrp_port_name = row.logical_port else: cr_lrp_port_name = 'cr-lrp-' + row.logical_port cr_lrp_info = self.ovn_local_cr_lrps.get(cr_lrp_port_name, {}) if not cr_lrp_info: # This means it is in a different chassis return cr_lrp_datapath = cr_lrp_info.get('provider_datapath') if not cr_lrp_datapath: return ips = cr_lrp_info.get('ips') evpn_vni = cr_lrp_info.get('vni') if not evpn_vni: LOG.debug("No EVPN information for CR-LRP Port with IPs %s. " "No need to withdraw it.", ips) return LOG.info("Delete BGP route for CR-LRP Port %s on VNI %s", ips, evpn_vni) datapath_bridge, vlan_tag = self._get_bridge_for_datapath( cr_lrp_datapath) if vlan_tag: self._disconnect_evpn_from_ovn(evpn_vni, datapath_bridge, ips, vlan_tag=vlan_tag) else: cr_lrps_on_same_provider = [ p for p in self.ovn_local_cr_lrps.values() if p['provider_datapath'] == cr_lrp_datapath] if (len(cr_lrps_on_same_provider) > 1): # NOTE: no need to remove the NDP proxy if there are other # cr-lrp ports on the same chassis connected to the same # provider flat network self._disconnect_evpn_from_ovn(evpn_vni, datapath_bridge, ips, cleanup_ndp_proxy=False) else: self._disconnect_evpn_from_ovn(evpn_vni, datapath_bridge, ips) nei_dev = cr_lrp_info['vlan'] if vlan_tag else cr_lrp_info['veth_vrf'] for ip in ips: linux_net.del_ip_nei(ip, cr_lrp_info['mac'], nei_dev) self._remove_evpn_devices(evpn_vni) ovs.remove_evpn_router_ovs_flows(datapath_bridge, constants.OVS_VRF_RULE_COOKIE, cr_lrp_info.get('mac')) evpn_info = {'vni': evpn_vni, 'bgp_as': cr_lrp_info.get('bgp_as')} frr.vrf_reconfigure(evpn_info, action="del-vrf") try: del self.ovn_local_cr_lrps[cr_lrp_port_name] except KeyError: LOG.debug("Gateway port already cleanup from the agent: %s", cr_lrp_port_name) @lockutils.synchronized('evpn') def expose_remote_ip(self, ips, row): if self.sb_idl.is_provider_network(row.datapath): return port_lrps = self.sb_idl.get_lrps_for_datapath(row.datapath) for port_lrp in port_lrps: if port_lrp in self.ovn_local_lrps.keys(): evpn_info = self.sb_idl.get_evpn_info_from_port_name(port_lrp) if not evpn_info: LOG.debug("No EVPN information for LRP Port %s. " "Not exposing IPs: %s.", port_lrp, ips) continue LOG.info("Add BGP route for tenant IP %s on chassis %s", ips, self.chassis) lo_name = constants.OVN_EVPN_LO_PREFIX + str(evpn_info['vni']) linux_net.add_ips_to_dev( lo_name, ips, clear_local_route_at_table=evpn_info['vni']) self._ovn_exposed_evpn_ips.setdefault( lo_name, []).extend(ips) @lockutils.synchronized('evpn') def withdraw_remote_ip(self, ips, row): if self.sb_idl.is_provider_network(row.datapath): return port_lrps = self.sb_idl.get_lrps_for_datapath(row.datapath) for port_lrp in port_lrps: if port_lrp in self.ovn_local_lrps.keys(): evpn_info = self.sb_idl.get_evpn_info_from_port_name(port_lrp) if not evpn_info: LOG.debug("No EVPN information for LRP Port %s. " "Not withdrawing IPs: %s.", port_lrp, ips) continue LOG.info("Delete BGP route for tenant IP %s on chassis %s", ips, self.chassis) lo_name = constants.OVN_EVPN_LO_PREFIX + str(evpn_info['vni']) linux_net.del_ips_from_dev(lo_name, ips) @lockutils.synchronized('evpn') def expose_subnet(self, row): evpn_info = self.sb_idl.get_evpn_info(row) ip = self.sb_idl.get_ip_from_port_peer(row) if not evpn_info: LOG.debug("No EVPN information for LRP Port %s. " "Not exposing IPs: %s.", row.logical_port, ip) return lrp_logical_port = 'lrp-' + row.logical_port lrp_datapath = self.sb_idl.get_port_datapath(lrp_logical_port) cr_lrp = self.sb_idl.is_router_gateway_on_chassis(lrp_datapath, self.chassis) if not cr_lrp: return LOG.info("Add IP Routes for network %s on chassis %s", ip, self.chassis) self.ovn_local_lrps[lrp_logical_port] = { 'datapath': lrp_datapath, 'ip': ip } cr_lrp_info = self.ovn_local_cr_lrps.get(cr_lrp, {}) cr_lrp_datapath = cr_lrp_info.get('provider_datapath') if not cr_lrp_datapath: LOG.info("Subnet not connected to the provider network. " "No need to expose it through EVPN") return if (evpn_info['bgp_as'] != cr_lrp_info.get('bgp_as') or evpn_info['vni'] != cr_lrp_info.get('vni')): LOG.error("EVPN information at router port (vni: %s, as: %s) does" " not match with information at subnet gateway port:" " %s", cr_lrp_info.get('vni'), cr_lrp_info.get('bgp_as'), evpn_info) return cr_lrp_ips = [ip_address.split('/')[0] for ip_address in cr_lrp_info.get('ips', [])] datapath_bridge, vlan_tag = self._get_bridge_for_datapath( cr_lrp_datapath) self._expose_subnet(ip, cr_lrp_ips, cr_lrp_info, datapath_bridge, vlan_tag, row.datapath) def _expose_subnet(self, router_interface, cr_lrp_ips, cr_lrp_info, datapath_bridge, vlan_tag, network_datapath): router_interface_ip_version = linux_net.get_ip_version( router_interface) if vlan_tag: dev = cr_lrp_info['vlan'] dev_ovs = dev strip_vlan = True else: dev = cr_lrp_info['veth_vrf'] dev_ovs = cr_lrp_info['veth_ovs'] strip_vlan = False for cr_lrp_ip in cr_lrp_ips: if (linux_net.get_ip_version(cr_lrp_ip) == router_interface_ip_version): linux_net.add_ip_route( self._ovn_routing_tables_routes, router_interface.split("/")[0], cr_lrp_info['vni'], dev, mask=router_interface.split("/")[1], via=cr_lrp_ip) break if router_interface_ip_version == constants.IP_VERSION_6: net_ip = '{}'.format(ipaddress.IPv6Network( router_interface, strict=False)) else: net_ip = '{}'.format(ipaddress.IPv4Network( router_interface, strict=False)) # NOTE(ltomasbo): strip_vlan is used for subnets/routers associated to # provider vlan networks assuming the EVPN VXLAN header is replacing # the vlan id in the fabric. If that is not the case, we could simply # set this to False in all the cases and have the traffic sent with # both vxlan header (for the EVPN) plus the vlan header (related to # the provider vlan id being used) ovs.ensure_evpn_ovs_flow(datapath_bridge, constants.OVS_VRF_RULE_COOKIE, cr_lrp_info['mac'], dev_ovs, dev, net_ip, strip_vlan=strip_vlan) # Check if there are VMs on the network # and if so expose the route if not network_datapath: return ports = self.sb_idl.get_ports_on_datapath( network_datapath) for port in ports: if (port.type not in (constants.OVN_VM_VIF_PORT_TYPE, constants.OVN_VIRTUAL_VIF_PORT_TYPE) or (port.type == constants.OVN_VM_VIF_PORT_TYPE and not port.chassis)): continue try: port_ips = port.mac[0].strip().split(' ')[1:] except IndexError: continue for port_ip in port_ips: # Only adding the port ips that match the lrp # IP version port_ip_version = linux_net.get_ip_version(port_ip) if port_ip_version == router_interface_ip_version: linux_net.add_ips_to_dev( cr_lrp_info['lo'], [port_ip], clear_local_route_at_table=cr_lrp_info['vni']) self._ovn_exposed_evpn_ips.setdefault( cr_lrp_info['lo'], []).extend([port_ip]) @lockutils.synchronized('evpn') def withdraw_subnet(self, row): lrp_logical_port = 'lrp-' + row.logical_port lrp_datapath = self.ovn_local_lrps.get(lrp_logical_port, {}).get( 'datapath') ip = self.ovn_local_lrps.get(lrp_logical_port, {}).get('ip') if not lrp_datapath: return cr_lrp = self.sb_idl.is_router_gateway_on_chassis(lrp_datapath, self.chassis) if not cr_lrp: return LOG.info("Delete IP Routes for network %s on chassis %s", ip, self.chassis) cr_lrp_info = self.ovn_local_cr_lrps.get(cr_lrp, {}) cr_lrp_datapath = cr_lrp_info.get('provider_datapath') if not cr_lrp_datapath: LOG.info("Subnet not connected to the provider network. " "No need to withdraw it from EVPN") return cr_lrp_ips = [ip_address.split('/')[0] for ip_address in cr_lrp_info.get('ips', [])] datapath_bridge, vlan_tag = self._get_bridge_for_datapath( cr_lrp_datapath) if vlan_tag: dev = cr_lrp_info['vlan'] else: dev = cr_lrp_info['veth_vrf'] ip_version = linux_net.get_ip_version(ip) for cr_lrp_ip in cr_lrp_ips: if linux_net.get_ip_version(cr_lrp_ip) == ip_version: linux_net.del_ip_route( self._ovn_routing_tables_routes, ip.split("/")[0], cr_lrp_info['vni'], dev, mask=ip.split("/")[1], via=cr_lrp_ip) if (linux_net.get_ip_version(cr_lrp_ip) == constants.IP_VERSION_6): net = ipaddress.IPv6Network(ip, strict=False) else: net = ipaddress.IPv4Network(ip, strict=False) break ovs.remove_evpn_network_ovs_flow(datapath_bridge, constants.OVS_VRF_RULE_COOKIE, cr_lrp_info['mac'], '{}'.format(net)) # Check if there are VMs on the network # and if so withdraw the routes vms_on_net = linux_net.get_exposed_ips_on_network( cr_lrp_info['lo'], net) linux_net.delete_exposed_ips(vms_on_net, cr_lrp_info['lo']) try: del self.ovn_local_lrps[lrp_logical_port] except KeyError: LOG.debug("Router Interface port already cleanup from the agent " "%s", lrp_logical_port) def _ensure_evpn_devices(self, datapath_bridge, vni, vlan_tag): '''Create the needed devices for EVPN connectivity This method creates and associate the needed devices for EVPN connectivity. It creates: - VRF device - Linux Bridge device, associated to the VRF - VXLAN device, using loopback IP, associate to the bridge - Dummy device to expose the IPs, associated to the VRF - If vlan_tag, create vlan device on OVS bridge, associated to the VRF - If no vlan_tag, create veth pair, one end associated to the VRF param datapath_bridge: OVS bridge to connect the vlan device param vni: VNI number to use for vxlan tunnel ids and vrf routing table param vlan_tag: vlan id to use for connectivity return: a namedtuple with the name of the devices created: vrf_name, lo_name, bridge_name, vxlan_name, veth_vrf, veth_ovs, and vlan_name. ''' # ensure vrf device. # NOTE: It uses vni id as table number vrf_name = constants.OVN_EVPN_VRF_PREFIX + str(vni) linux_net.ensure_vrf(vrf_name, vni) # ensure bridge device bridge_name = constants.OVN_EVPN_BRIDGE_PREFIX + str(vni) linux_net.ensure_bridge(bridge_name) # connect bridge to vrf linux_net.set_master_for_device(bridge_name, vrf_name) # ensure vxlan device vxlan_name = constants.OVN_EVPN_VXLAN_PREFIX + str(vni) local_ip = CONF.evpn_local_ip if not local_ip: local_nic = 'lo' prefixlen_filter = 32 # assuming IPv4 if CONF.evpn_nic: local_nic = CONF.evpn_nic prefixlen_filter = False # NOTE(ltomasbo): assuming only 1 IP on the device with /32 prefix local_ip = linux_net.get_nic_ip(local_nic, prefixlen_filter)[0] if not local_ip: LOG.error("EVPN device must have an IP associated for the " "VXLAN local ip") return None, None linux_net.ensure_vxlan(vxlan_name, vni, local_ip, CONF.evpn_udp_dstport) # connect vxlan to bridge linux_net.set_master_for_device(vxlan_name, bridge_name) # ensure dummy lo interface lo_name = constants.OVN_EVPN_LO_PREFIX + str(vni) linux_net.ensure_dummy_device(lo_name) # connect dummy to vrf linux_net.set_master_for_device(lo_name, vrf_name) if vlan_tag: vlan_name = constants.OVN_EVPN_VLAN_PREFIX + str(vni) # add vlan port to OVS bridge ovs.add_vlan_port_to_ovs_bridge(datapath_bridge, vlan_name, vlan_tag) linux_net.set_device_status(vlan_name, constants.LINK_UP) # connect vlan to vrf linux_net.set_master_for_device(vlan_name, vrf_name) # ensure proxy NDP is enabled for ipv6 traffic linux_net.enable_proxy_ndp(vlan_name) return EVPN_INFO(vrf_name, lo_name, bridge_name, vxlan_name, None, None, vlan_name) else: # ensure veth-pair interfaces veth_vrf = constants.OVN_EVPN_VETH_VRF_PREFIX + str(vni) veth_ovs = constants.OVN_EVPN_VETH_OVS_PREFIX + str(vni) linux_net.ensure_veth(veth_vrf, veth_ovs) # connect veth to vrf linux_net.set_master_for_device(veth_vrf, vrf_name) return EVPN_INFO(vrf_name, lo_name, bridge_name, vxlan_name, veth_vrf, veth_ovs, None) def _remove_evpn_devices(self, vni): vrf_name = constants.OVN_EVPN_VRF_PREFIX + str(vni) bridge_name = constants.OVN_EVPN_BRIDGE_PREFIX + str(vni) vxlan_name = constants.OVN_EVPN_VXLAN_PREFIX + str(vni) lo_name = constants.OVN_EVPN_LO_PREFIX + str(vni) veth_name = constants.OVN_EVPN_VETH_VRF_PREFIX + str(vni) vlan_name = constants.OVN_EVPN_VLAN_PREFIX + str(vni) for device in [lo_name, vrf_name, bridge_name, vxlan_name, veth_name, vlan_name]: linux_net.delete_device(device) def _connect_evpn_to_ovn(self, vrf, veth_vrf, veth_ovs, ips, datapath_bridge, vni, vlan, vlan_tag): # NOTE(ltomasbo): vlan device is already attached to ovs bridge # when created if not vlan_tag: # add veth to ovs bridge ovs.add_device_to_ovs_bridge(veth_ovs, datapath_bridge) # add route for ip to ovs provider bridge (at the vrf routing table) for ip in ips: ip_without_mask = ip.split("/")[0] if vlan_tag: # ip route add GW_PORT_IP dev VLAN_DEVICE table VRF_TABLE_ID linux_net.add_ip_route( self._ovn_routing_tables_routes, ip_without_mask, vni, vlan) # add proxy ndp config for ipv6 if (linux_net.get_ip_version(ip_without_mask) == constants.IP_VERSION_6): linux_net.add_ndp_proxy(ip, vlan) else: linux_net.add_ip_route( self._ovn_routing_tables_routes, ip_without_mask, vni, veth_vrf) # add proxy ndp config for ipv6 if (linux_net.get_ip_version(ip_without_mask) == constants.IP_VERSION_6): linux_net.add_ndp_proxy(ip, datapath_bridge) # add unreachable route to vrf linux_net.add_unreachable_route(vrf) def _disconnect_evpn_from_ovn(self, vni, datapath_bridge, ips, vlan_tag=None, cleanup_ndp_proxy=True): if vlan_tag: # remove vlan from ovs bridge device = constants.OVN_EVPN_VLAN_PREFIX + str(vni) else: # remove veth from ovs bridge device = constants.OVN_EVPN_VETH_OVS_PREFIX + str(vni) ovs.del_device_from_ovs_bridge(device, datapath_bridge) linux_net.delete_routes_from_table(vni) if cleanup_ndp_proxy: for ip in ips: if linux_net.get_ip_version(ip) == constants.IP_VERSION_6: linux_net.del_ndp_proxy(ip, datapath_bridge) def _remove_extra_vrfs(self): vrfs, los, bridges, vxlans, veths, vlans = ([], [], [], [], [], []) for cr_lrp_info in self.ovn_local_cr_lrps.values(): vrfs.append(cr_lrp_info['vrf']) los.append(cr_lrp_info['lo']) bridges.append(cr_lrp_info['bridge']) vxlans.append(cr_lrp_info['vxlan']) veths.append(cr_lrp_info['veth_vrf']) vlans.append(cr_lrp_info['vlan']) filter_out = ["{}.{}".format(key, value[0]['vlan']) for key, value in self._ovn_routing_tables_routes.items() if value[0]['vlan']] interfaces = linux_net.get_interfaces(filter_out) for interface in interfaces: if (interface.startswith(constants.OVN_EVPN_VRF_PREFIX) and interface not in vrfs): linux_net.delete_device(interface) elif (interface.startswith(constants.OVN_EVPN_LO_PREFIX) and interface not in los): linux_net.delete_device(interface) elif (interface.startswith(constants.OVN_EVPN_BRIDGE_PREFIX) and (interface not in bridges and interface != constants.OVN_INTEGRATION_BRIDGE and interface not in set(self.ovn_bridge_mappings.values()))): linux_net.delete_device(interface) elif (interface.startswith(constants.OVN_EVPN_VXLAN_PREFIX) and interface not in vxlans): linux_net.delete_device(interface) elif (interface.startswith(constants.OVN_EVPN_VETH_VRF_PREFIX) and interface not in veths): linux_net.delete_device(interface) ovs.del_device_from_ovs_bridge(interface) elif (interface.startswith(constants.OVN_EVPN_VLAN_PREFIX) and interface not in vlans): ovs.del_device_from_ovs_bridge(interface) def _remove_extra_routes(self): table_ids = self._get_table_ids() vrf_routes = linux_net.get_routes_on_tables(table_ids) if not vrf_routes: return # remove from vrf_routes the routes that should be kept for device, routes_info in self._ovn_routing_tables_routes.items(): for route_info in routes_info: oif = linux_net.get_interface_index(device) if 'gateway' in route_info['route'].keys(): # subnet route possible_matchings = [ r for r in vrf_routes if (r.get('dst') == route_info['route']['dst'] and r['dst_len'] == route_info['route']['dst_len'] and r.get('gateway') == ( route_info['route']['gateway']) and r['table'] == route_info['route']['table'])] else: # cr-lrp possible_matchings = [ r for r in vrf_routes if (r.get('dst') == route_info['route']['dst'] and r['dst_len'] == route_info['route']['dst_len'] and r.get('oif') == oif and r['table'] == route_info['route']['table'])] for r in possible_matchings: vrf_routes.remove(r) linux_net.delete_ip_routes(vrf_routes) def _remove_extra_ovs_flows(self): cr_lrp_mac_mappings = self._get_cr_lrp_mac_mapping() cookie_id = "cookie={}/-1".format(constants.OVS_VRF_RULE_COOKIE) for bridge in set(self.ovn_bridge_mappings.values()): current_flows = ovs.get_bridge_flows(bridge, filter_=cookie_id) for flow in current_flows: flow_info = ovs.get_flow_info(flow) if not flow_info.get('mac'): ovs.del_flow(flow, bridge, constants.OVS_VRF_RULE_COOKIE) elif flow_info['mac'] not in cr_lrp_mac_mappings.keys(): ovs.del_flow(flow, bridge, constants.OVS_VRF_RULE_COOKIE) elif flow_info['port']: if (not flow_info.get('nw_src') and not flow_info.get('ipv6_src')): ovs.del_flow(flow, bridge, constants.OVS_VRF_RULE_COOKIE) else: dev_info = cr_lrp_mac_mappings[flow_info['mac']] if dev_info.get('vlan'): dev = dev_info['vlan'] dev_ovs = dev else: dev = dev_info['veth_vrf'] dev_ovs = dev_info['veth_ovs'] dev_ovs_port = ovs.get_device_port_at_ovs( dev_ovs) if dev_ovs_port != flow_info['port']: ovs.del_flow(flow, bridge, constants.OVS_VRF_RULE_COOKIE) nw_src_ip = nw_src_mask = None matching_dst = False if flow_info.get('nw_src'): nw_src_ip = flow_info['nw_src'].split('/')[0] nw_src_mask = int( flow_info['nw_src'].split('/')[1]) elif flow_info.get('ipv6_src'): nw_src_ip = flow_info['ipv6_src'].split('/')[0] nw_src_mask = int( flow_info['ipv6_src'].split('/')[1]) for route_info in self._ovn_routing_tables_routes[ dev]: if (route_info['route']['dst'] == nw_src_ip and route_info['route'][ 'dst_len'] == nw_src_mask): matching_dst = True if not matching_dst: ovs.del_flow(flow, bridge, constants.OVS_VRF_RULE_COOKIE) def _remove_extra_exposed_ips(self): for lo, ips in self._ovn_exposed_evpn_ips.items(): exposed_ips_on_device = linux_net.get_exposed_ips(lo) for ip in exposed_ips_on_device: if ip not in ips: linux_net.del_ips_from_dev(lo, [ip]) def _get_table_ids(self): table_ids = [] for cr_lrp_info in self.ovn_local_cr_lrps.values(): table_ids.append(cr_lrp_info['vni']) return table_ids def _get_cr_lrp_mac_mapping(self): mac_mappings = {} for cr_lrp_info in self.ovn_local_cr_lrps.values(): mac_mappings[cr_lrp_info['mac']] = { 'veth_vrf': cr_lrp_info['veth_vrf'], 'veth_ovs': cr_lrp_info['veth_ovs'], 'vlan': cr_lrp_info['vlan']} return mac_mappings ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py000066400000000000000000000466341460327367600305070ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import collections import dataclasses import ipaddress import threading from oslo_concurrency import lockutils from oslo_config import cfg from oslo_log import log as logging from ovn_bgp_agent import constants from ovn_bgp_agent.drivers import driver_api from ovn_bgp_agent.drivers.openstack.utils import bgp as bgp_utils 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 ovn from ovn_bgp_agent.drivers.openstack.utils import ovs from ovn_bgp_agent.drivers.openstack.watchers import bgp_watcher as watcher from ovn_bgp_agent import exceptions as agent_exc from ovn_bgp_agent.utils import linux_net CONF = cfg.CONF LOG = logging.getLogger(__name__) OVN_TABLES = ["Port_Binding", "Chassis", "Datapath_Binding", "Chassis_Private"] @dataclasses.dataclass(frozen=True, eq=True) class HashedRoute: network: str prefix_len: int dst: str class OVNBGPStretchedL2Driver(driver_api.AgentDriverBase): def __init__(self): self.ovn_local_cr_lrps = {} self.vrf_routes = set() self.ovn_routing_tables_routes = collections.defaultdict() self.allowed_address_scopes = set(CONF.address_scopes or []) self.propagated_lrp_ports = {} self._sb_idl = None self._post_fork_event = threading.Event() @property def sb_idl(self): if not self._sb_idl: self._post_fork_event.wait() return self._sb_idl @sb_idl.setter def sb_idl(self, val): self._sb_idl = val def start(self): self.ovs_idl = ovs.OvsIdl() self.ovs_idl.start(CONF.ovsdb_connection) # Base BGP configuration # Ensure FRR is configured to leak only kernel routes by default frr.set_default_redistribute(['kernel']) bgp_utils.ensure_base_bgp_configuration() # Clear vrf routing table if CONF.clear_vrf_routes_on_startup: linux_net.delete_routes_from_table(CONF.bgp_vrf_table_id) self.chassis = self.ovs_idl.get_own_chassis_id() self.ovn_remote = self.ovs_idl.get_ovn_remote() LOG.debug("Loaded chassis %s.", self.chassis) if self.allowed_address_scopes: LOG.debug("Configured allowed address scopes: %s", ", ".join(self.allowed_address_scopes)) self._post_fork_event.clear() events = self._get_events() self.sb_idl = ovn.OvnSbIdl( self.ovn_remote, chassis=self.chassis, tables=OVN_TABLES, events=events, ).start() # Now IDL connections can be safely used self._post_fork_event.set() def _get_events(self): return { watcher.SubnetRouterAttachedEvent(self), watcher.SubnetRouterUpdateEvent(self), watcher.SubnetRouterDetachedEvent(self), watcher.PortBindingChassisCreatedEvent(self), watcher.PortBindingChassisDeletedEvent(self), } @lockutils.synchronized('bgp') def frr_sync(self): LOG.debug("Ensuring VRF configuration for advertising routes") # Base BGP configuration # Ensure FRR is configured to leak the routes bgp_utils.ensure_base_bgp_configuration() @lockutils.synchronized("bgp") def sync(self): self.ovn_local_cr_lrps = {} self.ovn_routing_tables_routes = collections.defaultdict() self.vrf_routes = set() self.propagated_lrp_ports = {} LOG.debug("Syncing current routes.") # Get all current exposed routes vrf_routes = linux_net.get_routes_on_tables([CONF.bgp_vrf_table_id]) for cr_lrp_port in self.sb_idl.get_cr_lrp_ports(): if (not cr_lrp_port.mac or len(cr_lrp_port.mac[0].strip().split(" ")) <= 1): continue self._expose_cr_lrp(cr_lrp_port.mac[0].strip().split(" ")[1:], cr_lrp_port) # remove all left over routes delete_routes = [] for route in vrf_routes: r = HashedRoute( network=route.dst, prefix_len=route.dst_len, dst=route.gateway if route.gateway else None) if r not in self.vrf_routes: delete_routes.append(route) linux_net.delete_ip_routes(delete_routes) def _add_route(self, network, prefix_len, dst=None): LOG.debug("Adding BGP route for Network %s/%d via %s", network, prefix_len, dst) linux_net.add_ip_route( self.ovn_routing_tables_routes, network, CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=prefix_len, via=dst) r = HashedRoute( network=network, prefix_len=prefix_len, dst=dst) self.vrf_routes.add(r) LOG.debug("Added BGP route for Network %s/%d via %s", network, prefix_len, dst) def _del_route(self, network, prefix_len, dst=None): LOG.debug("Deleting BGP route for Network %s/%d via %s", network, prefix_len, dst) linux_net.del_ip_route( self.ovn_routing_tables_routes, network, CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=prefix_len, via=dst) r = HashedRoute( network=network, prefix_len=prefix_len, dst=dst) if r in self.vrf_routes: self.vrf_routes.remove(r) LOG.debug("Deleted BGP route for Network %s/%d via %s", network, prefix_len, dst) def _address_scope_allowed(self, scope1, scope2, ip_version): if not self.allowed_address_scopes: # No address scopes to filter on => announce everything return True if scope1[ip_version] != scope2[ip_version]: # Not the same address scope => don't announce return False if scope1[ip_version] not in self.allowed_address_scopes: # This address scope does not match => don't announce return False return True @lockutils.synchronized("bgp") def expose_subnet(self, ip, row): try: cr_lrp = self.sb_idl.is_router_gateway_on_any_chassis(row.datapath) except agent_exc.DatapathNotFound: LOG.debug("Port %s not being exposed as its datapath %s was " "removed", row.logical_port, row.datapath) return if not cr_lrp: return self._ensure_network_exposed(row, cr_lrp.logical_port) @lockutils.synchronized("bgp") def update_subnet(self, old, row): try: cr_lrp = self.sb_idl.is_router_gateway_on_any_chassis(row.datapath) except agent_exc.DatapathNotFound: LOG.debug("Port %s not being updated as its datapath %s was " "removed", row.logical_port, row.datapath) return if (not cr_lrp or not cr_lrp.mac or len(cr_lrp.mac[0].strip().split(" ")) <= 1): return current_ips = row.mac[0].strip().split(" ")[1:] previous_ips = ( old.mac[0].strip().split(" ")[1:] if old.mac or len(old.mac[0].strip().split(" ")) > 1 else [] ) add_ips = list( filter(lambda ip: ip not in previous_ips, current_ips)) delete_ips = list( filter(lambda ip: ip not in current_ips, previous_ips)) self._update_network(row, cr_lrp.logical_port, add_ips, delete_ips) @lockutils.synchronized("bgp") def withdraw_subnet(self, ip, row): port_info = self.propagated_lrp_ports.get(row.logical_port) if not port_info: return self._withdraw_subnet(port_info, port_info["cr_lrp"]) gateway = self.ovn_local_cr_lrps.get(port_info["cr_lrp"]) if gateway and row.logical_port in gateway["lrp_ports"]: gateway["lrp_ports"].remove(row.logical_port) self.propagated_lrp_ports.pop(row.logical_port) def _withdraw_subnet(self, port_info, cr_lrp): gateway = self.ovn_local_cr_lrps.get(cr_lrp) if not gateway: # If we dont have it cached then its either not existing or # or we got an event while starting up which then the sync # function can fix. return gateway_ips = gateway["ips"] subnets = [ ipaddress.ip_network(subnet) for subnet in port_info["subnets"]] for gateway_ip in gateway_ips: for subnet in subnets: if gateway_ip.version != subnet.version: continue self._del_route( network=str(subnet.network_address), prefix_len=subnet.prefixlen, dst=str(gateway_ip.ip)) # Check if can delete the link-local route exposed_routes = linux_net.get_exposed_routes_on_network( [CONF.bgp_vrf_table_id], gateway_ip.network) if not exposed_routes: self._del_route( network=str(gateway_ip.network.network_address), prefix_len=gateway_ip.network.prefixlen) @lockutils.synchronized('bgp') def withdraw_ip(self, ips, row, associated_port=None): if not (row.type == constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE and row.logical_port.startswith("cr-")): return self._withdraw_cr_lrp(ips, row) def _withdraw_cr_lrp(self, ips, row): if self.allowed_address_scopes: # Validate address scopes address_scopes = self.ovn_local_cr_lrps[row.logical_port][ "address_scopes"] if not any([ scope in self.allowed_address_scopes for scope in address_scopes.values()]): return # Check if there are networks attached to the router, # and if so, remove them locally lrp_ports = self.ovn_local_cr_lrps[row.logical_port]["lrp_ports"] for lrp_logical_port in lrp_ports: port_info = self.propagated_lrp_ports.get(lrp_logical_port) if not port_info: continue # withdraw network self._withdraw_subnet(port_info, row.logical_port) self.propagated_lrp_ports.pop(lrp_logical_port, None) self.ovn_local_cr_lrps.pop(row.logical_port, None) @lockutils.synchronized("bgp") def expose_ip(self, ips, row, associated_port=None): if not (row.type == constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE and row.logical_port.startswith("cr-")): return self._expose_cr_lrp(ips, row) def _expose_cr_lrp(self, ips, row): LOG.debug("Adding BGP route for CR-LRP Port %s", row.logical_port) # Keeping information about the associated network for # tenant network advertisement self.ovn_local_cr_lrps[row.logical_port] = { "ips": [ipaddress.ip_interface(ip) for ip in ips], "address_scopes": {}, "lrp_ports": set(), } if self.allowed_address_scopes: # Validate address scopes patch_port = row.logical_port.split("cr-lrp-")[1] port = self.sb_idl.get_port_by_name(patch_port) if not port: LOG.error("Patchport %s for CR-LRP %s missing, skipping.", patch_port, row.logical_port) return address_scopes = driver_utils.get_addr_scopes(port) self.ovn_local_cr_lrps[row.logical_port][ "address_scopes"] = address_scopes if not any([ scope in self.allowed_address_scopes for scope in address_scopes.values()]): return # Check if there are networks attached to the router, # and if so, add the needed routes lrp_ports = self.sb_idl.get_lrp_ports_for_router(row.datapath) for lrp in lrp_ports: if ( lrp.chassis or not lrp.logical_port.startswith("lrp-") or "chassis-redirect-port" in lrp.options.keys() ): continue # expose network self._ensure_network_exposed(lrp, row.logical_port) def _update_network(self, router_port, gateway_port, add_ips, delete_ips): gateway = self.ovn_local_cr_lrps.get(gateway_port) if not gateway: # If we dont have it cached then its either not existing or # or we got an event while starting up which then the sync # function can fix. return gateway_ips = gateway["ips"] if (not router_port.mac or len(router_port.mac[0].strip().split(" ")) <= 1): return # get all ips from the router port ips_to_add = [ipaddress.ip_interface(ip) for ip in add_ips] ips_to_delete = [ipaddress.ip_interface(ip) for ip in delete_ips] for router_ip in ips_to_add + ips_to_delete: if router_ip in gateway_ips: return address_scopes = None if self.allowed_address_scopes: patch_port = router_port.logical_port.split("lrp-")[1] port = self.sb_idl.get_port_by_name(patch_port) if not port: LOG.error("Patchport %s for CR-LRP %s missing, skipping.", patch_port, gateway_port) return address_scopes = driver_utils.get_addr_scopes(port) # if we should filter on address scopes and this port has no # address scopes set we do not need to go further if not any(address_scopes.values()): return subnets = set() for gateway_ip in gateway_ips: for router_ip in ips_to_add: if gateway_ip.version != router_ip.version: continue if not self._address_scope_allowed( gateway["address_scopes"], address_scopes, router_ip.version): continue # Add link-local route self._add_route( network=str(gateway_ip.network.network_address), prefix_len=gateway_ip.network.prefixlen) # add route for the tenant network pointing to the # gateway ip self._add_route( network=str(router_ip.network.network_address), prefix_len=router_ip.network.prefixlen, dst=str(gateway_ip.ip)) subnets.add(str(router_ip.network)) for router_ip in ips_to_delete: if gateway_ip.version != router_ip.version: continue if not self._address_scope_allowed( gateway["address_scopes"], address_scopes, router_ip.version): continue self._del_route( network=str(router_ip.network.network_address), prefix_len=router_ip.network.prefixlen, dst=str(gateway_ip.ip)) # We only need to check this if we really deleted a route for # a tenant network if ips_to_delete: # Check if can delete the link-local route exposed_routes = linux_net.get_exposed_routes_on_network( [CONF.bgp_vrf_table_id], gateway_ip.network ) if not exposed_routes: self._del_route( network=str(gateway_ip.network.network_address), prefix_len=gateway_ip.network.prefixlen) self.ovn_local_cr_lrps[gateway_port]["lrp_ports"].add( router_port.logical_port) self.propagated_lrp_ports[router_port.logical_port] = { "cr_lrp": gateway_port, "subnets": subnets } def _ensure_network_exposed(self, router_port, gateway_port): gateway = self.ovn_local_cr_lrps.get(gateway_port) if not gateway: # If we dont have it cached then its either not existing or # or we got an event while starting up which then the sync # function can fix. return gateway_ips = gateway["ips"] if (not router_port.mac or len(router_port.mac[0].strip().split(" ")) <= 1): return # get all ips from the router port router_ips = [ ipaddress.ip_interface(ip) for ip in router_port.mac[0].strip().split(" ")[1:]] for router_ip in router_ips: if router_ip in gateway_ips: return address_scopes = None if self.allowed_address_scopes: patch_port = router_port.logical_port.split("lrp-")[1] port = self.sb_idl.get_port_by_name(patch_port) if not port: LOG.error("Patchport %s for CR-LRP %s missing, skipping.", patch_port, gateway_port) return address_scopes = driver_utils.get_addr_scopes(port) # if we have address scopes configured and none of them matches # for this port, we can skip further processing if not any(address_scopes.values()): return subnets = set() for gateway_ip in gateway_ips: for router_ip in router_ips: if gateway_ip.version != router_ip.version: continue if not self._address_scope_allowed( gateway["address_scopes"], address_scopes, router_ip.version): continue # Add link-local route self._add_route( network=str(gateway_ip.network.network_address), prefix_len=gateway_ip.network.prefixlen) # add route for the tenant network pointing to the # gateway ip self._add_route( network=str(router_ip.network.network_address), prefix_len=router_ip.network.prefixlen, dst=str(gateway_ip.ip)) subnets.add(str(router_ip.network)) if subnets: self.ovn_local_cr_lrps[gateway_port]["lrp_ports"].add( router_port.logical_port) self.propagated_lrp_ports[router_port.logical_port] = { "cr_lrp": gateway_port, "subnets": subnets } @lockutils.synchronized("bgp") def expose_remote_ip(self, ip_address): raise NotImplementedError() @lockutils.synchronized("bgp") def withdraw_remote_ip(self, ip_address): raise NotImplementedError() ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/utils/000077500000000000000000000000001460327367600236115ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/utils/__init__.py000066400000000000000000000000001460327367600257100ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/utils/bgp.py000066400000000000000000000034031460327367600247330ustar00rootroot00000000000000# Copyright 2023 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_config import cfg from oslo_log import log as logging from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.utils import frr from ovn_bgp_agent.utils import linux_net CONF = cfg.CONF LOG = logging.getLogger(__name__) def announce_ips(port_ips): linux_net.add_ips_to_dev(CONF.bgp_nic, port_ips) def withdraw_ips(port_ips): linux_net.del_ips_from_dev(CONF.bgp_nic, port_ips) def ensure_base_bgp_configuration(template=frr.LEAK_VRF_TEMPLATE): if CONF.exposing_method not in [constants.EXPOSE_METHOD_UNDERLAY, constants.EXPOSE_METHOD_DYNAMIC, constants.EXPOSE_METHOD_OVN]: return # Create VRF linux_net.ensure_vrf(CONF.bgp_vrf, CONF.bgp_vrf_table_id) # If we expose subnet routes, we should add kernel routes too. if CONF.advertisement_method_tenant_networks == 'subnet': frr.set_default_redistribute(['connected', 'kernel']) # Ensure FRR is configure to leak the routes frr.vrf_leak(CONF.bgp_vrf, CONF.bgp_AS, CONF.bgp_router_id, template=template) # Create OVN dummy device linux_net.ensure_ovn_device(CONF.bgp_nic, CONF.bgp_vrf) ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/utils/driver_utils.py000066400000000000000000000070111460327367600266750ustar00rootroot00000000000000# Copyright 2022 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import ipaddress from oslo_log import log as logging from ovn_bgp_agent import constants from ovn_bgp_agent.utils import linux_net LOG = logging.getLogger(__name__) def is_ipv6_gua(ip): if linux_net.get_ip_version(ip) != constants.IP_VERSION_6: return False ipv6 = ipaddress.IPv6Address(ip.split('/')[0]) if ipv6.is_global: return True return False def get_addr_scopes(port): return { constants.IP_VERSION_4: port.external_ids.get( constants.SUBNET_POOL_ADDR_SCOPE4), constants.IP_VERSION_6: port.external_ids.get( constants.SUBNET_POOL_ADDR_SCOPE6), } def get_port_chassis(port, chassis, default_port_type=constants.OVN_VM_VIF_PORT_TYPE): # row.options['requested-chassis'] superseeds the id in external_ids. # Since it is not used for virtual ports by ovn, this option will be # ignored for virtual ports. # since 'old' rows could be used, it will not hold the type information # if that is the case, please supply a default in the arguments. port_type = getattr(port, 'type', default_port_type) if (port_type not in [constants.OVN_VIRTUAL_VIF_PORT_TYPE] and hasattr(port, 'options') and port.options.get(constants.OVN_REQUESTED_CHASSIS)): # requested-chassis can be a comma separated list, # so lets only return our chassis if it is a list, to be able # to do a == equal comparison req_chassis = port.options[constants.OVN_REQUESTED_CHASSIS] if chassis in req_chassis.split(','): req_chassis = chassis return req_chassis.split(',')[0], constants.OVN_CHASSIS_AT_OPTIONS elif (hasattr(port, 'external_ids') and port.external_ids.get(constants.OVN_HOST_ID_EXT_ID_KEY)): return ( port.external_ids[constants.OVN_HOST_ID_EXT_ID_KEY], constants.OVN_CHASSIS_AT_EXT_IDS ) return None, None def check_name_prefix(entity, prefix): try: return entity.name.startswith(prefix) except AttributeError: return False def is_pf_lb(lb): return check_name_prefix(lb, constants.OVN_LB_PF_NAME_PREFIX) def get_prefixes_from_ips(ips: 'list[str]') -> 'list[str]': '''Return the network address for any given ip (with mask) For a list like ['192.168.0.1/24'] it will return ['192.168.0.0/24'] ''' return ['/'.join([ipaddress.ip_network(ip, strict=False)[0].compressed, ip.split('/')[-1]]) for ip in ips] def remove_port_from_ip(ip_address): last_colon_index = ip_address.rfind(':') # no port if last_colon_index == -1: return ip_address # check if right side from index is a digit, in positive case remove it. # For IPv6 it will come on format [ipv6]:port, so will also remove # correctly just only the port if ip_address[last_colon_index + 1:].isdigit(): return ip_address[:last_colon_index] return ip_address ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/utils/frr.py000066400000000000000000000105721460327367600247610ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import tempfile from jinja2 import Template from oslo_log import log as logging from ovn_bgp_agent import constants import ovn_bgp_agent.privileged.vtysh LOG = logging.getLogger(__name__) DEFAULT_REDISTRIBUTE = {'connected'} ADD_VRF_TEMPLATE = ''' vrf {{ vrf_name }} vni {{ vni }} exit-vrf router bgp {{ bgp_as }} vrf {{ vrf_name }} address-family ipv4 unicast {% for redist in redistribute %} redistribute {{ redist }} {% endfor %} exit-address-family address-family ipv6 unicast {% for redist in redistribute %} redistribute {{ redist }} {% endfor %} exit-address-family address-family l2vpn evpn advertise ipv4 unicast advertise ipv6 unicast exit-address-family ''' DEL_VRF_TEMPLATE = ''' no vrf {{ vrf_name }} no router bgp {{ bgp_as }} vrf {{ vrf_name }} ''' LEAK_VRF_TEMPLATE = ''' router bgp {{ bgp_as }} address-family ipv4 unicast import vrf {{ vrf_name }} exit-address-family address-family ipv6 unicast import vrf {{ vrf_name }} exit-address-family router bgp {{ bgp_as }} vrf {{ vrf_name }} bgp router-id {{ bgp_router_id }} address-family ipv4 unicast {% for redist in redistribute %} redistribute {{ redist }} {% endfor %} exit-address-family address-family ipv6 unicast {% for redist in redistribute %} redistribute {{ redist }} {% endfor %} exit-address-family ''' def _get_router_id(): output = ovn_bgp_agent.privileged.vtysh.run_vtysh_command( command='show ip bgp summary json') return json.loads(output).get('ipv4Unicast', {}).get('routerId') def _run_vtysh_config_with_tempfile(vrf_config): try: f = tempfile.NamedTemporaryFile(mode='w') f.write(vrf_config) f.flush() except (IOError, OSError) as e: LOG.error('Failed to create the VRF configuration ' 'file. Error: %s', e) if f is not None: f.close() raise try: ovn_bgp_agent.privileged.vtysh.run_vtysh_config(f.name) finally: if f is not None: f.close() def set_default_redistribute(redist_opts): if not isinstance(redist_opts, set): redist_opts = set(redist_opts) if redist_opts == DEFAULT_REDISTRIBUTE: # no update required. return DEFAULT_REDISTRIBUTE.clear() DEFAULT_REDISTRIBUTE.update(redist_opts) def vrf_leak(vrf, bgp_as, bgp_router_id=None, template=LEAK_VRF_TEMPLATE): LOG.info("Add VRF leak for VRF %s on router bgp %s", vrf, bgp_as) if not bgp_router_id: bgp_router_id = _get_router_id() if not bgp_router_id: LOG.error("Unknown router-id, needed for route leaking") return vrf_template = Template(template) vrf_config = vrf_template.render(vrf_name=vrf, bgp_as=bgp_as, redistribute=DEFAULT_REDISTRIBUTE, bgp_router_id=bgp_router_id) _run_vtysh_config_with_tempfile(vrf_config) def vrf_reconfigure(evpn_info, action): LOG.info("FRR reconfiguration (action = %s) for evpn: %s", action, evpn_info) if action == "add-vrf": vrf_template = Template(ADD_VRF_TEMPLATE) vrf_config = vrf_template.render( vrf_name="{}{}".format(constants.OVN_EVPN_VRF_PREFIX, evpn_info['vni']), bgp_as=evpn_info['bgp_as'], redistribute=DEFAULT_REDISTRIBUTE, 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) return _run_vtysh_config_with_tempfile(vrf_config) ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/utils/ovn.py000066400000000000000000000762771460327367600250100ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import netaddr from oslo_config import cfg from oslo_log import log as logging from ovs.stream import Stream from ovsdbapp.backend import ovs_idl from ovsdbapp.backend.ovs_idl import command from ovsdbapp.backend.ovs_idl import connection from ovsdbapp.backend.ovs_idl import idlutils from ovsdbapp.backend.ovs_idl import rowview from ovsdbapp import event from ovsdbapp.schema.ovn_northbound import impl_idl as nb_impl_idl from ovsdbapp.schema.ovn_southbound import impl_idl as sb_impl_idl from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.utils import driver_utils from ovn_bgp_agent import exceptions from ovn_bgp_agent.utils import helpers CONF = cfg.CONF LOG = logging.getLogger(__name__) class OvnIdl(connection.OvsdbIdl): def __init__(self, driver, remote, schema, **kwargs): super(OvnIdl, self).__init__(remote, schema, **kwargs) self.driver = driver self.notify_handler = OvnDbNotifyHandler(driver) def notify(self, event, row, updates=None): self.notify_handler.notify(event, row, updates) class OvnDbNotifyHandler(event.RowEventHandler): def __init__(self, driver): super(OvnDbNotifyHandler, self).__init__() self.driver = driver class OvnNbIdl(OvnIdl): SCHEMA = 'OVN_Northbound' def __init__(self, connection_string, events=None, tables=None, leader_only=False): if connection_string.startswith("ssl"): self._check_and_set_ssl_files(self.SCHEMA) helper = self._get_ovsdb_helper(connection_string) self._events = events if tables is None: tables = ('Logical_Switch_Port', 'NAT', 'NB_Global') for table in tables: helper.register_table(table) super(OvnNbIdl, self).__init__( None, connection_string, helper, leader_only=leader_only) def _get_ovsdb_helper(self, connection_string): return idlutils.get_schema_helper(connection_string, self.SCHEMA) def _check_and_set_ssl_files(self, schema_name): priv_key_file = CONF.ovn.ovn_nb_private_key cert_file = CONF.ovn.ovn_nb_certificate ca_cert_file = CONF.ovn.ovn_nb_ca_cert if priv_key_file: Stream.ssl_set_private_key_file(priv_key_file) if cert_file: Stream.ssl_set_certificate_file(cert_file) if ca_cert_file: Stream.ssl_set_ca_cert_file(ca_cert_file) def start(self): conn = connection.Connection( self, timeout=CONF.ovsdb_connection_timeout) ovsdbNbConn = OvsdbNbOvnIdl(conn) if self._events: self.notify_handler.watch_events(self._events) return ovsdbNbConn class OvnSbIdl(OvnIdl): SCHEMA = 'OVN_Southbound' def __init__(self, connection_string, chassis=None, events=None, tables=None): if connection_string.startswith("ssl"): self._check_and_set_ssl_files(self.SCHEMA) helper = self._get_ovsdb_helper(connection_string) self._events = events if tables is None: tables = ('Chassis', 'Encap', 'Port_Binding', 'Datapath_Binding', 'SB_Global') for table in tables: helper.register_table(table) super(OvnSbIdl, self).__init__( None, connection_string, helper, leader_only=False) if chassis: table = ('Chassis_Private' if 'Chassis_Private' in tables else 'Chassis') self.tables[table].condition = [['name', '==', chassis]] def _get_ovsdb_helper(self, connection_string): return idlutils.get_schema_helper(connection_string, self.SCHEMA) def _check_and_set_ssl_files(self, schema_name): priv_key_file = CONF.ovn.ovn_sb_private_key cert_file = CONF.ovn.ovn_sb_certificate ca_cert_file = CONF.ovn.ovn_sb_ca_cert if priv_key_file: Stream.ssl_set_private_key_file(priv_key_file) if cert_file: Stream.ssl_set_certificate_file(cert_file) if ca_cert_file: Stream.ssl_set_ca_cert_file(ca_cert_file) def start(self): conn = connection.Connection( self, timeout=CONF.ovsdb_connection_timeout) ovsdbSbConn = OvsdbSbOvnIdl(conn) if self._events: self.notify_handler.watch_events(self._events) return ovsdbSbConn class Backend(ovs_idl.Backend): lookup_table = {} ovsdb_connection = None def __init__(self, connection): self.ovsdb_connection = connection super(Backend, self).__init__(connection) @property def idl(self): return self.ovsdb_connection.idl @property def tables(self): return self.idl.tables # FIXME(ltomasbo): This can be removed once ovsdbapp version is >=2.3.0 class LSGetLocalnetPortsCommand(command.ReadOnlyCommand): def __init__(self, api, switch, if_exists=False): super().__init__(api) self.switch = switch self.if_exists = if_exists def localnet_port(self, row): return row.type == constants.OVN_LOCALNET_VIF_PORT_TYPE def run_idl(self, txn): try: lswitch = self.api.lookup('Logical_Switch', self.switch) self.result = [rowview.RowView(p) for p in lswitch.ports if self.localnet_port(p)] except idlutils.RowNotFound as e: if self.if_exists: self.result = [] return msg = "Logical Switch %s does not exist" % self.switch raise RuntimeError(msg) from e # FIXME(ltomasbo): This can be removed once ovsdbapp version is >=2.1.0 class _LrpNetworksCommand(command.BaseCommand): table = 'Logical_Router_Port' def __init__(self, api, port, networks, exists): super().__init__(api) self.port = port self.exists = exists if isinstance(networks, (str, bytes)): networks = [networks] self.networks = [str(netaddr.IPNetwork(network)) for network in networks] # FIXME(ltomasbo): This can be removed once ovsdbapp version is >=2.1.0 class LrpAddNetworksCommand(_LrpNetworksCommand): def run_idl(self, txn): lrp = self.api.lookup(self.table, self.port) for network in self.networks: if network in lrp.networks and not self.exists: msg = "Network '%s' already exist in networks of port %s" % ( network, lrp.uuid) raise RuntimeError(msg) lrp.addvalue('networks', network) # FIXME(ltomasbo): This can be removed once ovsdbapp supports it class LrRouteAddCommand(command.BaseCommand): def __init__(self, api, router, prefix, nexthop, port=None, policy='dst-ip', ecmp=False, may_exist=False): prefix = str(netaddr.IPNetwork(prefix)) if nexthop != constants.ROUTE_DISCARD: nexthop = str(netaddr.IPAddress(nexthop)) super().__init__(api) self.router = router self.prefix = prefix self.nexthop = nexthop self.port = port self.policy = policy self.ecmp = ecmp self.may_exist = may_exist def run_idl(self, txn): lr = self.api.lookup('Logical_Router', self.router) for route in lr.static_routes: if self.prefix == route.ip_prefix: if self.ecmp and self.nexthop != route.nexthop: continue if not self.may_exist: msg = "Route %s already exists on router %s" % ( self.prefix, self.router) raise RuntimeError(msg) route.nexthop = self.nexthop route.policy = self.policy if self.port: route.output_port = self.port self.result = rowview.RowView(route) return route = txn.insert(self.api.tables['Logical_Router_Static_Route']) route.ip_prefix = self.prefix route.nexthop = self.nexthop route.policy = self.policy if self.port: route.output_port = self.port lr.addvalue('static_routes', route) self.result = route.uuid # FIXME(ltomasbo): This can be removed once ovsdbapp supports it class LrRouteDelCommand(command.BaseCommand): def __init__(self, api, router, prefix=None, nexthop=None, if_exists=False): if prefix is not None: prefix = str(netaddr.IPNetwork(prefix)) super().__init__(api) self.router = router self.prefix = prefix self.nexthop = nexthop self.if_exists = if_exists def run_idl(self, txn): lr = self.api.lookup('Logical_Router', self.router) if not self.prefix: lr.static_routes = [] return for route in lr.static_routes: if self.prefix == route.ip_prefix: if self.nexthop and route.nexthop != self.nexthop: continue lr.delvalue('static_routes', route) return if not self.if_exists: msg = "Route for %s in router %s does not exist" % ( self.prefix, self.router) raise RuntimeError(msg) class StaticMACBindingFindCommand(command.DbFindCommand): table = 'Static_MAC_Binding' def __init__(self, api, port, ip): super().__init__( api, self.table, ('logical_port', '=', port), ('ip', '=', ip), row=True, ) # FIXME(ltomasbo): This can be removed once ovsdbapp supports it class StaticMACBindingAddCommand(command.AddCommand): table_name = 'Static_MAC_Binding' def __init__(self, api, port, ip, mac, override_dynamic_mac=False, may_exist=False, **columns): super().__init__(api) self.port = port self.ip = ip self.mac = mac self.override_dynamic_mac = override_dynamic_mac self.may_exist = may_exist self.columns = columns def run_idl(self, txn): cmd = StaticMACBindingFindCommand(self.api, self.port, self.ip) cmd.run_idl(txn) static_mac_binding_result = cmd.result if static_mac_binding_result: if len(static_mac_binding_result) > 1: # With the current database schema, this cannot happen, but # better safe than sorry. raise RuntimeError( "Unexpected duplicates in database for port %s " "and ip %s" % (self.port, self.ip)) binding = static_mac_binding_result[0] if self.may_exist: # When no changes are made to a record, the parent # `post_commit` method will not be called. # # Ensure consistent return to caller of `Command.execute()` # even when no changes have been applied. self.result = rowview.RowView(binding) return else: raise RuntimeError( "Static MAC Binding entry for port %s and ip %s exists" % ( self.port, self.ip)) binding = txn.insert(self.api.tables[self.table_name]) binding.logical_port = self.port binding.ip = self.ip binding.mac = self.mac binding.override_dynamic_mac = self.override_dynamic_mac self.set_columns(binding, **self.columns) # Setting the result to something other than a :class:`rowview.RowView` # or :class:`ovs.db.idl.Row` typed value will make the parent # `post_commit` method retrieve the newly insterted row from IDL and # return that to the caller. self.result = binding.uuid # FIXME(ltomasbo): This can be removed once ovsdbapp supports it class StaticMACBindingDelCommand(command.BaseCommand): table_name = 'Static_MAC_Binding' def __init__(self, api, port, ip, if_exists=False, **columns): super().__init__(api) self.port = port self.ip = ip self.if_exists = if_exists self.columns = columns def run_idl(self, txn): cmd = StaticMACBindingFindCommand(self.api, self.port, self.ip) cmd.run_idl(txn) static_mac_binding_result = cmd.result if static_mac_binding_result: if len(static_mac_binding_result) > 1: # With the current database schema, this cannot happen, but # better safe than sorry. raise RuntimeError( "Unexpected duplicates in database for port %s " "and ip %s" % (self.port, self.ip)) binding = static_mac_binding_result[0] binding.delete() return if self.if_exists: return else: raise RuntimeError( "Static MAC Binding entry for port %s and ip %s does not " "exist" % (self.port, self.ip)) class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend): def __init__(self, connection): super(OvsdbNbOvnIdl, self).__init__(connection) self.idl._session.reconnect.set_probe_interval(60000) def get_network_vlan_tags(self): tags = [] cmd = self.db_find_rows('Logical_Switch_Port', ('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE)) for row in cmd.execute(check_error=True): if row.tag: tags.append(row.tag[0]) return tags def get_network_vlan_tag_by_network_name(self, network_name): tags = [] cmd = self.db_find_rows('Logical_Switch_Port', ('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE)) 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]) return tags def ls_has_virtual_ports(self, logical_switch): ls = self.lookup('Logical_Switch', logical_switch) for port in ls.ports: if port.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE: return True return False def get_nat_by_logical_port(self, logical_port): cmd = self.db_find_rows('NAT', ('logical_port', '=', logical_port)) nat_info = cmd.execute(check_error=True) return nat_info[0] if nat_info else [] def get_active_lsp_on_chassis(self, chassis): ports = [] cmd = self.db_find_rows('Logical_Switch_Port', ('up', '=', True)) for row in cmd.execute(check_error=True): port_chassis, _ = driver_utils.get_port_chassis(row, chassis) if port_chassis == chassis: ports.append(row) return ports def get_active_cr_lrp_on_chassis(self, chassis): ports = [] rows = self.db_list_rows('Logical_Router_Port').execute( check_error=True) for row in rows: if not hasattr(row, 'status'): LOG.warning("OVN version does not include support for status " "information. Therefore router ports and tenant " "IPs cannot be exposed.") break if row.status.get(constants.OVN_STATUS_CHASSIS) == chassis: ports.append(row) return ports def get_active_local_lrps(self, local_gateway_ports): ports = [] cmd = self.db_find_rows( 'Logical_Switch_Port', ('up', '=', True), ('type', '=', constants.OVN_ROUTER_PORT_TYPE), ('external_ids', '=', {constants.OVN_DEVICE_OWNER_EXT_ID_KEY: constants.OVN_ROUTER_INTERFACE})) for row in cmd.execute(check_error=True): if (row.external_ids.get(constants.OVN_DEVICE_ID_EXT_ID_KEY) in local_gateway_ports): ports.append(row) return ports def get_active_lsp(self, network): ports = [] # port type "" cmd = self.db_find_rows( 'Logical_Switch_Port', ('up', '=', True), ('type', '=', constants.OVN_VM_VIF_PORT_TYPE), ('external_ids', '=', {constants.OVN_LS_NAME_EXT_ID_KEY: network})) ports.extend(cmd.execute(check_error=True)) # port type "virtual" cmd = self.db_find_rows( 'Logical_Switch_Port', ('up', '=', True), ('type', '=', constants.OVN_VIRTUAL_VIF_PORT_TYPE), ('external_ids', '=', {constants.OVN_LS_NAME_EXT_ID_KEY: network})) ports.extend(cmd.execute(check_error=True)) return ports def get_active_local_lbs(self, local_gateway_ports): lbs = [] cmd = self.db_find_rows('Load_Balancer', ('vips', '!=', {})) for row in cmd.execute(check_error=True): for ext_id_key in constants.OVN_LB_EXT_ID_ROUTER_KEY: try: router_name = row.external_ids[ext_id_key].replace( 'neutron-', '', 1) if router_name in local_gateway_ports: lbs.append(row) break except KeyError: continue return lbs # FIXME(ltomasbo): This can be removed once ovsdbapp version is >=2.3.0 def ls_get_localnet_ports(self, logical_switch, if_exists=True): return LSGetLocalnetPortsCommand(self, logical_switch, if_exists=if_exists) # FIXME(ltomasbo): This can be removed once ovsdbapp version is >=2.1.0 def lrp_add_networks(self, port, networks, may_exist=False): return LrpAddNetworksCommand(self, port, networks, may_exist) def lr_route_add(self, router, prefix, nexthop, port=None, policy='dst-ip', ecmp=False, may_exist=False): return LrRouteAddCommand(self, router, prefix, nexthop, port, policy, ecmp, may_exist) def lr_route_del(self, router, prefix=None, nexthop=None, if_exists=False): return LrRouteDelCommand(self, router, prefix, nexthop, if_exists) def static_mac_binding_add(self, port, ip, mac, override_dynamic_mac=False, may_exist=False): return StaticMACBindingAddCommand(self, port, ip, mac, override_dynamic_mac, may_exist) def static_mac_binding_del(self, port, ip, if_exists=False): return StaticMACBindingDelCommand(self, port, ip, if_exists) def get_router(self, router): router_name = 'neutron-' + router return self.lr_get(router_name).execute(check_error=True) class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend): def __init__(self, connection): super(OvsdbSbOvnIdl, self).__init__(connection) self.idl._session.reconnect.set_probe_interval(60000) def get_port_by_name(self, port): cmd = self.db_find_rows('Port_Binding', ('logical_port', '=', port)) port_info = cmd.execute(check_error=True) return port_info[0] if port_info else [] def get_ports_on_datapath(self, datapath, port_type=None): if port_type: cmd = self.db_find_rows('Port_Binding', ('datapath', '=', datapath), ('type', '=', port_type)) else: cmd = self.db_find_rows('Port_Binding', ('datapath', '=', datapath)) try: return cmd.execute(check_error=True) except ValueError: # Datapath has been removed. raise exceptions.DatapathNotFound(datapath=datapath) def get_ports_by_type(self, port_type): cmd = self.db_find_rows('Port_Binding', ('type', '=', port_type)) return cmd.execute(check_error=True) def is_provider_network(self, datapath): try: cmd = self.db_find_rows('Port_Binding', ('datapath', '=', datapath), ('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE)) return bool(cmd.execute(check_error=True)) except ValueError: # Datapath has been removed. raise exceptions.DatapathNotFound(datapath=datapath) def get_localnet_for_datapath(self, datapath): try: cmd = self.db_find_rows('Port_Binding', ('datapath', '=', datapath), ('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE)) localnet_info = cmd.execute(check_error=True) return localnet_info[0].logical_port if localnet_info else [] except ValueError: # Datapath has been removed. raise exceptions.DatapathNotFound(datapath=datapath) def get_fip_associated(self, port): cmd = self.db_find_rows( 'Port_Binding', ('type', '=', constants.OVN_PATCH_VIF_PORT_TYPE)) for row in cmd.execute(check_error=True): for fip in row.nat_addresses: if port in fip: return fip.split(" ")[1], row.datapath return None, None def is_port_on_chassis(self, port_name, chassis): port_info = self.get_port_by_name(port_name) try: return (port_info and port_info.chassis[0].name == chassis) except IndexError: pass return False def is_port_without_chassis(self, port_name): port_info = self.get_port_by_name(port_name) return (port_info and not port_info.chassis) def is_port_deleted(self, port_name): return False if self.get_port_by_name(port_name) else True def get_ports_on_chassis(self, chassis): rows = self.db_list_rows('Port_Binding').execute(check_error=True) return [r for r in rows if r.chassis and r.chassis[0].name == chassis] def get_cr_lrp_ports(self): return self.db_find_rows( "Port_Binding", ("type", "=", constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE), ).execute(check_error=True) def get_cr_lrp_ports_on_chassis(self, chassis): return [ r.logical_port for r in self.get_cr_lrp_ports() if r.chassis and r.chassis[0].name == chassis ] def get_cr_lrp_nat_addresses_info(self, cr_lrp_port_name, chassis, sb_idl): # NOTE: Assuming logical_port format is "cr-lrp-XXXX" patch_port_name = cr_lrp_port_name.split("cr-lrp-")[1] patch_port_row = self.get_port_by_name(patch_port_name) if not patch_port_row: return [], None ips = [] for row in patch_port_row.nat_addresses: nat_ips = row.split(" ")[1:-1] port = row.split(" ")[-1].split("\"")[1] if port and sb_idl and sb_idl.is_port_on_chassis(port, chassis): ips.extend(nat_ips) return ips, patch_port_row def get_provider_datapath_from_cr_lrp(self, cr_lrp): if cr_lrp.startswith('cr-lrp'): provider_port = cr_lrp.split("cr-lrp-")[1] return self.get_port_datapath(provider_port) return None def get_datapath_from_port_peer(self, port): peer_name = port.options['peer'] return self.get_port_datapath(peer_name) def get_network_name_and_tag(self, datapath, bridge_mappings): for row in self.get_ports_on_datapath( datapath, constants.OVN_LOCALNET_VIF_PORT_TYPE): if (row.options and row.options.get('network_name') in bridge_mappings): return row.options.get('network_name'), row.tag return None, None def get_network_vlan_tags(self): tags = [] cmd = self.db_find_rows('Port_Binding', ('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE)) for row in cmd.execute(check_error=True): if row.tag: tags.append(row.tag[0]) return tags def get_network_vlan_tag_by_network_name(self, network_name): tags = [] cmd = self.db_find_rows('Port_Binding', ('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE)) 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]) return tags def is_router_gateway_on_chassis(self, datapath, chassis): port_info = self.get_ports_on_datapath( datapath, constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE) try: if port_info and port_info[0].chassis[0].name == chassis: return port_info[0].logical_port except IndexError: return False def is_router_gateway_on_any_chassis(self, datapath): port_info = self.get_ports_on_datapath( datapath, constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE) try: if port_info and port_info[0].chassis[0].name: return port_info[0] except IndexError: return False def get_lrps_for_datapath(self, datapath): lrps = [] for row in self.get_ports_on_datapath( datapath, constants.OVN_PATCH_VIF_PORT_TYPE): if row.options: lrps.append(row.options['peer']) return lrps def get_lrp_ports_for_router(self, datapath): return self.get_ports_on_datapath( datapath, constants.OVN_PATCH_VIF_PORT_TYPE) def get_lrp_ports_on_provider(self): provider_lrp_ports = [] lrp_ports = self.get_ports_by_type(constants.OVN_PATCH_VIF_PORT_TYPE) for lrp_port in lrp_ports: if lrp_port.logical_port.startswith( constants.OVN_LRP_PORT_NAME_PREFIX): continue if self.is_provider_network(lrp_port.datapath): provider_lrp_ports.append(lrp_port) def get_port_datapath(self, port_name): port_info = self.get_port_by_name(port_name) if port_info: return port_info.datapath def get_ip_from_port_peer(self, port): peer_name = port.options['peer'] peer_port = self.get_port_by_name(peer_name) try: return peer_port.mac[0].split(' ')[1] except AttributeError: raise exceptions.PortNotFound(port=peer_name) def get_evpn_info_from_port_name(self, port_name): if port_name.startswith(constants.OVN_CRLRP_PORT_NAME_PREFIX): port_name = port_name.split( constants.OVN_CRLRP_PORT_NAME_PREFIX)[1] elif port_name.startswith(constants.OVN_LRP_PORT_NAME_PREFIX): port_name = port_name.split(constants.OVN_LRP_PORT_NAME_PREFIX)[1] port = self.get_port_by_name(port_name) return self.get_evpn_info(port) def get_evpn_info(self, port): try: return {'vni': int( port.external_ids[constants.OVN_EVPN_VNI_EXT_ID_KEY]), 'bgp_as': int( port.external_ids[constants.OVN_EVPN_AS_EXT_ID_KEY])} except (KeyError, ValueError): LOG.debug('Either "%s" or "%s" were not found or have an ' 'invalid value in the port %s ' 'external_ids %s', constants.OVN_EVPN_VNI_EXT_ID_KEY, constants.OVN_EVPN_AS_EXT_ID_KEY, port.logical_port, port.external_ids) return {} def get_port_if_local_chassis(self, port_name, chassis): port = self.get_port_by_name(port_name) if port.chassis[0].name == chassis: return port def get_virtual_ports_on_datapath_by_chassis(self, datapath, chassis): try: rows = self.get_ports_on_datapath( datapath, port_type=constants.OVN_VIRTUAL_VIF_PORT_TYPE) except exceptions.DatapathNotFound: return [] return [r for r in rows if r.chassis and r.chassis[0].name == chassis] def get_ovn_lb(self, name): cmd = self.db_find_rows('Load_Balancer', ('name', '=', name)) lb_info = cmd.execute(check_error=True) return lb_info[0] if lb_info else [] def get_provider_ovn_lbs_on_cr_lrp(self, provider_dp, router_dp): # return {vip_port: vip_ip, vip_port2: vip_ip2, ...} # ovn-sbctl find port_binding type=\"\" chassis=[] mac=[] up=false cmd = self.db_find_rows('Port_Binding', ('datapath', '=', provider_dp), ('type', '=', constants.OVN_VM_VIF_PORT_TYPE), ('chassis', '=', []), ('mac', '=', []), ('up', '=', False)) lbs = {} for row in cmd.execute(check_error=True): # This is depending on the external-id information added by # neutron, regarding the neutron:cidrs ip_info = row.external_ids.get( constants.OVN_CIDRS_EXT_ID_KEY, "") if not ip_info: continue port_name = row.external_ids.get( constants.OVN_PORT_NAME_EXT_ID_KEY) if (not port_name or len(port_name) <= len(constants.LB_VIP_PORT_PREFIX)): continue lb_name = port_name[len(constants.LB_VIP_PORT_PREFIX):] lb = self.get_ovn_lb(lb_name) if not lb: continue lb_dp, lr_dp = helpers.get_lb_datapath_groups(lb) if not lb_dp: lb_dp = lb.datapaths if not lr_dp: # assume all the members are connected through the same router # so only one datapath needs to be checked router_lrps = self.get_lrps_for_datapath(lb_dp[0]) for lrp in router_lrps: router_lrp_dp = self.get_port_datapath(lrp) if router_lrp_dp == router_dp: lb_ip = ip_info.split(" ")[0].split("/")[0] lbs[lb.name] = lb_ip break elif lr_dp == router_dp: lb_ip = ip_info.split(" ")[0].split("/")[0] lbs[lb.name] = lb_ip return lbs def get_ovn_vip_port(self, name): # ovn-sbctl find port_binding type=\"\" chassis=[] mac=[] up=false cmd = self.db_find_rows('Port_Binding', ('type', '=', constants.OVN_VM_VIF_PORT_TYPE), ('chassis', '=', []), ('mac', '=', []), ('up', '=', False)) for row in cmd.execute(check_error=True): port_name = row.external_ids.get( constants.OVN_PORT_NAME_EXT_ID_KEY) if port_name[len(constants.LB_VIP_PORT_PREFIX):] == name: return row ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/utils/ovs.py000066400000000000000000000271471460327367600250050ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_config import cfg from oslo_log import log as logging from ovs.db import idl from ovsdbapp.backend.ovs_idl import connection from ovsdbapp.backend.ovs_idl import idlutils from ovsdbapp.schema.open_vswitch import impl_idl as idl_ovs import socket import tenacity from ovn_bgp_agent import constants from ovn_bgp_agent import exceptions as agent_exc import ovn_bgp_agent.privileged.ovs_vsctl from ovn_bgp_agent.utils import linux_net CONF = cfg.CONF LOG = logging.getLogger(__name__) def _find_ovs_port(bridge): # TODO(ltomasbo): What happens if there are several patch ports on the # same bridge? ovs_port = None ovs_ports = ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-vsctl', ['list-ports', bridge])[0].rstrip() for p in ovs_ports.split('\n'): if p.startswith(constants.OVS_PATCH_PROVNET_PORT_PREFIX): ovs_port = p return ovs_port def get_bridge_flows(bridge, filter_=None): args = ['dump-flows', bridge] if filter_ is not None: args.append(filter_) return ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-ofctl', args)[0].split('\n')[1:-1] def get_device_port_at_ovs(device): return ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-vsctl', ['get', 'Interface', device, 'ofport'])[0].rstrip() def get_ovs_ports_info(bridge): ovs_ports = ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-vsctl', ['list-ports', bridge])[0].rstrip() return ovs_ports.split("\n") def get_ovs_patch_ports_info(bridge, prefix='patch-provnet-'): in_ports = [] ovs_ports = get_ovs_ports_info(bridge) for ovs_port in ovs_ports: if ovs_port.startswith(prefix): ovs_ofport = get_device_port_at_ovs(ovs_port) in_ports.append(ovs_ofport) return in_ports @tenacity.retry( retry=tenacity.retry_if_exception_type(agent_exc.PatchPortNotFound), wait=tenacity.wait_fixed(1), stop=tenacity.stop_after_delay(5), reraise=True) def get_ovs_patch_port_ofport(patch): patch_name = "patch-{}-to-br-int".format(patch) try: ofport = ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-vsctl', ['get', 'Interface', patch_name, 'ofport'] )[0].rstrip() except Exception: raise agent_exc.PatchPortNotFound(localnet=patch) if ofport == '[]': # NOTE(ltomasbo): there is a chance the patch port interface was # created but not yet added to ovs bridge, therefore it exists but # has an empty ofport. We should retry in this case raise agent_exc.PatchPortNotFound(localnet=patch) return ofport def ensure_mac_tweak_flows(bridge, mac, ports, cookie): cookie_id = "cookie={}/-1".format(cookie) current_flows = get_bridge_flows(bridge, cookie_id) flows_info = [flow.split("priority")[1].replace(" ", ",") for flow in current_flows] for in_port in ports: exist_flow = False exist_flow_v6 = False flow = ("cookie={},priority=900,ip,in_port={}," "actions=mod_dl_dst:{},NORMAL".format( cookie, in_port, mac)) flow_v6 = ("cookie={},priority=900,ipv6,in_port={}," "actions=mod_dl_dst:{},NORMAL".format( cookie, in_port, mac)) if flow.split("priority")[1] in flows_info: exist_flow = True if flow_v6.split("priority")[1] in flows_info: exist_flow_v6 = True if not exist_flow: ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-ofctl', ['add-flow', bridge, flow]) if not exist_flow_v6: ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-ofctl', ['add-flow', bridge, flow_v6]) def remove_extra_ovs_flows(ovs_flows, bridge, cookie): expected_flows = [] for port in ovs_flows[bridge].get('in_port'): flow = ("=900,ip,in_port={} actions=mod_dl_dst:{},NORMAL".format( port, ovs_flows[bridge]['mac'])) expected_flows.append(flow) flow_v6 = ("=900,ipv6,in_port={} actions=mod_dl_dst:{},NORMAL".format( port, ovs_flows[bridge]['mac'])) expected_flows.append(flow_v6) cookie_id = "cookie={}/-1".format(cookie) current_flows = get_bridge_flows(bridge, cookie_id) for flow in current_flows: if flow.split("priority")[1] not in expected_flows: del_flow = ('{},{}').format( cookie_id, flow.split("priority=900,")[1].split(" actions")[0]) ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-ofctl', ['del-flows', bridge, del_flow]) def ensure_flow(bridge, flow): ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-ofctl', ['add-flow', bridge, flow]) def ensure_evpn_ovs_flow(bridge, cookie, mac, output_port, port_dst, net, strip_vlan=False): ovs_port = _find_ovs_port(bridge) if not ovs_port: return ovs_ofport = get_device_port_at_ovs(ovs_port) vrf_ofport = get_device_port_at_ovs(output_port) strip_vlan_opt = 'strip_vlan,' if strip_vlan else '' ip_version = linux_net.get_ip_version(net) port_dst_mac = linux_net.get_interface_address(port_dst) if ip_version == constants.IP_VERSION_6: flow = ( "cookie={},priority=1000,ipv6,in_port={},dl_src:{}," "ipv6_src={} actions=mod_dl_dst:{},{}output={}".format( cookie, ovs_ofport, mac, net, port_dst_mac, strip_vlan_opt, vrf_ofport)) else: flow = ( "cookie={},priority=1000,ip,in_port={},dl_src:{},nw_src={}" "actions=mod_dl_dst:{},{}output={}".format( cookie, ovs_ofport, mac, net, port_dst_mac, strip_vlan_opt, vrf_ofport)) ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-ofctl', ['add-flow', bridge, flow]) def remove_evpn_router_ovs_flows(bridge, cookie, mac): ovs_port = _find_ovs_port(bridge) if not ovs_port: return ovs_ofport = get_device_port_at_ovs(ovs_port) cookie_id = "cookie={}/-1".format(cookie) flow = ("{},ip,in_port={},dl_src:{}".format( cookie_id, ovs_ofport, mac)) ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-ofctl', ['del-flows', bridge, flow]) flow_v6 = ("{},ipv6,in_port={},dl_src:{}".format(cookie_id, ovs_ofport, mac)) ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-ofctl', ['del-flows', bridge, flow_v6]) def remove_evpn_network_ovs_flow(bridge, cookie, mac, net): ovs_port = _find_ovs_port(bridge) if not ovs_port: return ovs_ofport = get_device_port_at_ovs(ovs_port) cookie_id = "cookie={}/-1".format(cookie) ip_version = linux_net.get_ip_version(net) if ip_version == constants.IP_VERSION_6: flow = ("{},ipv6,in_port={},dl_src:{},ipv6_src={}".format( cookie_id, ovs_ofport, mac, net)) else: flow = ("{},ip,in_port={},dl_src:{},nw_src={}".format( cookie_id, ovs_ofport, mac, net)) ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-ofctl', ['del-flows', bridge, flow]) def add_device_to_ovs_bridge(device, bridge, vlan_tag=None): args = ['--may-exist', 'add-port', bridge, device] if vlan_tag is not None: args.append('tag=%s' % vlan_tag) ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd('ovs-vsctl', args) def del_device_from_ovs_bridge(device, bridge=None): args = ['--if-exists', 'del-port'] if bridge: args.append(bridge) args.append(device) ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd('ovs-vsctl', args) def add_vlan_port_to_ovs_bridge(bridge, vlan, vlan_tag): # ovs-vsctl add-port BRIDGE VLAN tag=VALN_ID # -- set interface VLAN type=internal args = [ '--may-exist', 'add-port', bridge, vlan, 'tag={}'.format(vlan_tag), '--', 'set', 'interface', vlan, 'type=internal'] ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd('ovs-vsctl', args) def del_flow(flow, bridge, cookie): cookie_id = "cookie={}/-1".format(cookie) f = '{},priority{}'.format( cookie_id, flow.split(' actions')[0].split(' priority')[1]) ovn_bgp_agent.privileged.ovs_vsctl.ovs_cmd( 'ovs-ofctl', ['--strict', 'del-flows', bridge, f]) def get_flow_info(flow): # example: # cookie=0x3e7, duration=85.005s, table=0, n_packets=0, # n_bytes=0, idle_age=65534, priority=1000,ip,in_port=1 # nw_src=20.0.0.0/24 actions=mod_dl_dst:1a:bd:c3:dc:6a:4c, # output:5 flow_mac = flow_port = flow_nw_src = flow_ipv6_src = None try: flow_mac = flow.split('dl_src=')[1].split(',')[0] flow_port = flow.split('output:')[1].split(',')[0] except (IndexError, TypeError): pass flow_nw = flow.split('nw_src=') if len(flow_nw) == 2: flow_nw_src = flow_nw[1].split(' ')[0] flow_ipv6 = flow.split('ipv6_src=') if len(flow_ipv6) == 2: flow_ipv6_src = flow_ipv6[1].split(' ')[0] return {'mac': flow_mac, 'port': flow_port, 'nw_src': flow_nw_src, 'ipv6_src': flow_ipv6_src} class OvsIdl(object): def start(self, connection_string): helper = idlutils.get_schema_helper(connection_string, 'Open_vSwitch') tables = ('Open_vSwitch', 'Bridge', 'Port', 'Interface') for table in tables: helper.register_table(table) ovs_idl = idl.Idl(connection_string, helper) ovs_idl._session.reconnect.set_probe_interval(60000) conn = connection.Connection( ovs_idl, timeout=180) self.idl_ovs = idl_ovs.OvsdbIdl(conn) def _get_from_ext_ids(self, key): return self.idl_ovs.db_get( 'Open_vSwitch', '.', 'external_ids').execute()[key] def get_own_chassis_id(self): """Return the external_ids:system-id value of the Open_vSwitch table. """ return self._get_from_ext_ids('system-id') def get_own_chassis_name(self): """Return the external_ids:hostname value of the Open_vSwitch table. If the value is not configured, it will fetch the hostname from the current machine. """ try: return self._get_from_ext_ids('hostname') except KeyError: return socket.gethostname() def get_ovn_remote(self, nb=False): """Return the external_ids:ovn-remote value of the Open_vSwitch table. """ if nb: return (CONF.ovn.ovn_nb_connection if CONF.ovn.ovn_nb_connection else self._get_from_ext_ids('ovn-nb-remote')) return (CONF.ovn.ovn_sb_connection if CONF.ovn.ovn_sb_connection else self._get_from_ext_ids('ovn-remote')) def get_ovn_bridge_mappings(self, bridge=None): """Return a list of bridge mappings Return a list of bridge mappings based on the external_ids:ovn-bridge-mappings value of the Open_vSwitch table. """ key = 'ovn-bridge-mappings' if bridge: key = key + '-' + str(bridge) try: return [i.strip() for i in self._get_from_ext_ids(key).split(',')] except KeyError: return [] ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/utils/wire.py000066400000000000000000000710011460327367600251300ustar00rootroot00000000000000# Copyright 2023 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_config import cfg from oslo_log import log as logging from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.utils import ovs from ovn_bgp_agent import exceptions as agent_exc from ovn_bgp_agent.utils import helpers from ovn_bgp_agent.utils import linux_net CONF = cfg.CONF LOG = logging.getLogger(__name__) def ensure_base_wiring_config(idl, ovs_idl, ovn_idl=None, routing_tables={}): if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY: return _ensure_base_wiring_config_underlay(idl, ovs_idl, routing_tables) elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN: return _ensure_base_wiring_config_ovn(ovs_idl, ovn_idl) def _ensure_base_wiring_config_underlay(idl, ovs_idl, routing_tables): # Get bridge mappings: xxxx:br-ex,yyyy:br-ex2 bridge_mappings = ovs_idl.get_ovn_bridge_mappings() ovn_bridge_mappings = {} flows_info = {} for bridge_index, bridge_mapping in enumerate(bridge_mappings, 1): network, bridge = helpers.parse_bridge_mapping(bridge_mapping) if not network: continue ovn_bridge_mappings[network] = bridge linux_net.ensure_routing_table_for_bridge( routing_tables, bridge, CONF.bgp_vrf_table_id) vlan_tags = idl.get_network_vlan_tag_by_network_name(network) for vlan_tag in vlan_tags: linux_net.ensure_vlan_device_for_network(bridge, vlan_tag) linux_net.ensure_arp_ndp_enabled_for_bridge(bridge, bridge_index, vlan_tags) if not flows_info.get(bridge): mac = linux_net.get_interface_address(bridge) flows_info[bridge] = {'mac': mac, 'in_port': set()} flows_info[bridge]['in_port'] = ovs.get_ovs_patch_ports_info( bridge) ovs.ensure_mac_tweak_flows(bridge, mac, flows_info[bridge]['in_port'], constants.OVS_RULE_COOKIE) return ovn_bridge_mappings, flows_info def _ensure_base_wiring_config_ovn(ovs_idl, ovn_idl): """Base configuration for extra OVN cluster instead of kernel networking This function is in charge of the steps to ensure the in-node OVN cluster is properly configured by: 1. Get the information about the OpenStack provider bridge(s) and the flows info, such as the mac and the in_port 2. Add the egress ovs flows so that the destination mac is the one on the extra ovn-cluster, in the LR 3. Create the LR in the in-node OVN cluster 4. Create the LR policy in the in-node OVN cluster to redirect the traffic (with ECMP support) to the nexthops 5. Create the LR routes in the in-node OVN cluster to route any traffic the peers IPs 6. Create the LS (+ Localnet port) for the connection between the router and the OpenStack OVN networks. then it connects it to the LR 7. Create the LS (+ Localnet port) for the connection between the router and the external network. Then it connects it to the LR 8. Create the ingress_flow at the external OVN provider bridges to redirect the needed traffic to the in-cluster OVN networks :param ovs_idl: The idl to communicate with local ovs DB :param ovn_idl: The idl to communicate with local (in-node) NB DB :return: ovn_bridge_mappings (network and bridges association) and the flows_info per bridge """ # OpenStack Egress part # Get bridge mappings: xxxx:br-ex,yyyy:br-ex2 bridge_mappings = ovs_idl.get_ovn_bridge_mappings() ovn_bridge_mappings = {} flows_info = {} for bridge_mapping in bridge_mappings: network, bridge = helpers.parse_bridge_mapping(bridge_mapping) if not network: continue ovn_bridge_mappings[network] = bridge if not flows_info.get(bridge): mac = linux_net.get_interface_address(bridge) flows_info[bridge] = {'mac': mac, 'in_port': set()} flows_info[bridge]['in_port'] = ovs.get_ovs_patch_ports_info( bridge) _ensure_egress_flows(bridge, flows_info[bridge]['in_port']) # Extra OVN cluster configuration provider_cidrs = CONF.local_ovn_cluster.provider_networks_pool_prefixes # LR cmds = [] cmds.extend(_ensure_ovn_router(ovn_idl)) # FIXME(ltomasbo): we need to firsts create the router and then the # policies and routes in a different transaction until ovsdbapp # allows it to do it in one transaction. Once that happen the next # 2 lines can be removed _execute_commands(ovn_idl, cmds) cmds = [] cmds.extend(_ensure_ovn_policies(ovn_idl, CONF.local_ovn_cluster.peer_ips)) cmds.extend(_ensure_ovn_routes(ovn_idl, CONF.local_ovn_cluster.peer_ips)) # Creation of all router related cmds in a single transaction _execute_commands(ovn_idl, cmds) # LS bgp_bridge_mappings = ovs_idl.get_ovn_bridge_mappings( bridge=constants.OVN_CLUSTER_BRIDGE) for bridge_mapping in bgp_bridge_mappings: network, bridge = helpers.parse_bridge_mapping(bridge_mapping) if not network: continue # Create LS + Localnet port on it _ensure_ovn_switch(ovn_idl, network) # differentiate between internal LS (connecting to OpenStack) # and external LS (connecting to the NICs) if bridge in ovn_bridge_mappings.values(): # Internal Bridge connecting to OpenStack OVN cluster _ensure_ovn_network_link(ovn_idl, network, 'internal', provider_cidrs=provider_cidrs) else: ip, mac = linux_net.get_nic_info(bridge) # External Bridge connecting to the external networks _ensure_ovn_network_link(ovn_idl, network, 'external', ip=ip, mac=mac) _ensure_ingress_flows(bridge, mac, network, provider_cidrs) return ovn_bridge_mappings, flows_info def _ensure_ovn_router(ovn_idl): return [ovn_idl.lr_add(constants.OVN_CLUSTER_ROUTER, may_exist=True)] def _ensure_ovn_switch(ovn_idl, switch_name): ovn_idl.ls_add(switch_name, may_exist=True).execute(check_error=True) # Add localnet port to them localnet_port = "{}-localnet".format(switch_name) options = {'network_name': switch_name} cmds = _ensure_lsp_cmds(ovn_idl, localnet_port, switch_name, 'localnet', 'unknown', **options) _execute_commands(ovn_idl, cmds) def _execute_commands(idl, cmds): with idl.transaction(check_error=True) as txn: for command in cmds: txn.add(command) def _ensure_ovn_network_link(ovn_idl, switch_name, direction, provider_cidrs=None, ip=None, mac=None): """Base configuration for connecting LR and LSs This function is in charge of connecting the LR to the external or internal LS For the internal LS it configures: 1. Creates LRP to connect to the internal switch 2. If networks (provider_cidrs) are different, adding the new networks 3. Create LSP related to the LRP with the right options, including the arp_proxy 4. Bind the LRP to the local chassis For the external LS it configures: 1. Creates LRP to connect to the external switch 2. If networks (ip) is different than the nic network add the nic network and remove the extra ones 3. Create LSP related to the LRP with the right options :param ovn_idl: The idl to communicate with local (in-node) NB DB :param switch_name: the name of the logical switch to configure :param direction: can be 'internal' or 'external' :param provider_cidrs (optional): CIDRs to configure the networks of the LRP, as well as to configure the ARP proxy on the internal LSP (only for the internal) :param ip (optional): IP to configure in the LRP connected to the external switch (only for the external) :param mac (optional): MAC to configure in the LRP connected to the external switch (only for the external) """ # It accepts 2 values for direction: internal or external cmds = [] if direction == 'internal': # Connect BGP router to the internal logical switch r_port_name = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER) try: ovn_idl.lrp_add(constants.OVN_CLUSTER_ROUTER, r_port_name, constants.OVN_CLUSTER_ROUTER_INTERNAL_MAC, provider_cidrs, peer=[], may_exist=True).execute( check_error=True) except RuntimeError as rte: # TODO(ltomasbo): Change OVSDBAPP to return a different error for # this to avoid having to compare strings as this is error prone networks_message = 'with different networks' if networks_message not in str(rte): raise # Trying to sync the networks by adding them cmds.append(ovn_idl.lrp_add_networks(r_port_name, provider_cidrs, may_exist=True)) s_port_name = "openstack-{}".format(constants.OVN_CLUSTER_ROUTER) # NOTE(ltomasbo): require v23.06.0 so that proxy-arp works as expected. # If older version the provider_cidrs should contain all the provider # network cidrs, pointing to the gateway IP of the network. cidrs = ','.join(provider_cidrs) if provider_cidrs else '0.0.0.0/0' options = {'router-port': r_port_name, 'arp_proxy': cidrs} cmds.extend(_ensure_lsp_cmds(ovn_idl, s_port_name, switch_name, 'router', 'router', **options)) # bind to local chassis # ovn-nbctl lrp-set-gateway-chassis bgp-router-public bgp 1 cmds.append(ovn_idl.lrp_set_gateway_chassis( r_port_name, constants.OVN_CLUSTER_BRIDGE, 1)) else: # direction == 'external' # Connect BGP router to the external logical switch r_port_name = "{}-{}".format(constants.OVN_CLUSTER_ROUTER, switch_name) # LRP try: ovn_idl.lrp_add(constants.OVN_CLUSTER_ROUTER, r_port_name, mac, [ip], peer=[], may_exist=True).execute( check_error=True) except RuntimeError as rte: # TODO(ltomasbo): Change OVSDBAPP to return a different error for # this to avoid having to compare strings as this is error prone networks_message = 'with different networks' if networks_message not in str(rte): raise # Trying to sync the networks by adding them cmds.append(ovn_idl.lrp_add_networks(r_port_name, [ip], may_exist=True)) lrp = ovn_idl.lrp_get(r_port_name).execute(check_error=True) for net in lrp.networks: if net != ip: cmds.append(ovn_idl.lrp_del_networks(r_port_name, [net], if_exists=True)) # LSP s_port_name = "{}-{}".format(switch_name, constants.OVN_CLUSTER_ROUTER) options = {'router-port': r_port_name} cmds.extend(_ensure_lsp_cmds(ovn_idl, s_port_name, switch_name, 'router', 'router', **options)) if cmds: _execute_commands(ovn_idl, cmds) def _ensure_lsp_cmds(ovn_idl, port_name, switch, port_type, addresses, **options): cmds = [] cmds.append(ovn_idl.lsp_add(switch, port_name, may_exist=True)) cmds.append(ovn_idl.lsp_set_type(port_name, port_type)) cmds.append(ovn_idl.lsp_set_addresses(port_name, addresses=[addresses])) cmds.append(ovn_idl.lsp_set_options(port_name, **options)) return cmds def _ensure_ovn_policies(ovn_idl, next_hops): priority = 10 match = 'inport=="{}-openstack"'.format(constants.OVN_CLUSTER_ROUTER) action = 'reroute' columns = {} if len(next_hops) > 1: columns = {'nexthops': next_hops} elif len(next_hops) == 1: columns = {'nexthop': next_hops[0]} return [ovn_idl.lr_policy_add(constants.OVN_CLUSTER_ROUTER, priority, match, action, may_exist=True, **columns)] def _ensure_ovn_routes(ovn_idl, peer_ips): prefix = '0.0.0.0/0' cmds = [] for ip in peer_ips: cmds.append(ovn_idl.lr_route_add(constants.OVN_CLUSTER_ROUTER, prefix, ip, ecmp=True, may_exist=True)) return cmds def _ensure_ingress_flows(bridge, mac, switch_name, provider_cidrs): # incomming traffic flows # patch=`ovs-ofctl show br-ex | grep patch | cut -d "(" -f1 | xargs` # ovs-ofctl add-flow br-ex # "cookie=0xbadcaf2,ip,nw_dst=$PROVIDER_NET,in_port=enp2s0,priority=100, # actions=mod_dl_dst:$ENP2S0_MAC,output=$patch" if not provider_cidrs: return patch_port_prefix = 'patch-{}-'.format(switch_name) patch_ports = ovs.get_ovs_patch_ports_info(bridge, prefix=patch_port_prefix) if not patch_ports: return bridge_ports = set(ovs.get_ovs_ports_info(bridge)) external_nic = list(bridge_ports.intersection( set(CONF.local_ovn_cluster.external_nics))) if not external_nic: LOG.warning("NIC ports (%s) not found for bridge %s. Not possible to " "create the ingress flows. It will be retried if " "reconcile cycle is not disabled", CONF.local_ovn_cluster.external_nics, bridge) return else: # only one external_nic expected per bridge external_nic = external_nic[0] for provider_cidr in provider_cidrs: ip_version = linux_net.get_ip_version(provider_cidr) if ip_version == constants.IP_VERSION_6: ip = 'ipv6' else: ip = 'ip' flow = ( "cookie={},priority=1000,{},nw_dst={},in_port={}," " actions=mod_dl_dst:{},output={}".format( constants.OVS_RULE_COOKIE, ip, provider_cidr, external_nic, mac, patch_ports[0])) ovs.ensure_flow(bridge, flow) def _ensure_egress_flows(bridge, patch_ports): # outcomming traffic flows # patch=`ovs-ofctl show br-provider | grep patch | grep provnet | # cut -d "(" -f1 | xargs` # ovs-ofctl add-flow br-provider "cookie=0xbadcaf3,ip,in_port=$patch, # actions=mod_dl_dst:$ROUTER_MAC,NORMAL" for patch_port in patch_ports: flow = ( "cookie={},priority=1000,ip,in_port={}," " actions=mod_dl_dst:{},NORMAL".format( constants.OVS_RULE_COOKIE, patch_port, constants.OVN_CLUSTER_ROUTER_INTERNAL_MAC)) flow_v6 = ( "cookie={},priority=1000,ipv6,in_port={}," " actions=mod_dl_dst:{},NORMAL".format( constants.OVS_RULE_COOKIE, patch_port, constants.OVN_CLUSTER_ROUTER_INTERNAL_MAC)) ovs.ensure_flow(bridge, flow) ovs.ensure_flow(bridge, flow_v6) def cleanup_wiring(idl, bridge_mappings, ovs_flows, exposed_ips, routing_tables, routing_tables_routes): if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY: return _cleanup_wiring_underlay(idl, bridge_mappings, ovs_flows, exposed_ips, routing_tables, routing_tables_routes) elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN: # TODO(ltomasbo): clean up old policies, routes and proxy_arps cidrs return True def _cleanup_wiring_underlay(idl, bridge_mappings, ovs_flows, exposed_ips, routing_tables, routing_tables_routes): current_ips = linux_net.get_exposed_ips(CONF.bgp_nic) expected_ips = [ip for ip_dict in exposed_ips.values() for ip in ip_dict.keys()] ips_to_delete = [ip for ip in current_ips if ip not in expected_ips] linux_net.delete_exposed_ips(ips_to_delete, CONF.bgp_nic) extra_routes = {} for bridge in bridge_mappings.values(): extra_routes[bridge] = ( linux_net.get_extra_routing_table_for_bridge(routing_tables, bridge)) # delete extra ovs flows ovs.remove_extra_ovs_flows(ovs_flows, bridge, constants.OVS_RULE_COOKIE) # get rules and delete the old ones ovn_ip_rules = linux_net.get_ovn_ip_rules(routing_tables.values()) if ovn_ip_rules: for ip in expected_ips: if len(ip.split("/")) == 1: ip_version = linux_net.get_ip_version(ip) if ip_version == constants.IP_VERSION_6: ip_dst = "{}/128".format(ip) else: ip_dst = "{}/32".format(ip) else: ip_dst = ip ovn_ip_rules.pop(ip_dst, None) linux_net.delete_ip_rules(ovn_ip_rules) # remove all the extra routes not needed linux_net.delete_bridge_ip_routes(routing_tables, routing_tables_routes, extra_routes) # delete leaked vlan devices from previous vlan provider networks delete_vlan_devices_leftovers(idl, bridge_mappings) def delete_vlan_devices_leftovers(idl, bridge_mappings): vlan_tags = idl.get_network_vlan_tags() ovs_devices = set(bridge_mappings.values()) for ovs_device in ovs_devices: vlans = linux_net.get_bridge_vlans(ovs_device) for vlan in vlans: if vlan and vlan not in vlan_tags: linux_net.delete_vlan_device_for_network(ovs_device, vlan) def wire_provider_port(routing_tables_routes, ovs_flows, port_ips, bridge_device, bridge_vlan, localnet, routing_table, proxy_cidrs, lladdr=None, mac=None, ovn_idl=None): if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY: return _wire_provider_port_underlay(routing_tables_routes, ovs_flows, port_ips, bridge_device, bridge_vlan, localnet, routing_table, proxy_cidrs, lladdr=lladdr) elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN: # 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, # while it should not return _wire_provider_port_ovn(ovn_idl, port_ips, mac) def unwire_provider_port(routing_tables_routes, port_ips, bridge_device, bridge_vlan, routing_table, proxy_cidrs, lladdr=None, ovn_idl=None): if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY: return _unwire_provider_port_underlay(routing_tables_routes, port_ips, bridge_device, bridge_vlan, routing_table, proxy_cidrs, lladdr=lladdr) elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN: # 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, # while it should not return _unwire_provider_port_ovn(ovn_idl, port_ips) def _ensure_updated_mac_tweak_flows(localnet, bridge_device, ovs_flows): ofport = ovs.get_ovs_patch_port_ofport(localnet) if ofport not in ovs_flows[bridge_device]['in_port']: ovs_flows[bridge_device]['in_port'].append(ofport) ovs.ensure_mac_tweak_flows(bridge_device, ovs_flows[bridge_device]['mac'], [ofport], constants.OVS_RULE_COOKIE) def _wire_provider_port_underlay(routing_tables_routes, ovs_flows, port_ips, bridge_device, bridge_vlan, localnet, routing_table, proxy_cidrs, lladdr=None): if not bridge_device: return False for ip in port_ips: try: if lladdr: dev = bridge_device if bridge_vlan: dev = '{}.{}'.format(dev, bridge_vlan) linux_net.add_ip_rule(ip, routing_table[bridge_device], dev=dev, lladdr=lladdr) else: linux_net.add_ip_rule(ip, routing_table[bridge_device]) except agent_exc.InvalidPortIP: LOG.exception("Invalid IP to create a rule for port on the " "provider network: %s", ip) return False linux_net.add_ip_route(routing_tables_routes, ip, routing_table[bridge_device], bridge_device, vlan=bridge_vlan) # add proxy ndp config for ipv6 for n_cidr in proxy_cidrs: if linux_net.get_ip_version(n_cidr) == constants.IP_VERSION_6: linux_net.add_ndp_proxy(n_cidr, bridge_device, bridge_vlan) # NOTE(ltomasbo): This is needed as the patch ports are not created # until the first VM/FIP in that provider network is created in a node try: _ensure_updated_mac_tweak_flows(localnet, bridge_device, ovs_flows) except agent_exc.PatchPortNotFound: LOG.warning("Patch port %s for bridge %s not found. Not possible to " "create the needed ovs flows for the outgoing traffic. " "It will be retried at the resync.", localnet, bridge_device) return False return True def _wire_provider_port_ovn(ovn_idl, port_ips, mac): cmds = [] port = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER) for port_ip in port_ips: cmds.append(ovn_idl.static_mac_binding_add( port, port_ip, mac, override_dynamic_mac=True, may_exist=True)) if cmds: _execute_commands(ovn_idl, cmds) # to keep it consisten with the underlay method return True def _unwire_provider_port_underlay(routing_tables_routes, port_ips, bridge_device, bridge_vlan, routing_table, proxy_cidrs, lladdr=None): if not bridge_device: return False for ip in port_ips: if lladdr: if linux_net.get_ip_version(ip) == constants.IP_VERSION_6: cr_lrp_ip = '{}/128'.format(ip) else: cr_lrp_ip = '{}/32'.format(ip) try: dev = bridge_device if bridge_vlan: dev = '{}.{}'.format(dev, bridge_vlan) linux_net.del_ip_rule(cr_lrp_ip, routing_table[bridge_device], dev=dev, lladdr=lladdr) except agent_exc.InvalidPortIP: LOG.exception("Invalid IP to delete a rule for the " "provider port: %s", cr_lrp_ip) return False else: try: linux_net.del_ip_rule(ip, routing_table[bridge_device]) except agent_exc.InvalidPortIP: LOG.exception("Invalid IP to delete a rule for the " "provider port: %s", ip) return False linux_net.del_ip_route(routing_tables_routes, ip, routing_table[bridge_device], bridge_device, vlan=bridge_vlan) for n_cidr in proxy_cidrs: if linux_net.get_ip_version(n_cidr) == constants.IP_VERSION_6: linux_net.del_ndp_proxy(n_cidr, bridge_device, bridge_vlan) return True def _unwire_provider_port_ovn(ovn_idl, port_ips): cmds = [] port = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER) for port_ip in port_ips: cmds.append(ovn_idl.static_mac_binding_del( port, port_ip, if_exists=True)) if cmds: _execute_commands(ovn_idl, cmds) # to keep it consisten with the underlay method return True def wire_lrp_port(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips): if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY: return _wire_lrp_port_underlay(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips) elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN: # TODO(ltomasbo): Add flow on br-ex(-X) # ovs-ofctl add-flow br-ex # "cookie=0xbadcaf2,ip,nw_dst=20.0.0.0/24,in_port=enp2s0,priority=100, # actions=mod_dl_dst:$ENP2S0_MAC,output=$patch" # Add router route to go through cr-lrp ip: # ovn-nbctl lr-route-add bgp-router 20.0.0.0/24 172.16.100.143 # bgp-router-public return def _wire_lrp_port_underlay(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips): if not bridge_device: return False LOG.debug("Adding IP Rules for network %s", ip) try: linux_net.add_ip_rule(ip, routing_tables[bridge_device]) except agent_exc.InvalidPortIP: LOG.exception("Invalid IP to create a rule for the lrp (network " "router interface) port: %s", ip) return False LOG.debug("Added IP Rules for network %s", ip) LOG.debug("Adding IP Routes for network %s", ip) # NOTE(ltomasbo): This assumes the provider network can only have # (at most) 2 subnets, one for IPv4, one for IPv6 ip_version = linux_net.get_ip_version(ip) for cr_lrp_ip in cr_lrp_ips: if linux_net.get_ip_version(cr_lrp_ip) == ip_version: linux_net.add_ip_route( routing_tables_routes, ip.split("/")[0], routing_tables[bridge_device], bridge_device, vlan=bridge_vlan, mask=ip.split("/")[1], via=cr_lrp_ip) break LOG.debug("Added IP Routes for network %s", ip) return True def unwire_lrp_port(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips): if CONF.exposing_method == constants.EXPOSE_METHOD_UNDERLAY: return _unwire_lrp_port_underlay(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips) elif CONF.exposing_method == constants.EXPOSE_METHOD_OVN: # TODO(ltomasbo): Remove flow(s) and router route return def _unwire_lrp_port_underlay(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips): if not bridge_device: return False LOG.debug("Deleting IP Rules for network %s", ip) try: linux_net.del_ip_rule(ip, routing_tables[bridge_device]) except agent_exc.InvalidPortIP: LOG.exception("Invalid IP to delete a rule for the " "lrp (network router interface) port: %s", ip) return False LOG.debug("Deleted IP Rules for network %s", ip) LOG.debug("Deleting IP Routes for network %s", ip) ip_version = linux_net.get_ip_version(ip) for cr_lrp_ip in cr_lrp_ips: if linux_net.get_ip_version(cr_lrp_ip) == ip_version: linux_net.del_ip_route( routing_tables_routes, ip.split("/")[0], routing_tables[bridge_device], bridge_device, vlan=bridge_vlan, mask=ip.split("/")[1], via=cr_lrp_ip) LOG.debug("Deleted IP Routes for network %s", ip) return True ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/watchers/000077500000000000000000000000001460327367600242715ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/watchers/__init__.py000066400000000000000000000000001460327367600263700ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py000066400000000000000000000122321460327367600272720ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_log import log as logging from ovsdbapp.backend.ovs_idl import event as row_event from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.utils import driver_utils LOG = logging.getLogger(__name__) class Event(row_event.RowEvent): def run(self, *args, **kwargs): try: self._run(*args, **kwargs) except Exception: LOG.exception("Unexpected exception while running the event " "action") class PortBindingChassisEvent(Event): def __init__(self, bgp_agent, events): self.agent = bgp_agent table = 'Port_Binding' super(PortBindingChassisEvent, self).__init__( events, table, None) self.event_name = self.__class__.__name__ def _check_ip_associated(self, mac): return len(mac.strip().split(' ')) > 1 class OVNLBEvent(Event): def __init__(self, bgp_agent, events): self.agent = bgp_agent table = 'Load_Balancer' super(OVNLBEvent, self).__init__( events, table, None) self.event_name = self.__class__.__name__ def _get_router(self, row, key=constants.OVN_LB_LR_REF_EXT_ID_KEY): try: return row.external_ids[key].replace('neutron-', "", 1) except (AttributeError, KeyError): return def _get_ip_from_vips(self, row): return [driver_utils.remove_port_from_ip(ipport) for ipport in getattr(row, 'vips', {}).keys()] def _get_diff_ip_from_vips(self, new, old): """Returns a list of IPs that are present in 'new' but not in 'old' Note: As LB VIP contains a port (e.g., '192.168.1.1:80'), the port part is removed before comparison. """ return list(set(self._get_ip_from_vips(new)) - set(self._get_ip_from_vips(old))) def _is_vip_or_fip(self, row, ip, key): try: return ip == row.external_ids.get(key) except AttributeError: pass def _is_vip(self, row, ip): return self._is_vip_or_fip(row, ip, constants.OVN_LB_VIP_IP_EXT_ID_KEY) def _is_fip(self, row, ip): return self._is_vip_or_fip(row, ip, constants.OVN_LB_VIP_FIP_EXT_ID_KEY) class LSPChassisEvent(Event): def __init__(self, bgp_agent, events): self.agent = bgp_agent table = 'Logical_Switch_Port' super(LSPChassisEvent, self).__init__( events, table, None) self.event_name = self.__class__.__name__ def _check_ip_associated(self, mac): return len(mac.strip().split(' ')) > 1 def _get_chassis(self, row, default_type=constants.OVN_VM_VIF_PORT_TYPE): return driver_utils.get_port_chassis(row, self.agent.chassis, default_port_type=default_type) def _has_additional_binding(self, row): if (hasattr(row, 'options') and row.options.get(constants.OVN_REQUESTED_CHASSIS)): # requested-chassis can be a comma separated list, so if there # is a comma in the string, there is an additional binding. return ',' in row.options[constants.OVN_REQUESTED_CHASSIS] return False def _get_network(self, row): try: return row.external_ids[constants.OVN_LS_NAME_EXT_ID_KEY] except (AttributeError, KeyError): return def _get_ips_info(self, row): return { 'mac': row.addresses[0].strip().split(' ')[0], 'cidrs': row.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "").split(), 'type': row.type, 'logical_switch': self._get_network(row), } def _get_port_fip(self, row): return getattr(row, 'external_ids', {}).get( constants.OVN_FIP_EXT_ID_KEY) class LRPChassisEvent(Event): def __init__(self, bgp_agent, events): self.agent = bgp_agent table = 'Logical_Router_Port' super(LRPChassisEvent, self).__init__( events, table, None) self.event_name = self.__class__.__name__ def _get_network(self, row): try: return row.external_ids[constants.OVN_LS_NAME_EXT_ID_KEY] except (AttributeError, KeyError): return def _get_ips_info(self, row): return { 'mac': row.mac, 'cidrs': row.networks, 'type': constants.OVN_CR_LRP_PORT_TYPE, 'logical_switch': self._get_network(row), 'router': row.external_ids.get(constants.OVN_LR_NAME_EXT_ID_KEY), } ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py000066400000000000000000000470611460327367600271400ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_concurrency import lockutils from oslo_config import cfg from oslo_log import log as logging from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.watchers import base_watcher from ovn_bgp_agent.utils import helpers CONF = cfg.CONF LOG = logging.getLogger(__name__) _SYNC_STATE_LOCK = lockutils.ReaderWriterLock() class PortBindingChassisCreatedEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE,) super(PortBindingChassisCreatedEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: # single and dual-stack format if not self._check_ip_associated(row.mac[0]): return False if not bool(row.up[0]): return False if row.chassis[0].name != self.agent.chassis: return False if hasattr(old, 'chassis'): if not old.chassis or row.chassis != old.chassis: return True if hasattr(old, 'up'): if not bool(old.up[0]): return True except (IndexError, AttributeError): return False def _run(self, event, row, old): if row.type not in constants.OVN_VIF_PORT_TYPES: return with _SYNC_STATE_LOCK.read_lock(): ips = row.mac[0].split(' ')[1:] self.agent.expose_ip(ips, row) class PortBindingChassisDeletedEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE, self.ROW_DELETE,) super(PortBindingChassisDeletedEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: # single and dual-stack format if not self._check_ip_associated(row.mac[0]): return False if event == self.ROW_DELETE: return row.chassis[0].name == self.agent.chassis if hasattr(old, 'chassis'): if (old.chassis[0].name == self.agent.chassis and (not row.chassis or row.chassis != old.chassis)): return True if hasattr(old, 'up'): # this requires to have unchanged chassis and being the local # one. If there was a chassis change, then it was already # processed before if (row.chassis[0].name == self.agent.chassis and bool(old.up[0]) and not bool(row.up[0])): return True except (IndexError, AttributeError): return False return False def _run(self, event, row, old): if row.type not in constants.OVN_VIF_PORT_TYPES: return with _SYNC_STATE_LOCK.read_lock(): ips = row.mac[0].split(' ')[1:] self.agent.withdraw_ip(ips, row) class FIPSetEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE,) super(FIPSetEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: return (not row.chassis and row.nat_addresses != old.nat_addresses and not row.logical_port.startswith('lrp-')) except (AttributeError): return False def _run(self, event, row, old): if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): # NOTE(ltomasbo): nat_addresses has the same format, where # different IPs can be present: # ["fa:16:3e:77:7f:9c 172.24.100.229 172.24.100.112 # is_chassis_resident(\" # cr-lrp-add962d2-21ab-4733-b6ef-35538eff25a8\")"] old_cr_lrps = {} for nat in old.nat_addresses: ips = nat.strip().split(" ")[1:-1] port = nat.strip().split(" ")[-1].split("\"")[1] old_cr_lrps.setdefault(port, set()).update(ips) for nat in row.nat_addresses: ips = nat.strip().split(" ")[1:-1] port = nat.strip().split(" ")[-1].split("\"")[1] ips_to_expose = [ip for ip in ips if ip not in old_cr_lrps.get(port, set())] if ips_to_expose: self.agent.expose_ip(ips_to_expose, row, associated_port=port) class FIPUnsetEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE,) super(FIPUnsetEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: return (not row.chassis and row.nat_addresses != old.nat_addresses and not row.logical_port.startswith('lrp-')) except (AttributeError): return False def _run(self, event, row, old): if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): # NOTE(ltomasbo): nat_addresses has the same format, where # different IPs can be present: # ["fa:16:3e:77:7f:9c 172.24.100.229 172.24.100.112 # is_chassis_resident(\" # cr-lrp-add962d2-21ab-4733-b6ef-35538eff25a8\")"] current_cr_lrps = {} for nat in row.nat_addresses: ips = nat.strip().split(" ")[1:-1] port = nat.strip().split(" ")[-1].split("\"")[1] current_cr_lrps.setdefault(port, set()).update(ips) for nat in old.nat_addresses: ips = nat.strip().split(" ")[1:-1] port = nat.strip().split(" ")[-1].split("\"")[1] ips_to_withdraw = [ip for ip in ips if ip not in current_cr_lrps.get(port, set())] if ips_to_withdraw: self.agent.withdraw_ip(ips_to_withdraw, row, associated_port=port) class SubnetRouterAttachedEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_CREATE,) super(SubnetRouterAttachedEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: # single and dual-stack format if not self._check_ip_associated(row.mac[0]): return False return (not row.chassis and row.logical_port.startswith('lrp-') and "chassis-redirect-port" not in row.options.keys()) except (IndexError, AttributeError): return False def _run(self, event, row, old): if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): ip_address = row.mac[0].split(' ')[1] self.agent.expose_subnet(ip_address, row) class SubnetRouterUpdateEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE,) super(SubnetRouterUpdateEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): # This will match if the mac field has changed between old and row. # This can happen when you have multiple subnets in the same network, # those will be added/removed to/from the same lrp-port in the mac # field. # Format: # mac = [ff:ff:ff:ff:ff:ff subnet1/cidr subnet2/cidr [...]] try: # single and dual-stack format if (not self._check_ip_associated(row.mac[0]) and not self._check_ip_associated(old.mac[0])): return False return ( not row.chassis and row.logical_port.startswith("lrp-") and "chassis-redirect-port" not in row.options.keys() and old.mac != row.mac ) except (IndexError, AttributeError): return False def _run(self, event, row, old): if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): self.agent.update_subnet(old, row) class SubnetRouterDetachedEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_DELETE,) super(SubnetRouterDetachedEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: # single and dual-stack format if not self._check_ip_associated(row.mac[0]): return False return (not row.chassis and row.logical_port.startswith('lrp-') and "chassis-redirect-port" not in row.options.keys()) except (IndexError, AttributeError): return False def _run(self, event, row, old): if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): ip_address = row.mac[0].split(' ')[1] self.agent.withdraw_subnet(ip_address, row) class TenantPortCreatedEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE,) super(TenantPortCreatedEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: # Handling the case for unknown MACs when configdrive is used # instead of dhcp if row.mac == ['unknown']: n_cidrs = row.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "").split() if not n_cidrs: return False # single and dual-stack format elif not self._check_ip_associated(row.mac[0]): return False return (not old.chassis and row.chassis and self.agent.ovn_local_lrps != []) except (IndexError, AttributeError): return False def _run(self, event, row, old): if row.type not in (constants.OVN_VM_VIF_PORT_TYPE, constants.OVN_VIRTUAL_VIF_PORT_TYPE): return with _SYNC_STATE_LOCK.read_lock(): if row.mac == ['unknown']: # Handling the case for unknown MACs when configdrive is used # instead of dhcp n_cidrs = row.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "") ips = [ip.split("/")[0] for ip in n_cidrs.split(" ")] else: ips = row.mac[0].split(' ')[1:] self.agent.expose_remote_ip(ips, row) class TenantPortDeletedEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE, self.ROW_DELETE,) super(TenantPortDeletedEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: if row.mac == ['unknown']: # Handling the case for unknown MACs when configdrive is used # instead of dhcp n_cidrs = row.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "").split() if not n_cidrs: return False # single and dual-stack format elif not self._check_ip_associated(row.mac[0]): return False if event == self.ROW_UPDATE: return (old.chassis and not row.chassis and self.agent.ovn_local_lrps != []) if event == self.ROW_DELETE: return (self.agent.ovn_local_lrps != []) except (IndexError, AttributeError): return False def _run(self, event, row, old): if row.type not in (constants.OVN_VM_VIF_PORT_TYPE, constants.OVN_VIRTUAL_VIF_PORT_TYPE): return if event == self.ROW_UPDATE: chassis = old.chassis else: chassis = row.chassis with _SYNC_STATE_LOCK.read_lock(): if row.mac == ['unknown']: # Handling the case for unknown MACs when configdrive is used # instead of dhcp n_cidrs = row.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "") ips = [ip.split("/")[0] for ip in n_cidrs.split(" ")] else: ips = row.mac[0].split(' ')[1:] self.agent.withdraw_remote_ip(ips, row, chassis) class OVNLBVIPPortEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_CREATE, self.ROW_DELETE,) super(OVNLBVIPPortEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): # The ovn lb balancers are exposed through the cr-lrp, so if the # local agent does not have any cr-lrp associated there is no need # to process the event try: # it should not have mac, no chassis, and status down if not row.mac and not row.chassis and not row.up[0]: return bool(self.agent.ovn_local_cr_lrps) return False except (IndexError, AttributeError): return False def _run(self, event, row, old): if row.type != constants.OVN_VM_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): # This is depending on the external-id information added by # neutron, regarding the neutron:cidrs ext_n_cidr = row.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "") if not ext_n_cidr: return ovn_lb_ip = ext_n_cidr.strip().split(" ")[0].split("/")[0] if event == self.ROW_DELETE: self.agent.withdraw_ovn_lb(ovn_lb_ip, row) if event == self.ROW_CREATE: self.agent.expose_ovn_lb(ovn_lb_ip, row) class OVNLBMemberCreateEvent(base_watcher.OVNLBEvent): def __init__(self, bgp_agent): events = (self.ROW_CREATE,) super(OVNLBMemberCreateEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): # Only process event if the local node has a cr-lrp ports associated return bool(self.agent.ovn_local_cr_lrps) def _run(self, event, row, old): # Only process event if the local node has a cr-lrp port whose provider # datapath is included into the loadbalancer. This means the # loadbalancer has the VIP on a provider network # Also, the cr-lrp port needs to have subnet datapaths (LS) associated # to it that include the load balancer if not self.agent.ovn_local_cr_lrps: return try: row_dp = row.datapaths except AttributeError: row_dp = [] row_dp, router_dps = helpers.get_lb_datapath_groups(row) if not row_dp: # No need to continue. There is no need to expose it as there is # no datapaths (aka members). return vip_port = self.agent.sb_idl.get_ovn_vip_port(row.name) if not vip_port: return vip_ip = vip_port.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "") if not vip_ip: return vip_ip = vip_ip.strip().split(" ")[0].split("/")[0] associated_cr_lrp_port = None if not router_dps and not (CONF.expose_tenant_networks or CONF.expose_ipv6_gua_tenant_networks): # assume all the members are connected through the same router # so only one member needs to be checked member_dp = row_dp[0] # get lrps on that dp (patch ports) router_lrps = ( self.agent.sb_idl.get_lrps_for_datapath(member_dp)) for lrp in router_lrps: router_dps.append(self.agent.sb_idl.get_port_datapath(lrp)) for cr_lrp_port, cr_lrp_info in self.agent.ovn_local_cr_lrps.items(): if vip_port.datapath != cr_lrp_info.get('provider_datapath'): continue if cr_lrp_info.get('subnets_datapath'): if set(row_dp).intersection(set( cr_lrp_info.get('subnets_datapath').values())): associated_cr_lrp_port = cr_lrp_port break else: if cr_lrp_info.get('router_datapath') in router_dps: associated_cr_lrp_port = cr_lrp_port break else: return with _SYNC_STATE_LOCK.read_lock(): return self.agent.expose_ovn_lb_on_provider( vip_ip, row.name, associated_cr_lrp_port) class OVNLBMemberDeleteEvent(base_watcher.OVNLBEvent): def __init__(self, bgp_agent): events = (self.ROW_DELETE,) super(OVNLBMemberDeleteEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): # Only process event if the local node has the lb exported return bool(self.agent.provider_ovn_lbs.get(row.name)) def _run(self, event, row, old): associated_cr_lrp_port = self.agent.provider_ovn_lbs[row.name].get( 'gateway_port') if not associated_cr_lrp_port: # Something is wrong, not enough information to proceed return with _SYNC_STATE_LOCK.read_lock(): # loadbalancer deleted. Withdraw the VIP through the cr-lrp return self.agent.withdraw_ovn_lb_on_provider( row.name, associated_cr_lrp_port) class LocalnetCreateDeleteEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_CREATE, self.ROW_DELETE,) super(LocalnetCreateDeleteEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): return row.type == constants.OVN_LOCALNET_VIF_PORT_TYPE def _run(self, event, row, old): with _SYNC_STATE_LOCK.read_lock(): self.agent.sync() class ChassisCreateEventBase(base_watcher.Event): table = None def __init__(self, bgp_agent): self.agent = bgp_agent self.first_time = True events = (self.ROW_CREATE,) super(ChassisCreateEventBase, self).__init__( events, self.table, (('name', '=', self.agent.chassis),)) self.event_name = self.__class__.__name__ def _run(self, event, row, old): if self.first_time: self.first_time = False else: LOG.info("Connection to OVSDB established, doing a full sync") self.agent.sync() class ChassisCreateEvent(ChassisCreateEventBase): table = 'Chassis' class ChassisPrivateCreateEvent(ChassisCreateEventBase): table = 'Chassis_Private' ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py000066400000000000000000000215441460327367600273360ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_concurrency import lockutils from oslo_log import log as logging from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.watchers import base_watcher LOG = logging.getLogger(__name__) _SYNC_STATE_LOCK = lockutils.ReaderWriterLock() class PortBindingChassisCreatedEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE,) super(PortBindingChassisCreatedEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: if row.type != constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE: return False # single and dual-stack format if not self._check_ip_associated(row.mac[0]): return False return (row.chassis[0].name == self.agent.chassis and not old.chassis) except (IndexError, AttributeError): return False def _run(self, event, row, old): if row.type != constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): self.agent.expose_ip(row, cr_lrp=True) class PortBindingChassisDeletedEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE, self.ROW_DELETE,) super(PortBindingChassisDeletedEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: if row.type != constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE: return False # single and dual-stack format if not self._check_ip_associated(row.mac[0]): return False if event == self.ROW_UPDATE: return (old.chassis[0].name == self.agent.chassis and not row.chassis) else: return row.chassis[0].name == self.agent.chassis except (IndexError, AttributeError): return False def _run(self, event, row, old): if row.type != constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): self.agent.withdraw_ip(row, cr_lrp=True) class SubnetRouterAttachedEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE, self.ROW_CREATE,) super(SubnetRouterAttachedEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: if event == self.ROW_UPDATE: return (not row.chassis and not row.logical_port.startswith('lrp-') and row.external_ids[constants.OVN_EVPN_VNI_EXT_ID_KEY] and row.external_ids[constants.OVN_EVPN_AS_EXT_ID_KEY] and (not old.external_ids.get( constants.OVN_EVPN_VNI_EXT_ID_KEY) or not old.external_ids.get( constants.constants.OVN_EVPN_AS_EXT_ID_KEY))) else: return (not row.chassis and not row.logical_port.startswith('lrp-') and row.external_ids[constants.OVN_EVPN_VNI_EXT_ID_KEY] and row.external_ids[constants.OVN_EVPN_AS_EXT_ID_KEY]) except (IndexError, AttributeError, KeyError): return False def _run(self, event, row, old): if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): if row.nat_addresses: self.agent.expose_ip(row) else: self.agent.expose_subnet(row) class SubnetRouterDetachedEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE, self.ROW_DELETE,) super(SubnetRouterDetachedEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: if event == self.ROW_UPDATE: return (not row.chassis and not row.logical_port.startswith('lrp-') and old.external_ids[constants.OVN_EVPN_VNI_EXT_ID_KEY] and old.external_ids[constants.OVN_EVPN_AS_EXT_ID_KEY] and (not row.external_ids.get( constants.OVN_EVPN_VNI_EXT_ID_KEY) or not row.external_ids.get( constants.OVN_EVPN_AS_EXT_ID_KEY))) else: return (not row.chassis and not row.logical_port.startswith('lrp-') and row.external_ids[constants.OVN_EVPN_VNI_EXT_ID_KEY] and row.external_ids[constants.OVN_EVPN_AS_EXT_ID_KEY]) except (IndexError, AttributeError, KeyError): return False def _run(self, event, row, old): if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): if row.nat_addresses: self.agent.withdraw_ip(row) else: self.agent.withdraw_subnet(row) class TenantPortCreatedEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE,) super(TenantPortCreatedEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: # single and dual-stack format if not self._check_ip_associated(row.mac[0]): return False return (not old.chassis and row.chassis and self.agent.ovn_local_lrps != []) except (IndexError, AttributeError): return False def _run(self, event, row, old): if row.type not in (constants.OVN_VM_VIF_PORT_TYPE, constants.OVN_VIRTUAL_VIF_PORT_TYPE): return with _SYNC_STATE_LOCK.read_lock(): ips = row.mac[0].split(' ')[1:] self.agent.expose_remote_ip(ips, row) class TenantPortDeletedEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_DELETE, self.ROW_UPDATE,) super(TenantPortDeletedEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: # single and dual-stack format if not self._check_ip_associated(row.mac[0]): return False if event == self.ROW_UPDATE: return (old.chassis and not row.chassis and self.agent.ovn_local_lrps != []) else: return (self.agent.ovn_local_lrps != []) except (IndexError, AttributeError): return False def _run(self, event, row, old): if row.type not in (constants.OVN_VM_VIF_PORT_TYPE, constants.OVN_VIRTUAL_VIF_PORT_TYPE): return with _SYNC_STATE_LOCK.read_lock(): ips = row.mac[0].split(' ')[1:] self.agent.withdraw_remote_ip(ips, row) class LocalnetCreateDeleteEvent(base_watcher.PortBindingChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_CREATE, self.ROW_DELETE,) super(LocalnetCreateDeleteEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): return row.type == constants.OVN_LOCALNET_VIF_PORT_TYPE def _run(self, event, row, old): with _SYNC_STATE_LOCK.read_lock(): self.agent.sync() class ChassisCreateEventBase(base_watcher.Event): table = None def __init__(self, bgp_agent): self.agent = bgp_agent self.first_time = True events = (self.ROW_CREATE,) super(ChassisCreateEventBase, self).__init__( events, self.table, (('name', '=', self.agent.chassis),)) self.event_name = self.__class__.__name__ def _run(self, event, row, old): if self.first_time: self.first_time = False else: LOG.info("Connection to OVSDB established, doing a full sync") self.agent.sync() class ChassisCreateEvent(ChassisCreateEventBase): table = 'Chassis' class ChassisPrivateCreateEvent(ChassisCreateEventBase): table = 'Chassis_Private' ovn-bgp-agent-2.0.1/ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py000066400000000000000000000754651460327367600276300ustar00rootroot00000000000000# Copyright 2023 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_concurrency import lockutils 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.watchers import base_watcher LOG = logging.getLogger(__name__) _SYNC_STATE_LOCK = lockutils.ReaderWriterLock() class LogicalSwitchPortProviderCreateEvent(base_watcher.LSPChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE,) super(LogicalSwitchPortProviderCreateEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): '''Match port updates to see if we should expose this lsp If the event matches the following criteria, we should totally ignore this event, since it is not meant for this host. 1. this host does not own this lsp 2. the lsp is not up 3. the logical switch is not exposed with agent, which means it is not a provider network When the event still has not been rejected, then the only thing to do is to check if the ips for this lsp have not been exported yet. ''' if row.type not in [constants.OVN_VM_VIF_PORT_TYPE, constants.OVN_VIRTUAL_VIF_PORT_TYPE]: return try: # single and dual-stack format if not self._check_ip_associated(row.addresses[0]): return False current_chassis, _ = self._get_chassis(row) logical_switch = self._get_network(row) if logical_switch in self.agent.ovn_local_lrps: # This is a tenant network, routed through lrp, handled by # event LogicalSwitchPortTenantCreateEvent return False # Check for rejection criteria if (current_chassis != self.agent.chassis or not bool(row.up[0]) or not self.agent.is_ls_provider(logical_switch)): return False # At this point, the port is bound on this host, it is up and # the logical switch is exposable by the agent. # Only create the ips if not already exposed. ips = row.addresses[0].split(' ')[1:] return not self.agent.is_ip_exposed(logical_switch, ips) except (IndexError, AttributeError): return False def _run(self, event, row, old): with _SYNC_STATE_LOCK.read_lock(): ips = row.addresses[0].split(' ')[1:] ips_info = self._get_ips_info(row) self.agent.expose_ip(ips, ips_info) class LogicalSwitchPortProviderDeleteEvent(base_watcher.LSPChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE, self.ROW_DELETE,) super(LogicalSwitchPortProviderDeleteEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): '''Match port deletes or port downs or migrations 1. [DELETE] Port has been deleted, and we're hosting it 2. [UPDATE] Port went down, withdraw if we announced it 3. [UPDATE] Port has been migrated away and we're hosting it ''' if row.type not in [constants.OVN_VM_VIF_PORT_TYPE, constants.OVN_VIRTUAL_VIF_PORT_TYPE]: return try: # single and dual-stack format if not self._check_ip_associated(row.addresses[0]): return False ips = row.addresses[0].split(' ')[1:] logical_switch = self._get_network(row) if logical_switch in self.agent.ovn_local_lrps: # This is a tenant network, routed through lrp, handled by # event LogicalSwitchPortTenantDeleteEvent return False # Do nothing if we do not expose the current port if not self.agent.is_ip_exposed(logical_switch, ips): return False # Delete event, always execute (since we expose it) if event == self.ROW_DELETE: return True current_chassis, _ = self._get_chassis(row) # Delete the port from current chassis, if # 1. port went down (while only attached here) if (hasattr(old, 'up') and bool(old.up[0]) and # port was up not bool(row.up[0]) and # is now down not self._has_additional_binding(row)): # and bound here return True # 2. port no longer bound here return current_chassis != self.agent.chassis except (IndexError, AttributeError): return False def _run(self, event, row, old): with _SYNC_STATE_LOCK.read_lock(): ips = row.addresses[0].split(' ')[1:] ips_info = self._get_ips_info(row) self.agent.withdraw_ip(ips, ips_info) class LogicalSwitchPortFIPCreateEvent(base_watcher.LSPChassisEvent): '''Floating IP create events based on the LogicalSwitchPort The LSP has information about the host is should be exposed to, which adds a bit of complexity in the event match, but saves a lot of queries to the OVN NB DB. Should trigger on: - floating ip was attached to a lsp (external_ids.neutron:port_fip appeared with information) - port with floating ip attached was set to up (old.up = false and row.up = true) During a migration of a lsp, the following events happen (chronologically): 1. options.requested_chassis is updated (now a comma separated list) we also get external_ids, but only revision_number is updated. 2. update with only external_ids update (with only a revnum update) 3. port is set down (by ovn-controller on source host) 4. update with only external_ids update (with only a revnum update) 5. external_ids update (neutron:host_id is removed) 6. options.requested_chassis is updated (with only dest host) and external_ids update which now includes neutron:host_id again 7. port is set up (by ovn-controller on dest host) 8 and 9 are only a revnum update in the external_ids So for migration flow we are only interested in event 7. Otherwise the floating ip would be added upon event 2, deleted with event 3 and re-added with event 7. ''' def __init__(self, bgp_agent): events = (self.ROW_UPDATE,) super(LogicalSwitchPortFIPCreateEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): if row.type not in [constants.OVN_VM_VIF_PORT_TYPE, constants.OVN_VIRTUAL_VIF_PORT_TYPE]: return try: # single and dual-stack format if not self._check_ip_associated(row.addresses[0]): return False current_chassis, _ = self._get_chassis(row) current_port_fip = row.external_ids.get( constants.OVN_FIP_EXT_ID_KEY) if (current_chassis != self.agent.chassis or not bool(row.up[0]) or not current_port_fip): # Port is not bound on this host, is down or does not have a # floating ip attached. return False if hasattr(old, 'up') and not bool(old.up[0]): # Port changed up, which happens when the port is picked up # on this host by the ovn-controller during migrations return True old_port_fip = getattr(old, 'external_ids', {}).get( constants.OVN_FIP_EXT_ID_KEY) if old_port_fip == current_port_fip: # Only if the floating ip has changed (for example from empty # to something else) we need to process this update. # If nothing else changed in the external_ids, we do not care # as it would just cause unnecessary events during migrations. # (see the docstring of this class) return False # Check if the current port_fip has not been exposed yet return not self.agent.is_ip_exposed(self._get_network(row), current_port_fip) except (IndexError, AttributeError): return False def _run(self, event, row, old): external_ip, external_mac, ls_name = ( self.agent.get_port_external_ip_and_ls(row.name)) if not external_ip or not ls_name: return with _SYNC_STATE_LOCK.read_lock(): self.agent.expose_fip(external_ip, external_mac, ls_name, row) class LogicalSwitchPortFIPDeleteEvent(base_watcher.LSPChassisEvent): '''Floating IP delete events based on the LogicalSwitchPort The LSP has information about the host is should be exposed to, which adds a bit of complexity in the event match, but saves a lot of queries to the OVN NB DB. Should trigger on: - lsp deleted and bound on this host - floating ip removed from a lsp (external_ids.neutron:port_fip disappeared with information) - port with floating ip attached was set to down (old.up = true and row.up = false) - current floating ip is not the same as old floating ip ''' def __init__(self, bgp_agent): events = (self.ROW_UPDATE, self.ROW_DELETE,) super(LogicalSwitchPortFIPDeleteEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): '''Match port deletes or port downs or migrations or fip changes 1. [DELETE] Port has been deleted, and we're hosting it 2. [UPDATE] Port went down, withdraw if we announced it 3. [UPDATE] Floating IP has been disassociated (or re-associated with another floating ip) 4. [UPDATE] Port has been migrated away and we're hosting it ''' if row.type not in [constants.OVN_VM_VIF_PORT_TYPE, constants.OVN_VIRTUAL_VIF_PORT_TYPE]: return try: # single and dual-stack format if not self._check_ip_associated(row.addresses[0]): return False current_port_fip = self._get_port_fip(row) old_port_fip = self._get_port_fip(old) if not current_port_fip and not old_port_fip: # This port is not a floating ip update return False logical_switch = self._get_network(row) is_exposed = self.agent.is_ip_exposed(logical_switch, old_port_fip or current_port_fip) if not is_exposed: # already deleted or not exposed. return False # From here on we know we are exposing a FIP (either old or # current) if event == self.ROW_DELETE: # Port is deleting return True if (hasattr(old, 'up') and bool(old.up[0]) and # port was up not bool(row.up[0])): # is now down return True if old_port_fip is not None and current_port_fip != old_port_fip: # fip has changed, we should remove the old one. return True # If we reach here, just check if host changed current_chassis, _ = self._get_chassis(row) return current_chassis != self.agent.chassis except (IndexError, AttributeError): return False def _run(self, event, row, old): # First check to remove the fip provided in old (since this might # have been updated) fip = self._get_port_fip(old) if not fip: # Remove the fip provided in the current row, probably a # disassociate of the fip (or a down or a move) fip = self._get_port_fip(row) if not fip: return with _SYNC_STATE_LOCK.read_lock(): self.agent.withdraw_fip(fip, row) class LocalnetCreateDeleteEvent(base_watcher.LSPChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_CREATE, self.ROW_DELETE,) super(LocalnetCreateDeleteEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): if row.type == constants.OVN_LOCALNET_VIF_PORT_TYPE: return True return False def _run(self, event, row, old): with _SYNC_STATE_LOCK.read_lock(): self.agent.sync() class ChassisRedirectCreateEvent(base_watcher.LRPChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE,) super(ChassisRedirectCreateEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: if not row.networks: return False # check if hosting-chassis is being added hosting_chassis = row.status.get(constants.OVN_STATUS_CHASSIS) if hosting_chassis != self.agent.chassis_id: # No chassis set or different one return False if hasattr(old, 'status'): # status has changed old_hosting_chassis = old.status.get( constants.OVN_STATUS_CHASSIS) if old_hosting_chassis != hosting_chassis: return True except (IndexError, AttributeError): return False return False def _run(self, event, row, old): with _SYNC_STATE_LOCK.read_lock(): ips_info = self._get_ips_info(row) ips = [net.split("/")[0] for net in row.networks] self.agent.expose_ip(ips, ips_info) class ChassisRedirectDeleteEvent(base_watcher.LRPChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE, self.ROW_DELETE,) super(ChassisRedirectDeleteEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: if not row.networks: return if event == self.ROW_DELETE: return (row.status.get(constants.OVN_STATUS_CHASSIS) == self.agent.chassis_id) # ROW UPDATE EVENT if hasattr(old, 'status'): # status has changed hosting_chassis = row.status.get(constants.OVN_STATUS_CHASSIS) old_hosting_chassis = old.status.get( constants.OVN_STATUS_CHASSIS) if (hosting_chassis != old_hosting_chassis and old_hosting_chassis == self.agent.chassis_id): return True except (IndexError, AttributeError): return False return False def _run(self, event, row, old): with _SYNC_STATE_LOCK.read_lock(): ips_info = self._get_ips_info(row) ips = [net.split("/")[0] for net in row.networks] self.agent.withdraw_ip(ips, ips_info) class LogicalSwitchPortSubnetAttachEvent(base_watcher.LSPChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE,) super(LogicalSwitchPortSubnetAttachEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: if row.type != constants.OVN_ROUTER_PORT_TYPE: return False # skip route_gateway port events row_device_owner = row.external_ids.get( constants.OVN_DEVICE_OWNER_EXT_ID_KEY) if row_device_owner != constants.OVN_ROUTER_INTERFACE: return False if not bool(row.up[0]): return False associated_router = row.external_ids.get( constants.OVN_DEVICE_ID_EXT_ID_KEY) if associated_router not in self.agent.ovn_local_cr_lrps: return False if hasattr(old, 'up') and not bool(old.up[0]): return True if hasattr(old, 'external_ids'): previous_associated_router = old.external_ids.get( constants.OVN_DEVICE_ID_EXT_ID_KEY) if (associated_router != previous_associated_router and previous_associated_router not in self.agent.ovn_local_cr_lrps): return True except (IndexError, AttributeError): return False return False def _run(self, event, row, old): with _SYNC_STATE_LOCK.read_lock(): ips = row.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "").split() subnet_info = { 'associated_router': row.external_ids.get( constants.OVN_DEVICE_ID_EXT_ID_KEY), 'network': self._get_network(row), 'address_scopes': driver_utils.get_addr_scopes(row)} self.agent.expose_subnet(ips, subnet_info) class LogicalSwitchPortSubnetDetachEvent(base_watcher.LSPChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE, self.ROW_DELETE,) super(LogicalSwitchPortSubnetDetachEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: if row.type != constants.OVN_ROUTER_PORT_TYPE: return False # skip route_gateway port events row_device_owner = row.external_ids.get( constants.OVN_DEVICE_OWNER_EXT_ID_KEY) if row_device_owner != constants.OVN_ROUTER_INTERFACE: return False associated_router = row.external_ids.get( constants.OVN_DEVICE_ID_EXT_ID_KEY) if event == self.ROW_DELETE: if not bool(row.up[0]): return False if associated_router in self.agent.ovn_local_cr_lrps: return True return False # ROW UPDATE # We need to withdraw the subnet in the next cases: # 1. same/local associated router and status moves from up to down # 2. status changes to down and also associated router changes to a # non local one # 3. status is up (same) but associated router changes to a non # local one if hasattr(old, 'up'): if not bool(old.up[0]): return False if hasattr(old, 'external_ids'): previous_associated_router = old.external_ids.get( constants.OVN_DEVICE_ID_EXT_ID_KEY) if previous_associated_router in ( self.agent.ovn_local_cr_lrps): return True else: if associated_router in self.agent.ovn_local_cr_lrps: return True else: # no change in status if not bool(row.up[0]): # it was not exposed return False if hasattr(old, 'external_ids'): previous_associated_router = old.external_ids.get( constants.OVN_DEVICE_ID_EXT_ID_KEY) if (previous_associated_router and associated_router != previous_associated_router and previous_associated_router in self.agent.ovn_local_cr_lrps): return True except (IndexError, AttributeError): return False return False def _run(self, event, row, old): with _SYNC_STATE_LOCK.read_lock(): ips = row.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "").split() if event == self.ROW_DELETE: subnet_info = { 'associated_router': row.external_ids.get( constants.OVN_DEVICE_ID_EXT_ID_KEY), 'network': self._get_network(row), 'address_scopes': driver_utils.get_addr_scopes(row)} else: associated_router = row.external_ids.get( constants.OVN_DEVICE_ID_EXT_ID_KEY) if hasattr(old, 'external_ids'): previous_associated_router = old.external_ids.get( constants.OVN_DEVICE_ID_EXT_ID_KEY) if previous_associated_router != associated_router: associated_router = previous_associated_router subnet_info = { 'associated_router': associated_router, 'network': self._get_network(row), 'address_scopes': driver_utils.get_addr_scopes(row)} self.agent.withdraw_subnet(ips, subnet_info) class LogicalSwitchPortTenantCreateEvent(base_watcher.LSPChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE,) super(LogicalSwitchPortTenantCreateEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: # single and dual-stack format if not self._check_ip_associated(row.addresses[0]): return False if not bool(row.up[0]): return False current_network = self._get_network(row) if current_network not in self.agent.ovn_local_lrps: return False if hasattr(old, 'up'): if not bool(old.up[0]): return True if hasattr(old, 'external_ids'): old_network = self._get_network(old) if old_network != current_network: return True except (IndexError, AttributeError): return False return False def _run(self, event, row, old): if row.type not in [constants.OVN_VM_VIF_PORT_TYPE, constants.OVN_VIRTUAL_VIF_PORT_TYPE]: return with _SYNC_STATE_LOCK.read_lock(): ips = row.addresses[0].split(' ')[1:] mac = row.addresses[0].strip().split(' ')[0] ips_info = { 'mac': mac, 'cidrs': row.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "").split(), 'type': row.type, 'logical_switch': self._get_network(row) } self.agent.expose_remote_ip(ips, ips_info) class LogicalSwitchPortTenantDeleteEvent(base_watcher.LSPChassisEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE, self.ROW_DELETE,) super(LogicalSwitchPortTenantDeleteEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): try: # single and dual-stack format if not self._check_ip_associated(row.addresses[0]): return False current_network = self._get_network(row) # Assuming the current_network cannot be changed at once if current_network not in self.agent.ovn_local_lrps: return False if event == self.ROW_DELETE: return bool(row.up[0]) # ROW UPDATE EVENT if hasattr(old, 'up'): return bool(old.up[0]) except (IndexError, AttributeError): return False return False def _run(self, event, row, old): if row.type not in [constants.OVN_VM_VIF_PORT_TYPE, constants.OVN_VIRTUAL_VIF_PORT_TYPE]: return with _SYNC_STATE_LOCK.read_lock(): ips = row.addresses[0].split(' ')[1:] mac = row.addresses[0].strip().split(' ')[0] ips_info = { 'mac': mac, 'cidrs': row.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY, "").split(), 'type': row.type, 'logical_switch': self._get_network(row) } self.agent.withdraw_remote_ip(ips, ips_info) class OVNLBCreateEvent(base_watcher.OVNLBEvent): def __init__(self, bgp_agent): events = (self.ROW_UPDATE,) super(OVNLBCreateEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): # The ovn lb balancers are exposed through the cr-lrp, so if the # local agent does not have the matching router there is no need # to process the event try: if not row.vips: return False lb_router = self._get_router(row) if lb_router not in self.agent.ovn_local_cr_lrps.keys(): return False # Expose if there is a modification in the VIPS, first new item ( # that could happend with and non existing vips on old event or # empty one) or additional items because a bigger row.vips is # including old.vips if hasattr(old, 'vips'): if ((not old.vips and row.vips) or (old.vips != row.vips and set(old.vips.keys()).issubset(set(row.vips.keys())))): return True if hasattr(old, 'external_ids'): # Check if the lb_router was added old_lb_router = self._get_router(old) if lb_router != old_lb_router: return True except AttributeError: return False return False def _run(self, event, row, old): # vips field grows diff = self._get_diff_ip_from_vips(row, old) for ip in diff: with _SYNC_STATE_LOCK.read_lock(): if self._is_vip(row, ip): self.agent.expose_ovn_lb_vip(row) elif self._is_fip(row, ip): self.agent.expose_ovn_lb_fip(row) # router set ext-gw # NOTE(froyo): Not needed to check/call to expose_ovn_lb_fip, since up # to this point this LB could not have been associated with a FIP # since the subnet did not have access to the public network if hasattr(old, 'external_ids'): with _SYNC_STATE_LOCK.read_lock(): if self._get_router(old) != self._get_router(row): self.agent.expose_ovn_lb_vip(row) class OVNLBDeleteEvent(base_watcher.OVNLBEvent): def __init__(self, bgp_agent): events = (self.ROW_DELETE, self.ROW_UPDATE) super(OVNLBDeleteEvent, self).__init__( bgp_agent, events) def match_fn(self, event, row, old): # The ovn lb balancers are exposed through the cr-lrp, so if the # local agent does not have the matching router there is no need # to process the event try: if event == self.ROW_DELETE: if not row.vips: return False lb_router = self._get_router(row) if lb_router in self.agent.ovn_local_cr_lrps.keys(): return True return False # ROW UPDATE EVENT lb_router = self._get_router(row) if hasattr(old, 'external_ids'): old_lb_router = self._get_router(old) if not old_lb_router: return False if old_lb_router not in self.agent.ovn_local_cr_lrps.keys(): return False if old_lb_router != lb_router: # Router should not be removed, but if that is the case we # should remove the loadbalancer return True # Whatever the change removing any field from vips should be manage if hasattr(old, 'vips'): if ((old.vips != row.vips and set(row.vips.keys()).issubset( set(old.vips.keys())))): return True except AttributeError: return False return False def _run(self, event, row, old): # DELETE event need drop all if event == self.ROW_DELETE: diff = self._get_ip_from_vips(row) for ip in diff: with _SYNC_STATE_LOCK.read_lock(): if self._is_vip(row, ip): self.agent.withdraw_ovn_lb_vip(row) elif self._is_fip(row, ip): self.agent.withdraw_ovn_lb_fip(row) return # UPDATE event # vips field decrease diff = self._get_diff_ip_from_vips(old, row) for ip in diff: with _SYNC_STATE_LOCK.read_lock(): if self._is_vip(old, ip): self.agent.withdraw_ovn_lb_vip(old) elif self._is_fip(old, ip): self.agent.withdraw_ovn_lb_fip(old) # router unset ext-gw if hasattr(old, 'external_ids'): with _SYNC_STATE_LOCK.read_lock(): if self._get_router(old) != self._get_router(row): self.agent.withdraw_ovn_lb_vip(old) class OVNPFBaseEvent(base_watcher.OVNLBEvent): event = None def __init__(self, bgp_agent): super(OVNPFBaseEvent, self).__init__( bgp_agent, (self.event,)) def match_fn(self, event, row, old): # The ovn port forwarding are manage as OVN lb balancers and they are # exposed through the cr-lrp, so if the local agent does not have the # matching router there is no need to process the event if not driver_utils.check_name_prefix(row, constants.OVN_LB_PF_NAME_PREFIX): return False if not row.vips: return False lb_router = self._get_router(row, constants.OVN_LR_NAME_EXT_ID_KEY) return lb_router in self.agent.ovn_local_cr_lrps.keys() class OVNPFCreateEvent(OVNPFBaseEvent): event = OVNPFBaseEvent.ROW_CREATE def _run(self, event, row, old): with _SYNC_STATE_LOCK.read_lock(): self.agent.expose_ovn_pf_lb_fip(row) class OVNPFDeleteEvent(OVNPFBaseEvent): event = OVNPFBaseEvent.ROW_DELETE def _run(self, event, row, old): with _SYNC_STATE_LOCK.read_lock(): self.agent.withdraw_ovn_pf_lb_fip(row) ovn-bgp-agent-2.0.1/ovn_bgp_agent/exceptions.py000066400000000000000000000102231460327367600215350ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from neutron_lib._i18n import _ class OVNBGPAgentException(Exception): """Base OVN BGP Agebt Exception. To correctly use this class, inherit from it and define a 'message' property. That message will get printf'd with the keyword arguments provided to the constructor. """ message = _("An unknown exception occurred.") def __init__(self, **kwargs): super().__init__(self.message % kwargs) self.msg = self.message % kwargs def __str__(self): return self.msg class InvalidPortIP(OVNBGPAgentException): """OVN Port has Invalid IP. :param ip: The (wrong) IP of the port """ message = _("OVN port with invalid IP: %(ip)s.") class PortNotFound(OVNBGPAgentException): """OVN Port not found. :param port: The port name or UUID. """ message = _("OVN port was not found: %(port)s.") class DatapathNotFound(OVNBGPAgentException): """Datapath not found :param datapath: The datapath UUID """ message = _("Datapath was not found: %(datapath)s.") class PatchPortNotFound(OVNBGPAgentException): """Patch Port not found :param localnet: The localnet name """ message = _("Patch port not found for localnet: %(localnet)s.") class ExposeDeniedForAddressScope(OVNBGPAgentException): """Address Scope test failed :param addresses: The ip address used for checking address_scope :param address_scopes: The address scopes :param configured_scopes: The allowed address scopes in configuration """ message = _("Exposing addresses %(addresses)s with address scopes " "%(address_scopes)s was denied, required scopes: " "%(configured_scopes)s") class WireFailure(OVNBGPAgentException): """Wire port failed :param cidr: The cidr that failed to wire. :param message: The failure message """ message = _("Failure with wiring for CIDR %(cidr)s: %(message)s") class UnwireFailure(OVNBGPAgentException): """Unwire port failed :param cidr: The cidr that failed to wire. :param message: The failure message """ message = _("Failure with removing wiring for CIDR %(cidr)s: %(message)s") class IpAddressAlreadyExists(RuntimeError): message = _("IP address %(ip)s already configured on %(device)s.") def __init__(self, message=None, ip=None, device=None): message = message or self.message % {'ip': ip, 'device': device} super(IpAddressAlreadyExists, self).__init__(message) class NetworkInterfaceNotFound(RuntimeError): message = _("Network interface %(device)s not found") def __init__(self, message=None, device=None): message = message or self.message % {'device': device} super(NetworkInterfaceNotFound, self).__init__(message) class InterfaceAlreadyExists(RuntimeError): message = _("Interface %(device)s already exists.") def __init__(self, message=None, device=None): message = message or self.message % {'device': device} super(InterfaceAlreadyExists, self).__init__(message) class InterfaceOperationNotSupported(RuntimeError): message = _("Operation not supported on interface %(device)s.") def __init__(self, message=None, device=None): message = message or self.message % {'device': device} super(InterfaceOperationNotSupported, self).__init__(message) class InvalidArgument(RuntimeError): message = _("Invalid parameter/value used on interface %(device)s.") def __init__(self, message=None, device=None): message = message or self.message % {'device': device} super(InvalidArgument, self).__init__(message) ovn-bgp-agent-2.0.1/ovn_bgp_agent/privileged/000077500000000000000000000000001460327367600211365ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/privileged/__init__.py000066400000000000000000000025741460327367600232570ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_privsep import capabilities from oslo_privsep import priv_context default = priv_context.PrivContext( __name__, cfg_section='privsep', pypath=__name__ + '.default', capabilities=[capabilities.CAP_DAC_OVERRIDE, capabilities.CAP_DAC_READ_SEARCH, capabilities.CAP_NET_ADMIN, capabilities.CAP_SYS_ADMIN], ) ovs_vsctl_cmd = priv_context.PrivContext( __name__, cfg_section='privsep_ovs_vsctl', pypath=__name__ + '.ovs_vsctl_cmd', capabilities=[capabilities.CAP_SYS_ADMIN, capabilities.CAP_NET_ADMIN] ) vtysh_cmd = priv_context.PrivContext( __name__, cfg_section='privsep_vtysh', pypath=__name__ + '.vtysh_cmd', capabilities=[capabilities.CAP_SYS_ADMIN, capabilities.CAP_NET_ADMIN] ) ovn-bgp-agent-2.0.1/ovn_bgp_agent/privileged/linux_net.py000066400000000000000000000503151460327367600235210ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import errno import ipaddress import os import netaddr from oslo_concurrency import processutils from oslo_log import log as logging import pyroute2 from pyroute2 import iproute from pyroute2 import netlink as pyroute_netlink from pyroute2.netlink import exceptions as netlink_exceptions from pyroute2.netlink import rtnl from pyroute2.netlink.rtnl import ndmsg import tenacity import ovn_bgp_agent from ovn_bgp_agent import constants from ovn_bgp_agent import exceptions as agent_exc from ovn_bgp_agent.utils import common as common_utils from ovn_bgp_agent.utils import linux_net as l_net LOG = logging.getLogger(__name__) NUD_STATES = {state[1]: state[0] for state in ndmsg.states.items()} def get_scope_name(scope): """Return the name of the scope or the scope number if the name is unknown. For backward compatibility (with "ip" tool) "global" scope is converted to "universe" before converting to number """ scope = 'universe' if scope == 'global' else scope return rtnl.rt_scope.get(scope, scope) def set_device_state(device, state): set_link_attribute(device, state=state) @ovn_bgp_agent.privileged.default.entrypoint def ensure_vrf(vrf_name, vrf_table): try: set_device_state(vrf_name, constants.LINK_UP) except agent_exc.NetworkInterfaceNotFound: create_interface(vrf_name, 'vrf', vrf_table=vrf_table, state=constants.LINK_UP) @ovn_bgp_agent.privileged.default.entrypoint def ensure_bridge(bridge_name): try: set_device_state(bridge_name, constants.LINK_UP) except agent_exc.NetworkInterfaceNotFound: create_interface(bridge_name, 'bridge', br_stp_state=0, state=constants.LINK_UP) @ovn_bgp_agent.privileged.default.entrypoint def ensure_vxlan(vxlan_name, vni, local_ip, dstport): try: set_device_state(vxlan_name, constants.LINK_UP) except agent_exc.NetworkInterfaceNotFound: # FIXME: Perhaps we need to set neigh_suppress on create_interface(vxlan_name, 'vxlan', vxlan_id=vni, vxlan_port=dstport, vxlan_local=local_ip, vxlan_learning=False, state=constants.LINK_UP) @ovn_bgp_agent.privileged.default.entrypoint def ensure_veth(veth_name, veth_peer): try: set_device_state(veth_name, constants.LINK_UP) except agent_exc.NetworkInterfaceNotFound: create_interface(veth_name, 'veth', peer=veth_peer, state=constants.LINK_UP) set_device_state(veth_peer, constants.LINK_UP) @ovn_bgp_agent.privileged.default.entrypoint def ensure_dummy_device(device): try: set_device_state(device, constants.LINK_UP) except agent_exc.NetworkInterfaceNotFound: create_interface(device, 'dummy', state=constants.LINK_UP) @ovn_bgp_agent.privileged.default.entrypoint def ensure_vlan_device_for_network(bridge, vlan_tag): vlan_device_name = '{}.{}'.format(bridge, vlan_tag) try: set_device_state(vlan_device_name, constants.LINK_UP) except agent_exc.NetworkInterfaceNotFound: create_interface(vlan_device_name, 'vlan', physical_interface=bridge, vlan_id=vlan_tag, state=constants.LINK_UP) @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) @ovn_bgp_agent.privileged.default.entrypoint def set_master_for_device(device, master): try: with pyroute2.IPRoute() as ipr: dev_index = ipr.link_lookup(ifname=device)[0] master_index = ipr.link_lookup(ifname=master)[0] # Check if already associated to the master, # and associate it if not iface = ipr.link('get', index=dev_index)[0] if iface.get_attr('IFLA_MASTER') != master_index: ipr.link('set', index=dev_index, master=master_index) except IndexError: LOG.debug("No need to set %s on VRF %s, as one of them is deleted", device, master) @ovn_bgp_agent.privileged.default.entrypoint def delete_device(device): try: delete_interface(device) except agent_exc.NetworkInterfaceNotFound: LOG.debug("Interfaces %s already deleted.", device) @ovn_bgp_agent.privileged.default.entrypoint def route_create(route): scope = route.pop('scope', 'link') route['scope'] = get_scope_name(scope) if 'family' not in route: route['family'] = constants.AF_INET _run_iproute_route('replace', **route) @ovn_bgp_agent.privileged.default.entrypoint def route_delete(route): scope = route.pop('scope', 'link') route['scope'] = get_scope_name(scope) if 'family' not in route: route['family'] = constants.AF_INET _run_iproute_route('del', **route) @ovn_bgp_agent.privileged.default.entrypoint def set_kernel_flag(flag, value): command = ["sysctl", "-w", "{}={}".format(flag, value)] try: return processutils.execute(*command) except Exception as e: LOG.error("Unable to execute %s. Exception: %s", command, e) raise @ovn_bgp_agent.privileged.default.entrypoint def delete_exposed_ips(ips, nic): for ip_address in ips: delete_ip_address(ip_address, nic) @ovn_bgp_agent.privileged.default.entrypoint def rule_create(rule): _run_iproute_rule('add', **rule) @ovn_bgp_agent.privileged.default.entrypoint def rule_delete(rule): _run_iproute_rule('del', **rule) @ovn_bgp_agent.privileged.default.entrypoint def delete_ip_rules(ip_rules): for rule_ip, rule_info in ip_rules.items(): rule = l_net.create_rule_from_ip(rule_ip, int(rule_info['table'])) _run_iproute_rule('del', **rule) @ovn_bgp_agent.privileged.default.entrypoint def add_ndp_proxy(ip, dev, vlan=None): # FIXME(ltomasbo): This should use pyroute instead but I didn't find # out how net_ip = str(ipaddress.IPv6Network(ip, strict=False).network_address) dev_name = dev if vlan: dev_name = "{}.{}".format(dev, vlan) command = ["ip", "-6", "nei", "add", "proxy", net_ip, "dev", dev_name] try: return processutils.execute(*command) except Exception as e: LOG.error("Unable to execute %s. Exception: %s", command, e) raise @ovn_bgp_agent.privileged.default.entrypoint def del_ndp_proxy(ip, dev, vlan=None): # FIXME(ltomasbo): This should use pyroute instead but I didn't find # out how net_ip = str(ipaddress.IPv6Network(ip, strict=False).network_address) dev_name = dev if vlan: dev_name = "{}.{}".format(dev, vlan) command = ["ip", "-6", "nei", "del", "proxy", net_ip, "dev", dev_name] env = dict(os.environ) env['LC_ALL'] = 'C' try: return processutils.execute(*command, env_variables=env) except Exception as e: if "No such file or directory" in e.stderr: # Already deleted return LOG.error("Unable to execute %s. Exception: %s", command, e) raise @ovn_bgp_agent.privileged.default.entrypoint def add_ip_to_dev(ip, nic): add_ip_address(ip, nic) @ovn_bgp_agent.privileged.default.entrypoint def del_ip_from_dev(ip, nic): delete_ip_address(ip, nic) @ovn_bgp_agent.privileged.default.entrypoint def add_ip_nei(ip, lladdr, dev): ip_version = l_net.get_ip_version(ip) family = common_utils.IP_VERSION_FAMILY_MAP[ip_version] _run_iproute_neigh('replace', dev, dst=ip, lladdr=lladdr, family=family, state=ndmsg.states['permanent']) @ovn_bgp_agent.privileged.default.entrypoint def del_ip_nei(ip, lladdr, dev): ip_network = netaddr.IPNetwork(ip) family = common_utils.IP_VERSION_FAMILY_MAP[ip_network.version] _run_iproute_neigh('del', dev, dst=str(ip_network.ip), lladdr=lladdr, family=family, state=ndmsg.states['permanent']) @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_neigh_entries(device, ip_version, **kwargs): """Dump all neighbour entries. :param ip_version: IP version of entries to show (4 or 6) :param device: Device name to use in dumping entries :param kwargs: Callers add any filters they use as kwargs :return: a list of dictionaries, each representing a neighbour. The dictionary format is: {'dst': ip_address, 'lladdr': mac_address, 'device': device_name} """ family = common_utils.IP_VERSION_FAMILY_MAP[ip_version] dump = _run_iproute_neigh('dump', device, family=family, **kwargs) entries = [] for entry in dump: attrs = dict(entry['attrs']) entries.append({'dst': attrs['NDA_DST'], 'lladdr': attrs.get('NDA_LLADDR'), 'device': device, 'state': NUD_STATES[entry['state']]}) return entries def add_unreachable_route(vrf_name): # NOTE(ltomasbo): This method is to set the default route for the table # (and hence default route for the VRF) # ip route add table 10 unreachable default metric 4278198272 # Find vrf table. device = get_link_device(vrf_name) ifla_linkinfo = get_attr(device, 'IFLA_LINKINFO') ifla_data = get_attr(ifla_linkinfo, 'IFLA_INFO_DATA') vrf_table = get_attr(ifla_data, 'IFLA_VRF_TABLE') for ip_version in common_utils.IP_VERSION_FAMILY_MAP.values(): kwargs = {'dst': 'default', 'family': ip_version, 'table': vrf_table, 'type': 'unreachable', 'scope': None, 'proto': 'boot', 'priority': 4278198272} route_create(kwargs) @ovn_bgp_agent.privileged.default.entrypoint def create_routing_table_for_bridge(table_number, bridge): with open('/etc/iproute2/rt_tables', 'a') as rt_tables: rt_tables.write('{} {}\n'.format(table_number, bridge)) def _translate_ip_device_exception(e, device): if e.code == errno.ENODEV: raise agent_exc.NetworkInterfaceNotFound(device=device) if e.code == errno.EOPNOTSUPP: raise agent_exc.InterfaceOperationNotSupported(device=device) if e.code == errno.EINVAL: raise agent_exc.InvalidArgument(device=device) if e.code == errno.EEXIST: raise agent_exc.InterfaceAlreadyExists(device=device) raise e def _translate_ip_addr_exception(e, ip, device): if e.code == errno.EEXIST: raise agent_exc.IpAddressAlreadyExists(ip=ip, device=device) if e.code == errno.EADDRNOTAVAIL: LOG.debug('No need to delete IP address %s on dev %s as it does ' 'not exist', ip, device) return raise e def _translate_ip_route_exception(e, kwargs): if e.code == errno.EEXIST: # Already exists LOG.debug("Route %s already exists.", kwargs) return if e.code == errno.ENOENT or e.code == errno.ESRCH: # Not found LOG.debug("Route already deleted: %s", kwargs) return raise e def _translate_ip_rule_exception(e, kwargs): if e.code == errno.EEXIST: # Already exists LOG.debug("Rule %s already exists.", kwargs) return if e.code == errno.ENOENT: # Not found LOG.debug("Rule already deleted: %s", kwargs) return raise e def get_attr(pyroute2_obj, attr_name): """Get an attribute in a pyroute object pyroute2 object attributes are stored under a key called 'attrs'. This key contains a tuple of tuples. E.g.: pyroute2_obj = {'attrs': (('TCA_KIND': 'htb'), ('TCA_OPTIONS': {...}))} :param pyroute2_obj: (dict) pyroute2 object :param attr_name: (string) first value of the tuple we are looking for :return: (object) second value of the tuple, None if the tuple doesn't exist """ rule_attrs = pyroute2_obj.get('attrs', []) for attr in (attr for attr in rule_attrs if attr[0] == attr_name): return attr[1] def make_serializable(value): """Make a pyroute2 object serializable This function converts 'netlink.nla_slot' object (key, value) in a list of two elements. """ def _ensure_string(value): return value.decode() if isinstance(value, bytes) else value if isinstance(value, list): return [make_serializable(item) for item in value] elif isinstance(value, pyroute_netlink.nla_slot): return [_ensure_string(value[0]), make_serializable(value[1])] elif isinstance(value, pyroute_netlink.nla_base): return make_serializable(value.dump()) elif isinstance(value, dict): return {_ensure_string(key): make_serializable(data) for key, data in value.items()} elif isinstance(value, tuple): return tuple(make_serializable(item) for item in value) return _ensure_string(value) def _get_link_id(ifname, raise_exception=True): with iproute.IPRoute() as ip: link_id = ip.link_lookup(ifname=ifname) if not link_id or len(link_id) < 1: if raise_exception: raise agent_exc.NetworkInterfaceNotFound(device=ifname) LOG.debug('Interface %(dev)s not found', {'dev': ifname}) return return link_id[0] @ovn_bgp_agent.privileged.default.entrypoint def get_link_id(device): return _get_link_id(device, raise_exception=False) def get_link_state(device_name): device = get_link_device(device_name) return device['state'] if device else None def get_link_device(device_name): for device in get_link_devices(): if get_attr(device, 'IFLA_IFNAME') == device_name: return device @ovn_bgp_agent.privileged.default.entrypoint def get_bridge_vlans(device_name): index = _get_link_id(device_name, raise_exception=False) if not index: LOG.debug("OVS Bridge %s deleted, no need to get information about " "associated vlan devices", device_name) vlan_devices = get_link_devices(link=index) vlans = [] for vlan_device in vlan_devices: ifla_linkinfo = get_attr(vlan_device, 'IFLA_LINKINFO') if ifla_linkinfo: ifla_data = get_attr(ifla_linkinfo, 'IFLA_INFO_DATA') if ifla_data: vlans.append(get_attr(ifla_data, 'IFLA_VLAN_ID')) return vlans @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) @ovn_bgp_agent.privileged.default.entrypoint def get_link_devices(**kwargs): """List interfaces in a namespace :return: (list) interfaces in a namespace """ index = kwargs.pop('index') if 'index' in kwargs else 'all' try: with iproute.IPRoute() as ip: return make_serializable(ip.get_links(index, **kwargs)) except OSError: raise def _run_iproute_link(command, ifname, **kwargs): try: with iproute.IPRoute() as ip: idx = _get_link_id(ifname) return ip.link(command, index=idx, **kwargs) except netlink_exceptions.NetlinkError as e: _translate_ip_device_exception(e, ifname) def _run_iproute_addr(command, device, **kwargs): try: with iproute.IPRoute() as ip: idx = _get_link_id(device) return ip.addr(command, index=idx, **kwargs) except netlink_exceptions.NetlinkError as e: _translate_ip_addr_exception(e, ip=kwargs['address'], device=device) def _run_iproute_route(command, **kwargs): try: with iproute.IPRoute() as ip: ip.route(command, **kwargs) except netlink_exceptions.NetlinkError as e: _translate_ip_route_exception(e, kwargs) def _run_iproute_rule(command, **kwargs): try: with iproute.IPRoute() as ip: ip.rule(command, **kwargs) except netlink_exceptions.NetlinkError as e: _translate_ip_rule_exception(e, kwargs) def _run_iproute_neigh(command, device, **kwargs): try: with iproute.IPRoute() as ip: idx = _get_link_id(device) return ip.neigh(command, ifindex=idx, **kwargs) except agent_exc.NetworkInterfaceNotFound: LOG.debug("No need to %s nei for dev %s as it does not exists", command, device) @ovn_bgp_agent.privileged.default.entrypoint def create_interface(ifname, kind, **kwargs): ifname = ifname[:15] try: with iproute.IPRoute() as ip: physical_interface = kwargs.pop('physical_interface', None) if physical_interface: link_key = 'vxlan_link' if kind == 'vxlan' else 'link' kwargs[link_key] = _get_link_id(physical_interface) ip.link("add", ifname=ifname, kind=kind, **kwargs) except netlink_exceptions.NetlinkError as e: _translate_ip_device_exception(e, ifname) @ovn_bgp_agent.privileged.default.entrypoint def delete_interface(ifname, **kwargs): ifname = ifname[:15] _run_iproute_link('del', ifname, **kwargs) @ovn_bgp_agent.privileged.default.entrypoint def set_link_attribute(ifname, **kwargs): ifname = ifname[:15] _run_iproute_link("set", ifname, **kwargs) @ovn_bgp_agent.privileged.default.entrypoint def add_ip_address(ip_address, ifname): ifname = ifname[:15] net = netaddr.IPNetwork(ip_address) ip_version = l_net.get_ip_version(ip_address) address = str(net.ip) prefixlen = 32 if ip_version == 4 else 128 family = common_utils.IP_VERSION_FAMILY_MAP[ip_version] _run_iproute_addr('add', ifname, address=address, mask=prefixlen, family=family) @ovn_bgp_agent.privileged.default.entrypoint def delete_ip_address(ip_address, ifname): ifname = ifname[:15] net = netaddr.IPNetwork(ip_address) ip_version = l_net.get_ip_version(ip_address) address = str(net.ip) prefixlen = 32 if ip_version == 4 else 128 family = common_utils.IP_VERSION_FAMILY_MAP[ip_version] _run_iproute_addr("delete", ifname, address=address, mask=prefixlen, family=family) @ovn_bgp_agent.privileged.default.entrypoint def get_ip_addresses(**kwargs): """List of IP addresses in a namespace :return: (tuple) IP addresses in a namespace """ with iproute.IPRoute() as ip: return make_serializable(ip.get_addr(**kwargs)) @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) @ovn_bgp_agent.privileged.default.entrypoint def list_ip_routes(ip_version, device=None, table=None, **kwargs): """List IP routes""" kwargs['family'] = common_utils.IP_VERSION_FAMILY_MAP[ip_version] if device: kwargs['oif'] = _get_link_id(device) if table: kwargs['table'] = int(table) with iproute.IPRoute() as ip: return make_serializable(ip.route('show', **kwargs)) @ovn_bgp_agent.privileged.default.entrypoint def list_ip_rules(ip_version, **kwargs): """List all IP rules""" with iproute.IPRoute() as ip: return make_serializable(ip.get_rules( family=common_utils.IP_VERSION_FAMILY_MAP[ip_version], **kwargs)) ovn-bgp-agent-2.0.1/ovn_bgp_agent/privileged/ovs_vsctl.py000066400000000000000000000027061460327367600235370ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_concurrency import processutils from oslo_log import log as logging import ovn_bgp_agent.privileged.ovs_vsctl LOG = logging.getLogger(__name__) @ovn_bgp_agent.privileged.ovs_vsctl_cmd.entrypoint def ovs_cmd(command, args, timeout=None): full_args = [command] if timeout is not None: full_args += ['--timeout=%s' % timeout] full_args += args try: return processutils.execute(*full_args) except processutils.ProcessExecutionError: full_args += ['-O', 'OpenFlow13'] try: return processutils.execute(*full_args) except Exception as e: LOG.exception("Unable to execute %s %s. Exception: %s", command, full_args, e) raise except Exception as e: LOG.exception("Unable to execute %s %s. Exception: %s", command, full_args, e) raise ovn-bgp-agent-2.0.1/ovn_bgp_agent/privileged/vtysh.py000066400000000000000000000030501460327367600226630ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_concurrency import processutils from oslo_log import log as logging from ovn_bgp_agent import constants import ovn_bgp_agent.privileged.vtysh LOG = logging.getLogger(__name__) @ovn_bgp_agent.privileged.vtysh_cmd.entrypoint def run_vtysh_config(frr_config_file): full_args = ['/usr/bin/vtysh', '--vty_socket', constants.FRR_SOCKET_PATH, '-f', frr_config_file] try: return processutils.execute(*full_args) except Exception as e: LOG.exception("Unable to execute vtysh with %s. Exception: %s", full_args, e) raise @ovn_bgp_agent.privileged.vtysh_cmd.entrypoint def run_vtysh_command(command): full_args = ['/usr/bin/vtysh', '--vty_socket', constants.FRR_SOCKET_PATH, '-c', command] try: return processutils.execute(*full_args)[0] except Exception as e: LOG.exception("Unable to execute vtysh with %s. Exception: %s", full_args, e) raise ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/000077500000000000000000000000001460327367600201465ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/__init__.py000066400000000000000000000000001460327367600222450ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/base.py000066400000000000000000000025541460327367600214400ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2011 OpenStack Foundation # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslotest import base from ovn_bgp_agent import config from ovn_bgp_agent import privileged class TestCase(base.BaseTestCase): """Test case base class for all unit tests.""" def setUp(self): super(TestCase, self).setUp() privileged.default.client_mode = False privileged.ovs_vsctl_cmd.client_mode = False privileged.vtysh_cmd.client_mode = False config.register_opts() self.addCleanup(self._clean_up) self.addCleanup(mock.patch.stopall) def _clean_up(self): privileged.default.client_mode = True privileged.ovs_vsctl_cmd.client_mode = True privileged.vtysh_cmd.client_mode = True ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/functional/000077500000000000000000000000001460327367600223105ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/functional/__init__.py000066400000000000000000000000001460327367600244070ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/functional/base.py000066400000000000000000000103151460327367600235740ustar00rootroot00000000000000# Derived from: neutron/tests/functional/base.py # neutron/tests/base.py # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc import functools import inspect import os import sys import eventlet.timeout from oslo_config import cfg from oslo_log import log as logging from oslo_utils import fileutils from oslotest import base import ovn_bgp_agent from ovn_bgp_agent import config CONF = cfg.CONF LOG = logging.getLogger(__name__) def _get_test_log_path(): return os.environ.get('OS_LOG_PATH', '/tmp') # This is the directory from which infra fetches log files for functional tests DEFAULT_LOG_DIR = os.path.join(_get_test_log_path(), 'functional-logs') class _CatchTimeoutMetaclass(abc.ABCMeta): def __init__(cls, name, bases, dct): super(_CatchTimeoutMetaclass, cls).__init__(name, bases, dct) for name, method in inspect.getmembers( # NOTE(ihrachys): we should use isroutine because it will catch # both unbound methods (python2) and functions (python3) cls, predicate=inspect.isroutine): if name.startswith('test_'): setattr(cls, name, cls._catch_timeout(method)) @staticmethod def _catch_timeout(f): @functools.wraps(f) def func(self, *args, **kwargs): try: return f(self, *args, **kwargs) except eventlet.Timeout as e: self.fail('Execution of this test timed out: %s' % e) return func def setup_logging(component_name): """Sets up the logging options for a log with supplied name.""" logging.setup(cfg.CONF, component_name) LOG.info("Logging enabled!") LOG.info("%(prog)s version %(version)s", {'prog': sys.argv[0], 'version': ovn_bgp_agent.__version__}) LOG.debug("command line: %s", " ".join(sys.argv)) def sanitize_log_path(path): """Sanitize the string so that its log path is shell friendly""" return path.replace(' ', '-').replace('(', '_').replace(')', '_') # Test worker cannot survive eventlet's Timeout exception, which effectively # kills the whole worker, with all test cases scheduled to it. This metaclass # makes all test cases convert Timeout exceptions into unittest friendly # failure mode (self.fail). class BaseFunctionalTestCase(base.BaseTestCase, metaclass=_CatchTimeoutMetaclass): """Base class for functional tests.""" COMPONENT_NAME = 'ovn_bgp_agent' PRIVILEGED_GROUP = 'privsep' def setUp(self): super(BaseFunctionalTestCase, self).setUp() logging.register_options(CONF) setup_logging(self.COMPONENT_NAME) fileutils.ensure_tree(DEFAULT_LOG_DIR, mode=0o755) log_file = sanitize_log_path( os.path.join(DEFAULT_LOG_DIR, "%s.txt" % self.id())) self.flags(log_file=log_file) config.register_opts() config.setup_privsep() privsep_helper = os.path.join( os.getenv('VIRTUAL_ENV', os.path.dirname(sys.executable)[:-4]), 'bin', 'privsep-helper') self.flags( helper_command=' '.join(['sudo', '-E', privsep_helper]), group=self.PRIVILEGED_GROUP) def flags(self, **kw): """Override some configuration values. The keyword arguments are the names of configuration options to override and their values. If a group argument is supplied, the overrides are applied to the specified configuration option group. All overrides are automatically cleared at the end of the current test by the fixtures cleanup process. """ group = kw.pop('group', None) for k, v in kw.items(): CONF.set_override(k, v, group) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/functional/privileged/000077500000000000000000000000001460327367600244425ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/functional/privileged/__init__.py000066400000000000000000000000001460327367600265410ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/functional/privileged/test_linux_net.py000066400000000000000000000656601460327367600300750ustar00rootroot00000000000000# Copyright 2023 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import functools import random import netaddr from neutron_lib import constants as n_const from oslo_utils import uuidutils from pyroute2.iproute import linux as iproute_linux from pyroute2.netlink import rtnl from pyroute2.netlink.rtnl import ifaddrmsg from ovn_bgp_agent import constants from ovn_bgp_agent import exceptions as agent_exc from ovn_bgp_agent.privileged import linux_net from ovn_bgp_agent.tests.functional import base as base_functional from ovn_bgp_agent.tests import utils as test_utils from ovn_bgp_agent.utils import common as common_utils from ovn_bgp_agent.utils import linux_net as l_net IP_ADDRESS_EVENTS = {'RTM_NEWADDR': 'added', 'RTM_DELADDR': 'removed'} IP_ADDRESS_SCOPE = {rtnl.rtscopes['RT_SCOPE_UNIVERSE']: 'global', rtnl.rtscopes['RT_SCOPE_SITE']: 'site', rtnl.rtscopes['RT_SCOPE_LINK']: 'link', rtnl.rtscopes['RT_SCOPE_HOST']: 'host'} def set_up(ifname): linux_net.set_link_attribute(ifname, state='up') def ip_to_cidr(ip, prefix=None): """Convert an ip with no prefix to cidr notation :param ip: An ipv4 or ipv6 address. Convertible to netaddr.IPNetwork. :param prefix: Optional prefix. If None, the default 32 will be used for ipv4 and 128 for ipv6. """ net = netaddr.IPNetwork(ip) if prefix is not None: # Can't pass ip and prefix separately. Must concatenate strings. net = netaddr.IPNetwork(str(net.ip) + '/' + str(prefix)) return str(net) def _parse_ip_address(pyroute2_address, device_name): ip = linux_net.get_attr(pyroute2_address, 'IFA_ADDRESS') ip_length = pyroute2_address['prefixlen'] event = IP_ADDRESS_EVENTS.get(pyroute2_address.get('event')) cidr = ip_to_cidr(ip, prefix=ip_length) flags = linux_net.get_attr(pyroute2_address, 'IFA_FLAGS') dynamic = not bool(flags & ifaddrmsg.IFA_F_PERMANENT) tentative = bool(flags & ifaddrmsg.IFA_F_TENTATIVE) dadfailed = bool(flags & ifaddrmsg.IFA_F_DADFAILED) scope = IP_ADDRESS_SCOPE[pyroute2_address['scope']] return {'name': device_name, 'cidr': cidr, 'scope': scope, 'broadcast': linux_net.get_attr(pyroute2_address, 'IFA_BROADCAST'), 'dynamic': dynamic, 'tentative': tentative, 'dadfailed': dadfailed, 'event': event} def get_ip_addresses(ifname): device = get_devices_info(ifname=ifname) if not device: return ip_addresses = linux_net.get_ip_addresses( index=list(device.values())[0]['index']) return [_parse_ip_address(_ip, ifname) for _ip in ip_addresses] def get_devices_info(**kwargs): devices = linux_net.get_link_devices(**kwargs) retval = {} for device in devices: ret = {'index': device['index'], 'name': linux_net.get_attr(device, 'IFLA_IFNAME'), 'operstate': linux_net.get_attr(device, 'IFLA_OPERSTATE'), 'state': device['state'], 'linkmode': linux_net.get_attr(device, 'IFLA_LINKMODE'), 'mtu': linux_net.get_attr(device, 'IFLA_MTU'), 'promiscuity': linux_net.get_attr(device, 'IFLA_PROMISCUITY'), 'mac': linux_net.get_attr(device, 'IFLA_ADDRESS'), 'broadcast': linux_net.get_attr(device, 'IFLA_BROADCAST'), 'master': linux_net.get_attr(device, 'IFLA_MASTER'), } ifla_link = linux_net.get_attr(device, 'IFLA_LINK') if ifla_link: ret['parent_index'] = ifla_link ifla_linkinfo = linux_net.get_attr(device, 'IFLA_LINKINFO') if ifla_linkinfo: ret['kind'] = linux_net.get_attr(ifla_linkinfo, 'IFLA_INFO_KIND') ret['slave_kind'] = linux_net.get_attr(ifla_linkinfo, 'IFLA_INFO_SLAVE_KIND') ifla_data = linux_net.get_attr(ifla_linkinfo, 'IFLA_INFO_DATA') if ret['kind'] == 'vxlan': ret['vxlan_id'] = linux_net.get_attr(ifla_data, 'IFLA_VXLAN_ID') ret['vxlan_group'] = linux_net.get_attr(ifla_data, 'IFLA_VXLAN_GROUP') ret['vxlan_link_index'] = linux_net.get_attr(ifla_data, 'IFLA_VXLAN_LINK') ret['vxlan_port'] = linux_net.get_attr(ifla_data, 'IFLA_VXLAN_PORT') ret['vxlan_local'] = linux_net.get_attr(ifla_data, 'IFLA_VXLAN_LOCAL') ret['vxlan_learning'] = bool( linux_net.get_attr(ifla_data, 'IFLA_VXLAN_LEARNING')) elif ret['kind'] == 'vlan': ret['vlan_id'] = linux_net.get_attr(ifla_data, 'IFLA_VLAN_ID') elif ret['kind'] == 'bridge': ret['stp'] = linux_net.get_attr(ifla_data, 'IFLA_BR_STP_STATE') ret['forward_delay'] = linux_net.get_attr( ifla_data, 'IFLA_BR_FORWARD_DELAY') elif ret['kind'] == 'vrf': ret['vrf_table'] = linux_net.get_attr(ifla_data, 'IFLA_VRF_TABLE') retval[device['index']] = ret for device in retval.values(): if device.get('parent_index'): parent_device = retval.get(device['parent_index']) if parent_device: device['parent_name'] = parent_device['name'] elif device.get('vxlan_link_index'): device['vxlan_link_name'] = ( retval[device['vxlan_link_index']]['name']) return retval class _LinuxNetTestCase(base_functional.BaseFunctionalTestCase): def setUp(self): super().setUp() self.dev_name = uuidutils.generate_uuid()[:15] self.dev_name2 = uuidutils.generate_uuid()[:15] self.addCleanup(self._delete_interface) def _delete_interface(self): def delete_device(device_name): try: linux_net.delete_interface(device_name) except Exception: pass if self._get_device(self.dev_name): delete_device(self.dev_name) if self._get_device(self.dev_name2): delete_device(self.dev_name2) def _get_device(self, device_name): devices = get_devices_info() for device in devices.values(): if device['name'] == device_name: return device def _assert_state(self, device_name, state): device = self._get_device(device_name) return state == device['state'] def _check_status(self, device_name): fn = functools.partial(self._assert_state, device_name, constants.LINK_DOWN) test_utils.wait_until_true(fn, timeout=5) set_up(device_name) fn = functools.partial(self._assert_state, device_name, constants.LINK_UP) test_utils.wait_until_true(fn, timeout=5) class IpLinkTestCase(_LinuxNetTestCase): def test_create_interface_dummy(self): linux_net.create_interface(self.dev_name, 'dummy') device = self._get_device(self.dev_name) self.assertEqual('dummy', device['kind']) self._check_status(self.dev_name) def test_create_interface_vlan(self): vlan_id = random.randint(2, 4094) linux_net.create_interface(self.dev_name, 'dummy') linux_net.create_interface(self.dev_name2, 'vlan', physical_interface=self.dev_name, vlan_id=vlan_id) device = self._get_device(self.dev_name2) self.assertEqual('vlan', device['kind']) self.assertEqual(vlan_id, device['vlan_id']) self._check_status(self.dev_name) def test_create_interface_vxlan(self): vxlan_id = random.randint(2, 4094) vxlan_port = random.randint(10000, 65534) vxlan_local = '1.2.3.4' linux_net.create_interface(self.dev_name, 'vxlan', vxlan_id=vxlan_id, vxlan_port=vxlan_port, vxlan_local=vxlan_local, vxlan_learning=False, state=constants.LINK_UP) device = self._get_device(self.dev_name) self.assertEqual('vxlan', device['kind']) self.assertEqual(vxlan_id, device['vxlan_id']) self.assertEqual(vxlan_port, device['vxlan_port']) self.assertEqual(vxlan_local, device['vxlan_local']) self.assertEqual(constants.LINK_UP, device['state']) self.assertFalse(device['vxlan_learning']) def test_create_interface_veth(self): linux_net.create_interface(self.dev_name, 'veth', peer=self.dev_name2) device = self._get_device(self.dev_name) self.assertEqual('veth', device['kind']) self.assertEqual(self.dev_name2, device['parent_name']) device = self._get_device(self.dev_name2) self.assertEqual('veth', device['kind']) self.assertEqual(self.dev_name, device['parent_name']) self._check_status(self.dev_name) self._check_status(self.dev_name2) def test_create_interface_bridge(self): linux_net.create_interface(self.dev_name, 'bridge', br_stp_state=0) device = self._get_device(self.dev_name) self.assertEqual('bridge', device['kind']) self.assertEqual(0, device['stp']) self._check_status(self.dev_name) def test_create_interface_vrf(self): vrf_table = random.randint(10, 2000) linux_net.create_interface(self.dev_name, 'vrf', vrf_table=vrf_table) device = self._get_device(self.dev_name) self.assertEqual('vrf', device['kind']) self.assertEqual(vrf_table, device['vrf_table']) self._check_status(self.dev_name) def _check_device_master_vrf(self, device, master=None): device_info = self._get_device(device) if not master: self.assertIsNone(device_info['master']) self.assertIsNone(device_info['slave_kind']) else: master_info = self._get_device(master) self.assertEqual(master_info['index'], device_info['master']) self.assertEqual('vrf', device_info['slave_kind']) def test_set_master_for_device_bridge(self): vrf_table = random.randint(10, 2000) linux_net.create_interface(self.dev_name, 'vrf', vrf_table=vrf_table) linux_net.create_interface(self.dev_name2, 'bridge', br_stp_state=0) self._check_device_master_vrf(self.dev_name2) linux_net.set_master_for_device(self.dev_name2, self.dev_name) self._check_device_master_vrf(self.dev_name2, master=self.dev_name) def test_set_master_for_device_dummy(self): vrf_table = random.randint(10, 2000) linux_net.create_interface(self.dev_name, 'vrf', vrf_table=vrf_table) linux_net.create_interface(self.dev_name2, 'dummy') self._check_device_master_vrf(self.dev_name2) linux_net.set_master_for_device(self.dev_name2, self.dev_name) self._check_device_master_vrf(self.dev_name2, master=self.dev_name) def test_set_master_for_device_vlan(self): vrf_table = random.randint(10, 2000) linux_net.create_interface(self.dev_name, 'vrf', vrf_table=vrf_table) vlan_id = random.randint(2, 4094) dev_name3 = uuidutils.generate_uuid()[:15] linux_net.create_interface(self.dev_name2, 'dummy') linux_net.create_interface(dev_name3, 'vlan', physical_interface=self.dev_name2, vlan_id=vlan_id) self._check_device_master_vrf(dev_name3) linux_net.set_master_for_device(dev_name3, self.dev_name) self._check_device_master_vrf(dev_name3, master=self.dev_name) def test_set_master_for_device_veth(self): vrf_table = random.randint(10, 2000) linux_net.create_interface(self.dev_name, 'vrf', vrf_table=vrf_table) dev_name3 = uuidutils.generate_uuid()[:15] linux_net.create_interface(self.dev_name2, 'veth', peer=dev_name3) self._check_device_master_vrf(self.dev_name2) linux_net.set_master_for_device(self.dev_name2, self.dev_name) self._check_device_master_vrf(self.dev_name2, master=self.dev_name) def test_ensure_vlan_device_for_network(self): self.dev_name = uuidutils.generate_uuid()[:8] linux_net.create_interface(self.dev_name, 'dummy') linux_net.set_device_state(self.dev_name, constants.LINK_UP) vlan_id = random.randint(2, 4094) # Ensure the method call is idempotent. for _ in range(2): linux_net.ensure_vlan_device_for_network(self.dev_name, vlan_id) fn = functools.partial(self._assert_state, self.dev_name, constants.LINK_UP) test_utils.wait_until_true(fn, timeout=5) def test_ensure_vrf(self): vrf_table = random.randint(10, 2000) # Ensure the method call is idempotent. for _ in range(2): linux_net.ensure_vrf(self.dev_name, vrf_table) fn = functools.partial(self._assert_state, self.dev_name, constants.LINK_UP) test_utils.wait_until_true(fn, timeout=5) def test_ensure_bridge(self): # Ensure the method call is idempotent. for _ in range(2): linux_net.ensure_bridge(self.dev_name) fn = functools.partial(self._assert_state, self.dev_name, constants.LINK_UP) test_utils.wait_until_true(fn, timeout=5) def test_ensure_vxlan(self): vxlan_id = random.randint(2, 4094) vxlan_port = random.randint(10000, 65534) vxlan_local = '1.2.3.4' # Ensure the method call is idempotent. for _ in range(2): linux_net.ensure_vxlan(self.dev_name, vxlan_id, vxlan_local, vxlan_port) fn = functools.partial(self._assert_state, self.dev_name, constants.LINK_UP) test_utils.wait_until_true(fn, timeout=5) def test_ensure_veth(self): # Ensure the method call is idempotent. for _ in range(2): linux_net.ensure_veth(self.dev_name, self.dev_name2) fn = functools.partial(self._assert_state, self.dev_name, constants.LINK_UP) test_utils.wait_until_true(fn, timeout=5) def test_ensure_dummy(self): for _ in range(2): linux_net.ensure_dummy_device(self.dev_name) fn = functools.partial(self._assert_state, self.dev_name, constants.LINK_UP) test_utils.wait_until_true(fn, timeout=5) def test_get_bridge_vlan_devices(self): vlan_id = random.randint(2, 4094) linux_net.create_interface(self.dev_name, 'dummy') linux_net.create_interface(self.dev_name2, 'vlan', physical_interface=self.dev_name, vlan_id=vlan_id) vlan_devices = linux_net.get_bridge_vlans(self.dev_name) self.assertEqual(vlan_devices[0], vlan_id) class IpAddressTestCase(_LinuxNetTestCase): def test_add_and_delete_ip_address(self): def check_ip_address(ip_address, device_name, present=True): ip_addresses = get_ip_addresses(self.dev_name) if l_net.get_ip_version(ip_address) == constants.IP_VERSION_6: address = '{}/128'.format(ip_address) else: address = '{}/32'.format(ip_address) for _ip in ip_addresses: if _ip['cidr'] == address: if present: return else: self.fail('IP address %s present in device %s' % (ip_address, device_name)) if present: self.fail('IP address %s not found in device %s' % (ip_address, device_name)) ip_addresses = ('240.0.0.1', 'fd00::1') linux_net.create_interface(self.dev_name, 'dummy') for ip_address in ip_addresses: linux_net.add_ip_address(ip_address, self.dev_name) check_ip_address(ip_address, self.dev_name) # ensure nothing breaks if same IP gets added, # it should raise exception that is handled in the utils self.assertRaises(agent_exc.IpAddressAlreadyExists, linux_net.add_ip_address, ip_address, self.dev_name) for ip_address in ip_addresses: linux_net.delete_ip_address(ip_address, self.dev_name) check_ip_address(ip_address, self.dev_name, present=False) # ensure removing a missing IP is ok linux_net.delete_ip_address(ip_address, self.dev_name) def test_add_ip_address_no_device(self): self.assertRaises(agent_exc.NetworkInterfaceNotFound, linux_net.add_ip_address, '240.0.0.1', self.dev_name) def test_delete_ip_address_no_device(self): self.assertRaises(agent_exc.NetworkInterfaceNotFound, linux_net.delete_ip_address, '240.0.0.1', self.dev_name) def test_delete_ip_address_no_ip_on_device(self): linux_net.create_interface(self.dev_name, 'dummy') # No exception is raised. linux_net.delete_ip_address('192.168.0.1', self.dev_name) class IpRouteTestCase(_LinuxNetTestCase): def setUp(self): super().setUp() linux_net.create_interface(self.dev_name, 'dummy', state=constants.LINK_UP) self.device = self._get_device(self.dev_name) def _check_routes(self, cidrs, device_name, table=None, scope='link', proto='static', route_present=True): table = table or iproute_linux.DEFAULT_TABLE cidr = None for cidr in cidrs: ip_version = l_net.get_ip_version(cidr) if ip_version == n_const.IP_VERSION_6: scope = 0 if isinstance(scope, int): scope = linux_net.get_scope_name(scope) routes = linux_net.list_ip_routes(ip_version, device=device_name) for route in routes: ip = linux_net.get_attr(route, 'RTA_DST') mask = route['dst_len'] if not (ip == str(netaddr.IPNetwork(cidr).ip) and mask == netaddr.IPNetwork(cidr).cidr.prefixlen): continue self.assertEqual(table, route['table']) self.assertEqual( common_utils.IP_VERSION_FAMILY_MAP[ip_version], route['family']) ret_scope = linux_net.get_scope_name(route['scope']) self.assertEqual(scope, ret_scope) self.assertEqual(rtnl.rt_proto[proto], route['proto']) break else: if route_present: self.fail('CIDR %s not found in the list of routes' % cidr) else: return if not route_present: self.fail('CIDR %s found in the list of routes' % cidr) def _add_route_device_and_check(self, cidrs, table=None, scope='link', proto='static'): for cidr in cidrs: ip_version = l_net.get_ip_version(cidr) family = common_utils.IP_VERSION_FAMILY_MAP[ip_version] route = {'dst': cidr, 'oif': self.device['index'], 'table': table, 'family': family, 'scope': scope, 'proto': proto} linux_net.route_create(route) # recreate route to ensure it does not break anything linux_net.route_create(route) self._check_routes(cidrs, self.dev_name, table=table, scope=scope, proto=proto) for cidr in cidrs: ip_version = l_net.get_ip_version(cidr) family = common_utils.IP_VERSION_FAMILY_MAP[ip_version] route = {'dst': cidr, 'oif': self.device['index'], 'table': table, 'family': family, 'scope': scope, 'proto': proto} linux_net.route_delete(route) # redelete route to ensure it does not break anything linux_net.route_delete(route) self._check_routes(cidrs, self.dev_name, table=table, scope=scope, proto=proto, route_present=False) def test_add_route_device(self): cidrs = ['192.168.1.0/24', '2001:db1::/64'] self._add_route_device_and_check(cidrs=cidrs, table=None) def test_add_route_device_table(self): cidrs = ['192.168.2.0/24', '2001:db2::/64'] self._add_route_device_and_check(cidrs=cidrs, table=100) def test_add_route_device_scope_site(self): cidrs = ['192.168.3.0/24', '2003:db3::/64'] self._add_route_device_and_check(cidrs=cidrs, scope='site') def test_add_route_device_scope_host(self): cidrs = ['192.168.4.0/24', '2003:db4::/64'] self._add_route_device_and_check(cidrs=cidrs, scope='host') def test_add_route_device_proto_static(self): cidrs = ['192.168.5.0/24', '2003:db5::/64'] self._add_route_device_and_check(cidrs=cidrs, proto='static') def test_add_route_device_proto_redirect(self): cidrs = ['192.168.6.0/24', '2003:db6::/64'] self._add_route_device_and_check(cidrs=cidrs, proto='redirect') def test_add_route_device_proto_kernel(self): cidrs = ['192.168.7.0/24', '2003:db7::/64'] self._add_route_device_and_check(cidrs=cidrs, proto='kernel') def test_add_route_device_proto_boot(self): cidrs = ['192.168.8.0/24', '2003:db8::/64'] self._add_route_device_and_check(cidrs=cidrs, proto='boot') def test_add_unreachable_route(self): vrf_table = random.randint(10, 2000) linux_net.create_interface(self.dev_name2, 'vrf', vrf_table=vrf_table) linux_net.add_unreachable_route(self.dev_name2) for ip_version in (n_const.IP_VERSION_4, n_const.IP_VERSION_6): routes = linux_net.list_ip_routes(ip_version, table=vrf_table) self.assertEqual(1, len(routes)) self.assertEqual(rtnl.rt_proto['boot'], routes[0]['proto']) self.assertEqual(rtnl.rtypes['RTN_UNREACHABLE'], routes[0]['type']) self.assertEqual(4278198272, linux_net.get_attr(routes[0], 'RTA_PRIORITY')) class IpRuleTestCase(_LinuxNetTestCase): def _test_add_and_delete_ip_rule(self, ip_version, cidrs): table = random.randint(10, 250) rules_added = [] for cidr in cidrs: _ip = netaddr.IPNetwork(cidr) rule = {'dst': str(_ip.ip), 'dst_len': _ip.netmask.netmask_bits(), 'table': table, 'family': common_utils.IP_VERSION_FAMILY_MAP[ip_version]} rules_added.append(rule) linux_net.rule_create(rule) # recreate the last rule, to ensure recreation does not fail linux_net.rule_create(rule) rules = linux_net.list_ip_rules(ip_version, table=table) self.assertEqual(len(cidrs), len(rules)) for idx, rule in enumerate(rules): _ip = netaddr.IPNetwork(cidrs[idx]) self.assertEqual(str(_ip.ip), linux_net.get_attr(rule, 'FRA_DST')) self.assertEqual(_ip.netmask.netmask_bits(), rule['dst_len']) for rule in rules_added: linux_net.rule_delete(rule) # remove again the last rule to ensure it does not fail linux_net.rule_delete(rule) rules = linux_net.list_ip_rules(ip_version, table=table) self.assertEqual(0, len(rules)) def test_add_and_delete_ip_rule_v4(self): cidrs = ['192.168.0.0/24', '172.90.0.0/16', '10.0.0.0/8'] self._test_add_and_delete_ip_rule(n_const.IP_VERSION_4, cidrs) def test_add_and_delete_ip_rule_v6(self): cidrs = ['2001:db8::/64', 'fe80::/10'] self._test_add_and_delete_ip_rule(n_const.IP_VERSION_6, cidrs) class IpNeighTestCase(_LinuxNetTestCase): def setUp(self): super().setUp() linux_net.create_interface(self.dev_name, 'dummy', state=constants.LINK_UP) self.device = self._get_device(self.dev_name) def test_add_and_delete_neigh(self): # Initial check, nothing in the ARP table. neigh4 = linux_net.get_neigh_entries(self.dev_name, constants.IP_VERSION_4) neigh6 = linux_net.get_neigh_entries(self.dev_name, constants.IP_VERSION_6) self.assertEqual([], neigh4) self.assertEqual([], neigh6) # Add a set of IP/MAC addresses. ip_and_mac = [('10.0.0.1', 'ca:fe:ca:fe:00:01'), ('10.0.0.2', 'ca:fe:ca:fe:00:02'), ('2001:db8::3', 'ca:fe:ca:fe:00:03'), ('2001:db8::4', 'ca:fe:ca:fe:00:04')] for ip, mac in ip_and_mac: linux_net.add_ip_nei(ip, mac, self.dev_name) neigh4 = linux_net.get_neigh_entries(self.dev_name, constants.IP_VERSION_4) neigh6 = linux_net.get_neigh_entries(self.dev_name, constants.IP_VERSION_6) self.assertEqual(2, len(neigh4)) self.assertEqual(2, len(neigh6)) for neigh in neigh4 + neigh6: for ip, mac in ip_and_mac: if ip == neigh['dst'] and mac == neigh['lladdr']: break else: self.fail('IP/MAC %s/%s is not present in the ip-neigh table' % (neigh['dst'], neigh['lladdr'])) # Delete the entries. for ip, mac in ip_and_mac: linux_net.del_ip_nei(ip, mac, self.dev_name) neigh4 = linux_net.get_neigh_entries(self.dev_name, constants.IP_VERSION_4) neigh6 = linux_net.get_neigh_entries(self.dev_name, constants.IP_VERSION_6) self.assertEqual(0, len(neigh4)) self.assertEqual(0, len(neigh6)) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/functional/utils/000077500000000000000000000000001460327367600234505ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/functional/utils/__init__.py000066400000000000000000000000001460327367600255470ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/functional/utils/test_linux_net.py000066400000000000000000000135001460327367600270650ustar00rootroot00000000000000# Copyright 2023 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import netaddr from neutron_lib.utils import net as net_utils from oslo_utils import uuidutils from ovn_bgp_agent import exceptions as agent_exc from ovn_bgp_agent.privileged import linux_net as priv_linux_net from ovn_bgp_agent.tests.functional import base as base_functional from ovn_bgp_agent.tests.functional.privileged import test_linux_net as \ test_priv_linux_net from ovn_bgp_agent.utils import common as common_utils from ovn_bgp_agent.utils import linux_net class GetInterfaceTestCase(base_functional.BaseFunctionalTestCase): def _delete_interfaces(self, dev_names): for dev_name in dev_names: try: priv_linux_net.delete_interface(dev_name) except Exception: pass def _get_device(self, device_name): device_index = linux_net.get_interface_index(device_name) devices = test_priv_linux_net.get_devices_info(index=device_index) for device in devices.values(): if device['name'] == device_name: return device def test_get_interfaces(self): dev_names = list(map(lambda x: uuidutils.generate_uuid()[:15], range(3))) self.addCleanup(self._delete_interfaces, dev_names) for dev_name in dev_names: priv_linux_net.create_interface(dev_name, 'dummy') ret = linux_net.get_interfaces() for dev in dev_names: self.assertIn(dev, ret) def test_get_interface_index(self): dev_name = uuidutils.generate_uuid()[:15] self.addCleanup(self._delete_interfaces, [dev_name]) priv_linux_net.create_interface(dev_name, 'dummy') device = self._get_device(dev_name) ret = linux_net.get_interface_index(dev_name) self.assertEqual(device['index'], ret) def test_get_interface_address(self): dev_names = list(map(lambda x: uuidutils.generate_uuid()[:15], range(5))) self.addCleanup(self._delete_interfaces, dev_names) for dev_name in dev_names: mac_address = net_utils.get_random_mac( 'fa:16:3e:00:00:00'.split(':')) priv_linux_net.create_interface(dev_name, 'dummy', address=mac_address) mac = linux_net.get_interface_address(dev_name) self.assertEqual(mac_address, mac) def test_get_interface_address_no_interface(self): self.assertRaises(agent_exc.NetworkInterfaceNotFound, linux_net.get_interface_address, 'no_interface_name') def test_get_nic_info(self): dev_name = uuidutils.generate_uuid()[:15] ip = '172.24.10.100/32' self.addCleanup(self._delete_interfaces, [dev_name]) mac_address = net_utils.get_random_mac( 'fa:16:3e:00:00:00'.split(':')) priv_linux_net.create_interface(dev_name, 'dummy', address=mac_address) priv_linux_net.add_ip_address(ip, dev_name) ret = linux_net.get_nic_info(dev_name) self.assertEqual((ip, mac_address), ret) def test_get_nic_info_no_interface(self): self.assertRaises(agent_exc.NetworkInterfaceNotFound, linux_net.get_nic_info, 'no_interface_name') def test_get_exposed_ips(self): ips = ['240.0.0.1', 'fd00::1'] dev_name = uuidutils.generate_uuid()[:15] self.addCleanup(self._delete_interfaces, [dev_name]) priv_linux_net.create_interface(dev_name, 'dummy') for ip in ips: priv_linux_net.add_ip_address(ip, dev_name) ret = linux_net.get_exposed_ips(dev_name) self.assertEqual(ips, ret) def test_get_nic_ip(self): ips = ['240.0.0.1', 'fd00::1'] dev_name = uuidutils.generate_uuid()[:15] self.addCleanup(self._delete_interfaces, [dev_name]) priv_linux_net.create_interface(dev_name, 'dummy') for ip in ips: priv_linux_net.add_ip_address(ip, dev_name) ret = linux_net.get_nic_ip(dev_name) self.assertEqual(ips, ret) class GetRulesTestCase(base_functional.BaseFunctionalTestCase): def _delete_rules(self, rules): for rule in rules: try: priv_linux_net.rule_delete(rule) except Exception: pass def test_get_ovn_ip_rules(self): cidrs = ['192.168.0.0/24', '172.90.0.0/16', 'fd00::1/128'] table = 100 expected_rules = {} rules_added = [] for cidr in cidrs: _ip = netaddr.IPNetwork(cidr) ip_version = linux_net.get_ip_version(cidr) rule = {'dst': str(_ip.ip), 'dst_len': _ip.netmask.netmask_bits(), 'table': table, 'family': common_utils.IP_VERSION_FAMILY_MAP[ip_version]} dst = "{}/{}".format(str(_ip.ip), _ip.netmask.netmask_bits()) rules_added.append(rule) expected_rules[dst] = { 'table': table, 'family': common_utils.IP_VERSION_FAMILY_MAP[ip_version]} self.addCleanup(self._delete_rules, rules_added) for rule in rules_added: priv_linux_net.rule_create(rule) ret = linux_net.get_ovn_ip_rules([table]) self.assertEqual(expected_rules, ret) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/test_ovn_bgp_agent.py000066400000000000000000000014301460327367600243650ustar00rootroot00000000000000# -*- coding: utf-8 -*- # 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. """ test_ovn_bgp_agent ---------------------------------- Tests for `ovn_bgp_agent` module. """ from ovn_bgp_agent.tests import base class TestOvn_bgp_agent(base.TestCase): def test_something(self): pass ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/000077500000000000000000000000001460327367600211255ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/__init__.py000066400000000000000000000000001460327367600232240ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/cmd/000077500000000000000000000000001460327367600216705ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/cmd/__init__.py000066400000000000000000000000001460327367600237670ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/cmd/test_agent.py000066400000000000000000000016101460327367600243750ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock from ovn_bgp_agent.tests import base as test_base class TestAgentCmd(test_base.TestCase): @mock.patch('ovn_bgp_agent.agent.start') def test_start(self, m_start): from ovn_bgp_agent.cmd import agent # To make it import a mock. agent.start() m_start.assert_called() ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/000077500000000000000000000000001460327367600226035ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/__init__.py000066400000000000000000000000001460327367600247020ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/000077500000000000000000000000001460327367600245725ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/__init__.py000066400000000000000000000000001460327367600266710ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/test_nb_ovn_bgp_driver.py000066400000000000000000002230631460327367600316750ustar00rootroot00000000000000# Copyright 2023 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_config import cfg from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack import nb_ovn_bgp_driver from ovn_bgp_agent.drivers.openstack.utils import bgp as bgp_utils 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 ovn from ovn_bgp_agent.drivers.openstack.utils import ovs from ovn_bgp_agent.drivers.openstack.utils import wire as wire_utils from ovn_bgp_agent import exceptions from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.tests.unit import fakes from ovn_bgp_agent.tests import utils from ovn_bgp_agent.utils import linux_net CONF = cfg.CONF class TestNBOVNBGPDriver(test_base.TestCase): def setUp(self): super(TestNBOVNBGPDriver, self).setUp() CONF.set_override('expose_tenant_networks', True) self.bridge = 'fake-bridge' self.nb_bgp_driver = nb_ovn_bgp_driver.NBOVNBGPDriver() self.nb_bgp_driver._post_start_event = mock.Mock() self.nb_bgp_driver.nb_idl = mock.Mock() self.nb_bgp_driver.allowed_address_scopes = None self.nb_idl = self.nb_bgp_driver.nb_idl self.nb_bgp_driver.chassis = 'fake-chassis' self.nb_bgp_driver.chassis_id = 'fake-chassis-id' self.nb_bgp_driver.ovn_bridge_mappings = {'fake-network': self.bridge} self.mock_nbdb = mock.patch.object(ovn, 'OvnNbIdl').start() self.mock_ovs_idl = mock.patch.object(ovs, 'OvsIdl').start() self.nb_bgp_driver.ovs_idl = self.mock_ovs_idl self.ipv4 = '192.168.1.17' self.ipv6 = '2002::1234:abcd:ffff:c0a8:101' self.fip = '172.24.4.33' self.mac = 'aa:bb:cc:dd:ee:ff' self.router1_info = {'bridge_device': self.bridge, 'bridge_vlan': 100, 'ips': ['172.24.4.11'], 'provider_switch': 'provider-ls'} self.nb_bgp_driver.ovn_local_cr_lrps = { 'router1': self.router1_info} self.ovn_routing_tables = { self.bridge: 100, 'br-vlan': 200} self.nb_bgp_driver.ovn_routing_tables = self.ovn_routing_tables self.ovn_routing_tables_routes = mock.Mock() self.nb_bgp_driver.ovn_routing_tables_routes = ( self.ovn_routing_tables_routes) self.conf_ovsdb_connection = 'tcp:127.0.0.1:6642' @mock.patch.object(linux_net, 'ensure_vrf') @mock.patch.object(frr, 'vrf_leak') @mock.patch.object(linux_net, 'ensure_ovn_device') @mock.patch.object(linux_net, 'delete_routes_from_table') def test_start(self, mock_delete_routes_from_table, mock_ensure_ovn_device, mock_vrf_leak, mock_ensure_vrf): CONF.set_override('clear_vrf_routes_on_startup', True) self.addCleanup(CONF.clear_override, 'clear_vrf_routes_on_startup') self.mock_ovs_idl.get_own_chassis_name.return_value = 'chassis-name' self.mock_ovs_idl.get_own_chassis_id.return_value = 'chassis-id' self.mock_ovs_idl.get_ovn_remote.return_value = ( self.conf_ovsdb_connection) self.nb_bgp_driver.start() # Verify mock object method calls and arguments self.mock_ovs_idl().start.assert_called_once_with( CONF.ovsdb_connection) self.mock_ovs_idl().get_own_chassis_name.assert_called_once() self.mock_ovs_idl().get_own_chassis_id.assert_called_once() self.mock_ovs_idl().get_ovn_remote.assert_called_once() mock_ensure_vrf.assert_called_once_with( CONF.bgp_vrf, CONF.bgp_vrf_table_id) mock_vrf_leak.assert_called_once_with( CONF.bgp_vrf, CONF.bgp_AS, CONF.bgp_router_id, template=frr.LEAK_VRF_TEMPLATE) mock_ensure_ovn_device.assert_called_once_with(CONF.bgp_nic, CONF.bgp_vrf) mock_delete_routes_from_table.assert_called_once_with( CONF.bgp_vrf_table_id) self.mock_nbdb().start.assert_called_once_with() @mock.patch.object(linux_net, 'ensure_ovn_device') @mock.patch.object(frr, 'vrf_leak') @mock.patch.object(linux_net, 'ensure_vrf') def test_frr_sync(self, mock_ensure_vrf, mock_vrf_leak, mock_ensure_ovn_dev): self.nb_bgp_driver.frr_sync() mock_ensure_vrf.assert_called_once_with( CONF.bgp_vrf, CONF.bgp_vrf_table_id) mock_vrf_leak.assert_called_once_with( CONF.bgp_vrf, CONF.bgp_AS, CONF.bgp_router_id, template=frr.LEAK_VRF_TEMPLATE) mock_ensure_ovn_dev.assert_called_once_with( CONF.bgp_nic, CONF.bgp_vrf) @mock.patch.object(linux_net, 'delete_vlan_device_for_network') @mock.patch.object(linux_net, 'get_bridge_vlans') @mock.patch.object(linux_net, 'get_extra_routing_table_for_bridge') @mock.patch.object(linux_net, 'delete_bridge_ip_routes') @mock.patch.object(linux_net, 'delete_ip_rules') @mock.patch.object(linux_net, 'delete_exposed_ips') @mock.patch.object(linux_net, 'get_ovn_ip_rules') @mock.patch.object(linux_net, 'get_exposed_ips') @mock.patch.object(ovs, 'remove_extra_ovs_flows') @mock.patch.object(ovs, 'ensure_mac_tweak_flows') @mock.patch.object(ovs, 'get_ovs_patch_ports_info') @mock.patch.object(linux_net, 'get_interface_address') @mock.patch.object(linux_net, 'ensure_arp_ndp_enabled_for_bridge') @mock.patch.object(linux_net, 'ensure_vlan_device_for_network') @mock.patch.object(linux_net, 'ensure_routing_table_for_bridge') def test_sync(self, mock_routing_bridge, mock_ensure_vlan_network, mock_ensure_arp, mock_nic_address, mock_get_patch_ports, mock_ensure_mac, mock_remove_flows, mock_exposed_ips, mock_get_ip_rules, mock_del_exposed_ips, mock_del_ip_rules, mock_del_ip_routes, mock_get_extra_route, mock_get_bridge_vlans, mock_delete_vlan_dev): self.mock_ovs_idl.get_ovn_bridge_mappings.return_value = [ 'net0:bridge0', 'net1:bridge1'] self.nb_idl.get_network_vlan_tag_by_network_name.side_effect = ( [10], [11]) fake_ip_rules = 'fake-ip-rules' mock_get_ip_rules.return_value = fake_ip_rules ips = [self.ipv4, self.ipv6] mock_exposed_ips.return_value = ips crlrp_port = fakes.create_object({ 'name': 'crlrp_port'}) lrp0 = fakes.create_object({ 'name': 'lrp_port', 'external_ids': { constants.OVN_CIDRS_EXT_ID_KEY: "10.0.0.1/24", constants.OVN_LS_NAME_EXT_ID_KEY: 'network1', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'fake-router'}}) port0 = fakes.create_object({ 'name': 'port-0', 'type': constants.OVN_VM_VIF_PORT_TYPE}) port1 = fakes.create_object({ 'name': 'port-1', 'type': constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE}) lb1 = fakes.create_object({ 'name': 'lb1', 'external_ids': {constants.OVN_LB_VIP_FIP_EXT_ID_KEY: 'fake-fip'} }) self.nb_idl.get_active_cr_lrp_on_chassis.return_value = [crlrp_port] self.nb_idl.get_active_local_lrps.return_value = [lrp0] self.nb_idl.get_active_lsp_on_chassis.return_value = [ port0, port1] self.nb_idl.get_active_local_lbs.return_value = [lb1] mock_ensure_crlrp_exposed = mock.patch.object( self.nb_bgp_driver, '_ensure_crlrp_exposed').start() mock_expose_subnet = mock.patch.object( self.nb_bgp_driver, '_expose_subnet').start() mock_ensure_lsp_exposed = mock.patch.object( self.nb_bgp_driver, '_ensure_lsp_exposed').start() mock_expose_ovn_lb_vip = mock.patch.object( self.nb_bgp_driver, '_expose_ovn_lb_vip').start() mock_expose_ovn_lb_fip = mock.patch.object( self.nb_bgp_driver, '_expose_ovn_lb_fip').start() mock_routing_bridge.return_value = ['fake-route'] mock_nic_address.return_value = self.mac mock_get_patch_ports.return_value = [1, 2] self.nb_idl.get_network_vlan_tags.return_value = [10, 11] mock_get_bridge_vlans.side_effect = [[10, 12], [11]] self.nb_bgp_driver.sync() expected_calls = [mock.call({}, 'bridge0', CONF.bgp_vrf_table_id), mock.call({}, 'bridge1', CONF.bgp_vrf_table_id)] mock_routing_bridge.assert_has_calls(expected_calls) expected_calls = [mock.call('bridge0', 10), mock.call('bridge1', 11)] mock_ensure_vlan_network.assert_has_calls(expected_calls) expected_calls = [mock.call('bridge0', 1, [10]), mock.call('bridge1', 2, [11])] mock_ensure_arp.assert_has_calls(expected_calls) expected_calls = [ mock.call('bridge0'), mock.call('bridge1')] mock_get_patch_ports.assert_has_calls(expected_calls) expected_calls = [ mock.call('bridge0', mock.ANY, [1, 2], constants.OVS_RULE_COOKIE), mock.call('bridge1', mock.ANY, [1, 2], constants.OVS_RULE_COOKIE)] mock_ensure_mac.assert_has_calls(expected_calls) expected_calls = [ mock.call(mock.ANY, 'bridge0', constants.OVS_RULE_COOKIE), mock.call(mock.ANY, 'bridge1', constants.OVS_RULE_COOKIE)] mock_remove_flows.assert_has_calls(expected_calls) mock_get_ip_rules.assert_called_once() mock_ensure_crlrp_exposed.assert_called_once_with(crlrp_port) mock_expose_subnet.assert_called_once_with( ["10.0.0.1/24"], {'associated_router': 'fake-router', 'network': 'network1', 'address_scopes': {4: None, 6: None}}) mock_ensure_lsp_exposed.assert_called_once_with(port0) mock_expose_ovn_lb_vip.assert_called_once_with(lb1) mock_expose_ovn_lb_fip.assert_called_once_with(lb1) mock_del_exposed_ips.assert_called_once_with( ips, CONF.bgp_nic) mock_del_ip_rules.assert_called_once_with(fake_ip_rules) mock_del_ip_routes.assert_called_once() bridge = set(self.nb_bgp_driver.ovn_bridge_mappings.values()).pop() mock_delete_vlan_dev.assert_called_once_with(bridge, 12) def test__ensure_lsp_exposed_fip(self): port0 = fakes.create_object({ 'name': 'port-0', 'external_ids': {constants.OVN_FIP_EXT_ID_KEY: "fip"}}) mock_get_port_external_ip_and_ls = mock.patch.object( self.nb_bgp_driver, 'get_port_external_ip_and_ls').start() mock_get_port_external_ip_and_ls.return_value = ("192.168.0.10", "fake-mac", "test-ls") mock_expose_fip = mock.patch.object( self.nb_bgp_driver, '_expose_fip').start() mock_expose_ip = mock.patch.object( self.nb_bgp_driver, '_expose_ip').start() self.nb_bgp_driver._ensure_lsp_exposed(port0) mock_get_port_external_ip_and_ls.assert_called_once_with(port0.name) mock_expose_fip.assert_called_once_with("192.168.0.10", "fake-mac", "test-ls", port0) mock_expose_ip.assert_not_called() def test__ensure_lsp_exposed_tenant_ls(self): port0 = fakes.create_object({ 'name': 'port-0', 'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: "test-ls"}}) self.nb_bgp_driver.ovn_tenant_ls = {"test-ls": True} mock_get_port_external_ip_and_ls = mock.patch.object( self.nb_bgp_driver, 'get_port_external_ip_and_ls').start() mock_expose_fip = mock.patch.object( self.nb_bgp_driver, '_expose_fip').start() mock_expose_ip = mock.patch.object( self.nb_bgp_driver, '_expose_ip').start() self.nb_bgp_driver._ensure_lsp_exposed(port0) mock_get_port_external_ip_and_ls.assert_not_called() mock_expose_fip.assert_not_called() mock_expose_ip.assert_not_called() @mock.patch.object(linux_net, 'get_ip_version') def test__ensure_lsp_exposed_no_fip_no_tenant_ls(self, mock_ip_version): port0 = utils.create_row( name='port-0', addresses=["fake_mac 192.168.0.10"], type=constants.OVN_VM_VIF_PORT_TYPE, external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: "test-ls"}) self.nb_bgp_driver.ovn_tenant_ls = {} self.nb_bgp_driver.ovn_provider_ls = {} mock_get_port_external_ip_and_ls = mock.patch.object( self.nb_bgp_driver, 'get_port_external_ip_and_ls').start() mock_expose_fip = mock.patch.object( self.nb_bgp_driver, '_expose_fip').start() mock_expose_ip = mock.patch.object( self.nb_bgp_driver, '_expose_ip').start() mock_expose_ip.return_value = ['192.168.0.10'] mock_is_ls_provider = mock.patch.object( self.nb_bgp_driver, 'is_ls_provider', return_value=True).start() mock_get_ls_localnet_info = mock.patch.object( self.nb_bgp_driver, '_get_ls_localnet_info').start() mock_get_ls_localnet_info.return_value = ('fake-localnet', 'br-ex', 10) mock_ip_version.return_value = constants.IP_VERSION_4 self.nb_bgp_driver._ensure_lsp_exposed(port0) mock_get_port_external_ip_and_ls.assert_not_called() mock_is_ls_provider.assert_called_once_with('test-ls') mock_get_ls_localnet_info.assert_called_once_with('test-ls') mock_expose_fip.assert_not_called() mock_expose_ip.assert_called_once_with( ['192.168.0.10'], 'fake_mac', 'test-ls', 'br-ex', 10, constants.OVN_VM_VIF_PORT_TYPE, []) def test__ensure_crlrp_exposed(self): port = fakes.create_object({ 'name': 'lrp-port', 'networks': ['172.24.16.2/24'], 'mac': "fake_mac", 'status': {'hosting-chassis': self.nb_bgp_driver.chassis_id}, 'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: "test-ls"}}) mock_get_ls_localnet_info = mock.patch.object( self.nb_bgp_driver, '_get_ls_localnet_info').start() mock_get_ls_localnet_info.return_value = ('fake-localnet', 'br-ex', 10) mock_expose_ip = mock.patch.object( self.nb_bgp_driver, '_expose_ip').start() mock_is_ls_provider = mock.patch.object( self.nb_bgp_driver, 'is_ls_provider', return_value=True).start() self.nb_bgp_driver._ensure_crlrp_exposed(port) mock_is_ls_provider.assert_called_once_with('test-ls') mock_expose_ip.assert_called_once_with( ['172.24.16.2'], 'fake_mac', 'test-ls', 'br-ex', 10, constants.OVN_CR_LRP_PORT_TYPE, ['172.24.16.2/24'], router=None) def test__ensure_crlrp_exposed_no_networks(self): port = fakes.create_object({ 'name': 'lrp-port', 'networks': [], 'mac': "fake_mac", 'status': {'hosting-chassis': self.nb_bgp_driver.chassis_id}, 'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: "test-ls"}}) mock_expose_ip = mock.patch.object( self.nb_bgp_driver, '_expose_ip').start() self.nb_bgp_driver._ensure_crlrp_exposed(port) mock_expose_ip.assert_not_called() def test__ensure_crlrp_exposed_no_logical_switch(self): port = fakes.create_object({ 'name': 'lrp-port', 'networks': ['172.24.16.2/24'], 'mac': "fake_mac", 'status': {'hosting-chassis': self.nb_bgp_driver.chassis_id}, 'external_ids': {}}) mock_expose_ip = mock.patch.object( self.nb_bgp_driver, '_expose_ip').start() self.nb_bgp_driver._ensure_crlrp_exposed(port) mock_expose_ip.assert_not_called() def test__ensure_crlrp_exposed_no_bridge(self): port = fakes.create_object({ 'name': 'lrp-port', 'networks': ['172.24.16.2/24'], 'mac': "fake_mac", 'status': {'hosting-chassis': self.nb_bgp_driver.chassis_id}, 'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: "test-ls"}}) mock_get_ls_localnet_info = mock.patch.object( self.nb_bgp_driver, '_get_ls_localnet_info').start() mock_get_ls_localnet_info.return_value = (None, None, None) mock_expose_ip = mock.patch.object( self.nb_bgp_driver, '_expose_ip').start() self.nb_bgp_driver._ensure_crlrp_exposed(port) mock_expose_ip.assert_not_called() @mock.patch.object(wire_utils, 'wire_provider_port') @mock.patch.object(bgp_utils, 'announce_ips') def test__expose_provider_port_successful(self, mock_announce_ips, mock_wire_provider_port): mock_wire_provider_port.return_value = True port_ips = ['192.168.0.1', '192.168.0.2'] bridge_device = self.bridge bridge_vlan = None proxy_cidrs = ['192.168.0.0/24'] self.nb_bgp_driver._expose_provider_port( port_ips, 'fake-mac', 'test-ls', bridge_device, bridge_vlan, 'fake-localnet', proxy_cidrs) mock_wire_provider_port.assert_called_once_with( self.ovn_routing_tables_routes, {}, port_ips, bridge_device, bridge_vlan, 'fake-localnet', self.ovn_routing_tables, proxy_cidrs, mac='fake-mac', ovn_idl=mock.ANY) mock_announce_ips.assert_called_once_with(port_ips) @mock.patch.object(wire_utils, 'wire_provider_port') @mock.patch.object(bgp_utils, 'announce_ips') def test__expose_provider_port_failure(self, mock_announce_ips, mock_wire_provider_port): mock_wire_provider_port.return_value = False port_ips = ['192.168.0.1', '192.168.0.2'] bridge_device = self.bridge bridge_vlan = None proxy_cidrs = ['192.168.0.0/24'] self.nb_bgp_driver._expose_provider_port( port_ips, 'fake-mac', 'test-ls', bridge_device, bridge_vlan, 'fake-localnet', proxy_cidrs) mock_wire_provider_port.assert_called_once_with( self.ovn_routing_tables_routes, {}, port_ips, bridge_device, bridge_vlan, 'fake-localnet', self.ovn_routing_tables, proxy_cidrs, mac='fake-mac', ovn_idl=mock.ANY) mock_announce_ips.assert_not_called() @mock.patch.object(wire_utils, 'unwire_provider_port') @mock.patch.object(bgp_utils, 'withdraw_ips') def test__withdraw_provider_port(self, mock_withdraw_ips, mock_unwire_provider_port): port_ips = ['192.168.0.1', '192.168.0.2'] bridge_device = self.bridge bridge_vlan = None proxy_cidrs = ['192.168.0.0/24'] self.nb_bgp_driver._withdraw_provider_port( port_ips, 'test-ls', bridge_device, bridge_vlan, proxy_cidrs) mock_withdraw_ips.assert_called_once_with(port_ips) mock_unwire_provider_port.assert_called_once_with( self.ovn_routing_tables_routes, port_ips, bridge_device, bridge_vlan, self.ovn_routing_tables, proxy_cidrs, ovn_idl=mock.ANY) def test__get_bridge_for_localnet_port(self): localnet = fakes.create_object({ 'options': {'network_name': 'fake-network'}, 'tag': [10]}) bridge_device, bridge_vlan = ( self.nb_bgp_driver._get_bridge_for_localnet_port(localnet)) self.assertEqual(bridge_device, self.bridge) self.assertEqual(bridge_vlan, 10) def test__get_bridge_for_localnet_port_no_network_no_tag(self): localnet = fakes.create_object({ 'options': {}, 'tag': None}) bridge_device, bridge_vlan = ( self.nb_bgp_driver._get_bridge_for_localnet_port(localnet)) self.assertEqual(bridge_device, None) self.assertEqual(bridge_vlan, None) def test_is_ip_exposed(self): self.nb_bgp_driver._exposed_ips['fake-switch'] = {'fake-ip': {}} self.assertTrue(self.nb_bgp_driver.is_ip_exposed('fake-switch', 'fake-ip')) self.assertFalse(self.nb_bgp_driver.is_ip_exposed('no-switch', 'fake-ip')) self.assertFalse(self.nb_bgp_driver.is_ip_exposed('fake-switch', 'other-ip')) def _test_expose_ip(self, ips, ips_info): mock_expose_provider_port = mock.patch.object( self.nb_bgp_driver, '_expose_provider_port').start() mock_get_ls_localnet_info = mock.patch.object( self.nb_bgp_driver, '_get_ls_localnet_info').start() mock_get_ls_localnet_info.return_value = ('fake-localnet', 'br-ex', 10) self.nb_bgp_driver.ovn_bridge_mappings = {'fake-localnet': 'br-ex'} mock_expose_subnet = mock.patch.object( self.nb_bgp_driver, '_expose_subnet').start() if (ips_info.get('router') and ips_info['type'] == constants.OVN_CR_LRP_PORT_TYPE): lrp0 = fakes.create_object({ 'name': 'lrp_port', 'external_ids': { constants.OVN_CIDRS_EXT_ID_KEY: "10.0.0.1/24", constants.OVN_LS_NAME_EXT_ID_KEY: 'network1', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}}) self.nb_idl.get_active_local_lrps.return_value = [lrp0] lb1 = fakes.create_object({ 'name': 'lb1', 'external_ids': { constants.OVN_LB_VIP_FIP_EXT_ID_KEY: 'fake-fip'}}) self.nb_idl.get_active_local_lbs.return_value = [lb1] mock_expose_ovn_lb_vip = mock.patch.object( self.nb_bgp_driver, '_expose_ovn_lb_vip').start() mock_expose_ovn_lb_fip = mock.patch.object( self.nb_bgp_driver, '_expose_ovn_lb_fip').start() self.nb_bgp_driver.expose_ip(ips, ips_info) if not ips_info['logical_switch']: mock_expose_provider_port.assert_not_called() mock_get_ls_localnet_info.assert_not_called() return mock_get_ls_localnet_info.assert_called_once_with( ips_info['logical_switch']) self.assertEqual( self.nb_bgp_driver.ovn_provider_ls[ips_info['logical_switch']], {'bridge_device': 'br-ex', 'bridge_vlan': 10, 'localnet': 'fake-localnet'}) if (ips_info['type'] in [constants.OVN_VIRTUAL_VIF_PORT_TYPE, constants.OVN_CR_LRP_PORT_TYPE] and ips_info['cidrs']): mock_expose_provider_port.assert_called_once_with( ips, 'fake-mac', 'test-ls', 'br-ex', 10, 'fake-localnet', ips_info['cidrs']) else: mock_expose_provider_port.assert_called_once_with( ips, 'fake-mac', 'test-ls', 'br-ex', 10, 'fake-localnet', []) if (ips_info.get('router') and ips_info['type'] == constants.OVN_CR_LRP_PORT_TYPE): mock_expose_subnet.assert_called_once_with( ["10.0.0.1/24"], {'associated_router': 'router1', 'network': 'network1', 'address_scopes': {4: None, 6: None}}) mock_expose_ovn_lb_vip.assert_called_once_with(lb1) mock_expose_ovn_lb_fip.assert_called_once_with(lb1) def test_expose_ip(self): ips = [self.ipv4, self.ipv6] ips_info = { 'mac': 'fake-mac', 'cidrs': [], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': 'test-ls' } self._test_expose_ip(ips, ips_info) def test_expose_ip_virtual(self): ips = [self.ipv4, self.ipv6] ips_info = { 'mac': 'fake-mac', 'cidrs': ['test-cidr'], 'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE, 'logical_switch': 'test-ls' } self._test_expose_ip(ips, ips_info) def test_expose_ip_no_switch(self): ips = [self.ipv4, self.ipv6] ips_info = { 'mac': 'fake-mac', 'cidrs': [], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': None } self._test_expose_ip(ips, ips_info) def test_expose_ip_router(self): ips = [self.ipv4, self.ipv6] ips_info = { 'mac': 'fake-mac', 'cidrs': ['test-cidr'], 'type': constants.OVN_CR_LRP_PORT_TYPE, 'logical_switch': 'test-ls', 'router': 'router1' } self._test_expose_ip(ips, ips_info) @mock.patch.object(linux_net, 'get_ip_version') def _test_withdraw_ip(self, ips, ips_info, provider, mock_ip_version): mock_withdraw_provider_port = mock.patch.object( self.nb_bgp_driver, '_withdraw_provider_port').start() mock_get_ls_localnet_info = mock.patch.object( self.nb_bgp_driver, '_get_ls_localnet_info').start() mock_ip_version.return_value = constants.IP_VERSION_6 self.nb_idl.ls_has_virtual_ports.return_value = False self.nb_idl.get_active_lsp_on_chassis.return_value = False if provider: mock_get_ls_localnet_info.return_value = ('fake-localnet', 'br-ex', 10) else: mock_get_ls_localnet_info.return_value = (None, None, None) mock_withdraw_subnet = mock.patch.object( self.nb_bgp_driver, '_withdraw_subnet').start() if (ips_info.get('router') and ips_info['type'] == constants.OVN_CR_LRP_PORT_TYPE): lrp0 = fakes.create_object({ 'name': 'lrp_port', 'external_ids': { constants.OVN_CIDRS_EXT_ID_KEY: "10.0.0.1/24", constants.OVN_LS_NAME_EXT_ID_KEY: 'network1', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}}) self.nb_idl.get_active_local_lrps.return_value = [lrp0] lb1 = fakes.create_object({ 'name': 'lb1', 'external_ids': { constants.OVN_LB_VIP_FIP_EXT_ID_KEY: 'fake-fip'}}) self.nb_idl.get_active_local_lbs.return_value = [lb1] mock_withdraw_ovn_lb_vip = mock.patch.object( self.nb_bgp_driver, '_withdraw_ovn_lb_vip').start() mock_withdraw_ovn_lb_fip = mock.patch.object( self.nb_bgp_driver, '_withdraw_ovn_lb_fip').start() self.nb_bgp_driver.withdraw_ip(ips, ips_info) if not ips_info['logical_switch']: mock_get_ls_localnet_info.assert_not_called() mock_withdraw_provider_port.assert_not_called() return if not provider: mock_get_ls_localnet_info.assert_called_once_with( ips_info['logical_switch']) mock_withdraw_provider_port.assert_not_called() return mock_get_ls_localnet_info.assert_called_once_with( ips_info['logical_switch']) if (ips_info['type'] in [constants.OVN_VIRTUAL_VIF_PORT_TYPE, constants.OVN_CR_LRP_PORT_TYPE] and ips_info['cidrs']): mock_withdraw_provider_port.assert_called_once_with( ips, 'test-ls', 'br-ex', 10, ips_info['cidrs']) else: mock_withdraw_provider_port.assert_called_once_with( ips, 'test-ls', 'br-ex', 10, []) if ips_info.get('router'): mock_withdraw_subnet.assert_called_once_with( ["10.0.0.1/24"], {'associated_router': 'router1', 'network': 'network1', 'address_scopes': {4: None, 6: None}}) mock_withdraw_ovn_lb_vip.assert_called_once_with(lb1) mock_withdraw_ovn_lb_fip.assert_called_once_with(lb1) def test_withdraw_ip(self): ips = [self.ipv4, self.ipv6] ips_info = { 'mac': 'fake-mac', 'cidrs': [], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': 'test-ls' } self._test_withdraw_ip(ips, ips_info, True) def test_withdraw_ip_no_provider(self): ips = [self.ipv4, self.ipv6] ips_info = { 'mac': 'fake-mac', 'cidrs': [], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': 'test-ls' } self._test_withdraw_ip(ips, ips_info, False) def test_withdraw_ip_virtual(self): ips = [self.ipv4, self.ipv6] ips_info = { 'mac': 'fake-mac', 'cidrs': ['test-cidr'], 'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE, 'logical_switch': 'test-ls' } self._test_withdraw_ip(ips, ips_info, True) def test_withdraw_ip_no_switch(self): ips = [self.ipv4, self.ipv6] ips_info = { 'mac': 'fake-mac', 'cidrs': [], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': None } self._test_withdraw_ip(ips, ips_info, True) def test_withdraw_ip_router(self): ips = [self.ipv4, self.ipv6] ips_info = { 'mac': 'fake-mac', 'cidrs': ['test-cidr'], 'type': constants.OVN_CR_LRP_PORT_TYPE, 'logical_switch': 'test-ls', 'router': 'router1' } self._test_withdraw_ip(ips, ips_info, True) def test__get_ls_localnet_info(self): logical_switch = 'lswitch1' fake_localnet_port = fakes.create_object({ 'name': 'fake-localnet-port'}) localnet_ports = [fake_localnet_port] self.nb_idl.ls_get_localnet_ports.return_value.execute.return_value = ( localnet_ports) mock_get_bridge_for_localnet_port = mock.patch.object( self.nb_bgp_driver, '_get_bridge_for_localnet_port').start() mock_get_bridge_for_localnet_port.return_value = ('br-ex', 10) ret = self.nb_bgp_driver._get_ls_localnet_info(logical_switch) self.assertEqual(ret, (fake_localnet_port.name, 'br-ex', 10)) self.nb_idl.ls_get_localnet_ports.assert_called_once_with( logical_switch, if_exists=True) mock_get_bridge_for_localnet_port.assert_called_once_with( localnet_ports[0]) def test_get_ls_localnet_info_not_provider_network(self): logical_switch = 'lswitch1' localnet_ports = [] self.nb_idl.ls_get_localnet_ports.return_value.execute.return_value = ( localnet_ports) mock_get_bridge_for_localnet_port = mock.patch.object( self.nb_bgp_driver, '_get_bridge_for_localnet_port').start() ret = self.nb_bgp_driver._get_ls_localnet_info(logical_switch) self.nb_idl.ls_get_localnet_ports.assert_called_once_with( logical_switch, if_exists=True) mock_get_bridge_for_localnet_port.assert_not_called() self.assertEqual(ret, (None, None, None)) def test_get_port_external_ip_and_ls(self): nat_entry = fakes.create_object({ 'external_ids': {constants.OVN_FIP_NET_EXT_ID_KEY: 'net1'}, 'external_ip': 'fake-ip', 'external_mac': 'fake-mac'}) self.nb_idl.get_nat_by_logical_port.return_value = nat_entry ret = self.nb_bgp_driver.get_port_external_ip_and_ls('fake-port') expected_result = (nat_entry.external_ip, nat_entry.external_mac, "neutron-net1") self.assertEqual(ret, expected_result) def test_get_port_external_ip_and_ls_no_nat_entry(self): self.nb_idl.get_nat_by_logical_port.return_value = None ret = self.nb_bgp_driver.get_port_external_ip_and_ls('fake-port') self.assertEqual(ret, (None, None, None)) def test_get_port_external_ip_and_ls_no_external_id(self): nat_entry = fakes.create_object({ 'external_ids': {}, 'external_ip': 'fake-ip', 'external_mac': 'fake-mac'}) self.nb_idl.get_nat_by_logical_port.return_value = nat_entry ret = self.nb_bgp_driver.get_port_external_ip_and_ls('fake-port') self.assertEqual(ret, (nat_entry.external_ip, nat_entry.external_mac, None)) def test_expose_fip(self): ip = '10.0.0.1' mac = 'fake-mac' logical_switch = 'lswitch1' mock_get_ls_localnet_info = mock.patch.object( self.nb_bgp_driver, '_get_ls_localnet_info').start() mock_get_ls_localnet_info.return_value = ('fake-localnet', 'br-ex', 100) self.nb_bgp_driver.ovn_bridge_mappings = {'fake-localnet': 'br-ex'} mock_expose_provider_port = mock.patch.object( self.nb_bgp_driver, '_expose_provider_port').start() row = fakes.create_object({ 'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}}) ret = self.nb_bgp_driver.expose_fip(ip, mac, logical_switch, row) mock_get_ls_localnet_info.assert_called_once_with(logical_switch) mock_expose_provider_port.assert_called_once_with([ip], mac, 'test-ls', 'br-ex', 100, 'fake-localnet') self.assertTrue(ret) def test_expose_fip_no_device(self): ip = '10.0.0.1' mac = 'fake-mac' logical_switch = 'lswitch1' mock_get_ls_localnet_info = mock.patch.object( self.nb_bgp_driver, '_get_ls_localnet_info').start() mock_get_ls_localnet_info.return_value = (None, None, None) mock_expose_provider_port = mock.patch.object( self.nb_bgp_driver, '_expose_provider_port').start() row = fakes.create_object({ 'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}}) ret = self.nb_bgp_driver.expose_fip(ip, mac, logical_switch, row) mock_get_ls_localnet_info.assert_called_once_with(logical_switch) mock_expose_provider_port.assert_not_called() self.assertNotIn( ip, self.nb_bgp_driver._exposed_ips.get('test-ls', {}).keys()) self.assertFalse(ret) def test_withdraw_fip(self): ip = '10.0.0.1' self.nb_bgp_driver._exposed_ips['test-ls'] = { ip: {'bridge_device': 'br-ex', 'bridge_vlan': 100}} mock_withdraw_provider_port = mock.patch.object( self.nb_bgp_driver, '_withdraw_provider_port').start() row = fakes.create_object({ 'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}}) self.nb_bgp_driver.withdraw_fip(ip, row) mock_withdraw_provider_port.assert_called_once_with([ip], 'test-ls', 'br-ex', 100) def test_withdraw_fip_not_found(self): ip = '10.0.0.1' self.nb_bgp_driver._exposed_ips = {} mock_withdraw_provider_port = mock.patch.object( self.nb_bgp_driver, '_withdraw_provider_port').start() row = fakes.create_object({ 'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}}) self.nb_bgp_driver.withdraw_fip(ip, row) mock_withdraw_provider_port.assert_not_called() @mock.patch.object(bgp_utils, 'announce_ips') def test_expose_remote_ip(self, m_announce_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) @mock.patch.object(driver_utils, 'is_ipv6_gua') @mock.patch.object(bgp_utils, 'announce_ips') def test_expose_remote_ip_gua(self, m_announce_ips, m_gua): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') ips = [self.ipv4, self.ipv6] ips_info = { 'mac': 'fake-mac', 'cidrs': [], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': 'test-ls' } m_gua.side_effect = [False, True] self.nb_bgp_driver.expose_remote_ip(ips, ips_info) m_announce_ips.assert_called_once_with([self.ipv6]) @mock.patch.object(bgp_utils, 'withdraw_ips') def test_withdraw_remote_ip(self, m_withdraw_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) @mock.patch.object(driver_utils, 'is_ipv6_gua') @mock.patch.object(bgp_utils, 'withdraw_ips') def test_withdraw_remote_ip_gua(self, m_withdraw_ips, m_gua): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') ips = [self.ipv4, self.ipv6] ips_info = { 'mac': 'fake-mac', 'cidrs': [], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': 'test-ls' } m_gua.side_effect = [False, True] self.nb_bgp_driver.withdraw_remote_ip(ips, ips_info) m_withdraw_ips.assert_called_once_with([self.ipv6]) def test_expose_subnet(self): ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': 'router1', 'network': 'network1', 'address_scopes': {4: None, 6: None}} mock_expose_router_lsp = mock.patch.object( self.nb_bgp_driver, '_expose_router_lsp').start() mock_expose_remote_ip = mock.patch.object( self.nb_bgp_driver, '_expose_remote_ip').start() port0 = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.5'], external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "192.168.0.5/24", constants.OVN_LS_NAME_EXT_ID_KEY: 'network1' }) port1 = utils.create_row( type=constants.OVN_VIRTUAL_VIF_PORT_TYPE, addresses=['mac 192.168.0.6'], external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "192.168.0.6/24", constants.OVN_LS_NAME_EXT_ID_KEY: 'network1' }) self.nb_idl.get_active_lsp.return_value = [port0, port1] self.nb_bgp_driver.expose_subnet(ips, subnet_info) mock_expose_router_lsp.assert_called_once_with( ips, subnet_info, self.router1_info) ips_info0 = {'mac': 'mac', 'cidrs': ['192.168.0.5/24'], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': 'network1'} ips_info1 = {'mac': 'mac', 'cidrs': ['192.168.0.6/24'], 'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE, 'logical_switch': 'network1'} expected_calls = [mock.call(['192.168.0.5'], ips_info0), mock.call(['192.168.0.6'], ips_info1)] mock_expose_remote_ip.assert_has_calls(expected_calls) def test_expose_subnet_no_router(self): ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': None, 'network': 'network1', 'address_scopes': {4: None, 6: None}} mock_expose_router_lsp = mock.patch.object( self.nb_bgp_driver, '_expose_router_lsp').start() self.nb_bgp_driver.expose_subnet(ips, subnet_info) mock_expose_router_lsp.assert_not_called() def test_expose_subnet_no_cr_lrp(self): ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': 'other-router', 'network': 'network1', 'address_scopes': {4: None, 6: None}} mock_expose_router_lsp = mock.patch.object( self.nb_bgp_driver, '_expose_router_lsp').start() self.nb_bgp_driver.expose_subnet(ips, subnet_info) mock_expose_router_lsp.assert_not_called() def test_expose_subnet_not_per_lsp(self): CONF.set_override('advertisement_method_tenant_networks', constants.ADVERTISEMENT_METHOD_SUBNET) self.addCleanup(CONF.clear_override, 'advertisement_method_tenant_networks') ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': 'router1', 'network': 'network1', 'address_scopes': {4: None, 6: None}} mock_expose_router_lsp = mock.patch.object( self.nb_bgp_driver, '_expose_router_lsp').start() mock_expose_remote_ip = mock.patch.object( self.nb_bgp_driver, '_expose_remote_ip').start() self.nb_bgp_driver.expose_subnet(ips, subnet_info) mock_expose_router_lsp.assert_called_once_with(ips, subnet_info, self.router1_info) self.nb_idl.get_active_lsp.assert_not_called() mock_expose_remote_ip.assert_not_called() def _test_expose_subnet_require_snat_disabled(self, partial_continue=False): CONF.set_override('require_snat_disabled_for_tenant_networks', True) self.addCleanup(CONF.clear_override, 'require_snat_disabled_for_tenant_networks') ips = ['10.0.0.1/24'] if partial_continue: ips.append(self.ipv6 + '/64') subnet_info = { 'associated_router': 'router1', 'network': 'network1', 'address_scopes': {4: None, 6: None}} mock_expose_router_lsp = mock.patch.object( self.nb_bgp_driver, '_expose_router_lsp').start() mock_expose_remote_ip = mock.patch.object( self.nb_bgp_driver, '_expose_remote_ip').start() router = utils.create_row( nat=[utils.create_row( type=constants.OVN_SNAT, logical_ip='10.0.0.0/24', )], ) self.nb_idl.get_router.return_value = router self.nb_bgp_driver.expose_subnet(ips, subnet_info) gateway_router = subnet_info['associated_router'] self.nb_idl.get_router.assert_called_once_with(gateway_router) if not partial_continue: self.nb_idl.get_active_lsp.assert_not_called() mock_expose_remote_ip.assert_not_called() mock_expose_router_lsp.assert_not_called() else: # partial continue scenario is when SNAT is not enabled for the # router, so only the ipv6 should match mock_expose_router_lsp.assert_called_once_with( [self.ipv6 + '/64'], subnet_info, self.router1_info) ips_info0 = {'mac': 'mac', 'cidrs': ['192.168.0.5/24'], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': 'network1'} ips_info1 = {'mac': 'mac', 'cidrs': ['192.168.0.6/24'], 'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE, 'logical_switch': 'network1'} expected_calls = [mock.call(['192.168.0.5'], ips_info0), mock.call(['192.168.0.6'], ips_info1)] mock_expose_remote_ip.assert_has_calls(expected_calls) def test_expose_subnet_require_snat_disabled(self): self._test_expose_subnet_require_snat_disabled( partial_continue=False, ) def test_expose_subnet_require_snat_disabled_partial_continue(self): # Setup get_active_lsp for partial_continue scenario port0 = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.5'], external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "192.168.0.5/24", constants.OVN_LS_NAME_EXT_ID_KEY: 'network1' }) port1 = utils.create_row( type=constants.OVN_VIRTUAL_VIF_PORT_TYPE, addresses=['mac 192.168.0.6'], external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "192.168.0.6/24", constants.OVN_LS_NAME_EXT_ID_KEY: 'network1' }) self.nb_idl.get_active_lsp.return_value = [port0, port1] self._test_expose_subnet_require_snat_disabled( partial_continue=True, ) def test_withdraw_subnet(self): ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': 'router1', 'network': 'network1', 'address_scopes': {4: None, 6: None}} mock_withdraw_router_lsp = mock.patch.object( self.nb_bgp_driver, '_withdraw_router_lsp').start() mock_withdraw_remote_ip = mock.patch.object( self.nb_bgp_driver, '_withdraw_remote_ip').start() port0 = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.5'], external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "192.168.0.5/24", constants.OVN_LS_NAME_EXT_ID_KEY: 'network1' }) port1 = utils.create_row( type=constants.OVN_VIRTUAL_VIF_PORT_TYPE, addresses=['mac 192.168.0.6'], external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "192.168.0.6/24", constants.OVN_LS_NAME_EXT_ID_KEY: 'network1' }) self.nb_idl.get_active_lsp.return_value = [port0, port1] self.nb_bgp_driver.withdraw_subnet(ips, subnet_info) mock_withdraw_router_lsp.assert_called_once_with( ips, subnet_info, self.router1_info) ips_info0 = {'mac': 'mac', 'cidrs': ['192.168.0.5/24'], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': 'network1'} ips_info1 = {'mac': 'mac', 'cidrs': ['192.168.0.6/24'], 'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE, 'logical_switch': 'network1'} expected_calls = [mock.call(['192.168.0.5'], ips_info0), mock.call(['192.168.0.6'], ips_info1)] mock_withdraw_remote_ip.assert_has_calls(expected_calls) def test_withdraw_subnet_no_router(self): ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': None, 'network': 'network1', 'address_scopes': {4: None, 6: None}} mock_withdraw_router_lsp = mock.patch.object( self.nb_bgp_driver, '_withdraw_router_lsp').start() self.nb_bgp_driver.withdraw_subnet(ips, subnet_info) mock_withdraw_router_lsp.assert_not_called() def test_withdraw_subnet_no_cr_lrp(self): ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': 'other-router', 'network': 'network1', 'address_scopes': {4: None, 6: None}} mock_withdraw_router_lsp = mock.patch.object( self.nb_bgp_driver, '_withdraw_router_lsp').start() self.nb_bgp_driver.withdraw_subnet(ips, subnet_info) mock_withdraw_router_lsp.assert_not_called() @mock.patch.object(wire_utils, 'wire_lrp_port') def test__expose_router_lsp(self, mock_wire): ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': 'other-router', 'network': 'network1', 'address_scopes': {4: None, 6: None}} CONF.set_override('advertisement_method_tenant_networks', constants.ADVERTISEMENT_METHOD_SUBNET) self.addCleanup(CONF.clear_override, 'advertisement_method_tenant_networks') ret = self.nb_bgp_driver._expose_router_lsp(ips, subnet_info, self.router1_info) self.assertTrue(ret) mock_wire.assert_called_once_with( mock.ANY, '10.0.0.0/24', self.router1_info['bridge_device'], self.router1_info['bridge_vlan'], mock.ANY, self.router1_info['ips']) @mock.patch.object(wire_utils, 'wire_lrp_port') def test__expose_router_lsp_per_host(self, mock_wire): ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': 'other-router', 'network': 'network1', 'address_scopes': {4: None, 6: None}} ret = self.nb_bgp_driver._expose_router_lsp(ips, subnet_info, self.router1_info) self.assertTrue(ret) mock_wire.assert_called_once_with( mock.ANY, '10.0.0.1/24', self.router1_info['bridge_device'], self.router1_info['bridge_vlan'], mock.ANY, self.router1_info['ips']) @mock.patch.object(wire_utils, 'wire_lrp_port') def test__expose_router_lsp_exception(self, mock_wire): ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': 'other-router', 'network': 'network1', 'address_scopes': {4: None, 6: None}} mock_wire.side_effect = Exception CONF.set_override('advertisement_method_tenant_networks', constants.ADVERTISEMENT_METHOD_SUBNET) self.addCleanup(CONF.clear_override, 'advertisement_method_tenant_networks') self.assertRaises(exceptions.WireFailure, self.nb_bgp_driver._expose_router_lsp, ips, subnet_info, self.router1_info) mock_wire.assert_called_once_with( mock.ANY, '10.0.0.0/24', self.router1_info['bridge_device'], self.router1_info['bridge_vlan'], mock.ANY, self.router1_info['ips']) @mock.patch.object(wire_utils, 'wire_lrp_port') def test__expose_router_lsp_no_tenants(self, mock_wire): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': 'other-router', 'network': 'network1', 'address_scopes': {4: None, 6: None}} ret = self.nb_bgp_driver._expose_router_lsp(ips, subnet_info, self.router1_info) self.assertTrue(ret) mock_wire.assert_not_called() @mock.patch.object(driver_utils, 'is_ipv6_gua') @mock.patch.object(wire_utils, 'wire_lrp_port') def test__expose_router_lsp_no_tenants_but_gua(self, mock_wire, mock_gua): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') CONF.set_override('advertisement_method_tenant_networks', constants.ADVERTISEMENT_METHOD_SUBNET) self.addCleanup(CONF.clear_override, 'advertisement_method_tenant_networks') ips = ['10.0.0.1/24', '2002::1/64'] subnet_info = { 'associated_router': 'other-router', 'network': 'network1', 'address_scopes': {4: None, 6: None}} mock_gua.side_effect = [False, True] ret = self.nb_bgp_driver._expose_router_lsp(ips, subnet_info, self.router1_info) self.assertTrue(ret) mock_wire.assert_called_once_with( mock.ANY, '2002::/64', self.router1_info['bridge_device'], self.router1_info['bridge_vlan'], mock.ANY, self.router1_info['ips']) @mock.patch.object(wire_utils, 'unwire_lrp_port') def test__withdraw_router_lsp(self, mock_unwire): ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': 'other-router', 'network': 'network1', 'address_scopes': {4: None, 6: None}} CONF.set_override('advertisement_method_tenant_networks', constants.ADVERTISEMENT_METHOD_SUBNET) self.addCleanup(CONF.clear_override, 'advertisement_method_tenant_networks') ret = self.nb_bgp_driver._withdraw_router_lsp(ips, subnet_info, self.router1_info) self.assertTrue(ret) mock_unwire.assert_called_once_with( mock.ANY, '10.0.0.0/24', self.router1_info['bridge_device'], self.router1_info['bridge_vlan'], mock.ANY, self.router1_info['ips']) @mock.patch.object(wire_utils, 'unwire_lrp_port') def test__withdraw_router_lsp_per_host(self, mock_unwire): ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': 'other-router', 'network': 'network1', 'address_scopes': {4: None, 6: None}} ret = self.nb_bgp_driver._withdraw_router_lsp(ips, subnet_info, self.router1_info) self.assertTrue(ret) mock_unwire.assert_called_once_with( mock.ANY, '10.0.0.1/24', self.router1_info['bridge_device'], self.router1_info['bridge_vlan'], mock.ANY, self.router1_info['ips']) @mock.patch.object(wire_utils, 'unwire_lrp_port') def test__withdraw_router_lsp_exception(self, mock_unwire): ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': 'other-router', 'network': 'network1', 'address_scopes': {4: None, 6: None}} mock_unwire.side_effect = Exception CONF.set_override('advertisement_method_tenant_networks', constants.ADVERTISEMENT_METHOD_SUBNET) self.addCleanup(CONF.clear_override, 'advertisement_method_tenant_networks') self.assertRaises( exceptions.UnwireFailure, self.nb_bgp_driver._withdraw_router_lsp, ips, subnet_info, self.router1_info) mock_unwire.assert_called_once_with( mock.ANY, '10.0.0.0/24', self.router1_info['bridge_device'], self.router1_info['bridge_vlan'], mock.ANY, self.router1_info['ips']) @mock.patch.object(wire_utils, 'unwire_lrp_port') def test__withdraw_router_lsp_no_tenants(self, mock_unwire): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') ips = ['10.0.0.1/24'] subnet_info = { 'associated_router': 'other-router', 'network': 'network1', 'address_scopes': {4: None, 6: None}} CONF.set_override('advertisement_method_tenant_networks', constants.ADVERTISEMENT_METHOD_SUBNET) self.addCleanup(CONF.clear_override, 'advertisement_method_tenant_networks') ret = self.nb_bgp_driver._withdraw_router_lsp(ips, subnet_info, self.router1_info) self.assertTrue(ret) mock_unwire.assert_not_called() @mock.patch.object(driver_utils, 'is_ipv6_gua') @mock.patch.object(wire_utils, 'unwire_lrp_port') def test__withdraw_router_lsp_no_tenants_but_gua(self, mock_unwire, mock_gua): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') CONF.set_override('advertisement_method_tenant_networks', constants.ADVERTISEMENT_METHOD_SUBNET) self.addCleanup(CONF.clear_override, 'advertisement_method_tenant_networks') ips = ['10.0.0.1/24', '2002::1/64'] subnet_info = { 'associated_router': 'other-router', 'network': 'network1', 'address_scopes': {4: None, 6: None}} mock_gua.side_effect = [False, True] ret = self.nb_bgp_driver._withdraw_router_lsp(ips, subnet_info, self.router1_info) self.assertTrue(ret) mock_unwire.assert_called_once_with( mock.ANY, '2002::/64', self.router1_info['bridge_device'], self.router1_info['bridge_vlan'], mock.ANY, self.router1_info['ips']) def test__ips_in_address_scope(self): subnet_pool_addr_scope4 = '88e8aec3-da29-402d-becf-9fa2c38e69b8' subnet_pool_addr_scope6 = 'b7834aeb-2aa2-40ac-a8b5-2cded713cb58' _scopes = { constants.IP_VERSION_4: subnet_pool_addr_scope4, constants.IP_VERSION_6: subnet_pool_addr_scope6, } self.nb_bgp_driver.allowed_address_scopes = [subnet_pool_addr_scope4] ips = ['10.0.0.1/24', '2002::1/64'] # Allowed address scope is v4, so v6 should be removed. ret = self.nb_bgp_driver._ips_in_address_scope(ips, _scopes) self.assertListEqual(ret, ['10.0.0.1/24']) def test__address_scope_allowed(self): subnet_pool_addr_scope4 = '88e8aec3-da29-402d-becf-9fa2c38e69b8' subnet_pool_addr_scope6 = 'b7834aeb-2aa2-40ac-a8b5-2cded713cb58' _scopes = { constants.IP_VERSION_4: subnet_pool_addr_scope4, constants.IP_VERSION_6: subnet_pool_addr_scope6, } # Configure ipv4 scope to be allowed self.nb_bgp_driver.allowed_address_scopes = [subnet_pool_addr_scope4] # Check if ipv4 address with correct scope matches self.assertTrue(self.nb_bgp_driver._address_scope_allowed(self.ipv4, _scopes)) def test__address_scope_allowed_not_configured(self): # Check not configured (should always return True) self.assertTrue(self.nb_bgp_driver._address_scope_allowed(self.ipv4, {})) def test__address_scope_allowed_no_match(self): subnet_pool_addr_scope4 = '88e8aec3-da29-402d-becf-9fa2c38e69b8' subnet_pool_addr_scope6 = 'b7834aeb-2aa2-40ac-a8b5-2cded713cb58' _scopes = { constants.IP_VERSION_4: subnet_pool_addr_scope4, constants.IP_VERSION_6: subnet_pool_addr_scope6, } self.nb_bgp_driver.allowed_address_scopes = [subnet_pool_addr_scope4] # Make sure ipv6 address with scope not in list fails self.assertFalse(self.nb_bgp_driver._address_scope_allowed(self.ipv6, _scopes)) # Check IPv4 address without scope given, should fail self.assertFalse(self.nb_bgp_driver._address_scope_allowed(self.ipv4, {})) def test_expose_ovn_lb_vip_tenant(self): self.nb_bgp_driver.ovn_local_lrps = {'net1': ['ip1']} lb = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_PORT_EXT_ID_KEY: 'vip_port', constants.OVN_LB_VIP_IP_EXT_ID_KEY: 'vip'}, vips={'vip': 'member', 'fip': 'member'}) vip_lsp = utils.create_row( external_ids={ constants.OVN_LS_NAME_EXT_ID_KEY: 'net1' }) self.nb_idl.lsp_get.return_value.execute.return_value = vip_lsp mock_expose_remote_ip = mock.patch.object( self.nb_bgp_driver, '_expose_remote_ip').start() mock_expose_provider_port = mock.patch.object( self.nb_bgp_driver, '_expose_provider_port').start() self.nb_bgp_driver.expose_ovn_lb_vip(lb) mock_expose_remote_ip.assert_called_once_with( ['vip'], {'logical_switch': 'router1'} ) mock_expose_provider_port.assert_not_called() def test_expose_ovn_lb_vip_provider(self): self.nb_bgp_driver.ovn_local_lrps = {'net1': ['ip1']} lb = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_PORT_EXT_ID_KEY: 'vip_port', constants.OVN_LB_VIP_IP_EXT_ID_KEY: 'vip'}, vips={'vip': 'member', 'fip': 'member'}) vip_lsp = utils.create_row( external_ids={ constants.OVN_LS_NAME_EXT_ID_KEY: 'net2' }) self.nb_idl.lsp_get.return_value.execute.return_value = vip_lsp mock_expose_remote_ip = mock.patch.object( self.nb_bgp_driver, '_expose_remote_ip').start() mock_expose_provider_port = mock.patch.object( self.nb_bgp_driver, '_expose_provider_port').start() mock_get_ls_localnet_info = mock.patch.object( self.nb_bgp_driver, '_get_ls_localnet_info').start() mock_get_ls_localnet_info.return_value = (None, None, None) self.nb_bgp_driver.expose_ovn_lb_vip(lb) mock_expose_remote_ip.assert_not_called() mock_get_ls_localnet_info.assert_called_once_with('net2') mock_expose_provider_port.assert_called_once_with( ['vip'], None, 'net2', mock.ANY, mock.ANY, mock.ANY) def test_expose_ovn_lb_vip_no_vip(self): self.nb_bgp_driver.ovn_local_lrps = {'net1': ['ip1']} lb = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_PORT_EXT_ID_KEY: 'vip_port', constants.OVN_LB_VIP_IP_EXT_ID_KEY: 'vip'}, vips={'vip': 'member', 'fip': 'member'}) self.nb_idl.lsp_get.return_value.execute.return_value = None mock_expose_remote_ip = mock.patch.object( self.nb_bgp_driver, '_expose_remote_ip').start() mock_expose_provider_port = mock.patch.object( self.nb_bgp_driver, '_expose_provider_port').start() self.nb_bgp_driver.expose_ovn_lb_vip(lb) mock_expose_remote_ip.assert_not_called() mock_expose_provider_port.assert_not_called() def test_withdraw_ovn_lb_vip_tenant(self): lb = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_PORT_EXT_ID_KEY: 'vip_port', constants.OVN_LB_VIP_IP_EXT_ID_KEY: 'vip'}, vips={'vip': 'member', 'fip': 'member'}) mock_withdraw_remote_ip = mock.patch.object( self.nb_bgp_driver, '_withdraw_remote_ip').start() mock_withdraw_provider_port = mock.patch.object( self.nb_bgp_driver, '_withdraw_provider_port').start() self.nb_bgp_driver.withdraw_ovn_lb_vip(lb) mock_withdraw_provider_port.assert_not_called() mock_withdraw_remote_ip.assert_called_once_with( ['vip'], {'logical_switch': 'router1'}) def test_withdraw_ovn_lb_vip_provider(self): self.nb_bgp_driver._exposed_ips = { 'provider-ls': {'vip': {'bridge_device': self.bridge, 'bridge_vlan': None}}} lb = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_PORT_EXT_ID_KEY: 'vip_port', constants.OVN_LB_VIP_IP_EXT_ID_KEY: 'vip'}, vips={'vip': 'member', 'fip': 'member'}) mock_withdraw_remote_ip = mock.patch.object( self.nb_bgp_driver, '_withdraw_remote_ip').start() mock_withdraw_provider_port = mock.patch.object( self.nb_bgp_driver, '_withdraw_provider_port').start() self.nb_bgp_driver.withdraw_ovn_lb_vip(lb) mock_withdraw_provider_port.assert_called_once_with( ['vip'], self.router1_info['provider_switch'], self.router1_info['bridge_device'], self.router1_info['bridge_vlan']) mock_withdraw_remote_ip.assert_not_called() def test_withdraw_ovn_lb_vip_no_router(self): lb = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router2', constants.OVN_LB_VIP_PORT_EXT_ID_KEY: 'vip_port', constants.OVN_LB_VIP_IP_EXT_ID_KEY: 'vip'}, vips={'vip': 'member', 'fip': 'member'}) mock_withdraw_remote_ip = mock.patch.object( self.nb_bgp_driver, '_withdraw_remote_ip').start() mock_withdraw_provider_port = mock.patch.object( self.nb_bgp_driver, '_withdraw_provider_port').start() self.nb_bgp_driver.withdraw_ovn_lb_vip(lb) mock_withdraw_remote_ip.assert_not_called() mock_withdraw_provider_port.assert_not_called() def test_expose_ovn_pf_lb_fip(self): lb = utils.create_row( external_ids={ constants.OVN_LR_NAME_EXT_ID_KEY: 'neutron-router1'}, name='pf-floatingip-uuid-tcp', vips={'fip:port': 'member:port'}) mock_get_ls_localnet_info = mock.patch.object( self.nb_bgp_driver, '_get_ls_localnet_info').start() mock_get_ls_localnet_info.return_value = ('provider-ls', 'br-ex', 100) mock_expose_provider_port = mock.patch.object( self.nb_bgp_driver, '_expose_provider_port').start() self.nb_bgp_driver.expose_ovn_pf_lb_fip(lb) kwargs = { 'port_ips': ['fip'], 'mac': None, 'logical_switch': 'provider-ls', 'bridge_device': 'br-ex', 'bridge_vlan': 100, 'localnet': 'provider-ls'} mock_expose_provider_port.assert_called_once_with(**kwargs) def test_expose_ovn_pf_lb_fip_no_router(self): lb = utils.create_row( external_ids={}, name='pf-floatingip-uuid-tcp', vips={'fip:port': 'member:port'}) mock_expose_provider_port = mock.patch.object( self.nb_bgp_driver, '_expose_provider_port').start() self.nb_bgp_driver.expose_ovn_pf_lb_fip(lb) mock_expose_provider_port.assert_not_called() def test_expose_ovn_pf_lb_fip_no_router_cr_lrp(self): lb = utils.create_row( external_ids={ constants.OVN_LR_NAME_EXT_ID_KEY: 'neutron-router2'}, name='pf-floatingip-uuid-tcp', vips={'fip:port': 'member:port'}) mock_expose_provider_port = mock.patch.object( self.nb_bgp_driver, '_expose_provider_port').start() self.nb_bgp_driver.expose_ovn_pf_lb_fip(lb) mock_expose_provider_port.assert_not_called() def test_expose_ovn_lb_fip(self): lb = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_PORT_EXT_ID_KEY: 'vip_port', constants.OVN_LB_VIP_IP_EXT_ID_KEY: 'vip'}, vips={'vip': 'member', 'fip': 'member'}) vip_lsp = utils.create_row( name='vip-port-name', external_ids={ constants.OVN_LS_NAME_EXT_ID_KEY: 'net2' }) self.nb_idl.lsp_get.return_value.execute.return_value = vip_lsp mock_get_port_external_ip_and_ls = mock.patch.object( self.nb_bgp_driver, 'get_port_external_ip_and_ls').start() mock_get_port_external_ip_and_ls.return_value = ('fip', 'fip-mac', 'provider-ls') mock_expose_fip = mock.patch.object( self.nb_bgp_driver, '_expose_fip').start() self.nb_bgp_driver.expose_ovn_lb_fip(lb) mock_expose_fip.assert_called_once_with( 'fip', 'fip-mac', 'provider-ls', vip_lsp) def test_expose_ovn_lb_fip_no_vip_port(self): lb = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_PORT_EXT_ID_KEY: 'vip_port', constants.OVN_LB_VIP_IP_EXT_ID_KEY: 'vip'}, vips={'vip': 'member', 'fip': 'member'}) self.nb_idl.lsp_get.return_value.execute.return_value = None mock_expose_fip = mock.patch.object( self.nb_bgp_driver, '_expose_fip').start() self.nb_bgp_driver.expose_ovn_lb_fip(lb) mock_expose_fip.assert_not_called() def test_expose_ovn_lb_fip_no_external_ip(self): lb = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_PORT_EXT_ID_KEY: 'vip_port', constants.OVN_LB_VIP_IP_EXT_ID_KEY: 'vip'}, vips={'vip': 'member', 'fip': 'member'}) vip_lsp = utils.create_row( name='vip-port-name', external_ids={ constants.OVN_LS_NAME_EXT_ID_KEY: 'net2' }) self.nb_idl.lsp_get.return_value.execute.return_value = vip_lsp mock_get_port_external_ip_and_ls = mock.patch.object( self.nb_bgp_driver, 'get_port_external_ip_and_ls').start() mock_get_port_external_ip_and_ls.return_value = (None, None, None) mock_expose_fip = mock.patch.object( self.nb_bgp_driver, '_expose_fip').start() self.nb_bgp_driver.expose_ovn_lb_fip(lb) mock_expose_fip.assert_not_called() def test_withdraw_ovn_lb_fip(self): lb = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_PORT_EXT_ID_KEY: 'vip_port', constants.OVN_LB_VIP_IP_EXT_ID_KEY: 'vip', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: 'vip-fip'}, vips={'vip': 'member', 'fip': 'member'}) mock_withdraw_provider_port = mock.patch.object( self.nb_bgp_driver, '_withdraw_provider_port').start() self.nb_bgp_driver.withdraw_ovn_lb_fip(lb) mock_withdraw_provider_port.assert_called_once_with( ['vip-fip'], self.router1_info['provider_switch'], self.router1_info['bridge_device'], self.router1_info['bridge_vlan']) def test_withdraw_ovn_lb_fip_no_vip_router(self): lb = utils.create_row( external_ids={ constants.OVN_LB_VIP_PORT_EXT_ID_KEY: 'vip_port', constants.OVN_LB_VIP_IP_EXT_ID_KEY: 'vip', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: 'vip-fip'}, vips={'vip': 'member', 'fip': 'member'}) mock_withdraw_provider_port = mock.patch.object( self.nb_bgp_driver, '_withdraw_provider_port').start() self.nb_bgp_driver.withdraw_ovn_lb_fip(lb) mock_withdraw_provider_port.assert_not_called() def test_withdraw_ovn_lb_fip_no_cr_lrp(self): lb = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router2', constants.OVN_LB_VIP_PORT_EXT_ID_KEY: 'vip_port', constants.OVN_LB_VIP_IP_EXT_ID_KEY: 'vip', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: 'vip-fip'}, vips={'vip': 'member', 'fip': 'member'}) mock_withdraw_provider_port = mock.patch.object( self.nb_bgp_driver, '_withdraw_provider_port').start() self.nb_bgp_driver.withdraw_ovn_lb_fip(lb) mock_withdraw_provider_port.assert_not_called() def test_withdraw_ovn_pf_lb_fip(self): lb = utils.create_row( external_ids={ constants.OVN_LR_NAME_EXT_ID_KEY: 'neutron-router1'}, name='pf-floatingip-uuid-tcp', vips={'fip:port': 'member:port'}) mock_get_ls_localnet_info = mock.patch.object( self.nb_bgp_driver, '_get_ls_localnet_info').start() mock_get_ls_localnet_info.return_value = ('provider-ls', 'br-ex', 100) mock_withdraw_provider_port = mock.patch.object( self.nb_bgp_driver, '_withdraw_provider_port').start() self.nb_bgp_driver.withdraw_ovn_pf_lb_fip(lb) kwargs = { 'port_ips': ['fip'], 'logical_switch': 'provider-ls', 'bridge_device': 'br-ex', 'bridge_vlan': 100} mock_withdraw_provider_port.assert_called_once_with(**kwargs) def test_withdraw_ovn_pf_lb_fip_no_router(self): lb = utils.create_row( external_ids={}, name='pf-floatingip-uuid-tcp', vips={'fip:port': 'member:port'}) mock_withdraw_provider_port = mock.patch.object( self.nb_bgp_driver, '_withdraw_provider_port').start() self.nb_bgp_driver.withdraw_ovn_pf_lb_fip(lb) mock_withdraw_provider_port.assert_not_called() def test_withdraw_ovn_pf_lb_fip_no_cr_lrp(self): lb = utils.create_row( external_ids={ constants.OVN_LR_NAME_EXT_ID_KEY: 'neutron-router2'}, name='pf-floatingip-uuid-tcp', vips={'fip:port': 'member:port'}) mock_withdraw_provider_port = mock.patch.object( self.nb_bgp_driver, '_withdraw_provider_port').start() self.nb_bgp_driver.withdraw_ovn_pf_lb_fip(lb) mock_withdraw_provider_port.assert_not_called() ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/test_ovn_bgp_driver.py000066400000000000000000003154541460327367600312240ustar00rootroot00000000000000# Copyright 2022 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_config import cfg from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack import ovn_bgp_driver from ovn_bgp_agent.drivers.openstack.utils import bgp as bgp_utils 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 ovn from ovn_bgp_agent.drivers.openstack.utils import ovs from ovn_bgp_agent.drivers.openstack.utils import wire as wire_utils from ovn_bgp_agent import exceptions as agent_exc from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.tests.unit import fakes from ovn_bgp_agent.utils import linux_net CONF = cfg.CONF class TestOVNBGPDriver(test_base.TestCase): def setUp(self): super(TestOVNBGPDriver, self).setUp() CONF.set_override('expose_tenant_networks', True) self.bridge = 'fake-bridge' self.bgp_driver = ovn_bgp_driver.OVNBGPDriver() self.bgp_driver._post_fork_event = mock.Mock() self.bgp_driver.sb_idl = mock.Mock() self.sb_idl = self.bgp_driver.sb_idl self.bgp_driver.chassis = 'fake-chassis' self.bgp_driver.ovn_routing_tables = {self.bridge: 'fake-table'} self.bgp_driver.ovn_bridge_mappings = {'fake-network': self.bridge} self.mock_sbdb = mock.patch.object(ovn, 'OvnSbIdl').start() self.mock_ovs_idl = mock.patch.object(ovs, 'OvsIdl').start() self.ipv4 = '192.168.1.17' self.ipv6 = '2002::1234:abcd:ffff:c0a8:101' self.fip = '172.24.4.33' self.mac = 'aa:bb:cc:dd:ee:ff' self.loadbalancer_vip_port = 'fake-lb-vip-port' self.bgp_driver.ovs_idl = self.mock_ovs_idl self.cr_lrp0 = 'cr-fake-logical-port' self.cr_lrp1 = 'cr-fake-logical-port1' self.lrp0 = 'lrp-fake-logical-port' self.bgp_driver.ovn_local_cr_lrps = { self.cr_lrp0: {'provider_datapath': 'fake-provider-dp', 'router_datapath': 'fake-router-dp', 'ips': [self.fip], 'subnets_datapath': {self.lrp0: 'fake-lrp-dp'}, 'subnets_cidr': ['192.168.1.1/24'], 'provider_ovn_lbs': [], 'bridge_device': self.bridge, 'bridge_vlan': None}, self.cr_lrp1: {'provider_datapath': 'fake-provider-dp2'}} self.bgp_driver.provider_ovn_lbs = { self.loadbalancer_vip_port: {'ips': [self.ipv4, self.ipv6], 'gateway_port': self.cr_lrp0}} @mock.patch.object(linux_net, 'ensure_ovn_device') @mock.patch.object(linux_net, 'ensure_vrf') @mock.patch.object(frr, 'vrf_leak') def test_start(self, mock_vrf, *args): self.bgp_driver.start() mock_vrf.assert_called_once_with( CONF.bgp_vrf, CONF.bgp_AS, CONF.bgp_router_id, template=frr.LEAK_VRF_TEMPLATE) # Assert connections were started self.mock_ovs_idl().start.assert_called_once_with( CONF.ovsdb_connection) self.mock_sbdb().start.assert_called_once_with() @mock.patch.object(linux_net, 'ensure_ovn_device') @mock.patch.object(frr, 'vrf_leak') @mock.patch.object(linux_net, 'ensure_vrf') def test_frr_sync(self, mock_ensure_vrf, mock_vrf_leak, mock_ensure_ovn_dev): self.bgp_driver.frr_sync() mock_ensure_vrf.assert_called_once_with( CONF.bgp_vrf, CONF.bgp_vrf_table_id) mock_vrf_leak.assert_called_once_with( CONF.bgp_vrf, CONF.bgp_AS, CONF.bgp_router_id, template=frr.LEAK_VRF_TEMPLATE) mock_ensure_ovn_dev.assert_called_once_with( CONF.bgp_nic, CONF.bgp_vrf) @mock.patch.object(wire_utils, 'delete_vlan_devices_leftovers') @mock.patch.object(linux_net, 'delete_bridge_ip_routes') @mock.patch.object(linux_net, 'delete_ip_rules') @mock.patch.object(linux_net, 'delete_exposed_ips') @mock.patch.object(ovs, 'remove_extra_ovs_flows') @mock.patch.object(ovs, 'ensure_mac_tweak_flows') @mock.patch.object(ovs, 'get_ovs_patch_ports_info') @mock.patch.object(linux_net, 'get_ovn_ip_rules') @mock.patch.object(linux_net, 'get_exposed_ips') @mock.patch.object(linux_net, 'get_interface_address') @mock.patch.object(linux_net, 'ensure_vlan_device_for_network') @mock.patch.object(linux_net, 'ensure_routing_table_for_bridge') @mock.patch.object(linux_net, 'ensure_arp_ndp_enabled_for_bridge') def test_sync( self, mock_ensure_arp, mock_routing_bridge, mock_ensure_vlan_network, mock_nic_address, mock_exposed_ips, mock_get_ip_rules, mock_get_patch_ports, mock_ensure_mac, mock_remove_flows, mock_del_exposed_ips, mock_del_ip_rules, mock_del_ip_routes, mock_vlan_leftovers): self.mock_ovs_idl.get_ovn_bridge_mappings.return_value = [ 'net0:bridge0', 'net1:bridge1'] self.sb_idl.get_network_vlan_tag_by_network_name.side_effect = ( [10], [11]) fake_ip_rules = 'fake-ip-rules' mock_get_ip_rules.return_value = fake_ip_rules ips = [self.ipv4, self.ipv6] mock_exposed_ips.return_value = ips self.sb_idl.get_ports_on_chassis.return_value = [ 'fake-port0', 'fake-port1'] self.sb_idl.get_cr_lrp_ports_on_chassis.return_value = [ 'fake-cr-port0', 'fake-cr-port1'] mock_ensure_port_exposed = mock.patch.object( self.bgp_driver, '_ensure_port_exposed').start() mock_ensure_cr_port_exposed = mock.patch.object( self.bgp_driver, '_ensure_cr_lrp_associated_ports_exposed').start() mock_routing_bridge.return_value = ['fake-route'] mock_nic_address.return_value = self.mac mock_get_patch_ports.return_value = [1, 2] self.bgp_driver.sync() expected_calls = [mock.call('bridge0', 1, [10]), mock.call('bridge1', 2, [11])] mock_ensure_arp.assert_has_calls(expected_calls) expected_calls = [mock.call({}, 'bridge0', CONF.bgp_vrf_table_id), mock.call({}, 'bridge1', CONF.bgp_vrf_table_id)] mock_routing_bridge.assert_has_calls(expected_calls) expected_calls = [mock.call('bridge0', 10), mock.call('bridge1', 11)] mock_ensure_vlan_network.assert_has_calls(expected_calls) expected_calls = [ mock.call('bridge0'), mock.call('bridge1')] mock_get_patch_ports.assert_has_calls(expected_calls) expected_calls = [ mock.call('bridge0', mock.ANY, [1, 2], constants.OVS_RULE_COOKIE), mock.call('bridge1', mock.ANY, [1, 2], constants.OVS_RULE_COOKIE)] mock_ensure_mac.assert_has_calls(expected_calls) expected_calls = [ mock.call(mock.ANY, 'bridge0', constants.OVS_RULE_COOKIE), mock.call(mock.ANY, 'bridge1', constants.OVS_RULE_COOKIE)] mock_remove_flows.assert_has_calls(expected_calls) expected_calls = [mock.call('fake-port0', ips, fake_ip_rules), mock.call('fake-port1', ips, fake_ip_rules)] mock_ensure_port_exposed.assert_has_calls(expected_calls) expected_calls = [mock.call('fake-cr-port0', ips, fake_ip_rules), mock.call('fake-cr-port1', ips, fake_ip_rules)] mock_ensure_cr_port_exposed.assert_has_calls(expected_calls) mock_del_exposed_ips.assert_called_once_with( ips, CONF.bgp_nic) mock_del_ip_rules.assert_called_once_with(fake_ip_rules) mock_del_ip_routes.assert_called_once_with( {}, mock.ANY, {'bridge0': ['fake-route'], 'bridge1': ['fake-route']}) mock_get_ip_rules.assert_called_once_with(mock.ANY) mock_vlan_leftovers.assert_called_once_with( self.sb_idl, self.bgp_driver.ovn_bridge_mappings) @mock.patch.object(linux_net, 'get_ip_version') def test__ensure_cr_lrp_associated_ports_exposed(self, mock_ip_version): mock_expose_ip = mock.patch.object( self.bgp_driver, '_expose_ip').start() mock_ip_version.side_effect = (constants.IP_VERSION_4, constants.IP_VERSION_6) patch_port_row = fakes.create_object({'name': 'patch-port'}) self.sb_idl.get_cr_lrp_nat_addresses_info.return_value = ( [self.ipv4, self.ipv6], patch_port_row) exposed_ips = [self.ipv4, '192.168.1.20'] mock_expose_ip.return_value = exposed_ips ip_rules = {"{}/128".format(self.ipv6): 'fake-rules'} self.bgp_driver._ensure_cr_lrp_associated_ports_exposed( 'fake-cr-lrp', exposed_ips, ip_rules) mock_expose_ip.assert_called_once_with( [self.ipv4, self.ipv6], patch_port_row, associated_port='fake-cr-lrp') self.assertEqual(['192.168.1.20'], exposed_ips) def test__ensure_port_exposed(self): mock_expose_ip = mock.patch.object( self.bgp_driver, '_expose_ip').start() mock_expose_ip.return_value = [self.ipv4, self.ipv6] port = fakes.create_object({ 'name': 'fake-port', 'type': '', 'mac': ['{} {} {}'.format(self.mac, self.ipv4, self.ipv6)]}) exposed_ips = [self.ipv4, self.ipv6] ip_rules = {"{}/128".format(self.ipv6): 'fake-rules'} self.bgp_driver._ensure_port_exposed(port, exposed_ips, ip_rules) mock_expose_ip.assert_called_once_with( [self.ipv4, self.ipv6], port) self.assertEqual([], exposed_ips) self.assertEqual({}, ip_rules) def test__ensure_port_exposed_fip(self): fip = '172.24.4.225' mock_expose_ip = mock.patch.object( self.bgp_driver, '_expose_ip').start() mock_expose_ip.return_value = [fip] port = fakes.create_object({ 'name': 'fake-port', 'type': '', 'mac': ['{} {} {}'.format(self.mac, self.ipv4, self.ipv6)]}) exposed_ips = [self.ipv4, fip] ip_rules = {"{}/128".format(self.ipv6): 'fake-rules'} self.bgp_driver._ensure_port_exposed(port, exposed_ips, ip_rules) mock_expose_ip.assert_called_once_with( [self.ipv4, self.ipv6], port) self.assertEqual([self.ipv4], exposed_ips) self.assertEqual({"{}/128".format(self.ipv6): 'fake-rules'}, ip_rules) def test__ensure_port_exposed_fip_unknown_mac(self): fip = '172.24.4.225' mock_expose_ip = mock.patch.object( self.bgp_driver, '_expose_ip').start() mock_expose_ip.return_value = [fip] port = fakes.create_object({ 'name': 'fake-port', 'type': '', 'mac': ['unknown'], 'datapath': 'fake-dp'}) exposed_ips = [self.ipv4, fip] ip_rules = {"{}/128".format(self.ipv6): 'fake-rules'} self.sb_idl.is_provider_network.return_value = False self.bgp_driver._ensure_port_exposed(port, exposed_ips, ip_rules) mock_expose_ip.assert_called_once_with([], port) self.assertEqual([self.ipv4], exposed_ips) self.assertEqual({"{}/128".format(self.ipv6): 'fake-rules'}, ip_rules) def test__ensure_port_exposed_wrong_port_type(self): mock_expose_ip = mock.patch.object( self.bgp_driver, '_expose_ip').start() port = fakes.create_object({ 'name': 'fake-port', 'type': 'non-existing-type', 'mac': ['{} {} {}'.format(self.mac, self.ipv4, self.ipv6)]}) self.bgp_driver._ensure_port_exposed(port, [], {}) # Assert it was never called, the method should just return if # the port type is not OVN_VIF_PORT_TYPES mock_expose_ip.assert_not_called() @mock.patch.object(wire_utils, '_ensure_updated_mac_tweak_flows') @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') def test__expose_provider_port(self, mock_add_rule, mock_add_route, mock_add_ips_dev, mock_ensure_mac_tweak): port_ips = [self.ipv4] provider_datapath = 'fake-provider-dp' mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) self.bgp_driver._expose_provider_port(port_ips, provider_datapath) mock_ensure_mac_tweak.assert_called_once_with(mock.ANY, self.bridge, {}) mock_add_ips_dev.assert_called_once_with( CONF.bgp_nic, [self.ipv4]) mock_add_rule.assert_called_once_with( self.ipv4, 'fake-table') mock_add_route.assert_called_once_with( mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=10) @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') def test__expose_provider_port_no_device(self, mock_add_rule, mock_add_route, mock_add_ips_dev): port_ips = [self.ipv4] provider_datapath = 'fake-provider-dp' mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (None, None) ret = self.bgp_driver._expose_provider_port(port_ips, provider_datapath) self.assertEqual(False, ret) mock_add_ips_dev.assert_not_called() mock_add_rule.assert_not_called() mock_add_route.assert_not_called() @mock.patch.object(wire_utils, '_ensure_updated_mac_tweak_flows') @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') def test__expose_provider_port_invalid_ip( self, mock_add_rule, mock_add_route, mock_add_ips_dev, mock_ensure_mac_tweak): port_ips = [self.ipv4] provider_datapath = 'fake-provider-dp' mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) mock_add_rule.side_effect = agent_exc.InvalidPortIP(ip=self.ipv4) self.sb_idl.get_localnet_for_datapath.return_value = 'fake-localnet' ret = self.bgp_driver._expose_provider_port(port_ips, provider_datapath) self.assertEqual(False, ret) mock_add_ips_dev.assert_not_called() mock_add_rule.assert_called_once_with( self.ipv4, 'fake-table') mock_add_route.assert_not_called() mock_ensure_mac_tweak.assert_not_called() @mock.patch.object(wire_utils, '_ensure_updated_mac_tweak_flows') @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') def test__expose_provider_port_with_lladdr( self, mock_add_rule, mock_add_route, mock_add_ips_dev, mock_ensure_mac_tweak): port_ips = [self.ipv4] provider_datapath = 'fake-provider-dp' mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) self.bgp_driver._expose_provider_port(port_ips, provider_datapath, lladdr='fake-mac') mock_ensure_mac_tweak.assert_called_once_with(mock.ANY, self.bridge, {}) mock_add_ips_dev.assert_called_once_with( CONF.bgp_nic, [self.ipv4]) mock_add_rule.assert_called_once_with( self.ipv4, 'fake-table', dev='{}.{}'.format(self.bridge, 10), lladdr='fake-mac') mock_add_route.assert_called_once_with( mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=10) @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'get_ip_version') def test__expose_tenant_port(self, mock_ip_version, mock_add_ips_dev): tenant_port = fakes.create_object({ 'name': 'fake-port', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': ['aa:bb:cc:dd:ee:ee 192.168.1.10 192.168.1.11'], 'chassis': 'fake-chassis1', 'external_ids': {}}) ip_version = constants.IP_VERSION_4 mock_ip_version.return_value = constants.IP_VERSION_4 self.bgp_driver._expose_tenant_port(tenant_port, ip_version) expected_calls = [mock.call(CONF.bgp_nic, ['192.168.1.10']), mock.call(CONF.bgp_nic, ['192.168.1.11'])] mock_add_ips_dev.assert_has_calls(expected_calls) @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'get_ip_version') def test__expose_tenant_port_ovn_lb(self, mock_ip_version, mock_add_ips_dev): tenant_port = fakes.create_object({ 'name': 'fake-port', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': [], 'chassis': '', 'external_ids': {'neutron:cidrs': '192.168.1.10/24'}, 'up': [False]}) ip_version = constants.IP_VERSION_4 mock_ip_version.return_value = constants.IP_VERSION_4 self.bgp_driver._expose_tenant_port(tenant_port, ip_version) mock_add_ips_dev.assert_called_once_with(CONF.bgp_nic, ['192.168.1.10']) @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'get_ip_version') def test__expose_tenant_port_no_ip(self, mock_ip_version, mock_add_ips_dev): tenant_port = fakes.create_object({ 'name': 'fake-port', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': ['aa:bb:cc:dd:ee:ee'], 'chassis': 'fake-chassis1', 'external_ids': {}}) ip_version = constants.IP_VERSION_4 mock_ip_version.return_value = constants.IP_VERSION_4 self.bgp_driver._expose_tenant_port(tenant_port, ip_version) mock_ip_version.assert_not_called() mock_add_ips_dev.assert_not_called() @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'get_ip_version') def test__expose_tenant_port_no_mac(self, mock_ip_version, mock_add_ips_dev): tenant_port = fakes.create_object({ 'name': 'fake-port', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': [], 'chassis': 'fake-chassis1', 'external_ids': {}}) ip_version = constants.IP_VERSION_4 mock_ip_version.return_value = constants.IP_VERSION_4 self.bgp_driver._expose_tenant_port(tenant_port, ip_version) mock_ip_version.assert_not_called() mock_add_ips_dev.assert_not_called() @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'get_ip_version') def test__expose_tenant_port_unknown_mac(self, mock_ip_version, mock_add_ips_dev): tenant_port = fakes.create_object({ 'name': 'fake-port', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': ['unknown'], 'chassis': 'fake-chassis1', 'external_ids': {'neutron:cidrs': '192.168.1.10/24'}, 'up': [False]}) ip_version = constants.IP_VERSION_4 mock_ip_version.return_value = constants.IP_VERSION_4 self.bgp_driver._expose_tenant_port(tenant_port, ip_version) mock_add_ips_dev.assert_called_once_with(CONF.bgp_nic, ['192.168.1.10']) @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'get_ip_version') def test__expose_tenant_port_wrong_type(self, mock_ip_version, mock_add_ips_dev): tenant_port = fakes.create_object({ 'name': 'fake-port', 'type': constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, 'mac': ['aa:bb:cc:dd:ee:ee 192.168.1.10 192.168.1.11'], 'chassis': 'fake-chassis1', 'external_ids': {}}) ip_version = constants.IP_VERSION_4 mock_ip_version.return_value = constants.IP_VERSION_4 self.bgp_driver._expose_tenant_port(tenant_port, ip_version) mock_ip_version.assert_not_called() mock_add_ips_dev.assert_not_called() @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'get_ip_version') def test__expose_tenant_port_no_chassis(self, mock_ip_version, mock_add_ips_dev): tenant_port = fakes.create_object({ 'name': 'fake-port', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': ['aa:bb:cc:dd:ee:ee 192.168.1.10 192.168.1.11'], 'chassis': '', 'external_ids': {}}) ip_version = constants.IP_VERSION_4 mock_ip_version.return_value = constants.IP_VERSION_4 self.bgp_driver._expose_tenant_port(tenant_port, ip_version) mock_ip_version.assert_not_called() mock_add_ips_dev.assert_not_called() @mock.patch.object(linux_net, 'del_ips_from_dev') @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') def test__withdraw_provider_port(self, mock_del_rule, mock_del_route, mock_del_ips_dev): port_ips = [self.ipv4] provider_datapath = 'fake-provider-dp' mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) self.bgp_driver._withdraw_provider_port(port_ips, provider_datapath) mock_del_ips_dev.assert_called_once_with( CONF.bgp_nic, [self.ipv4]) mock_del_rule.assert_called_once_with( self.ipv4, 'fake-table') mock_del_route.assert_called_once_with( mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=10) @mock.patch.object(linux_net, 'del_ips_from_dev') @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') def test__withdraw_provider_port_no_device(self, mock_del_rule, mock_del_route, mock_del_ips_dev): port_ips = [self.ipv4] provider_datapath = 'fake-provider-dp' mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (None, None) ret = self.bgp_driver._withdraw_provider_port(port_ips, provider_datapath) self.assertEqual(False, ret) mock_del_ips_dev.assert_called_once_with( CONF.bgp_nic, [self.ipv4]) mock_del_rule.assert_not_called() mock_del_route.assert_not_called() @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(linux_net, 'del_ips_from_dev') @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') def test__withdraw_provider_port_lladdr( self, mock_del_rule, mock_del_route, mock_del_ips_dev, mock_ip_version): port_ips = [self.ipv4] provider_datapath = 'fake-provider-dp' mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) dev = '{}.{}'.format(self.bridge, 10) mock_ip_version.return_value = constants.IP_VERSION_4 self.bgp_driver._withdraw_provider_port(port_ips, provider_datapath, lladdr='fake-mac') mock_del_ips_dev.assert_called_once_with( CONF.bgp_nic, [self.ipv4]) mock_del_rule.assert_called_once_with( '{}/32'.format(self.ipv4), 'fake-table', dev=dev, lladdr='fake-mac') mock_del_route.assert_called_once_with( mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=10) @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(linux_net, 'del_ips_from_dev') @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') def test__withdraw_provider_port_lladdr_ipv6( self, mock_del_rule, mock_del_route, mock_del_ips_dev, mock_ip_version): port_ips = [self.ipv6] provider_datapath = 'fake-provider-dp' mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) dev = '{}.{}'.format(self.bridge, 10) mock_ip_version.return_value = constants.IP_VERSION_6 self.bgp_driver._withdraw_provider_port(port_ips, provider_datapath, lladdr='fake-mac') mock_del_ips_dev.assert_called_once_with( CONF.bgp_nic, [self.ipv6]) mock_del_rule.assert_called_once_with( '{}/128'.format(self.ipv6), 'fake-table', dev=dev, lladdr='fake-mac') mock_del_route.assert_called_once_with( mock.ANY, self.ipv6, 'fake-table', self.bridge, vlan=10) @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'get_ip_version') def test__process_lrp_port(self, mock_ip_version, mock_add_rule, mock_add_route, mock_add_ips_dev): mock_ip_version.return_value = constants.IP_VERSION_4 gateway = {} gateway['ips'] = ['{}/32'.format(self.fip), '2003::1234:abcd:ffff:c0a8:102/128'] gateway['provider_datapath'] = 'bc6780f4-9510-4270-b4d2-b8d5c6802713' gateway['subnets_datapath'] = {} gateway['subnets_cidr'] = [] gateway['bridge_device'] = self.bridge gateway['bridge_vlan'] = 10 self.bgp_driver.ovn_local_cr_lrps = {'gateway_port': gateway} router_port = fakes.create_object({ 'chassis': [], 'mac': ['{} {}/32'.format(self.mac, self.ipv4)], 'logical_port': 'lrp-fake-logical-port', 'options': {'peer': 'fake-peer'}}) mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) dp_port0 = fakes.create_object({ 'name': 'fake-port-dp0', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': ['aa:bb:cc:dd:ee:ee 192.168.1.10 192.168.1.11'], 'chassis': 'fake-chassis1', 'external_ids': {}}) dp_port1 = fakes.create_object({ 'name': 'fake-port-dp1', 'type': 'fake-type', 'mac': ['aa:bb:cc:dd:ee:ee 192.168.1.12 192.168.1.13'], 'chassis': 'fake-chassis2', 'external_ids': {}}) dp_port2 = fakes.create_object({ 'name': 'fake-port-dp2', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': [], 'chassis': 'fake-chassis2', 'external_ids': {}}) dp_port3 = fakes.create_object({ 'name': 'fake-port-dp3', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': [], 'chassis': '', 'up': [False], 'external_ids': {}}) dp_port4 = fakes.create_object({ 'name': 'fake-port-dp4', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': [], 'chassis': '', 'up': [False], 'external_ids': { constants.OVN_CIDRS_EXT_ID_KEY: "192.168.1.13/24"}}) self.sb_idl.get_ports_on_datapath.return_value = [dp_port0, dp_port1, dp_port2, dp_port3, dp_port4] self.bgp_driver._process_lrp_port(router_port, 'gateway_port') # Assert that the add methods were called mock_add_rule.assert_called_once_with( '{}/32'.format(self.ipv4), 'fake-table') mock_add_route.assert_called_once_with( mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=10, mask='32', via=self.fip) expected_calls = [mock.call(CONF.bgp_nic, ['192.168.1.10']), mock.call(CONF.bgp_nic, ['192.168.1.11']), mock.call(CONF.bgp_nic, ['192.168.1.13'])] mock_add_ips_dev.assert_has_calls(expected_calls) @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(driver_utils, 'is_ipv6_gua') def test__process_lrp_port_gua(self, mock_ipv6_gua, mock_ip_version, mock_add_rule, mock_add_route, mock_add_ips_dev): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') mock_ipv6_gua.return_value = True mock_ip_version.return_value = constants.IP_VERSION_6 gateway = {} gateway['ips'] = ['{}/32'.format(self.fip), '2003::1234:abcd:ffff:c0a8:102/128'] gateway['provider_datapath'] = 'bc6780f4-9510-4270-b4d2-b8d5c6802713' gateway['subnets_datapath'] = {} gateway['subnets_cidr'] = [] gateway['bridge_device'] = self.bridge gateway['bridge_vlan'] = 10 self.bgp_driver.ovn_local_cr_lrps = {'gateway_port': gateway} router_port = fakes.create_object({ 'chassis': [], 'mac': ['{} {}/128'.format(self.mac, self.ipv6)], 'logical_port': 'lrp-fake-logical-port', 'options': {'peer': 'fake-peer'}}) mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) dp_port0 = fakes.create_object({ 'name': 'fake-port-dp0', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': ['aa:bb:cc:dd:ee:ee 2002::1234:abcd:ffff:c0a8:111'], 'chassis': 'fake-chassis1', 'external_ids': {}}) dp_port1 = fakes.create_object({ 'name': 'fake-port-dp1', 'type': 'fake-type', 'mac': ['aa:bb:cc:dd:ee:ee 2002::1234:abcd:ffff:c0a8:112'], 'chassis': 'fake-chassis2', 'external_ids': {}}) dp_port2 = fakes.create_object({ 'name': 'fake-port-dp2', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': [], 'chassis': 'fake-chassis2', 'external_ids': {}}) dp_port3 = fakes.create_object({ 'name': 'fake-port-dp3', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': [], 'chassis': '', 'up': [False], 'external_ids': {}}) dp_port4 = fakes.create_object({ 'name': 'fake-port-dp4', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': [], 'chassis': '', 'up': [False], 'external_ids': { constants.OVN_CIDRS_EXT_ID_KEY: "2002::1234:abcd:ffff:c0a8:121/64"}}) self.sb_idl.get_ports_on_datapath.return_value = [dp_port0, dp_port1, dp_port2, dp_port3, dp_port4] self.bgp_driver._process_lrp_port(router_port, 'gateway_port') # Assert that the add methods were called mock_ipv6_gua.assert_called_once_with('{}/128'.format(self.ipv6)) mock_add_rule.assert_called_once_with( '{}/128'.format(self.ipv6), 'fake-table') mock_add_route.assert_called_once_with( mock.ANY, self.ipv6, 'fake-table', self.bridge, vlan=10, mask='128', via=self.fip) expected_calls = [mock.call(CONF.bgp_nic, ['2002::1234:abcd:ffff:c0a8:111']), mock.call(CONF.bgp_nic, ['2002::1234:abcd:ffff:c0a8:121'])] mock_add_ips_dev.assert_has_calls(expected_calls) @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(driver_utils, 'is_ipv6_gua') def test__process_lrp_port_not_gua( self, mock_ipv6_gua, mock_add_rule, mock_add_route, mock_add_ips_dev): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') mock_ipv6_gua.return_value = False gateway = {} gateway['ips'] = ['{}/32'.format(self.fip), '2003::1234:abcd:ffff:c0a8:102/128'] gateway['provider_datapath'] = 'bc6780f4-9510-4270-b4d2-b8d5c6802713' gateway['subnets_datapath'] = {} gateway['subnets_cidr'] = [] gateway['bridge_device'] = self.bridge gateway['bridge_vlan'] = 10 self.bgp_driver.ovn_local_cr_lrps = {'gateway_port': gateway} router_port = fakes.create_object({ 'chassis': [], 'mac': ['{} fdab:4ad8:e8fb:0:f816:3eff:fec6:469c/128'.format( self.mac)], 'logical_port': 'lrp-fake-logical-port', 'options': {'peer': 'fake-peer'}}) mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) self.bgp_driver._process_lrp_port(router_port, 'gateway_port') # Assert that the add methods were called mock_ipv6_gua.assert_called_once_with( 'fdab:4ad8:e8fb:0:f816:3eff:fec6:469c/128') mock_add_rule.assert_not_called() mock_add_route.assert_not_called() mock_add_ips_dev.assert_not_called() @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'get_ip_version') def test__process_lrp_port_invalid_ip( self, mock_ip_version, mock_add_rule, mock_add_route): mock_ip_version.return_value = constants.IP_VERSION_4 gateway = {} gateway['ips'] = ['{}/32'.format(self.fip), '2003::1234:abcd:ffff:c0a8:102/128'] gateway['provider_datapath'] = 'bc6780f4-9510-4270-b4d2-b8d5c6802713' gateway['subnets_datapath'] = {} gateway['subnets_cidr'] = [] gateway['bridge_device'] = self.bridge gateway['bridge_vlan'] = 10 self.bgp_driver.ovn_local_cr_lrps = {'gateway_port': gateway} router_port = fakes.create_object({ 'chassis': [], 'mac': ['{} {}/32'.format(self.mac, self.ipv4)], 'logical_port': 'lrp-fake-logical-port', 'options': {'peer': 'fake-peer'}}) mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) # Raise an exception on add_ip_rule() mock_add_rule.side_effect = agent_exc.InvalidPortIP(ip=self.ipv4) self.bgp_driver._process_lrp_port(router_port, 'gateway_port') mock_add_rule.assert_called_once_with( '{}/32'.format(self.ipv4), 'fake-table') # Assert that add_ip_route() was not called mock_add_route.assert_not_called() @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') def test__process_lrp_port_address_scopes( self, mock_add_rule, mock_add_route, mock_add_ips_dev): gateway = {} gateway['ips'] = ['{}/32'.format(self.fip), '2003::1234:abcd:ffff:c0a8:102/128'] gateway['provider_datapath'] = 'bc6780f4-9510-4270-b4d2-b8d5c6802713' gateway['subnets_datapath'] = {} gateway['subnets_cidr'] = [] gateway['bridge_device'] = self.bridge gateway['bridge_vlan'] = 10 self.bgp_driver.ovn_local_cr_lrps = {'gateway_port': gateway} mock_address_scope_allowed = mock.patch.object( self.bgp_driver, '_address_scope_allowed').start() mock_address_scope_allowed.return_value = False router_port = fakes.create_object({ 'chassis': [], 'mac': ['{} {}/32'.format(self.mac, self.ipv4)], 'logical_port': 'lrp-fake-logical-port', 'options': {'peer': 'fake-peer'}}) self.bgp_driver._process_lrp_port(router_port, 'gateway_port') # Assert that the add methods were called mock_add_rule.assert_not_called() mock_add_route.assert_not_called() mock_add_ips_dev.assert_not_called() def test__get_bridge_for_datapath(self): self.sb_idl.get_network_name_and_tag.return_value = ( 'fake-network', [10]) ret = self.bgp_driver._get_bridge_for_datapath('fake-dp') self.assertEqual((self.bridge, 10), ret) def test__get_bridge_for_datapath_no_tag(self): self.sb_idl.get_network_name_and_tag.return_value = ( 'fake-network', None) ret = self.bgp_driver._get_bridge_for_datapath('fake-dp') self.assertEqual((self.bridge, None), ret) def test__get_bridge_for_datapath_no_network_name(self): self.sb_idl.get_network_name_and_tag.return_value = (None, None) ret = self.bgp_driver._get_bridge_for_datapath('fake-dp') self.assertEqual((None, None), ret) def test_expose_ovn_lb(self): mock_process_ovn_lb = mock.patch.object( self.bgp_driver, '_process_ovn_lb').start() self.bgp_driver.expose_ovn_lb('fake-ip', 'fake-row') mock_process_ovn_lb.assert_called_once_with( 'fake-ip', 'fake-row', constants.EXPOSE) def test_withdraw_ovn_lb(self): mock_process_ovn_lb = mock.patch.object( self.bgp_driver, '_process_ovn_lb').start() self.bgp_driver.withdraw_ovn_lb('fake-ip', 'fake-row') mock_process_ovn_lb.assert_called_once_with( 'fake-ip', 'fake-row', constants.WITHDRAW) def _test_process_ovn_lb(self, action, provider=False): mock_expose_remote_ip = mock.patch.object( self.bgp_driver, '_expose_remote_ip').start() mock_withdraw_remote_ip = mock.patch.object( self.bgp_driver, '_withdraw_remote_ip').start() self.sb_idl.is_provider_network.return_value = provider ip = 'fake-vip-ip' row = fakes.create_object({ 'logical_port': 'fake-vip', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'datapath': 'fake-provider-dp'}) self.bgp_driver._process_ovn_lb(ip, row, action) if provider: mock_expose_remote_ip.assert_not_called() mock_withdraw_remote_ip.assert_not_called() else: if action == constants.EXPOSE: mock_expose_remote_ip.assert_called_once_with([ip], row) mock_withdraw_remote_ip.assert_not_called() elif action == constants.WITHDRAW: mock_expose_remote_ip.assert_not_called() mock_withdraw_remote_ip.assert_called_once_with([ip], row) else: mock_expose_remote_ip.assert_not_called() mock_withdraw_remote_ip.assert_not_called() def test__process_ovn_lb_expose_provider(self): self._test_process_ovn_lb(action=constants.EXPOSE, provider=True) def test__process_ovn_lb_expose_no_provider(self): self._test_process_ovn_lb(action=constants.EXPOSE) def test__process_ovn_lb_withdraw_provider(self): self._test_process_ovn_lb(action=constants.WITHDRAW, provider=True) def test__process_ovn_lb_withdraw_no_provider(self): self._test_process_ovn_lb(action=constants.WITHDRAW) def test__process_ovn_lb_unknown_action(self): self._test_process_ovn_lb(action="fake-action") def test__process_ovn_lb_datapath_exception(self): ip = 'fake-vip-ip' row = fakes.create_object({ 'logical_port': 'fake-vip', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'datapath': 'fake-provider-dp'}) mock_expose_remote_ip = mock.patch.object( self.bgp_driver, '_expose_remote_ip').start() mock_withdraw_remote_ip = mock.patch.object( self.bgp_driver, '_withdraw_remote_ip').start() self.sb_idl.is_provider_network.side_effect = ( agent_exc.DatapathNotFound(datapath=row.datapath)) self.bgp_driver._process_ovn_lb(ip, row, mock.ANY) mock_expose_remote_ip.assert_not_called() mock_withdraw_remote_ip.assert_not_called() def test_expose_ovn_lb_on_provider(self): mock_expose_provider_port = mock.patch.object( self.bgp_driver, '_expose_provider_port').start() self.bgp_driver.expose_ovn_lb_on_provider( self.ipv4, 'ovn-lb-2', self.cr_lrp0) # Assert that the add methods were called mock_expose_provider_port.assert_called_once_with( [self.ipv4], self.bgp_driver.ovn_local_cr_lrps[self.cr_lrp0][ 'provider_datapath'], bridge_device=self.bridge, bridge_vlan=None) def test__expose_ovn_lb_on_provider_failure(self): mock_expose_provider_port = mock.patch.object( self.bgp_driver, '_expose_provider_port').start() mock_expose_provider_port.return_value = False ret = self.bgp_driver._expose_ovn_lb_on_provider( self.ipv4, self.loadbalancer_vip_port, self.cr_lrp0) # Assert that the add methods were called mock_expose_provider_port.assert_called_once_with( [self.ipv4], self.bgp_driver.ovn_local_cr_lrps[self.cr_lrp0][ 'provider_datapath'], bridge_device=self.bridge, bridge_vlan=None) self.assertEqual(False, ret) def test__expose_ovn_lb_on_provider_keyerror(self): mock_expose_provider_port = mock.patch.object( self.bgp_driver, '_expose_provider_port').start() ret = self.bgp_driver._expose_ovn_lb_on_provider( self.ipv4, self.loadbalancer_vip_port, 'wrong-cr-logical-port') # Assert that the add methods were called mock_expose_provider_port.assert_not_called() self.assertEqual(False, ret) @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_ovn_lb_on_provider( self, mock_del_ip_dev, mock_del_rule, mock_del_route): self.bgp_driver.withdraw_ovn_lb_on_provider( self.loadbalancer_vip_port, self.cr_lrp0) # Assert that the del methods were called expected_calls = [mock.call(CONF.bgp_nic, [self.ipv4]), mock.call(CONF.bgp_nic, [self.ipv6])] mock_del_ip_dev.assert_has_calls(expected_calls) expected_calls = [mock.call(self.ipv4, 'fake-table'), mock.call(self.ipv6, 'fake-table')] mock_del_rule.assert_has_calls(expected_calls) expected_calls = [mock.call(mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=None), mock.call(mock.ANY, self.ipv6, 'fake-table', self.bridge, vlan=None)] mock_del_route.assert_has_calls(expected_calls) @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') @mock.patch.object(linux_net, 'del_ips_from_dev') def test__withdraw_ovn_lb_on_provider_keyerror( self, mock_del_ip_dev, mock_del_rule, mock_del_route): ret = self.bgp_driver._withdraw_ovn_lb_on_provider( self.loadbalancer_vip_port, 'wrong-cr-logical-port') # Assert that the del methods were called self.assertEqual(False, ret) mock_del_ip_dev.assert_not_called() mock_del_rule.assert_not_called() mock_del_route.assert_not_called() def test__withdraw_ovn_lb_on_provider_failure(self): mock_withdraw_provider_port = mock.patch.object( self.bgp_driver, '_withdraw_provider_port').start() mock_withdraw_provider_port.return_value = False ret = self.bgp_driver._withdraw_ovn_lb_on_provider( self.loadbalancer_vip_port, self.cr_lrp0) self.assertEqual(False, ret) @mock.patch.object(wire_utils, '_ensure_updated_mac_tweak_flows') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'add_ips_to_dev') def test_expose_ip_vm_on_provider_network( self, mock_add_ip_dev, mock_add_rule, mock_add_route, mock_ensure_mac_tweak): self.sb_idl.is_provider_network.return_value = True mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) row = fakes.create_object({ 'logical_port': 'fake-row', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.expose_ip(ips, row) # Assert that the add methods were called mock_ensure_mac_tweak.assert_called_once_with(mock.ANY, self.bridge, {}) mock_add_ip_dev.assert_called_once_with( CONF.bgp_nic, ips) expected_calls = [mock.call(self.ipv4, 'fake-table'), mock.call(self.ipv6, 'fake-table')] mock_add_rule.assert_has_calls(expected_calls) expected_calls = [mock.call(mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=10), mock.call(mock.ANY, self.ipv6, 'fake-table', self.bridge, vlan=10)] mock_add_route.assert_has_calls(expected_calls) @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'add_ips_to_dev') def test__expose_ip_vm_on_provider_network_datapath_not_found( self, mock_add_ip_dev, mock_add_rule, mock_add_route): self.sb_idl.is_provider_network.side_effect = ( agent_exc.DatapathNotFound(datapath="fake-dp")) row = fakes.create_object({ 'logical_port': 'fake-row', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] ret = self.bgp_driver._expose_ip(ips, row) # Assert that the add methods were called self.assertEqual([], ret) mock_add_ip_dev.assert_not_called() mock_add_rule.assert_not_called() mock_add_route.assert_not_called() @mock.patch.object(wire_utils, 'wire_provider_port') @mock.patch.object(bgp_utils, 'announce_ips') def test__expose_ip_vm_on_provider_network_expose_failure( self, mock_bgp_announce, mock_wire_port): self.sb_idl.is_provider_network.return_value = True mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) mock_wire_port.return_value = False row = fakes.create_object({ 'logical_port': 'fake-row', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] ret = self.bgp_driver._expose_ip(ips, row) # Assert that the add methods were called self.assertEqual([], ret) mock_wire_port.assert_called_once() mock_bgp_announce.assert_not_called() @mock.patch.object(wire_utils, '_ensure_updated_mac_tweak_flows') @mock.patch.object(linux_net, 'add_ndp_proxy') @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'add_ips_to_dev') def test__expose_ip_virtual_port_on_provider_network( self, mock_add_ip_dev, mock_add_rule, mock_add_route, mock_ip_version, mock_add_ndp_proxy, mock_ensure_mac_tweak): self.sb_idl.is_provider_network.return_value = True mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) mock_ip_version.return_value = constants.IP_VERSION_6 row = fakes.create_object({ 'logical_port': 'fake-row', 'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE, 'datapath': 'fake-dp', 'external_ids': {'neutron:cidrs': '{}/128'.format(self.ipv6)}}) ips = [self.ipv4, self.ipv6] ret = self.bgp_driver._expose_ip(ips, row) # Assert that the add methods were called mock_ensure_mac_tweak.assert_called_once_with(mock.ANY, self.bridge, {}) self.assertEqual(ips, ret) mock_add_ip_dev.assert_called_once_with( CONF.bgp_nic, ips) expected_calls = [mock.call(self.ipv4, 'fake-table'), mock.call(self.ipv6, 'fake-table')] mock_add_rule.assert_has_calls(expected_calls) expected_calls = [mock.call(mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=10), mock.call(mock.ANY, self.ipv6, 'fake-table', self.bridge, vlan=10)] mock_add_route.assert_has_calls(expected_calls) mock_add_ndp_proxy.assert_called_once_with( '{}/128'.format(self.ipv6), self.bridge, 10) @mock.patch.object(linux_net, 'add_ndp_proxy') @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'add_ips_to_dev') def test__expose_ip_virtual_port_on_provider_network_expose_failure( self, mock_add_ip_dev, mock_add_rule, mock_add_route, mock_ip_version, mock_add_ndp_proxy): self.sb_idl.is_provider_network.return_value = True mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (None, None) mock_ip_version.return_value = constants.IP_VERSION_6 row = fakes.create_object({ 'logical_port': 'fake-row', 'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE, 'datapath': 'fake-dp', 'external_ids': {'neutron:cidrs': '{}/128'.format(self.ipv6)}}) ips = [self.ipv4, self.ipv6] ret = self.bgp_driver._expose_ip(ips, row) # Assert that the add methods were called self.assertEqual([], ret) mock_add_ip_dev.assert_not_called() mock_add_rule.assert_not_called() mock_add_route.assert_not_called() mock_add_ndp_proxy.assert_not_called() @mock.patch.object(wire_utils, '_ensure_updated_mac_tweak_flows') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'add_ips_to_dev') def test__expose_ip_vm_with_fip( self, mock_add_ip_dev, mock_add_rule, mock_add_route, mock_ensure_mac_tweak): self.sb_idl.is_provider_network.side_effect = [False, True] self.sb_idl.get_fip_associated.return_value = ( self.fip, 'fake-dp') mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) row = fakes.create_object({ 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_port': 'fake-logical-port', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] ret = self.bgp_driver._expose_ip(ips, row) # Assert that the add methods were called self.assertEqual([self.fip], ret) mock_ensure_mac_tweak.assert_called_once_with(mock.ANY, self.bridge, {}) mock_add_ip_dev.assert_called_once_with( CONF.bgp_nic, [self.fip]) mock_add_rule.assert_called_once_with( self.fip, 'fake-table') mock_add_route.assert_called_once_with( mock.ANY, self.fip, 'fake-table', self.bridge, vlan=10) @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'add_ips_to_dev') def test__expose_ip_vm_with_fip_no_provider( self, mock_add_ip_dev, mock_add_rule, mock_add_route): self.sb_idl.is_provider_network.side_effect = [False, False] self.sb_idl.get_fip_associated.return_value = ( self.fip, 'fake-dp') mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) row = fakes.create_object({ 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_port': 'fake-logical-port', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] ret = self.bgp_driver._expose_ip(ips, row) # Assert that the add methods were called self.assertEqual([], ret) mock_add_ip_dev.assert_not_called() mock_add_rule.assert_not_called() mock_add_route.assert_not_called() @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'add_ips_to_dev') def test__expose_ip_vm_with_fip_no_fip_address( self, mock_add_ip_dev, mock_add_rule, mock_add_route): self.sb_idl.is_provider_network.return_value = False self.sb_idl.get_fip_associated.return_value = (None, None) row = fakes.create_object({ 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_port': 'fake-logical-port', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] ret = self.bgp_driver._expose_ip(ips, row) # Assert that the add methods were not called self.assertEqual([], ret) mock_add_ip_dev.assert_not_called() mock_add_rule.assert_not_called() mock_add_route.assert_not_called() @mock.patch.object(wire_utils, '_ensure_updated_mac_tweak_flows') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'add_ips_to_dev') def test__expose_ip_fip_association_to_vm( self, mock_add_ip_dev, mock_add_rule, mock_add_route, mock_ensure_mac_tweak): self.sb_idl.is_provider_network.return_value = True self.sb_idl.is_port_on_chassis.return_value = True mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) row = fakes.create_object({ 'type': constants.OVN_PATCH_VIF_PORT_TYPE, 'logical_port': 'fake-logical-port', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] ret = self.bgp_driver._expose_ip(ips, row, associated_port=self.cr_lrp0) # Assert that the add methods were called self.assertEqual(ips, ret) mock_ensure_mac_tweak.assert_called_once_with(mock.ANY, self.bridge, {}) mock_add_ip_dev.assert_called_once_with( CONF.bgp_nic, ips) expected_calls = [mock.call(self.ipv4, 'fake-table'), mock.call(self.ipv6, 'fake-table')] mock_add_rule.assert_has_calls(expected_calls) expected_calls = [mock.call(mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=10), mock.call(mock.ANY, self.ipv6, 'fake-table', self.bridge, vlan=10)] mock_add_route.assert_has_calls(expected_calls) @mock.patch.object(wire_utils, '_ensure_updated_mac_tweak_flows') @mock.patch.object(linux_net, 'add_ndp_proxy') @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'add_ips_to_dev') def test__expose_ip_chassisredirect_port( self, mock_add_ip_dev, mock_add_rule, mock_add_route, mock_ip_version, mock_ndp_proxy, mock_ensure_mac_tweak): self.sb_idl.get_provider_datapath_from_cr_lrp.return_value = ( 'fake-provider-dp') mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) lrp0 = fakes.create_object({ 'logical_port': self.lrp0, 'chassis': '', 'mac': ["fa:16:3e:50:ec:81 192.168.1.1/24"], 'options': {}}) lrp1 = fakes.create_object({'logical_port': 'lrp-1', 'chassis': 'fake-chassis', 'options': {}}) lrp2 = fakes.create_object({'logical_port': 'fake-lrp', 'chassis': '', 'options': {}}) self.sb_idl.get_lrp_ports_for_router.return_value = [lrp0, lrp1, lrp2] self.sb_idl.get_port_datapath.return_value = 'fake-lrp-dp' self.sb_idl.get_cr_lrp_nat_addresses_info.return_value = ( [], self.cr_lrp0) mock_process_lrp_port = mock.patch.object( self.bgp_driver, '_process_lrp_port').start() mock_ip_version.side_effect = (constants.IP_VERSION_4, constants.IP_VERSION_6) row = fakes.create_object({ 'type': constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, 'logical_port': self.cr_lrp0, 'mac': ['{} {} {}'.format(self.mac, self.ipv4, self.ipv6)], 'datapath': 'fake-router-dp'}) ovn_lb_vip = '172.24.4.5' ovn_lbs = {'fake-vip-port': ovn_lb_vip} self.sb_idl.get_provider_ovn_lbs_on_cr_lrp.return_value = ( ovn_lbs) mock_expose_ovn_lb = mock.patch.object( self.bgp_driver, '_expose_ovn_lb_on_provider').start() ips = [self.ipv4, self.ipv6] ret = self.bgp_driver._expose_ip(ips, row) # Assert that the add methods were called self.assertEqual(ips, ret) mock_ensure_mac_tweak.assert_called_once_with(mock.ANY, self.bridge, {}) mock_add_ip_dev.assert_called_once_with( CONF.bgp_nic, ips) expected_calls = [mock.call(self.ipv4, 'fake-table', dev='{}.{}'.format(self.bridge, 10), lladdr=self.mac), mock.call(self.ipv6, 'fake-table', dev='{}.{}'.format(self.bridge, 10), lladdr=self.mac)] mock_add_rule.assert_has_calls(expected_calls) expected_calls = [mock.call(mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=10), mock.call(mock.ANY, self.ipv6, 'fake-table', self.bridge, vlan=10)] mock_add_route.assert_has_calls(expected_calls) mock_ndp_proxy.assert_called_once_with(self.ipv6, self.bridge, 10) expected_calls = [mock.call(lrp0, self.cr_lrp0), mock.call(lrp1, self.cr_lrp0), mock.call(lrp2, self.cr_lrp0)] mock_process_lrp_port.assert_has_calls(expected_calls) mock_expose_ovn_lb.assert_called_once_with( ovn_lb_vip, 'fake-vip-port', self.cr_lrp0) @mock.patch.object(linux_net, 'add_ndp_proxy') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'add_ips_to_dev') def test__expose_ip_chassisredirect_port_no_datapath( self, mock_add_ip_dev, mock_add_rule, mock_add_route, mock_ndp_proxy): self.sb_idl.get_provider_datapath_from_cr_lrp.return_value = None row = fakes.create_object({ 'type': constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, 'logical_port': self.cr_lrp0, 'mac': ['{} {} {}'.format(self.mac, self.ipv4, self.ipv6)], 'datapath': 'fake-router-dp'}) ips = [self.ipv4, self.ipv6] ret = self.bgp_driver._expose_ip(ips, row) # Assert that the add methods were called self.assertEqual([], ret) mock_add_ip_dev.assert_not_called() mock_add_rule.assert_not_called() mock_add_route.assert_not_called() mock_ndp_proxy.assert_not_called() @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_ip_vm_on_provider_network( self, mock_del_ip_dev, mock_del_rule, mock_del_route): self.sb_idl.is_provider_network.return_value = True mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) row = fakes.create_object({ 'logical_port': 'fake-row', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.withdraw_ip(ips, row) # Assert that the del methods were called mock_del_ip_dev.assert_called_once_with( CONF.bgp_nic, ips) expected_calls = [mock.call(self.ipv4, 'fake-table'), mock.call(self.ipv6, 'fake-table')] mock_del_rule.assert_has_calls(expected_calls) expected_calls = [mock.call(mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=10), mock.call(mock.ANY, self.ipv6, 'fake-table', self.bridge, vlan=10)] mock_del_route.assert_has_calls(expected_calls) @mock.patch.object(linux_net, 'del_ndp_proxy') @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_ip_virtual_port_on_provider_network( self, mock_del_ip_dev, mock_del_rule, mock_del_route, mock_ip_version, mock_del_ndp_proxy): self.sb_idl.is_provider_network.return_value = True mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) row = fakes.create_object({ 'logical_port': 'fake-row', 'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE, 'datapath': 'fake-dp', 'external_ids': {'neutron:cidrs': '{}/128'.format(self.ipv6)}}) ips = [self.ipv4, self.ipv6] self.sb_idl.get_virtual_ports_on_datapath_by_chassis.return_value = [] mock_ip_version.return_value = constants.IP_VERSION_6 self.bgp_driver.withdraw_ip(ips, row) # Assert that the del methods were called mock_del_ip_dev.assert_called_once_with( CONF.bgp_nic, ips) expected_calls = [mock.call(self.ipv4, 'fake-table'), mock.call(self.ipv6, 'fake-table')] mock_del_rule.assert_has_calls(expected_calls) expected_calls = [mock.call(mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=10), mock.call(mock.ANY, self.ipv6, 'fake-table', self.bridge, vlan=10)] mock_del_route.assert_has_calls(expected_calls) mock_del_ndp_proxy.assert_called_once_with( '{}/128'.format(self.ipv6), self.bridge, 10) @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_ip_vm_with_fip( self, mock_del_ip_dev, mock_del_rule, mock_del_route): self.sb_idl.is_provider_network.side_effect = [False, True] self.sb_idl.get_fip_associated.return_value = ( self.fip, 'fake-dp') mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) row = fakes.create_object({ 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_port': 'fake-logical-port', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.withdraw_ip(ips, row) # Assert that the del methods were called mock_del_ip_dev.assert_called_once_with( CONF.bgp_nic, [self.fip]) mock_del_rule.assert_called_once_with( self.fip, 'fake-table') mock_del_route.assert_called_once_with( mock.ANY, self.fip, 'fake-table', self.bridge, vlan=10) @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_ip_vm_with_fip_no_fip_address( self, mock_del_ip_dev, mock_del_rule, mock_del_route): self.sb_idl.is_provider_network.return_value = False self.sb_idl.get_fip_associated.return_value = (None, None) row = fakes.create_object({ 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_port': 'fake-logical-port', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.withdraw_ip(ips, row) # Assert that the del methods were not called mock_del_ip_dev.assert_not_called() mock_del_rule.assert_not_called() mock_del_route.assert_not_called() @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_ip_fip_association_to_vm( self, mock_del_ip_dev, mock_del_rule, mock_del_route): self.sb_idl.is_provider_network.return_value = True self.sb_idl.is_port_on_chassis.return_value = True mock_get_bridge = mock.patch.object( self.bgp_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, 10) row = fakes.create_object({ 'type': constants.OVN_PATCH_VIF_PORT_TYPE, 'logical_port': 'fake-logical-port', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.withdraw_ip(ips, row, associated_port=self.cr_lrp0) # Assert that the del methods were called mock_del_ip_dev.assert_called_once_with( CONF.bgp_nic, ips) expected_calls = [mock.call(self.ipv4, 'fake-table'), mock.call(self.ipv6, 'fake-table')] mock_del_rule.assert_has_calls(expected_calls) expected_calls = [mock.call(mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=10), mock.call(mock.ANY, self.ipv6, 'fake-table', self.bridge, vlan=10)] mock_del_route.assert_has_calls(expected_calls) @mock.patch.object(linux_net, 'del_ndp_proxy') @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_ip_chassisredirect_port( self, mock_del_ip_dev, mock_del_rule, mock_del_route, mock_ip_version, mock_ndp_proxy): mock_withdraw_lrp_port = mock.patch.object( self.bgp_driver, '_withdraw_lrp_port').start() mock_ip_version.side_effect = (constants.IP_VERSION_4, constants.IP_VERSION_6, constants.IP_VERSION_4, constants.IP_VERSION_6, constants.IP_VERSION_6) row = fakes.create_object({ 'type': constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, 'logical_port': self.cr_lrp0, 'mac': ['{} {} {}'.format(self.mac, self.ipv4, self.ipv6)], 'datapath': 'fake-router-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.withdraw_ip(ips, row) # Assert that the del methods were called mock_del_ip_dev.assert_called_once_with( CONF.bgp_nic, ips) expected_calls = [mock.call('{}/32'.format(self.ipv4), 'fake-table', dev=self.bridge, lladdr=self.mac), mock.call('{}/128'.format(self.ipv6), 'fake-table', dev=self.bridge, lladdr=self.mac)] mock_del_rule.assert_has_calls(expected_calls) expected_calls = [mock.call(mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=None), mock.call(mock.ANY, self.ipv6, 'fake-table', self.bridge, vlan=None)] mock_del_route.assert_has_calls(expected_calls) mock_ndp_proxy.assert_called_once_with(self.ipv6, self.bridge, None) mock_withdraw_lrp_port.assert_called_once_with( '192.168.1.1/24', None, self.cr_lrp0) @mock.patch.object(linux_net, 'add_ips_to_dev') def test_expose_remote_ip(self, mock_add_ip_dev): self.sb_idl.is_provider_network.return_value = False lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.bgp_driver.ovn_local_lrps = {lrp: 'fake-cr-lrp'} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.expose_remote_ip(ips, row) mock_add_ip_dev.assert_called_once_with(CONF.bgp_nic, ips) @mock.patch.object(linux_net, 'add_ips_to_dev') def test_expose_remote_ip_is_provider_network(self, mock_add_ip_dev): self.sb_idl.is_provider_network.return_value = True row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.expose_remote_ip(ips, row) mock_add_ip_dev.assert_not_called() @mock.patch.object(linux_net, 'add_ips_to_dev') def test_expose_remote_ip_not_local(self, mock_add_ip_dev): self.sb_idl.is_provider_network.return_value = False lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.bgp_driver.ovn_local_lrps = {} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.expose_remote_ip(ips, row) mock_add_ip_dev.assert_not_called() @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(driver_utils, 'is_ipv6_gua') def test_expose_remote_ip_gua(self, mock_ipv6_gua, mock_add_ip_dev): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') mock_ipv6_gua.side_effect = [False, True] self.sb_idl.is_provider_network.return_value = False lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.bgp_driver.ovn_local_lrps = {lrp: 'fake-cr-lrp'} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.expose_remote_ip(ips, row) mock_add_ip_dev.assert_called_once_with(CONF.bgp_nic, [self.ipv6]) @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(driver_utils, 'is_ipv6_gua') def test_expose_remote_ip_not_gua(self, mock_ipv6_gua, mock_add_ip_dev): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') mock_ipv6_gua.side_effect = [False, False] self.sb_idl.is_provider_network.return_value = False lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.bgp_driver.ovn_local_lrps = {lrp: 'fake-cr-lrp'} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, 'fdab:4ad8:e8fb:0:f816:3eff:fec6:469c'] self.bgp_driver.expose_remote_ip(ips, row) mock_add_ip_dev.assert_not_called() @mock.patch.object(linux_net, 'add_ips_to_dev') def test_expose_remote_ip_address_scope(self, mock_add_ip_dev): self.sb_idl.is_provider_network.return_value = False lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.bgp_driver.ovn_local_lrps = {lrp: 'fake-cr-lrp'} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) mock_address_scope_allowed = mock.patch.object( self.bgp_driver, '_address_scope_allowed').start() mock_address_scope_allowed.side_effect = [False, True] ips = [self.ipv4, self.ipv6] self.bgp_driver.expose_remote_ip(ips, row) mock_add_ip_dev.assert_called_once_with(CONF.bgp_nic, [self.ipv6]) @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_remote_ip(self, mock_del_ip_dev): self.sb_idl.is_provider_network.return_value = False lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.bgp_driver.ovn_local_lrps = {lrp: 'fake-cr-lrp'} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.withdraw_remote_ip(ips, row) mock_del_ip_dev.assert_called_once_with(CONF.bgp_nic, ips) @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_remote_ip_is_provider_network(self, mock_del_ip_dev): self.sb_idl.is_provider_network.return_value = True row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.withdraw_remote_ip(ips, row) mock_del_ip_dev.assert_not_called() @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_remote_ip_not_local(self, mock_del_ip_dev): self.sb_idl.is_provider_network.return_value = False lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.bgp_driver.ovn_local_lrps = {} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.withdraw_remote_ip(ips, row) mock_del_ip_dev.assert_not_called() @mock.patch.object(linux_net, 'del_ips_from_dev') @mock.patch.object(driver_utils, 'is_ipv6_gua') def test_withdraw_remote_ip_gua(self, mock_ipv6_gua, mock_del_ip_dev): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') mock_ipv6_gua.side_effect = [False, True] self.sb_idl.is_provider_network.return_value = False lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.bgp_driver.ovn_local_lrps = {lrp: 'fake-cr-lrp'} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.bgp_driver.withdraw_remote_ip(ips, row) mock_del_ip_dev.assert_called_once_with(CONF.bgp_nic, [self.ipv6]) @mock.patch.object(linux_net, 'del_ips_from_dev') @mock.patch.object(driver_utils, 'is_ipv6_gua') def test_withdraw_remote_ip_not_gua(self, mock_ipv6_gua, mock_del_ip_dev): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') mock_ipv6_gua.side_effect = [False, False] self.sb_idl.is_provider_network.return_value = False lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.bgp_driver.ovn_local_lrps = {lrp: 'fake-cr-lrp'} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, 'fdab:4ad8:e8fb:0:f816:3eff:fec6:469c'] self.bgp_driver.withdraw_remote_ip(ips, row) mock_del_ip_dev.assert_not_called() @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_remote_ip_address_scope(self, mock_del_ip_dev): self.sb_idl.is_provider_network.return_value = False lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.bgp_driver.ovn_local_lrps = {lrp: 'fake-cr-lrp'} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) mock_address_scope_allowed = mock.patch.object( self.bgp_driver, '_address_scope_allowed').start() mock_address_scope_allowed.side_effect = [False, True] ips = [self.ipv4, self.ipv6] self.bgp_driver.withdraw_remote_ip(ips, row) mock_del_ip_dev.assert_called_once_with(CONF.bgp_nic, [self.ipv6]) @mock.patch.object(linux_net, 'get_ip_version') def test__expose_cr_lrp_port(self, mock_ip_version): mock_expose_provider_port = mock.patch.object( self.bgp_driver, '_expose_provider_port').start() mock_process_lrp_port = mock.patch.object( self.bgp_driver, '_process_lrp_port').start() mock_expose_ovn_lb = mock.patch.object( self.bgp_driver, '_expose_ovn_lb_on_provider').start() ips = [self.ipv4, self.ipv6] mock_ip_version.side_effect = [constants.IP_VERSION_4, constants.IP_VERSION_6] dp_port0 = mock.Mock() self.sb_idl.get_lrp_ports_for_router.return_value = [dp_port0] ovn_lbs = {'fake-vip-port': 'fake-vip-ip'} self.sb_idl.get_provider_ovn_lbs_on_cr_lrp.return_value = ( ovn_lbs) ips_without_mask = [ip.split("/")[0] for ip in ips] self.sb_idl.get_cr_lrp_nat_addresses_info.return_value = ( [ips_without_mask[0]], self.cr_lrp0) self.bgp_driver._expose_cr_lrp_port( ips, self.mac, self.bridge, None, router_datapath='fake-router-dp', provider_datapath='fake-provider-dp', cr_lrp_port=self.cr_lrp0) mock_expose_provider_port.assert_called_once_with( ips_without_mask, 'fake-provider-dp', self.bridge, None, lladdr=self.mac, proxy_cidrs=ips) mock_process_lrp_port.assert_called_once_with(dp_port0, self.cr_lrp0) mock_expose_ovn_lb.assert_called_once_with( 'fake-vip-ip', 'fake-vip-port', self.cr_lrp0) def test__expose_cr_lrp_port_failure(self): mock_expose_provider_port = mock.patch.object( self.bgp_driver, '_expose_provider_port').start() mock_expose_provider_port.return_value = False mock_process_lrp_port = mock.patch.object( self.bgp_driver, '_process_lrp_port').start() mock_expose_ovn_lb = mock.patch.object( self.bgp_driver, '_expose_ovn_lb_on_provider').start() ips = [self.ipv4, self.ipv6] ret = self.bgp_driver._expose_cr_lrp_port( ips, self.mac, self.bridge, None, router_datapath='fake-router-dp', provider_datapath='fake-provider-dp', cr_lrp_port=self.cr_lrp0) self.assertEqual(False, ret) ips_without_mask = [ip.split("/")[0] for ip in ips] mock_expose_provider_port.assert_called_once_with( ips_without_mask, 'fake-provider-dp', self.bridge, None, lladdr=self.mac, proxy_cidrs=ips) mock_process_lrp_port.assert_not_called() mock_expose_ovn_lb.assert_not_called() @mock.patch.object(linux_net, 'get_ip_version') def test__withdraw_cr_lrp_port(self, mock_ip_version): mock_withdraw_provider_port = mock.patch.object( self.bgp_driver, '_withdraw_provider_port').start() mock_withdraw_lrp_port = mock.patch.object( self.bgp_driver, '_withdraw_lrp_port').start() mock_withdraw_ovn_lb_on_provider = mock.patch.object( self.bgp_driver, '_withdraw_ovn_lb_on_provider').start() ips = [self.ipv4, self.ipv6] mock_ip_version.side_effect = [constants.IP_VERSION_4, constants.IP_VERSION_6] ovn_lb_vip_port = mock.Mock() gateway = { 'ips': ips, 'provider_datapath': 'fake-provider-dp', 'subnets_cidr': ['192.168.1.1/24'], 'bridge_device': self.bridge, 'bridge_vlan': 10, 'mac': self.mac, 'provider_ovn_lbs': [ovn_lb_vip_port]} self.bgp_driver.ovn_local_cr_lrps = {'gateway_port': gateway} self.bgp_driver._withdraw_cr_lrp_port( ips, self.mac, self.bridge, 10, provider_datapath='fake-provider-dp', cr_lrp_port='gateway_port') ips_without_mask = [ip.split("/")[0] for ip in ips] mock_withdraw_provider_port.assert_called_once_with( ips_without_mask, 'fake-provider-dp', bridge_device=self.bridge, bridge_vlan=10, lladdr=self.mac, proxy_cidrs=[self.ipv6]) mock_withdraw_lrp_port.assert_called_once_with('192.168.1.1/24', None, 'gateway_port') mock_withdraw_ovn_lb_on_provider.assert_called_once_with( ovn_lb_vip_port, 'gateway_port') @mock.patch.object(linux_net, 'get_ip_version') def test__withdraw_cr_lrp_port_withdraw_failure(self, mock_ip_version): mock_withdraw_provider_port = mock.patch.object( self.bgp_driver, '_withdraw_provider_port').start() mock_withdraw_provider_port.return_value = False mock_withdraw_lrp_port = mock.patch.object( self.bgp_driver, '_withdraw_lrp_port').start() mock_withdraw_ovn_lb_on_provider = mock.patch.object( self.bgp_driver, '_withdraw_ovn_lb_on_provider').start() ips = [self.ipv4, self.ipv6] mock_ip_version.side_effect = [constants.IP_VERSION_4, constants.IP_VERSION_6] ovn_lb_vip_port = mock.Mock() gateway = { 'ips': ips, 'provider_datapath': 'fake-provider-dp', 'subnets_cidr': ['192.168.1.1/24'], 'bridge_device': self.bridge, 'bridge_vlan': 10, 'mac': self.mac, 'provider_ovn_lbs': [ovn_lb_vip_port]} self.bgp_driver.ovn_local_cr_lrps = {'gateway_port': gateway} ret = self.bgp_driver._withdraw_cr_lrp_port( ips, self.mac, self.bridge, 10, provider_datapath='fake-provider-dp', cr_lrp_port='gateway_port') self.assertEqual(False, ret) ips_without_mask = [ip.split("/")[0] for ip in ips] mock_withdraw_provider_port.assert_called_once_with( ips_without_mask, 'fake-provider-dp', bridge_device=self.bridge, bridge_vlan=10, lladdr=self.mac, proxy_cidrs=[self.ipv6]) mock_withdraw_lrp_port.assert_not_called() mock_withdraw_ovn_lb_on_provider.assert_not_called() @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'get_ip_version') def test__expose_lrp_port( self, mock_ip_version, mock_add_rule, mock_add_route): mock_ip_version.return_value = constants.IP_VERSION_4 dp_port0 = fakes.create_object({ 'name': 'fake-port-dp0', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': ['aa:bb:cc:dd:ee:ee 192.168.1.10 192.168.1.11'], 'chassis': 'fake-chassis1'}) dp_port1 = fakes.create_object({ 'name': 'fake-port-dp1', 'type': 'fake-type', 'mac': ['aa:bb:cc:dd:ee:ee 192.168.1.12 192.168.1.13'], 'chassis': 'fake-chassis2'}) dp_port2 = fakes.create_object({ 'name': 'fake-port-dp1', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': [], 'chassis': 'fake-chassis2'}) self.sb_idl.get_ports_on_datapath.return_value = [dp_port0, dp_port1, dp_port2] mock_expose_tenant_port = mock.patch.object( self.bgp_driver, '_expose_tenant_port').start() self.bgp_driver._expose_lrp_port( '{}/32'.format(self.ipv4), self.lrp0, self.cr_lrp0, 'fake-lrp-dp') mock_add_rule.assert_called_once_with( '{}/32'.format(self.ipv4), 'fake-table') mock_add_route.assert_called_once_with( mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=None, mask='32', via=self.fip) expected_calls = [ mock.call(dp_port0, ip_version=constants.IP_VERSION_4, exposed_ips=None, ovn_ip_rules=None), mock.call(dp_port1, ip_version=constants.IP_VERSION_4, exposed_ips=None, ovn_ip_rules=None), mock.call(dp_port2, ip_version=constants.IP_VERSION_4, exposed_ips=None, ovn_ip_rules=None)] mock_expose_tenant_port.assert_has_calls(expected_calls) @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'get_ip_version') def test__expose_lrp_port_invalid_ip( self, mock_ip_version, mock_add_rule, mock_add_route): mock_ip_version.return_value = constants.IP_VERSION_4 dp_port0 = fakes.create_object({ 'name': 'fake-port-dp0', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': ['aa:bb:cc:dd:ee:ee 192.168.1.10 192.168.1.11'], 'chassis': 'fake-chassis1'}) dp_port1 = fakes.create_object({ 'name': 'fake-port-dp1', 'type': 'fake-type', 'mac': ['aa:bb:cc:dd:ee:ee 192.168.1.12 192.168.1.13'], 'chassis': 'fake-chassis2'}) dp_port2 = fakes.create_object({ 'name': 'fake-port-dp1', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': [], 'chassis': 'fake-chassis2'}) self.sb_idl.get_ports_on_datapath.return_value = [dp_port0, dp_port1, dp_port2] mock_expose_tenant_port = mock.patch.object( self.bgp_driver, '_expose_tenant_port').start() mock_add_rule.side_effect = agent_exc.InvalidPortIP(ip=self.ipv4) self.bgp_driver._expose_lrp_port( '{}/32'.format(self.ipv4), self.lrp0, self.cr_lrp0, 'fake-lrp-dp') mock_add_rule.assert_called_once_with( '{}/32'.format(self.ipv4), 'fake-table') mock_add_route.assert_not_called() mock_expose_tenant_port.assert_not_called() @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(driver_utils, 'is_ipv6_gua') def test__expose_lrp_port_gua( self, mock_ipv6_gua, mock_ip_version, mock_add_rule, mock_add_route): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') mock_ipv6_gua.return_value = True mock_ip_version.return_value = constants.IP_VERSION_6 dp_port0 = fakes.create_object({ 'name': 'fake-port-dp0', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': ['aa:bb:cc:dd:ee:ee 2002::1234:abcd:ffff:c0a8:111'], 'chassis': 'fake-chassis1'}) dp_port1 = fakes.create_object({ 'name': 'fake-port-dp1', 'type': 'fake-type', 'mac': ['aa:bb:cc:dd:ee:ee 192.168.1.12 192.168.1.13'], 'chassis': 'fake-chassis2'}) dp_port2 = fakes.create_object({ 'name': 'fake-port-dp1', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': [], 'chassis': 'fake-chassis2'}) self.sb_idl.get_ports_on_datapath.return_value = [dp_port0, dp_port1, dp_port2] mock_expose_tenant_port = mock.patch.object( self.bgp_driver, '_expose_tenant_port').start() self.bgp_driver._expose_lrp_port( '{}/128'.format(self.ipv6), self.lrp0, self.cr_lrp0, 'fake-lrp-dp') mock_add_rule.assert_called_once_with( '{}/128'.format(self.ipv6), 'fake-table') mock_add_route.assert_called_once_with( mock.ANY, self.ipv6, 'fake-table', self.bridge, vlan=None, mask='128', via=self.fip) expected_calls = [ mock.call(dp_port0, ip_version=constants.IP_VERSION_6, exposed_ips=None, ovn_ip_rules=None), mock.call(dp_port1, ip_version=constants.IP_VERSION_6, exposed_ips=None, ovn_ip_rules=None), mock.call(dp_port2, ip_version=constants.IP_VERSION_6, exposed_ips=None, ovn_ip_rules=None)] mock_expose_tenant_port.assert_has_calls(expected_calls) @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'add_ip_rule') @mock.patch.object(driver_utils, 'is_ipv6_gua') def test__expose_lrp_port_no_gua( self, mock_ipv6_gua, mock_add_rule, mock_add_route): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') mock_ipv6_gua.return_value = False dp_port0 = fakes.create_object({ 'name': 'fake-port-dp0', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': ['aa:bb:cc:dd:ee:ee 2002::1234:abcd:ffff:c0a8:111'], 'chassis': 'fake-chassis1'}) dp_port1 = fakes.create_object({ 'name': 'fake-port-dp1', 'type': 'fake-type', 'mac': ['aa:bb:cc:dd:ee:ee 192.168.1.12 192.168.1.13'], 'chassis': 'fake-chassis2'}) dp_port2 = fakes.create_object({ 'name': 'fake-port-dp1', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'mac': [], 'chassis': 'fake-chassis2'}) self.sb_idl.get_ports_on_datapath.return_value = [dp_port0, dp_port1, dp_port2] mock_expose_tenant_port = mock.patch.object( self.bgp_driver, '_expose_tenant_port').start() self.bgp_driver._expose_lrp_port( 'fdab:4ad8:e8fb:0:f816:3eff:fec6:469c/128', self.lrp0, self.cr_lrp0, 'fake-lrp-dp') mock_add_rule.assert_not_called() mock_add_route.assert_not_called() mock_expose_tenant_port.assert_not_called() def test_expose_subnet(self): self.sb_idl.is_router_gateway_on_chassis.return_value = self.cr_lrp0 self.sb_idl.get_port_datapath.return_value = 'fake-port-dp' row = fakes.create_object({ 'name': 'fake-row', 'logical_port': 'subnet_port', 'datapath': 'fake-dp', 'options': {'peer': 'fake-peer'}}) mock_expose_lrp_port = mock.patch.object( self.bgp_driver, '_expose_lrp_port').start() self.bgp_driver.expose_subnet('fake-ip', row) mock_expose_lrp_port.assert_called_once_with( 'fake-ip', row.logical_port, self.cr_lrp0, 'fake-port-dp') def test_expose_subnet_no_cr_lrp(self): self.sb_idl.is_router_gateway_on_chassis.return_value = None self.sb_idl.get_port_datapath.return_value = 'fake-port-dp' row = fakes.create_object({ 'name': 'fake-row', 'logical_port': 'subnet_port', 'datapath': 'fake-dp', 'options': {'peer': 'fake-peer'}}) mock_expose_lrp_port = mock.patch.object( self.bgp_driver, '_expose_lrp_port').start() self.bgp_driver.expose_subnet('fake-ip', row) mock_expose_lrp_port.assert_not_called() def test_expose_subnet_address_scope(self): self.sb_idl.is_router_gateway_on_chassis.return_value = self.cr_lrp0 self.sb_idl.get_port_datapath.return_value = 'fake-port-dp' row = fakes.create_object({ 'name': 'fake-row', 'logical_port': 'subnet_port', 'datapath': 'fake-dp', 'options': {'peer': 'fake-peer'}}) mock_expose_lrp_port = mock.patch.object( self.bgp_driver, '_expose_lrp_port').start() mock_address_scope_allowed = mock.patch.object( self.bgp_driver, '_address_scope_allowed').start() mock_address_scope_allowed.return_value = False self.bgp_driver.expose_subnet('fake-ip', row) mock_expose_lrp_port.assert_not_called() def test_withdraw_subnet(self): row = fakes.create_object({ 'name': 'fake-row', 'logical_port': 'subnet_port', 'datapath': 'fake-dp'}) self.sb_idl.is_router_gateway_on_chassis.return_value = self.cr_lrp0 mock_withdraw_lrp_port = mock.patch.object( self.bgp_driver, '_withdraw_lrp_port').start() self.bgp_driver.withdraw_subnet('{}/32'.format(self.ipv4), row) mock_withdraw_lrp_port.assert_called_once_with( '{}/32'.format(self.ipv4), row.logical_port, self.cr_lrp0) def test_withdraw_subnet_no_cr_lrp(self): row = fakes.create_object({ 'name': 'fake-row', 'logical_port': 'subnet_port', 'datapath': 'fake-dp'}) self.sb_idl.is_router_gateway_on_chassis.return_value = None mock_withdraw_lrp_port = mock.patch.object( self.bgp_driver, '_withdraw_lrp_port').start() self.bgp_driver.withdraw_subnet('{}/32'.format(self.ipv4), row) mock_withdraw_lrp_port.assert_not_called() def test_withdraw_subnet_no_datapath_error(self): row = fakes.create_object({ 'name': 'fake-row', 'logical_port': 'fake-logical-port', # to match the cr-lrp name 'datapath': 'fake-dp'}) self.sb_idl.is_router_gateway_on_chassis.side_effect = ( agent_exc.DatapathNotFound(datapath="fake-dp")) mock_withdraw_lrp_port = mock.patch.object( self.bgp_driver, '_withdraw_lrp_port').start() self.bgp_driver.withdraw_subnet('{}/32'.format(self.ipv4), row) mock_withdraw_lrp_port.assert_not_called() @mock.patch.object(linux_net, 'get_exposed_ips_on_network') @mock.patch.object(linux_net, 'delete_exposed_ips') @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') @mock.patch.object(linux_net, 'get_ip_version') def test__withdraw_lrp_port( self, mock_ip_version, mock_del_rule, mock_del_route, mock_del_exposed_ips, mock_get_exposed_ips): mock_ip_version.return_value = constants.IP_VERSION_4 mock_get_exposed_ips.return_value = [self.ipv4] self.bgp_driver.ovn_local_lrps = {self.lrp0: self.cr_lrp0} self.bgp_driver._withdraw_lrp_port( '{}/32'.format(self.ipv4), self.lrp0, self.cr_lrp0) mock_del_rule.assert_called_once_with( '{}/32'.format(self.ipv4), 'fake-table') mock_del_route.assert_called_once_with( mock.ANY, self.ipv4, 'fake-table', self.bridge, vlan=None, mask='32', via=self.fip) mock_del_exposed_ips.assert_called_once_with( [self.ipv4], CONF.bgp_nic) @mock.patch.object(linux_net, 'get_exposed_ips_on_network') @mock.patch.object(linux_net, 'delete_exposed_ips') @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(driver_utils, 'is_ipv6_gua') def test__withdraw_lrp_port_gua( self, mock_ipv6_gua, mock_ip_version, mock_del_rule, mock_del_route, mock_del_exposed_ips, mock_get_exposed_ips): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') mock_ipv6_gua.return_value = True mock_ip_version.return_value = constants.IP_VERSION_6 mock_get_exposed_ips.return_value = [self.ipv6] self.bgp_driver.ovn_local_lrps = {self.lrp0: self.cr_lrp0} self.bgp_driver._withdraw_lrp_port( '{}/128'.format(self.ipv6), self.lrp0, self.cr_lrp0) mock_del_rule.assert_called_once_with( '{}/128'.format(self.ipv6), 'fake-table') mock_del_route.assert_called_once_with( mock.ANY, self.ipv6, 'fake-table', self.bridge, vlan=None, mask='128', via=self.fip) mock_del_exposed_ips.assert_called_once_with( [self.ipv6], CONF.bgp_nic) @mock.patch.object(linux_net, 'get_exposed_ips_on_network') @mock.patch.object(linux_net, 'delete_exposed_ips') @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'del_ip_rule') @mock.patch.object(driver_utils, 'is_ipv6_gua') def test__withdraw_lrp_port_no_gua( self, mock_ipv6_gua, mock_del_rule, mock_del_route, mock_del_exposed_ips, mock_get_exposed_ips): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') CONF.set_override('expose_ipv6_gua_tenant_networks', True) self.addCleanup(CONF.clear_override, 'expose_ipv6_gua_tenant_networks') mock_ipv6_gua.return_value = False mock_get_exposed_ips.return_value = [self.ipv6] self.bgp_driver.ovn_local_lrps = {self.lrp0: self.cr_lrp0} self.bgp_driver._withdraw_lrp_port( 'fdab:4ad8:e8fb:0:f816:3eff:fec6:469c/128', self.lrp0, self.cr_lrp0) mock_del_rule.assert_not_called() mock_del_route.assert_not_called() mock_del_exposed_ips.assert_not_called() @mock.patch.object(driver_utils, 'get_addr_scopes') def test__address_scope_allowed(self, m_addr_scopes): self.bgp_driver.allowed_address_scopes = {"fake_address_scope"} port_ip = self.ipv4 port_name = "fake-port" sb_port = "fake-sb-port" self.sb_idl.get_port_by_name.return_value = sb_port address_scopes = { constants.IP_VERSION_4: "fake_address_scope", constants.IP_VERSION_6: "fake_ipv6_address_scope"} m_addr_scopes.return_value = address_scopes ret = self.bgp_driver._address_scope_allowed(port_ip, port_name) self.assertEqual(True, ret) m_addr_scopes.assert_called_once_with(sb_port) def test__address_scope_allowed_not_configured(self): self.bgp_driver.allowed_address_scopes = set() port_ip = self.ipv4 port_name = "fake-port" sb_port = "fake-sb-port" ret = self.bgp_driver._address_scope_allowed( port_ip, port_name, sb_port) self.assertEqual(True, ret) @mock.patch.object(driver_utils, 'get_addr_scopes') def test__address_scope_allowed_no_match(self, m_addr_scopes): self.bgp_driver.allowed_address_scopes = {"fake_address_scope"} port_ip = self.ipv4 port_name = "fake-port" sb_port = "fake-sb-port" self.sb_idl.get_port_by_name.return_value = sb_port address_scopes = { constants.IP_VERSION_4: "different_fake_address_scope", constants.IP_VERSION_6: "fake_ipv6_address_scope"} m_addr_scopes.return_value = address_scopes ret = self.bgp_driver._address_scope_allowed(port_ip, port_name) self.assertEqual(False, ret) m_addr_scopes.assert_called_once_with(sb_port) @mock.patch.object(driver_utils, 'get_addr_scopes') def test__address_scope_allowed_no_port(self, m_addr_scopes): self.bgp_driver.allowed_address_scopes = {"fake_address_scope"} port_ip = self.ipv4 port_name = "fake-port" self.sb_idl.get_port_by_name.return_value = [] ret = self.bgp_driver._address_scope_allowed(port_ip, port_name) self.assertEqual(False, ret) m_addr_scopes.assert_not_called() @mock.patch.object(driver_utils, 'get_addr_scopes') def test__address_scope_allowed_no_address_scope(self, m_addr_scopes): self.bgp_driver.allowed_address_scopes = {"fake_address_scope"} port_ip = self.ipv4 port_name = "fake-port" sb_port = "fake-sb-port" self.sb_idl.get_port_by_name.return_value = sb_port address_scopes = { constants.IP_VERSION_4: "", constants.IP_VERSION_6: ""} m_addr_scopes.return_value = address_scopes ret = self.bgp_driver._address_scope_allowed(port_ip, port_name) self.assertEqual(False, ret) m_addr_scopes.assert_called_once_with(sb_port) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/test_ovn_evpn_driver.py000066400000000000000000001026761460327367600314240ustar00rootroot00000000000000# Copyright 2022 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_config import cfg from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack import ovn_evpn_driver from ovn_bgp_agent.drivers.openstack.utils import frr from ovn_bgp_agent.drivers.openstack.utils import ovn from ovn_bgp_agent.drivers.openstack.utils import ovs from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.tests.unit import fakes from ovn_bgp_agent.utils import linux_net CONF = cfg.CONF class TestOVNEVPNDriver(test_base.TestCase): def setUp(self): super(TestOVNEVPNDriver, self).setUp() self.evpn_driver = ovn_evpn_driver.OVNEVPNDriver() self.mock_sbdb = mock.patch.object(ovn, 'OvnSbIdl').start() self.mock_ovs_idl = mock.patch.object(ovs, 'OvsIdl').start() self.evpn_driver.ovs_idl = self.mock_ovs_idl self.evpn_driver.sb_idl = mock.Mock() self.sb_idl = self.evpn_driver.sb_idl self.evpn_driver.chassis = 'fake-chassis' self.ipv4 = '192.168.1.17' self.ipv6 = '2002::1234:abcd:ffff:c0a8:101' self.fip = '172.24.4.33' self.mac = 'aa:bb:cc:dd:ee:ff' self.mac1 = 'aa:bb:cc:dd:ee:ee' self.bridge = 'fake-bridge' self.vni = 77 self.vni1 = 88 self.vlan_tag = 10 self.evpn_driver.ovn_bridge_mappings = {'fake-network': self.bridge} self.evpn_info = {'bgp_as': 'fake-bgp-as', 'vni': self.vni} self.evpn_device = fakes.create_object({ 'lo_name': 'fake-lo-name', 'bridge_name': self.bridge, 'vxlan_name': 'fake-vxlan-name', 'vrf_name': 'fake-vrf-name', 'veth_vrf': 'fake-veth-vrf', 'veth_ovs': 'fake-veth-ovs', 'vlan_name': 'fake-vlan-name'}) self.cr_lrp = 'cr-fake-logical-port' self.cr_lrp1 = 'cr-fake-logical-port1' self.evpn_driver.ovn_local_cr_lrps = { self.cr_lrp: { 'provider_datapath': 'fake-provider-dp', 'ips': [self.fip], 'vni': self.vni, 'bgp_as': 'fake-bgp-as', 'bridge': self.bridge, 'vlan': 'fake-vlan', 'vxlan': 'fake-vxlan', 'vrf': 'fake-vrf', 'veth_vrf': 'fake-veth-vrf', 'veth_ovs': 'fake-veth-ovs', 'lo': 'fake-lo', 'mac': self.mac}, self.cr_lrp1: { 'provider_datapath': 'fake-provider-dp1', 'ips': [self.fip], 'vni': self.vni1, 'bgp_as': 'fake-bgp-as1', 'bridge': self.bridge, 'vlan': 'fake-vlan1', 'vxlan': 'fake-vxlan1', 'vrf': 'fake-vrf1', 'veth_vrf': 'fake-veth-vrf1', 'veth_ovs': 'fake-veth-ovs1', 'lo': 'fake-lo1', 'mac': self.mac1}, } self.evpn_driver._ovn_routing_tables_routes = { 'fake-vlan': [{ 'route': { 'oif': 'fake-oif', 'gateway': 'fake-gateway', 'dst': '{}/32'.format(self.ipv4), 'dst_len': 32, 'table': 'fake-table'}, 'vlan': 88, }] } def test_start(self): self.evpn_driver.start() # Assert connections were started self.mock_ovs_idl().start.assert_called_once_with( CONF.ovsdb_connection) self.mock_sbdb().start.assert_called_once_with() @mock.patch.object(linux_net, 'ensure_arp_ndp_enabled_for_bridge') def test_sync(self, mock_ensure_ndp): self.mock_ovs_idl.get_ovn_bridge_mappings.return_value = [ 'net0:bridge0', 'net1:bridge1'] port0 = fakes.create_object({ 'name': 'fake-port0', 'type': constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE}) port1 = fakes.create_object({ 'name': 'fake-port1', 'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE}) self.sb_idl.get_ports_on_chassis.return_value = [ port0, port1] mock_expose_ip = mock.patch.object( self.evpn_driver, '_expose_ip').start() mock_remove_extra_exposed_ips = mock.patch.object( self.evpn_driver, '_remove_extra_exposed_ips').start() mock_remove_extra_routes = mock.patch.object( self.evpn_driver, '_remove_extra_routes').start() mock_remove_extra_ovs_flows = mock.patch.object( self.evpn_driver, '_remove_extra_ovs_flows').start() mock_remove_extra_vrfs = mock.patch.object( self.evpn_driver, '_remove_extra_vrfs').start() self.evpn_driver.sync() expected_calls = [mock.call('bridge0', 1), mock.call('bridge1', 2)] mock_ensure_ndp.assert_has_calls(expected_calls) mock_expose_ip.assert_called_once_with(port0, cr_lrp=True) mock_remove_extra_exposed_ips.assert_called_once_with() mock_remove_extra_routes.assert_called_once_with() mock_remove_extra_ovs_flows.assert_called_once_with() mock_remove_extra_vrfs.assert_called_once_with() def test__ensure_network_exposed(self): self.sb_idl.get_evpn_info_from_port_name.return_value = 'fake-info' self.sb_idl.get_port_datapath.return_value = 'fake-dp' mock_expose_subnet = mock.patch.object( self.evpn_driver, '_expose_subnet').start() mock_get_bridge = mock.patch.object( self.evpn_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, self.vlan_tag) gateway = {} gateway['ips'] = ['10.10.10.1/32'] gateway['provider_datapath'] = 'fake-prov-dp' lrp = fakes.create_object({ 'name': 'fake-lrp', 'logical_port': self.cr_lrp, 'mac': ['{} {} {}'.format(self.mac, self.ipv4, self.ipv6)], 'options': {'peer': 'fake-peer'}, 'datapath': 'fake-dp'}) self.evpn_driver._ensure_network_exposed(lrp, gateway) mock_expose_subnet.assert_called_once_with( self.ipv4, ['10.10.10.1'], {'ips': ['10.10.10.1/32'], 'provider_datapath': 'fake-prov-dp'}, self.bridge, self.vlan_tag, 'fake-dp') def test__get_bridge_for_datapath(self): self.sb_idl.get_network_name_and_tag.return_value = ( 'fake-network', [self.vlan_tag]) ret = self.evpn_driver._get_bridge_for_datapath('fake-dp') self.assertEqual((self.bridge, self.vlan_tag), ret) def test__get_bridge_for_datapath_no_tag(self): self.sb_idl.get_network_name_and_tag.return_value = ( 'fake-network', None) ret = self.evpn_driver._get_bridge_for_datapath('fake-dp') self.assertEqual((self.bridge, None), ret) def test__get_bridge_for_datapath_no_network_name(self): self.sb_idl.get_network_name_and_tag.return_value = (None, None) ret = self.evpn_driver._get_bridge_for_datapath('fake-dp') self.assertEqual((None, None), ret) @mock.patch.object(linux_net, 'add_ip_nei') @mock.patch.object(frr, 'vrf_reconfigure') def _test_expose_ip( self, mock_vrf_reconfigure, mock_add_ip_nei, cr_lrp=False): mock_get_bridge = mock.patch.object( self.evpn_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, self.vlan_tag) mock_ensure_evpn = mock.patch.object( self.evpn_driver, '_ensure_evpn_devices').start() mock_ensure_evpn.return_value = self.evpn_device mock_connect_evpn = mock.patch.object( self.evpn_driver, '_connect_evpn_to_ovn').start() mock_ensure_net_exposed = mock.patch.object( self.evpn_driver, '_ensure_network_exposed').start() row = fakes.create_object({ 'name': 'fake-row', 'logical_port': self.cr_lrp, 'mac': ['{} {} {}'.format(self.mac, self.ipv4, self.ipv6)], 'datapath': 'fake-dp'}) self.sb_idl.get_fip_associated.return_value = ( self.fip, 'fake-dp') lrp0 = fakes.create_object({'chassis': 'fake-chassis', 'options': {}}) lrp1 = fakes.create_object({'chassis': '', 'options': {}}) self.sb_idl.get_lrp_ports_for_router.return_value = [lrp0, lrp1] if not cr_lrp: self.sb_idl.get_port_if_local_chassis.return_value = row self.sb_idl.get_evpn_info.return_value = self.evpn_info else: self.sb_idl.get_evpn_info_from_port_name.return_value = ( self.evpn_info) self.evpn_driver.expose_ip(row, cr_lrp=cr_lrp) # Assertions mock_connect_evpn.assert_called_once_with( 'fake-vrf-name', 'fake-veth-vrf', 'fake-veth-ovs', [self.ipv4, self.ipv6], self.bridge, self.vni, 'fake-vlan-name', self.vlan_tag) mock_ensure_evpn.assert_called_once_with( self.bridge, self.vni, self.vlan_tag) mock_ensure_net_exposed.assert_called_once_with( lrp1, {'router_datapath': 'fake-dp', 'provider_datapath': 'fake-dp', 'ips': [self.ipv4, self.ipv6], 'mac': self.mac, 'vni': self.vni, 'bgp_as': 'fake-bgp-as', 'lo': 'fake-lo-name', 'bridge': self.bridge, 'vxlan': 'fake-vxlan-name', 'vrf': 'fake-vrf-name', 'veth_vrf': 'fake-veth-vrf', 'veth_ovs': 'fake-veth-ovs', 'vlan': 'fake-vlan-name'}) mock_vrf_reconfigure.assert_called_once_with( self.evpn_info, action='add-vrf') expected_calls = [mock.call(self.ipv4, self.mac, 'fake-vlan-name'), mock.call(self.ipv6, self.mac, 'fake-vlan-name')] mock_add_ip_nei.assert_has_calls(expected_calls) def test_expose_ip(self): self._test_expose_ip(cr_lrp=False) def test_expose_ip_cr_lrp(self): self._test_expose_ip(cr_lrp=True) @mock.patch.object(ovs, 'remove_evpn_router_ovs_flows') @mock.patch.object(frr, 'vrf_reconfigure') def _test_withdraw_ip( self, mock_vrf_reconfigure, mock_remove_evpn_flows, cr_lrp=True, ret_vlan_tag=True): mock_remove_evpn = mock.patch.object( self.evpn_driver, '_remove_evpn_devices').start() mock_disconnect_epvn = mock.patch.object( self.evpn_driver, '_disconnect_evpn_from_ovn').start() mock_get_bridge = mock.patch.object( self.evpn_driver, '_get_bridge_for_datapath').start() vlan_tag = self.vlan_tag if ret_vlan_tag else None mock_get_bridge.return_value = (self.bridge, vlan_tag) row = fakes.create_object({ 'name': 'fake-row', 'logical_port': self.cr_lrp}) self.evpn_driver.withdraw_ip(row, cr_lrp=cr_lrp) mock_remove_evpn_flows.assert_called_once_with( self.bridge, constants.OVS_VRF_RULE_COOKIE, self.mac) mock_vrf_reconfigure.assert_called_once_with( {'vni': self.vni, 'bgp_as': 'fake-bgp-as'}, action='del-vrf') kwargs = {} if ret_vlan_tag: kwargs.update({'vlan_tag': vlan_tag}) mock_disconnect_epvn.assert_called_once_with( self.vni, self.bridge, [self.fip], **kwargs) mock_remove_evpn.assert_called_once_with(self.vni) def test_withdraw_ip(self): self._test_withdraw_ip() def test_withdraw_ip_no_vlan_tag(self): self._test_withdraw_ip(ret_vlan_tag=False) @mock.patch.object(linux_net, 'add_ips_to_dev') def test_expose_remote_ip(self, mock_add_ip_dev): self.sb_idl.is_provider_network.return_value = False self.sb_idl.get_evpn_info_from_port_name.return_value = self.evpn_info lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.evpn_driver.ovn_local_lrps = {lrp: 'fake-lrp'} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.evpn_driver.expose_remote_ip(ips, row) lo_name = constants.OVN_EVPN_LO_PREFIX + str(self.vni) mock_add_ip_dev.assert_called_once_with( lo_name, ips, clear_local_route_at_table=self.vni) @mock.patch.object(linux_net, 'add_ips_to_dev') def test_expose_remote_ip_is_provider_network(self, mock_add_ip_dev): self.sb_idl.is_provider_network.return_value = True row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.evpn_driver.expose_remote_ip(ips, row) mock_add_ip_dev.assert_not_called() @mock.patch.object(linux_net, 'add_ips_to_dev') def test_expose_remote_ip_not_local(self, mock_add_ip_dev): self.sb_idl.is_provider_network.return_value = False lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.evpn_driver.ovn_local_lrps = {} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.evpn_driver.expose_remote_ip(ips, row) mock_add_ip_dev.assert_not_called() @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_remote_ip(self, mock_del_ip_dev): self.sb_idl.is_provider_network.return_value = False self.sb_idl.get_evpn_info_from_port_name.return_value = self.evpn_info lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.evpn_driver.ovn_local_lrps = {lrp: 'fake-lrp'} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.evpn_driver.withdraw_remote_ip(ips, row) lo_name = constants.OVN_EVPN_LO_PREFIX + str(self.vni) mock_del_ip_dev.assert_called_once_with(lo_name, ips) @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_remote_ip_is_provider_network(self, mock_del_ip_dev): self.sb_idl.is_provider_network.return_value = True row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.evpn_driver.withdraw_remote_ip(ips, row) mock_del_ip_dev.assert_not_called() @mock.patch.object(linux_net, 'del_ips_from_dev') def test_withdraw_remote_ip_not_local(self, mock_del_ip_dev): self.sb_idl.is_provider_network.return_value = False lrp = 'fake-lrp' self.sb_idl.get_lrps_for_datapath.return_value = [lrp] self.evpn_driver.ovn_local_lrps = {} row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp'}) ips = [self.ipv4, self.ipv6] self.evpn_driver.withdraw_remote_ip(ips, row) mock_del_ip_dev.assert_not_called() def test_expose_subnet(self): self.sb_idl.get_evpn_info.return_value = self.evpn_info self.sb_idl.get_ip_from_port_peer.return_value = self.ipv4 self.sb_idl.get_port_datapath.return_value = 'fake-dp' self.sb_idl.is_router_gateway_on_chassis.return_value = self.cr_lrp mock_get_bridge = mock.patch.object( self.evpn_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, self.vlan_tag) mock_expose_subnet = mock.patch.object( self.evpn_driver, '_expose_subnet').start() row = fakes.create_object({ 'name': 'fake-row', 'datapath': 'fake-dp', 'logical_port': self.cr_lrp}) self.evpn_driver.expose_subnet(row) mock_expose_subnet.assert_called_once_with( self.ipv4, [self.fip], self.evpn_driver.ovn_local_cr_lrps[self.cr_lrp], self.bridge, self.vlan_tag, 'fake-dp') @mock.patch.object(linux_net, 'add_ips_to_dev') @mock.patch.object(ovs, 'ensure_evpn_ovs_flow') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'get_ip_version') def _test__expose_subnet( self, mock_ip_version, mock_add_route, mock_ensure_evpn_flow, mock_add_ip_dev, use_ipv6=False): # IPv4 vs IPv6 mocks ip = self.ipv6 if use_ipv6 else self.ipv4 mock_ip_version.return_value = ( constants.IP_VERSION_6 if use_ipv6 else constants.IP_VERSION_4) cidr = '128' if use_ipv6 else '32' port0 = fakes.create_object({ 'name': 'fake-port0', 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_port': self.cr_lrp, 'chassis': 'fake-chassis', 'mac': ['{} {}'.format(self.mac, ip)], 'datapath': 'fake-dp'}) port1 = fakes.create_object({ 'name': 'fake-port1', 'chassis': 'fake-chassis', 'type': 'unknown-type'}) self.sb_idl.get_ports_on_datapath.return_value = [port0, port1] self.evpn_driver._expose_subnet( '{}/{}'.format(ip, cidr), [self.fip], self.evpn_driver.ovn_local_cr_lrps[self.cr_lrp], self.bridge, 10, 'fake-dp') mock_add_route.assert_called_once_with( mock.ANY, ip, self.vni, 'fake-vlan', mask=cidr, via=self.fip) mock_ensure_evpn_flow.assert_called_once_with( self.bridge, constants.OVS_VRF_RULE_COOKIE, self.mac, 'fake-vlan', 'fake-vlan', '{}/{}'.format(ip, cidr), strip_vlan=True) mock_add_ip_dev.assert_called_once_with( 'fake-lo', [ip], clear_local_route_at_table=self.vni) def test__expose_subnet(self): self._test__expose_subnet() def test__expose_subnet_ipv6(self): self._test__expose_subnet(use_ipv6=True) @mock.patch.object(linux_net, 'delete_exposed_ips') @mock.patch.object(linux_net, 'get_exposed_ips_on_network') @mock.patch.object(ovs, 'remove_evpn_network_ovs_flow') @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'get_ip_version') def _test_withdraw_subnet( self, mock_ip_version, mock_del_route, mock_remove_evpn_flows, mock_get_ips, mock_del_ips, use_ipv6=False): # IPv4 vs IPv6 mocks ip = self.ipv6 if use_ipv6 else self.ipv4 mock_ip_version.return_value = ( constants.IP_VERSION_6 if use_ipv6 else constants.IP_VERSION_4) cidr = '128' if use_ipv6 else '32' self.evpn_driver.ovn_local_lrps = { 'lrp-port': {'datapath': 'fake-dp', 'ip': '{}/{}'.format(ip, cidr)}} self.sb_idl.is_router_gateway_on_chassis.return_value = self.cr_lrp mock_get_bridge = mock.patch.object( self.evpn_driver, '_get_bridge_for_datapath').start() mock_get_bridge.return_value = (self.bridge, self.vlan_tag) net_ips = ['10.10.10.1', '2001:0db8:85a3:0000:0000:8a2e:0370:7334'] mock_get_ips.return_value = net_ips row = fakes.create_object({ 'name': 'fake-row', 'logical_port': 'port'}) self.evpn_driver.withdraw_subnet(row) mock_del_route.assert_called_once_with( mock.ANY, ip, self.vni, 'fake-vlan', mask=cidr, via=self.fip) mock_remove_evpn_flows.assert_called_once_with( self.bridge, constants.OVS_VRF_RULE_COOKIE, self.mac, '{}/{}'.format(ip, cidr)) mock_del_ips.assert_called_once_with(net_ips, 'fake-lo') def test_withdraw_subnet(self): self._test_withdraw_subnet() def test_withdraw_subnet_ipv6(self): self._test_withdraw_subnet(use_ipv6=True) @mock.patch.object(linux_net, 'ensure_veth') @mock.patch.object(linux_net, 'enable_proxy_ndp') @mock.patch.object(linux_net, 'set_device_status') @mock.patch.object(ovs, 'add_vlan_port_to_ovs_bridge') @mock.patch.object(linux_net, 'get_nic_ip') @mock.patch.object(linux_net, 'ensure_dummy_device') @mock.patch.object(linux_net, 'ensure_vxlan') @mock.patch.object(linux_net, 'set_master_for_device') @mock.patch.object(linux_net, 'ensure_bridge') @mock.patch.object(linux_net, 'ensure_vrf') def _test__ensure_evpn_devices( self, mock_ensure_vrf, mock_ensure_bridge, mock_set_master, mock_ensure_vxlan, mock_ensure_dummy, mock_get_nic_ip, mock_add_vlan, mock_set_status, mock_proxy_ndp, mock_ensure_veth, use_vlan=True): mock_get_nic_ip.return_value = [self.ipv4] dp_bridge = 'datapath-bridge' vlan_tag = self.vlan_tag if use_vlan else None self.evpn_driver._ensure_evpn_devices(dp_bridge, self.vni, vlan_tag) # Asserts vrf_name = constants.OVN_EVPN_VRF_PREFIX + str(self.vni) mock_ensure_vrf.assert_called_once_with(vrf_name, self.vni) bridge_name = constants.OVN_EVPN_BRIDGE_PREFIX + str(self.vni) mock_ensure_bridge.assert_called_once_with(bridge_name) vxlan_name = constants.OVN_EVPN_VXLAN_PREFIX + str(self.vni) mock_ensure_vxlan.assert_called_once_with( vxlan_name, self.vni, self.ipv4, CONF.evpn_udp_dstport) lo_name = constants.OVN_EVPN_LO_PREFIX + str(self.vni) mock_ensure_dummy.assert_called_once_with(lo_name) set_master_expected_calls = [ mock.call(bridge_name, vrf_name), mock.call(vxlan_name, bridge_name), mock.call(lo_name, vrf_name)] if use_vlan: vlan_name = constants.OVN_EVPN_VLAN_PREFIX + str(self.vni) mock_add_vlan.assert_called_once_with( dp_bridge, vlan_name, self.vlan_tag) mock_set_status.assert_called_once_with( vlan_name, constants.LINK_UP) mock_proxy_ndp.assert_called_once_with(vlan_name) set_master_expected_calls.append(mock.call(vlan_name, vrf_name)) else: veth_vrf = constants.OVN_EVPN_VETH_VRF_PREFIX + str(self.vni) veth_ovs = constants.OVN_EVPN_VETH_OVS_PREFIX + str(self.vni) mock_ensure_veth.assert_called_once_with(veth_vrf, veth_ovs) set_master_expected_calls.append(mock.call(veth_vrf, vrf_name)) mock_set_master.assert_has_calls(set_master_expected_calls) def test__ensure_evpn_devices(self): self._test__ensure_evpn_devices() def test__ensure_evpn_devices_not_vlan(self): self._test__ensure_evpn_devices(use_vlan=False) @mock.patch.object(linux_net, 'delete_device') def test__remove_evpn_devices(self, mock_del_device): vrf_name = constants.OVN_EVPN_VRF_PREFIX + str(self.vni) bridge_name = constants.OVN_EVPN_BRIDGE_PREFIX + str(self.vni) vxlan_name = constants.OVN_EVPN_VXLAN_PREFIX + str(self.vni) lo_name = constants.OVN_EVPN_LO_PREFIX + str(self.vni) veth_name = constants.OVN_EVPN_VETH_VRF_PREFIX + str(self.vni) vlan_name = constants.OVN_EVPN_VLAN_PREFIX + str(self.vni) self.evpn_driver._remove_evpn_devices(self.vni) expected_calls = [mock.call(lo_name), mock.call(vrf_name), mock.call(bridge_name), mock.call(vxlan_name), mock.call(veth_name), mock.call(vlan_name)] mock_del_device.assert_has_calls(expected_calls) @mock.patch.object(linux_net, 'add_unreachable_route') @mock.patch.object(linux_net, 'add_ndp_proxy') @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(ovs, 'add_device_to_ovs_bridge') def _test__connect_evpn_to_ovn( self, mock_add_ovs_bridge, mock_add_route, mock_ip_version, mock_add_ndp_proxy, mock_add_unreachable_route, use_vlan=True): mock_ip_version.side_effect = (constants.IP_VERSION_4, constants.IP_VERSION_6) vrf = 'fake-vrf' veth_vrf = 'fake-veth-vrf' veth_ovs = 'fake-veth-ovs' ips = [self.ipv4, self.ipv6] dp_bridge = 'datapath-bridge' vlan = 'fake-vlan' vlan_tag = self.vlan_tag if use_vlan else None self.evpn_driver._connect_evpn_to_ovn( vrf, veth_vrf, veth_ovs, ips, dp_bridge, self.vni, vlan, vlan_tag) mock_add_unreachable_route.assert_called_once_with(vrf) if not use_vlan: mock_add_ndp_proxy.assert_called_once_with(self.ipv6, dp_bridge) mock_add_ovs_bridge.assert_called_once_with(veth_ovs, dp_bridge) add_route_expected_calls = [ mock.call(mock.ANY, self.ipv4, self.vni, veth_vrf), mock.call(mock.ANY, self.ipv6, self.vni, veth_vrf)] else: mock_add_ndp_proxy.assert_called_once_with(self.ipv6, vlan) add_route_expected_calls = [ mock.call(mock.ANY, self.ipv4, self.vni, vlan), mock.call(mock.ANY, self.ipv6, self.vni, vlan)] mock_add_route.assert_has_calls(add_route_expected_calls) def test__connect_evpn_to_ovn(self): self._test__connect_evpn_to_ovn() def test__connect_evpn_to_ovn_not_vlan(self): self._test__connect_evpn_to_ovn(use_vlan=False) @mock.patch.object(linux_net, 'del_ndp_proxy') @mock.patch.object(linux_net, 'delete_routes_from_table') @mock.patch.object(ovs, 'del_device_from_ovs_bridge') @mock.patch.object(linux_net, 'get_ip_version') def _test_disconnect_evpn_from_ovn( self, mock_ip_version, mock_del_device, mock_delete_routes, mock_del_ndp, use_vlan=True, clean_ndp=True): mock_ip_version.side_effect = (constants.IP_VERSION_4, constants.IP_VERSION_6) dp_bridge = 'datapath-bridge' ips = [self.ipv4, self.ipv6] vlan_tag = self.vlan_tag if use_vlan else None self.evpn_driver._disconnect_evpn_from_ovn( self.vni, dp_bridge, ips, vlan_tag=vlan_tag, cleanup_ndp_proxy=clean_ndp) # Assertions device = constants.OVN_EVPN_VETH_OVS_PREFIX + str(self.vni) if use_vlan: device = constants.OVN_EVPN_VLAN_PREFIX + str(self.vni) mock_delete_routes.assert_called_once_with(self.vni) mock_del_device.assert_called_once_with(device, dp_bridge) if clean_ndp: mock_del_ndp.assert_called_once_with(self.ipv6, dp_bridge) else: mock_del_ndp.assert_not_called() def test_disconnect_evpn_from_ovn(self): self._test_disconnect_evpn_from_ovn() def test_disconnect_evpn_from_ovn_dont_clean_ndp(self): self._test_disconnect_evpn_from_ovn(clean_ndp=False) def test_disconnect_evpn_from_ovn_not_vlan(self): self._test_disconnect_evpn_from_ovn(use_vlan=False) @mock.patch.object(ovs, 'del_device_from_ovs_bridge') @mock.patch.object(linux_net, 'delete_device') @mock.patch.object(linux_net, 'get_interfaces') def test__remove_extra_vrfs( self, mock_get_ifaces, mock_del_device, mock_del_device_bridge): # NOTE(lucasagomes): Remove cr_lrp1 to simplify the test self.evpn_driver.ovn_local_cr_lrps.pop(self.cr_lrp1, None) mock_get_ifaces.return_value = [ '%siface' % type_ for type_ in ( constants.OVN_EVPN_VRF_PREFIX, constants.OVN_EVPN_LO_PREFIX, constants.OVN_EVPN_BRIDGE_PREFIX, constants.OVN_EVPN_VXLAN_PREFIX, constants.OVN_EVPN_VETH_VRF_PREFIX, constants.OVN_EVPN_VLAN_PREFIX)] self.evpn_driver._remove_extra_vrfs() # Assertions expected_calls = [mock.call('vrf-iface'), mock.call('lo-iface'), mock.call('br-iface'), mock.call('vxlan-iface'), mock.call('veth-vrf-iface')] mock_del_device.assert_has_calls(expected_calls) expected_calls = [mock.call('veth-vrf-iface'), mock.call('vlan-iface')] mock_del_device_bridge.assert_has_calls(expected_calls) @mock.patch.object(linux_net, 'get_interface_index') @mock.patch.object(linux_net, 'delete_ip_routes') @mock.patch.object(linux_net, 'get_routes_on_tables') def test__remove_extra_routes( self, mock_get_routes, mock_del_ip_routes, mock_index): mock_index.return_value = 'fake-oif' mock_table_ids = mock.patch.object( self.evpn_driver, '_get_table_ids').start() mock_table_ids.return_value = ['fake-table-id'] route_to_del = { 'oif': 'fake-oif0', 'gateway': 'fake-gateway0', 'dst': 'fake-dst0', 'dst_len': 'fake-dst-len0', 'table': 'fake-table0'} mock_get_routes.return_value = [ self.evpn_driver._ovn_routing_tables_routes[ 'fake-vlan'][0]['route'], route_to_del] self.evpn_driver._remove_extra_routes() # Assert the route meant to be deleted was deleted mock_del_ip_routes.assert_called_once_with([route_to_del]) @mock.patch.object(ovs, 'get_flow_info') @mock.patch.object(ovs, 'get_bridge_flows') @mock.patch.object(ovs, 'del_flow') def test_remove_extra_ovs_flows_mac( self, mock_del_flow, mock_get_flows, mock_flow_info): mock_flow_info.return_value = {'mac': 'aa:aa:aa:aa:aa:aa'} mock_get_flows.return_value = ['fake-flow0', 'fake-flow1'] self.evpn_driver._remove_extra_ovs_flows() expected_calls = [mock.call('fake-flow0', self.bridge, constants.OVS_VRF_RULE_COOKIE), mock.call('fake-flow1', self.bridge, constants.OVS_VRF_RULE_COOKIE)] mock_del_flow.assert_has_calls(expected_calls) @mock.patch.object(ovs, 'get_flow_info') @mock.patch.object(ovs, 'get_bridge_flows') @mock.patch.object(ovs, 'del_flow') def test_remove_extra_ovs_flows_port( self, mock_del_flow, mock_get_flows, mock_flow_info): mock_flow_info.return_value = { 'mac': self.mac, 'port': 'fake-port', } mock_get_flows.return_value = ['fake-flow0', 'fake-flow1'] self.evpn_driver._remove_extra_ovs_flows() expected_calls = [mock.call('fake-flow0', self.bridge, constants.OVS_VRF_RULE_COOKIE), mock.call('fake-flow1', self.bridge, constants.OVS_VRF_RULE_COOKIE)] mock_del_flow.assert_has_calls(expected_calls) @mock.patch.object(ovs, 'get_device_port_at_ovs') @mock.patch.object(ovs, 'get_flow_info') @mock.patch.object(ovs, 'get_bridge_flows') @mock.patch.object(ovs, 'del_flow') def test_remove_extra_ovs_flows_port_nw_src( self, mock_del_flow, mock_get_flows, mock_flow_info, mock_get_port_ovs): mock_get_port_ovs.return_value = 'fake-ovs-port' mock_flow_info.return_value = { 'mac': self.mac, 'port': 'fake-port', 'nw_src': '10.10.1.88/32', } mock_get_flows.return_value = ['fake-flow0', 'fake-flow1'] self.evpn_driver._remove_extra_ovs_flows() expected_calls = [mock.call('fake-flow0', self.bridge, constants.OVS_VRF_RULE_COOKIE), mock.call('fake-flow1', self.bridge, constants.OVS_VRF_RULE_COOKIE)] mock_del_flow.assert_has_calls(expected_calls) @mock.patch.object(ovs, 'get_flow_info') @mock.patch.object(ovs, 'get_bridge_flows') @mock.patch.object(ovs, 'del_flow') def test_remove_extra_ovs_flows( self, mock_del_flow, mock_get_flows, mock_flow_info): mock_flow_info.return_value = {} mock_get_flows.return_value = ['fake-flow0', 'fake-flow1'] self.evpn_driver._remove_extra_ovs_flows() expected_calls = [mock.call('fake-flow0', self.bridge, constants.OVS_VRF_RULE_COOKIE), mock.call('fake-flow1', self.bridge, constants.OVS_VRF_RULE_COOKIE)] mock_del_flow.assert_has_calls(expected_calls) @mock.patch.object(linux_net, 'del_ips_from_dev') @mock.patch.object(linux_net, 'get_exposed_ips') def test__remove_extra_exposed_ips(self, mock_get_ips, mock_del_ips): self.evpn_driver._ovn_exposed_evpn_ips = { 'fake-lo': [self.ipv4, self.ipv6]} another_ip = '10.10.1.76' mock_get_ips.return_value = [another_ip] self.evpn_driver._remove_extra_exposed_ips() mock_del_ips.assert_called_once_with('fake-lo', [another_ip]) def test__get_table_ids(self): ret = self.evpn_driver._get_table_ids() self.assertEqual([self.vni, self.vni1], ret) def test_get_cr_lrp_mac_mapping(self): ret = self.evpn_driver._get_cr_lrp_mac_mapping() expected_ret = { self.mac: { 'veth_ovs': 'fake-veth-ovs', 'veth_vrf': 'fake-veth-vrf', 'vlan': 'fake-vlan'}, self.mac1: { 'veth_ovs': 'fake-veth-ovs1', 'veth_vrf': 'fake-veth-vrf1', 'vlan': 'fake-vlan1'} } self.assertEqual(expected_ret, ret) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/test_ovn_stretched_l2_bgp_driver.py000066400000000000000000001224061460327367600336570ustar00rootroot00000000000000# Copyright 2022 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_config import cfg from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack import ovn_stretched_l2_bgp_driver 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 ovn from ovn_bgp_agent.drivers.openstack.utils import ovs from ovn_bgp_agent import exceptions as agent_exc from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.tests.unit import fakes from ovn_bgp_agent.utils import linux_net import ipaddress CONF = cfg.CONF class TestHashedRoute(test_base.TestCase): def setUp(self): super(TestHashedRoute, self).setUp() self.table = set() self.route = ovn_stretched_l2_bgp_driver.HashedRoute( "192.168.0.0", 24, "192.168.1.1") self.invalid_route = ovn_stretched_l2_bgp_driver.HashedRoute( "192.168.0.0", 24, "192.168.1.2") self.table.add(self.route) def test_lookup(self): self.assertTrue(self.route in self.table) self.assertFalse(self.invalid_route in self.table) def test_delete(self): self.table.remove(self.route) self.assertEqual(0, len(self.table)) class TestOVNBGPStretchedL2Driver(test_base.TestCase): def setUp(self): super(TestOVNBGPStretchedL2Driver, self).setUp() CONF.set_override( "address_scopes", "11111111-1111-1111-1111-11111111,22222222-2222-2222-2222-22222222", # NOQA E501 ) self.bgp_driver = ovn_stretched_l2_bgp_driver.OVNBGPStretchedL2Driver() self.bgp_driver._post_fork_event = mock.Mock() self.bgp_driver.sb_idl = mock.Mock() self.sb_idl = self.bgp_driver.sb_idl self.bgp_driver.chassis = "fake-chassis" # self.bgp_driver.ovn_routing_tables = {self.bridge: 'fake-table'} # self.bgp_driver.ovn_bridge_mappings = {'fake-network': self.bridge} self.mock_sbdb = mock.patch.object(ovn, "OvnSbIdl").start() self.mock_ovs_idl = mock.patch.object(ovs, "OvsIdl").start() self.ipv4 = "192.168.1.17" self.ipv6 = "2002::1234:abcd:ffff:c0a8:101" self.fip = "172.24.4.33" self.mac = "aa:bb:cc:dd:ee:ff" self.bgp_driver.ovs_idl = self.mock_ovs_idl self.test_route_ipv4 = ovn_stretched_l2_bgp_driver.HashedRoute( network="192.168.1.0", prefix_len=24, dst="10.0.0.1", ) self.test_route_ipv6 = ovn_stretched_l2_bgp_driver.HashedRoute( network="fdcc:8cf2:d40c:2::", prefix_len=64, dst="fd51:f4b3:872:eda::1", ) self.addr_scopev4 = "11111111-1111-1111-1111-11111111" self.addr_scopev6 = "22222222-2222-2222-2222-22222222" self.addr_scope = { constants.IP_VERSION_4: self.addr_scopev4, constants.IP_VERSION_6: self.addr_scopev6, } self.addr_scope_external_ids = { "neutron:subnet_pool_addr_scope4": self.addr_scopev4, "neutron:subnet_pool_addr_scope6": self.addr_scopev6, } self.cr_lrp0 = mock.Mock() self.cr_lrp0.mac = [ "ff:ff:ff:ff:ff:00 10.0.0.1/24 fd51:f4b3:872:eda::1/64" ] self.cr_lrp0.datapath = "fake-router-dp" self.cr_lrp0.type = constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE self.cr_lrp0.logical_port = "cr-lrp-fake-port" self.lp0 = mock.Mock() self.lp0.external_ids = self.addr_scope_external_ids self.router_port = fakes.create_object( { "name": "fake-router-port", "mac": [ "ff:ff:ff:ff:ff:01 192.168.1.1/24 fdcc:8cf2:d40c:2::1/64" ], "logical_port": "lrp-fake-logical-port", } ) self.fake_patch_port = fakes.create_object( { "name": "fake-patch-port", "mac": [ "ff:ff:ff:ff:ff:01 192.168.1.1/24 fdcc:8cf2:d40c:2::1/64" ], "external_ids": self.addr_scope_external_ids, "logical_port": "fake-port", } ) # Mock pyroute2.NDB context manager object self.mock_ndb = mock.patch.object(linux_net.pyroute2, "NDB").start() self.fake_ndb = self.mock_ndb().__enter__() @mock.patch.object(linux_net, "ensure_vrf") @mock.patch.object(linux_net, "ensure_ovn_device") @mock.patch.object(linux_net, "delete_routes_from_table") @mock.patch.object(frr, "vrf_leak") def test_start(self, mock_vrf, mock_delete_routes, mock_ensure_ovn_device, *args): CONF.set_override("clear_vrf_routes_on_startup", True) mock_redistribute = mock.patch.object( frr, "set_default_redistribute" ).start() self.bgp_driver.start() mock_redistribute.assert_called_with(['kernel']) mock_vrf.assert_called_once_with( CONF.bgp_vrf, CONF.bgp_AS, CONF.bgp_router_id, template=frr.LEAK_VRF_TEMPLATE, ) # Assert connections were started self.mock_ovs_idl().start.assert_called_once_with( CONF.ovsdb_connection ) self.mock_sbdb().start.assert_called_once_with() mock_delete_routes.assert_called_once_with(CONF.bgp_vrf_table_id) mock_ensure_ovn_device.assert_called_once_with( CONF.bgp_nic, CONF.bgp_vrf) @mock.patch.object(linux_net, "ensure_vrf") @mock.patch.object(linux_net, "ensure_ovn_device") @mock.patch.object(linux_net, "delete_routes_from_table") @mock.patch.object(frr, "vrf_leak") def test_start_clear_routes( self, mock_vrf, mock_delete_routes, mock_ensure_ovn_device, *args): CONF.set_override("clear_vrf_routes_on_startup", False) mock_redistribute = mock.patch.object( frr, "set_default_redistribute" ).start() self.bgp_driver.start() mock_redistribute.assert_called_with(['kernel']) mock_vrf.assert_called_once_with( CONF.bgp_vrf, CONF.bgp_AS, CONF.bgp_router_id, template=frr.LEAK_VRF_TEMPLATE, ) # Assert connections were started self.mock_ovs_idl().start.assert_called_once_with( CONF.ovsdb_connection ) self.mock_sbdb().start.assert_called_once_with() mock_delete_routes.assert_not_called() mock_ensure_ovn_device.assert_called_once_with( CONF.bgp_nic, CONF.bgp_vrf) @mock.patch.object(linux_net, "add_ip_route") def test__add_route(self, mock_add_route): for test_route in [self.test_route_ipv4, self.test_route_ipv6]: self.bgp_driver._add_route( test_route.network, test_route.prefix_len, test_route.dst, ) mock_add_route.assert_called_with( mock.ANY, test_route.network, CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=test_route.prefix_len, via=test_route.dst, ) self.assertTrue(test_route in self.bgp_driver.vrf_routes) @mock.patch.object(linux_net, "del_ip_route") def test__del_route(self, mock_del_route): self.bgp_driver.vrf_routes.add(self.test_route_ipv4) self.bgp_driver.vrf_routes.add(self.test_route_ipv6) for test_route in [self.test_route_ipv4, self.test_route_ipv6]: self.bgp_driver._del_route( test_route.network, test_route.prefix_len, test_route.dst, ) mock_del_route.assert_called_with( mock.ANY, test_route.network, CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=test_route.prefix_len, via=test_route.dst, ) self.assertTrue(test_route not in self.bgp_driver.vrf_routes) def test__address_scope_allowed(self): test_scope2 = { constants.IP_VERSION_4: self.addr_scopev4, constants.IP_VERSION_6: "33333333-3333-3333-3333-33333333", } self.assertTrue( self.bgp_driver._address_scope_allowed( self.addr_scope, test_scope2, constants.IP_VERSION_4, ) ) self.assertFalse( self.bgp_driver._address_scope_allowed( self.addr_scope, test_scope2, constants.IP_VERSION_6, ) ) def test__address_scope_not_allowed_scope(self): test_scope1 = { constants.IP_VERSION_4: "33333333-3333-3333-3333-33333333", constants.IP_VERSION_6: "33333333-3333-3333-3333-33333333", } test_scope2 = { constants.IP_VERSION_4: "33333333-3333-3333-3333-33333333", constants.IP_VERSION_6: "33333333-3333-3333-3333-33333333", } self.assertFalse( self.bgp_driver._address_scope_allowed( test_scope1, test_scope2, constants.IP_VERSION_4, ) ) self.assertFalse( self.bgp_driver._address_scope_allowed( test_scope1, test_scope2, constants.IP_VERSION_6, ) ) def test__address_scope_allowed_no_scope(self): self.bgp_driver.allowed_address_scopes = set() test_scope2 = { constants.IP_VERSION_4: None, constants.IP_VERSION_6: None, } self.assertTrue( self.bgp_driver._address_scope_allowed( self.addr_scope, test_scope2, constants.IP_VERSION_4, ) ) self.assertTrue( self.bgp_driver._address_scope_allowed( self.addr_scope, test_scope2, constants.IP_VERSION_6, ) ) def test__address_scope_allowed_no_match(self): test_scope2 = { constants.IP_VERSION_4: None, constants.IP_VERSION_6: "44444444-4444-4444-4444-44444444", } self.assertFalse( self.bgp_driver._address_scope_allowed( self.addr_scope, test_scope2, constants.IP_VERSION_4, ) ) self.assertFalse( self.bgp_driver._address_scope_allowed( self.addr_scope, test_scope2, constants.IP_VERSION_6, ) ) def test_expose_subnet(self): mock__ensure_network_exposed = mock.patch.object( self.bgp_driver, "_ensure_network_exposed" ).start() self.sb_idl.is_router_gateway_on_any_chassis.return_value = ( self.cr_lrp0 ) row = mock.Mock() row.datapath = "fake-dp" self.bgp_driver.expose_subnet(None, row) self.sb_idl.is_router_gateway_on_any_chassis.assert_called_once_with( row.datapath ) mock__ensure_network_exposed.assert_called_once_with( row, self.cr_lrp0.logical_port ) def test_expose_subnet_no_gateway_port(self): mock__ensure_network_exposed = mock.patch.object( self.bgp_driver, "_ensure_network_exposed" ).start() self.sb_idl.is_router_gateway_on_any_chassis.return_value = None row = mock.Mock() row.datapath = "fake-dp" self.bgp_driver.expose_subnet(None, row) self.sb_idl.is_router_gateway_on_any_chassis.assert_called_once_with( row.datapath ) mock__ensure_network_exposed.assert_not_called() def test_expose_subnet_no_datapath(self): mock__ensure_network_exposed = mock.patch.object( self.bgp_driver, "_ensure_network_exposed" ).start() row = mock.Mock() row.datapath = "fake-dp" self.sb_idl.is_router_gateway_on_any_chassis.side_effect = ( agent_exc.DatapathNotFound(datapath=row.datapath)) self.bgp_driver.expose_subnet(None, row) self.sb_idl.is_router_gateway_on_any_chassis.assert_called_once_with( row.datapath ) mock__ensure_network_exposed.assert_not_called() def test_update_subnet(self): mock__update_network = mock.patch.object( self.bgp_driver, "_update_network" ).start() self.sb_idl.is_router_gateway_on_any_chassis.return_value = ( self.cr_lrp0 ) old = mock.Mock() old.mac = ["ff:ff:ff:ff:ff:01 1.1.1.1/24 2.2.2.2/24"] row = mock.Mock() row.datapath = "fake-dp" row.mac = ["ff:ff:ff:ff:ff:01 2.2.2.2/24 3.3.3.3/24"] self.bgp_driver.update_subnet(old, row) self.sb_idl.is_router_gateway_on_any_chassis.assert_called_once_with( row.datapath ) mock__update_network.assert_called_once_with( row, self.cr_lrp0.logical_port, ["3.3.3.3/24"], ["1.1.1.1/24"] ) def test_update_subnet_no_datapath(self): mock__update_network = mock.patch.object( self.bgp_driver, "_update_network" ).start() self.sb_idl.is_router_gateway_on_any_chassis.return_value = ( self.cr_lrp0 ) old = mock.Mock() old.mac = ["ff:ff:ff:ff:ff:01 1.1.1.1/24 2.2.2.2/24"] row = mock.Mock() row.datapath = "fake-dp" row.mac = ["ff:ff:ff:ff:ff:01 2.2.2.2/24 3.3.3.3/24"] self.sb_idl.is_router_gateway_on_any_chassis.side_effect = ( agent_exc.DatapathNotFound(datapath=row.datapath)) self.bgp_driver.update_subnet(old, row) self.sb_idl.is_router_gateway_on_any_chassis.assert_called_once_with( row.datapath ) mock__update_network.assert_not_called() @mock.patch.object(linux_net, "get_exposed_routes_on_network") @mock.patch.object(linux_net, "del_ip_route") @mock.patch.object(linux_net, "add_ip_route") def test__update_network( self, mock_add_ip_route, mock_del_ip_route, mock_get_exposed_routes_on_network, ): gateway = {} gateway["ips"] = [ ipaddress.ip_interface(ip) for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] ] gateway["address_scopes"] = self.addr_scope gateway["lrp_ports"] = set() self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} self.router_lrp = mock.Mock() self.router_lrp.mac = [ "ff:ff:ff:ff:ff:01 192.168.1.1/24 fdcc:8cf2:d40c:2::1/64" ] add_ips = ["192.168.1.1/24", "fdcc:8cf2:d40c:2::1/64"] delete_ips = ["192.168.0.1/24"] mock_get_exposed_routes_on_network.side_effect = ( ["route-v4"], ["route-v6"], ) self.sb_idl.get_port_by_name.return_value = self.fake_patch_port self.bgp_driver._update_network( self.router_port, "gateway_port", add_ips, delete_ips ) self.sb_idl.get_port_by_name.assert_called_once_with( "fake-logical-port" ) expected_calls = [ mock.call( mock.ANY, "10.0.0.0", CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=26, via=None, ), mock.call( mock.ANY, "192.168.1.0", CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=24, via="10.0.0.10", ), mock.call( mock.ANY, "fd51:f4b3:872:eda::", CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=64, via=None, ), mock.call( mock.ANY, "fdcc:8cf2:d40c:2::", CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=64, via="fd51:f4b3:872:eda::10", ), ] mock_add_ip_route.assert_has_calls(expected_calls) mock_del_ip_route.assert_called_once_with( mock.ANY, "192.168.0.0", CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=24, via="10.0.0.10", ) self.assertDictEqual( self.bgp_driver.propagated_lrp_ports, { self.router_port.logical_port: { "cr_lrp": "gateway_port", "subnets": { "fdcc:8cf2:d40c:2::/64", "192.168.1.0/24" } } } ) @mock.patch.object(linux_net, "get_exposed_routes_on_network") @mock.patch.object(linux_net, "del_ip_route") @mock.patch.object(linux_net, "add_ip_route") def test__update_network_no_gateway( self, mock_add_ip_route, mock_del_ip_route, mock_get_exposed_routes_on_network, ): self.bgp_driver.ovn_local_cr_lrps = {} self.router_lrp = mock.Mock() self.router_lrp.mac = [ "ff:ff:ff:ff:ff:01 192.168.1.1/24 fdcc:8cf2:d40c:2::1/64" ] add_ips = ["192.168.1.1/24", "fdcc:8cf2:d40c:2::1/64"] delete_ips = ["192.168.0.1/24"] self.bgp_driver._update_network( self.router_port, "gateway_port", add_ips, delete_ips ) mock_get_exposed_routes_on_network.assert_not_called() mock_del_ip_route.assert_not_called() mock_add_ip_route.assert_not_called() self.sb_idl.get_port_by_name.assert_not_called() @mock.patch.object(linux_net, "get_exposed_routes_on_network") @mock.patch.object(linux_net, "del_ip_route") @mock.patch.object(linux_net, "add_ip_route") def test__update_network_no_mac( self, mock_add_ip_route, mock_del_ip_route, mock_get_exposed_routes_on_network, ): gateway = {} gateway["ips"] = [ ipaddress.ip_interface(ip) for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] ] gateway["address_scopes"] = self.addr_scope self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} self.router_port.mac = [] add_ips = ["192.168.1.1/24", "fdcc:8cf2:d40c:2::1/64"] delete_ips = ["192.168.0.1/24"] self.bgp_driver._update_network( self.router_port, "gateway_port", add_ips, delete_ips ) mock_get_exposed_routes_on_network.assert_not_called() mock_del_ip_route.assert_not_called() mock_add_ip_route.assert_not_called() self.sb_idl.get_port_by_name.assert_not_called() def test_withdraw_subnet(self): mock__withdraw_subnet = mock.patch.object( self.bgp_driver, "_withdraw_subnet" ).start() row = mock.Mock() row.datapath = "fake-dp" row.logical_port = "fake-lport" port_info = { "cr_lrp": self.cr_lrp0.logical_port, "subnets": { "fdcc:8cf2:d40c:2::/64", "192.168.1.0/24" } } self.bgp_driver.propagated_lrp_ports = { row.logical_port: port_info, "another_lrp_port": {} } self.bgp_driver.ovn_local_cr_lrps = { self.cr_lrp0.logical_port: { "lrp_ports": {row.logical_port, "another_lrp_port"} } } self.bgp_driver.withdraw_subnet(None, row) mock__withdraw_subnet.assert_called_once_with( port_info, self.cr_lrp0.logical_port ) self.assertDictEqual( self.bgp_driver.propagated_lrp_ports, { "another_lrp_port": {} } ) self.assertDictEqual( self.bgp_driver.ovn_local_cr_lrps, { self.cr_lrp0.logical_port: { "lrp_ports": {"another_lrp_port"} } } ) @mock.patch.object(linux_net, "add_ip_route") def test__ensure_network_exposed(self, mock_add_ip_route): gateway = {} gateway["ips"] = [ ipaddress.ip_interface(ip) for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] ] gateway["address_scopes"] = self.addr_scope gateway["lrp_ports"] = set() self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} self.router_lrp = mock.Mock() self.router_lrp.mac = [ "ff:ff:ff:ff:ff:01 192.168.1.1/24 fdcc:8cf2:d40c:2::1/64" ] self.sb_idl.get_port_by_name.return_value = self.fake_patch_port self.bgp_driver._ensure_network_exposed( self.router_port, "gateway_port" ) self.sb_idl.get_port_by_name.assert_called_once_with( "fake-logical-port" ) expected_calls = [ mock.call( mock.ANY, "10.0.0.0", CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=26, via=None, ), mock.call( mock.ANY, "192.168.1.0", CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=24, via="10.0.0.10", ), mock.call( mock.ANY, "fd51:f4b3:872:eda::", CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=64, via=None, ), mock.call( mock.ANY, "fdcc:8cf2:d40c:2::", CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=64, via="fd51:f4b3:872:eda::10", ), ] mock_add_ip_route.assert_has_calls(expected_calls) self.assertDictEqual( self.bgp_driver.ovn_local_cr_lrps, { 'gateway_port': { 'address_scopes': { 4: '11111111-1111-1111-1111-11111111', 6: '22222222-2222-2222-2222-22222222'}, 'ips': [ ipaddress.IPv4Interface('10.0.0.10/26'), ipaddress.IPv6Interface('fd51:f4b3:872:eda::10/64')], 'lrp_ports': {'lrp-fake-logical-port'} } } ) self.assertDictEqual( self.bgp_driver.propagated_lrp_ports, { "lrp-fake-logical-port": { 'cr_lrp': 'gateway_port', 'subnets': {'192.168.1.0/24', 'fdcc:8cf2:d40c:2::/64'} } } ) @mock.patch.object(linux_net, "add_ip_route") def test__ensure_network_exposed_invalid_addr_scopes( self, mock_add_ip_route ): gateway = {} gateway["ips"] = [ ipaddress.ip_interface(ip) for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] ] # Both of them are valid but none of them matches to the correct # IP version gateway["address_scopes"] = { constants.IP_VERSION_4: self.addr_scopev6, constants.IP_VERSION_6: self.addr_scopev4, } gateway["lrp_ports"] = set() self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} self.router_lrp = mock.Mock() self.router_lrp.mac = [ "ff:ff:ff:ff:ff:01 192.168.1.1/24 fdcc:8cf2:d40c:2::1/64" ] self.sb_idl.get_port_by_name.return_value = self.fake_patch_port self.bgp_driver._ensure_network_exposed( self.router_port, "gateway_port" ) self.sb_idl.get_port_by_name.assert_called_once_with( "fake-logical-port" ) mock_add_ip_route.assert_not_called() self.assertDictEqual( self.bgp_driver.ovn_local_cr_lrps, { 'gateway_port': { 'address_scopes': { 4: '22222222-2222-2222-2222-22222222', 6: '11111111-1111-1111-1111-11111111'}, 'ips': [ ipaddress.IPv4Interface('10.0.0.10/26'), ipaddress.IPv6Interface('fd51:f4b3:872:eda::10/64')], 'lrp_ports': set() } } ) self.assertDictEqual( self.bgp_driver.propagated_lrp_ports, {} ) @mock.patch.object(linux_net, "add_ip_route") def test__ensure_network_exposed_no_gateway(self, mock_add_ip_route): self.bgp_driver.ovn_local_cr_lrps = {} self.bgp_driver._ensure_network_exposed( self.router_port, "gateway_port" ) self.sb_idl.get_port_by_name.assert_not_called() mock_add_ip_route.assert_not_called() self.assertDictEqual( self.bgp_driver.propagated_lrp_ports, {} ) @mock.patch.object(linux_net, "add_ip_route") def test__ensure_network_exposed_duplicate_ip(self, mock_add_ip_route): gateway = {} gateway["ips"] = [ ipaddress.ip_interface(ip) for ip in ["192.168.1.1/24", "fd51:f4b3:872:eda::10/64"] ] gateway["address_scopes"] = self.addr_scope self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} self.bgp_driver._ensure_network_exposed( self.router_port, "gateway_port" ) self.sb_idl.get_port_by_name.assert_not_called() mock_add_ip_route.assert_not_called() self.assertDictEqual( self.bgp_driver.propagated_lrp_ports, {} ) @mock.patch.object(driver_utils, "get_addr_scopes") @mock.patch.object(linux_net, "add_ip_route") def test__ensure_network_exposed_port_not_existing(self, mock_add_ip_route, mock_addr_scopes): gateway = {} gateway["ips"] = [ ipaddress.ip_interface(ip) for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] ] gateway["address_scopes"] = self.addr_scope self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} self.sb_idl.get_port_by_name.return_value = [] self.bgp_driver._ensure_network_exposed( self.router_port, "gateway_port" ) mock_addr_scopes.assert_not_called() mock_add_ip_route.assert_not_called() self.assertDictEqual( self.bgp_driver.propagated_lrp_ports, {} ) @mock.patch.object(linux_net, "add_ip_route") def test__ensure_network_exposed_port_addr_scope_no_match( self, mock_add_ip_route ): gateway = {} gateway["ips"] = [ ipaddress.ip_interface(ip) for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] ] gateway["address_scopes"] = { constants.IP_VERSION_4: "address_scope_v4", constants.IP_VERSION_6: "address_scope_v6", } self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} self.sb_idl.get_port_by_name.return_value = self.fake_patch_port self.bgp_driver._ensure_network_exposed( self.router_port, "gateway_port" ) self.sb_idl.get_port_by_name.assert_called_once_with( "fake-logical-port" ) mock_add_ip_route.assert_not_called() self.assertDictEqual( self.bgp_driver.propagated_lrp_ports, {} ) @mock.patch.object(linux_net, "get_exposed_routes_on_network") @mock.patch.object(linux_net, "del_ip_route") def test__withdraw_subnet( self, mock_del_ip_route, mock_get_exposed_routes_on_network ): gateway = {} gateway["ips"] = [ ipaddress.ip_interface(ip) for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] ] gateway["address_scopes"] = self.addr_scope gateway["lrp_ports"] = {""} self.bgp_driver.ovn_local_cr_lrps = {"gateway_port": gateway} port_info = { "cr_lrp": self.cr_lrp0.logical_port, "subnets": { "fdcc:8cf2:d40c:2::/64", "192.168.1.0/24" } } mock_get_exposed_routes_on_network.side_effect = (["route-v4"], []) self.bgp_driver._withdraw_subnet(port_info, "gateway_port") expected_calls = [ mock.call( mock.ANY, "192.168.1.0", CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=24, via="10.0.0.10", ), mock.call( mock.ANY, "fdcc:8cf2:d40c:2::", CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=64, via="fd51:f4b3:872:eda::10", ), mock.call( mock.ANY, "fd51:f4b3:872:eda::", CONF.bgp_vrf_table_id, CONF.bgp_nic, vlan=None, mask=64, via=None, ), ] mock_del_ip_route.assert_has_calls(expected_calls) @mock.patch.object(linux_net, "get_exposed_routes_on_network") @mock.patch.object(linux_net, "del_ip_route") def test__withdraw_subnet_no_gateway( self, mock_del_ip_route, mock_get_exposed_routes_on_network ): self.bgp_driver.ovn_local_cr_lrps = {} self.bgp_driver._withdraw_subnet(self.router_port, "gateway_port") mock_del_ip_route.assert_not_called() mock_get_exposed_routes_on_network.assert_not_called() @mock.patch.object(linux_net, "delete_ip_routes") @mock.patch.object(linux_net, "get_routes_on_tables") def test_sync(self, mock_get_routes_on_tables, mock_delete_ip_routes): def create_route(dst, dst_len, gateway): m = mock.Mock([]) m.dst = dst m.dst_len = dst_len m.gateway = gateway return m def create_hashed_route(dst, dst_len, gateway): return ovn_stretched_l2_bgp_driver.HashedRoute( network=dst, prefix_len=dst_len, dst=gateway, ) mock__expose_cr_lrp = mock.patch.object( self.bgp_driver, "_expose_cr_lrp" ).start() vrf_routes = [ create_hashed_route(dst, dst_len, gateway) for (dst, dst_len, gateway) in [ ("192.168.1.0", 24, "10.0.0.1"), ("10.0.0.0", 24, None), ("fdcc:8cf2:d40c:2::", 64, "fd51:f4b3:872:eda::1"), ("fd51:f4b3:872:eda::", 64, None), ] ] # really hacky way to get the routes into self.bgp_driver.vrf_routes mock__expose_cr_lrp.side_effect = ( lambda _, __: self.bgp_driver.vrf_routes.update(vrf_routes) ) self.sb_idl.get_cr_lrp_ports.return_value = [self.cr_lrp0] delete_route = create_route("192.168.0.0", 24, "10.0.0.1") routes = [ create_route(dst, dst_len, gateway) for (dst, dst_len, gateway) in [ ("192.168.1.0", 24, "10.0.0.1"), ("10.0.0.0", 24, None), ("fdcc:8cf2:d40c:2::", 64, "fd51:f4b3:872:eda::1"), ("fd51:f4b3:872:eda::", 64, None), ] ] routes.append(delete_route) mock_get_routes_on_tables.return_value = routes self.bgp_driver.sync() mock_get_routes_on_tables.assert_called_once_with( [CONF.bgp_vrf_table_id] ) mock_delete_ip_routes.assert_called_once_with([delete_route]) mock__expose_cr_lrp.assert_called_once_with( ["10.0.0.1/24", "fd51:f4b3:872:eda::1/64"], self.cr_lrp0 ) def test_withdraw_ip(self): mock__withdraw_cr_lrp = mock.patch.object( self.bgp_driver, "_withdraw_cr_lrp" ).start() self.bgp_driver.withdraw_ip(None, self.cr_lrp0) mock__withdraw_cr_lrp.assert_called_once_with(None, self.cr_lrp0) def test__withdraw_cr_lrp(self): mock__withdraw_subnet = mock.patch.object( self.bgp_driver, "_withdraw_subnet" ).start() gateway = {} gateway["ips"] = [ ipaddress.ip_interface(ip) for ip in ["10.0.0.10/26", "fd51:f4b3:872:eda::10/64"] ] gateway["address_scopes"] = self.addr_scope self.bgp_driver.ovn_local_cr_lrps = { self.cr_lrp0.logical_port: gateway } gateway["lrp_ports"] = { "lrp-lrp_port0", "lrp-lrp_port1", "lrp-lrp_port2", } lrp_port0 = { "cr_lrp": self.cr_lrp0.logical_port, "subnets": { "fdcc:8cf2:d40c:1::/64", "192.168.0.0/24"} } lrp_port1 = { "cr_lrp": self.cr_lrp0.logical_port, "subnets": { "fdcc:8cf2:d40c:2::/64", "192.168.1.0/24"} } self.bgp_driver.propagated_lrp_ports = { "lrp-lrp_port0": lrp_port0, "lrp-lrp_port1": lrp_port1, } self.bgp_driver.ovn_local_cr_lrps = { self.cr_lrp0.logical_port: gateway } self.bgp_driver._withdraw_cr_lrp(None, self.cr_lrp0) mock__withdraw_subnet.assert_has_calls( [ mock.call(lrp_port0, self.cr_lrp0.logical_port), mock.call(lrp_port1, self.cr_lrp0.logical_port), ], any_order=True ) self.assertDictEqual( self.bgp_driver.ovn_local_cr_lrps, {} ) self.assertDictEqual( self.bgp_driver.propagated_lrp_ports, {} ) def test__withdraw_cr_lrp_invalid_addr_scope(self): mock__withdraw_subnet = mock.patch.object( self.bgp_driver, "_withdraw_subnet" ).start() gateway = { "address_scopes": { constants.IP_VERSION_4: '', constants.IP_VERSION_6: '', } } gateway["lrp_ports"] = { "lrp-lrp_port0", "lrp-lrp_port1", "lrp-lrp_port2", } self.bgp_driver.propagated_lrp_ports = { "lrp-lrp_port0": {}, "lrp-lrp_port1": {}, "lrp-lrp_port2": {}, } self.bgp_driver.ovn_local_cr_lrps = { self.cr_lrp0.logical_port: gateway } self.bgp_driver._withdraw_cr_lrp(None, self.cr_lrp0) self.sb_idl.get_lrp_ports_for_router.assert_not_called() mock__withdraw_subnet.assert_not_called() def test_expose_ip(self): mock__expose_cr_lrp = mock.patch.object( self.bgp_driver, "_expose_cr_lrp" ).start() ips = ["10.0.0.1/24", "fd51:f4b3:872:eda::1/64"] self.bgp_driver.expose_ip(ips, self.cr_lrp0) mock__expose_cr_lrp.assert_called_once_with(ips, self.cr_lrp0) def test_expose_ip_invalid_type(self): mock__expose_cr_lrp = mock.patch.object( self.bgp_driver, "_expose_cr_lrp" ).start() patch_port = mock.Mock() patch_port.type = constants.OVN_PATCH_VIF_PORT_TYPE self.bgp_driver.expose_ip([], patch_port) mock__expose_cr_lrp.assert_not_called() def test__expose_cr_lrp(self): mock__ensure_network_exposed = mock.patch.object( self.bgp_driver, "_ensure_network_exposed" ).start() lrp_port0 = fakes.create_object( { "name": "fake-port-lrp0", "type": constants.OVN_PATCH_VIF_PORT_TYPE, "chassis": "fake-chassis1", "external_ids": {}, "logical_port": "lrp-lrp_port0", "options": { "chassis-redirect-port": self.cr_lrp0.logical_port, }, } ) lrp_port1 = fakes.create_object( { "name": "fake-port-lrp1", "type": constants.OVN_PATCH_VIF_PORT_TYPE, "mac": ["aa:bb:cc:dd:ee:ee 192.168.1.12 192.168.1.13"], "logical_port": "lrp-lrp_port1", "chassis": "fake-chassis1", "external_ids": {}, "options": {}, } ) lrp_port2 = fakes.create_object( { "name": "fake-port-lrp2", "type": constants.OVN_PATCH_VIF_PORT_TYPE, "mac": [], "logical_port": "lrp-lrp_port2", "chassis": "fake-chassis1", "external_ids": {}, "options": {}, } ) lrp_port3 = fakes.create_object( { "name": "fake-port-lrp3", "type": constants.OVN_PATCH_VIF_PORT_TYPE, "mac": [], "logical_port": "lrp-lrp_port3", "chassis": "", "up": [False], "external_ids": {}, "options": {}, } ) lrp_port4 = fakes.create_object( { "name": "fake-port-lrp4", "type": constants.OVN_PATCH_VIF_PORT_TYPE, "mac": [], "logical_port": "lrp-lrp_port4", "chassis": "", "up": [False], "options": {}, } ) lrp_port5 = fakes.create_object( { "name": "fake-port-lrp5", "type": constants.OVN_PATCH_VIF_PORT_TYPE, "mac": [], "logical_port": "lrp-lrp_port5", "chassis": "", "up": [False], "options": { "chassis-redirect-port": self.cr_lrp0.logical_port, }, } ) self.sb_idl.get_lrp_ports_for_router.return_value = [ lrp_port0, lrp_port1, lrp_port2, lrp_port3, lrp_port4, lrp_port5, ] self.sb_idl.get_port_by_name.return_value = self.fake_patch_port ips = ["10.0.0.1/24", "fd51:f4b3:872:eda::1/64"] self.bgp_driver._expose_cr_lrp(ips, self.cr_lrp0) mock__ensure_network_exposed.assert_has_calls( [ mock.call(lrp_port3, self.cr_lrp0.logical_port), mock.call(lrp_port4, self.cr_lrp0.logical_port), ] ) self.sb_idl.get_port_by_name.assert_called_once_with("fake-port") self.assertEqual( { self.cr_lrp0.logical_port: { "ips": [ipaddress.ip_interface(ip) for ip in ips], "address_scopes": self.addr_scope, "lrp_ports": set() } }, self.bgp_driver.ovn_local_cr_lrps) def test__expose_cr_lrp_no_port(self): mock__ensure_network_exposed = mock.patch.object( self.bgp_driver, "_ensure_network_exposed" ).start() self.sb_idl.get_port_by_name.return_value = [] ips = ["10.0.0.1/24", "fd51:f4b3:872:eda::1/64"] self.bgp_driver._expose_cr_lrp(ips, self.cr_lrp0) mock__ensure_network_exposed.assert_not_called() self.sb_idl.get_port_by_name.assert_called_once_with("fake-port") @mock.patch.object(driver_utils, "get_addr_scopes") def test__expose_cr_lrp_no_addr_scope(self, mock_addr_scopes): mock__ensure_network_exposed = mock.patch.object( self.bgp_driver, "_ensure_network_exposed" ).start() self.sb_idl.get_port_by_name.return_value = self.fake_patch_port mock_addr_scopes.return_value = { constants.IP_VERSION_4: "address_scope_v4", constants.IP_VERSION_6: "address_scope_v6", } self.bgp_driver._expose_cr_lrp([], self.cr_lrp0) self.sb_idl.get_port_by_name.assert_called_once_with("fake-port") mock_addr_scopes.assert_called_once_with(self.fake_patch_port) self.sb_idl.get_lrp_ports_for_router.assert_not_called() mock__ensure_network_exposed.assert_not_called() def test_expose_remote_ip(self): self.assertRaises( NotImplementedError, self.bgp_driver.expose_remote_ip, "1.2.3.4/24") def test_withdraw_remote_ip(self): self.assertRaises( NotImplementedError, self.bgp_driver.withdraw_remote_ip, "1.2.3.4/24") ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/utils/000077500000000000000000000000001460327367600257325ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/utils/__init__.py000066400000000000000000000000001460327367600300310ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_driver_utils.py000066400000000000000000000127271460327367600320670ustar00rootroot00000000000000# 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 ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.utils import driver_utils from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.tests import utils class TestDriverUtils(test_base.TestCase): def setUp(self): super(TestDriverUtils, self).setUp() def test_is_ipv6_gua(self): self.assertFalse(driver_utils.is_ipv6_gua('1.1.1.1')) self.assertFalse(driver_utils.is_ipv6_gua('fe80::1337')) self.assertTrue(driver_utils.is_ipv6_gua('2a01:db8::1337')) def test_get_addr_scopes(self): subnet_pool_addr_scope4 = '88e8aec3-da29-402d-becf-9fa2c38e69b8' subnet_pool_addr_scope6 = 'b7834aeb-2aa2-40ac-a8b5-2cded713cb58' # Both address pools set port = utils.create_row(external_ids={ constants.SUBNET_POOL_ADDR_SCOPE4: subnet_pool_addr_scope4, constants.SUBNET_POOL_ADDR_SCOPE6: subnet_pool_addr_scope6, }) self.assertDictEqual(driver_utils.get_addr_scopes(port), { constants.IP_VERSION_4: subnet_pool_addr_scope4, constants.IP_VERSION_6: subnet_pool_addr_scope6, }) # Only IPv4 port = utils.create_row(external_ids={ constants.SUBNET_POOL_ADDR_SCOPE4: subnet_pool_addr_scope4, }) self.assertDictEqual(driver_utils.get_addr_scopes(port), { constants.IP_VERSION_4: subnet_pool_addr_scope4, constants.IP_VERSION_6: None, }) # Only IPv6 port = utils.create_row(external_ids={ constants.SUBNET_POOL_ADDR_SCOPE6: subnet_pool_addr_scope6, }) self.assertDictEqual(driver_utils.get_addr_scopes(port), { constants.IP_VERSION_4: None, constants.IP_VERSION_6: subnet_pool_addr_scope6, }) # No Address pools port = utils.create_row(external_ids={}) self.assertDictEqual(driver_utils.get_addr_scopes(port), { constants.IP_VERSION_4: None, constants.IP_VERSION_6: None, }) def test_get_port_chassis_from_options(self): my_host = 'foo-host' # it is a VM port type, should use options field. row = utils.create_row( external_ids={constants.OVN_HOST_ID_EXT_ID_KEY: 'bar-host'}, options={constants.OVN_REQUESTED_CHASSIS: my_host}) self.assertEqual(driver_utils.get_port_chassis(row, chassis=my_host), (my_host, constants.OVN_CHASSIS_AT_OPTIONS)) def test_get_port_chassis_from_external_ids(self): my_host = 'foo-host' # it is a VM port type, should use options field. row = utils.create_row( external_ids={constants.OVN_HOST_ID_EXT_ID_KEY: my_host}) self.assertEqual(driver_utils.get_port_chassis(row, chassis=my_host), (my_host, constants.OVN_CHASSIS_AT_EXT_IDS)) def test_get_port_chassis_from_external_ids_virtual_port(self): my_host = 'foo-host' # it is a VM port type, should use options field. row = utils.create_row( external_ids={constants.OVN_HOST_ID_EXT_ID_KEY: my_host}, options={constants.OVN_REQUESTED_CHASSIS: 'bar-host'}, type=constants.OVN_VIRTUAL_VIF_PORT_TYPE) self.assertEqual(driver_utils.get_port_chassis(row, chassis=my_host), (my_host, constants.OVN_CHASSIS_AT_EXT_IDS)) def test_get_port_chassis_no_information(self): row = utils.create_row() self.assertEqual(driver_utils.get_port_chassis(row, chassis='foo'), (None, None)) def test_check_name_prefix(self): lb = utils.create_row(name='some-name') self.assertTrue(driver_utils.check_name_prefix(lb, 'some')) self.assertFalse(driver_utils.check_name_prefix(lb, 'other')) lb = utils.create_row(no_name='aa') self.assertFalse(driver_utils.check_name_prefix(lb, '')) def is_pf_lb(self): lb = utils.create_row(name='pf-floating-ip-someuuid') self.assertTrue(driver_utils.is_pf_lb(lb)) lb = utils.create_row(name='lb-someuuid') self.assertFalse(driver_utils.is_pf_lb(lb)) def test_get_prefixes_from_ips(self): # IPv4 ips = ['192.168.0.1/24', '192.168.0.244/28', '172.13.37.59/27'] self.assertListEqual(driver_utils.get_prefixes_from_ips(ips), ['192.168.0.0/24', '192.168.0.240/28', '172.13.37.32/27']) # IPv6 ips = ['fe80::5097/64', 'ff00::13:37/112', 'fc00::1/46'] self.assertListEqual(driver_utils.get_prefixes_from_ips(ips), ['fe80::/64', 'ff00::13:0/112', 'fc00::/46']) # combined. ips = ['172.13.37.59/27', 'ff00::13:37/112'] self.assertListEqual(driver_utils.get_prefixes_from_ips(ips), ['172.13.37.32/27', 'ff00::13:0/112']) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_frr.py000066400000000000000000000073151460327367600301420ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import tempfile from unittest import mock from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.utils import frr as frr_utils from ovn_bgp_agent.tests import base as test_base class TestFrr(test_base.TestCase): def setUp(self): super(TestFrr, self).setUp() self.mock_vtysh = mock.patch('ovn_bgp_agent.privileged.vtysh').start() def test__get_router_id(self): router_id = 'fake-router' self.mock_vtysh.run_vtysh_command.return_value = ( '{"ipv4Unicast": {"routerId": "%s"}}' % router_id) ret = frr_utils._get_router_id() self.assertEqual(router_id, ret) def test__get_router_id_no_ipv4_settings(self): self.mock_vtysh.run_vtysh_command.return_value = '{}' ret = frr_utils._get_router_id() self.assertIsNone(ret) @mock.patch.object(frr_utils, '_get_router_id') @mock.patch.object(tempfile, 'NamedTemporaryFile') def test_vrf_leak(self, mock_tf, mock_gri): vrf = 'fake-vrf' bgp_as = 'fake-bgp-as' router_id = 'fake-router-id' mock_gri.return_value = router_id frr_utils.vrf_leak(vrf, bgp_as) write_arg = mock_tf.return_value.write.call_args_list[0][0][0] self.assertIn(vrf, write_arg) self.assertIn(bgp_as, write_arg) # Assert the file was closed mock_tf.return_value.close.assert_called_once_with() @mock.patch.object(frr_utils, '_get_router_id') @mock.patch.object(tempfile, 'NamedTemporaryFile') def test_vrf_leak_no_router_id(self, mock_tf, mock_gri): mock_gri.return_value = None frr_utils.vrf_leak('fake-vrf', 'fake-bgp-as') # Assert no file was created self.assertFalse(mock_tf.called) @mock.patch.object(tempfile, 'NamedTemporaryFile') def _test_vrf_reconfigure(self, mock_tf, add_vrf=True): action = 'add-vrf' if add_vrf else 'del-vrf' evpn_info = {'vni': '1001', 'bgp_as': 'fake-bgp-as'} frr_utils.vrf_reconfigure(evpn_info, action) vrf_name = "{}{}".format(constants.OVN_EVPN_VRF_PREFIX, evpn_info['vni']) write_arg = mock_tf.return_value.write.call_args_list[0][0][0] if add_vrf: self.assertIn('\nvrf %s' % vrf_name, write_arg) self.assertIn('\nrouter bgp %s' % evpn_info['bgp_as'], write_arg) else: self.assertIn("no vrf %s" % vrf_name, write_arg) self.assertIn("no router bgp %s" % evpn_info['bgp_as'], 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_vrf_reconfigure_add_vrf(self): self._test_vrf_reconfigure() def test_vrf_reconfigure_del_vrf(self): self._test_vrf_reconfigure(add_vrf=False) def test_vrf_reconfigure_unknown_action(self): frr_utils.vrf_reconfigure('fake-evpn-info', 'non-existing-action') # Assert run_vtysh_command() wasn't called self.assertFalse(self.mock_vtysh.run_vtysh_config.called) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovn.py000066400000000000000000001061531460327367600301530ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_config import cfg from ovs.stream import Stream from ovsdbapp.backend.ovs_idl import connection from ovsdbapp.backend.ovs_idl import idlutils from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.utils import ovn as ovn_utils from ovn_bgp_agent import exceptions from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.tests.unit import fakes CONF = cfg.CONF class TestOvsdbNbOvnIdl(test_base.TestCase): def setUp(self): super(TestOvsdbNbOvnIdl, self).setUp() self.nb_idl = ovn_utils.OvsdbNbOvnIdl(mock.Mock()) # Monkey-patch parent class methods self.nb_idl.db_find_rows = mock.Mock() self.nb_idl.db_list_rows = mock.Mock() self.nb_idl.lookup = mock.Mock() def test_get_network_vlan_tags(self): tag = [123] lsp = fakes.create_object({'name': 'port-0', 'tag': tag}) self.nb_idl.db_find_rows.return_value.execute.return_value = [ lsp] ret = self.nb_idl.get_network_vlan_tags() self.assertEqual(tag, ret) self.nb_idl.db_find_rows.assert_called_once_with( 'Logical_Switch_Port', ('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE)) def test_get_network_vlan_tag_by_network_name(self): network_name = 'net0' tag = [123] lsp = fakes.create_object({'name': 'port-0', 'options': {'network_name': network_name}, 'tag': tag}) self.nb_idl.db_find_rows.return_value.execute.return_value = [ lsp] ret = self.nb_idl.get_network_vlan_tag_by_network_name(network_name) self.assertEqual(tag, ret) self.nb_idl.db_find_rows.assert_called_once_with( 'Logical_Switch_Port', ('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE)) def test_ls_has_virtual_ports(self): ls_name = 'logical_switch' port = fakes.create_object( {'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE}) ls = fakes.create_object({'ports': [port]}) self.nb_idl.lookup.return_value = ls ret = self.nb_idl.ls_has_virtual_ports(ls_name) self.assertEqual(True, ret) self.nb_idl.lookup.assert_called_once_with('Logical_Switch', ls_name) def test_ls_has_virtual_ports_not_found(self): ls_name = 'logical_switch' port = fakes.create_object({'type': constants.OVN_VM_VIF_PORT_TYPE}) ls = fakes.create_object({'ports': [port]}) self.nb_idl.lookup.return_value = ls ret = self.nb_idl.ls_has_virtual_ports(ls_name) self.assertEqual(False, ret) self.nb_idl.lookup.assert_called_once_with('Logical_Switch', ls_name) def test_get_nat_by_logical_port(self): logical_port = 'logical_port' nat_info = ['nat_info'] self.nb_idl.db_find_rows.return_value.execute.return_value = nat_info ret = self.nb_idl.get_nat_by_logical_port(logical_port) self.assertEqual('nat_info', ret) self.nb_idl.db_find_rows.assert_called_once_with( 'NAT', ('logical_port', '=', logical_port)) def test_get_active_lsp_on_chassis_options(self): chassis = 'local_chassis' row1 = fakes.create_object({ 'options': {'requested-chassis': chassis}, 'external_ids': {}}) row2 = fakes.create_object({ 'options': {'requested-chassis': 'other_chassis'}, 'external_ids': {}}) self.nb_idl.db_find_rows.return_value.execute.return_value = [ row1, row2] ret = self.nb_idl.get_active_lsp_on_chassis(chassis) self.assertEqual([row1], ret) self.nb_idl.db_find_rows.assert_called_once_with( 'Logical_Switch_Port', ('up', '=', True)) def test_get_active_lsp_on_chassis_external_ids(self): chassis = 'local_chassis' row1 = fakes.create_object({ 'options': {}, 'external_ids': {'neutron:host_id': chassis}}) row2 = fakes.create_object({ 'options': {}, 'external_ids': {'neutron:host_id': 'other_chassis'}}) self.nb_idl.db_find_rows.return_value.execute.return_value = [ row1, row2] ret = self.nb_idl.get_active_lsp_on_chassis(chassis) self.assertEqual([row1], ret) self.nb_idl.db_find_rows.assert_called_once_with( 'Logical_Switch_Port', ('up', '=', True)) def test_get_active_cr_lrp_on_chassis(self): chassis = 'local_chassis' row1 = fakes.create_object({ 'status': {'hosting-chassis': 'local_chassis'}}) row2 = fakes.create_object({ 'status': {'hosting-chassis': 'other_chassis'}}) row3 = fakes.create_object({}) self.nb_idl.db_list_rows.return_value.execute.return_value = [ row1, row2, row3] ret = self.nb_idl.get_active_cr_lrp_on_chassis(chassis) self.assertEqual([row1], ret) self.nb_idl.db_list_rows.assert_called_once_with( 'Logical_Router_Port') def test_get_active_local_lrps(self): local_gateway_ports = ['router1'] row1 = fakes.create_object({ 'external_ids': { constants.OVN_DEVICE_OWNER_EXT_ID_KEY: constants.OVN_ROUTER_INTERFACE, constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1' }}) row2 = fakes.create_object({ 'external_ids': { constants.OVN_DEVICE_OWNER_EXT_ID_KEY: constants.OVN_ROUTER_INTERFACE, constants.OVN_DEVICE_ID_EXT_ID_KEY: 'other_router' }}) self.nb_idl.db_find_rows.return_value.execute.return_value = [ row1, row2] ret = self.nb_idl.get_active_local_lrps(local_gateway_ports) self.assertEqual([row1], ret) self.nb_idl.db_find_rows.assert_called_once_with( 'Logical_Switch_Port', ('up', '=', True), ('type', '=', constants.OVN_ROUTER_PORT_TYPE), ('external_ids', '=', {constants.OVN_DEVICE_OWNER_EXT_ID_KEY: constants.OVN_ROUTER_INTERFACE})) def test_get_active_lsp(self): row1 = fakes.create_object({ 'type': constants.OVN_VM_VIF_PORT_TYPE, 'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}}) row2 = fakes.create_object({ 'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE, 'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}}) self.nb_idl.db_find_rows.return_value.execute.side_effect = [ [row1], [row2]] ret = self.nb_idl.get_active_lsp('net1') self.assertEqual([row1, row2], ret) expected_calls = [ mock.call('Logical_Switch_Port', ('up', '=', True), ('type', '=', constants.OVN_VM_VIF_PORT_TYPE), ('external_ids', '=', {constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'})), mock.call().execute(check_error=True), mock.call('Logical_Switch_Port', ('up', '=', True), ('type', '=', constants.OVN_VIRTUAL_VIF_PORT_TYPE), ('external_ids', '=', {constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'})), mock.call().execute(check_error=True)] self.nb_idl.db_find_rows.assert_has_calls(expected_calls) def test_get_active_local_lbs(self): local_gateway_ports = ['router1'] lb1 = fakes.create_object({ 'vips': {'vip': 'member1,member2'}, 'external_ids': { constants.OVN_LB_LR_REF_EXT_ID_KEY: "neutron-router1"}}) lb2 = fakes.create_object({ 'vips': {'vip': 'member1,member2'}, 'external_ids': { constants.OVN_LB_LR_REF_EXT_ID_KEY: "neutron-router2"}}) self.nb_idl.db_find_rows.return_value.execute.return_value = [lb1, lb2] ret = self.nb_idl.get_active_local_lbs(local_gateway_ports) self.assertEqual([lb1], ret) self.nb_idl.db_find_rows.assert_called_once_with( 'Load_Balancer', ('vips', '!=', {})) self.nb_idl.db_find_rows.reset_mock() lb3 = fakes.create_object({ 'vips': {'fip': 'member1'}, 'external_ids': { constants.OVN_LR_NAME_EXT_ID_KEY: "neutron-router1"}}) self.nb_idl.db_find_rows.return_value.execute.return_value = [ lb1, lb2, lb3] ret = self.nb_idl.get_active_local_lbs(local_gateway_ports) self.assertEqual([lb1, lb3], ret) self.nb_idl.db_find_rows.assert_called_once_with( 'Load_Balancer', ('vips', '!=', {})) class TestOvsdbSbOvnIdl(test_base.TestCase): def setUp(self): super(TestOvsdbSbOvnIdl, self).setUp() self.sb_idl = ovn_utils.OvsdbSbOvnIdl(mock.Mock()) # Monkey-patch parent class methods self.sb_idl.db_find_rows = mock.Mock() self.sb_idl.db_list_rows = mock.Mock() def test_get_port_by_name(self): fake_p_info = 'fake-port-info' port = 'fake-port' self.sb_idl.db_find_rows.return_value.execute.return_value = [ fake_p_info] ret = self.sb_idl.get_port_by_name(port) self.assertEqual(fake_p_info, ret) self.sb_idl.db_find_rows.assert_called_once_with( 'Port_Binding', ('logical_port', '=', port)) def test_get_port_by_name_empty(self): port = 'fake-port' self.sb_idl.db_find_rows.return_value.execute.return_value = [] ret = self.sb_idl.get_port_by_name(port) self.assertEqual([], ret) self.sb_idl.db_find_rows.assert_called_once_with( 'Port_Binding', ('logical_port', '=', port)) def test_get_ports_on_datapath(self): dp = 'fake-datapath' self.sb_idl.db_find_rows.return_value.execute.return_value = [ 'fake-port'] ret = self.sb_idl.get_ports_on_datapath(dp) self.assertEqual(['fake-port'], ret) self.sb_idl.db_find_rows.assert_called_once_with( 'Port_Binding', ('datapath', '=', dp)) def test_get_ports_on_datapath_port_type(self): dp = 'fake-datapath' p_type = 'fake-type' self.sb_idl.db_find_rows.return_value.execute.return_value = [ 'fake-port'] ret = self.sb_idl.get_ports_on_datapath(dp, port_type=p_type) self.assertEqual(['fake-port'], ret) self.sb_idl.db_find_rows.assert_called_once_with( 'Port_Binding', ('datapath', '=', dp), ('type', '=', p_type)) def test_get_ports_by_type(self): fake_p_info = 'fake-port-info' port_type = 'fake-type' self.sb_idl.db_find_rows.return_value.execute.return_value = [ fake_p_info] ret = self.sb_idl.get_ports_by_type(port_type) self.assertEqual([fake_p_info], ret) self.sb_idl.db_find_rows.assert_called_once_with( 'Port_Binding', ('type', '=', port_type)) def test_is_provider_network(self): dp = 'fake-datapath' self.sb_idl.db_find_rows.return_value.execute.return_value = ['fake'] self.assertTrue(self.sb_idl.is_provider_network(dp)) self.sb_idl.db_find_rows.assert_called_once_with( 'Port_Binding', ('datapath', '=', dp), ('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE)) def test_is_provider_network_false(self): dp = 'fake-datapath' self.sb_idl.db_find_rows.return_value.execute.return_value = [] self.assertFalse(self.sb_idl.is_provider_network(dp)) self.sb_idl.db_find_rows.assert_called_once_with( 'Port_Binding', ('datapath', '=', dp), ('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE)) def test_get_fip_associated(self): port = '1ad5f7e1-fcca-4791-bf50-120c4c73e602' datapath = '3e2dc454-6970-4419-9132-b3593d19cdfa' fip = '172.24.200.7' row = fakes.create_object({ 'datapath': datapath, 'nat_addresses': ['aa:bb:cc:dd:ee:ff {} is_chassis_resident(' '"cr-lrp-{}")'.format(fip, port)]}) self.sb_idl.db_find_rows.return_value.execute.return_value = [row] fip_addr, fip_dp = self.sb_idl.get_fip_associated(port) self.assertEqual(fip, fip_addr) self.assertEqual(datapath, fip_dp) self.sb_idl.db_find_rows.assert_called_once_with( 'Port_Binding', ('type', '=', constants.OVN_PATCH_VIF_PORT_TYPE)) def test_get_fip_associated_not_found(self): self.sb_idl.db_find_rows.return_value.execute.return_value = [] fip_addr, fip_dp = self.sb_idl.get_fip_associated('fake-port') self.assertIsNone(fip_addr) self.assertIsNone(fip_dp) self.sb_idl.db_find_rows.assert_called_once_with( 'Port_Binding', ('type', '=', constants.OVN_PATCH_VIF_PORT_TYPE)) def _test_is_port_on_chassis(self, should_match=True): chassis_name = 'fake-chassis' with mock.patch.object(self.sb_idl, 'get_port_by_name') as mock_p: ch = fakes.create_object({'name': chassis_name}) mock_p.return_value = fakes.create_object( {'type': constants.OVN_VM_VIF_PORT_TYPE, 'chassis': [ch]}) if should_match: self.assertTrue(self.sb_idl.is_port_on_chassis( 'fake-port', chassis_name)) else: self.assertFalse(self.sb_idl.is_port_on_chassis( 'fake-port', 'wrong-chassis')) def test_is_port_on_chassis(self): self._test_is_port_on_chassis() def test_is_port_on_chassis_no_match_on_chassis(self): self._test_is_port_on_chassis(should_match=False) def test_is_port_on_chassis_port_not_found(self): with mock.patch.object(self.sb_idl, 'get_port_by_name') as mock_p: mock_p.return_value = [] self.assertFalse(self.sb_idl.is_port_on_chassis( 'fake-port', 'fake-chassis')) def test_is_port_without_chassis(self): chassis_name = 'fake-chassis' with mock.patch.object(self.sb_idl, 'get_port_by_name') as mock_p: ch = fakes.create_object({'name': chassis_name}) mock_p.return_value = fakes.create_object( {'type': constants.OVN_VM_VIF_PORT_TYPE, 'chassis': [ch]}) self.assertFalse(self.sb_idl.is_port_without_chassis('fake-port')) def test_is_port_without_chassis_no_chassis(self): with mock.patch.object(self.sb_idl, 'get_port_by_name') as mock_p: mock_p.return_value = fakes.create_object( {'type': constants.OVN_VM_VIF_PORT_TYPE, 'chassis': []}) self.assertTrue(self.sb_idl.is_port_without_chassis('fake-port')) def _test_is_port_deleted(self, port_exist=True): ret_value = mock.Mock() if port_exist else [] with mock.patch.object(self.sb_idl, 'get_port_by_name') as mock_p: mock_p.return_value = ret_value if port_exist: # Should return False as the port is not deleted self.assertFalse(self.sb_idl.is_port_deleted('fake-port')) else: self.assertTrue(self.sb_idl.is_port_deleted('fake-port')) def test_is_port_deleted(self): self._test_is_port_deleted() def test_is_port_deleted_false(self): self._test_is_port_deleted(port_exist=False) def test_get_ports_on_chassis(self): ch0 = fakes.create_object({'name': 'chassis-0'}) ch1 = fakes.create_object({'name': 'chassis-1'}) port0 = fakes.create_object({'name': 'port-0', 'chassis': [ch0]}) port1 = fakes.create_object({'name': 'port-1', 'chassis': [ch1]}) port2 = fakes.create_object({'name': 'port-2', 'chassis': [ch0]}) self.sb_idl.db_list_rows.return_value.execute.return_value = [ port0, port1, port2] ret = self.sb_idl.get_ports_on_chassis('chassis-0') self.assertIn(port0, ret) self.assertIn(port2, ret) # Port-1 is bound to chassis-1 self.assertNotIn(port1, ret) def _test_get_provider_datapath_from_cr_lrp(self, port, found_port=True): ret_value = (fakes.create_object({'datapath': 'dp1'}) if found_port else None) with mock.patch.object(self.sb_idl, 'get_port_by_name') as mock_p: mock_p.return_value = ret_value if found_port: self.assertEqual( self.sb_idl.get_provider_datapath_from_cr_lrp(port), 'dp1') else: self.assertIsNone( self.sb_idl.get_provider_datapath_from_cr_lrp(port)) if port.startswith('cr-lrp'): mock_p.assert_called_once_with(port.split("cr-lrp-")[1]) else: mock_p.assert_not_called() def test_get_provider_datapath_from_cr_lrp(self): port = 'cr-lrp-port' self._test_get_provider_datapath_from_cr_lrp(port) def test_get_provider_datapath_from_cr_lrp_no_cr_lrp(self): port = 'port' self._test_get_provider_datapath_from_cr_lrp(port, found_port=False) def test_get_provider_datapath_from_cr_lrp_no_port(self): port = 'cr-lrp-port' self._test_get_provider_datapath_from_cr_lrp(port, found_port=False) def test_get_datapath_from_port_peer(self): with mock.patch.object(self.sb_idl, 'get_port_datapath') as m_dp: port0 = fakes.create_object({'name': 'port-0', 'options': {'peer': 'port-peer'}}) self.sb_idl.get_datapath_from_port_peer(port0) m_dp.assert_called_once_with('port-peer') def _test_get_network_name_and_tag(self, network_in_bridge_map=True): tag = 1001 network = 'public' if network_in_bridge_map else 'spongebob' with mock.patch.object(self.sb_idl, 'get_ports_on_datapath') as m_dp: row = fakes.create_object({ 'options': {'network_name': network}, 'tag': tag}) m_dp.return_value = [row, ] net_name, net_tag = self.sb_idl.get_network_name_and_tag( 'fake-dp', 'br-ex:public'.format(network)) if network_in_bridge_map: self.assertEqual(network, net_name) self.assertEqual(tag, net_tag) else: self.assertIsNone(net_name) self.assertIsNone(net_tag) def test_get_network_name_and_tag(self): self._test_get_network_name_and_tag() def test_get_network_name_and_tag_not_in_bridge_mappings(self): self._test_get_network_name_and_tag(network_in_bridge_map=False) def test_get_netweork_vlan_tags(self): tag = [1001] row = fakes.create_object({'tag': tag}) self.sb_idl.db_find_rows.return_value.execute.return_value = [row, ] ret = self.sb_idl.get_network_vlan_tags() self.assertEqual(tag, ret) def _test_get_network_vlan_tag_by_network_name(self, match=True): network = 'public' if match else 'spongebob' tag = [1001] row = fakes.create_object({ 'options': {'network_name': 'public'}, 'tag': tag}) self.sb_idl.db_find_rows.return_value.execute.return_value = [row, ] ret = self.sb_idl.get_network_vlan_tag_by_network_name(network) if match: self.assertEqual(tag, ret) else: self.assertEqual([], ret) def test_get_network_vlan_tag_by_network_name(self): self._test_get_network_vlan_tag_by_network_name() def test_get_network_vlan_tag_by_network_name_no_match(self): self._test_get_network_vlan_tag_by_network_name(match=False) def _test_is_router_gateway_on_chassis(self, match=True): chassis = 'chassis-0' if match else 'spongebob' port = '39c38ce6-f0ea-484e-a57c-aec0d4e961a5' with mock.patch.object(self.sb_idl, 'get_ports_on_datapath') as m_dp: ch = fakes.create_object({'name': 'chassis-0'}) row = fakes.create_object({'logical_port': port, 'chassis': [ch]}) m_dp.return_value = [row, ] ret = self.sb_idl.is_router_gateway_on_chassis('fake-dp', chassis) if match: self.assertEqual(port, ret) else: self.assertIsNone(ret) def test_is_router_gateway_on_chassis(self): self._test_is_router_gateway_on_chassis() def test_is_router_gateway_on_chassis_not_on_chassis(self): self._test_is_router_gateway_on_chassis(match=False) def _test_is_router_gateway_on_any_chassis(self, match=True): if match: ch = fakes.create_object({'name': 'chassis-0'}) else: ch = fakes.create_object({'name': ''}) port = '39c38ce6-f0ea-484e-a57c-aec0d4e961a5' with mock.patch.object(self.sb_idl, 'get_ports_on_datapath') as m_dp: row = fakes.create_object({'logical_port': port, 'chassis': [ch]}) m_dp.return_value = [row, ] ret = self.sb_idl.is_router_gateway_on_any_chassis('fake-dp') if match: self.assertEqual(row, ret) else: self.assertIsNone(ret) def test_is_router_gateway_on_any_chassis(self): self._test_is_router_gateway_on_any_chassis() def test_is_router_gateway_on_chassis_not_on_any_chassis(self): self._test_is_router_gateway_on_any_chassis(match=False) def _test_get_lrps_for_datapath(self, has_options=True): peer = '75c793bd-d865-48f3-8f05-68ba4239d14e' with mock.patch.object(self.sb_idl, 'get_ports_on_datapath') as m_dp: options = {} if has_options: options.update({'peer': peer}) row = fakes.create_object({'options': options}) m_dp.return_value = [row, ] ret = self.sb_idl.get_lrps_for_datapath('fake-dp') if has_options: self.assertEqual([peer], ret) else: self.assertEqual([], ret) def test_get_lrps_for_datapath(self): self._test_get_lrps_for_datapath() def test_get_lrps_for_datapath_no_options(self): self._test_get_lrps_for_datapath(has_options=False) def test_get_lrp_ports_for_router(self): with mock.patch.object(self.sb_idl, 'get_ports_on_datapath') as m_dp: datapath = 'router-dp' self.sb_idl.get_lrp_ports_for_router(datapath) m_dp.assert_called_once_with(datapath, constants.OVN_PATCH_VIF_PORT_TYPE) def test_get_lrp_ports_on_provider(self): port = '39c38ce6-f0ea-484e-a57c-aec0d4e961a5' with mock.patch.object(self.sb_idl, 'get_ports_by_type') as m_pt: ch = fakes.create_object({'name': 'chassis-0'}) row = fakes.create_object({'logical_port': port, 'chassis': [ch], 'datapath': 'fake-dp'}) m_pt.return_value = [row, ] with mock.patch.object(self.sb_idl, 'is_provider_network') as m_pn: self.sb_idl.get_lrp_ports_on_provider() m_pt.assert_called_once_with(constants.OVN_PATCH_VIF_PORT_TYPE) m_pn.assert_called_once_with(row.datapath) def test_get_lrp_ports_on_provider_starts_with_lrp(self): port = 'lrp-39c38ce6-f0ea-484e-a57c-aec0d4e961a5' with mock.patch.object(self.sb_idl, 'get_ports_by_type') as m_pt: ch = fakes.create_object({'name': 'chassis-0'}) row = fakes.create_object({'logical_port': port, 'chassis': [ch]}) m_pt.return_value = [row, ] with mock.patch.object(self.sb_idl, 'is_provider_network') as m_pn: self.sb_idl.get_lrp_ports_on_provider() m_pt.assert_called_once_with(constants.OVN_PATCH_VIF_PORT_TYPE) m_pn.assert_not_called() def _test_get_port_datapath(self, port_found=True): dp = '3fce2c5f-7801-469b-894e-05561e3bda15' with mock.patch.object(self.sb_idl, 'get_port_by_name') as mock_p: port_info = None if port_found: port_info = fakes.create_object({'datapath': dp}) mock_p.return_value = port_info ret = self.sb_idl.get_port_datapath('fake-port') if port_found: self.assertEqual(dp, ret) else: self.assertIsNone(ret) def test_get_port_datapath(self): self._test_get_port_datapath() def test_get_port_datapath_port_not_found(self): self._test_get_port_datapath(port_found=False) def test_get_ip_from_port_peer(self): ip = '172.24.200.7' port = fakes.create_object({'options': {'peer': 'fake-peer'}}) with mock.patch.object(self.sb_idl, 'get_port_by_name') as mock_p: port_peer = fakes.create_object({ 'mac': ['aa:bb:cc:dd:ee:ff 172.24.200.7']}) mock_p.return_value = port_peer ret = self.sb_idl.get_ip_from_port_peer(port) self.assertEqual(ip, ret) def test_get_ip_from_port_peer_port_not_found(self): port = fakes.create_object({'options': {'peer': 'fake-peer'}}) with mock.patch.object(self.sb_idl, 'get_port_by_name') as mock_p: mock_p.return_value = [] self.assertRaises(exceptions.PortNotFound, self.sb_idl.get_ip_from_port_peer, port) def _test_get_evpn_info_from_port_name(self, crlrp=False, lrp=False): port = '48dc4289-a1b9-4505-b513-4eff0c460c29' if crlrp: port_name = constants.OVN_CRLRP_PORT_NAME_PREFIX + port elif lrp: port_name = constants.OVN_LRP_PORT_NAME_PREFIX + port else: port_name = port expected_return = 'spongebob' with mock.patch.object(self.sb_idl, 'get_port_by_name') as mock_p: with mock.patch.object(self.sb_idl, 'get_evpn_info') as mock_evpn: mock_evpn.return_value = expected_return ret = self.sb_idl.get_evpn_info_from_port_name(port_name) mock_p.assert_called_once_with(port) self.assertEqual(expected_return, ret) def test_get_evpn_info_from_port_name(self): self._test_get_evpn_info_from_port_name() def test_get_evpn_info_from_port_name_crlrp(self): self._test_get_evpn_info_from_port_name(crlrp=True) def test_get_evpn_info_from_port_name_lrp(self): self._test_get_evpn_info_from_port_name(lrp=True) def _test_get_evpn_info(self, value_error=False): vni = 'invalid-vni' if value_error else '1001' port = fakes.create_object({ 'logical_port': 'fake-port', 'external_ids': {constants.OVN_EVPN_VNI_EXT_ID_KEY: vni, constants.OVN_EVPN_AS_EXT_ID_KEY: '123'}}) ret = self.sb_idl.get_evpn_info(port) expected_return = {} if not value_error: expected_return.update({'vni': 1001, 'bgp_as': 123}) self.assertEqual(expected_return, ret) def test_get_evpn_info(self): self._test_get_evpn_info() def test_get_evpn_info_value_error(self): self._test_get_evpn_info(value_error=True) def test_get_evpn_info_key_error(self): port = fakes.create_object({'logical_port': 'fake-port', 'external_ids': {}}) ret = self.sb_idl.get_evpn_info(port) self.assertEqual({}, ret) def _test_get_port_if_local_chassis(self, wrong_chassis=False): chassis = 'wrong-chassis' if wrong_chassis else 'chassis-0' with mock.patch.object(self.sb_idl, 'get_port_by_name') as mock_p: ch = fakes.create_object({'name': 'chassis-0'}) port = fakes.create_object({'chassis': [ch]}) mock_p.return_value = port ret = self.sb_idl.get_port_if_local_chassis('fake-port', chassis) if wrong_chassis: self.assertIsNone(ret) else: self.assertEqual(port, ret) def test_get_port_if_local_chassis(self): self._test_get_port_if_local_chassis() def test_get_port_if_local_chassis_wrong_chassis(self): self._test_get_port_if_local_chassis(wrong_chassis=True) def test_get_virtual_ports_on_datapath_by_chassis(self): with mock.patch.object(self.sb_idl, 'get_ports_on_datapath') as mock_p: ch1 = fakes.create_object({'name': 'chassis-1'}) ch2 = fakes.create_object({'name': 'chassis-2'}) port1 = fakes.create_object({'chassis': [ch1]}) port2 = fakes.create_object({'chassis': [ch2]}) mock_p.return_value = [port1, port2] ret = self.sb_idl.get_virtual_ports_on_datapath_by_chassis( 'fake-datapath', 'chassis-1') self.assertEqual([port1], ret) def test_get_ovn_lb(self): fake_lb_info = 'fake-lbinfo' lb = 'fake-lb' self.sb_idl.db_find_rows.return_value.execute.return_value = [ fake_lb_info] ret = self.sb_idl.get_ovn_lb(lb) self.assertEqual(fake_lb_info, ret) self.sb_idl.db_find_rows.assert_called_once_with( 'Load_Balancer', ('name', '=', lb)) def test_get_ovn_lb_empty(self): lb = 'fake-port' self.sb_idl.db_find_rows.return_value.execute.return_value = [] ret = self.sb_idl.get_ovn_lb(lb) self.assertEqual([], ret) self.sb_idl.db_find_rows.assert_called_once_with( 'Load_Balancer', ('name', '=', lb)) def test_get_provider_ovn_lbs_on_cr_lrp(self): lb1_name = 'ovn-lb-vip-fake-lb1' lb2_name = 'ovn-lb-vip-fake-lb2' provider_dp = 'fake-provider-dp' router_dp = ['fake-router-dp'] router_lrp = 'fake-router-lrp' dp1 = fakes.create_object({'datapaths': ['fake-subnet-dp']}) lb1 = fakes.create_object({'datapath_group': [dp1], 'name': 'fake-lb1'}) port0 = fakes.create_object({ 'logical_port': 'fake-port-0', 'external_ids': {constants.OVN_CIDRS_EXT_ID_KEY: '10.0.0.15/24', constants.OVN_PORT_NAME_EXT_ID_KEY: lb1_name}}) port1 = fakes.create_object({ 'logical_port': 'fake-port-1', 'external_ids': {constants.OVN_CIDRS_EXT_ID_KEY: '10.0.0.16/24'}}) port2 = fakes.create_object({ 'logical_port': 'fake-port-0', 'external_ids': {constants.OVN_CIDRS_EXT_ID_KEY: '10.0.0.17/24', constants.OVN_PORT_NAME_EXT_ID_KEY: lb2_name}}) self.sb_idl.db_find_rows.return_value.execute.return_value = [ port0, port1, port2] mock_lb = mock.patch.object(self.sb_idl, 'get_ovn_lb').start() mock_lb.side_effect = (lb1, []) mock_lrp = mock.patch.object(self.sb_idl, 'get_lrps_for_datapath').start() mock_lrp.return_value = [router_lrp] mock_get_port_dp = mock.patch.object(self.sb_idl, 'get_port_datapath').start() mock_get_port_dp.return_value = router_dp ret = self.sb_idl.get_provider_ovn_lbs_on_cr_lrp(provider_dp, router_dp) expected_return = {'fake-lb1': '10.0.0.15'} self.assertEqual(expected_return, ret) def test_get_ovn_vip_port(self): lb_name = 'ovn-lb-vip-fake-lb' lb1 = fakes.create_object( {'external_ids': { constants.OVN_PORT_NAME_EXT_ID_KEY: 'different-name'}}) lb2 = fakes.create_object( {'external_ids': {constants.OVN_PORT_NAME_EXT_ID_KEY: lb_name}}) self.sb_idl.db_find_rows.return_value.execute.return_value = [ lb1, lb2] ret = self.sb_idl.get_ovn_vip_port('fake-lb') self.assertEqual(lb2, ret) class TestOvnNbIdl(test_base.TestCase): def setUp(self): super(TestOvnNbIdl, self).setUp() mock.patch.object(idlutils, 'get_schema_helper').start() mock.patch.object(ovn_utils.OvnIdl, '__init__').start() self.nb_idl = ovn_utils.OvnNbIdl('tcp:127.0.0.1:6640') @mock.patch.object(Stream, 'ssl_set_ca_cert_file') @mock.patch.object(Stream, 'ssl_set_certificate_file') @mock.patch.object(Stream, 'ssl_set_private_key_file') def test__check_and_set_ssl_files( self, mock_ssl_priv_key, mock_ssl_cert, mock_ssl_ca_cert): CONF.set_override('ovn_nb_private_key', 'fake-priv-key', group='ovn') CONF.set_override('ovn_nb_certificate', 'fake-cert', group='ovn') CONF.set_override('ovn_nb_ca_cert', 'fake-ca-cert', group='ovn') self.nb_idl._check_and_set_ssl_files('fake-schema') mock_ssl_priv_key.assert_called_once_with('fake-priv-key') mock_ssl_cert.assert_called_once_with('fake-cert') mock_ssl_ca_cert.assert_called_once_with('fake-ca-cert') @mock.patch.object(connection, 'Connection') def test_start(self, mock_conn): notify_handler = mock.Mock() self.nb_idl.notify_handler = notify_handler self.nb_idl._events = ['fake-event0', 'fake-event1'] self.nb_idl.start() mock_conn.assert_called_once_with(self.nb_idl, timeout=180) notify_handler.watch_events.assert_called_once_with( ['fake-event0', 'fake-event1']) class TestOvnSbIdl(test_base.TestCase): def setUp(self): super(TestOvnSbIdl, self).setUp() mock.patch.object(idlutils, 'get_schema_helper').start() mock.patch.object(ovn_utils.OvnIdl, '__init__').start() self.sb_idl = ovn_utils.OvnSbIdl('tcp:127.0.0.1:6640') @mock.patch.object(Stream, 'ssl_set_ca_cert_file') @mock.patch.object(Stream, 'ssl_set_certificate_file') @mock.patch.object(Stream, 'ssl_set_private_key_file') def test__check_and_set_ssl_files( self, mock_ssl_priv_key, mock_ssl_cert, mock_ssl_ca_cert): CONF.set_override('ovn_sb_private_key', 'fake-priv-key', group='ovn') CONF.set_override('ovn_sb_certificate', 'fake-cert', group='ovn') CONF.set_override('ovn_sb_ca_cert', 'fake-ca-cert', group='ovn') self.sb_idl._check_and_set_ssl_files('fake-schema') mock_ssl_priv_key.assert_called_once_with('fake-priv-key') mock_ssl_cert.assert_called_once_with('fake-cert') mock_ssl_ca_cert.assert_called_once_with('fake-ca-cert') @mock.patch.object(connection, 'Connection') def test_start(self, mock_conn): notify_handler = mock.Mock() self.sb_idl.notify_handler = notify_handler self.sb_idl._events = ['fake-event0', 'fake-event1'] self.sb_idl.start() mock_conn.assert_called_once_with(self.sb_idl, timeout=180) notify_handler.watch_events.assert_called_once_with( ['fake-event0', 'fake-event1']) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovs.py000066400000000000000000000515301460327367600301560ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from ovsdbapp.schema.open_vswitch import impl_idl as idl_ovs from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.utils import ovs as ovs_utils from ovn_bgp_agent import exceptions as agent_exc from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.utils import linux_net class TestOVS(test_base.TestCase): def setUp(self): super(TestOVS, self).setUp() self.mock_ovs_vsctl = mock.patch( 'ovn_bgp_agent.privileged.ovs_vsctl').start() # Helper variables that are used across multiple methods self.bridge = 'br-fake' self.flows_info = {self.bridge: {'in_port': set()}} self.cookie = 'fake-cookie' self.cookie_id = 'cookie=%s/-1' % self.cookie self.mac = 'aa:bb:cc:dd:ee:ff' def _test_get_bridge_flows(self, has_filter=False): port_iface = '1' fake_flow_0 = '{},ip,in_port={}'.format(self.cookie_id, port_iface) fake_flow_1 = '{},ipv6,in_port={}'.format(self.cookie_id, port_iface) fake_filter = 'cookie=fake-cookie/-1' if has_filter else None flows = 'HEADER\n%s\n%s\n' % (fake_flow_0, fake_flow_1) self.mock_ovs_vsctl.ovs_cmd.return_value = [flows] ret = ovs_utils.get_bridge_flows(self.bridge, filter_=fake_filter) expected_args = ['dump-flows', self.bridge] if has_filter: expected_args.append(fake_filter) self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with( 'ovs-ofctl', expected_args) self.assertEqual([fake_flow_0, fake_flow_1], ret) def test_get_bridge_flows(self): self._test_get_bridge_flows() def test_get_bridge_flows_with_filters(self): self._test_get_bridge_flows(has_filter=True) def test_get_device_port_at_ovs(self): port = 'fake-port' port_iface = '1' self.mock_ovs_vsctl.ovs_cmd.return_value = port_iface ret = ovs_utils.get_device_port_at_ovs(port) self.assertEqual(port_iface, ret) self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with( 'ovs-vsctl', ['get', 'Interface', port, 'ofport']) def test_get_ovs_ports_info(self): bridge = 'fake-bridge' bridge_ports = ['br-ex'] self.mock_ovs_vsctl.ovs_cmd.return_value = bridge_ports ret = ovs_utils.get_ovs_ports_info(bridge) self.assertEqual(bridge_ports, ret) self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with( 'ovs-vsctl', ['list-ports', bridge]) def test_get_ovs_patch_port_ofport(self): patch = 'fake-patch' ofport = ['1'] self.mock_ovs_vsctl.ovs_cmd.return_value = ofport ret = ovs_utils.get_ovs_patch_port_ofport(patch) self.assertEqual(ofport[0], ret) self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with( 'ovs-vsctl', ['get', 'Interface', 'patch-fake-patch-to-br-int', 'ofport']) def test_get_ovs_patch_port_ofport_exception(self): patch = 'fake-patch' self.mock_ovs_vsctl.ovs_cmd.side_effect = Exception self.assertRaises(agent_exc.PatchPortNotFound, ovs_utils.get_ovs_patch_port_ofport, patch) expected_calls = [ mock.call('ovs-vsctl', ['get', 'Interface', 'patch-fake-patch-to-br-int', 'ofport']), mock.call('ovs-vsctl', ['get', 'Interface', 'patch-fake-patch-to-br-int', 'ofport']), mock.call('ovs-vsctl', ['get', 'Interface', 'patch-fake-patch-to-br-int', 'ofport']), mock.call('ovs-vsctl', ['get', 'Interface', 'patch-fake-patch-to-br-int', 'ofport']), mock.call('ovs-vsctl', ['get', 'Interface', 'patch-fake-patch-to-br-int', 'ofport'])] self.mock_ovs_vsctl.ovs_cmd.assert_has_calls(expected_calls) def test_get_ovs_patch_port_ofport_no_port(self): patch = 'fake-patch' ofport = ['[]'] self.mock_ovs_vsctl.ovs_cmd.return_value = ofport self.assertRaises(agent_exc.PatchPortNotFound, ovs_utils.get_ovs_patch_port_ofport, patch) expected_calls = [ mock.call('ovs-vsctl', ['get', 'Interface', 'patch-fake-patch-to-br-int', 'ofport']), mock.call('ovs-vsctl', ['get', 'Interface', 'patch-fake-patch-to-br-int', 'ofport']), mock.call('ovs-vsctl', ['get', 'Interface', 'patch-fake-patch-to-br-int', 'ofport']), mock.call('ovs-vsctl', ['get', 'Interface', 'patch-fake-patch-to-br-int', 'ofport']), mock.call('ovs-vsctl', ['get', 'Interface', 'patch-fake-patch-to-br-int', 'ofport'])] self.mock_ovs_vsctl.ovs_cmd.assert_has_calls(expected_calls) @mock.patch.object(ovs_utils, 'get_bridge_flows') def test_remove_extra_ovs_flows(self, mock_flows): port_iface = '1' extra_port_iface = '2' extra_mac = 'ff:ee:dd:cc:bb:aa' self.flows_info[self.bridge]['in_port'] = {port_iface} self.flows_info[self.bridge]['mac'] = self.mac expected_flow = ("cookie={},priority=900,ip,in_port={} " "actions=mod_dl_dst:{},NORMAL".format( self.cookie, port_iface, self.mac)) expected_flow_v6 = ("cookie={},priority=900,ipv6,in_port={} " "actions=mod_dl_dst:{},NORMAL".format( self.cookie, port_iface, self.mac)) extra_flow = ("cookie={},priority=900,ip,in_port={} " "actions=mod_dl_dst:{},NORMAL".format( self.cookie, extra_port_iface, extra_mac)) mock_flows.return_value = [expected_flow, expected_flow_v6, extra_flow] # Invoke the method ovs_utils.remove_extra_ovs_flows(self.flows_info, self.bridge, self.cookie) expected_del_flow = ('%s,ip,in_port=%s' % (self.cookie_id, extra_port_iface)) self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with( 'ovs-ofctl', ['del-flows', self.bridge, expected_del_flow]) mock_flows.assert_called_once_with(self.bridge, self.cookie_id) def test_ensure_flow(self): bridge = 'fake-bridge' flow = 'fake-flow' ovs_utils.ensure_flow(bridge, flow) self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with( 'ovs-ofctl', ['add-flow', bridge, flow]) @mock.patch.object(ovs_utils, 'get_device_port_at_ovs') @mock.patch.object(linux_net, 'get_interface_address') @mock.patch.object(linux_net, 'get_ip_version') def _test_ensure_evpn_ovs_flow(self, mock_ip_version, mock_nic_address, mock_ofport, ip_version, strip_vlan=False): address = '00:00:00:00:00:00' mock_ip_version.return_value = ip_version mock_nic_address.return_value = address port = 'fake-port' port_dst = 'fake-port-dst' ovs_port = constants.OVS_PATCH_PROVNET_PORT_PREFIX + 'fake-port' port_iface = '1' ovs_port_iface = '2' net = 'fake-net' self.mock_ovs_vsctl.ovs_cmd.side_effect = ( ['%s\n%s\n' % (port, ovs_port)], None) mock_ofport.side_effect = (ovs_port_iface, port_iface) # Invoke the method ovs_utils.ensure_evpn_ovs_flow( self.bridge, self.cookie, self.mac, port, port_dst, net, strip_vlan=strip_vlan) mock_ip_version.assert_called_once_with(net) strip_vlan_opt = 'strip_vlan,' if strip_vlan else '' if ip_version == constants.IP_VERSION_4: expected_flow = ( "cookie={},priority=1000,ip,in_port={},dl_src:{},nw_src={}" "actions=mod_dl_dst:{},{}output={}".format( self.cookie, ovs_port_iface, self.mac, net, address, strip_vlan_opt, port_iface)) else: expected_flow = ( "cookie={},priority=1000,ipv6,in_port={},dl_src:{}," "ipv6_src={} actions=mod_dl_dst:{},{}output={}".format( self.cookie, ovs_port_iface, self.mac, net, address, strip_vlan_opt, port_iface)) expected_calls = [ mock.call('ovs-vsctl', ['list-ports', self.bridge]), mock.call('ovs-ofctl', ['add-flow', self.bridge, expected_flow])] self.mock_ovs_vsctl.ovs_cmd.assert_has_calls(expected_calls) self.assertEqual(len(expected_calls), self.mock_ovs_vsctl.ovs_cmd.call_count) expected_calls_ofport = [mock.call(ovs_port), mock.call(port)] mock_ofport.assert_has_calls(expected_calls_ofport) self.assertEqual(len(expected_calls_ofport), mock_ofport.call_count) def test_ensure_evpn_ovs_flow_ipv4(self): self._test_ensure_evpn_ovs_flow(ip_version=constants.IP_VERSION_4) def test_ensure_evpn_ovs_flow_ipv4_strip_vlan(self): self._test_ensure_evpn_ovs_flow( ip_version=constants.IP_VERSION_4, strip_vlan=True) def test_ensure_evpn_ovs_flow_ipv6(self): self._test_ensure_evpn_ovs_flow(ip_version=constants.IP_VERSION_6) def test_ensure_evpn_ovs_flow_ipv6_strip_vlan(self): self._test_ensure_evpn_ovs_flow( ip_version=constants.IP_VERSION_6, strip_vlan=True) def test_ensure_evpn_ovs_flow_no_ovs_ports(self): port = 'non-patch-provnet-port' self.mock_ovs_vsctl.ovs_cmd.return_value = [port] ret = ovs_utils.ensure_evpn_ovs_flow( self.bridge, self.cookie, self.mac, port, 'fake-port-dst', 'fake-net') self.assertIsNone(ret) self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with( 'ovs-vsctl', ['list-ports', self.bridge]) @mock.patch.object(ovs_utils, 'get_device_port_at_ovs') def test_remove_evpn_router_ovs_flows(self, mock_ofport): ovs_port = constants.OVS_PATCH_PROVNET_PORT_PREFIX + 'fake-port' ovs_port_iface = '1' self.mock_ovs_vsctl.ovs_cmd.side_effect = ([ovs_port], None, None) mock_ofport.return_value = ovs_port_iface # Invoke the method ovs_utils.remove_evpn_router_ovs_flows( self.bridge, self.cookie, self.mac) expected_flow = '{},ip,in_port={},dl_src:{}'.format( self.cookie_id, ovs_port_iface, self.mac) expected_flow_v6 = '{},ipv6,in_port={},dl_src:{}'.format( self.cookie_id, ovs_port_iface, self.mac) expected_calls = [ mock.call('ovs-vsctl', ['list-ports', self.bridge]), mock.call('ovs-ofctl', ['del-flows', self.bridge, expected_flow]), mock.call('ovs-ofctl', ['del-flows', self.bridge, expected_flow_v6])] self.mock_ovs_vsctl.ovs_cmd.assert_has_calls(expected_calls) self.assertEqual(len(expected_calls), self.mock_ovs_vsctl.ovs_cmd.call_count) mock_ofport.assert_called_once_with(ovs_port) def test_remove_evpn_router_ovs_flows_no_ovs_port(self): port = 'non-patch-provnet-port' self.mock_ovs_vsctl.ovs_cmd.return_value = [port] ret = ovs_utils.remove_evpn_router_ovs_flows( self.bridge, self.cookie, self.mac) self.assertIsNone(ret) self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with( 'ovs-vsctl', ['list-ports', self.bridge]) @mock.patch.object(ovs_utils, 'get_device_port_at_ovs') @mock.patch.object(linux_net, 'get_ip_version') def _test_remove_evpn_network_ovs_flow(self, mock_ip_version, mock_ofport, ip_version): ovs_port = constants.OVS_PATCH_PROVNET_PORT_PREFIX + 'fake-port' ovs_port_iface = '1' net = 'fake-net' mock_ip_version.return_value = ip_version mock_ofport.return_value = ovs_port_iface self.mock_ovs_vsctl.ovs_cmd.side_effect = ([ovs_port], None) ovs_utils.remove_evpn_network_ovs_flow( self.bridge, self.cookie, self.mac, net) if ip_version == constants.IP_VERSION_6: expected_flow = ("{},ipv6,in_port={},dl_src:{},ipv6_src={}".format( self.cookie_id, ovs_port_iface, self.mac, net)) else: expected_flow = ("{},ip,in_port={},dl_src:{},nw_src={}".format( self.cookie_id, ovs_port_iface, self.mac, net)) expected_calls = [ mock.call('ovs-vsctl', ['list-ports', self.bridge]), mock.call('ovs-ofctl', ['del-flows', self.bridge, expected_flow])] self.mock_ovs_vsctl.ovs_cmd.assert_has_calls(expected_calls) self.assertEqual(len(expected_calls), self.mock_ovs_vsctl.ovs_cmd.call_count) mock_ip_version.assert_called_once_with(net) def test_remove_evpn_network_ovs_flow_ipv4(self): self._test_remove_evpn_network_ovs_flow( ip_version=constants.IP_VERSION_4) def test_remove_evpn_network_ovs_flow_ipv6(self): self._test_remove_evpn_network_ovs_flow( ip_version=constants.IP_VERSION_6) def test_remove_evpn_network_ovs_flow_no_ovs_port(self): port = 'non-patch-provnet-port' self.mock_ovs_vsctl.ovs_cmd.return_value = [port] ovs_utils.remove_evpn_network_ovs_flow( self.bridge, self.cookie, self.mac, 'fake-net') self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with( 'ovs-vsctl', ['list-ports', self.bridge]) def _test_add_device_to_ovs_bridge(self, vlan_tag=False): device = 'ethX' vtag = '1001' if vlan_tag else None ovs_utils.add_device_to_ovs_bridge(device, self.bridge, vlan_tag=vtag) expected_args = ['--may-exist', 'add-port', self.bridge, device] if vlan_tag: expected_args.append('tag=%s' % vtag) self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with( 'ovs-vsctl', expected_args) def test_add_device_to_ovs_bridge(self): self._test_add_device_to_ovs_bridge() def test_add_device_to_ovs_bridge_vlan_tag(self): self._test_add_device_to_ovs_bridge(vlan_tag=True) def _test_del_device_from_ovs_bridge(self, bridge=False): device = 'ethX' br = self.bridge if bridge else None ovs_utils.del_device_from_ovs_bridge(device, bridge=br) expected_args = ['--if-exists', 'del-port'] if bridge: expected_args.append(br) expected_args.append(device) self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with( 'ovs-vsctl', expected_args) def test_del_device_from_ovs_bridge(self): self._test_del_device_from_ovs_bridge() def test_del_device_from_ovs_bridge_specifying_bridge(self): self._test_del_device_from_ovs_bridge(bridge=True) def test_del_flow(self): flow = ('cookie=0x3e6, duration=11.647s, table=0, n_packets=0, ' 'n_bytes=0, idle_age=3378, priority=1000,ip,dl_src=fa:16:3e' ':15:9e:f0,nw_src=20.0.0.0/24 actions=mod_dl_dst:d2:33:c5:' 'fd:7c:42,output:3,in_port=1') ovs_utils.del_flow(flow, self.bridge, self.cookie) expected_flow = ('{},priority=1000,ip,dl_src=fa:16:3e:15:9e:f0,' 'nw_src=20.0.0.0/24'.format(self.cookie_id)) self.mock_ovs_vsctl.ovs_cmd.assert_called_once_with( 'ovs-ofctl', ['--strict', 'del-flows', self.bridge, expected_flow]) def test_get_flow_info(self): flow = ('cookie=0x3e6, duration=11.647s, table=0, n_packets=0, ' 'n_bytes=0, idle_age=3378, priority=1000,ip,dl_src=fa:16:3e' ':15:9e:f0,nw_src=20.0.0.0/24 actions=mod_dl_dst:d2:33:c5:' 'fd:7c:42,output:3,in_port=1') ret = ovs_utils.get_flow_info(flow) expected_ret = {'ipv6_src': None, 'mac': 'fa:16:3e:15:9e:f0', 'nw_src': '20.0.0.0/24', 'port': '3'} self.assertEqual(expected_ret, ret) def test_get_flow_info_ipv6(self): flow = ('cookie=0x3e6, duration=9.275s, table=0, n_packets=0, ' 'n_bytes=0, idle_age=14326, priority=1000,ipv6,in_port=1,' 'dl_src=fa:16:3e:15:9e:f0,ipv6_src=fdaa:4ad8:e8fb::/64 ' 'actions=mod_dl_dst:d2:33:c5:fd:7c:42,output:3') ret = ovs_utils.get_flow_info(flow) expected_ret = {'ipv6_src': 'fdaa:4ad8:e8fb::/64', 'mac': 'fa:16:3e:15:9e:f0', 'nw_src': None, 'port': '3'} self.assertEqual(expected_ret, ret) class TestOvsIdl(test_base.TestCase): def setUp(self): super(TestOvsIdl, self).setUp() self.ovs_idl = ovs_utils.OvsIdl() self.ovs_idl.idl_ovs = mock.Mock() self.execute_ref = self.ovs_idl.idl_ovs.db_get.return_value.execute @mock.patch('ovsdbapp.backend.ovs_idl.connection.Connection') @mock.patch('ovs.db.idl.Idl') @mock.patch('ovsdbapp.backend.ovs_idl.idlutils.get_schema_helper') def test_start(self, mock_schema_helper, mock_idl, mock_conn): conn_str = 'fake-connection' self.ovs_idl.start(conn_str) mock_schema_helper.assert_called_once_with(conn_str, 'Open_vSwitch') helper = mock_schema_helper.return_value expected_calls = [ mock.call('Open_vSwitch'), mock.call('Bridge'), mock.call('Port'), mock.call('Interface')] helper.register_table.assert_has_calls(expected_calls) mock_idl.assert_called_once_with(conn_str, helper) mock_conn.assert_called_once_with( mock_idl.return_value, timeout=mock.ANY) # Assert the OvsdbIdl instance was created self.assertIsInstance(self.ovs_idl.idl_ovs, idl_ovs.OvsdbIdl) def _test_ovs_ext_ids_getters(self, method, row, expected_return): self.execute_ref.return_value = row ret = method() self.assertEqual(expected_return, ret) self.ovs_idl.idl_ovs.db_get.assert_called_once_with( 'Open_vSwitch', '.', 'external_ids') def test_get_own_chassis_id(self): expected_return = 'fake-sys' row = {'system-id': expected_return} self._test_ovs_ext_ids_getters( self.ovs_idl.get_own_chassis_id, row, expected_return) def test_get_own_chassis_name(self): expected_return = 'fake-name' row = {'hostname': expected_return} self._test_ovs_ext_ids_getters( self.ovs_idl.get_own_chassis_name, row, expected_return) def test_get_ovn_remote(self): expected_return = 'fake-ovn-remote' row = {'ovn-remote': expected_return} self._test_ovs_ext_ids_getters( self.ovs_idl.get_ovn_remote, row, expected_return) def test_get_ovn_remote_nb(self): expected_return = 'fake-ovn-remote' row = {'ovn-nb-remote': expected_return} self.execute_ref.return_value = row ret = self.ovs_idl.get_ovn_remote(nb=True) self.assertEqual(expected_return, ret) self.ovs_idl.idl_ovs.db_get.assert_called_once_with( 'Open_vSwitch', '.', 'external_ids') def test_get_ovn_bridge_mappings(self): self.execute_ref.return_value = { 'ovn-bridge-mappings': 'net0:bridge0,net1:bridge1, net2:bridge2'} ret = self.ovs_idl.get_ovn_bridge_mappings() self.assertEqual(['net0:bridge0', 'net1:bridge1', 'net2:bridge2'], ret) self.ovs_idl.idl_ovs.db_get.assert_called_once_with( 'Open_vSwitch', '.', 'external_ids') def test_get_ovn_bridge_mappings_not_set(self): self.execute_ref.return_value = {} ret = self.ovs_idl.get_ovn_bridge_mappings() self.assertEqual([], ret) self.ovs_idl.idl_ovs.db_get.assert_called_once_with( 'Open_vSwitch', '.', 'external_ids') def test_get_ovn_bridge_mappings_bridge(self): bridge = 'bgp' self.execute_ref.return_value = { 'ovn-bridge-mappings-bgp': 'net0:bridge0,net1:bridge1, net2:bridge2'} ret = self.ovs_idl.get_ovn_bridge_mappings(bridge=bridge) self.assertEqual(['net0:bridge0', 'net1:bridge1', 'net2:bridge2'], ret) self.ovs_idl.idl_ovs.db_get.assert_called_once_with( 'Open_vSwitch', '.', 'external_ids') ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_wire.py000066400000000000000000000613171460327367600303210ustar00rootroot00000000000000# Copyright 2023 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_config import cfg from ovn_bgp_agent import constants 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 wire from ovn_bgp_agent import exceptions as agent_exc from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.utils import linux_net CONF = cfg.CONF class TestWire(test_base.TestCase): def setUp(self): super(TestWire, self).setUp() self.nb_idl = ovn_utils.OvsdbNbOvnIdl(mock.Mock()) self.sb_idl = ovn_utils.OvsdbSbOvnIdl(mock.Mock()) self.ovs_idl = mock.Mock() # Helper variables that are used across multiple methods self.bridge_mappings = 'datacentre:br-ex' # Monkey-patch parent class methods self.nb_idl.ls_add = mock.Mock() self.nb_idl.lr_add = mock.Mock() self.nb_idl.lrp_add = mock.Mock() self.nb_idl.lrp_set_gateway_chassis = mock.Mock() self.nb_idl.lrp_add_networks = mock.Mock() self.nb_idl.lr_route_add = mock.Mock() self.nb_idl.lr_policy_add = mock.Mock() @mock.patch.object(wire, '_ensure_base_wiring_config_underlay') def test_ensure_base_wiring_config(self, mock_underlay): wire.ensure_base_wiring_config(self.sb_idl, self.ovs_idl, routing_tables={}) mock_underlay.assert_called_once_with(self.sb_idl, self.ovs_idl, {}) @mock.patch.object(wire, '_ensure_base_wiring_config_ovn') def test_ensure_base_wiring_config_ovn(self, mock_ovn): CONF.set_override('exposing_method', 'ovn') self.addCleanup(CONF.clear_override, 'exposing_method') wire.ensure_base_wiring_config(self.sb_idl, self.ovs_idl, ovn_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_ovn') def test_ensure_base_wiring_config_not_implemeneted(self, mock_ovn, mock_underlay): CONF.set_override('exposing_method', 'vrf') self.addCleanup(CONF.clear_override, 'exposing_method') wire.ensure_base_wiring_config(self.sb_idl, self.ovs_idl, ovn_idl=self.nb_idl) mock_ovn.assert_not_called() mock_underlay.assert_not_called() def test__ensure_base_wiring_config_ovn(self): pass def test__ensure_ovn_router(self): wire._ensure_ovn_router(self.nb_idl) self.nb_idl.lr_add.assert_called_once_with( constants.OVN_CLUSTER_ROUTER, may_exist=True) @mock.patch.object(wire, '_execute_commands') @mock.patch.object(wire, '_ensure_lsp_cmds') def test__ensure_ovn_switch(self, m_ensure_lsp, m_cmds): ls_name = 'test-ls' localnet_port = "{}-localnet".format(ls_name) options = {'network_name': ls_name} wire._ensure_ovn_switch(self.nb_idl, ls_name) self.nb_idl.ls_add.assert_called_once_with(ls_name, may_exist=True) m_ensure_lsp.assert_called_once_with( self.nb_idl, localnet_port, ls_name, 'localnet', 'unknown', **options) @mock.patch.object(wire, '_execute_commands') @mock.patch.object(wire, '_ensure_lsp_cmds') def test__ensure_ovn_network_link_internal(self, m_ensure_lsp, m_cmds): switch_name = 'internal' provider_cidrs = ['172.16.0.0/16'] r_port_name = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER) options = {'router-port': r_port_name, 'arp_proxy': '172.16.0.0/16'} wire._ensure_ovn_network_link(self.nb_idl, switch_name, 'internal', provider_cidrs=provider_cidrs) self.nb_idl.lrp_add.assert_called_once_with( constants.OVN_CLUSTER_ROUTER, r_port_name, constants.OVN_CLUSTER_ROUTER_INTERNAL_MAC, provider_cidrs, peer=[], may_exist=True) m_ensure_lsp.assert_called_once_with( self.nb_idl, mock.ANY, switch_name, 'router', 'router', **options) self.nb_idl.lrp_set_gateway_chassis.assert_called_once_with( r_port_name, constants.OVN_CLUSTER_BRIDGE, 1) @mock.patch.object(wire, '_execute_commands') @mock.patch.object(wire, '_ensure_lsp_cmds') def test__ensure_ovn_network_link_internal_runtime_error( self, m_ensure_lsp, m_cmds): switch_name = 'internal' provider_cidrs = ['172.16.0.0/16'] r_port_name = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER) options = {'router-port': r_port_name, 'arp_proxy': '172.16.0.0/16'} self.nb_idl.lrp_add.side_effect = RuntimeError( 'with different networks') wire._ensure_ovn_network_link(self.nb_idl, switch_name, 'internal', provider_cidrs=provider_cidrs) self.nb_idl.lrp_add.assert_called_once_with( constants.OVN_CLUSTER_ROUTER, r_port_name, constants.OVN_CLUSTER_ROUTER_INTERNAL_MAC, provider_cidrs, peer=[], may_exist=True) self.nb_idl.lrp_add_networks.assert_called_once_with( r_port_name, provider_cidrs, may_exist=True) m_ensure_lsp.assert_called_once_with( self.nb_idl, mock.ANY, switch_name, 'router', 'router', **options) self.nb_idl.lrp_set_gateway_chassis.assert_called_once_with( r_port_name, constants.OVN_CLUSTER_BRIDGE, 1) @mock.patch.object(wire, '_ensure_lsp_cmds') def test__ensure_ovn_network_link_external(self, m_ensure_lsp): switch_name = 'external' ip = '1.1.1.2' mac = 'fake-map' r_port_name = "{}-{}".format(constants.OVN_CLUSTER_ROUTER, switch_name) options = {'router-port': r_port_name} wire._ensure_ovn_network_link(self.nb_idl, switch_name, 'external', ip=ip, mac=mac) self.nb_idl.lrp_add.assert_called_once_with( constants.OVN_CLUSTER_ROUTER, r_port_name, mac, [ip], peer=[], may_exist=True) m_ensure_lsp.assert_called_once_with( self.nb_idl, mock.ANY, switch_name, 'router', 'router', **options) def _ensure_ovn_policies(self, next_hops): if len(next_hops) > 1: columns = {'nexthops': next_hops} elif len(next_hops) == 1: columns = {'nexthop': next_hops[0]} wire._ensure_ovn_policies(self.nb_idl, next_hops) self.nb_idl.lr_policy_add.assert_called_once_with( constants.OVN_CLUSTER_ROUTER, 10, mock.ANY, 'reroute', may_exist=True, **columns) def test__ensure_ovn_policies_dual_nexthop(self): next_hops = ['1.1.1.1', '2.2.2.2'] self._ensure_ovn_policies(next_hops) def test__ensure_ovn_policies_single_nexthop(self): next_hops = ['1.1.1.1'] self._ensure_ovn_policies(next_hops) @mock.patch.object(wire, '_execute_commands') def test_ensure_ovn_routes(self, m_cmds): peer_ips = ['1.1.1.1'] wire._ensure_ovn_routes(self.nb_idl, peer_ips) self.nb_idl.lr_route_add.assert_called_once_with( constants.OVN_CLUSTER_ROUTER, '0.0.0.0/0', peer_ips[0], ecmp=True, may_exist=True) @mock.patch.object(ovs_utils, 'ensure_flow') @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(ovs_utils, 'get_ovs_ports_info') @mock.patch.object(ovs_utils, 'get_ovs_patch_ports_info') def test__ensure_ingress_flows(self, m_ovs_patch, m_ovs_get, m_ip_version, m_ovn_flow): CONF.set_override('external_nics', ['eth1'], group='local_ovn_cluster') self.addCleanup(CONF.clear_override, 'external_nics', group='local_ovn_cluster') bridge = 'br-ex' mac = 'fake-mac' switch_name = 'test-ls' provider_cidrs = ['172.16.0.0/16'] patch_port_prefix = 'patch-{}-'.format(switch_name) m_ovs_patch.return_value = ['fake-patch'] m_ovs_get.return_value = ['eth1'] wire._ensure_ingress_flows(bridge, mac, switch_name, provider_cidrs) m_ovs_patch.assert_called_once_with(bridge, prefix=patch_port_prefix) m_ovs_get.assert_called_once_with(bridge) m_ip_version.assert_called_once_with(provider_cidrs[0]) m_ovn_flow.assert_called_once_with(bridge, mock.ANY) @mock.patch.object(ovs_utils, 'get_ovs_patch_ports_info') def test__ensure_ingress_flows_no_network(self, m_ovs): bridge = 'br-ex' mac = 'fake-mac' switch_name = 'test-ls' provider_cidrs = [] wire._ensure_ingress_flows(bridge, mac, switch_name, provider_cidrs) m_ovs.assert_not_called() @mock.patch.object(ovs_utils, 'get_ovs_ports_info') @mock.patch.object(ovs_utils, 'get_ovs_patch_ports_info') def test__ensure_ingress_flows_no_patch_port(self, m_ovs_patch, m_ovs_get): bridge = 'br-ex' mac = 'fake-mac' switch_name = 'test-ls' provider_cidrs = ['172.16.0.0/16'] patch_port_prefix = 'patch-{}-'.format(switch_name) m_ovs_patch.return_value = [] wire._ensure_ingress_flows(bridge, mac, switch_name, provider_cidrs) m_ovs_patch.assert_called_once_with(bridge, prefix=patch_port_prefix) m_ovs_get.assert_not_called() @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(ovs_utils, 'get_ovs_ports_info') @mock.patch.object(ovs_utils, 'get_ovs_patch_ports_info') def test__ensure_ingress_flows_no_external_nic(self, m_ovs_patch, m_ovs_get, m_ip_version): bridge = 'br-ex' mac = 'fake-mac' switch_name = 'test-ls' provider_cidrs = ['172.16.0.0/16'] patch_port_prefix = 'patch-{}-'.format(switch_name) m_ovs_patch.return_value = ['fake-patch'] m_ovs_get.return_value = ['eth1'] wire._ensure_ingress_flows(bridge, mac, switch_name, provider_cidrs) m_ovs_patch.assert_called_once_with(bridge, prefix=patch_port_prefix) m_ovs_get.assert_called_once_with(bridge) m_ip_version.assert_not_called() @mock.patch.object(wire, '_cleanup_wiring_underlay') def test_cleanup_wiring_underlay(self, mock_underlay): ovs_flows = {} exposed_ips = {} routing_tables = {} routing_tables_routes = {} wire.cleanup_wiring(self.sb_idl, self.bridge_mappings, ovs_flows, exposed_ips, routing_tables, routing_tables_routes) mock_underlay.assert_called_once_with( self.sb_idl, self.bridge_mappings, ovs_flows, exposed_ips, routing_tables, routing_tables_routes) def test_cleanup_wiring_ovn(self): CONF.set_override('exposing_method', 'ovn') self.addCleanup(CONF.clear_override, 'exposing_method') ovs_flows = {} 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) self.assertTrue(ret) @mock.patch.object(wire, '_cleanup_wiring_underlay') def test_cleanup_wiring_not_implemeneted(self, mock_underlay): CONF.set_override('exposing_method', 'vrf') self.addCleanup(CONF.clear_override, 'exposing_method') ovs_flows = {} exposed_ips = {} routing_tables = {} routing_tables_routes = {} wire.cleanup_wiring(self.sb_idl, self.bridge_mappings, ovs_flows, exposed_ips, routing_tables, routing_tables_routes) mock_underlay.assert_not_called() @mock.patch.object(wire, '_wire_provider_port_underlay') def test_wire_provider_port_underlay(self, mock_underlay): routing_tables_routes = {} ovs_flows = {} port_ips = [] bridge_device = 'fake-bridge' bridge_vlan = '101' localnet = 'fake-localnet' routing_table = 5 proxy_cidrs = [] wire.wire_provider_port(routing_tables_routes, ovs_flows, port_ips, bridge_device, bridge_vlan, localnet, routing_table, proxy_cidrs) mock_underlay.assert_called_once_with( routing_tables_routes, ovs_flows, port_ips, bridge_device, bridge_vlan, localnet, routing_table, proxy_cidrs, lladdr=None) @mock.patch.object(wire, '_wire_provider_port_ovn') def test_wire_provider_port_ovn(self, mock_ovn): CONF.set_override('exposing_method', 'ovn') self.addCleanup(CONF.clear_override, 'exposing_method') routing_tables_routes = {} ovs_flows = {} port_ips = [] bridge_device = 'fake-bridge' bridge_vlan = '101' localnet = 'fake-localnet' routing_table = 5 proxy_cidrs = [] mac = 'fake-mac' 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) mock_ovn.assert_called_once_with(self.nb_idl, port_ips, mac) @mock.patch.object(wire, '_wire_provider_port_underlay') @mock.patch.object(wire, '_wire_provider_port_ovn') def test_wire_provider_port_not_implemented(self, mock_ovn, mock_underlay): CONF.set_override('exposing_method', 'vrf') self.addCleanup(CONF.clear_override, 'exposing_method') routing_tables_routes = {} ovs_flows = {} port_ips = [] bridge_device = 'fake-bridge' bridge_vlan = '101' localnet = 'fake-localnet' routing_table = 5 proxy_cidrs = [] wire.wire_provider_port(routing_tables_routes, ovs_flows, port_ips, bridge_device, bridge_vlan, localnet, routing_table, proxy_cidrs) mock_ovn.assert_not_called() mock_underlay.assert_not_called() @mock.patch.object(wire, '_execute_commands') def test__wire_provider_port_ovn(self, m_cmds): port_ips = ['1.1.1.1', '2.2.2.2'] mac = 'fake-mac' port = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER) wire._wire_provider_port_ovn(self.nb_idl, port_ips, mac) cmds = [ ovn_utils.StaticMACBindingAddCommand( self.nb_idl, port, port_ips[0], mac, True, may_exist=True), ovn_utils.StaticMACBindingAddCommand( self.nb_idl, port, port_ips[1], mac, True, may_exist=True)] # FIXME(ltomasbo): The standard assert called ones is not working # with the object # m_cmds.assert_called_once_with(self.nb_idl, cmds) # so we are checking this by comparing the object dict instead self.assertEqual( m_cmds.call_args_list[0][0][1][0].__dict__, cmds[0].__dict__ ) @mock.patch.object(wire, '_execute_commands') def test__wire_provider_port_ovn_no_action(self, m_cmds): port_ips = [] mac = 'fake-mac' wire._wire_provider_port_ovn(self.nb_idl, port_ips, mac) m_cmds.assert_not_called() @mock.patch.object(wire, '_unwire_provider_port_underlay') def test_unwire_provider_port_underlay(self, mock_underlay): 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, bridge_device, bridge_vlan, routing_table, proxy_cidrs) mock_underlay.assert_called_once_with( routing_tables_routes, port_ips, bridge_device, bridge_vlan, routing_table, proxy_cidrs, lladdr=None) @mock.patch.object(wire, '_unwire_provider_port_ovn') def test_unwire_provider_port_ovn(self, mock_ovn): CONF.set_override('exposing_method', 'ovn') 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, bridge_device, bridge_vlan, routing_table, proxy_cidrs, ovn_idl=self.nb_idl) 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_ovn') def test_unwire_provider_port_not_implemented(self, mock_ovn, mock_underlay): CONF.set_override('exposing_method', 'vrf') 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, bridge_device, bridge_vlan, routing_table, proxy_cidrs) mock_ovn.assert_not_called() mock_underlay.assert_not_called() @mock.patch.object(wire, '_execute_commands') def test__unwire_provider_port_ovn(self, m_cmds): port_ips = ['1.1.1.1'] port = "{}-openstack".format(constants.OVN_CLUSTER_ROUTER) wire._unwire_provider_port_ovn(self.nb_idl, port_ips) cmds = [ovn_utils.StaticMACBindingDelCommand( self.nb_idl, port, port_ips[0], if_exists=True)] # FIXME(ltomasbo): The standard assert called ones is not working # with the object # m_cmds.assert_called_once_with(self.nb_idl, cmds) # so we are checking this by comparing the object dict instead self.assertEqual( m_cmds.call_args_list[0][0][1][0].__dict__, cmds[0].__dict__ ) @mock.patch.object(wire, '_execute_commands') def test__unwire_provider_port_ovn_no_action(self, m_cmds): port_ips = [] wire._unwire_provider_port_ovn(self.nb_idl, port_ips) m_cmds.assert_not_called() @mock.patch.object(wire, '_wire_lrp_port_underlay') def test_wire_lrp_port_underlay(self, mock_underlay): routing_tables_routes = {} ip = 'fake-ip' bridge_device = 'fake-bridge' bridge_vlan = '101' routing_tables = {'fake-bridge': 5} cr_lrp_ips = ['fake-crlrp-ip'] wire.wire_lrp_port(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips) mock_underlay.assert_called_once_with( routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips) @mock.patch.object(wire, '_unwire_lrp_port_underlay') def test_unwire_lrp_port_underlay(self, mock_underlay): routing_tables_routes = {} ip = 'fake-ip' bridge_device = 'fake-bridge' bridge_vlan = '101' routing_tables = {'fake-bridge': 5} cr_lrp_ips = ['fake-crlrp-ip'] wire.unwire_lrp_port(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips) mock_underlay.assert_called_once_with( routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips) @mock.patch.object(linux_net, 'add_ip_route') @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(linux_net, 'add_ip_rule') def test__wire_lrp_port_underlay(self, m_ip_rule, m_ip_version, m_ip_route): routing_tables_routes = {} ip = '10.0.0.1/24' bridge_device = 'fake-bridge' bridge_vlan = '101' routing_tables = {'fake-bridge': 5} cr_lrp_ips = ['fake-crlrp-ip'] ret = wire._wire_lrp_port_underlay(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips) self.assertTrue(ret) m_ip_rule.assert_called_once_with(ip, 5) m_ip_route.assert_called_once_with( routing_tables_routes, '10.0.0.1', 5, 'fake-bridge', vlan='101', mask='24', via='fake-crlrp-ip') @mock.patch.object(linux_net, 'add_ip_rule') def test__wire_lrp_port_underlay_no_bridge(self, m_ip_rule): routing_tables_routes = {} ip = 'fake-ip' bridge_device = None bridge_vlan = None routing_tables = {'fake-bridge': 5} cr_lrp_ips = ['fake-crlrp-ip'] ret = wire._wire_lrp_port_underlay(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips) self.assertFalse(ret) m_ip_rule.assert_not_called() @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(linux_net, 'add_ip_rule') def test__wire_lrp_port_underlay_invalid_ip(self, m_ip_rule, m_ip_version): routing_tables_routes = {} ip = 'fake-ip' bridge_device = 'fake-bridge' bridge_vlan = '101' routing_tables = {'fake-bridge': 5} cr_lrp_ips = ['fake-crlrp-ip'] m_ip_rule.side_effect = agent_exc.InvalidPortIP(ip=ip) ret = wire._wire_lrp_port_underlay(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips) self.assertFalse(ret) m_ip_rule.assert_called_once_with(ip, 5) m_ip_version.assert_not_called() @mock.patch.object(linux_net, 'del_ip_route') @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(linux_net, 'del_ip_rule') def test__unwire_lrp_port_underlay(self, m_ip_rule, m_ip_version, m_ip_route): routing_tables_routes = {} ip = '10.0.0.1/24' bridge_device = 'fake-bridge' bridge_vlan = '101' routing_tables = {'fake-bridge': 5} cr_lrp_ips = ['fake-crlrp-ip'] ret = wire._unwire_lrp_port_underlay(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips) self.assertTrue(ret) m_ip_rule.assert_called_once_with(ip, 5) m_ip_route.assert_called_once_with( routing_tables_routes, '10.0.0.1', 5, 'fake-bridge', vlan='101', mask='24', via='fake-crlrp-ip') @mock.patch.object(linux_net, 'del_ip_rule') def test__unwire_lrp_port_underlay_no_bridge(self, m_ip_rule): routing_tables_routes = {} ip = 'fake-ip' bridge_device = None bridge_vlan = None routing_tables = {'fake-bridge': 5} cr_lrp_ips = ['fake-crlrp-ip'] ret = wire._unwire_lrp_port_underlay(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips) self.assertFalse(ret) m_ip_rule.assert_not_called() @mock.patch.object(linux_net, 'get_ip_version') @mock.patch.object(linux_net, 'del_ip_rule') def test__unwire_lrp_port_underlay_invalid_ip(self, m_ip_rule, m_ip_version): routing_tables_routes = {} ip = 'fake-ip' bridge_device = 'fake-bridge' bridge_vlan = '101' routing_tables = {'fake-bridge': 5} cr_lrp_ips = ['fake-crlrp-ip'] m_ip_rule.side_effect = agent_exc.InvalidPortIP(ip=ip) ret = wire._unwire_lrp_port_underlay(routing_tables_routes, ip, bridge_device, bridge_vlan, routing_tables, cr_lrp_ips) self.assertFalse(ret) m_ip_rule.assert_called_once_with(ip, 5) m_ip_version.assert_not_called() ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/000077500000000000000000000000001460327367600264125ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/__init__.py000066400000000000000000000000001460327367600305110ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_base_watcher.py000066400000000000000000000205321460327367600324540ustar00rootroot00000000000000# Copyright 2022 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.watchers import base_watcher from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.tests import utils class FakePortBindingChassisEvent(base_watcher.PortBindingChassisEvent): def run(self): pass class TestPortBindingChassisEvent(test_base.TestCase): def setUp(self): super(TestPortBindingChassisEvent, self).setUp() self.pb_event = FakePortBindingChassisEvent( mock.Mock(), [mock.Mock()]) def test__check_ip_associated(self): self.assertTrue(self.pb_event._check_ip_associated( 'aa:bb:cc:dd:ee:ff 10.10.1.16')) self.assertTrue(self.pb_event._check_ip_associated( 'aa:bb:cc:dd:ee:ff 10.10.1.16 10.10.1.17')) self.assertFalse(self.pb_event._check_ip_associated( 'aa:bb:cc:dd:ee:ff')) self.assertTrue(self.pb_event._check_ip_associated( 'aa:bb:cc:dd:ee:ff 10.10.1.16 10.10.1.17 10.10.1.18')) class FakeOVNLBEvent(base_watcher.OVNLBEvent): def run(self): pass class TestOVNLBEvent(test_base.TestCase): def setUp(self): super(TestOVNLBEvent, self).setUp() self.ovnlb_event = FakeOVNLBEvent( mock.Mock(), [mock.Mock()]) def test__get_router(self): row = utils.create_row( external_ids={constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-net'}) self.assertEqual('net', self.ovnlb_event._get_router( row, constants.OVN_LB_LR_REF_EXT_ID_KEY)) self.assertEqual('net', self.ovnlb_event._get_router(row)) row = utils.create_row( external_ids={constants.OVN_LR_NAME_EXT_ID_KEY: 'neutron-router1'}) self.assertEqual('router1', self.ovnlb_event._get_router( row, constants.OVN_LR_NAME_EXT_ID_KEY)) row = utils.create_row(external_ids={}) self.assertEqual(None, self.ovnlb_event._get_router(row)) def test__is_vip(self): row = utils.create_row( external_ids={constants.OVN_LB_VIP_IP_EXT_ID_KEY: '192.168.1.50', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) self.assertFalse(self.ovnlb_event._is_vip(row, '172.24.4.5')) self.assertTrue(self.ovnlb_event._is_vip(row, '192.168.1.50')) row = utils.create_row(external_ids={}) self.assertFalse(self.ovnlb_event._is_vip(row, '172.24.4.5')) self.assertFalse(self.ovnlb_event._is_vip(row, '192.168.1.50')) def test__is_fip(self): row = utils.create_row( external_ids={constants.OVN_LB_VIP_IP_EXT_ID_KEY: '192.168.1.50', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) self.assertTrue(self.ovnlb_event._is_fip(row, '172.24.4.5')) self.assertFalse(self.ovnlb_event._is_fip(row, '192.168.1.50')) row = utils.create_row(external_ids={}) self.assertFalse(self.ovnlb_event._is_fip(row, '172.24.4.5')) self.assertFalse(self.ovnlb_event._is_fip(row, '192.168.1.50')) def test__get_ip_from_vips(self): row = utils.create_row( external_ids={constants.OVN_LB_VIP_IP_EXT_ID_KEY: '192.168.1.50', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) self.assertEqual(self.ovnlb_event._get_ip_from_vips(row), ['192.168.1.50', '172.24.4.5']) class FakeLSPChassisEvent(base_watcher.LSPChassisEvent): def run(self): pass class TestLSPChassisEvent(test_base.TestCase): def setUp(self): super(TestLSPChassisEvent, self).setUp() self.lsp_event = FakeLSPChassisEvent( mock.Mock(), [mock.Mock()]) def test__check_ip_associated(self): self.assertTrue(self.lsp_event._check_ip_associated( 'aa:bb:cc:dd:ee:ff 10.10.1.16')) self.assertTrue(self.lsp_event._check_ip_associated( 'aa:bb:cc:dd:ee:ff 10.10.1.16 10.10.1.17')) self.assertFalse(self.lsp_event._check_ip_associated( 'aa:bb:cc:dd:ee:ff')) self.assertTrue(self.lsp_event._check_ip_associated( 'aa:bb:cc:dd:ee:ff 10.10.1.16 10.10.1.17 10.10.1.18')) def test__check_chassis_from_options(self): my_host = 'foo-host' # it is a VM port type, should use options field. row = utils.create_row( external_ids={constants.OVN_HOST_ID_EXT_ID_KEY: 'bar-host'}, options={constants.OVN_REQUESTED_CHASSIS: my_host}) self.assertEqual(self.lsp_event._get_chassis(row), (my_host, constants.OVN_CHASSIS_AT_OPTIONS)) def test__check_chassis_from_external_ids(self): my_host = 'foo-host' # it is a VM port type, should use options field. row = utils.create_row( external_ids={constants.OVN_HOST_ID_EXT_ID_KEY: my_host}) self.assertEqual(self.lsp_event._get_chassis(row), (my_host, constants.OVN_CHASSIS_AT_EXT_IDS)) def test__check_chassis_from_external_ids_virtual_port(self): my_host = 'foo-host' # it is a VM port type, should use options field. row = utils.create_row( external_ids={constants.OVN_HOST_ID_EXT_ID_KEY: my_host}, options={constants.OVN_REQUESTED_CHASSIS: 'bar-host'}, type=constants.OVN_VIRTUAL_VIF_PORT_TYPE) self.assertEqual(self.lsp_event._get_chassis(row), (my_host, constants.OVN_CHASSIS_AT_EXT_IDS)) def test__check_chassis_no_information(self): row = utils.create_row() self.assertEqual(self.lsp_event._get_chassis(row), (None, None)) def test__check_chassis_no_information_virtual_port(self): row = utils.create_row( options={constants.OVN_REQUESTED_CHASSIS: 'bar-host'}, type=constants.OVN_VIRTUAL_VIF_PORT_TYPE) self.assertEqual(self.lsp_event._get_chassis(row), (None, None)) def test__has_additional_binding(self): row = utils.create_row( options={constants.OVN_REQUESTED_CHASSIS: 'host1,host2'}) self.assertTrue(self.lsp_event._has_additional_binding(row)) def test__has_additional_binding_no_options(self): row = utils.create_row() self.assertFalse(self.lsp_event._has_additional_binding(row)) def test__has_additional_binding_single_host(self): row = utils.create_row( options={constants.OVN_REQUESTED_CHASSIS: 'host1'}) self.assertFalse(self.lsp_event._has_additional_binding(row)) def test__get_network(self): row = utils.create_row( external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'test-net'}) self.assertEqual('test-net', self.lsp_event._get_network(row)) row = utils.create_row(external_ids={}) self.assertEqual(None, self.lsp_event._get_network(row)) class FakeLRPChassisEvent(base_watcher.LRPChassisEvent): def run(self): pass class TestLRPChassisEvent(test_base.TestCase): def setUp(self): super(TestLRPChassisEvent, self).setUp() self.lrp_event = FakeLRPChassisEvent( mock.Mock(), [mock.Mock()]) def test__get_network(self): row = utils.create_row( external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'test-net'}) self.assertEqual('test-net', self.lrp_event._get_network(row)) row = utils.create_row(external_ids={}) self.assertEqual(None, self.lrp_event._get_network(row)) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_bgp_watcher.py000066400000000000000000001377001460327367600323200ustar00rootroot00000000000000# Copyright 2022 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_config import cfg from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.watchers import bgp_watcher from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.tests import utils CONF = cfg.CONF class TestPortBindingChassisCreatedEvent(test_base.TestCase): def setUp(self): super(TestPortBindingChassisCreatedEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.event = bgp_watcher.PortBindingChassisCreatedEvent(self.agent) def test_match_fn(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], up=[True]) old = utils.create_row(chassis=[]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_not_single_or_dual_stack(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[ch], mac=['aa:bb:cc:dd:ee:ff']) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_old_chassis_set(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], up=[True]) old = utils.create_row(chassis=[ch]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_no_old_chassis(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], up=[True]) old = utils.create_row(chassis=[]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_different_old_chassis(self): ch = utils.create_row(name=self.chassis) ch_old = utils.create_row(name='old-chassis') row = utils.create_row(chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], up=[True]) old = utils.create_row(chassis=[ch_old]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_no_up(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock)) def test_match_fn_no_old_up(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], up=[True]) old = utils.create_row(chassis=[ch], up=[False]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_index_error(self): row = utils.create_row(chassis=[], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_run(self): row = utils.create_row(type=constants.OVN_VIF_PORT_TYPES[0], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_ip.assert_called_once_with(['10.10.1.16'], row) def test_run_dual_stack(self): row = utils.create_row( type=constants.OVN_VIF_PORT_TYPES[0], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16 2002::1234:abcd:ffff:c0a8:101']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_ip.assert_called_once_with( ['10.10.1.16', '2002::1234:abcd:ffff:c0a8:101'], row) def test_run_wrong_type(self): row = utils.create_row(type='farofa', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_ip.assert_not_called() class TestPortBindingChassisDeletedEvent(test_base.TestCase): def setUp(self): super(TestPortBindingChassisDeletedEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.event = bgp_watcher.PortBindingChassisDeletedEvent(self.agent) def test_match_fn(self): event = self.event.ROW_DELETE ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[]) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_not_single_or_dual_stack(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[ch], mac=['aa:bb:cc:dd:ee:ff']) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_update(self): event = self.event.ROW_UPDATE ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[ch]) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_update_old_chassis_set_up_false(self): event = self.event.ROW_UPDATE ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], up=[True]) old = utils.create_row(chassis=[ch], up=[False]) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_update_old_chassis_set_up_true(self): event = self.event.ROW_UPDATE ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], up=[False]) old = utils.create_row(chassis=[ch], up=[True]) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_update_different_chassis_set_up_false(self): event = self.event.ROW_UPDATE ch = utils.create_row(name='other-chassis') row = utils.create_row(chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], up=[False]) old = utils.create_row(up=[True]) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_update_no_chassis(self): event = self.event.ROW_UPDATE ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[ch]) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_update_different_chassis(self): event = self.event.ROW_UPDATE ch = utils.create_row(name=self.chassis) ch_new = utils.create_row(name='new-chassis') row = utils.create_row(chassis=[ch_new], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[ch]) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_index_error(self): row = utils.create_row(chassis=[], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_run(self): row = utils.create_row(type=constants.OVN_VIF_PORT_TYPES[0], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_ip.assert_called_once_with(['10.10.1.16'], row) def test_run_dual_stack(self): row = utils.create_row( type=constants.OVN_VIF_PORT_TYPES[0], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16 2002::1234:abcd:ffff:c0a8:101']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_ip.assert_called_once_with( ['10.10.1.16', '2002::1234:abcd:ffff:c0a8:101'], row) def test_run_wrong_type(self): row = utils.create_row(type='farofa', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_ip.assert_not_called() class TestFIPSetEvent(test_base.TestCase): def setUp(self): super(TestFIPSetEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.event = bgp_watcher.FIPSetEvent(self.agent) def test_match_fn(self): row = utils.create_row(chassis=[], nat_addresses=['10.10.1.16'], logical_port='fake-lp') old = utils.create_row(nat_addresses=['10.10.1.17']) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_same_nat_addresses(self): row = utils.create_row(chassis=[], nat_addresses=['10.10.1.16'], logical_port='fake-lp') old = utils.create_row(nat_addresses=['10.10.1.16']) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_chassis_set(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[ch], nat_addresses=['10.10.1.16'], logical_port='fake-lp') old = utils.create_row(nat_addresses=['10.10.1.17']) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_lrp(self): row = utils.create_row(chassis=[], nat_addresses=['10.10.1.16'], logical_port='lrp-fake') old = utils.create_row(nat_addresses=['10.10.1.17']) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_attribute_errir(self): row = utils.create_row() old = utils.create_row(nat_addresses=['10.10.1.17']) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_run(self): row = utils.create_row( type=constants.OVN_PATCH_VIF_PORT_TYPE, nat_addresses=[ 'aa:aa:aa:aa:aa:aa 10.10.1.16 10.10.1.17 ' 'is_chassis_resident(\\"cr-lrp-aaaaaaaa-aaaa-aaaa-' 'aaaa-aaaaaaaaaaaa\\")']) old = utils.create_row( nat_addresses=[ 'aa:aa:aa:aa:aa:aa 10.10.1.16 10.10.1.18 ' 'is_chassis_resident(\\"cr-lrp-bbbbbbbb-bbbb-bbbb-' 'bbbb-bbbbbbbbbbbb\\")']) self.event.run(mock.Mock(), row, old) self.agent.expose_ip.assert_called_once_with( ['10.10.1.16', '10.10.1.17'], row, associated_port='cr-lrp-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\\') def test_run_same_port(self): row = utils.create_row( type=constants.OVN_PATCH_VIF_PORT_TYPE, nat_addresses=[ 'aa:aa:aa:aa:aa:aa 10.10.1.16 10.10.1.17 ' 'is_chassis_resident(\\"cr-lrp-aaaaaaaa-aaaa-aaaa-' 'aaaa-aaaaaaaaaaaa\\")']) old = utils.create_row( nat_addresses=[ 'aa:aa:aa:aa:aa:aa 10.10.1.16 10.10.1.18 ' 'is_chassis_resident(\\"cr-lrp-aaaaaaaa-aaaa-aaaa-' 'aaaa-aaaaaaaaaaaa\\")']) self.event.run(mock.Mock(), row, old) self.agent.expose_ip.assert_called_once_with( ['10.10.1.17'], row, associated_port='cr-lrp-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\\') def test_run_empty_old_nat_addresses(self): row = utils.create_row( type=constants.OVN_PATCH_VIF_PORT_TYPE, nat_addresses=[ 'aa:aa:aa:aa:aa:aa 10.10.1.16 10.10.1.17 ' 'is_chassis_resident(\\"cr-lrp-aaaaaaaa-aaaa-aaaa-' 'aaaa-aaaaaaaaaaaa\\")']) old = utils.create_row(nat_addresses=[]) self.event.run(mock.Mock(), row, old) self.agent.expose_ip.assert_called_once_with( ['10.10.1.16', '10.10.1.17'], row, associated_port='cr-lrp-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\\') def test_run_wrong_type(self): row = utils.create_row( type='feijoada', nat_addresses=[ 'aa:aa:aa:aa:aa:aa 10.10.1.16 10.10.1.17 ' 'is_chassis_resident(\\"cr-lrp-aaaaaaaa-aaaa-aaaa-' 'aaaa-aaaaaaaaaaaa\\")']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_ip.assert_not_called() class TestFIPUnsetEvent(test_base.TestCase): def setUp(self): super(TestFIPUnsetEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.event = bgp_watcher.FIPUnsetEvent(self.agent) def test_match_fn(self): row = utils.create_row(chassis=[], nat_addresses=['10.10.1.16'], logical_port='fake-lp') old = utils.create_row(nat_addresses=['10.10.1.17']) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_same_nat_addresses(self): row = utils.create_row(chassis=[], nat_addresses=['10.10.1.16'], logical_port='fake-lp') old = utils.create_row(nat_addresses=['10.10.1.16']) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_chassis_set(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(chassis=[ch], nat_addresses=['10.10.1.16'], logical_port='fake-lp') old = utils.create_row(nat_addresses=['10.10.1.17']) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_lrp(self): row = utils.create_row(chassis=[], nat_addresses=['10.10.1.16'], logical_port='lrp-fake') old = utils.create_row(nat_addresses=['10.10.1.17']) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_attribute_errir(self): row = utils.create_row() old = utils.create_row(nat_addresses=['10.10.1.17']) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_run(self): row = utils.create_row( type=constants.OVN_PATCH_VIF_PORT_TYPE, nat_addresses=[ 'aa:aa:aa:aa:aa:aa 10.10.1.16 10.10.1.17 ' 'is_chassis_resident(\\"cr-lrp-aaaaaaaa-aaaa-aaaa-' 'aaaa-aaaaaaaaaaaa\\")']) old = utils.create_row( nat_addresses=[ 'aa:aa:aa:aa:aa:aa 10.10.1.16 10.10.1.18 ' 'is_chassis_resident(\\"cr-lrp-bbbbbbbb-bbbb-bbbb-' 'bbbb-bbbbbbbbbbbb\\")']) self.event.run(mock.Mock(), row, old) self.agent.withdraw_ip.assert_called_once_with( ['10.10.1.16', '10.10.1.18'], row, associated_port='cr-lrp-bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb\\') def test_run_same_port(self): row = utils.create_row( type=constants.OVN_PATCH_VIF_PORT_TYPE, nat_addresses=[ 'aa:aa:aa:aa:aa:aa 10.10.1.16 10.10.1.17 ' 'is_chassis_resident(\\"cr-lrp-aaaaaaaa-aaaa-aaaa-' 'aaaa-aaaaaaaaaaaa\\")']) old = utils.create_row( nat_addresses=[ 'aa:aa:aa:aa:aa:aa 10.10.1.16 10.10.1.18 ' 'is_chassis_resident(\\"cr-lrp-aaaaaaaa-aaaa-aaaa-' 'aaaa-aaaaaaaaaaaa\\")']) self.event.run(mock.Mock(), row, old) self.agent.withdraw_ip.assert_called_once_with( ['10.10.1.18'], row, associated_port='cr-lrp-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\\') def test_run_empty_row_nat_addresses(self): row = utils.create_row( type=constants.OVN_PATCH_VIF_PORT_TYPE, nat_addresses=[]) old = utils.create_row( nat_addresses=[ 'aa:aa:aa:aa:aa:aa 10.10.1.16 10.10.1.17 ' 'is_chassis_resident(\\"cr-lrp-aaaaaaaa-aaaa-aaaa-' 'aaaa-aaaaaaaaaaaa\\")']) self.event.run(mock.Mock(), row, old) self.agent.withdraw_ip.assert_called_once_with( ['10.10.1.16', '10.10.1.17'], row, associated_port='cr-lrp-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\\') def test_run_wrong_type(self): row = utils.create_row( type='feijoada', nat_addresses=[ 'aa:aa:aa:aa:aa:aa 10.10.1.16 10.10.1.17 ' 'is_chassis_resident(\\"cr-lrp-aaaaaaaa-aaaa-aaaa-' 'aaaa-aaaaaaaaaaaa\\")']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_ip.assert_not_called() class TestSubnetRouterAttachedEvent(test_base.TestCase): def setUp(self): super(TestSubnetRouterAttachedEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.event = bgp_watcher.SubnetRouterAttachedEvent(self.agent) def test_match_fn(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={}) self.assertTrue(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_single_or_dual_stack(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff'], options={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_lrp(self): row = utils.create_row(chassis=[], logical_port='fake-lp', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_chassis_redirect(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={'chassis-redirect-port': True}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_chassis_set(self): row = utils.create_row(chassis=[mock.Mock()], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_index_error(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=[], options={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): row = utils.create_row(type=constants.OVN_PATCH_VIF_PORT_TYPE, mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_subnet.assert_called_once_with('10.10.1.16', row) def test_run_wrong_type(self): row = utils.create_row(type='coxinha', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_subnet.assert_not_called() class TestSubnetRouterUpdateEvent(test_base.TestCase): def setUp(self): super(TestSubnetRouterUpdateEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.event = bgp_watcher.SubnetRouterUpdateEvent(self.agent) def test_match_fn(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={}) self.assertTrue(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_single_or_dual_stack(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff'], options={}) old = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff'], options={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_not_lrp(self): row = utils.create_row(chassis=[], logical_port='fake-lp', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_chassis_redirect(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={'chassis-redirect-port': True}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_chassis_set(self): row = utils.create_row(chassis=[mock.Mock()], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_mac_not_changed(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={}) old = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_mac_changed(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={}) old = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16 10.10.1.17'], options={}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_index_error(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=[], options={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): row = utils.create_row(type=constants.OVN_PATCH_VIF_PORT_TYPE, mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(type=constants.OVN_PATCH_VIF_PORT_TYPE, mac=['aa:bb:cc:dd:ee:ff']) self.event.run(mock.Mock(), row, old) self.agent.update_subnet.assert_called_once_with(old, row) def test_run_wrong_type(self): row = utils.create_row(type='coxinha', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(type='coxinha', mac=['aa:bb:cc:dd:ee:ff']) self.event.run(mock.Mock(), row, old) self.agent.update_subnet.assert_not_called() class TestSubnetRouterDetachedEvent(test_base.TestCase): def setUp(self): super(TestSubnetRouterDetachedEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.event = bgp_watcher.SubnetRouterDetachedEvent(self.agent) def test_match_fn(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={}) self.assertTrue(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_single_or_dual_stack(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff'], options={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_lrp(self): row = utils.create_row(chassis=[], logical_port='fake-lp', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_chassis_redirect(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={'chassis-redirect-port': True}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_chassis_set(self): row = utils.create_row(chassis=[mock.Mock()], logical_port='lrp-fake', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], options={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_index_error(self): row = utils.create_row(chassis=[], logical_port='lrp-fake', mac=[], options={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): row = utils.create_row(type=constants.OVN_PATCH_VIF_PORT_TYPE, mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_subnet.assert_called_once_with('10.10.1.16', row) def test_run_wrong_type(self): row = utils.create_row(type='coxinha', mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_subnet.assert_not_called() class TestTenantPortCreatedEvent(test_base.TestCase): def setUp(self): super(TestTenantPortCreatedEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.agent.ovn_local_lrps = ['172.24.100.111'] self.event = bgp_watcher.TenantPortCreatedEvent(self.agent) def test_match_fn(self): row = utils.create_row(chassis=[mock.Mock()], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_unknown_mac(self): event = self.event.ROW_UPDATE row = utils.create_row(chassis=[mock.Mock()], mac=['unknown'], external_ids={ 'neutron:cidrs': '10.10.1.16/24'}) old = utils.create_row(chassis=[], mac=['unknown'], external_ids={ 'neutron:cidrs': '10.10.1.16/24'}) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_unknown_mac_no_cidr(self): event = self.event.ROW_UPDATE row = utils.create_row(chassis=[mock.Mock()], mac=['unknown'], external_ids={}) old = utils.create_row(chassis=[], mac=['unknown'], external_ids={}) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_no_chassis(self): row = utils.create_row(mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_not_single_or_dual_stack(self): row = utils.create_row(mac=['aa:bb:cc:dd:ee:ff']) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_old_chassis_set(self): row = utils.create_row(chassis=[mock.Mock()], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[mock.Mock()]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_empty_ovn_local_lrps(self): self.agent.ovn_local_lrps = [] row = utils.create_row(chassis=[mock.Mock()], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_index_error(self): row = utils.create_row(chassis=[mock.Mock()], mac=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_remote_ip.assert_called_once_with( ['10.10.1.16'], row) def test_run_unknown_mac(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, mac=['unknown'], external_ids={ 'neutron:cidrs': '10.10.1.16/24'}) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_remote_ip.assert_called_once_with( ['10.10.1.16'], row) def test_run_dual_stack(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, mac=['aa:bb:cc:dd:ee:ff 10.10.1.16 2002::1234:abcd:ffff:c0a8:101']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_remote_ip.assert_called_once_with( ['10.10.1.16', '2002::1234:abcd:ffff:c0a8:101'], row) def test_run_wrong_type(self): row = utils.create_row(type='brigadeiro') self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_remote_ip.assert_not_called() class TestTenantPortDeletedEvent(test_base.TestCase): def setUp(self): super(TestTenantPortDeletedEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.agent.ovn_local_lrps = ['172.24.100.111'] self.event = bgp_watcher.TenantPortDeletedEvent(self.agent) def test_match_fn(self): event = self.event.ROW_UPDATE row = utils.create_row(chassis=[], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[mock.Mock()], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_unknown_mac(self): event = self.event.ROW_UPDATE row = utils.create_row(chassis=[], mac=['unknown'], external_ids={ 'neutron:cidrs': '192.168.1.10/24'}) old = utils.create_row(chassis=[mock.Mock()], mac=['unknown'], external_ids={ 'neutron:cidrs': '192.168.1.10/24'}) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_unknown_mac_no_cidr(self): event = self.event.ROW_UPDATE row = utils.create_row(chassis=[], mac=['unknown'], external_ids={}) old = utils.create_row(chassis=[mock.Mock()], mac=['unknown'], external_ids={}) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_delete(self): event = self.event.ROW_DELETE row = utils.create_row(chassis=[], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.assertTrue(self.event.match_fn(event, row, mock.Mock())) def test_match_fn_not_single_or_dual_stack(self): row = utils.create_row(mac=['aa:bb:cc:dd:ee:ff']) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_empty_ovn_local_lrps(self): row = utils.create_row(chassis=[], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[mock.Mock()], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.agent.ovn_local_lrps = [] self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_index_error(self): row = utils.create_row(mac=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): event = self.event.ROW_UPDATE row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], chassis=[mock.Mock()]) self.event.run(event, row, mock.Mock()) self.agent.withdraw_remote_ip.assert_called_once_with( ['10.10.1.16'], row, mock.ANY) def test_run_unknown_mac(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, chassis=[mock.Mock()], mac=['unknown'], external_ids={ 'neutron:cidrs': '10.10.1.16/24'}) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_remote_ip.assert_called_once_with( ['10.10.1.16'], row, mock.ANY) def test_run_dual_stack(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, mac=['aa:bb:cc:dd:ee:ff 10.10.1.16 2002::1234:abcd:ffff:c0a8:101'], chassis=[mock.Mock()]) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_remote_ip.assert_called_once_with( ['10.10.1.16', '2002::1234:abcd:ffff:c0a8:101'], row, mock.ANY) def test_run_wrong_type(self): row = utils.create_row(type='brigadeiro') self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_remote_ip.assert_not_called() def test_run_delete(self): event = self.event.ROW_DELETE row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], chassis=[mock.Mock()]) self.event.run(event, row, []) self.agent.withdraw_remote_ip.assert_called_once_with( ['10.10.1.16'], row, mock.ANY) class OVNLBVIPPortEvent(test_base.TestCase): def setUp(self): super(OVNLBVIPPortEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.agent.ovn_local_cr_lrps = {'fake-cr-lrp-port': {}} self.event = bgp_watcher.OVNLBVIPPortEvent(self.agent) def test_match_fn(self): row = utils.create_row(chassis=[], mac=[], up=[False]) self.assertTrue(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_chassis(self): row = utils.create_row(chassis=[mock.Mock()], mac=[], up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_mac(self): row = utils.create_row(chassis=[], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], up=['False']) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_up(self): row = utils.create_row(chassis=[], mac=[], up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_empty_ovn_local_cr_lrps(self): self.agent.ovn_local_cr_lrps = [] row = utils.create_row(chassis=[], mac=[], up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_attribute_error(self): row = utils.create_row(mac=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): event = self.event.ROW_CREATE row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, mac=[], chassis=[], up=[False], external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "10.10.1.16/24"}) self.event.run(event, row, mock.Mock()) self.agent.expose_ovn_lb.assert_called_once_with( '10.10.1.16', row) def test_run_delete(self): event = self.event.ROW_DELETE row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, mac=[], chassis=[], up=[False], external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "10.10.1.16/24"}) self.event.run(event, row, mock.Mock()) self.agent.withdraw_ovn_lb.assert_called_once_with( '10.10.1.16', row) def test_run_no_external_id(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, mac=[], chassis=[], up=[False], external_ids={}) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_ovn_lb.assert_not_called() self.agent.withdraw_ovn_lb.assert_not_called() def test_run_wrong_type(self): row = utils.create_row( type=constants.OVN_VIRTUAL_VIF_PORT_TYPE, mac=[], chassis=[], up=[False], external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "10.10.1.16/24"}) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_ovn_lb.assert_not_called() self.agent.withdraw_ovn_lb.assert_not_called() class TestOVNLBMemberCreateEvent(test_base.TestCase): def setUp(self): super(TestOVNLBMemberCreateEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.agent.ovn_local_cr_lrps = { 'cr-lrp1': {'provider_datapath': 'dp1', 'subnets_datapath': {'lrp1': 's_dp1'}, 'ovn_lbs': 'ovn-lb1'}} self.event = bgp_watcher.OVNLBMemberCreateEvent(self.agent) def test_match_fn(self): self.assertTrue(self.event.match_fn(mock.Mock(), mock.Mock(), mock.Mock())) def test_match_fn_no_cr_lrp(self): self.agent.ovn_local_cr_lrps = {} self.assertFalse(self.event.match_fn(mock.Mock(), mock.Mock(), mock.Mock())) def test_run(self): dpg1 = utils.create_row(_uuid='fake_dp_group', datapaths=['s_dp1']) row = utils.create_row(name='ovn-lb1', datapath_group=[dpg1], vips={'172.24.100.66:80': '10.0.0.5:8080'}) vip_port = utils.create_row( datapath='dp1', logical_port='ovn-lb-port-1', external_ids={constants.OVN_CIDRS_EXT_ID_KEY: '172.24.100.66/26'}) self.agent.sb_idl.get_ovn_vip_port.return_value = vip_port self.agent.sb_idl.get_lrps_for_datapath.return_value = ['fake-lrp'] self.agent.sb_idl.get_port_datapath.return_value = 'r_dp' self.event.run(self.event.ROW_CREATE, row, mock.Mock()) self.agent.expose_ovn_lb_on_provider.assert_called_once_with( '172.24.100.66', row.name, 'cr-lrp1') self.agent.withdraw_ovn_lb_on_provider.assert_not_called() def test_run_no_subnets_datapath(self): CONF.set_override('expose_tenant_networks', False) self.addCleanup(CONF.clear_override, 'expose_tenant_networks') dpg1 = utils.create_row(_uuid='fake_dp_group', datapaths=['s_dp1']) row = utils.create_row(name='ovn-lb1', ls_datapath_group=[dpg1], vips={'172.24.100.66:80': '10.0.0.5:8080'}) self.agent.ovn_local_cr_lrps = { 'cr-lrp1': {'provider_datapath': 'dp1', 'router_datapath': 'r_dp', 'subnets_datapath': {}, 'ovn_lbs': 'ovn-lb1'}} vip_port = utils.create_row( datapath='dp1', logical_port='ovn-lb-port-1', external_ids={constants.OVN_CIDRS_EXT_ID_KEY: '172.24.100.66/26'}) self.agent.sb_idl.get_ovn_vip_port.return_value = vip_port self.agent.sb_idl.get_lrps_for_datapath.return_value = ['fake-lrp'] self.agent.sb_idl.get_port_datapath.return_value = 'r_dp' self.event.run(self.event.ROW_CREATE, row, mock.Mock()) self.agent.expose_ovn_lb_on_provider.assert_called_once_with( '172.24.100.66', row.name, 'cr-lrp1') self.agent.withdraw_ovn_lb_on_provider.assert_not_called() def test_run_no_vip_port(self): dpg1 = utils.create_row(_uuid='fake_dp_group', datapaths=['s_dp1']) row = utils.create_row(name='ovn-lb1', lr_datapath_group=[dpg1], vips={'172.24.100.66:80': '10.0.0.5:8080'}) self.agent.sb_idl.get_ovn_vip_port.return_value = [] self.event.run(self.event.ROW_CREATE, row, mock.Mock()) self.agent.expose_ovn_lb_on_provider.assert_not_called() self.agent.withdraw_ovn_lb_on_provider.assert_not_called() def test_run_different_provider(self): dpg1 = utils.create_row(_uuid='fake_dp_group', datapaths=['s_dp1']) row = utils.create_row(name='ovn-lb1', datapath_group=[dpg1], vips={'172.24.100.66:80': '10.0.0.5:8080'}) vip_port = utils.create_row( datapath='dp2', logical_port='ovn-lb-port-1', external_ids={constants.OVN_CIDRS_EXT_ID_KEY: '172.24.100.66/26'}) self.agent.sb_idl.get_lrps_for_datapath.return_value = ['fake-lrp'] self.agent.sb_idl.get_port_datapath.return_value = 'dp2' self.agent.sb_idl.get_ovn_vip_port.return_value = vip_port self.event.run(self.event.ROW_CREATE, row, mock.Mock()) self.agent.expose_ovn_lb_on_provider.assert_not_called() self.agent.withdraw_ovn_lb_on_provider.assert_not_called() def test_run_no_cr_lrp_match(self): dpg1 = utils.create_row(_uuid='fake_dp_group', datapaths=['s_dp2']) row = utils.create_row(name='ovn-lb1', ls_datapath_group=[dpg1], vips={'172.24.100.66:80': '10.0.0.5:8080'}) vip_port = utils.create_row( datapath='dp1', logical_port='ovn-lb-port-1', external_ids={constants.OVN_CIDRS_EXT_ID_KEY: '172.24.100.66/26'}) self.agent.sb_idl.get_lrps_for_datapath.return_value = ['fake-lrp'] self.agent.sb_idl.get_port_datapath.return_value = 'r_dp' self.agent.sb_idl.get_ovn_vip_port.return_value = vip_port self.event.run(self.event.ROW_CREATE, row, mock.Mock()) self.agent.expose_ovn_lb_on_provider.assert_not_called() self.agent.withdraw_ovn_lb_on_provider.assert_not_called() def test_run_no_vip(self): dpg1 = utils.create_row(_uuid='fake_dp_group', datapaths=['s_dp1']) row = utils.create_row(name='ovn-lb1', lr_datapath_group=[dpg1], vips={'172.24.100.66:80': '10.0.0.5:8080'}) vip_port = utils.create_row( datapath='dp1', logical_port='port-1', external_ids={}) self.agent.sb_idl.get_lrps_for_datapath.return_value = ['fake-lrp'] self.agent.sb_idl.get_port_datapath.return_value = 'r_dp' self.agent.sb_idl.get_ovn_vip_port.return_value = vip_port self.event.run(self.event.ROW_CREATE, row, mock.Mock()) self.agent.expose_ovn_lb_on_provider.assert_not_called() self.agent.withdraw_ovn_lb_on_provider.assert_not_called() class TestOVNLBMemberDeleteEvent(test_base.TestCase): def setUp(self): super(TestOVNLBMemberDeleteEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.agent.provider_ovn_lbs = { 'ovn-lb1': {'ips': ['fake-ip'], 'gateway_port': 'cr-lrp1'}} self.event = bgp_watcher.OVNLBMemberDeleteEvent(self.agent) def test_match_fn(self): row = utils.create_row(name='ovn-lb1') self.assertTrue(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_no_lb(self): row = utils.create_row(name='ovn-lb2') self.agent.ovn_local_cr_lrps = {} self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): dpg1 = utils.create_row(_uuid='fake_dp_group', datapaths=['s_dp1']) row = utils.create_row(name='ovn-lb1', datapath_group=[dpg1], vips={'172.24.100.66:80': '10.0.0.5:8080'}) vip_port = utils.create_row( datapath='dp1', logical_port='ovn-lb-port-1', external_ids={constants.OVN_CIDRS_EXT_ID_KEY: '172.24.100.66/26'}) self.agent.sb_idl.get_lrps_for_datapath.return_value = ['fake-lrp'] self.agent.sb_idl.get_port_datapath.return_value = 'r_dp' self.agent.sb_idl.get_ovn_vip_port.return_value = vip_port self.event.run(self.event.ROW_DELETE, row, mock.Mock()) self.agent.withdraw_ovn_lb_on_provider.assert_called_once_with( row.name, 'cr-lrp1') self.agent.expose_ovn_lb_on_provider.assert_not_called() class TestLocalnetCreateDeleteEvent(test_base.TestCase): def setUp(self): super(TestLocalnetCreateDeleteEvent, self).setUp() self.agent = mock.Mock() self.event = bgp_watcher.LocalnetCreateDeleteEvent(self.agent) def test_match_fn(self): row = utils.create_row(type=constants.OVN_LOCALNET_VIF_PORT_TYPE) self.assertTrue(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_match(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): self.event.run(mock.Mock(), mock.Mock(), mock.Mock()) self.agent.sync.assert_called_once() class TestChassisCreateEvent(test_base.TestCase): _event = bgp_watcher.ChassisCreateEvent def setUp(self): super(TestChassisCreateEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.event = self._event(self.agent) def test_run(self): self.assertTrue(self.event.first_time) self.event.run(mock.Mock(), mock.Mock(), mock.Mock()) self.assertFalse(self.event.first_time) self.agent.sync.assert_not_called() def test_run_not_first_time(self): self.event.first_time = False self.event.run(mock.Mock(), mock.Mock(), mock.Mock()) self.agent.sync.assert_called_once_with() class TestChassisPrivateCreateEvent(TestChassisCreateEvent): _event = bgp_watcher.ChassisPrivateCreateEvent ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_evpn_watcher.py000066400000000000000000000544561460327367600325260ustar00rootroot00000000000000# Copyright 2022 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.watchers import evpn_watcher from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.tests import utils class TestPortBindingChassisCreatedEvent(test_base.TestCase): def setUp(self): super(TestPortBindingChassisCreatedEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.event = evpn_watcher.PortBindingChassisCreatedEvent(self.agent) def test_match_fn(self): ch = utils.create_row(name=self.chassis) row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_not_single_or_dual_stack(self): ch = utils.create_row(name=self.chassis) row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, chassis=[ch], mac=['aa:bb:cc:dd:ee:ff']) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_old_chassis_set(self): ch = utils.create_row(name=self.chassis) row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=self.chassis) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_index_error(self): row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, chassis=[], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_wrong_type(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(type='farofa', chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_run(self): row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_ip.assert_called_once_with(row, cr_lrp=True) def test_run_dual_stack(self): row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_ip.assert_called_once_with(row, cr_lrp=True) def test_run_wrong_type(self): row = utils.create_row(type='farofa') self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_ip.assert_not_called() class TestPortBindingChassisDeletedEvent(test_base.TestCase): def setUp(self): super(TestPortBindingChassisDeletedEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.event = evpn_watcher.PortBindingChassisDeletedEvent(self.agent) def test_match_fn(self): ch = utils.create_row(name=self.chassis) row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_not_single_or_dual_stack(self): ch = utils.create_row(name=self.chassis) row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, chassis=[ch], mac=['aa:bb:cc:dd:ee:ff']) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_update(self): event = self.event.ROW_UPDATE ch = utils.create_row(name=self.chassis) row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, chassis=[], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[ch]) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_update_old_chassis_set(self): event = self.event.ROW_UPDATE ch = utils.create_row(name=self.chassis) row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, chassis=[ch], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=self.chassis) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_index_error(self): row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, chassis=[], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_wrong_type(self): row = utils.create_row( type='farofa', chassis=[], mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_run(self): row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_ip.assert_called_once_with(row, cr_lrp=True) def test_run_wrong_type(self): row = utils.create_row(type='farofa') self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_ip.assert_not_called() class TestSubnetRouterAttachedEvent(test_base.TestCase): def setUp(self): super(TestSubnetRouterAttachedEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.event = evpn_watcher.SubnetRouterAttachedEvent(self.agent) def test_match_fn(self): ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[], logical_port='fake-lrp', external_ids=ext_ids) self.assertTrue(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_chassis_set(self): ch = utils.create_row(name=self.chassis) ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[ch], logical_port='fake-lrp', external_ids=ext_ids) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_lrp(self): ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[], logical_port='lrp-fake', external_ids=ext_ids) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_missing_ext_ids(self): row = utils.create_row( chassis=[], logical_port='fake-lrp', external_ids={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_update(self): event = self.event.ROW_UPDATE ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[], logical_port='fake-lrp', external_ids=ext_ids) old = utils.create_row(external_ids={}) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_update_chassis_set(self): event = self.event.ROW_UPDATE ch = utils.create_row(name=self.chassis) ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[ch], logical_port='fake-lrp', external_ids=ext_ids) old = utils.create_row(external_ids={}) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_update_lrp(self): event = self.event.ROW_UPDATE ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[], logical_port='lrp-fake', external_ids=ext_ids) old = utils.create_row(external_ids={}) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_update_missing_ext_ids(self): event = self.event.ROW_UPDATE row = utils.create_row( chassis=[], logical_port='fake-lrp', external_ids={}) old = utils.create_row(external_ids={}) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_update_old_ext_ids(self): event = self.event.ROW_UPDATE ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[], logical_port='fake-lrp', external_ids=ext_ids) old = utils.create_row(external_ids=ext_ids) self.assertFalse(self.event.match_fn(event, row, old)) def test_run(self): row = utils.create_row( type=constants.OVN_PATCH_VIF_PORT_TYPE, nat_addresses=[]) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_subnet.assert_called_once_with(row) def test_run_nat_addresses(self): row = utils.create_row( type=constants.OVN_PATCH_VIF_PORT_TYPE, nat_addresses=['10.10.1.16']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_ip.assert_called_once_with(row) def test_run_wrong_type(self): row = utils.create_row(type='farofa') self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_ip.assert_not_called() self.agent.expose_subnet.assert_not_called() class TestSubnetRouterDetachedEvent(test_base.TestCase): def setUp(self): super(TestSubnetRouterDetachedEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.event = evpn_watcher.SubnetRouterDetachedEvent(self.agent) def test_match_fn(self): ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[], logical_port='fake-lrp', external_ids=ext_ids) self.assertTrue(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_chassis_set(self): ch = utils.create_row(name=self.chassis) ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[ch], logical_port='fake-lrp', external_ids=ext_ids) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_lrp(self): ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[], logical_port='lrp-fake', external_ids=ext_ids) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_missing_ext_ids(self): row = utils.create_row( chassis=[], logical_port='fake-lrp', external_ids={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_update(self): event = self.event.ROW_UPDATE ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[], logical_port='fake-lrp', external_ids={}) old = utils.create_row(external_ids=ext_ids) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_update_chassis_set(self): event = self.event.ROW_UPDATE ch = utils.create_row(name=self.chassis) ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[ch], logical_port='fake-lrp', external_ids={}) old = utils.create_row(external_ids=ext_ids) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_update_lrp(self): event = self.event.ROW_UPDATE ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[], logical_port='lrp-fake', external_ids={}) old = utils.create_row(external_ids=ext_ids) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_update_missing_ext_ids(self): event = self.event.ROW_UPDATE row = utils.create_row( chassis=[], logical_port='fake-lrp', external_ids={}) old = utils.create_row(external_ids={}) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_update_row_ext_ids(self): event = self.event.ROW_UPDATE ext_ids = {constants.OVN_EVPN_VNI_EXT_ID_KEY: 'fake-vni-id', constants.OVN_EVPN_AS_EXT_ID_KEY: 'fake-as-id'} row = utils.create_row( chassis=[], logical_port='fake-lrp', external_ids=ext_ids) old = utils.create_row(external_ids=ext_ids) self.assertFalse(self.event.match_fn(event, row, old)) def test_run(self): row = utils.create_row( type=constants.OVN_PATCH_VIF_PORT_TYPE, nat_addresses=[]) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_subnet.assert_called_once_with(row) def test_run_nat_addresses(self): row = utils.create_row( type=constants.OVN_PATCH_VIF_PORT_TYPE, nat_addresses=['10.10.1.16']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_ip.assert_called_once_with(row) def test_run_wrong_type(self): row = utils.create_row(type='farofa') self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_ip.assert_not_called() self.agent.withdraw_subnet.assert_not_called() class TestTenantPortCreatedEvent(test_base.TestCase): def setUp(self): super(TestTenantPortCreatedEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.agent.ovn_local_lrps = ['172.24.100.111'] self.event = evpn_watcher.TenantPortCreatedEvent(self.agent) def test_match_fn(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], chassis=[ch]) old = utils.create_row(chassis=[]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_not_single_or_dual_stack(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(mac=['aa:bb:cc:dd:ee:ff'], chassis=[ch]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_old_chassis_set(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], chassis=[ch]) old = utils.create_row(chassis=[ch]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_empty_ovn_local_lrps(self): ch = utils.create_row(name=self.chassis) self.agent.ovn_local_lrps = [] row = utils.create_row(mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], chassis=[ch]) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_index_error(self): row = utils.create_row(mac=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_remote_ip.assert_called_once_with( ['10.10.1.16'], row) def test_run_dual_stack(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, mac=['aa:bb:cc:dd:ee:ff 10.10.1.16 2002::1234:abcd:ffff:c0a8:101']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_remote_ip.assert_called_once_with( ['10.10.1.16', '2002::1234:abcd:ffff:c0a8:101'], row) def test_run_wrong_type(self): row = utils.create_row(type='brigadeiro') self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_remote_ip.assert_not_called() class TestTenantPortDeletedEvent(test_base.TestCase): def setUp(self): super(TestTenantPortDeletedEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.agent.ovn_local_lrps = ['172.24.100.111'] self.event = evpn_watcher.TenantPortDeletedEvent(self.agent) def test_match_fn(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], chassis=[ch]) self.assertTrue(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_single_or_dual_stack(self): ch = utils.create_row(name=self.chassis) row = utils.create_row(mac=['aa:bb:cc:dd:ee:ff'], chassis=[ch]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_empty_ovn_local_lrps(self): ch = utils.create_row(name=self.chassis) self.agent.ovn_local_lrps = [] row = utils.create_row(mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], chassis=[ch]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_update_old_chassis_set(self): event = self.event.ROW_UPDATE ch = utils.create_row(name=self.chassis) row = utils.create_row(mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], chassis=[ch]) old = utils.create_row(chassis=[ch]) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_update_empty_ovn_local_lrps(self): event = self.event.ROW_UPDATE ch = utils.create_row(name=self.chassis) self.agent.ovn_local_lrps = [] row = utils.create_row(mac=['aa:bb:cc:dd:ee:ff 10.10.1.16'], chassis=[ch]) old = utils.create_row(chassis=[]) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_index_error(self): row = utils.create_row(mac=[]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, mac=['aa:bb:cc:dd:ee:ff 10.10.1.16']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_remote_ip.assert_called_once_with( ['10.10.1.16'], row) def test_run_dual_stack(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, mac=['aa:bb:cc:dd:ee:ff 10.10.1.16 2002::1234:abcd:ffff:c0a8:101']) self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_remote_ip.assert_called_once_with( ['10.10.1.16', '2002::1234:abcd:ffff:c0a8:101'], row) def test_run_wrong_type(self): row = utils.create_row(type='brigadeiro') self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_remote_ip.assert_not_called() class TestLocalnetCreateDeleteEvent(test_base.TestCase): def setUp(self): super(TestLocalnetCreateDeleteEvent, self).setUp() self.agent = mock.Mock() self.event = evpn_watcher.LocalnetCreateDeleteEvent(self.agent) def test_match_fn(self): row = utils.create_row(type=constants.OVN_LOCALNET_VIF_PORT_TYPE) self.assertTrue(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_match(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): self.event.run(mock.Mock(), mock.Mock(), mock.Mock()) self.agent.sync.assert_called_once() class TestChassisCreateEvent(test_base.TestCase): _event = evpn_watcher.ChassisCreateEvent def setUp(self): super(TestChassisCreateEvent, self).setUp() self.chassis = '935f91fa-b8f8-47b9-8b1b-3a7a90ef7c26' self.agent = mock.Mock(chassis=self.chassis) self.event = self._event(self.agent) def test_run(self): self.assertTrue(self.event.first_time) self.event.run(mock.Mock(), mock.Mock(), mock.Mock()) self.assertFalse(self.event.first_time) self.agent.sync.assert_not_called() def test_run_not_first_time(self): self.event.first_time = False self.event.run(mock.Mock(), mock.Mock(), mock.Mock()) self.agent.sync.assert_called_once_with() class TestChassisPrivateCreateEvent(TestChassisCreateEvent): _event = evpn_watcher.ChassisPrivateCreateEvent ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_nb_bgp_watcher.py000066400000000000000000002052211460327367600327710ustar00rootroot00000000000000# Copyright 2023 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from ovn_bgp_agent import constants from ovn_bgp_agent.drivers.openstack.watchers import nb_bgp_watcher from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.tests import utils class TestLogicalSwitchPortProviderCreateEvent(test_base.TestCase): def setUp(self): super(TestLogicalSwitchPortProviderCreateEvent, self).setUp() self.chassis = 'fake-chassis' self.agent = mock.Mock(chassis=self.chassis) self.agent.ovn_local_lrps = { 'net1': ['10.0.0.5']} # Assume the logical switch has been setup properly. self.agent.is_ls_provider.return_value = True # Assume the ip is not exposed yet self.agent.is_ip_exposed.return_value = False self.event = nb_bgp_watcher.LogicalSwitchPortProviderCreateEvent( self.agent) def test_match_fn(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, up=[True]) old = utils.create_row(options={}, up=[True]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_port_up(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, up=[True]) old = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, up=[False]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_external_id(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], external_ids={'neutron:host_id': self.chassis}, up=[True]) old = utils.create_row(external_ids={}, up=[True]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_exception(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_up(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_invalid_address(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac '], options={'requested-chassis': self.chassis}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_wrong_chassis(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': 'fake_chassis'}, up=[True]) old = utils.create_row(options={}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_tenant_create(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, up=[True]) old = utils.create_row(options={}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_additional_bindings(self): event = self.event.ROW_UPDATE bindings = ','.join([self.chassis, 'other-chassis']) row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': bindings}, up=[True]) old = utils.create_row(options={'requested-chassis': self.chassis}) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_wrong_type(self): row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, up=[True]) self.assertIsNone(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, external_ids={'neutron:host_id': self.chassis, constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, up=[True]) ips_info = { 'mac': 'mac', 'cidrs': [], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': 'test-ls', } self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_ip.assert_called_once_with(['192.168.0.1'], ips_info) class TestLogicalSwitchPortProviderDeleteEvent(test_base.TestCase): def setUp(self): super(TestLogicalSwitchPortProviderDeleteEvent, self).setUp() self.chassis = 'fake-chassis' self.agent = mock.Mock(chassis=self.chassis) self.agent.ovn_local_lrps = { 'net1': ['10.0.0.5']} # Assume the logical switch has been setup properly. self.agent.is_ls_provider.return_value = True # Assume the ip is exposed self.agent.is_ip_exposed.return_value = True self.event = nb_bgp_watcher.LogicalSwitchPortProviderDeleteEvent( self.agent) def test_match_fn_delete(self): event = self.event.ROW_DELETE row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, up=[True]) self.assertTrue(self.event.match_fn(event, row, mock.Mock())) def test_match_fn_update(self): event = self.event.ROW_UPDATE row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, up=[False]) old = utils.create_row(options={'requested-chassis': self.chassis}, up=[True]) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_update_chassis(self): event = self.event.ROW_UPDATE row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], external_ids={'neutron:host_id': 'chassis2'}, up=[True]) old = utils.create_row(external_ids={'neutron:host_id': self.chassis}, up=[True]) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_update_status_different_chassis(self): # Update test assumption, since the ip should not be exposed self.agent.is_ip_exposed.return_value = False event = self.event.ROW_UPDATE row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': 'other-chassis'}, up=[False]) old = utils.create_row(options={'requested-chassis': 'other-chassis'}, up=[True]) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_exception(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_up(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, up=[False]) old = utils.create_row(options={'requested-chassis': self.chassis}, up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_ignore_not_up_with_additional_bindings(self): event = self.event.ROW_UPDATE bindings = ','.join([self.chassis, 'other-chassis']) row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': bindings}, up=[False]) old = utils.create_row(options={'requested-chassis': self.chassis}, up=[True]) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_invalid_address(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac '], options={'requested-chassis': self.chassis}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_wrong_chassis(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, up=[True]) old = utils.create_row(options={'requested-chassis': 'other_chassis'}) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_tenant_delete(self): event = self.event.ROW_DELETE row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, up=[True]) self.assertFalse(self.event.match_fn(event, row, mock.Mock())) def test_match_fn_wrong_type(self): row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, up=[True]) self.assertIsNone(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, external_ids={'neutron:host_id': self.chassis, constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, up=[True]) ips_info = { 'mac': 'mac', 'cidrs': [], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': 'test-ls' } self.event.run(mock.Mock(), row, mock.Mock()) self.agent.withdraw_ip.assert_called_once_with(['192.168.0.1'], ips_info) class TestLogicalSwitchPortFIPCreateEvent(test_base.TestCase): def setUp(self): super(TestLogicalSwitchPortFIPCreateEvent, self).setUp() self.chassis = 'fake-chassis' self.agent = mock.Mock(chassis=self.chassis) # Assume the logical switch has been setup properly. self.agent.is_ls_provider.return_value = True # Assume the ip is not exposed yet self.agent.is_ip_exposed.return_value = False self.event = nb_bgp_watcher.LogicalSwitchPortFIPCreateEvent( self.agent) def test_match_fn_chassis_change(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[True]) old = utils.create_row(options={}, up=[True]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_chassis_change_external_ids(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], external_ids={ constants.OVN_HOST_ID_EXT_ID_KEY: self.chassis, constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[True]) old = utils.create_row(external_ids={}, up=[True]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_status_change(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[True]) old = utils.create_row(options={'requested-chassis': self.chassis}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[False]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_fip_addition(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[True]) old = utils.create_row(options={'requested-chassis': self.chassis}, external_ids={}, up=[True]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_no_fip(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, external_ids={}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_wrong_chassis(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': 'wrong_chassis'}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_port_down(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_wrong_address(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac '], options={'requested-chassis': self.chassis}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_exception(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_wrong_type(self): row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE) self.assertIsNone(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): external_ip = '10.0.0.10' ls_name = 'neutron-net-id' self.agent.get_port_external_ip_and_ls.return_value = (external_ip, 'mac', ls_name) row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], name='net-id') self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_fip.assert_called_once_with(external_ip, 'mac', ls_name, row) def test_run_no_external_ip(self): external_ip = None ls_name = 'logical_switch' self.agent.get_port_external_ip_and_ls.return_value = (external_ip, 'mac', ls_name) row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], name='net-id') self.event.run(mock.Mock(), row, mock.Mock()) self.agent.expose_fip.assert_not_called() class TestLogicalSwitchPortFIPDeleteEvent(test_base.TestCase): def setUp(self): super(TestLogicalSwitchPortFIPDeleteEvent, self).setUp() self.chassis = 'fake-chassis' self.agent = mock.Mock(chassis=self.chassis) # Assume the logical switch has been setup properly. self.agent.is_ls_provider.return_value = True # Assume the ip is exposed self.agent.is_ip_exposed.return_value = True self.event = nb_bgp_watcher.LogicalSwitchPortFIPDeleteEvent( self.agent) def test_match_fn_delete(self): event = self.event.ROW_DELETE row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[True]) self.assertTrue(self.event.match_fn(event, row, utils.create_row())) def test_match_fn_update(self): event = self.event.ROW_UPDATE row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[False]) old = utils.create_row(up=[True]) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_update_different_chassis(self): # Update test assumption, since the ip should not be exposed self.agent.is_ip_exposed.return_value = False event = self.event.ROW_UPDATE row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': 'other-chassis'}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[False]) old = utils.create_row(up=[True]) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_update_external_id(self): event = self.event.ROW_UPDATE row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], external_ids={ constants.OVN_HOST_ID_EXT_ID_KEY: 'other-chassis', constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[True]) old = utils.create_row(external_ids={ constants.OVN_HOST_ID_EXT_ID_KEY: self.chassis, constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_update_external_id_remove_fip(self): event = self.event.ROW_UPDATE row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], external_ids={ constants.OVN_HOST_ID_EXT_ID_KEY: self.chassis}, up=[True]) old = utils.create_row(external_ids={ constants.OVN_HOST_ID_EXT_ID_KEY: self.chassis, constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}) self.assertTrue(self.event.match_fn(event, row, old)) def test_match_fn_update_external_id_no_fip(self): event = self.event.ROW_UPDATE row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], external_ids={ constants.OVN_HOST_ID_EXT_ID_KEY: self.chassis}, up=[True]) old = utils.create_row(external_ids={ constants.OVN_HOST_ID_EXT_ID_KEY: self.chassis}) self.assertFalse(self.event.match_fn(event, row, old)) def test_match_fn_exception(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], up=[False]) old = utils.create_row() self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_not_up(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[False]) old = utils.create_row(options={'requested-chassis': self.chassis}, up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_invalid_address(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac '], options={'requested-chassis': self.chassis}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_wrong_chassis(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[True]) old = utils.create_row(options={'requested-chassis': 'other_chassis'}) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_chassis_update(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': 'other_chassis'}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[True]) old = utils.create_row(options={'requested-chassis': self.chassis}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_fip_update(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 192.168.0.1'], options={'requested-chassis': self.chassis}, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'new-fip-ip'}, up=[True]) old = utils.create_row( external_ids={constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_wrong_type(self): row = utils.create_row( type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE) self.assertIsNone(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, external_ids={ constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'}, up=[True]) self.event.run(mock.Mock(), row, utils.create_row()) self.agent.withdraw_fip.assert_called_once_with('fip-ip', row) def test_run_no_fip(self): row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, external_ids={}) old = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE, external_ids={}) self.event.run(mock.Mock(), row, old) self.agent.withdraw_fip.assert_not_called() class TestLocalnetCreateDeleteEvent(test_base.TestCase): def setUp(self): super(TestLocalnetCreateDeleteEvent, self).setUp() self.agent = mock.Mock() self.event = nb_bgp_watcher.LocalnetCreateDeleteEvent( self.agent) def test_match_fn(self): row = utils.create_row(type=constants.OVN_LOCALNET_VIF_PORT_TYPE) self.assertTrue(self.event.match_fn(None, row, None)) row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE) self.assertFalse(self.event.match_fn(None, row, None)) def test_run(self): row = utils.create_row(type=constants.OVN_LOCALNET_VIF_PORT_TYPE) self.event.run(None, row, None) self.agent.sync.assert_called_once() class TestChassisRedirectCreateEvent(test_base.TestCase): def setUp(self): super(TestChassisRedirectCreateEvent, self).setUp() self.chassis = 'fake-chassis' self.chassis_id = 'fake-chassis-id' self.agent = mock.Mock(chassis=self.chassis, chassis_id=self.chassis_id) self.event = nb_bgp_watcher.ChassisRedirectCreateEvent( self.agent) def test_match_fn(self): row = utils.create_row(mac='fake-mac', networks=['192.168.0.2/24'], status={'hosting-chassis': self.chassis_id}) old = utils.create_row(mac='fake-mac', networks=['192.168.0.2/24'], status={}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_exception(self): row = utils.create_row(mac='fake-mac', networks=['192.168.0.2/24']) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_no_status_change(self): row = utils.create_row(mac='fake-mac', networks=['192.168.0.2/24'], status={'hosting-chassis': self.chassis_id}) old = utils.create_row() self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_different_chassis(self): row = utils.create_row(mac='fake-mac', networks=['192.168.0.2/24'], status={'hosting-chassis': 'other_chassis'}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_no_networks(self): row = utils.create_row( mac='fake-mac', networks=[], status={'hosting-chassis': self.chassis_id}, external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}) self.assertFalse(self.event.match_fn(None, row, None)) def test_run(self): row = utils.create_row( mac='fake-mac', networks=['192.168.0.2/24'], status={'hosting-chassis': self.chassis_id}, external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}) ips_info = {'mac': 'fake-mac', 'cidrs': ['192.168.0.2/24'], 'type': constants.OVN_CR_LRP_PORT_TYPE, 'logical_switch': 'test-ls', 'router': None} self.event.run(None, row, None) self.agent.expose_ip.assert_called_once_with(['192.168.0.2'], ips_info) class TestChassisRedirectDeleteEvent(test_base.TestCase): def setUp(self): super(TestChassisRedirectDeleteEvent, self).setUp() self.chassis = 'fake-chassis' self.chassis_id = 'fake-chassis-id' self.agent = mock.Mock(chassis=self.chassis, chassis_id=self.chassis_id) self.event = nb_bgp_watcher.ChassisRedirectDeleteEvent( self.agent) def test_match_fn(self): row = utils.create_row(mac='fake-mac', networks=['192.168.0.2/24'], status={}) old = utils.create_row(mac='fake-mac', networks=['192.168.0.2/24'], status={'hosting-chassis': self.chassis_id}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_delete(self): event = self.event.ROW_DELETE row = utils.create_row(mac='fake-mac', networks=['192.168.0.2/24'], status={'hosting-chassis': self.chassis_id}) self.assertTrue(self.event.match_fn(event, row, mock.Mock())) def test_match_fn_exception(self): row = utils.create_row(mac='fake-mac', networks=['192.168.0.2/24']) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_no_status_change(self): row = utils.create_row(mac='fake-mac', networks=['192.168.0.2/24'], status={'hosting-chassis': self.chassis_id}) old = utils.create_row(mac='fake-mac', networks=['192.168.0.2/24'], status={'hosting-chassis': self.chassis_id}) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_different_chassis(self): row = utils.create_row(mac='fake-mac', networks=['192.168.0.2/24'], status={'hosting-chassis': self.chassis_id}) old = utils.create_row(mac='fake-mac', networks=['192.168.0.2/24'], status={'hosting-chassis': 'different_chassis'}) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_no_networks(self): row = utils.create_row( mac='fake-mac', networks=[], status={'hosting-chassis': self.chassis_id}, external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}) self.assertFalse(self.event.match_fn(None, row, None)) def test_run(self): row = utils.create_row( mac='fake-mac', networks=['192.168.0.2/24'], status={'hosting-chassis': self.chassis_id}, external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}) ips_info = {'mac': 'fake-mac', 'cidrs': ['192.168.0.2/24'], 'type': constants.OVN_CR_LRP_PORT_TYPE, 'logical_switch': 'test-ls', 'router': None} self.event.run(None, row, None) self.agent.withdraw_ip.assert_called_once_with(['192.168.0.2'], ips_info) class TestLogicalSwitchPortSubnetAttachEvent(test_base.TestCase): def setUp(self): super(TestLogicalSwitchPortSubnetAttachEvent, self).setUp() self.chassis = 'fake-chassis' self.chassis_id = 'fake-chassis-id' self.agent = mock.Mock(chassis=self.chassis, chassis_id=self.chassis_id) self.agent.ovn_local_cr_lrps = { 'router1': {'bridge_device': 'br-ex', 'bridge_vlan': None, 'ips': ['172.24.16.2']}} self.event = nb_bgp_watcher.LogicalSwitchPortSubnetAttachEvent( self.agent) def test_match_fn(self): row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[True]) old = utils.create_row(up=[False]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_associate_router(self): row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[True]) old = utils.create_row( external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface'}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_exception(self): row = utils.create_row( external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_wrong_type(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_wrong_device_owner(self): row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_gateway', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_up(self): row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_local_crlrp(self): row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router2'}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "192.168.24.1/24", constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_LS_NAME_EXT_ID_KEY: 'network1', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[True]) subnet_info = { 'associated_router': 'router1', 'network': 'network1', 'address_scopes': {4: None, 6: None}} self.event.run(None, row, None) self.agent.expose_subnet.assert_called_once_with(["192.168.24.1/24"], subnet_info) class TestLogicalSwitchPortSubnetDetachEvent(test_base.TestCase): def setUp(self): super(TestLogicalSwitchPortSubnetDetachEvent, self).setUp() self.chassis = 'fake-chassis' self.chassis_id = 'fake-chassis-id' self.agent = mock.Mock(chassis=self.chassis, chassis_id=self.chassis_id) self.agent.ovn_local_cr_lrps = { 'router1': {'bridge_device': 'br-ex', 'bridge_vlan': None, 'ips': ['172.24.16.2']}} self.event = nb_bgp_watcher.LogicalSwitchPortSubnetDetachEvent( self.agent) def test_match_fn(self): row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[False]) old = utils.create_row(up=[True]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_delete(self): event = self.event.ROW_DELETE row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[True]) self.assertTrue(self.event.match_fn(event, row, mock.Mock())) def test_match_fn_delete_down(self): event = self.event.ROW_DELETE row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[False]) self.assertFalse(self.event.match_fn(event, row, mock.Mock())) def test_match_fn_disassociate_router(self): row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface'}, up=[True]) old = utils.create_row( external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_exception(self): row = utils.create_row( external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_wrong_type(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_wrong_device_owner(self): row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_gateway', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_up(self): row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[True]) old = utils.create_row(up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_not_local_crlrp(self): row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface'}, up=[True]) old = utils.create_row( external_ids={ constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'other_router'}) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_run(self): row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "192.168.24.1/24", constants.OVN_LS_NAME_EXT_ID_KEY: 'network1', constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface'}, up=[True]) old = utils.create_row( external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "192.168.24.1/24", constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_LS_NAME_EXT_ID_KEY: 'network1', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}) subnet_info = { 'associated_router': 'router1', 'network': 'network1', 'address_scopes': {4: None, 6: None}} self.event.run(None, row, old) self.agent.withdraw_subnet.assert_called_once_with( ["192.168.24.1/24"], subnet_info) def test_run_no_old_external_ids(self): row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "192.168.24.1/24", constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_LS_NAME_EXT_ID_KEY: 'network1', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[True]) old = utils.create_row() subnet_info = { 'associated_router': 'router1', 'network': 'network1', 'address_scopes': {4: None, 6: None}} self.event.run(None, row, old) self.agent.withdraw_subnet.assert_called_once_with( ["192.168.24.1/24"], subnet_info) def test_run_delete(self): event = self.event.ROW_DELETE row = utils.create_row( type=constants.OVN_ROUTER_PORT_TYPE, external_ids={ constants.OVN_CIDRS_EXT_ID_KEY: "192.168.24.1/24", constants.OVN_DEVICE_OWNER_EXT_ID_KEY: 'network:router_interface', constants.OVN_LS_NAME_EXT_ID_KEY: 'network1', constants.OVN_DEVICE_ID_EXT_ID_KEY: 'router1'}, up=[True]) subnet_info = { 'associated_router': 'router1', 'network': 'network1', 'address_scopes': {4: None, 6: None}} self.event.run(event, row, mock.Mock()) self.agent.withdraw_subnet.assert_called_once_with( ["192.168.24.1/24"], subnet_info) class TestLogicalSwitchPortTenantCreateEvent(test_base.TestCase): def setUp(self): super(TestLogicalSwitchPortTenantCreateEvent, self).setUp() self.chassis = 'fake-chassis' self.chassis_id = 'fake-chassis-id' self.agent = mock.Mock(chassis=self.chassis, chassis_id=self.chassis_id) self.agent.ovn_local_lrps = { 'net1': ['10.0.0.5']} self.event = nb_bgp_watcher.LogicalSwitchPortTenantCreateEvent( self.agent) def test_match_fn(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 10.0.0.6'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, up=[True]) old = utils.create_row(up=[False]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_network_set(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 10.0.0.6'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, up=[True]) old = utils.create_row(external_ids={}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_wong_ip(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_up(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 10.0.0.6'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_local_lrp(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 10.0.0.6'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net2'}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_exception(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 10.0.0.6'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 10.0.0.6'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1', constants.OVN_CIDRS_EXT_ID_KEY: "10.0.0.6/24"}, up=[True]) ips_info = { 'mac': 'mac', 'cidrs': ["10.0.0.6/24"], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': 'net1'} self.event.run(None, row, mock.Mock()) self.agent.expose_remote_ip.assert_called_once_with( ["10.0.0.6"], ips_info) def test_run_wrong_type(self): row = utils.create_row( type=constants.OVN_PATCH_VIF_PORT_TYPE, addresses=['mac 10.0.0.6'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1', constants.OVN_CIDRS_EXT_ID_KEY: "10.0.0.6/24"}, up=[True]) self.event.run(None, row, mock.Mock()) self.agent.expose_remote_ip.assert_not_called() class TestLogicalSwitchPortTenantDeleteEvent(test_base.TestCase): def setUp(self): super(TestLogicalSwitchPortTenantDeleteEvent, self).setUp() self.chassis = 'fake-chassis' self.chassis_id = 'fake-chassis-id' self.agent = mock.Mock(chassis=self.chassis, chassis_id=self.chassis_id) self.agent.ovn_local_lrps = { 'net1': ['10.0.0.5']} self.event = nb_bgp_watcher.LogicalSwitchPortTenantDeleteEvent( self.agent) def test_match_fn(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 10.0.0.6'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, up=[False]) old = utils.create_row(up=[True]) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_delete(self): event = self.event.ROW_DELETE row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 10.0.0.6'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, up=[True]) self.assertTrue(self.event.match_fn(event, row, mock.Mock())) def test_match_fn_wong_ip(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_not_up(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 10.0.0.6'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, up=[True]) old = utils.create_row(up=[False]) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_not_local_lrp(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 10.0.0.6'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net2'}, up=[True]) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_exception(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run(self): row = utils.create_row( type=constants.OVN_VM_VIF_PORT_TYPE, addresses=['mac 10.0.0.6'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1', constants.OVN_CIDRS_EXT_ID_KEY: "10.0.0.6/24"}, up=[True]) ips_info = { 'mac': 'mac', 'cidrs': ["10.0.0.6/24"], 'type': constants.OVN_VM_VIF_PORT_TYPE, 'logical_switch': 'net1'} self.event.run(None, row, mock.Mock()) self.agent.withdraw_remote_ip.assert_called_once_with( ["10.0.0.6"], ips_info) def test_run_wrong_type(self): row = utils.create_row( type=constants.OVN_PATCH_VIF_PORT_TYPE, addresses=['mac 10.0.0.6'], external_ids={constants.OVN_LS_NAME_EXT_ID_KEY: 'net1', constants.OVN_CIDRS_EXT_ID_KEY: "10.0.0.6/24"}, up=[True]) self.event.run(None, row, mock.Mock()) self.agent.withdraw_remote_ip.assert_not_called() class TestOVNLBCreateEvent(test_base.TestCase): def setUp(self): super(TestOVNLBCreateEvent, self).setUp() self.chassis = 'fake-chassis' self.chassis_id = 'fake-chassis-id' self.agent = mock.Mock(chassis=self.chassis, chassis_id=self.chassis_id) self.agent.ovn_local_cr_lrps = { 'router1': {'bridge_device': 'br-ex', 'bridge_vlan': None, 'ips': ['172.24.16.2']}} self.event = nb_bgp_watcher.OVNLBCreateEvent( self.agent) def test_match_fn(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) old = utils.create_row(vips={}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_router_added(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) old = utils.create_row(external_ids={}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_fip_added(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) old = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1'}, vips={'192.168.1.50:80': '192.168.1.100:80'}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_vips_no_change(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) old = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1'}) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_no_vips(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_match_fn_no_local_crlrp(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router2', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock())) def test_run_vip(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_IP_EXT_ID_KEY: '192.168.1.50', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80'}) old = utils.create_row(vips={}) self.event.run(None, row, old) self.agent.expose_ovn_lb_vip.assert_called_once_with(row) self.agent.expose_ovn_lb_fip.assert_not_called() def test_run_vip_and_fip(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LB_VIP_IP_EXT_ID_KEY: '192.168.1.50', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) old = utils.create_row(vips={}) self.event.run(None, row, old) self.agent.expose_ovn_lb_vip.assert_called_once_with(row) self.agent.expose_ovn_lb_fip.assert_called_once_with(row) def test_run_vip_added_router(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1', 'other': 'info'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) old = utils.create_row( external_ids={ constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) self.event.run(None, row, old) self.agent.expose_ovn_lb_vip.assert_called_once_with(row) self.agent.expose_ovn_lb_fip.assert_not_called() def test_run_fip(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) old = utils.create_row(external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1'}) self.event.run(None, row, old) self.agent.expose_ovn_lb_vip.assert_not_called() self.agent.expose_ovn_lb_fip.assert_called_once_with(row) class TestOVNLBDeleteEvent(test_base.TestCase): def setUp(self): super(TestOVNLBDeleteEvent, self).setUp() self.chassis = 'fake-chassis' self.chassis_id = 'fake-chassis-id' self.agent = mock.Mock(chassis=self.chassis, chassis_id=self.chassis_id) self.agent.ovn_local_cr_lrps = { 'router1': {'bridge_device': 'br-ex', 'bridge_vlan': None, 'ips': ['172.24.16.2']}} self.event = nb_bgp_watcher.OVNLBDeleteEvent( self.agent) def test_match_fn(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={}) old = utils.create_row( vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_delete(self): event = self.event.ROW_DELETE row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) self.assertTrue(self.event.match_fn(event, row, mock.Mock())) def test_match_fn_delete_no_vips(self): event = self.event.ROW_DELETE row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={}) self.assertFalse(self.event.match_fn(event, row, mock.Mock())) def test_match_fn_delete_no_local_router(self): event = self.event.ROW_DELETE row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router2', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) self.assertFalse(self.event.match_fn(event, row, mock.Mock())) def test_match_fn_router_deleted(self): row = utils.create_row( external_ids={ constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) old = utils.create_row(external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1' }) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_no_old_router(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) old = utils.create_row(external_ids={}) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_old_router_non_local(self): row = utils.create_row( external_ids={ constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) old = utils.create_row(external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router2', }) self.assertFalse(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_fip_deleted(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1', constants.OVN_LB_VIP_IP_EXT_ID_KEY: '192.168.1.50'}, vips={'192.168.1.50:80': '192.168.1.100:80'}) old = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LB_VIP_IP_EXT_ID_KEY: '192.168.1.50'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_match_fn_vip_deleted_with_ext_id_update(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={}) old = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1', constants.OVN_LB_VIP_IP_EXT_ID_KEY: '192.168.1.50'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) self.assertTrue(self.event.match_fn(mock.Mock(), row, old)) def test_run_vip_delete_without_external_ids_on_old(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_IP_EXT_ID_KEY: '192.168.1.50', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={}) old = utils.create_row(vips={'192.168.1.50:80': '192.168.1.100:80'}) self.event.run(None, row, old) self.agent.withdraw_ovn_lb_vip.assert_not_called() self.agent.withdraw_ovn_lb_fip.assert_not_called() def test_run_vip_delete(self): event = self.event.ROW_DELETE row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1', constants.OVN_LB_VIP_IP_EXT_ID_KEY: '192.168.1.50'}, vips={'192.168.1.50:80': '192.168.1.100:80'}) self.event.run(event, row, None) self.agent.withdraw_ovn_lb_vip.assert_called_once_with(row) def test_run_vip_deleted_extra_ext_id_info(self): old = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LB_VIP_IP_EXT_ID_KEY: '192.168.1.50', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1', 'other': 'info'}, vips={}) self.event.run(None, row, old) self.agent.withdraw_ovn_lb_vip.assert_called_once_with(old) self.agent.withdraw_ovn_lb_fip.assert_called_once_with(old) def test_run_fip(self): row = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LS_NAME_EXT_ID_KEY: 'net1'}, vips={'192.168.1.50:80': '192.168.1.100:80'}) old = utils.create_row( external_ids={ constants.OVN_LB_LR_REF_EXT_ID_KEY: 'neutron-router1', constants.OVN_LB_VIP_FIP_EXT_ID_KEY: '172.24.4.5'}, vips={'192.168.1.50:80': '192.168.1.100:80', '172.24.4.5:80': '192.168.1.100:80'}) self.event.run(None, row, old) self.agent.withdraw_ovn_lb_vip.assert_not_called() self.agent.withdraw_ovn_lb_fip.assert_called_once_with(old) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/fakes.py000066400000000000000000000013101460327367600225630ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. def create_object(attr_dict): return type('FakeObject', (object,), attr_dict) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/privileged/000077500000000000000000000000001460327367600232575ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/privileged/__init__.py000066400000000000000000000000001460327367600253560ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/privileged/test_linux_net.py000066400000000000000000000100651460327367600266770ustar00rootroot00000000000000# Copyright 2022 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_concurrency import processutils from ovn_bgp_agent.privileged import linux_net as priv_linux_net from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.utils import linux_net class FakeException(Exception): stderr = '' class TestPrivilegedLinuxNet(test_base.TestCase): def setUp(self): super(TestPrivilegedLinuxNet, self).setUp() # Mock pyroute2.NDB context manager object self.mock_ndb = mock.patch.object(linux_net.pyroute2, 'NDB').start() self.fake_ndb = self.mock_ndb().__enter__() # Mock pyroute2.IPRoute context manager object self.mock_iproute = mock.patch.object( linux_net.pyroute2, 'IPRoute').start() self.fake_iproute = self.mock_iproute().__enter__() self.mock_exc = mock.patch.object(processutils, 'execute').start() # Helper variables used accross many tests self.ip = '10.10.1.16' self.ipv6 = '2002::1234:abcd:ffff:c0a8:101' self.dev = 'ethfake' self.mac = 'aa:bb:cc:dd:ee:ff' def test_set_kernel_flag(self): priv_linux_net.set_kernel_flag('net.ipv6.conf.fake', 1) self.mock_exc.assert_called_once_with( 'sysctl', '-w', 'net.ipv6.conf.fake=1') def test_set_kernel_flag_exception(self): self.mock_exc.side_effect = FakeException() self.assertRaises( FakeException, priv_linux_net.set_kernel_flag, 'net.ipv6.conf.fake', 1) def test_add_ndp_proxy(self): priv_linux_net.add_ndp_proxy(self.ipv6, self.dev) self.mock_exc.assert_called_once_with( 'ip', '-6', 'nei', 'add', 'proxy', self.ipv6, 'dev', self.dev) def test_add_ndp_proxy_vlan(self): priv_linux_net.add_ndp_proxy(self.ipv6, self.dev, vlan=10) self.mock_exc.assert_called_once_with( 'ip', '-6', 'nei', 'add', 'proxy', self.ipv6, 'dev', '%s.10' % self.dev) def test_add_ndp_proxy_exception(self): self.mock_exc.side_effect = FakeException() self.assertRaises( FakeException, priv_linux_net.add_ndp_proxy, self.ipv6, self.dev) def test_del_ndp_proxy(self): priv_linux_net.del_ndp_proxy(self.ipv6, self.dev) self.mock_exc.assert_called_once_with( 'ip', '-6', 'nei', 'del', 'proxy', self.ipv6, 'dev', self.dev, env_variables=mock.ANY) def test_del_ndp_proxy_vlan(self): priv_linux_net.del_ndp_proxy(self.ipv6, self.dev, vlan=10) self.mock_exc.assert_called_once_with( 'ip', '-6', 'nei', 'del', 'proxy', self.ipv6, 'dev', '%s.10' % self.dev, env_variables=mock.ANY) def test_del_ndp_proxy_exception(self): self.mock_exc.side_effect = FakeException() self.assertRaises( FakeException, priv_linux_net.del_ndp_proxy, self.ipv6, self.dev) def test_del_ndp_proxy_exception_no_such_file(self): exp = FakeException() exp.stderr = 'No such file or directory' self.mock_exc.side_effect = exp self.assertIsNone(priv_linux_net.del_ndp_proxy(self.ipv6, self.dev)) @mock.patch('builtins.open', new_callable=mock.mock_open()) def test_create_routing_table_for_bridge(self, mock_o): priv_linux_net.create_routing_table_for_bridge(17, 'fake-bridge') mock_o.assert_called_once_with('/etc/iproute2/rt_tables', 'a') mock_o().__enter__().write.assert_called_once_with('17 fake-bridge\n') ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/privileged/test_ovs_vsctl.py000066400000000000000000000062421460327367600267160ustar00rootroot00000000000000# Copyright 2022 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import importlib from unittest import mock from oslo_concurrency import processutils from ovn_bgp_agent.privileged import ovs_vsctl from ovn_bgp_agent.tests import base as test_base # Mock the privsep decorator and reload the module mock.patch('ovn_bgp_agent.privileged.ovs_vsctl_cmd.entrypoint', lambda x: x).start() importlib.reload(ovs_vsctl) class FakeException(Exception): stderr = '' class TestPrivilegedOvsVsctl(test_base.TestCase): def setUp(self): super(TestPrivilegedOvsVsctl, self).setUp() # Mock processutils.execute() self.mock_exc = mock.patch.object(processutils, 'execute').start() def test_ovs_cmd(self): ovs_vsctl.ovs_cmd( 'ovs-vsctl', ['--if-exists', 'del-port', 'fake-port']) self.mock_exc.assert_called_once_with( 'ovs-vsctl', '--if-exists', 'del-port', 'fake-port') def test_ovs_cmd_timeout(self): ovs_vsctl.ovs_cmd( 'ovs-vsctl', ['--if-exists', 'del-port', 'fake-port'], timeout=10) self.mock_exc.assert_called_once_with( 'ovs-vsctl', '--timeout=10', '--if-exists', 'del-port', 'fake-port') def test_ovs_cmd_fallback_OF_version(self): self.mock_exc.side_effect = ( processutils.ProcessExecutionError(), None) ovs_vsctl.ovs_cmd( 'ovs-vsctl', ['--if-exists', 'del-port', 'fake-port']) calls = [mock.call('ovs-vsctl', '--if-exists', 'del-port', 'fake-port'), mock.call('ovs-vsctl', '--if-exists', 'del-port', 'fake-port', '-O', 'OpenFlow13')] self.mock_exc.assert_has_calls(calls) def test_ovs_cmd_exception(self): self.mock_exc.side_effect = FakeException() self.assertRaises( FakeException, ovs_vsctl.ovs_cmd, 'ovs-vsctl', ['--if-exists', 'del-port', 'fake-port']) self.mock_exc.assert_called_once_with( 'ovs-vsctl', '--if-exists', 'del-port', 'fake-port') def test_ovs_cmd_fallback_exception(self): self.mock_exc.side_effect = ( processutils.ProcessExecutionError(), FakeException()) self.assertRaises( FakeException, ovs_vsctl.ovs_cmd, 'ovs-vsctl', ['--if-exists', 'del-port', 'fake-port']) calls = [mock.call('ovs-vsctl', '--if-exists', 'del-port', 'fake-port'), mock.call('ovs-vsctl', '--if-exists', 'del-port', 'fake-port', '-O', 'OpenFlow13')] self.mock_exc.assert_has_calls(calls) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/privileged/test_vtysh.py000066400000000000000000000042711460327367600260510ustar00rootroot00000000000000# Copyright 2022 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import importlib from unittest import mock from oslo_concurrency import processutils from ovn_bgp_agent import constants from ovn_bgp_agent.privileged import vtysh from ovn_bgp_agent.tests import base as test_base # Mock the privsep decorator and reload the module mock.patch('ovn_bgp_agent.privileged.vtysh_cmd.entrypoint', lambda x: x).start() importlib.reload(vtysh) class FakeException(Exception): stderr = '' class TestPrivilegedVtysh(test_base.TestCase): def setUp(self): super(TestPrivilegedVtysh, self).setUp() # Mock processutils.execute() self.mock_exc = mock.patch.object(processutils, 'execute').start() def test_run_vtysh_config(self): vtysh.run_vtysh_config('/fake/frr.config') self.mock_exc.assert_called_once_with( '/usr/bin/vtysh', '--vty_socket', constants.FRR_SOCKET_PATH, '-f', '/fake/frr.config') def test_run_vtysh_config_exception(self): self.mock_exc.side_effect = FakeException() self.assertRaises( FakeException, vtysh.run_vtysh_config, '/fake/frr.config') def test_run_vtysh_command(self): cmd = 'show ip bgp summary json' vtysh.run_vtysh_command(cmd) self.mock_exc.assert_called_once_with( '/usr/bin/vtysh', '--vty_socket', constants.FRR_SOCKET_PATH, '-c', cmd) def test_run_vtysh_command_exception(self): self.mock_exc.side_effect = FakeException() self.assertRaises( FakeException, vtysh.run_vtysh_command, 'show ip bgp summary json') ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/test_agent.py000066400000000000000000000026201460327367600236340ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock from ovn_bgp_agent import agent from ovn_bgp_agent.tests import base as test_base class TestAgent(test_base.TestCase): @mock.patch('oslo_service.service.launch') @mock.patch('ovn_bgp_agent.config.register_opts') @mock.patch('ovn_bgp_agent.config.init') @mock.patch('ovn_bgp_agent.config.setup_logging') @mock.patch('ovn_bgp_agent.agent.BGPAgent') def test_start(self, m_agent, m_setup_logging, m_config_init, m_register_opts, m_oslo_launch): m_launcher = mock.Mock() m_oslo_launch.return_value = m_launcher agent.start() m_register_opts.assert_called() m_config_init.assert_called() m_setup_logging.assert_called() m_agent.assert_called() m_oslo_launch.assert_called() m_launcher.wait.assert_called() ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/utils/000077500000000000000000000000001460327367600222655ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/utils/__init__.py000066400000000000000000000000001460327367600243640ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/utils/test_helpers.py000066400000000000000000000056671460327367600253560ustar00rootroot00000000000000# Copyright 2023 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.tests import utils from ovn_bgp_agent.utils import helpers class TestHelpers(test_base.TestCase): def setUp(self): super(TestHelpers, self).setUp() def test_parse_bridge_mappings(self): bridge_mappings = "provider-1:br-ex" ret_net, ret_bridge = helpers.parse_bridge_mapping(bridge_mappings) self.assertEqual(ret_net, 'provider-1') self.assertEqual(ret_bridge, 'br-ex') def test_parse_bridge_mappings_missing_mapping(self): bridge_mappings = "" ret_net, ret_bridge = helpers.parse_bridge_mapping(bridge_mappings) self.assertEqual(ret_net, None) self.assertEqual(ret_bridge, None) def test_parse_bridge_mappings_wrong_format(self): bridge_mappings = "provider-1:br-ex:extra_field" ret_net, ret_bridge = helpers.parse_bridge_mapping(bridge_mappings) self.assertEqual(ret_net, None) self.assertEqual(ret_bridge, None) class TestHelperGetLBDatapathGroup(test_base.TestCase): def setUp(self): super(TestHelperGetLBDatapathGroup, self).setUp() self.dp_group = utils.create_row(_uuid='fake_dp_group', datapaths=['dp']) self.dp_group1 = utils.create_row(_uuid='fake_dp_group1', datapaths=['dp1']) def test_get_lb_datapath_group(self): lb = utils.create_row(name='ovn-lb', datapath_group=[self.dp_group]) self.assertEqual((['dp'], []), helpers.get_lb_datapath_groups(lb)) def test_get_lb_datapath_group_ls_datapath(self): lb = utils.create_row(name='ovn-lb', ls_datapath_group=[self.dp_group]) self.assertEqual((['dp'], []), helpers.get_lb_datapath_groups(lb)) def test_get_lb_datapath_group_lr_datapath(self): lb = utils.create_row(name='ovn-lb', lr_datapath_group=[self.dp_group]) self.assertEqual(([], ['dp']), helpers.get_lb_datapath_groups(lb)) def test_get_lb_datapath_group_ls_and_lr_datapath(self): lb = utils.create_row(name='ovn-lb', ls_datapath_group=[self.dp_group], lr_datapath_group=[self.dp_group1]) self.assertEqual((['dp'], ['dp1']), helpers.get_lb_datapath_groups(lb)) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/unit/utils/test_linux_net.py000066400000000000000000001071641460327367600257140ustar00rootroot00000000000000# Copyright 2022 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import ipaddress from unittest import mock from ovn_bgp_agent import constants from ovn_bgp_agent import exceptions as agent_exc from ovn_bgp_agent.tests import base as test_base from ovn_bgp_agent.utils import linux_net class IPRouteDict(dict): def get_attr(self, attr_name): for attr in self['attrs']: if attr[0] == attr_name: return attr[1] return class TestLinuxNet(test_base.TestCase): def setUp(self): super(TestLinuxNet, self).setUp() # Mock pyroute2.NDB context manager object self.mock_ndb = mock.patch.object(linux_net.pyroute2, 'NDB').start() self.fake_ndb = self.mock_ndb().__enter__() # Mock pyroute2.IPRoute context manager object self.mock_ipr = mock.patch.object(linux_net.pyroute2, 'IPRoute').start() self.fake_ipr = self.mock_ipr().__enter__() # Helper variables used accross many tests self.ip = '10.10.1.16' self.ipv6 = '2002::1234:abcd:ffff:c0a8:101' self.dev = 'ethfake' self.mac = 'aa:bb:cc:dd:ee:ff' self.bridge = 'br-fake' self.table_id = 100 self.network = ipaddress.IPv4Network("10.10.1.0/24") self.network_v6 = ipaddress.IPv6Network("2002:0:0:1234:0:0:0:0/64") def test_get_ip_version_v4(self): self.assertEqual(4, linux_net.get_ip_version('%s/32' % self.ip)) self.assertEqual(4, linux_net.get_ip_version(self.ip)) def test_get_ip_version_v6(self): self.assertEqual(6, linux_net.get_ip_version('%s/64' % self.ipv6)) self.assertEqual(6, linux_net.get_ip_version(self.ipv6)) def test_get_interfaces(self): iface0 = IPRouteDict({'attrs': [('IFLA_IFNAME', 'ethfake0')]}) iface1 = IPRouteDict({'attrs': [('IFLA_IFNAME', 'ethfake1')]}) iface2 = IPRouteDict({'attrs': [('IFLA_IFNAME', 'ethfake2')]}) self.fake_ipr.get_links.return_value = [iface0, iface1, iface2] ret = linux_net.get_interfaces(filter_out='ethfake1') self.assertEqual(['ethfake0', 'ethfake2'], ret) def test_get_interface_index(self): self.fake_ipr.link_lookup.return_value = [7] ret = linux_net.get_interface_index('fake-nic') self.assertEqual(7, ret) def test_get_interface_index_error(self): self.fake_ipr.link_lookup.return_value = '' self.assertRaises(agent_exc.NetworkInterfaceNotFound, linux_net.get_interface_index, 'fake-nic') def test_get_interface_address(self): device_idx = 7 self.fake_ipr.link_lookup.return_value = [device_idx] fake_link = mock.MagicMock() fake_link.get_attr.return_value = self.mac self.fake_ipr.get_links.return_value = [fake_link] ret = linux_net.get_interface_address('fake-nic') self.assertEqual(self.mac, ret) def test_get_interface_address_index_error(self): self.fake_ipr.link_lookup.return_value = '' self.assertRaises(agent_exc.NetworkInterfaceNotFound, linux_net.get_interface_address, 'fake-nic') def test_get_nic_info(self): device_idx = 7 nic_addr = IPRouteDict({'prefixlen': 32, 'attrs': [('IFA_ADDRESS', self.ip)]}) self.fake_ipr.link_lookup.return_value = [device_idx] self.fake_ipr.get_addr.return_value = [nic_addr] fake_link = mock.MagicMock() fake_link.get_attr.return_value = self.mac self.fake_ipr.get_links.return_value = [fake_link] ret = linux_net.get_nic_info('fake-nic') self.assertEqual(('{}/32'.format(self.ip), self.mac), ret) def test_get_nic_info_index_error(self): self.fake_ipr.link_lookup.return_value = '' self.assertRaises(agent_exc.NetworkInterfaceNotFound, linux_net.get_nic_info, 'fake-nic') @mock.patch('ovn_bgp_agent.privileged.linux_net.ensure_vrf') def test_ensure_vrf(self, mock_ensure_vrf): linux_net.ensure_vrf('fake-vrf', 10) mock_ensure_vrf.assert_called_once_with('fake-vrf', 10) @mock.patch('ovn_bgp_agent.privileged.linux_net.ensure_bridge') def test_ensure_bridge(self, mock_ensure_bridge): linux_net.ensure_bridge('fake-bridge') mock_ensure_bridge.assert_called_once_with('fake-bridge') @mock.patch('ovn_bgp_agent.privileged.linux_net.ensure_vxlan') def test_ensure_vxlan(self, mock_ensure_vxlan): linux_net.ensure_vxlan('fake-vxlan', 11, self.ip, 7) mock_ensure_vxlan.assert_called_once_with('fake-vxlan', 11, self.ip, 7) @mock.patch('ovn_bgp_agent.privileged.linux_net.ensure_veth') def test_ensure_veth(self, mock_ensure_veth): linux_net.ensure_veth('fake-veth', 'fake-veth-peer') mock_ensure_veth.assert_called_once_with('fake-veth', 'fake-veth-peer') @mock.patch('ovn_bgp_agent.privileged.linux_net.ensure_dummy_device') def test_ensure_dummy_device(self, mock_ensure_dummy_device): linux_net.ensure_dummy_device('fake-dev') mock_ensure_dummy_device.assert_called_once_with('fake-dev') @mock.patch.object(linux_net, 'ensure_dummy_device') @mock.patch.object(linux_net, 'set_master_for_device') def test_ensure_ovn_device(self, mock_master, mock_dummy): linux_net.ensure_ovn_device('ifname', 'fake-vrf') mock_dummy.assert_called_once_with('ifname') mock_master.assert_called_once_with('ifname', 'fake-vrf') @mock.patch('ovn_bgp_agent.privileged.linux_net.delete_device') def test_delete_device(self, mock_delete_device): linux_net.delete_device('fake-dev') mock_delete_device.assert_called_once_with('fake-dev') @mock.patch.object(linux_net, 'enable_proxy_arp') @mock.patch.object(linux_net, 'enable_proxy_ndp') @mock.patch('ovn_bgp_agent.privileged.linux_net.add_ip_to_dev') def test_ensure_arp_ndp_enabled_for_bridge(self, mock_add_ip_to_dev, mock_ndp, mock_arp): linux_net.ensure_arp_ndp_enabled_for_bridge('fake-bridge', 511) # NOTE(ltomasbo): hardoced starting ipv4 is 192.168.0.0, and ipv6 is # fd53:d91e:400:7f17::0 ipv4 = '169.254.1.255' # base + 511 offset ipv6 = 'fd53:d91e:400:7f17::1ff' # base + 5122 offset (to hex) calls = [mock.call(ipv4, 'fake-bridge'), mock.call(ipv6, 'fake-bridge')] mock_add_ip_to_dev.assert_has_calls(calls) mock_ndp.assert_called_once_with('fake-bridge') mock_arp.assert_called_once_with('fake-bridge') @mock.patch.object(linux_net, 'enable_proxy_arp') @mock.patch.object(linux_net, 'enable_proxy_ndp') @mock.patch('ovn_bgp_agent.privileged.linux_net.add_ip_to_dev') def test_ensure_arp_ndp_enabled_for_bridge_vlan(self, mock_add_ip_to_dev, mock_ndp, mock_arp): linux_net.ensure_arp_ndp_enabled_for_bridge('fake-bridge', 511, 11) # NOTE(ltomasbo): hardoced starting ipv4 is 192.168.0.0, and ipv6 is # fd53:d91e:400:7f17::0 ipv4 = '169.254.1.255' # base + 511 offset ipv6 = 'fd53:d91e:400:7f17::1ff' # base + 5122 offset (to hex) calls = [mock.call(ipv4, 'fake-bridge'), mock.call(ipv6, 'fake-bridge')] mock_add_ip_to_dev.assert_has_calls(calls) mock_ndp.assert_called_once_with('fake-bridge') mock_arp.assert_called_once_with('fake-bridge') @mock.patch.object(linux_net, 'enable_proxy_arp') @mock.patch.object(linux_net, 'enable_proxy_ndp') @mock.patch( 'ovn_bgp_agent.privileged.linux_net.ensure_vlan_device_for_network') def test_ensure_vlan_device_for_network( self, mock_ensure_vlan_device_for_network, mock_ndp, mock_arp): linux_net.ensure_vlan_device_for_network('fake-br', 10) expected_dev = 'fake-br/10' mock_ensure_vlan_device_for_network.assert_called_once_with( 'fake-br', 10) mock_ndp.assert_called_once_with(expected_dev) mock_arp.assert_called_once_with(expected_dev) @mock.patch.object(linux_net, 'delete_device') def test_delete_vlan_device_for_network(self, mock_del): linux_net.delete_vlan_device_for_network('fake-br', 10) vlan_name = 'fake-br.10' mock_del.assert_called_once_with(vlan_name) @mock.patch('ovn_bgp_agent.privileged.linux_net.set_kernel_flag') def test_enable_proxy_ndp(self, mock_flag): linux_net.enable_proxy_ndp(self.dev) expected_flag = 'net.ipv6.conf.%s.proxy_ndp' % self.dev mock_flag.assert_called_once_with(expected_flag, 1) @mock.patch('ovn_bgp_agent.privileged.linux_net.set_kernel_flag') def test_enable_proxy_arp(self, mock_flag): linux_net.enable_proxy_arp(self.dev) expected_flag = 'net.ipv4.conf.%s.proxy_arp' % self.dev mock_flag.assert_called_once_with(expected_flag, 1) def test_get_exposed_ips(self): ip0 = IPRouteDict({'prefixlen': 32, 'attrs': [('IFA_ADDRESS', self.ip)]}) ip1 = IPRouteDict({'prefixlen': 128, 'attrs': [('IFA_ADDRESS', self.ipv6)]}) ip2 = IPRouteDict({'prefixlen': 24, 'attrs': [('IFA_ADDRESS', '10.10.1.18')]}) ip3 = IPRouteDict( {'prefixlen': 64, 'attrs': [('IFA_ADDRESS', '2001:0DB8:0000:000b::')]}) self.fake_ipr.get_addr.return_value = [ip0, ip1, ip2, ip3] ips = linux_net.get_exposed_ips(self.dev) expected_ips = [self.ip, self.ipv6] self.assertEqual(expected_ips, ips) def test_get_nic_ip(self): ip0 = IPRouteDict({'attrs': [('IFA_ADDRESS', '10.10.1.16')]}) ip1 = IPRouteDict({'attrs': [('IFA_ADDRESS', '10.10.1.17')]}) self.fake_ipr.get_addr.return_value = [ip0, ip1] ips = linux_net.get_nic_ip(self.dev) expected_ips = ['10.10.1.16', '10.10.1.17'] self.assertEqual(expected_ips, ips) def test_get_exposed_ips_on_network(self): ip0 = IPRouteDict({'prefixlen': 32, 'attrs': [('IFA_ADDRESS', self.ip)]}) ip1 = IPRouteDict({'prefixlen': 128, 'attrs': [('IFA_ADDRESS', '10.10.1.17')]}) ip2 = IPRouteDict({'prefixlen': 128, 'attrs': [('IFA_ADDRESS', self.ipv6)]}) ip3 = IPRouteDict({ 'prefixlen': 128, 'attrs': [('IFA_ADDRESS', '2001:db8:3333:4444:5555:6666:7777:8888') ]}) self.fake_ipr.get_addr.return_value = [ip0, ip1, ip2, ip3] network_ips = [ipaddress.ip_address(self.ip), ipaddress.ip_address(self.ipv6)] ret = linux_net.get_exposed_ips_on_network(self.dev, network_ips) self.assertEqual([self.ip, self.ipv6], ret) def test_get_exposed_routes_on_network_v4(self): route0 = mock.MagicMock( dst=mock.Mock(), table=self.table_id, scope=1, proto=11, gateway=self.ip, ) route1 = mock.MagicMock( dst=mock.Mock(), table=self.table_id, scope=1, proto=11, gateway=self.ipv6, ) route2 = mock.MagicMock( dst=mock.Mock(), table=self.table_id, scope=1, proto=11, gateway=None, ) self.fake_ndb.routes.dump.return_value = [route0, route1, route2] ret = linux_net.get_exposed_routes_on_network( [self.table_id], self.network ) self.assertEqual([route0], ret) def test_get_exposed_routes_on_network_v6(self): route0 = mock.MagicMock( dst=mock.Mock(), table=self.table_id, scope=1, proto=11, gateway=self.ip, ) route1 = mock.MagicMock( dst=mock.Mock(), table=self.table_id, scope=1, proto=11, gateway=self.ipv6, ) route2 = mock.MagicMock( dst=mock.Mock(), table=self.table_id, scope=1, proto=11, gateway=None, ) self.fake_ndb.routes.dump.return_value = [route0, route1, route2] ret = linux_net.get_exposed_routes_on_network( [self.table_id], self.network_v6 ) self.assertEqual([route1], ret) def test_get_ovn_ip_rules(self): rule0 = IPRouteDict({'dst_len': 128, 'family': 10, 'attrs': [('FRA_TABLE', 7), ('FRA_DST', 10)]}) rule1 = IPRouteDict({'dst_len': 32, 'family': 2, 'attrs': [('FRA_TABLE', 7), ('FRA_DST', 11)]}) rule2 = IPRouteDict({'dst_len': 24, 'family': 2, 'attrs': [('FRA_TABLE', 9), ('FRA_DST', 5)]}) rule3 = IPRouteDict({'dst_len': 128, 'family': 10, 'attrs': [('FRA_TABLE', 10), ('FRA_DST', 6)]}) self.fake_ipr.get_rules.side_effect = [[rule1, rule2], [rule0, rule3]] ret = linux_net.get_ovn_ip_rules([7, 10]) expected_ret = {'10/128': {'table': 7, 'family': 10}, '11/32': {'table': 7, 'family': 2}, '6/128': {'table': 10, 'family': 10}} self.assertEqual(expected_ret, ret) @mock.patch('ovn_bgp_agent.privileged.linux_net.delete_exposed_ips') def test_delete_exposed_ips(self, mock_delete_exposed_ips): linux_net.delete_exposed_ips([self.ip], self.dev) mock_delete_exposed_ips.assert_called_once_with([self.ip], self.dev) @mock.patch('ovn_bgp_agent.privileged.linux_net.delete_ip_rules') def test_delete_ip_rules(self, mock_delete_ip_rules): ip_rules = {'10/128': {'table': 7, 'family': 'fake'}, '6/128': {'table': 10, 'family': 'fake'}} linux_net.delete_ip_rules(ip_rules) mock_delete_ip_rules.assert_called_once_with(ip_rules) @mock.patch.object(linux_net, 'get_interface_index') def _test_delete_bridge_ip_routes(self, mock_route_delete, mock_get_index, is_vlan=False, has_gateway=False): gateway = '1.1.1.1' oif = 11 vlan = 30 if is_vlan else None mock_get_index.return_value = oif route = {'route': {'dst': self.ip, 'dst_len': 32, 'table': 20}, 'vlan': vlan} if has_gateway: route['route']['gateway'] = gateway routing_tables = {self.bridge: 20} routing_tables_routes = {self.bridge: [route]} # extra_route0 matches with the route extra_route0 = IPRouteDict({ 'dst_len': 32, 'family': constants.AF_INET, 'table': 20, 'attrs': [('RTA_DST', self.ip), ('RTA_OIF', oif), ('RTA_GATEWAY', gateway)]}) # extra_route1 does not match with route and should be removed extra_route1 = IPRouteDict({ 'dst_len': 32, 'family': constants.AF_INET, 'table': 20, 'attrs': [('RTA_DST', '10.10.1.17'), ('RTA_OIF', oif), ('RTA_GATEWAY', gateway)]}) extra_routes = {self.bridge: [extra_route0, extra_route1]} linux_net.delete_bridge_ip_routes( routing_tables, routing_tables_routes, extra_routes) # Assert extra_route1 has been removed expected_route = {'dst': '10.10.1.17', 'dst_len': 32, 'family': constants.AF_INET, 'oif': oif, 'gateway': gateway, 'table': 20} mock_route_delete.assert_called_once_with(expected_route) @mock.patch('ovn_bgp_agent.privileged.linux_net.route_delete') def test_delete_bridge_ip_routes(self, mock_route_delete): self._test_delete_bridge_ip_routes(mock_route_delete) @mock.patch('ovn_bgp_agent.privileged.linux_net.route_delete') def test_delete_bridge_ip_routes_vlan(self, mock_route_delete): self._test_delete_bridge_ip_routes(mock_route_delete, is_vlan=True) @mock.patch('ovn_bgp_agent.privileged.linux_net.route_delete') def test_delete_bridge_ip_routes_gateway(self, mock_route_delete): self._test_delete_bridge_ip_routes(mock_route_delete, has_gateway=True) @mock.patch('ovn_bgp_agent.utils.linux_net.delete_ip_routes') def test_delete_routes_from_table(self, mock_delete_ip_routes): route0 = {'scope': 1, 'proto': 11} route1 = {'scope': 2, 'proto': 22} route2 = {'scope': 254, 'proto': 186} self.fake_ipr.get_routes.return_value = [ route0, route1, route2] linux_net.delete_routes_from_table('fake-table') mock_delete_ip_routes.assert_called_once_with([route0, route1]) def test_get_routes_on_tables(self): route0 = IPRouteDict({ 'proto': 10, 'table': 10, 'attrs': [('RTA_DST', '10.10.10.10')]}) # Route1 has proto 186, should be ignored route1 = IPRouteDict({ 'proto': 186, 'table': 11, 'attrs': [('RTA_DST', '11.11.11.11')]}) route2 = IPRouteDict({ 'proto': 12, 'table': 11, 'attrs': [('RTA_DST', '12.12.12.12')]}) # Route3 is in the list but dst is empty route3 = IPRouteDict({ 'proto': 10, 'table': 22, 'attrs': [('RTA_DST', '')]}) self.fake_ipr.get_routes.side_effect = [ [route0], [route1, route2], [route3]] ret = linux_net.get_routes_on_tables([10, 11, 22]) self.assertEqual([route0, route2], ret) @mock.patch('ovn_bgp_agent.privileged.linux_net.route_delete') def test_delete_ip_routes(self, mock_route_delete): route0 = dict( table=10, dst='10.10.10.10', proto=10, dst_len=128, oif='ethout', family='fake', gateway='1.1.1.1') route1 = dict( table=11, dst='11.11.11.11', proto=11, dst_len=64, oif='ethout', family='fake', gateway='2.2.2.2') routes = [route0, route1] linux_net.delete_ip_routes(routes) route0.pop('proto') route1.pop('proto') mock_route_delete.assert_has_calls( [mock.call(route0), mock.call(route1)]) @mock.patch('ovn_bgp_agent.privileged.linux_net.add_ndp_proxy') def test_add_ndp_proxy(self, mock_ndp_proxy): linux_net.add_ndp_proxy(self.ip, self.dev, vlan=10) mock_ndp_proxy.assert_called_once_with(self.ip, self.dev, 10) @mock.patch('ovn_bgp_agent.privileged.linux_net.del_ndp_proxy') def test_del_ndp_proxy(self, mock_ndp_proxy): linux_net.del_ndp_proxy(self.ip, self.dev, vlan=10) mock_ndp_proxy.assert_called_once_with(self.ip, self.dev, 10) @mock.patch.object(linux_net, 'get_interface_index') @mock.patch('ovn_bgp_agent.privileged.linux_net.route_delete') @mock.patch('ovn_bgp_agent.privileged.linux_net.add_ip_to_dev') def test_add_ips_to_dev(self, mock_add_ip_to_dev, mock_route_delete, mock_get_index): ips = [self.ip, self.ipv6] oif = 7 mock_get_index.return_value = oif linux_net.add_ips_to_dev( self.dev, ips, clear_local_route_at_table=123) # Assert called for each ip calls = [mock.call(self.ip, self.dev), mock.call(self.ipv6, self.dev)] mock_add_ip_to_dev.assert_has_calls(calls) r1 = {'table': 123, 'proto': 2, 'scope': 254, 'dst': self.ip, 'oif': oif} r2 = {'table': 123, 'proto': 2, 'scope': 254, 'dst': self.ipv6, 'oif': oif} calls = [mock.call(r1), mock.call(r2)] mock_route_delete.assert_has_calls(calls) @mock.patch('ovn_bgp_agent.privileged.linux_net.del_ip_from_dev') def test_del_ips_from_dev(self, mock_del_ip_from_dev): ips = [self.ip, self.ipv6] linux_net.del_ips_from_dev(self.dev, ips) calls = [mock.call(self.ip, self.dev), mock.call(self.ipv6, self.dev)] mock_del_ip_from_dev.assert_has_calls(calls) @mock.patch.object(linux_net, 'add_ip_nei') @mock.patch('ovn_bgp_agent.privileged.linux_net.rule_create') def test_add_ip_rule(self, mock_rule_create, mock_add_ip_nei): linux_net.add_ip_rule( self.ip, 7, dev=self.dev, lladdr=self.mac) expected_args = {'dst': self.ip, 'table': 7, 'dst_len': 32, 'family': constants.AF_INET} mock_rule_create.assert_called_once_with(expected_args) mock_add_ip_nei.assert_called_once_with(self.ip, self.mac, self.dev) @mock.patch.object(linux_net, 'add_ip_nei') @mock.patch('ovn_bgp_agent.privileged.linux_net.rule_create') def test_add_ip_rule_ipv6(self, mock_rule_create, mock_add_ip_nei): linux_net.add_ip_rule(self.ipv6, 7, dev=self.dev, lladdr=self.mac) expected_args = {'dst': self.ipv6, 'table': 7, 'dst_len': 128, 'family': constants.AF_INET6} mock_rule_create.assert_called_once_with(expected_args) mock_add_ip_nei.assert_called_once_with(self.ipv6, self.mac, self.dev) @mock.patch('ovn_bgp_agent.privileged.linux_net.rule_create') def test_add_ip_rule_invalid_ip(self, mock_rule_create): self.assertRaises(agent_exc.InvalidPortIP, linux_net.add_ip_rule, '10.10.1.6/30/128', 7) mock_rule_create.assert_not_called() @mock.patch('ovn_bgp_agent.privileged.linux_net.add_ip_nei') def test_add_ip_nei(self, mock_add_ip_nei): linux_net.add_ip_nei(self.ip, self.mac, self.dev) mock_add_ip_nei.assert_called_once_with(self.ip, self.mac, self.dev) @mock.patch.object(linux_net, 'del_ip_nei') @mock.patch('ovn_bgp_agent.privileged.linux_net.rule_delete') def test_del_ip_rule(self, mock_rule_delete, mock_del_ip_nei): linux_net.del_ip_rule(self.ip, 7, dev=self.dev, lladdr=self.mac) expected_args = {'dst': self.ip, 'table': 7, 'dst_len': 32, 'family': constants.AF_INET} mock_rule_delete.assert_called_once_with(expected_args) mock_del_ip_nei.assert_called_once_with(self.ip, self.mac, self.dev) @mock.patch.object(linux_net, 'del_ip_nei') @mock.patch('ovn_bgp_agent.privileged.linux_net.rule_delete') def test_del_ip_rule_ipv6(self, mock_rule_delete, mock_del_ip_nei): linux_net.del_ip_rule(self.ipv6, 7, dev=self.dev, lladdr=self.mac) expected_args = {'dst': self.ipv6, 'table': 7, 'dst_len': 128, 'family': constants.AF_INET6} mock_rule_delete.assert_called_once_with(expected_args) mock_del_ip_nei.assert_called_once_with(self.ipv6, self.mac, self.dev) @mock.patch.object(linux_net, 'del_ip_nei') @mock.patch('ovn_bgp_agent.privileged.linux_net.rule_delete') def test_del_ip_rule_invalid_ip(self, mock_rule_delete, mock_del_ip_nei): self.assertRaises(agent_exc.InvalidPortIP, linux_net.del_ip_rule, '10.10.1.6/30/128', 7) mock_rule_delete.assert_not_called() mock_del_ip_nei.assert_not_called() @mock.patch('ovn_bgp_agent.privileged.linux_net.del_ip_nei') def test_del_ip_nei(self, mock_del_ip_nei): linux_net.del_ip_nei(self.ip, self.mac, self.dev) mock_del_ip_nei.assert_called_once_with(self.ip, self.mac, self.dev) @mock.patch('ovn_bgp_agent.privileged.linux_net.add_unreachable_route') def test_add_unreachable_route(self, mock_add_route): linux_net.add_unreachable_route('fake-vrf') mock_add_route.assert_called_once_with('fake-vrf') @mock.patch('ovn_bgp_agent.privileged.linux_net.route_create') def test_add_ip_route(self, mock_route_create): routes = {} linux_net.add_ip_route(routes, self.ip, 7, self.dev) expected_routes = { self.dev: [{'route': {'dst': self.ip, 'dst_len': 32, 'oif': mock.ANY, 'proto': 3, 'scope': 253, 'table': 7}, 'vlan': None}]} self.assertEqual(expected_routes, routes) mock_route_create.assert_not_called() @mock.patch('ovn_bgp_agent.privileged.linux_net.route_create') def test_add_ip_route_ipv6(self, mock_route_create): routes = {} linux_net.add_ip_route(routes, self.ipv6, 7, self.dev) expected_routes = { self.dev: [{'route': {'dst': self.ipv6, 'dst_len': 128, 'family': constants.AF_INET6, 'oif': mock.ANY, 'proto': 3, 'table': 7}, 'vlan': None}]} self.assertEqual(expected_routes, routes) mock_route_create.assert_not_called() @mock.patch('ovn_bgp_agent.privileged.linux_net.route_create') def test_add_ip_route_via(self, mock_route_create): routes = {} linux_net.add_ip_route(routes, self.ip, 7, self.dev, via='1.1.1.1') expected_routes = { self.dev: [{'route': {'dst': self.ip, 'dst_len': 32, 'gateway': '1.1.1.1', 'oif': mock.ANY, 'proto': 3, 'scope': 0, 'table': 7}, 'vlan': None}]} self.assertEqual(expected_routes, routes) mock_route_create.assert_not_called() @mock.patch('ovn_bgp_agent.privileged.linux_net.route_create') def test_add_ip_route_vlan(self, mock_route_create): routes = {} linux_net.add_ip_route(routes, self.ip, 7, self.dev, vlan=10) expected_routes = { self.dev: [{'route': {'dst': self.ip, 'dst_len': 32, 'oif': mock.ANY, 'proto': 3, 'scope': 253, 'table': 7}, 'vlan': 10}]} self.assertEqual(expected_routes, routes) mock_route_create.assert_not_called() @mock.patch.object(linux_net, 'get_interface_index') @mock.patch.object(linux_net, 'ensure_vlan_device_for_network') @mock.patch('ovn_bgp_agent.privileged.linux_net.route_create') def test_add_ip_route_vlan_keyerror(self, mock_route_create, mock_ensure_vlan_device, mock_get_index): routes = {} oif = '5' mock_get_index.side_effect = [agent_exc.NetworkInterfaceNotFound, oif] linux_net.add_ip_route(routes, self.ip, 7, self.dev, vlan=10) expected_routes = { self.dev: [{'route': {'dst': self.ip, 'dst_len': 32, 'oif': oif, 'proto': 3, 'scope': 253, 'table': 7}, 'vlan': 10}]} self.assertEqual(expected_routes, routes) mock_ensure_vlan_device.assert_called_once_with(self.dev, 10) mock_route_create.assert_not_called() @mock.patch('ovn_bgp_agent.privileged.linux_net.route_create') def test_add_ip_route_mask(self, mock_route_create): routes = {} linux_net.add_ip_route(routes, self.ip, 7, self.dev, mask=30) expected_routes = { self.dev: [{'route': {'dst': self.ip, 'dst_len': 30, 'oif': mock.ANY, 'proto': 3, 'scope': 253, 'table': 7}, 'vlan': None}]} self.assertEqual(expected_routes, routes) mock_route_create.assert_not_called() @mock.patch('ovn_bgp_agent.privileged.linux_net.route_create') def test_add_ip_route_no_route(self, mock_route_create): self.fake_ipr.route.return_value = () routes = {} linux_net.add_ip_route(routes, self.ip, 7, self.dev) expected_routes = { self.dev: [{'route': {'dst': self.ip, 'dst_len': 32, 'oif': mock.ANY, 'proto': 3, 'scope': 253, 'table': 7}, 'vlan': None}]} self.assertEqual(expected_routes, routes) mock_route_create.assert_called_once_with( expected_routes[self.dev][0]['route']) @mock.patch('ovn_bgp_agent.privileged.linux_net.route_delete') def test_del_ip_route(self, mock_route_delete): routes = { self.dev: [{'route': {'dst': self.ip, 'dst_len': 32, 'oif': mock.ANY, 'proto': 3, 'scope': 253, 'table': 7}, 'vlan': None}]} route = copy.deepcopy(routes[self.dev][0]['route']) linux_net.del_ip_route(routes, self.ip, 7, self.dev) self.assertEqual({self.dev: []}, routes) mock_route_delete.assert_called_once_with(route) @mock.patch('ovn_bgp_agent.privileged.linux_net.route_delete') def test_del_ip_route_ipv6(self, mock_route_delete): routes = { self.dev: [{'route': {'dst': self.ipv6, 'dst_len': 128, 'family': constants.AF_INET6, 'oif': mock.ANY, 'proto': 3, 'table': 7}, 'vlan': None}]} route = copy.deepcopy(routes[self.dev][0]['route']) linux_net.del_ip_route(routes, self.ipv6, 7, self.dev) self.assertEqual({self.dev: []}, routes) mock_route_delete.assert_called_once_with(route) @mock.patch('ovn_bgp_agent.privileged.linux_net.route_delete') def test_del_ip_route_via(self, mock_route_delete): routes = { self.dev: [{'route': {'dst': self.ip, 'dst_len': 32, 'oif': mock.ANY, 'gateway': '1.1.1.1', 'proto': 3, 'scope': 0, 'table': 7}, 'vlan': None}]} route = copy.deepcopy(routes[self.dev][0]['route']) linux_net.del_ip_route(routes, self.ip, 7, self.dev, via='1.1.1.1') self.assertEqual({self.dev: []}, routes) mock_route_delete.assert_called_once_with(route) @mock.patch('ovn_bgp_agent.privileged.linux_net.route_delete') def test_del_ip_route_vlan(self, mock_route_delete): routes = { self.dev: [{'route': {'dst': self.ip, 'dst_len': 32, 'oif': mock.ANY, 'proto': 3, 'scope': 253, 'table': 7}, 'vlan': 10}]} route = copy.deepcopy(routes[self.dev][0]['route']) linux_net.del_ip_route(routes, self.ip, 7, self.dev, vlan=10) self.assertEqual({self.dev: []}, routes) mock_route_delete.assert_called_once_with(route) @mock.patch('ovn_bgp_agent.privileged.linux_net.route_delete') def test_del_ip_route_mask(self, mock_route_delete): routes = { self.dev: [{'route': {'dst': self.ip, 'dst_len': 30, 'oif': mock.ANY, 'proto': 3, 'scope': 253, 'table': 7}, 'vlan': None}]} route = copy.deepcopy(routes[self.dev][0]['route']) linux_net.del_ip_route(routes, self.ip, 7, self.dev, mask=30) self.assertEqual({self.dev: []}, routes) mock_route_delete.assert_called_once_with(route) class TestEnsureRoutingTableForBridge(test_base.TestCase): def setUp(self): super().setUp() self.ovn_routing_tables = {} self.bridge_name = "br-test" self.vrf_table = 4 self.generated_number = 2 self.testing_multiline_file_content = [ "1 foo", "# commented line", "random garbage text", "3 another bridge", "2", ] self.m_ensure_rt_routes = mock.patch.object( linux_net, '_ensure_routing_table_routes').start() # The 'random' generator will always generate number 2 because the # range is 1-4 while 1 and 3 are used in the file and 4 is the vrf mock.patch.object(constants, 'ROUTING_TABLE_MIN', 1).start() mock.patch.object(constants, 'ROUTING_TABLE_MAX', 4).start() def _create_fake_file_content(self): return "\n".join(self.testing_multiline_file_content) def test_ensure_routing_table_for_bridge_table_missing(self): self._test_ensure_routing_table_for_bridge_table_missing() def _test_ensure_routing_table_for_bridge_table_missing(self): with mock.patch( 'builtins.open', mock.mock_open(read_data=self._create_fake_file_content())): linux_net.ensure_routing_table_for_bridge( self.ovn_routing_tables, self.bridge_name, self.vrf_table) self.assertDictEqual( {self.bridge_name: self.generated_number}, self.ovn_routing_tables) def test_ensure_routing_table_for_bridge_table_present(self): present_bridge_value = 5 self.testing_multiline_file_content.insert( 2, "%d %s" % (present_bridge_value, self.bridge_name)) with mock.patch( 'builtins.open', mock.mock_open(read_data=self._create_fake_file_content())): linux_net.ensure_routing_table_for_bridge( self.ovn_routing_tables, self.bridge_name, self.vrf_table) self.assertDictEqual( {self.bridge_name: present_bridge_value}, self.ovn_routing_tables) def test_ensure_routing_table_for_bridge_table_vrf_not_generated(self): self.vrf_table = 2 self.generated_number = 4 self._test_ensure_routing_table_for_bridge_table_missing() def test_ensure_routing_table_for_bridge_tables_depleted(self): present_bridge_value = 2 self.testing_multiline_file_content.insert( 2, "%d %s" % (present_bridge_value, "foo")) with mock.patch( 'builtins.open', mock.mock_open(read_data=self._create_fake_file_content())): self.assertRaises( SystemExit, linux_net.ensure_routing_table_for_bridge, self.ovn_routing_tables, self.bridge_name, self.vrf_table) self.assertDictEqual({}, self.ovn_routing_tables) ovn-bgp-agent-2.0.1/ovn_bgp_agent/tests/utils.py000066400000000000000000000033271460327367600216650ustar00rootroot00000000000000# Copyright 2022 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import eventlet class WaitTimeout(Exception): """Default exception coming from wait_until_true() function.""" def create_row(**kwargs): return type('FakeRow', (object,), kwargs) def wait_until_true(predicate, timeout=60, sleep=1, exception=None): """Wait until callable predicate is evaluated as True Imported from ``neutron.common.utils``. :param predicate: Callable deciding whether waiting should continue. Best practice is to instantiate predicate with functools.partial() :param timeout: Timeout in seconds how long should function wait. :param sleep: Polling interval for results in seconds. :param exception: Exception instance to raise on timeout. If None is passed (default) then WaitTimeout exception is raised. """ try: with eventlet.Timeout(timeout): while not predicate(): eventlet.sleep(sleep) except eventlet.Timeout: if exception is not None: # pylint: disable=raising-bad-type raise exception raise WaitTimeout('Timed out after %d seconds' % timeout) ovn-bgp-agent-2.0.1/ovn_bgp_agent/utils/000077500000000000000000000000001460327367600201445ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/utils/__init__.py000066400000000000000000000000001460327367600222430ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/ovn_bgp_agent/utils/common.py000066400000000000000000000012531460327367600220070ustar00rootroot00000000000000# Copyright 2024 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from ovn_bgp_agent import constants IP_VERSION_FAMILY_MAP = {4: constants.AF_INET, 6: constants.AF_INET6} ovn-bgp-agent-2.0.1/ovn_bgp_agent/utils/helpers.py000066400000000000000000000025521460327367600221640ustar00rootroot00000000000000# Copyright 2023 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_log import log as logging LOG = logging.getLogger(__name__) def parse_bridge_mapping(bridge_mapping): try: network, bridge = bridge_mapping.split(":") except ValueError: LOG.warning("Incorrect bridge mapping settings: %s", bridge_mapping) return None, None return network, bridge def _get_lb_datapath_group(lb, attr): try: dp = getattr(lb, attr)[0].datapaths if dp: return dp except (AttributeError, IndexError): pass return [] def get_lb_datapath_groups(lb): for attr in ('ls_datapath_group', 'datapath_group'): ls_dp = _get_lb_datapath_group(lb, attr) if ls_dp: break lr_dp = _get_lb_datapath_group(lb, 'lr_datapath_group') return (ls_dp, lr_dp) ovn-bgp-agent-2.0.1/ovn_bgp_agent/utils/linux_net.py000066400000000000000000000657701460327367600225420ustar00rootroot00000000000000# Copyright 2021 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import ipaddress import random import re import sys import netaddr from oslo_log import log as logging import pyroute2 from pyroute2.netlink import exceptions as netlink_exceptions import tenacity from ovn_bgp_agent import constants from ovn_bgp_agent import exceptions as agent_exc import ovn_bgp_agent.privileged.linux_net from ovn_bgp_agent.utils import common as common_utils LOG = logging.getLogger(__name__) RE_TABLE_ROW = re.compile(r"^(?P[0-9]+)\s+(?P\S+)") def get_ip_version(ip): # IP network can consume both an IP address and a network with cidr # notation return netaddr.IPNetwork(ip).version @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_interfaces(filter_out=[]): with pyroute2.IPRoute() as ipr: return [iface.get_attr('IFLA_IFNAME') for iface in ipr.get_links() if iface.get_attr('IFLA_IFNAME') not in filter_out] @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_interface_index(nic): try: with pyroute2.IPRoute() as ipr: return ipr.link_lookup(ifname=nic)[0] except IndexError: raise agent_exc.NetworkInterfaceNotFound(device=nic) @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_interface_address(nic): try: with pyroute2.IPRoute() as ipr: idx = ipr.link_lookup(ifname=nic)[0] return ipr.get_links(idx)[0].get_attr('IFLA_ADDRESS') except IndexError: raise agent_exc.NetworkInterfaceNotFound(device=nic) @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_nic_info(nic): try: with pyroute2.IPRoute() as ipr: idx = ipr.link_lookup(ifname=nic)[0] nic_addr = ipr.get_addr(index=idx)[0] ip = '{}/{}'.format( nic_addr.get_attr('IFA_ADDRESS'), nic_addr.get('prefixlen')) mac = ipr.get_links(idx)[0].get_attr('IFLA_ADDRESS') return ip, mac except IndexError: raise agent_exc.NetworkInterfaceNotFound(device=nic) def ensure_vrf(vrf_name, vrf_table): ovn_bgp_agent.privileged.linux_net.ensure_vrf(vrf_name, vrf_table) def ensure_bridge(bridge_name): ovn_bgp_agent.privileged.linux_net.ensure_bridge(bridge_name) def ensure_vxlan(vxlan_name, vni, local_ip, dstport): ovn_bgp_agent.privileged.linux_net.ensure_vxlan(vxlan_name, vni, local_ip, dstport) def ensure_veth(veth_name, veth_peer): ovn_bgp_agent.privileged.linux_net.ensure_veth(veth_name, veth_peer) def set_master_for_device(device, master): ovn_bgp_agent.privileged.linux_net.set_master_for_device(device, master) def ensure_dummy_device(device): ovn_bgp_agent.privileged.linux_net.ensure_dummy_device(device) def ensure_ovn_device(ovn_ifname, vrf_name): ensure_dummy_device(ovn_ifname) set_master_for_device(ovn_ifname, vrf_name) def delete_device(device): ovn_bgp_agent.privileged.linux_net.delete_device(device) def ensure_arp_ndp_enabled_for_bridge(bridge, offset, vlan_tag=None): ipv4 = "%s%d.%s" % ( constants.ARP_IPV4_PREFIX, offset / constants.IPV4_OCTET_RANGE, offset % constants.IPV4_OCTET_RANGE) ipv6 = "%s%x" % (constants.NDP_IPV6_PREFIX, offset) for ip in (ipv4, ipv6): try: ovn_bgp_agent.privileged.linux_net.add_ip_to_dev(ip, bridge) except agent_exc.IpAddressAlreadyExists: LOG.debug("IP %s already added on bridge %s", ip, bridge) except KeyError as e: if "object exists" not in str(e): LOG.error("Unable to add IP on bridge %s to enable arp/ndp. " "Exception: %s", bridge, e) raise # also enable the arp/ndp on the bridge in case there are flat networks enable_proxy_arp(bridge) enable_proxy_ndp(bridge) def ensure_routing_table_for_bridge(ovn_routing_tables, bridge, vrf_table): # check a routing table with the bridge name exists on # /etc/iproute2/rt_tables found_tables = {vrf_table} with open(constants.ROUTING_TABLES_FILE, 'r') as rt_file: for line in rt_file.readlines(): match = RE_TABLE_ROW.match(line) if match: if match.group('bridge') == bridge: # We don't need to catch exception for TypeError because # the regular expression matches only integers ovn_routing_tables[match.group('bridge')] = int( match.group('table')) LOG.debug("Found routing table for %s with: %s", bridge, match.group('table')) break else: found_tables.add(int(match.group('table'))) else: LOG.debug("Routing table for bridge %s not configured at ", bridge) try: routing_table_range = set( range(constants.ROUTING_TABLE_MIN, constants.ROUTING_TABLE_MAX + 1)) table_number = random.choice( list(routing_table_range - found_tables)) except IndexError: LOG.error("No more routing tables available for bridge %s " "at %s", constants.ROUTING_TABLES_FILE, bridge) sys.exit(1) ovn_bgp_agent.privileged.linux_net.create_routing_table_for_bridge( table_number, bridge) ovn_routing_tables[bridge] = int(table_number) LOG.debug("Added routing table for %s with number: %s", bridge, table_number) return _ensure_routing_table_routes(ovn_routing_tables, bridge) @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def _ensure_routing_table_routes(ovn_routing_tables, bridge): # add default route on that table if it does not exist extra_routes = [] bridge_idx = get_interface_index(bridge) with pyroute2.IPRoute() as ip: table_route_dsts = { (r.get_attr('RTA_DST'), r['dst_len']) for r in ip.get_routes(table=ovn_routing_tables[bridge]) } if not table_route_dsts: r1 = {'dst': 'default', 'oif': bridge_idx, 'table': ovn_routing_tables[bridge], 'scope': 253, 'proto': 3} ovn_bgp_agent.privileged.linux_net.route_create(r1) r2 = {'dst': 'default', 'oif': bridge_idx, 'table': ovn_routing_tables[bridge], 'family': constants.AF_INET6, 'proto': 3} ovn_bgp_agent.privileged.linux_net.route_create(r2) else: route_missing = True route6_missing = True for (dst, dst_len) in table_route_dsts: if not dst: # default route try: route = [ r for r in ip.get_routes( table=ovn_routing_tables[bridge], family=constants.AF_INET) if not r.get_attr('RTA_DST')][0] if bridge_idx == route.get_attr('RTA_OIF'): route_missing = False else: extra_routes.append(route) except IndexError: pass # no ipv4 default rule try: route_6 = [ r for r in ip.get_routes( table=ovn_routing_tables[bridge], family=constants.AF_INET6) if not r.get_attr('RTA_DST')][0] if bridge_idx == route_6.get_attr('RTA_OIF'): route6_missing = False else: extra_routes.append(route_6) except IndexError: pass # no ipv6 default rule else: if get_ip_version(dst) == constants.IP_VERSION_6: extra_routes.append( ip.get_routes( table=ovn_routing_tables[bridge], dst=dst, dst_len=dst_len, family=constants.AF_INET6)[0]) else: extra_routes.append( ip.get_routes( table=ovn_routing_tables[bridge], dst=dst, dst_len=dst_len, family=constants.AF_INET)[0]) if route_missing: r = {'dst': 'default', 'oif': bridge_idx, 'table': ovn_routing_tables[bridge], 'scope': 253, 'proto': 3} ovn_bgp_agent.privileged.linux_net.route_create(r) if route6_missing: r = {'dst': 'default', 'oif': bridge_idx, 'table': ovn_routing_tables[bridge], 'family': constants.AF_INET6, 'proto': 3} ovn_bgp_agent.privileged.linux_net.route_create(r) return extra_routes @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_extra_routing_table_for_bridge(ovn_routing_tables, bridge): extra_routes = [] bridge_idx = get_interface_index(bridge) with pyroute2.IPRoute() as ip: table_route_dsts = { (r.get_attr('RTA_DST'), r['dst_len']) for r in ip.get_routes(table=ovn_routing_tables[bridge]) } if not table_route_dsts: return extra_routes for (dst, dst_len) in table_route_dsts: if not dst: # default route try: route = [ r for r in ip.get_routes( table=ovn_routing_tables[bridge], family=constants.AF_INET) if not r.get_attr('RTA_DST')][0] if bridge_idx != route.get_attr('RTA_OIF'): extra_routes.append(route) except IndexError: pass # no IPv4 default rule try: route_6 = [ r for r in ip.get_routes( table=ovn_routing_tables[bridge], family=constants.AF_INET6) if not r.get_attr('RTA_DST')][0] if bridge_idx != route_6.get_attr('RTA_OIF'): extra_routes.append(route_6) except IndexError: pass # no IPv6 default rule else: if get_ip_version(dst) == constants.IP_VERSION_6: extra_routes.append( ip.get_routes( table=ovn_routing_tables[bridge], dst=dst, dst_len=dst_len, family=constants.AF_INET6)[0]) else: extra_routes.append( ip.get_routes( table=ovn_routing_tables[bridge], dst=dst, dst_len=dst_len, family=constants.AF_INET)[0]) return extra_routes def ensure_vlan_device_for_network(bridge, vlan_tag): ovn_bgp_agent.privileged.linux_net.ensure_vlan_device_for_network(bridge, vlan_tag) device = "{}/{}".format(bridge, vlan_tag) enable_proxy_arp(device) enable_proxy_ndp(device) def delete_vlan_device_for_network(bridge, vlan_tag): vlan_device_name = '{}.{}'.format(bridge, vlan_tag) delete_device(vlan_device_name) def get_bridge_vlans(bridge): return ovn_bgp_agent.privileged.linux_net.get_bridge_vlans(bridge) def enable_proxy_ndp(device): flag = "net.ipv6.conf.{}.proxy_ndp".format(device) ovn_bgp_agent.privileged.linux_net.set_kernel_flag(flag, 1) def enable_proxy_arp(device): flag = "net.ipv4.conf.{}.proxy_arp".format(device) ovn_bgp_agent.privileged.linux_net.set_kernel_flag(flag, 1) @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_exposed_ips(nic): nic_idx = get_interface_index(nic) try: with pyroute2.IPRoute() as ipr: return [ip.get_attr('IFA_ADDRESS') for ip in ipr.get_addr(index=nic_idx) if ip['prefixlen'] in (32, 128)] except pyroute2.netlink.exceptions.NetlinkError: # Nic does not exist LOG.debug("NIC %s does not yet exist, so it does not have exposed IPs", nic) return [] @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_nic_ip(nic, prefixlen_filter=None): nic_idx = get_interface_index(nic) with pyroute2.IPRoute() as ipr: if prefixlen_filter: return [ ip.get_attr('IFA_ADDRESS') for ip in ipr.get_addr(index=nic_idx, prefixlen=prefixlen_filter) ] else: return [ ip.get_attr('IFA_ADDRESS') for ip in ipr.get_addr(index=nic_idx) ] def get_exposed_ips_on_network(nic, network): exposed_ips = get_exposed_ips(nic) return [ip for ip in exposed_ips if ipaddress.ip_address(ip) in network] @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_exposed_routes_on_network(table_ids, network): with pyroute2.NDB() as ndb: # NOTE: skip bgp routes (proto 186) return [ r for r in ndb.routes.dump() if r.table in table_ids and r.dst != "" and r.gateway is not None and r.proto != 186 and ipaddress.ip_address(r.gateway) in network ] @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_ovn_ip_rules(routing_tables): ovn_ip_rules = {} with pyroute2.IPRoute() as ipr: rules_info = [ (rule.get_attr('FRA_TABLE'), "{}/{}".format(rule.get_attr('FRA_DST'), rule['dst_len']), rule['family']) for rule in ( ipr.get_rules(family=constants.AF_INET) + ipr.get_rules(family=constants.AF_INET6)) if rule.get_attr('FRA_TABLE') in routing_tables ] for table, dst, family in rules_info: ovn_ip_rules[dst] = {'table': table, 'family': family} return ovn_ip_rules def delete_exposed_ips(ips, nic): ovn_bgp_agent.privileged.linux_net.delete_exposed_ips(ips, nic) def delete_ip_rules(ip_rules): ovn_bgp_agent.privileged.linux_net.delete_ip_rules(ip_rules) def delete_bridge_ip_routes(routing_tables, routing_tables_routes, extra_routes): for device, routes_info in routing_tables_routes.items(): if not extra_routes.get(device): continue for route_info in routes_info: oif = get_interface_index(device) if route_info['vlan']: vlan_device_name = '{}.{}'.format(device, route_info['vlan']) oif = get_interface_index(vlan_device_name) if 'gateway' in route_info['route'].keys(): # subnet route possible_matchings = [ r for r in extra_routes[device] if (r.get_attr('RTA_DST') == route_info['route']['dst'] and r['dst_len'] == route_info['route']['dst_len'] and r.get_attr('RTA_GATEWAY') == route_info['route'][ 'gateway'])] else: # cr-lrp possible_matchings = [ r for r in extra_routes[device] if (r.get_attr('RTA_DST') == route_info['route']['dst'] and r['dst_len'] == route_info['route']['dst_len'] and r.get_attr('RTA_OIF') == oif)] for r in possible_matchings: extra_routes[device].remove(r) for bridge, routes in extra_routes.items(): for route in routes: r_info = {'dst': route.get_attr('RTA_DST'), 'dst_len': route['dst_len'], 'family': route['family'], 'oif': route.get_attr('RTA_OIF'), 'table': routing_tables[bridge]} if route.get_attr('RTA_GATEWAY'): r_info['gateway'] = route.get_attr('RTA_GATEWAY') ovn_bgp_agent.privileged.linux_net.route_delete(r_info) def delete_routes_from_table(table): table_routes = _get_table_routes(table) delete_ip_routes(table_routes) @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def _get_table_routes(table): with pyroute2.IPRoute() as ipr: return [ r for r in ipr.get_routes(table=table) if r['scope'] != 254 and r['proto'] != 186 ] @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_routes_on_tables(table_ids): routes = [] with pyroute2.IPRoute() as ipr: for table_id in table_ids: table_routes = [ r for r in ipr.get_routes(table=table_id) if r.get_attr('RTA_DST') and r['proto'] != 186 ] routes.extend(table_routes) return routes def delete_ip_routes(routes): for route in routes: r_info = {'dst': route.get('dst'), 'dst_len': route['dst_len'], 'family': route['family'], 'oif': route.get('oif'), 'gateway': route.get('gateway'), 'table': route['table']} ovn_bgp_agent.privileged.linux_net.route_delete(r_info) def add_ndp_proxy(ip, dev, vlan=None): ovn_bgp_agent.privileged.linux_net.add_ndp_proxy(ip, dev, vlan) def del_ndp_proxy(ip, dev, vlan=None): ovn_bgp_agent.privileged.linux_net.del_ndp_proxy(ip, dev, vlan) def add_ips_to_dev(nic, ips, clear_local_route_at_table=False): already_added_ips = [] for ip in ips: try: ovn_bgp_agent.privileged.linux_net.add_ip_to_dev(ip, nic) except agent_exc.IpAddressAlreadyExists: already_added_ips.append(ip) if clear_local_route_at_table: for ip in ips: if ip in already_added_ips: continue oif = get_interface_index(nic) route = {'table': clear_local_route_at_table, 'proto': 2, 'scope': 254, 'dst': ip, 'oif': oif} ovn_bgp_agent.privileged.linux_net.route_delete(route) def del_ips_from_dev(nic, ips): for ip in ips: ovn_bgp_agent.privileged.linux_net.del_ip_from_dev(ip, nic) def create_rule_from_ip(ip, table): try: ip_network = netaddr.IPNetwork(ip) except (netaddr.AddrFormatError, ValueError): raise agent_exc.InvalidPortIP(ip=ip) return { 'dst': str(ip_network.ip), 'table': table, 'dst_len': ip_network.prefixlen, 'family': common_utils.IP_VERSION_FAMILY_MAP[ip_network.version], } def add_ip_rule(ip, table, dev=None, lladdr=None): rule = create_rule_from_ip(ip, table) ovn_bgp_agent.privileged.linux_net.rule_create(rule) if lladdr: add_ip_nei(ip, lladdr, dev) def add_ip_nei(ip, lladdr, dev): """Add ip neighbor permament entry param ip: IP of the neighbor to add an entry for param lladdr: link layer address of the neighbor to associate to that IP param dev: the interface to which the neighbor is attached """ ovn_bgp_agent.privileged.linux_net.add_ip_nei(ip, lladdr, dev) def del_ip_rule(ip, table, dev=None, lladdr=None): rule = create_rule_from_ip(ip, table) ovn_bgp_agent.privileged.linux_net.rule_delete(rule) if lladdr: del_ip_nei(ip, lladdr, dev) def del_ip_nei(ip, lladdr, dev): """Del ip neighbor permament entry param ip: IP of the neighbor to delete the entry param lladdr: link layer address of the neighbor to disassociate param dev: the interface to which the neighbor is attached """ ovn_bgp_agent.privileged.linux_net.del_ip_nei(ip, lladdr, dev) def add_unreachable_route(vrf_name): ovn_bgp_agent.privileged.linux_net.add_unreachable_route(vrf_name) @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def add_ip_route(ovn_routing_tables_routes, ip_address, route_table, dev, vlan=None, mask=None, via=None): net_ip = ip_address if not mask: # default /32 or /128 if get_ip_version(ip_address) == constants.IP_VERSION_6: mask = 128 else: mask = 32 else: ip = '{}/{}'.format(ip_address, mask) if get_ip_version(ip_address) == constants.IP_VERSION_6: net_ip = '{}'.format(ipaddress.IPv6Network( ip, strict=False).network_address) else: net_ip = '{}'.format(ipaddress.IPv4Network( ip, strict=False).network_address) if vlan: oif_name = '{}.{}'.format(dev, vlan) try: oif = get_interface_index(oif_name) except agent_exc.NetworkInterfaceNotFound: # Most provider network was recently created an # there has not been a sync since then, therefore # the vlan device has not yet been created # Trying to create the device and retrying ensure_vlan_device_for_network(dev, vlan) oif = get_interface_index(oif_name) else: oif = get_interface_index(dev) route = {'dst': net_ip, 'dst_len': int(mask), 'oif': oif, 'table': int(route_table), 'proto': 3} if via: route['gateway'] = via route['scope'] = 0 else: route['scope'] = 253 if get_ip_version(net_ip) == constants.IP_VERSION_6: route['family'] = constants.AF_INET6 del route['scope'] with pyroute2.IPRoute() as ipr: if not ipr.route('show', **route): LOG.debug("Creating route at table %s: %s", route_table, route) ovn_bgp_agent.privileged.linux_net.route_create(route) LOG.debug("Route created at table %s: %s", route_table, route) else: LOG.debug("Route already existing: %s", route) route_info = {'vlan': vlan, 'route': route} ovn_routing_tables_routes.setdefault(dev, []).append(route_info) def del_ip_route(ovn_routing_tables_routes, ip_address, route_table, dev, vlan=None, mask=None, via=None): net_ip = ip_address if not mask: # default /32 or /128 if get_ip_version(ip_address) == constants.IP_VERSION_6: mask = 128 else: mask = 32 else: ip = '{}/{}'.format(ip_address, mask) if get_ip_version(ip_address) == constants.IP_VERSION_6: net_ip = '{}'.format(ipaddress.IPv6Network( ip, strict=False).network_address) else: net_ip = '{}'.format(ipaddress.IPv4Network( ip, strict=False).network_address) try: if vlan: oif_name = '{}.{}'.format(dev, vlan) oif = get_interface_index(oif_name) else: oif = get_interface_index(dev) except agent_exc.NetworkInterfaceNotFound: LOG.debug("Device %s does not exists, so the associated " "routes should have been automatically deleted.", dev) ovn_routing_tables_routes.pop(dev, None) return route = {'dst': net_ip, 'dst_len': int(mask), 'oif': oif, 'table': int(route_table), 'proto': 3} if via: route['gateway'] = via route['scope'] = 0 else: route['scope'] = 253 if get_ip_version(net_ip) == constants.IP_VERSION_6: route['family'] = constants.AF_INET6 del route['scope'] LOG.debug("Deleting route at table %s: %s", route_table, route) ovn_bgp_agent.privileged.linux_net.route_delete(route) LOG.debug("Route deleted at table %s: %s", route_table, route) route_info = {'vlan': vlan, 'route': route} if route_info in ovn_routing_tables_routes.get(dev, []): ovn_routing_tables_routes[dev].remove(route_info) def set_device_status(device, status, ndb=None): ovn_bgp_agent.privileged.linux_net.set_device_state( device, status, ndb=ndb) ovn-bgp-agent-2.0.1/releasenotes/000077500000000000000000000000001460327367600166655ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/releasenotes/notes/000077500000000000000000000000001460327367600200155ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/releasenotes/notes/.placeholder000066400000000000000000000000001460327367600222660ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/releasenotes/notes/nb_driver-cc7098183fcedb0a.yaml000066400000000000000000000007211460327367600252270ustar00rootroot00000000000000--- features: - | This patch introduces a new driver that instead of connecting to the OVN SB DB to watch for relevant events, it connects to the OVN NB DB. The main reasons for doing so are: 1) scalability purposes; and 2) rely on the stable fields offered by the NB DB, instead of the SB DB that may change any time and break our watchers logic (as it has already happened with the OVN Load_Balancer table and its datapath field usage). ovn-bgp-agent-2.0.1/releasenotes/source/000077500000000000000000000000001460327367600201655ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/releasenotes/source/2023.2.rst000066400000000000000000000002021460327367600214370ustar00rootroot00000000000000=========================== 2023.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.2 ovn-bgp-agent-2.0.1/releasenotes/source/_static/000077500000000000000000000000001460327367600216135ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/releasenotes/source/_static/.placeholder000066400000000000000000000000001460327367600240640ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/releasenotes/source/_templates/000077500000000000000000000000001460327367600223225ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/releasenotes/source/_templates/.placeholder000066400000000000000000000000001460327367600245730ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/releasenotes/source/conf.py000066400000000000000000000215311460327367600214660ustar00rootroot00000000000000# -*- coding: utf-8 -*- # 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. # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'openstackdocstheme', 'reno.sphinxext', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'ovn_bgp_agent Release Notes' copyright = u'2017, OpenStack Developers' # openstackdocstheme options openstackdocs_repo_name = 'x/ovn-bgp-agent' openstackdocs_bug_project = 'ovn-bgp-agent' openstackdocs_bug_tag = '' openstackdocs_auto_name = False # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. # The full version, including alpha/beta/rc tags. release = '' # The short X.Y version. version = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'ovn_bgp_agentReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'ovn_bgp_agentReleaseNotes.tex', u'ovn_bgp_agent Release Notes Documentation', u'OpenStack Foundation', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'ovn_bgp_agentrereleasenotes', u'ovn_bgp_agent Release Notes Documentation', [u'OpenStack Foundation'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'ovn_bgp_agent ReleaseNotes', u'ovn_bgp_agent Release Notes Documentation', u'OpenStack Foundation', 'ovn_bgp_agentReleaseNotes', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] ovn-bgp-agent-2.0.1/releasenotes/source/index.rst000066400000000000000000000002561460327367600220310ustar00rootroot00000000000000============================================ ovn_bgp_agent Release Notes ============================================ .. toctree:: :maxdepth: 1 unreleased 2023.2 ovn-bgp-agent-2.0.1/releasenotes/source/unreleased.rst000066400000000000000000000001601460327367600230430ustar00rootroot00000000000000============================== Current Series Release Notes ============================== .. release-notes:: ovn-bgp-agent-2.0.1/requirements.txt000066400000000000000000000013151460327367600174600ustar00rootroot00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr>=2.0 # Apache-2.0 Jinja2>=2.10 # BSD License (3 clause) netaddr>=0.7.18 # BSD neutron-lib>=2.10.1 # Apache-2.0 oslo.concurrency>=3.26.0 # Apache-2.0 oslo.config>=6.1.0 # Apache-2.0 oslo.log>=3.36.0 # Apache-2.0 oslo.privsep>=2.3.0 # Apache-2.0 oslo.rootwrap>=5.15.0 # Apache-2.0 oslo.service>=1.40.2 # Apache-2.0 ovs>=2.8.0 # Apache-2.0 ovsdbapp>=1.16.0 # Apache-2.0 pyroute2>=0.6.6;sys_platform!='win32' # Apache-2.0 (+ dual licensed GPL2) stevedore>=1.20.0 # Apache-2.0 tenacity>=6.0.0 # Apache-2.0 ovn-bgp-agent-2.0.1/setup.cfg000066400000000000000000000031031460327367600160120ustar00rootroot00000000000000[metadata] name = ovn-bgp-agent summary = The OVN BGP Agent allows to expose VMs/Containers/Networks through BGP on OVN description-file = README.rst author = OpenStack author-email = openstack-discuss@lists.openstack.org home-page = https://www.openstack.org/ python-requires = >=3.6 classifier = Environment :: OpenStack Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython [files] packages = ovn_bgp_agent data_files = etc/ovn-bgp-agent = etc/ovn-bgp-agent/rootwrap.conf etc/ovn-bgp-agent/rootwrap.d = etc/ovn-bgp-agent/rootwrap.d/* [entry_points] console_scripts = ovn-bgp-agent = ovn_bgp_agent.cmd.agent:start ovn-bgp-agent-rootwrap = oslo_rootwrap.cmd:main ovn-bgp-agent-rootwrap-daemon = oslo_rootwrap.cmd:daemon ovn_bgp_agent.drivers = ovn_bgp_driver = ovn_bgp_agent.drivers.openstack.ovn_bgp_driver:OVNBGPDriver nb_ovn_bgp_driver = ovn_bgp_agent.drivers.openstack.nb_ovn_bgp_driver:NBOVNBGPDriver ovn_evpn_driver = ovn_bgp_agent.drivers.openstack.ovn_evpn_driver:OVNEVPNDriver ovn_stretched_l2_bgp_driver = ovn_bgp_agent.drivers.openstack.ovn_stretched_l2_bgp_driver:OVNBGPStretchedL2Driver oslo.config.opts = ovnbgpagent = ovn_bgp_agent.config:list_opts [egg_info] tag_build = tag_date = 0 ovn-bgp-agent-2.0.1/setup.py000066400000000000000000000013671460327367600157150ustar00rootroot00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # 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. # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools setuptools.setup( setup_requires=['pbr'], pbr=True) ovn-bgp-agent-2.0.1/test-requirements.txt000066400000000000000000000007551460327367600204440ustar00rootroot00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. hacking>=3.0,<3.1 # Apache-2.0 coverage>=4.0,!=4.4 # Apache-2.0 eventlet>=0.26.1 # MIT python-subunit>=0.0.18 # Apache-2.0/BSD oslotest>=1.10.0 # Apache-2.0 pyroute2>=0.6.4;sys_platform!='win32' # Apache-2.0 (+ dual licensed GPL2) stestr>=1.0.0 # Apache-2.0 testtools>=1.4.0 # MIT ovn-bgp-agent-2.0.1/tox.ini000066400000000000000000000031601460327367600155070ustar00rootroot00000000000000[tox] minversion = 3.2.0 envlist = py37,pep8 skipsdist = False ignore_basepython_conflict = true [testenv] basepython = python3 usedevelop = True setenv = VIRTUAL_ENV={envdir} PYTHONWARNINGS=default::DeprecationWarning OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2024.1} -r{toxinidir}/test-requirements.txt commands = stestr run --exclude-regex ".tests.functional" {posargs} [testenv:pep8] commands = flake8 {posargs} [testenv:venv] commands = {posargs} [testenv:functional] setenv = {[testenv]setenv} commands = stestr run --exclude-regex ".tests.unit" {posargs} [testenv:cover] setenv = VIRTUAL_ENV={envdir} PYTHON=coverage run --source ovn_bgp_agent --parallel-mode commands = stestr run --exclude-regex ".tests.functional" {posargs} coverage combine coverage html -d cover coverage xml -o cover/coverage.xml coverage report --fail-under=82 --skip-covered [testenv:docs] deps = -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -b html doc/source doc/build/html [testenv:releasenotes] deps = {[testenv:docs]deps} commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:debug] commands = oslo_debug_helper {posargs} [testenv:genconfig] commands = oslo-config-generator --config-file=etc/oslo-config-generator/bgp-agent.conf [flake8] # E123, E125 skipped as they are invalid PEP-8. show-source = True ignore = E123,E125,W504 builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build ovn-bgp-agent-2.0.1/zuul.d/000077500000000000000000000000001460327367600154155ustar00rootroot00000000000000ovn-bgp-agent-2.0.1/zuul.d/project.yaml000066400000000000000000000010161460327367600177450ustar00rootroot00000000000000- project: templates: - openstack-python3-jobs-neutron - openstack-cover-jobs - release-notes-jobs-python3 - publish-openstack-docs-pti vars: rtd_webhook_id: '224878' check: jobs: - openstack-tox-functional-with-sudo - ovn-bgp-agent-tempest-plugin: voting: false experimental: jobs: - openstack-tox-py310-with-oslo-master periodic-weekly: jobs: - openstack-tox-py310 - openstack-tox-py310-with-oslo-master ovn-bgp-agent-2.0.1/zuul.d/tempest-singlenode.yaml000066400000000000000000000006601460327367600221110ustar00rootroot00000000000000- job: name: ovn-bgp-agent-tempest-plugin parent: neutron-tempest-plugin-ovn timeout: 10800 required-projects: - openstack/devstack - openstack/ovn-bgp-agent - openstack/neutron-tempest-plugin vars: devstack_localrc: ENABLE_TLS: True devstack_plugins: ovn-bgp-agent: https://git.openstack.org/openstack/ovn-bgp-agent devstack_services: tls-proxy: true