././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8539977 networking_baremetal-7.2.0/0000775000175000017500000000000015157004110014537 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/.pre-commit-config.yaml0000664000175000017500000000316015157004031021022 0ustar00zuulzuul--- repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace - id: mixed-line-ending args: ['--fix', 'lf'] exclude: | (?x)( .*.svg$| ) - id: fix-byte-order-marker - id: check-merge-conflict - id: debug-statements - id: check-json files: .*\.json$ - id: check-yaml files: .*\.(yaml|yml)$ exclude: releasenotes/.*$ - repo: https://github.com/Lucas-C/pre-commit-hooks rev: v1.5.5 hooks: - id: remove-tabs exclude: '.*\.(svg)$' - repo: https://opendev.org/openstack/hacking rev: 8.0.0 hooks: - id: hacking additional_dependencies: [] exclude: '^(doc|releasenotes|tools)/.*$' - repo: https://github.com/codespell-project/codespell rev: v2.2.6 hooks: - id: codespell args: [--write-changes] - repo: https://github.com/sphinx-contrib/sphinx-lint rev: v1.0.0 hooks: - id: sphinx-lint args: [--enable=default-role] files: ^doc/|releasenotes|api-ref - repo: https://opendev.org/openstack/bashate rev: 2.1.1 hooks: - id: bashate args: ["-iE006,E044", "-eE005,E042"] name: bashate description: This hook runs bashate for linting shell scripts entry: bashate language: python types: [shell] - repo: https://github.com/PyCQA/doc8 rev: v2.0.0 hooks: - id: doc8 - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.6 hooks: - id: ruff args: ['--fix', '--unsafe-fixes'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/.stestr.conf0000664000175000017500000000010115157004031017002 0ustar00zuulzuul[DEFAULT] test_path=./networking_baremetal/tests/unit top_dir=./ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930567.0 networking_baremetal-7.2.0/AUTHORS0000664000175000017500000000340515157004107015617 0ustar00zuulzuul98k <18552437190@163.com> Afonne-CID Andreas Jaeger Boden R Charles Short Dmitry Tantsur Dmitry Tantsur Dongcan Ye Doug Goldstein Doug Hellmann Ghanshyam Mann Harald Jensas Harald Jensås Hervé Beraud Ian Wienand Iury Gregory Melo Ferreira Iury Gregory Melo Ferreira Ivan Anfimov Jay Faulkner Julia Kreger Kaifeng Wang Le Hou Mark Goddard OpenStack Release Bot Pavlo Shchelokovskyy Pierre Riteau Riccardo Pittau Sam Betts Sean McGinnis Sharpz7 Sven Kieske Takashi Kajinami Thomas Goirand Tuan Do Anh Vasyl Saienko Vieri <15050873171@163.com> Vladyslav Drok Vu Cong Tuan YuehuiLei aarefiev cid huang.zhiping inspurericzhang leiyashuai likui melissaml pengyuesheng wangfaxin wangjiaqi07 zhulingjie ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/CONTRIBUTING.rst0000664000175000017500000000123015157004031017176 0ustar00zuulzuulIf you would like to contribute to the development of OpenStack, you must follow the steps in this page: http://docs.openstack.org/infra/manual/developers.html If you already have a good understanding of how the system works and your OpenStack accounts are set up, you can skip to the development workflow section of this documentation to learn how changes to OpenStack should be submitted for review via the Gerrit tool: http://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. Bugs should be filed in Launchpad, not GitHub: https://bugs.launchpad.net/networking-baremetal ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930567.0 networking_baremetal-7.2.0/ChangeLog0000664000175000017500000002226015157004107016321 0ustar00zuulzuulCHANGES ======= 7.2.0 ----- * docs: l2vni - Clean up admin documentation * Fix router interface HA binding for baremetal VLAN networks * Fix incorrect is\_partial\_segment() call on PortContext * L2VNI multi-link LAG support + fix OVN LLDP filter * Add VNI to L2VNI trunk subport binding profiles * Add targeted single-VLAN reconcile for OVN events * Add OVN event-driven reconcile for L2VNI trunks * Fix L2VNI trunk port binding for switch config * Suppress misleading ERROR logs from OVN port lookups * Skip binding for ports on L2VNI anchor network * Allow hostname in l2vni\_network\_nodes.yaml config * Use other\_config for bridge-mapping lookup (OVN) * fix neutron port reconcilator * fix: correct client ovs/ovn idl object creation * Fix hash ring broken after eventlet removal * Revise localnet port text 7.1.0 ----- * vxlan: trunk-reconciler: Dial back logging * vxlan: follow-up on trunk reconciler * CI: implement vxlan job * ci: switch over to redfish based jobs * Change tenant\_network\_types to project\_network\_types * vlxan: explicitly prevent heiarchical agent triggered binding * vxlan: Fix mech looping and supported type declaration * vxlan: Fix port stickiness * vxlan: fix orphanded localnet binding * Add a reconciler to fix bug 1995078 * tox: Drop redundant injection of VIRTUAL\_ENV variable * Trunk port reconciliation for L2VNI attachments * l2vni baremetal mech driver * Remove MANIFEST.in * Add conductor group sharding support * update pre-commit config to match ironic * Swap networking-baremetal to use pyproject.toml * reno: Update master for unmaintained/2024.1 * Fix min version of Neutron * Update master for stable/2025.2 7.0.0 ----- * Add releasenote about removed eventlet * tox: Remove ineffective ignore\_basepython\_conflict * Remove Python 3.9 support * Clean up baremetal agents on node delete * Drop explicit dependency on python-subunit 6.6.0 ----- * Remove explicit use of eventlet * Use new syntax for neutron enablement * Drop redundant allowlist\_externals * Fix failing genconfig target * add pre-commit and adjust tox to utilize it * fix sphinx-lint issues * fix awkward logic that the linter didn't like * fix spelling mistakes * Update master for stable/2025.1 6.5.0 ----- * reno: Update master for unmaintained/2023.1 * prevent break on communications failure * Remove Python 3.8 support * Drop unnecessary 'x' bit from doc config file * add pyproject.toml to support pip 23.1 * Update master for stable/2024.2 * avoid attribute error on bad password or config 6.4.0 ----- * Update to match latest development cycle * Fix codespell reported errors * Remove call to enable\_python3\_package * reno: Update master for unmaintained/zed * Update master for stable/2024.1 * reno: Update master for unmaintained/xena * reno: Update master for unmaintained/wallaby * reno: Update master for unmaintained/victoria 6.3.0 ----- * don't force amqp\_auto\_delete for quorum queues * [codespell] Adding CI target for Tox Codespell * [codespell] Adding Tox Target for Codespell * [codespell] Fixing Spelling Mistakes * Bump hacking to 6.1.0 * reno: Update master for unmaintained/yoga * Remove deprecated pbr options * Do not try to bind port when we can't * Update master for stable/2023.2 6.2.0 ----- * Bugs are now in launchpad, doc fixes * Update to hacking v6 * Update master for stable/2023.1 * [CI] Explicitly disable port security 6.1.0 ----- * Fix tox4 errors * Fixes for tox 4.0 * Switch to 2023.1 Python3 unit tests and generic template name * Update master for stable/zed 6.0.0 ----- * remove unicode from code * Doc - network device configuration capabilities * Add support for pre-configured link aggregates * Add LACP support to Netconf OpenConfig driver * Add netconf-openconfig device driver * Device management driver iface * OpenConfig YANG Model, python-bindings releasenote * Add OpenConfig classes for LACP * Add OpenConfig classes for interface aggregate * Add OpenConfig classes for network-instance * Add OpenConfig classes for switch vlans * Add OpenConfig classes for iface vlan plugging * The Python 3.6 and Python 3.7 Support has been dropped since zed * Remove babel.cfg * Replace deprecated UPPER\_CONSTRAINTS\_FILE variable * Drop lower-constraints.txt and its testing * Register neutron common config options * Set agent\_type in tests * Add Python3 zed unit tests * Update master for stable/yoga 5.1.0 ----- * Re-add python 3.6/3.7 in classifier * Updating yoga tested python versions in classifier * Add Python3 yoga unit tests * Update master for stable/xena 5.0.0 ----- * Add lower-constraints job to current development branch * Increase version of hacking and pycodestyle * Update min version of tox to use allowlist * setup.cfg: Replace dashes with underscores * Add Python3 xena unit tests * Update master for stable/wallaby 4.0.0 ----- * Update minversion of tox * Add doc/requirements * Fix exception handling when querying ironic ports * Remove lower-constraints job * Fix lower-constraints with the new pip resolver * Set safe version of hacking * Add Python3 wallaby unit tests * Update master for stable/victoria 3.0.0 ----- * Fix lower-constraints for networking-baremetal * Add missing keystoneauth1 and oslo.service to requirements * Set min version of tox to 3.2.1 * drop mock from lower-constraints * Use openstacksdk for ironic connection * Remove the unused coding style modules * Switch to newer openstackdocstheme and reno versions * Convert networking-baremetal job to dib * Bump hacking version to 3.0.0 and fix pep8 test * Update lower-constraints.txt * Add unit tests for \_get\_notification\_transport\_url() * Add py38 package metadata * Add Python3 victoria unit tests * Update master for stable/ussuri * Upgrade flake8-import-order version to 0.17.1 2.0.0 ----- * Stop configuring install\_command in tox * Remove the unused oslo.i18n bits * BUILD\_TIMEOUT is not needed * Use mock from unittest * Cleanup py27 support * Explicitly set ramdisk type * Enforce running tox with correct python version based on env * Stop using six library * Fix region option name in documentation * Drop python 2.7 support and testing * Add genconfig env to tox * fixed review link * Drop py2 job * Switch jobs to python3 * Switch to Ussuri jobs * Update neutron requirement * Add versions to release notes series * Update the constraints url * Fix unit tests with ironicclient >=3.0.0 * Update master for stable/train 1.4.0 ----- * Build pdf doc * Blacklist sphinx 2.1.0 (autodoc bug) * Fix networking-baremetal CI * Fix unit tests for networking-baremetal * Bump the openstackdocstheme extension to 1.20 * Update api-ref location * Update networking-baremetal installation * Update Python 3 test runtimes for Train * Update sphinx requirements * Use opendev repository * OpenDev Migration Patch * Replace openstack.org git:// URLs with https:// * Update master for stable/stein 1.3.0 ----- * Supporting all py3 environments with tox * Zuulv3 - Use ironic-base job * Rename agent queue - fixes broken minor update * Clean up oslo.messaging listener properly * Ensure notifications are consumed from non-pool queue * Set amqp\_auto\_delete=true for notifications transport * Docs: Devstack quickstart guides - change drivers * Break out ironic client from neutron agent * Change networking-baremetal to zuulv3/python3 * Correcting a typo in plugin.sh * Change openstack-dev to openstack-discuss * Restrict bashate to devstack/lib instead of lib * Change openstack-dev to openstack-discuss * Don't quote {posargs} in tox.ini * add python 3.6 unit test job * import zuul job settings from project-config * Changing CI job templates for python3-first * Update reno for stable/rocky 1.2.0 ----- * Remove testrepository and .testr.conf * Update neutron-lib requirement for rocky * Add release notes link in README * Updating required neutron version * Switch to using stestr * fix tox python3 overrides * Remove the duplicated "the" 1.1.0 ----- * fix tox python3 overrides * add lower-constraints job * Do not run functional (API) tests in the CI * ML2 Agent: Handle SIGHUP mutable config options * Change Launchpad references to Storyboard * Updated from global requirements * Avoid tox\_install.sh * use common agent topics from neutron-lib * Update reno for stable/queens 1.0.0 ----- * Add unit tests for member manager * Make the agent distributed using hashring and notifications * Fix devstack example * Node state configuration - add log\_agent\_heartbeat * Fix nits in networking-baremetal docs * Add dsvm job * Update docs and generate config file example * Add support to bind type vlan networks * Devstack - Add ironic-neutron-agent * Use reporting\_interval option from neutron * Switch from MechanismDriver to SimpleAgentMechanismDriverBase * Updated from global requirements * start\_flag = True, only first time, or conf change * Add baremetal neutron agent * Updated from global requirements * Updated from global requirements * Updated from global requirements * Use constants from neutron-lib * Update URLs in documents according to document migration * Fix to use "." to source script files * Update reno for stable/pike 0.1.0 ----- * Add initial release note * Add installation documentation * Add devstack plugin to install networking\_baremetal * Add baremetal ML2 driver * Add .gitignore * Initial commit from cookiecutter * Added .gitreview ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/HACKING.rst0000664000175000017500000000025215157004031016336 0ustar00zuulzuulnetworking-baremetal Style Commandments =============================================== Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/LICENSE0000664000175000017500000002363715157004031015561 0ustar00zuulzuul 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. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8539977 networking_baremetal-7.2.0/PKG-INFO0000644000175000017500000000510715157004110015635 0ustar00zuulzuulMetadata-Version: 2.4 Name: networking-baremetal Version: 7.2.0 Summary: Neutron plugin that provides deep Ironic/Neutron integration. Author-email: OpenStack License: Apache-2.0 Project-URL: Homepage, https://docs.openstack.org/networking-baremetal/latest/ 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 :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: ncclient>=0.6.9 Requires-Dist: neutron-lib>=1.28.0 Requires-Dist: oslo.config>=9.7.1 Requires-Dist: oslo.i18n>=6.5.1 Requires-Dist: oslo.log>=7.1.0 Requires-Dist: oslo.utils>=8.2.0 Requires-Dist: oslo.messaging>=16.1.0 Requires-Dist: oslo.service[threading]>=4.2.0 Requires-Dist: pbr>=6.0.0 Requires-Dist: openstacksdk>=4.9.0 Requires-Dist: tooz>=6.3.0 Requires-Dist: neutron>=27.0.0.0rc1 Requires-Dist: tenacity>=6.0.0 Requires-Dist: keystoneauth1>=3.14.0 Dynamic: license-file Dynamic: requires-dist networking-baremetal plugin --------------------------- This project's goal is to provide deep integration between the Networking service and the Bare Metal service and advanced networking features like notifications of port status changes and routed networks support in clouds with Bare Metal service. Features -------- * **L2VNI Mechanism Driver**: Enables baremetal servers to connect to VXLAN and Geneve overlay networks by dynamically allocating VLAN segments and creating OVN localnet ports. See documentation for configuration details. * **Port Status Notifications**: Real-time notifications of port status changes from the Bare Metal service to the Networking service. * **Multi-tenant Network Support**: Advanced networking features for baremetal deployments with tenant isolation. * Free software: Apache license * Documentation: http://docs.openstack.org/networking-baremetal/latest * Source: http://opendev.org/openstack/networking-baremetal * Bugs: https://bugs.launchpad.net/networking-baremetal * Release notes: https://docs.openstack.org/releasenotes/networking-baremetal/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/README.rst0000664000175000017500000000211615157004031016230 0ustar00zuulzuulnetworking-baremetal plugin --------------------------- This project's goal is to provide deep integration between the Networking service and the Bare Metal service and advanced networking features like notifications of port status changes and routed networks support in clouds with Bare Metal service. Features -------- * **L2VNI Mechanism Driver**: Enables baremetal servers to connect to VXLAN and Geneve overlay networks by dynamically allocating VLAN segments and creating OVN localnet ports. See documentation for configuration details. * **Port Status Notifications**: Real-time notifications of port status changes from the Bare Metal service to the Networking service. * **Multi-tenant Network Support**: Advanced networking features for baremetal deployments with tenant isolation. * Free software: Apache license * Documentation: http://docs.openstack.org/networking-baremetal/latest * Source: http://opendev.org/openstack/networking-baremetal * Bugs: https://bugs.launchpad.net/networking-baremetal * Release notes: https://docs.openstack.org/releasenotes/networking-baremetal/ ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1773930567.831994 networking_baremetal-7.2.0/devstack/0000775000175000017500000000000015157004110016343 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1773930567.831994 networking_baremetal-7.2.0/devstack/lib/0000775000175000017500000000000015157004110017111 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/devstack/lib/networking-baremetal0000664000175000017500000000372415157004031023165 0ustar00zuulzuul#!/bin/bash # # lib/networking-baremetal # # Functions to control the configuration and operation of the **Networking Baremetal** # Dependencies: # (none) # Save trace setting _XTRACE_NETWORKING_BAREMETAL=$(set +o | grep xtrace) set +o xtrace # Defaults # -------- # networking-baremetal service NETWORKING_BAREMETAL_REPO=${NETWORKING_BAREMETAL_REPO:-${GIT_BASE}/openstack/networking-baremetal.git} NETWORKING_BAREMETAL_BRANCH=${NETWORKING_BAREMETAL_BRANCH:-master} NETWORKING_BAREMETAL_DIR=${NETWORKING_BAREMETAL_DIR:-$DEST/networking-baremetal} NETWORKING_BAREMETAL_DATA_DIR=""$DATA_DIR/networking-baremetal"" # Support entry points installation of console scripts NETWORKING_BAREMETAL_BIN_DIR=$(get_python_exec_prefix) # Functions # --------- function install_networking_baremetal { setup_develop $NETWORKING_BAREMETAL_DIR } function configure_networking_baremetal { if [[ -z "$Q_ML2_PLUGIN_MECHANISM_DRIVERS" ]]; then Q_ML2_PLUGIN_MECHANISM_DRIVERS='baremetal-l2vni,baremetal' else if [[ ! $Q_ML2_PLUGIN_MECHANISM_DRIVERS =~ $(echo '\') ]]; then Q_ML2_PLUGIN_MECHANISM_DRIVERS+=',baremetal-l2vni,baremetal' fi fi populate_ml2_config /$Q_PLUGIN_CONF_FILE ml2 mechanism_drivers=$Q_ML2_PLUGIN_MECHANISM_DRIVERS # Set a handy default physical network parameter, so we don't have to # update each port in CI. populate_ml2_config /$Q_PLUGIN_CONF_FILE baremetal_l2vni default_physical_network=${PHYSICAL_NETWORK:-mynetwork} } function configure_networking_baremetal_neutron_agent { configure_keystone_authtoken_middleware $NEUTRON_CONF ironic ironic configure_placement_nova_compute $NEUTRON_CONF } function start_networking_baremetal_neutron_agent { run_process ir-neutronagt "$NETWORKING_BAREMETAL_BIN_DIR/ironic-neutron-agent" } function stop_networking_baremetal_neutron_agent { stop_process ir-neutronagt } function cleanup_networking_baremetal { rm -rf $NETWORKING_BAREMETAL_DATA_DIR } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/devstack/plugin.sh0000664000175000017500000000233015157004031020175 0ustar00zuulzuul#!/usr/bin/env bash # plugin.sh - DevStack plugin.sh dispatch script template echo_summary "networking-baremetal devstack plugin.sh called: $1/$2" source $DEST/networking-baremetal/devstack/lib/networking-baremetal # check for service enabled if is_service_enabled networking_baremetal; then if [[ "$1" == "stack" && "$2" == "install" ]]; then # Perform installation of service source echo_summary "Installing Networking Baremetal ML2" install_networking_baremetal elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then # Configure after the other layer 1 and 2 services have been configured echo_summary "Configuring Networking Baremetal Ml2" configure_networking_baremetal echo_summary "Configuring Networking Baremetal Neutron Agent" configure_networking_baremetal_neutron_agent echo_summary "Starting Networking Baremetal Neutron Agent" start_networking_baremetal_neutron_agent fi if [[ "$1" == "unstack" ]]; then echo_summary "Cleaning Networking Baremetal Ml2" cleanup_networking_baremetal echo_summary "Cleaning Networking Baremtal Neutron Agent" stop_networking_baremetal_neutron_agent fi fi ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/devstack/settings0000664000175000017500000000033315157004031020127 0ustar00zuulzuul# settings file for networking_baremetal define_plugin networking-baremetal plugin_requires networking-baremetal ironic plugin_requires networking-baremetal networking-generic-switch enable_service networking_baremetal ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1773930567.831994 networking_baremetal-7.2.0/doc/0000775000175000017500000000000015157004110015304 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/requirements.txt0000664000175000017500000000017715157004031020577 0ustar00zuulzuulreno>=3.1.0 # Apache-2.0 sphinx>=2.0.0,!=2.1.0 # BSD sphinxcontrib-apidoc>=0.2.0 # BSD openstackdocstheme>=2.2.1 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1773930567.831994 networking_baremetal-7.2.0/doc/source/0000775000175000017500000000000015157004110016604 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8329942 networking_baremetal-7.2.0/doc/source/admin/0000775000175000017500000000000015157004110017674 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/admin/ha-chassis-group-alignment.rst0000664000175000017500000001715515157004031025572 0ustar00zuulzuul=================================== HA Chassis Group Alignment =================================== Overview ======== The HA Chassis Group Alignment feature addresses connectivity issues that can occur in OVN deployments with baremetal nodes when router gateway ports and baremetal external ports have mismatched HA chassis group priorities. This feature implements automatic reconciliation to ensure router ports use the same HA chassis group configuration as baremetal external ports on the same network, eliminating intermittent connectivity failures. Problem Statement ================= In OpenStack deployments using OVN with baremetal nodes, external connectivity can fail intermittently due to a configuration mismatch. This occurs when: 1. A baremetal node has an external port (``device_owner=baremetal:none``) on a provider network 2. A router is attached to the same network via a router interface port 3. OVN assigns different HA chassis groups with different priorities to the baremetal external port and the router gateway port 4. The active chassis for the baremetal port differs from the active chassis for the router port When this mismatch occurs, traffic routing becomes inconsistent, causing baremetal nodes to lose external connectivity intermittently as different chassis believe they are the active gateway. This issue is tracked in Launchpad as bug #1995078: https://bugs.launchpad.net/neutron/+bug/1995078 Solution ======== The ironic-neutron-agent now includes a periodic reconciliation loop that: 1. Discovers all networks with baremetal external ports 2. Identifies the HA chassis group used by baremetal ports on each network 3. Finds router interface ports on the same networks 4. Updates router ports to use the same HA chassis group as baremetal ports 5. Only processes networks managed by the agent instance (via hash ring) This ensures consistent HA chassis group configuration across all ports on networks with baremetal nodes, preventing the priority mismatch that causes connectivity failures. Configuration ============= The feature is controlled by options in the ``[baremetal_agent]`` section of the agent configuration file (typically ``/etc/neutron/ironic_neutron_agent.ini``). Enable/Disable -------------- .. code-block:: ini [baremetal_agent] # Enable HA chassis group alignment reconciliation # Default: True enable_ha_chassis_group_alignment = True Set to ``False`` to disable the feature if you are not experiencing the connectivity issue or if you have resolved it through other means. Reconciliation Interval ----------------------- .. code-block:: ini [baremetal_agent] # Interval in seconds between alignment reconciliation runs # Default: 600 (10 minutes) # Minimum: 60 ha_chassis_group_alignment_interval = 600 Controls how frequently the agent checks for and fixes HA chassis group mismatches. The default of 10 minutes provides a balance between: - Timely detection and correction of mismatches - Minimal impact on Neutron API and OVN database load For deployments with frequent network topology changes, you may want to reduce this interval. For stable deployments, you can increase it to reduce overhead. Time Window Filtering --------------------- .. code-block:: ini [baremetal_agent] # Only check recently created/updated resources # Default: True limit_ha_chassis_group_alignment_to_recent_changes_only = True # Time window in seconds for "recent" resources # Default: 1200 (20 minutes, 2x the alignment interval) # Minimum: 0 ha_chassis_group_alignment_window = 1200 When enabled, reconciliation only examines ports that have been created or updated within the specified time window. This significantly reduces API and database load in large deployments by focusing on resources most likely to have mismatches (newly created ports). **When to disable:** Set ``limit_ha_chassis_group_alignment_to_recent_changes_only = False`` if you: - Want to perform full reconciliation on every run - Are recovering from a period where the agent was disabled - Suspect existing ports have mismatches that need correction Operational Considerations ========================== Multi-Agent Deployments ----------------------- In deployments with multiple ironic-neutron-agent instances: - Each agent uses a distributed hash ring to determine which networks it manages - Only the responsible agent will reconcile a given network - This prevents duplicate work and API contention - If an agent fails, other agents will automatically take over its networks Monitoring ---------- The agent logs alignment activities at the INFO level: .. code-block:: text INFO ... Started HA chassis group alignment reconciliation loop (interval: 600s, first run in 42s) INFO ... Updating router port HA chassis group from to (network ) INFO ... Successfully updated router port HA chassis group Failed updates are logged at ERROR level with full exception details. Performance Impact ------------------ The reconciliation loop has minimal performance impact: - **Default configuration:** Queries Neutron for baremetal ports every 10 minutes - **With windowing enabled (default):** Only checks recently updated ports - **Uses existing OVN connections:** Reuses connections from L2VNI trunk manager if available - **Distributed load:** Multiple agents split work via hash ring In a deployment with 1000 baremetal nodes and default settings: - First Neutron query returns ~1000 ports - With 20-minute window, ~50 ports processed per reconciliation (assuming 5% churn rate) - Per-network processing: 1-2 additional Neutron queries, 2-3 OVN queries - Total: ~100-150 API calls every 10 minutes across all agents Troubleshooting =============== Verifying the Feature is Running --------------------------------- Check agent logs for startup message: .. code-block:: console $ grep "HA chassis group alignment" /var/log/neutron/ironic-neutron-agent.log INFO ... HA chassis group alignment reconciliation enabled INFO ... Started HA chassis group alignment reconciliation loop (interval: 600s, first run in 42s) Checking for Mismatches ----------------------- If you suspect an alignment issue: 1. Identify the affected network and baremetal ports 2. Check OVN for the HA chassis group on baremetal ports: .. code-block:: console $ ovn-nbctl lsp-get-ha-chassis-group 3. Check router ports on the same network: .. code-block:: console $ ovn-nbctl lrp-get-ha-chassis-group lrp- 4. If different, the next reconciliation cycle will align them (check logs) Forcing Immediate Reconciliation --------------------------------- To trigger reconciliation without waiting for the interval: 1. Restart the ironic-neutron-agent 2. The first reconciliation runs within 60 seconds (with random jitter) Alternatively, temporarily reduce the interval: .. code-block:: console $ openstack-config --set /etc/neutron/ironic_neutron_agent.ini \ baremetal_agent ha_chassis_group_alignment_interval 60 $ systemctl restart ironic-neutron-agent Related Features ================ This feature complements the L2VNI trunk reconciliation feature: - **L2VNI reconciliation:** Manages VLAN trunk configurations for network nodes - **HA alignment:** Ensures consistent HA chassis group configuration Both features can be enabled independently and run on separate schedules. References ========== - Launchpad Bug #1995078: https://bugs.launchpad.net/neutron/+bug/1995078 - OVN HA Chassis Groups: https://www.ovn.org/support/dist-docs/ovn-nb.5.html - ironic-neutron-agent: https://docs.openstack.org/networking-baremetal/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/admin/index.rst0000664000175000017500000000045215157004031021540 0ustar00zuulzuul==================== Administrator Guide ==================== This guide provides information for deploying, configuring, and operating networking-baremetal in production environments. .. toctree:: :maxdepth: 2 ha-chassis-group-alignment router-ha-binding l2vni-trunk-reconciliation ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/admin/l2vni-trunk-reconciliation.rst0000664000175000017500000013464215157004031025635 0ustar00zuulzuul========================================== L2VNI Trunk Reconciliation Configuration ========================================== Overview ======== The L2VNI trunk reconciliation feature enables automatic management of trunk ports on network nodes to bridge OVN overlay networks to physical network infrastructure. This feature is essential for deployments where baremetal nodes need to connect to overlay networks through network nodes acting as gateways. What Problem Does This Solve? ------------------------------ In deployments using the L2VNI mechanism driver, baremetal nodes connect to VXLAN/Geneve overlay networks via VLAN segments on physical networks. For this to work, network nodes must: 1. Have trunk ports configured with the correct VLAN subports 2. Keep those VLANs synchronized with active overlay networks 3. Clean up VLANs when overlay networks are removed Manual management of these trunk ports becomes impractical at scale, especially when: - Multiple network nodes form ha_chassis_groups for high availability - Overlay networks are frequently created and deleted - Router migrations cause chassis membership changes - Network nodes are added or removed from the infrastructure The trunk reconciliation feature automates this entire process using two complementary mechanisms that can be used independently or together: 1. **OVN Event-Driven Reconciliation**: Watches OVN database for localnet port changes and triggers immediate reconciliation 2. **Periodic Reconciliation**: Runs at regular intervals as a safety net to ensure eventual consistency **Recommended for production**: Enable both mechanisms. Event-driven provides immediate response to changes, while periodic reconciliation ensures eventual consistency even if events are missed or the agent is restarted. Architecture ============ Reconciliation Mechanisms -------------------------- The agent uses two reconciliation mechanisms that can be used independently or together. **1. Event-Driven L2VNI Trunk Reconciliation (Default: Enabled)** When enabled (``enable_l2vni_trunk_reconciliation_events = True``), the agent watches OVN Northbound database for localnet port creation and deletion events. This provides immediate reconciliation when the L2VNI mechanism driver creates or deletes localnet ports during baremetal port binding operations. This eliminates stale IDL cache issues and enables targeted updates for specific VLANs without scanning all trunk ports. **How It Works:** 1. L2VNI mechanism driver creates localnet port in OVN Northbound DB 2. OVN sends notification to all connected IDL clients 3. Agent's IDL processes the notification via ``idl.run()`` 4. ``LocalnetPortEvent.matches()`` filters for L2VNI localnet ports and hash ring ownership 5. ``LocalnetPortEvent.run()`` extracts network ID, physnet, and VLAN ID from the event 6. Agent performs **targeted reconciliation** for that specific VLAN: - Ensures infrastructure networks exist - Queries overlay segment information (VNI) for the network - Finds/creates trunks for chassis with the physnet - Adds or removes only the specific VLAN subport with VNI in binding profile (idempotent) - Skips scanning all other VLANs on the trunk This targeted approach is significantly faster than full reconciliation, especially on trunks with many VLANs. The event handler is registered at agent startup and watches continuously for localnet port changes. **2. Periodic Reconciliation (Default: Enabled)** Periodic reconciliation runs at the configured interval (``l2vni_reconciliation_interval``) as a safety net to catch any missed events, handle agent restarts, and ensure eventual consistency. **Operating Modes** The agent supports three operating modes: 1. **Both Enabled (Recommended for Production)** .. code-block:: ini [l2vni] enable_l2vni_trunk_reconciliation = True enable_l2vni_trunk_reconciliation_events = True - Event-driven reconciliation provides immediate response - Periodic reconciliation ensures eventual consistency - Best reliability and performance 2. **Event-Driven Only (Testing/Advanced)** .. code-block:: ini [l2vni] enable_l2vni_trunk_reconciliation = False enable_l2vni_trunk_reconciliation_events = True - Only event-driven reconciliation runs - No periodic safety net - Useful for testing event-driven behavior in isolation - Lower overhead but no eventual consistency guarantee 3. **Periodic Only** .. code-block:: ini [l2vni] enable_l2vni_trunk_reconciliation = True enable_l2vni_trunk_reconciliation_events = False - Only periodic reconciliation runs - Reconciliation occurs at configured interval How It Works ------------ The ironic-neutron-agent runs a reconciliation process that: 1. **Discovers Network Nodes**: Identifies chassis that are members of OVN ha_chassis_groups by querying the OVN Northbound database. 2. **Creates Trunk Infrastructure**: For each (chassis, physical_network) combination, ensures: - An anchor port exists on the ha_chassis_group network - A trunk port is created using that anchor port - The trunk is properly named for tracking 3. **Calculates Required VLANs**: Analyzes OVN state to determine which VLANs are needed on each trunk: - Queries logical router ports for gateway chassis assignments - Identifies overlay networks attached to those routers - Finds the dynamic VLAN segment allocated for each overlay network - Retrieves overlay segment information (VNI) for L2VNI mapping 4. **Reconciles Subports**: Ensures each trunk has exactly the right set of VLAN subports: - Adds missing subports for new overlay networks with VNI in binding profile - Removes subports for deleted overlay networks - Updates subport binding profiles with switch connection information 5. **Cleans Up Orphans**: Removes infrastructure for deleted network nodes: - Deletes trunks for chassis no longer in ha_chassis_groups - Removes anchor ports and subports - Cleans up networks for deleted ha_chassis_groups Components and Data Flow ------------------------- .. code-block:: text ┌─────────────────────────────────────────────────────────┐ │ OVN Northbound DB │ │ - HA Chassis Groups │ │ - Logical Router Ports │ │ - Gateway Chassis Assignments │ └────────────────┬────────────────────────────────────────┘ │ │ Queries │ ┌────────────────▼────────────────────────────────────────┐ │ ironic-neutron-agent │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ L2VNI Trunk Manager │ │ │ │ │ │ │ │ 1. Discover chassis in ha_groups │ │ │ │ 2. Calculate required VLANs │ │ │ │ 3. Reconcile trunk subports │ │ │ │ 4. Cleanup orphaned resources │ │ │ └────┬────────────────────────────────────┘ │ │ │ │ └───────┼─────────────────────────────────────────────────┘ │ │ Neutron API calls │ ┌───────▼───────────────────────────────────────────────────┐ │ Neutron Server │ │ - Creates/deletes trunk ports │ │ - Manages subports │ │ - Coordinates with ML2 plugin │ └────────────────┬──────────────────────────────────────────┘ │ │ ML2 mechanism drivers │ ┌────────────────▼───────────────────────────────────────────┐ │ Physical Switch Plugins │ │ (e.g., genericswitch) │ │ - Configures trunk ports on physical switches │ │ - Maps VLANs to network node ports │ └────────────────────────────────────────────────────────────┘ Infrastructure Objects ---------------------- **HA Chassis Group Networks** For each ha_chassis_group in OVN, the reconciliation process creates a Neutron network named ``l2vni-ha-group-{group_uuid}``. These networks are used to host anchor ports and provide network context for trunk ports. **Anchor Ports** Each (chassis, physical_network) combination gets an anchor port named ``l2vni-anchor-{system_id}-{physnet}``. The anchor port: - Is created on the ha_chassis_group network - Has device_owner ``baremetal:l2vni_anchor`` - Contains binding profile with system_id, physical_network, and local_link_information - Serves as the parent port for the trunk - Does not have binding:host_id set (not required for switch configuration) The local_link_information in the anchor port's binding profile is used by networking-generic-switch to configure the physical switch port when subports are added to the trunk. Note that local_link_information is a list containing one or more link connection dictionaries: - **Single link**: ``[{switch_id: ..., port_id: ..., switch_info: ...}]`` - **Multiple links (LAG/bonding)**: ``[{...}, {...}, ...]`` When multiple links are configured (from OVN LLDP, Ironic, or YAML config), they are all aggregated into this list, enabling ML2 mechanism drivers to configure link aggregation groups on the physical switch. **Trunk Ports** Trunks are named ``l2vni-trunk-{system_id}-{physnet}`` and use the anchor port as their parent. The trunk carries multiple VLAN-tagged subports. **Subports** Each subport represents one overlay network's VLAN segment: - Named ``l2vni-subport-{system_id}-{physnet}-vlan{vlan_id}`` - Created on the subport anchor network (``l2vni-subport-anchor`` by default) - Has device_owner ``baremetal:l2vni_subport`` - Has binding:host_id set to the chassis hostname for proper ML2 binding - Segmentation type is always ``vlan`` - Contains ``binding:profile`` with: - ``physical_network``: The physical network name - ``vni``: The overlay segment ID (VXLAN/Geneve VNI) for L2VNI mapping The VNI information in the binding profile enables ML2 mechanism drivers to configure complete VLAN-to-VNI mappings on physical switches. This is essential for proper EVPN/VXLAN bridging between overlay networks and physical VLANs. Note: ML2 mechanism drivers use the parent port's (anchor port's) local_link_information when configuring the physical switch, so subports do not need local_link_information in their binding profile. **Subport Anchor Network** A shared network (default name: ``l2vni-subport-anchor``) hosts all subports across all trunks. This network is used to signal VLAN bindings to ML2 switch plugins and does not pass actual traffic. Switch Connection Information ------------------------------ The reconciliation process attempts to discover switch connection information for each trunk from multiple sources, in priority order: 1. **OVN LLDP Data**: Extracted from OVN Southbound Port table external_ids (``lldp_chassis_id``, ``lldp_port_id``, ``lldp_system_name``). If multiple ports on the chassis have LLDP data for the same bridge, all links are aggregated for LAG/bonding support. 2. **Ironic Node Data**: Retrieved from Ironic port local_link_information for nodes matching the chassis system-id. **Cached per system_id** with configurable TTL to reduce API load. If multiple Ironic ports have the same physical_network, all links are aggregated for LAG/bonding support. Queries use field filtering (only fetching uuid, properties, physical_network, and local_link_information) for optimal performance. Optional filtering by conductor_group and shard reduces query scope in large deployments. 3. **YAML Configuration File**: Fallback configuration from ``l2vni_network_nodes_config`` file. Supports both single-link and multi-link (LAG/bonding) configurations. **Performance Note:** Ironic data is cached per system_id with individual expiration times (TTL + jitter). In a deployment with 1000 nodes, this reduces API calls from ~16,000 per reconciliation to ~2 (when cached), while still refreshing stale data automatically. This information is included in subport binding profiles to enable switch management plugins (e.g., genericswitch) to configure the physical switch ports correctly. .. warning:: Where the cache may be problematic is if you are re-cabling networker nodes on a fairly regular basis. While fundimentally such an action *is* a breaking change in itself in any operating environment, the cache will retain details for up to an hour and may also reflect incorrect details if the data sources (Ironic, or the YAML configuration) are not updated. Neutron guidance around changing configuration in such cases is also to change the agents which will reset the cache. Prerequisites ============= Required Components ------------------- - **OpenStack Neutron** with ML2 plugin - **OVN Backend** (Open Virtual Network) - required for ha_chassis_groups - **L2VNI Mechanism Driver** - must be enabled and configured - **Physical Network Switches** - configured for VLAN trunking - **Switch Management Plugin** (e.g., genericswitch) - for VNI↔VLAN mapping - **Network Nodes** - chassis that are members of OVN ha_chassis_groups Network Architecture Requirements ---------------------------------- Your deployment must use: - OVN as the Neutron backend (ML2/OVN) - HA chassis groups for gateway routers - Network nodes with bridge-mappings to physical networks - VLAN-capable physical switches connecting network nodes Service Dependencies -------------------- The ironic-neutron-agent requires: - Connectivity to Neutron API - Connectivity to OVN Northbound database - Connectivity to OVN Southbound database - (Optional) Connectivity to Ironic API for enhanced switch information Configuration ============= Enabling Trunk Reconciliation ------------------------------ Edit ``/etc/neutron/neutron.conf`` (or a separate config file in ``/etc/neutron/neutron.conf.d/``) and add the ``[l2vni]`` section: .. code-block:: ini [l2vni] # Enable L2VNI trunk reconciliation enable_l2vni_trunk_reconciliation = True # Enable event-driven L2VNI trunk reconciliation enable_l2vni_trunk_reconciliation_events = True # Baseline reconciliation interval (seconds) l2vni_reconciliation_interval = 300 # Network node configuration file (fallback for switch info) l2vni_network_nodes_config = /etc/neutron/l2vni_network_nodes.yaml # Auto-create infrastructure networks l2vni_auto_create_networks = True # Subport anchor network name l2vni_subport_anchor_network = l2vni-subport-anchor # Network type for infrastructure networks (geneve or vxlan) l2vni_subport_anchor_network_type = geneve # Startup jitter to prevent thundering herd (seconds) l2vni_startup_jitter_max = 60 # Ironic caching (if using Ironic for switch info) ironic_cache_ttl = 3600 Configuration Options Reference -------------------------------- ``enable_l2vni_trunk_reconciliation`` **Type**: Boolean **Default**: ``True`` **Description**: Enable periodic L2VNI trunk port reconciliation. When enabled, the agent runs reconciliation at regular intervals (``l2vni_reconciliation_interval``) to ensure eventual consistency. This can be used independently or together with event-driven reconciliation. For production deployments, keeping this enabled is recommended as a safety net to catch missed events and handle agent restarts. Set this to ``False`` to disable periodic reconciliation. Event-driven reconciliation (if enabled) will still work, but there will be no periodic safety net. ``enable_l2vni_trunk_reconciliation_events`` **Type**: Boolean **Default**: ``True`` **Description**: Enable OVN RowEvent-based event-driven L2VNI trunk reconciliation. When enabled, the agent watches OVN Northbound database for localnet port creation and deletion events and triggers immediate reconciliation. This eliminates the stale IDL cache issue and provides fast reconciliation response. This can be used independently or together with periodic reconciliation. For production deployments, using both event-driven and periodic reconciliation is recommended. Event-driven provides immediate response, while periodic ensures eventual consistency. Requires OVN IDL connection to be available. If the OVN IDL does not support event handlers, the agent will log a warning and fall back to periodic reconciliation only. Set this to ``False`` to disable event-driven reconciliation. Periodic reconciliation (if enabled) will still work. ``l2vni_reconciliation_interval`` **Type**: Integer (seconds) **Default**: ``300`` (5 minutes) **Minimum**: ``30`` **Description**: Baseline interval between reconciliation runs. This is the steady-state reconciliation frequency. **Tuning guidance:** - Smaller values (60-120s) provide faster convergence but increase API load - Larger values (300-600s) reduce overhead but slow convergence - For small deployments: 60-120 seconds - For large deployments (100+ network nodes): 300-600 seconds ``l2vni_network_nodes_config`` **Type**: String (file path) **Default**: ``/etc/neutron/l2vni_network_nodes.yaml`` **Description**: Path to YAML configuration file containing network node trunk port configuration. This file serves as a fallback source for local_link_information when LLDP data is not available from OVN and Ironic. See `Network Node Configuration File`_ for file format details. ``l2vni_auto_create_networks`` **Type**: Boolean **Default**: ``True`` **Description**: Automatically create Neutron networks for ha_chassis_groups and the subport anchor network if they do not exist. **When True (recommended):** - Networks are created automatically on first reconciliation - Simplifies deployment and upgrades - Networks are cleaned up when no longer needed **When False:** - Networks must be pre-created manually - Network names must match expected patterns exactly - Useful for environments with strict network creation policies ``l2vni_subport_anchor_network`` **Type**: String **Default**: ``l2vni-subport-anchor`` **Description**: Name of the shared network used for all trunk subports across all network nodes. This network is used to signal VLAN bindings to ML2 switch plugins and does not pass actual traffic. All subports are created on this network regardless of which ha_chassis_group or overlay network they represent. .. note:: If you change this value, ensure the network exists or set ``l2vni_auto_create_networks = True``. ``l2vni_subport_anchor_network_type`` **Type**: String (enum) **Default**: ``geneve`` **Choices**: ``geneve``, ``vxlan`` **Description**: Network type to use for L2VNI infrastructure networks (both subport anchor and ha_chassis_group networks). These networks are used for metadata and modeling only, not for passing traffic. Must match the overlay network type configured in your environment (ml2_type_drivers). If the specified type is not available, network creation will fail with an explicit error rather than falling back to an alternative type. .. warning:: Ensure the selected network type is enabled in Neutron's ml2_type_drivers configuration before enabling trunk reconciliation. ``ironic_cache_ttl`` **Type**: Integer (seconds) **Default**: ``3600`` (1 hour) **Minimum**: ``300`` (5 minutes) **Description**: Time-to-live for cached Ironic node and port data. Each system_id entry is cached independently and expires after this duration from when it was fetched. A small amount of jitter (10-20%) is automatically added to spread cache refresh times across multiple agents, avoiding thundering herd issues when cache entries expire. **Tuning guidance:** - Smaller values (300-900s): More frequent Ironic API calls, faster detection of changes to node/port configurations - Larger values (3600-7200s): Reduced API load, suitable for stable deployments where node configurations rarely change - For deployments with frequent node changes: 300-600 seconds - For stable deployments (1000+ nodes): 3600-7200 seconds **Performance impact:** In deployments with 1000 nodes × 8 ports each, efficient caching reduces per-reconciliation API calls from ~16,000 to ~2 (when cached). ``ironic_conductor_group`` **Type**: String **Default**: ``None`` (no filtering) **Description**: Ironic conductor group to filter nodes when querying for local_link_information data. This allows the agent to only query nodes managed by a specific conductor group, significantly reducing API load in large deployments with conductor group partitioning. If not specified, all nodes are queried (subject to shard filtering). **Example use case:** In a deployment with separate conductor groups for different availability zones, set this to match the zone where trunk reconciliation is needed. ``ironic_shard`` **Type**: String **Default**: ``None`` (no filtering) **Description**: Ironic shard to filter nodes when querying for local_link_information data. This allows the agent to only query nodes in a specific shard, significantly reducing API load in large sharded deployments. If not specified, all nodes are queried (subject to conductor group filtering). **Example use case:** In a deployment sharded by region (shard-us-west, shard-us-east), set this to match the region where trunk reconciliation is needed. ``l2vni_startup_jitter_max`` **Type**: Integer (seconds) **Default**: ``60`` **Minimum**: ``0`` **Description**: Maximum random delay added to initial reconciliation start time after agent startup. This prevents thundering herd issues when multiple agents restart simultaneously (e.g., after a rolling upgrade). Each agent will start reconciliation within 0 to ``l2vni_startup_jitter_max`` seconds of startup, spreading the initial load. **Recommended values:** - Single agent: ``0`` (no jitter needed) - 2-5 agents: ``30-60`` seconds - 6+ agents: ``60-120`` seconds Network Node Configuration File ================================ The network node configuration file provides fallback local_link_information when LLDP and Ironic data are not available. File Format ----------- The file is in YAML format at the path specified by ``l2vni_network_nodes_config``: .. code-block:: yaml network_nodes: # Using system_id (explicit, no hostname lookup needed) - system_id: 0f563ca5-4a94-4d26-a21e-a4ce3dbcd372 trunks: - physical_network: physnet1 local_link_information: - switch_id: "00:11:22:33:44:55" port_id: "Ethernet1/1" switch_info: "tor-switch-1" - physical_network: physnet2 local_link_information: - switch_id: "aa:bb:cc:dd:ee:ff" port_id: "GigabitEthernet1/0/1" switch_info: "tor-switch-2" # Using hostname with LAG/bonding (multiple links) - hostname: network-node-2.example.com trunks: - physical_network: physnet1 local_link_information: - switch_id: "22:57:f8:dd:03:01" port_id: "Ethernet1/3" switch_info: "leaf01.netlab.example.com" - switch_id: "22:57:f8:dd:03:01" port_id: "Ethernet1/5" switch_info: "leaf01.netlab.example.com" Field Descriptions ------------------ **network_nodes** (required) List of network node configurations **system_id** or **hostname** (one required) Network nodes can be identified by either: - **system_id**: The OVN chassis UUID (chassis.name in OVN Southbound). Explicit and requires no lookup. Takes priority if both are specified. - **hostname**: The OVN chassis hostname (chassis.hostname in OVN Southbound). Human-readable and survives chassis reinstalls, but requires an OVN SB lookup to resolve to the chassis UUID. You can find both values with: .. code-block:: bash ovn-sbctl --columns=name,hostname list Chassis Choose based on your needs: ``system_id`` for explicitness and performance, ``hostname`` for maintainability and readability. **trunks** (required) List of trunk configurations for this network node **physical_network** (required) The physical network name (must match ``network_vlan_ranges`` and ``ovn-bridge-mappings`` configuration) **local_link_information** (required) List of switch connection information dictionaries. Each entry contains: - **switch_id**: Switch MAC address or identifier - **port_id**: Switch port name/identifier - **switch_info**: Optional switch hostname or description Specify a single-item list for single links, or multiple items for LAG/bonding configurations. All links are aggregated into the anchor port's ``binding:profile['local_link_information']`` list and passed to switch management plugins to enable automatic switch port configuration. Multi-Agent Deployment ======================= Hash Ring Distribution ----------------------- When multiple ironic-neutron-agents are deployed, they use a hash ring to distribute work. Each chassis is hashed to a specific agent, and only that agent manages trunks for that chassis. **Benefits:** - Load distribution across agents - Reduced API call volume - Parallel processing of reconciliation tasks **How It Works:** 1. Agents register with Tooz coordinator (typically backed by etcd or Redis) 2. A consistent hash ring is built from agent memberships 3. Each chassis system-id is hashed to determine the owning agent 4. During reconciliation, agents skip chassis not in their hash ring segment Example: 3 agents managing 10 chassis: - Agent A manages: chassis-1, chassis-4, chassis-7, chassis-10 - Agent B manages: chassis-2, chassis-5, chassis-8 - Agent C manages: chassis-3, chassis-6, chassis-9 Cleanup Considerations ---------------------- **Important**: Cleanup operations do **not** use hash ring filtering. When orphaned trunks are detected (chassis removed from ha_chassis_group or deleted entirely), all agents will attempt cleanup. This is intentional: **Scenario**: Agent A managed chassis-5, then Agent A crashes. Chassis-5 is deleted from OVN. Agents B and C both detect the orphaned trunk and attempt cleanup. The first agent to run cleanup succeeds; the other gets a "not found" error (harmless). This approach provides: - **Resilience**: Cleanup happens even if the original managing agent is down - **Simplicity**: No need to track previous ownership - **Correctness**: No orphaned resources due to agent failures The cost is minimal: redundant API calls that return 404, logged as warnings. High Availability ----------------- For production deployments, run at least 3 ironic-neutron-agents: .. code-block:: bash # On controller-1 systemctl start ironic-neutron-agent # On controller-2 systemctl start ironic-neutron-agent # On controller-3 systemctl start ironic-neutron-agent If an agent fails: 1. Tooz coordinator detects the failure 2. Hash ring is recalculated 3. Remaining agents automatically take over the failed agent's chassis 4. Reconciliation continues without interruption Deployment Guide ================ Step 1: Enable the L2VNI Mechanism Driver ------------------------------------------ Ensure the L2VNI mechanism driver is enabled and configured. See :doc:`/configuration/ml2/l2vni-mechanism-driver` for details. Step 2: Configure Trunk Reconciliation --------------------------------------- Edit ``/etc/neutron/neutron.conf``: .. code-block:: ini [l2vni] enable_l2vni_trunk_reconciliation = True enable_l2vni_trunk_reconciliation_events = True l2vni_reconciliation_interval = 300 l2vni_auto_create_networks = True l2vni_subport_anchor_network_type = geneve l2vni_startup_jitter_max = 60 Step 3: (Optional) Create Network Node Config ---------------------------------------------- If LLDP data is not available in OVN, create ``/etc/neutron/l2vni_network_nodes.yaml``: .. code-block:: yaml network_nodes: - hostname: network-node-1.example.com trunks: - physical_network: physnet1 local_link_information: - switch_id: "00:11:22:33:44:55" port_id: "Ethernet1/1" switch_info: "tor-switch-1" .. note:: See `Network Node Configuration File`_ for complete format details, including multi-link LAG/bonding configurations. Step 4: Restart ironic-neutron-agent ------------------------------------- .. code-block:: bash systemctl restart ironic-neutron-agent Step 5: Verify Operation ------------------------- Check logs for successful reconciliation: .. code-block:: bash journalctl -u ironic-neutron-agent -f | grep -E "L2VNI|OVN event" You should see: .. code-block:: text Registered OVN event handler for L2VNI localnet port creation Started L2VNI trunk reconciliation loop (interval: 300s, initial delay: 345s with 45s jitter) Starting L2VNI trunk reconciliation Discovered trunk l2vni-trunk-network-node-1-physnet1 Added subport port-uuid (VLAN 100) to trunk trunk-uuid L2VNI trunk reconciliation completed successfully When a baremetal port is bound and a localnet port is created, you should see event-driven reconciliation: .. code-block:: text OVN localnet port CREATE event: neutron-abc123-localnet-physnet1 (network: abc123, owned: True) Triggering L2VNI trunk reconciliation due to localnet port CREATE event Starting L2VNI trunk reconciliation L2VNI trunk reconciliation completed successfully Step 6: Create Test Overlay Network ------------------------------------ Create a test setup to verify trunk reconciliation: .. code-block:: bash # Create overlay network openstack network create test-overlay # Create subnet openstack subnet create --network test-overlay \ --subnet-range 192.168.100.0/24 test-subnet # Create router with external gateway openstack router create test-router openstack router set --external-gateway public test-router # Add overlay network to router openstack router add subnet test-router test-subnet # Create baremetal port openstack port create \ --network test-overlay \ --vnic-type baremetal \ --binding-profile physical_network=physnet1 \ test-bm-port After the next reconciliation cycle, verify trunk subports: .. code-block:: bash openstack network trunk list openstack network trunk show You should see a subport with the VLAN allocated for test-overlay. Monitoring and Operations ========================== Log Messages ------------ **Normal Operation:** .. code-block:: text Started L2VNI trunk reconciliation loop Starting L2VNI trunk reconciliation Discovered trunk l2vni-trunk-system-1-physnet1 Added subport port-123 (VLAN 100) to trunk trunk-456 L2VNI trunk reconciliation completed successfully **OVN Event-Driven Reconciliation:** .. code-block:: text Registered OVN event handler for L2VNI localnet port creation OVN localnet port CREATE event: neutron-abc123-localnet-physnet1 (network: abc123, owned: True) Triggering L2VNI trunk reconciliation due to localnet port CREATE event Starting L2VNI trunk reconciliation L2VNI trunk reconciliation completed successfully OVN localnet port DELETE event: neutron-abc123-localnet-physnet1 (network: abc123, owned: True) Triggering L2VNI trunk reconciliation due to localnet port DELETE event **Event-Driven Fast Mode:** .. code-block:: text Router notification received, triggering fast reconciliation Switched to fast reconciliation mode (interval: 90s, duration: 600s) Exiting fast reconciliation mode, returning to baseline interval **Cleanup Operations:** .. code-block:: text Cleaning up orphaned trunk trunk-789 for chassis deleted-system physnet1 Deleted orphaned trunk trunk-789 Deleted orphaned anchor port port-999 Cleaning up orphaned ha_chassis_group network net-111 for group deleted-group **Warnings (Expected During Cleanup):** .. code-block:: text Failed to delete subport subport-222 Failed to delete trunk trunk-333 These warnings are normal when multiple agents attempt cleanup simultaneously. The first agent succeeds; others log warnings. Metrics and Health Indicators ------------------------------ Monitor these indicators for healthy operation: 1. **Reconciliation Completion**: Should see "completed successfully" messages at configured intervals 2. **API Error Rate**: Occasional 404 errors during cleanup are normal; frequent 500 errors indicate problems 3. **Reconciliation Duration**: Should complete in seconds; if taking minutes, check for API performance issues 4. **Orphaned Resource Count**: Should be zero or near-zero in steady state Troubleshooting =============== Reconciliation Not Running --------------------------- **Symptom**: No L2VNI reconciliation log messages. **Possible Causes:** 1. **Feature disabled**: ``enable_l2vni_trunk_reconciliation = False`` **Solution**: Set to ``True`` in config and restart agent. 2. **OVN connection failure**: Agent cannot connect to OVN NB/SB databases. **Solution**: Check OVN connection settings in config. Verify OVN services are running and accessible. 3. **No ha_chassis_groups**: No network nodes are members of ha_chassis_groups. **Solution**: This is normal if you have no routers with gateway ports. Create a router with external gateway to trigger ha_chassis_group creation. Trunks Not Created ------------------ **Symptom**: Reconciliation runs but no trunks appear. **Possible Causes:** 1. **No chassis in ha_chassis_groups**: Chassis exists in OVN but not assigned to any ha_chassis_group. **Solution**: Create a router with external gateway. OVN will automatically assign gateway chassis. 2. **Missing bridge-mappings**: Chassis lacks ``ovn-bridge-mappings`` in external_ids. **Solution**: Configure bridge-mappings on the chassis: .. code-block:: bash ovs-vsctl set Open_vSwitch . \ external-ids:ovn-bridge-mappings=physnet1:br-provider 3. **Network creation disabled**: ``l2vni_auto_create_networks = False`` but ha_chassis_group network doesn't exist. **Solution**: Either enable auto-creation or manually create network: .. code-block:: bash openstack network create l2vni-ha-group-{group-uuid} Subports Not Added ------------------ **Symptom**: Trunks exist but have no subports. **Possible Causes:** 1. **No overlay networks**: No VXLAN/Geneve networks are attached to routers. **Solution**: This is normal. Create overlay networks and attach to routers. 2. **No VLAN segments allocated**: L2VNI mechanism driver didn't allocate VLAN segments. **Solution**: Check that baremetal ports exist on overlay networks. Verify L2VNI mechanism driver is enabled and working. 3. **Subport anchor network missing**: Subport creation fails because anchor network doesn't exist. **Solution**: Enable auto-creation or manually create: .. code-block:: bash openstack network create l2vni-subport-anchor Missing Switch Information --------------------------- **Symptom**: Ports created but binding profile lacks local_link_information. **Possible Causes:** 1. **No LLDP data in OVN**: OVN doesn't have LLDP information for the chassis. **Solution**: Ensure switches are sending LLDP and OVN is configured to collect it, or provide fallback config file. 2. **Ironic not available**: Agent cannot query Ironic API. **Solution**: Check Ironic connectivity or provide fallback config file. 3. **Config file missing/incorrect**: Fallback config file doesn't exist or has wrong format. **Solution**: Create config file following the format in `Network Node Configuration File`_. Reconciliation Taking Too Long ------------------------------- **Symptom**: Reconciliation runs for minutes instead of seconds. **Possible Causes:** 1. **Large number of network nodes**: Many chassis with many physical networks. **Solution**: Increase ``l2vni_reconciliation_interval`` to reduce frequency. Consider deploying more agents for load distribution. 2. **Neutron API slow**: API calls taking long time. **Solution**: Investigate Neutron API performance. Check database load. 3. **OVN database queries slow**: Queries to OVN NB/SB taking long time. **Solution**: Check OVN database performance. Ensure indexes are healthy. 4. **Ironic queries slow**: Querying thousands of Ironic nodes/ports per run. **Solution**: Enable Ironic caching and use conductor_group/shard filtering: .. code-block:: ini [l2vni] ironic_cache_ttl = 3600 ironic_conductor_group = network-nodes ironic_shard = region-1 Infrastructure Network Creation Fails -------------------------------------- **Symptom**: Reconciliation fails with errors about network creation. **Example Error:** .. code-block:: text ERROR Failed to create L2VNI network 'l2vni-subport-anchor' with type 'geneve'. This indicates a misconfiguration - the requested network type is not available in your environment. **Possible Causes:** 1. **Network type not enabled**: Configured network type (geneve or vxlan) is not enabled in Neutron's ml2_type_drivers. **Solution**: Add the network type to ml2_type_drivers in neutron.conf: .. code-block:: ini [ml2] type_drivers = flat,vlan,geneve,vxlan Then restart neutron-server. 2. **Wrong network type configured**: ``l2vni_subport_anchor_network_type`` doesn't match your deployment's overlay network type. **Solution**: Set to match your environment: .. code-block:: ini [l2vni] # For VXLAN-based deployments l2vni_subport_anchor_network_type = vxlan # For Geneve-based deployments (default) l2vni_subport_anchor_network_type = geneve Then restart ironic-neutron-agent. Agent Crashes During Reconciliation ------------------------------------ **Symptom**: ironic-neutron-agent crashes or restarts during reconciliation. **Check Logs:** .. code-block:: bash journalctl -u ironic-neutron-agent --since "1 hour ago" **Possible Causes:** 1. **Uncaught exception**: Bug in reconciliation code. **Solution**: Report the bug with full traceback. As a workaround, disable trunk reconciliation until fixed. 2. **Memory exhaustion**: Agent runs out of memory in large deployments. **Solution**: Increase agent memory limits. Consider deploying more agents to distribute load. 3. **Deadlock or timeout**: Operation hangs waiting for response. **Solution**: Check network connectivity to Neutron/OVN. Review timeout settings. OVN Event Reconciliation Not Working ------------------------------------- **Symptom**: No event-driven reconciliation logs, only periodic reconciliation. **Check Logs:** .. code-block:: bash journalctl -u ironic-neutron-agent | grep "OVN event handler" **Possible Causes:** 1. **Feature disabled**: ``enable_l2vni_trunk_reconciliation_events = False`` **Solution**: Set to ``True`` in config and restart agent. 2. **OVN IDL not available**: Agent cannot connect to OVN Northbound database. **Solution**: Check OVN connection settings. Verify OVN services are running and accessible. Check for error messages about OVN connection failures. 3. **Event handler registration failed**: Agent logged warning about IDL not supporting event handlers. **Solution**: This indicates the OVN IDL connection was not established at agent startup. Check OVN connectivity and restart the agent. If the problem persists, check for OVN version compatibility issues. **Expected Behavior:** When working correctly, you should see this log message at agent startup: .. code-block:: text Registered OVN event handler for L2VNI localnet port creation If you see this warning instead, event-driven reconciliation is not active: .. code-block:: text OVN IDL does not support event handlers, falling back to periodic reconciliation only Upgrade Considerations ====================== Upgrading from Previous Versions --------------------------------- When upgrading to a version with trunk reconciliation: 1. **First upgrade**: Feature is disabled by default. No impact. 2. **Enabling the feature**: - Add ``[l2vni]`` configuration to neutron.conf - Set ``enable_l2vni_trunk_reconciliation = True`` - Restart ironic-neutron-agent 3. **First reconciliation**: Agent will: - Create infrastructure networks - Create anchor ports and trunks for existing network nodes - Add subports for existing overlay networks This initial reconciliation may take longer than usual. Watch logs for progress. Rolling Upgrades ---------------- The startup jitter feature (``l2vni_startup_jitter_max``) is specifically designed to handle rolling upgrades gracefully: **Scenario**: 3 agents running, performing rolling upgrade: 1. Stop agent-1, upgrade, restart → starts reconciliation after random delay (0-60s) 2. Stop agent-2, upgrade, restart → starts reconciliation after different delay (0-60s) 3. Stop agent-3, upgrade, restart → starts reconciliation after different delay (0-60s) This prevents all agents from hitting Neutron API simultaneously after restart. **Best Practices:** - Use default ``l2vni_startup_jitter_max = 60`` or higher - Upgrade one agent at a time - Wait for agent to complete first reconciliation before upgrading next - Monitor API load during upgrade Compatibility ------------- **Required OpenStack Release:** - Queens or later (requires OVN backend and ML2/OVN) **L2VNI Mechanism Driver:** - Must be version 7.1.0 or later for full compatibility - Trunk reconciliation works independently but is designed to complement the mechanism driver **Ironic Integration:** - Optional: Ironic API is used for enhanced switch information if available - Works without Ironic using LLDP or config file fallback Disabling the Feature --------------------- The agent supports flexible configuration of reconciliation mechanisms: **To disable event-driven L2VNI trunk reconciliation only (keep periodic):** 1. Set ``enable_l2vni_trunk_reconciliation_events = False`` in config 2. Restart ironic-neutron-agent 3. Event-driven reconciliation stops; periodic reconciliation continues **To disable periodic reconciliation only (keep event-driven):** 1. Set ``enable_l2vni_trunk_reconciliation = False`` in config 2. Restart ironic-neutron-agent 3. Periodic reconciliation stops; event-driven reconciliation continues 4. Note: Without periodic reconciliation, there is no safety net for missed events **To disable trunk reconciliation completely:** 1. Set both ``enable_l2vni_trunk_reconciliation = False`` and ``enable_l2vni_trunk_reconciliation_events = False`` in config 2. Restart ironic-neutron-agent 3. All reconciliation stops; existing trunks remain unchanged To fully clean up: .. code-block:: bash # List L2VNI trunks openstack network trunk list | grep l2vni-trunk # Delete each trunk (subports are automatically removed) openstack network trunk delete # Delete infrastructure networks openstack network list | grep l2vni- openstack network delete Performance Tuning ================== Small Deployments (< 10 network nodes) --------------------------------------- Optimize for fast convergence: .. code-block:: ini [l2vni] l2vni_reconciliation_interval = 60 l2vni_startup_jitter_max = 10 Medium Deployments (10-50 network nodes) ----------------------------------------- Balance convergence and overhead: .. code-block:: ini [l2vni] l2vni_reconciliation_interval = 180 l2vni_startup_jitter_max = 30 Large Deployments (50+ network nodes) -------------------------------------- Optimize for reduced API load: .. code-block:: ini [l2vni] l2vni_reconciliation_interval = 600 l2vni_startup_jitter_max = 120 Ironic Integration Performance ------------------------------- For deployments using Ironic for switch connection information: **Small Ironic Deployments (< 100 nodes)** Fast cache refresh: .. code-block:: ini [l2vni] ironic_cache_ttl = 600 **Medium Ironic Deployments (100-500 nodes)** Balanced refresh: .. code-block:: ini [l2vni] ironic_cache_ttl = 1800 **Large Ironic Deployments (500+ nodes)** Optimize for API load reduction: .. code-block:: ini [l2vni] ironic_cache_ttl = 7200 # Optional: Filter to reduce query scope ironic_conductor_group = network-nodes ironic_shard = region-west **Sharded Deployments** Use shard filtering to query only relevant nodes: .. code-block:: ini [l2vni] ironic_shard = shard-region-1 ironic_cache_ttl = 3600 See Also ======== * :doc:`router-ha-binding` - Router HA Binding for VLAN Networks * :doc:`/configuration/ml2/l2vni-mechanism-driver` - L2VNI Mechanism Driver * :doc:`/configuration/ironic-neutron-agent/index` - Agent Configuration * :doc:`/contributor/index` - Contributing Guide * OpenStack Neutron Documentation: https://docs.openstack.org/neutron/ * OVN Documentation: https://www.ovn.org/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/admin/router-ha-binding.rst0000664000175000017500000001706115157004031023753 0ustar00zuulzuul=================================== Router HA Binding for VLAN Networks =================================== Overview ======== The Router HA Binding feature ensures router interface ports are bound to the same HA chassis group as the network's external ports, enabling proper connectivity between baremetal nodes and routers on VLAN networks. This feature uses event-driven binding when HA chassis groups are created, plus periodic reconciliation. Without this feature, baremetal nodes on VLAN networks cannot communicate with their router gateway because the router's internal interface port (Logical Router Port) is not bound to any chassis. When a baremetal node tries to ARP for the router IP, no chassis responds because the router interface port has no HA chassis group set. Implementation ============== The ironic-neutron-agent now includes a **RouterHABindingManager** that automatically binds router interface ports to network HA chassis groups using a dual approach: **Event-Driven Binding** The agent monitors OVN's ``HA_Chassis_Group`` table for network-level groups. When a network HA chassis group is created or updated: 1. ``HAChassisGroupNetworkEvent`` fires immediately 2. Agent finds all router interface ports on that network 3. Binds each router port to the network's HA chassis group 4. Router can now respond to ARP requests on the physical VLAN network This provides **immediate** connectivity with no delay. **Periodic Reconciliation** A periodic reconciliation loop (default: 10 minutes) ensures eventual consistency by: 1. Discovering all networks with HA chassis groups 2. Finding router interface ports on those networks 3. Binding any unbound or incorrectly bound router ports 4. Only processing networks managed by this agent (via hash ring) This catches edge cases such as: - Routers added to existing networks (no event fires) - Missed events (agent down during HA chassis group creation) - Manual changes to router port configuration - Race conditions or out-of-order event processing Configuration ============= The feature is controlled by options in the ``[baremetal_agent]`` section of the agent configuration file (typically ``/etc/neutron/ironic_neutron_agent.ini``). Enable/Disable -------------- .. code-block:: ini [baremetal_agent] # Enable router HA binding for VLAN networks # Default: True enable_router_ha_binding = True Set to ``False`` to disable the feature if you are not using baremetal nodes on VLAN networks with routers. Event-Driven Binding -------------------- .. code-block:: ini [baremetal_agent] # Enable event-driven router HA binding # Default: True enable_router_ha_binding_events = True When enabled, the agent responds immediately to HA chassis group creation events by binding router interface ports on the affected network. This provides instant connectivity when networks are created. Set to ``False`` to disable event-driven binding and rely only on periodic reconciliation. This may result in connectivity delays until the next reconciliation cycle (default: 10 minutes). **Note:** Requires ``enable_router_ha_binding = True`` to have any effect. Reconciliation Interval ----------------------- .. code-block:: ini [baremetal_agent] # Interval in seconds between periodic reconciliation runs # Default: 600 (10 minutes) # Minimum: 60 router_ha_binding_interval = 600 Controls how frequently the agent performs full reconciliation. Startup Jitter -------------- .. code-block:: ini [baremetal_agent] # Maximum random delay for initial reconciliation start # Default: 60 seconds # Minimum: 0 router_ha_binding_startup_jitter_max = 60 Adds random delay (0 to max seconds) before first reconciliation run to prevent thundering herd when multiple agents restart simultaneously. A value of 60 means each agent starts reconciliation within 0-60 seconds of startup. Operational Considerations ========================== Multi-Agent Deployments ----------------------- In deployments with multiple ironic-neutron-agent instances: - Each agent uses a distributed hash ring to determine which networks it manages - Only the responsible agent will reconcile a given network - This prevents duplicate work and API contention - If an agent fails, other agents will automatically take over its networks Monitoring ---------- The agent logs binding activities at the INFO level: .. code-block:: text INFO ... Router HA binding enabled, initializing manager INFO ... Started router HA binding reconciliation loop (interval: 600s, first run in 42s) INFO ... Registered OVN event handler for HA chassis group network events INFO ... Network HA chassis group ... created/updated for network ..., triggering router interface binding INFO ... Updated router port HA chassis group from to (network ) INFO ... Router HA binding reconciliation complete: processed N networks, updated M router ports Failed updates are logged at ERROR level with full exception details. Performance Impact ------------------ The feature has minimal performance impact: - **Event-driven binding:** Immediate response with no periodic overhead - **Periodic reconciliation:** Runs every 10 minutes (configurable) - **Idempotent operations:** Most checks are "already correct" (cheap) - **Uses existing OVN connections:** Reuses connections from L2VNI trunk manager - **Distributed load:** Multiple agents split work via hash ring In a deployment with 100 networks with HA chassis groups: - Event-driven: 1-2 Neutron queries, 1-2 OVN updates per HA chassis group creation - Periodic: Scans all HA chassis groups, queries router ports per network - Per-network processing: 1 Neutron query, 1-2 OVN operations - Total periodic: ~100-200 operations every 10 minutes across all agents **Since events handle 99% of cases immediately, periodic reconciliation overhead is minimal.** Troubleshooting =============== Verifying the Feature is Running --------------------------------- Check agent logs for startup messages: .. code-block:: console $ grep "router HA binding" /var/log/neutron/ironic-neutron-agent.log INFO ... Router HA binding enabled, initializing manager INFO ... Started router HA binding reconciliation loop (interval: 600s, first run in 42s) INFO ... Registered OVN event handler for HA chassis group network events Checking Router Interface Binding ---------------------------------- If baremetal nodes cannot reach their gateway: 1. **Verify the network has an HA chassis group:** .. code-block:: console $ sudo ovn-nbctl ha-chassis-group-list | grep neutron- c18ab533-... (neutron-a72fd10e-...) 5668117a-... (3773bfbe-...) priority 1 2. **Check if router interface port is bound:** .. code-block:: console $ ROUTER_PORT_ID=$(openstack port list --network \ --device-owner network:router_interface -c ID -f value) $ sudo ovn-nbctl get Logical_Router_Port lrp-$ROUTER_PORT_ID ha_chassis_group # Should show UUID, not [] c18ab533-09b9-48fc-8acd-9407bd3f25d2 3. **If router port shows []:** Wait for next reconciliation or check logs for errors Forcing Immediate Reconciliation -------------------------------- To trigger reconciliation without waiting: .. code-block:: console $ systemctl restart ironic-neutron-agent # First reconciliation runs within 60 seconds (random jitter) See Also ======== * :doc:`l2vni-trunk-reconciliation` - L2VNI Trunk Reconciliation Configuration * :doc:`/configuration/ironic-neutron-agent/index` - Agent Configuration Reference ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/conf.py0000664000175000017500000000771315157004031020115 0ustar00zuulzuul# -*- 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 logging import os import sys # NOTE(amotoki): In case of oslo_config.sphinxext is enabled, # when resolving automodule neutron.tests.functional.db.test_migrations, # sphinx accesses tests/functional/__init__.py is processed, # eventlet.monkey_patch() is called and monkey_patch() tries to access # pyroute2.common.__class__ attribute. It raises pyroute2 warning and # it causes sphinx build failure due to warning-is-error = 1. # To pass sphinx build, ignore pyroute2 warning explicitly. logging.getLogger('pyroute2').setLevel(logging.ERROR) 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 = [ 'sphinxcontrib.apidoc', 'oslo_config.sphinxext', 'oslo_config.sphinxconfiggen', 'openstackdocstheme', ] # 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. copyright = 'OpenStack Foundation' config_generator_config_file = [ ('../../tools/config/networking-baremetal-ironic-neutron-agent.conf', '_static/ironic_neutron_agent.ini'), ('../../tools/config/networking-baremetal-common-device-driver-opts.conf', '_static/common_device_driver_opts'), ('../../tools/config/networking-baremetal-netconf-openconfig-driver-opts.conf', '_static/netconf_openconfig_device_driver') ] # sample_config_basename = '_static/ironic_neutron_agent.ini' # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['networking_baremetal.'] # 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' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # -- 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 = 'openstackdocs' # openstackdocstheme options openstackdocs_repo_name = 'openstack/networking-baremetal' openstackdocs_pdf_link = True openstackdocs_use_storyboard = False # Output file base name for HTML help builder. htmlhelp_basename = 'networking-baremetaldoc' latex_use_xindy = False # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', 'doc-networking-baremetal.tex', 'Networking Baremetal Documentation', 'OpenStack Foundation', 'manual'), ] # Example configuration for intersphinx: refer to the Python standard library. #intersphinx_mapping = {'http://docs.python.org/': None} # -- sphinxcontrib.apidoc configuration -------------------------------------- apidoc_module_dir = '../../networking_baremetal' apidoc_output_dir = 'contributor/api' apidoc_excluded_paths = [ 'tests', ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8329942 networking_baremetal-7.2.0/doc/source/configuration/0000775000175000017500000000000015157004110021453 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/configuration/index.rst0000664000175000017500000000025115157004031023314 0ustar00zuulzuul===================== Configuration Options ===================== .. toctree:: :maxdepth: 3 Ironic Neutron agent ML2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8329942 networking_baremetal-7.2.0/doc/source/configuration/ironic-neutron-agent/0000775000175000017500000000000015157004110025522 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/configuration/ironic-neutron-agent/config.rst0000664000175000017500000000060515157004031027524 0ustar00zuulzuul============================================ ironic-neutron-agent - Configuration Options ============================================ The following is an overview of all available configuration options in networking-baremetal. For a sample configuration file, refer to :doc:`sample-config`. .. show-options:: :config-file: tools/config/networking-baremetal-ironic-neutron-agent.conf ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/configuration/ironic-neutron-agent/index.rst0000664000175000017500000000047615157004031027374 0ustar00zuulzuul======================= Configuration Reference ======================= The following pages describe configuration options that can be used to adjust the ``ironic-neutron-agent`` service to your particular situation. .. toctree:: :maxdepth: 1 Configuration Options Sample Config File ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/configuration/ironic-neutron-agent/sample-config.rst0000664000175000017500000000123415157004031031002 0ustar00zuulzuul========================= Sample Configuration File ========================= The following is a sample ironic-neutron-agent configuration for adaptation and use. For a detailed overview of all available configuration options, refer to :doc:`config`. The sample configuration can also be viewed in :download:`file form `. .. important:: The sample configuration file is auto-generated from networking-baremetal when this documentation is built. You must ensure your version of networking-baremetal matches the version of this documentation. .. literalinclude:: /_static/ironic_neutron_agent.ini.conf.sample ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8339944 networking_baremetal-7.2.0/doc/source/configuration/ml2/0000775000175000017500000000000015157004110022145 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8339944 networking_baremetal-7.2.0/doc/source/configuration/ml2/device_drivers/0000775000175000017500000000000015157004110025142 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/configuration/ml2/device_drivers/common_config.rst0000664000175000017500000000217415157004031030517 0ustar00zuulzuul=================================================== Common configuration options for all device drivers =================================================== This page describes configuration options that is common to all networking-baremetal device drivers. Individual drivers may have independent configuration requirements depending on the implementation, refer to the device driver specific documentation. Configuration options ^^^^^^^^^^^^^^^^^^^^^ .. show-options:: :config-file: tools/config/networking-baremetal-common-device-driver-opts.conf Sample Configuration File ^^^^^^^^^^^^^^^^^^^^^^^^^ The following is a sample configuration section that would be added to ``/etc/neutron/plugins/ml2/ml2_conf.ini``. The sample configuration can also be viewed in :download:`file form `. .. important:: The sample configuration file is auto-generated from networking-baremetal when this documentation is built. You must ensure your version of networking-baremetal matches the version of this documentation. .. literalinclude:: /_static/common_device_driver_opts.conf.sample ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/configuration/ml2/device_drivers/index.rst0000664000175000017500000000127715157004031027014 0ustar00zuulzuul============== Device drivers ============== The baremetal mechanism ML2 plug-in provides a device driver plug-in interface, this interface can be used to add device (switch) configuration capabilities. The interface uses `stevedore `__ for dynamic loading. Individual drivers may have independent configuration requirements depending on the implementation. :ref:`Driver specific options ` are documented separately. .. toctree:: :maxdepth: 2 Common configuration options .. _device_drivers: Available device drivers ~~~~~~~~~~~~~~~~~~~~~~~~ .. toctree:: :maxdepth: 3 netconf-openconfig ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/configuration/ml2/device_drivers/netconf-openconfig.rst0000664000175000017500000000356215157004031031465 0ustar00zuulzuulDevice driver - netconf-openconfig ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``netconf-openconfig`` device driver uses the Network Configuration Protocol (`NETCONF `__) and open source vendor-neutral `OpenConfig `__ YANG models. This driver has been tested with the following switch vendor/operating systems: * Cisco NXOS * Arista vEOS **Example configuration for Cisco NXOS device**: .. code-block:: ini [networking_baremetal] enabled_devices = nexus.example.net [nexus.example.net] driver = netconf-openconfig device_params = name:nexus switch_info = nexus switch_id = 00:53:00:0a:0a:0a host = nexus.example.net username = user key_filename = /etc/neutron/ssh_keys/nexus_sshkey **Example configuration for Arista EOS device**: .. code-block:: ini [networking_baremetal] enabled_devices = arista.example.net [arista.example.net] driver = netconf-openconfig device_params = name:default switch_info = arista switch_id = 00:53:00:0b:0b:0b host = arista.example.net username = user key_filename = /etc/neutron/ssh_keys/arista_sshkey Configuration options ^^^^^^^^^^^^^^^^^^^^^ .. show-options:: :config-file: tools/config/networking-baremetal-netconf-openconfig-driver-opts.conf Sample Configuration File ^^^^^^^^^^^^^^^^^^^^^^^^^ The following is a sample configuration section that would be added to ``/etc/neutron/plugins/ml2/ml2_conf.ini``. The sample configuration can also be viewed in :download:`file form `. .. important:: The sample configuration file is auto-generated from networking-baremetal when this documentation is built. You must ensure your version of networking-baremetal matches the version of this documentation. .. literalinclude:: /_static/netconf_openconfig_device_driver.conf.sample ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/configuration/ml2/index.rst0000664000175000017500000000215615157004031024014 0ustar00zuulzuul======================= Configuration Reference ======================= The following pages describe configuration options that can be used to adjust the neutron ML2 configuration and the baremetal ML2 plug-in and device drivers to your particular situation. To enable mechanism drivers in the ML2 plug-in, edit the ``/etc/neutron/plugins/ml2/ml2_conf.ini`` configuration file. For example, this enables the ``openvswitch`` and ``baremetal`` mechanism drivers: .. code-block:: ini [ml2] mechanism_drivers = openvswitch,baremetal To add a device to manage, edit the ``/etc/neutron/plugins/ml2/ml2_conf.ini`` configuration file. The example below enables devices: ``device_a.example.net`` and ``device_b.example.net``. For each device a separate section in the same configuration file defines the device and driver specific configuration. Please refer to :doc:`device_drivers/index` for details. .. code-block:: ini [networking_baremetal] enabled_device = device_a.example.net,device_b.example.net .. toctree:: :maxdepth: 4 Device Drivers L2VNI Mechanism Driver ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/configuration/ml2/l2vni-example.ini0000664000175000017500000000651415157004031025341 0ustar00zuulzuul# Example configuration for L2VNI mechanism driver # This file shows typical configuration for enabling baremetal # connectivity to VXLAN/Geneve overlay networks # # IMPORTANT: This driver requires OVN as the ML2 backend [ml2] # Enable the L2VNI mechanism driver # ORDER IS CRITICAL - genericswitch must be last! mechanism_drivers = ovn,baremetal_l2vni,baremetal,genericswitch # Enable overlay network types type_drivers = flat,vlan,vxlan,geneve # Set the default project network type for new networks. project_network_types = vxlan # Configure VLAN ranges for dynamic segment allocation # Each overlay network with baremetal ports will allocate one VLAN [ml2_type_vlan] network_vlan_ranges = physnet1:100:200,physnet2:300:400 # L2VNI mechanism driver configuration [baremetal_l2vni] # Automatically create OVN localnet ports (default: True) # # When to use True (default): # - Direct VLAN-to-VXLAN fabric attachment scenarios # - Using ML2 for direct attachment to a VLAN-to-VXLAN fabric # - OVN localnet ports enable the overlay<->physical network translation # # When to use False: # - Pure EVPN deployments where Neutron ensures attachment to remote # network infrastructure through tunnels rather than localnet ports # - When localnet ports are managed externally # # If using EVPN with tunnels, you likely want False create_localnet_ports = True # Default physical network for baremetal ports # If set, ports don't need to specify physical_network in binding profile # If not set, each port must specify physical_network in binding profile default_physical_network = physnet1 # OVN configuration # Ensure bridge mappings are configured for your physical networks [ovn] ovn_nb_connection = tcp:127.0.0.1:6641 ovn_sb_connection = tcp:127.0.0.1:6642 # On each OVN chassis/compute node, configure bridge mappings: # ovs-vsctl set Open_vSwitch . \ # external-ids:ovn-bridge-mappings=physnet1:br-provider,physnet2:br-external # Example: Create a VXLAN network and baremetal port # # 1. Create tenant overlay network: # openstack network create \ # overlay-network # # Note: Only VXLAN and Geneve are supported. You must explicitly # specify the network type if your default is set to something else. # # 2. Create subnet: # openstack subnet create \ # --network overlay-network \ # --subnet-range 192.168.100.0/24 \ # overlay-subnet # # IMPORTANT: This example is if you are manually creating # a bare metal port and attaching it. Normal Ironic usage # and flow does not require the creation of a port with a # specific vnic-type nor providing a binding profile. # Ironic takes care of these aspects for a user. # # 3. Create baremetal port: # openstack port create \ # --network overlay-network \ # --vnic-type baremetal \ # --binding-profile physical_network=physnet1 \ # baremetal-port # # OR if default_physical_network is set: # openstack port create \ # --network overlay-network \ # --vnic-type baremetal \ # baremetal-port # # The driver will automatically: # - Allocate a dynamic VLAN segment (e.g., VLAN 150) on physnet1 # - Create an OVN localnet port to bridge VXLAN <-> VLAN 150 # - Bind the port using the VLAN segment # - A ML2 plugin, for example, Genericswitch, will confiure any required # VXLAN to VLAN mapping inside of attached physical switches and physical # switch port. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/configuration/ml2/l2vni-mechanism-driver.rst0000664000175000017500000003617215157004031027177 0ustar00zuulzuul==================================== L2VNI Mechanism Driver Configuration ==================================== Overview ======== The L2VNI (Layer 2 Virtual Network Identifier) mechanism driver enables baremetal servers to connect to VXLAN and Geneve overlay networks by dynamically creating VLAN segments that bridge the overlay network to the physical network infrastructure. This driver is essential for deployments where baremetal nodes need to participate in tenant overlay networks alongside virtual machines. Architecture ============ How it Works ------------ The L2VNI mechanism driver operates as follows: 1. **Overlay Network Detection**: When a baremetal port is bound to a VXLAN or Geneve network, the driver is triggered. 2. **Dynamic VLAN Allocation**: The driver allocates a dynamic VLAN segment on the specified physical network to carry traffic for the overlay network. 3. **OVN Localnet Port Creation**: If OVN is the backend, the driver creates a localnet port in OVN to bridge the overlay network to the physical VLAN. 4. **Port Binding**: The driver instructs Neutron to continue binding the port using the dynamically allocated VLAN segment. 5. **Traffic Flow**: Traffic flows from the overlay network (VXLAN/Geneve) through the localnet port to the VLAN segment, then to the baremetal server. .. code-block:: text ┌──────────────┐ │ Baremetal │ │ Server │ └──────┬───────┘ │ VLAN 100 │ ┌──────▼───────┐ │ Physical │ │ Switch │ └──────┬───────┘ │ VLAN 100 │ ┌──────▼────────────────┐ │ OVN Localnet Port │ │ (bridges VLAN↔VXLAN) │ └──────┬────────────────┘ │ ┌──────▼───────┐ │ VXLAN/Geneve │ │ Overlay │ └──────────────┘ Switch Management Integration ============================= The L2VNI mechanism driver works in conjunction with switch management plugins (such as genericswitch) to provide complete end-to-end connectivity for baremetal servers on overlay networks. Role of Switch Management Plugins --------------------------------- Switch management plugins handle the crucial task of configuring physical network switches to map VNI (VXLAN/Geneve Network Identifier) values to VLAN tags on the physical ports where baremetal servers connect. When a baremetal port is created or deleted, the following workflow occurs: 1. **L2VNI Driver** (this driver): - Allocates a dynamic VLAN segment for the overlay network - Creates an OVN localnet port to bridge overlay ↔ VLAN - Continues the port binding process 2. **Switch Management Plugin** (e.g., genericswitch): - Configures the physical switch to map the VLAN to the server's port - This is the **final step** in port binding - Must be listed **last** in mechanism_drivers Mechanism Driver Ordering ------------------------- The order of mechanism drivers in ``ml2_conf.ini`` is critical: .. code-block:: ini [ml2] # CORRECT ORDER - switch management MUST be last mechanism_drivers = ovn,baremetal_l2vni,baremetal,genericswitch # INCORRECT - will break port binding mechanism_drivers = ovn,genericswitch,baremetal_l2vni # WRONG! **Why order matters:** - OVN provides the overlay network backend - baremetal_l2vni allocates the VLAN and creates localnet ports - baremetal handles standard baremetal port binding - genericswitch (or other switch management) performs the **final** switch configuration step If the switch management plugin runs too early, it won't have the correct VLAN information allocated by baremetal_l2vni, causing port binding to fail. Requirements ============ - OpenStack Neutron with ML2 plugin - OVN (Open Virtual Network) backend (**required** - this driver requires OVN) - Physical network switches configured for VLAN trunking - Switch management ML2 plugin (e.g., genericswitch) for VNI↔VLAN mapping - Baremetal nodes with appropriate VLAN configuration Configuration ============= Enabling the Driver ------------------- Edit ``/etc/neutron/plugins/ml2/ml2_conf.ini`` and add ``baremetal_l2vni`` to the list of mechanism drivers: .. code-block:: ini [ml2] mechanism_drivers = ovn,baremetal_l2vni,baremetal,genericswitch .. important:: **Driver order is critical:** - ``ovn`` must be first (provides overlay network backend) - ``baremetal_l2vni`` allocates VLANs and creates localnet ports - ``baremetal`` handles standard baremetal port binding - ``genericswitch`` (or other switch management) must be **last** to perform final switch configuration Configuration Options --------------------- Add a ``[baremetal_l2vni]`` section to your configuration file: .. note:: A complete configuration example is available at :download:`l2vni-example.ini ` .. code-block:: ini [baremetal_l2vni] # Enable automatic creation of OVN localnet ports (default: True) create_localnet_ports = True # Default physical network for baremetal ports (optional) # If not set, ports must specify physical_network in binding profile default_physical_network = physnet1 Configuration Parameters ~~~~~~~~~~~~~~~~~~~~~~~~ ``create_localnet_ports`` **Type**: Boolean **Default**: ``True`` **Description**: Automatically create OVN localnet ports to bridge VXLAN/Geneve overlay networks to physical networks. **When to use True (default):** - Direct VLAN-to-VXLAN fabric attachment scenarios - When using ML2 plugin for direct attachment to a VLAN-to-VXLAN fabric - The OVN localnet ports enable the overlay↔physical network translation **When to use False:** - Pure EVPN deployments where Neutron is responsible for ensuring attachment to the remote network infrastructure through tunnels rather than through localnet ports in OVN - When localnet ports are managed externally .. note:: If you're using EVPN where network attachment is handled via tunnels, you likely want to set this to ``False`` since localnet ports are not needed for that architecture. ``default_physical_network`` **Type**: String **Default**: ``None`` **Description**: Default physical network name to use for baremetal L2VNI bindings when the port binding profile does not specify a ``physical_network``. If not set and the port lacks ``physical_network`` in its binding profile, port binding will fail. Port Binding Profile -------------------- When creating baremetal ports, you can specify the physical network in the binding profile: .. code-block:: bash openstack port create \ --network overlay-network \ --vnic-type baremetal \ --binding-profile physical_network=physnet1 \ baremetal-port If ``default_physical_network`` is configured, the binding profile is optional. Network Configuration ===================== Physical Networks ----------------- Ensure your physical networks are properly configured in ML2: .. code-block:: ini [ml2_type_vlan] network_vlan_ranges = physnet1:100:200 On each chassis (compute/network node), configure OVN bridge mappings: .. code-block:: bash ovs-vsctl set Open_vSwitch . \ external-ids:ovn-bridge-mappings=physnet1:br-provider Router Configuration -------------------- When baremetal networks are attached to Neutron routers, ensure the router has an external gateway configured for proper routing behavior. The driver automatically configures router gateway chassis bindings when necessary. Deployment Guide ================ Step 1: Enable the Mechanism Driver ----------------------------------- Edit ``/etc/neutron/plugins/ml2/ml2_conf.ini``: .. code-block:: ini [ml2] mechanism_drivers = ovn,baremetal_l2vni,baremetal,genericswitch type_drivers = flat,vlan,vxlan,geneve project_network_types = vxlan [baremetal_l2vni] create_localnet_ports = True default_physical_network = physnet1 .. important:: Ensure mechanism drivers are in the correct order: OVN, baremetal_l2vni, baremetal, genericswitch (or other switch management plugin last). Step 2: Configure Physical Networks ------------------------------------ Ensure VLAN ranges are configured: .. code-block:: ini [ml2_type_vlan] network_vlan_ranges = physnet1:100:200 Step 3: Configure OVN Bridge Mappings ------------------------------------- On each chassis that will handle baremetal traffic: .. code-block:: bash ovs-vsctl set Open_vSwitch . \ external-ids:ovn-bridge-mappings=physnet1:br-provider Step 4: Restart Neutron Server ------------------------------ .. code-block:: bash systemctl restart neutron-server Step 5: Create Overlay Network ------------------------------ Create a tenant overlay network. You must explicitly specify the network type as VXLAN or Geneve (the only supported types for this driver): .. code-block:: bash openstack network create \ overlay-network openstack subnet create \ --network overlay-network \ --subnet-range 192.168.100.0/24 \ overlay-subnet .. warning:: **Do not use provider networks** (``--provider-physical-network``, with this driver. Provider networks are intended to be pre-configured for direct attachment, where as this model and interaciton requires additional confiuration and actions to occur. .. note:: Only VXLAN and Geneve network types are supported. If your default network type is configured to something else (e.g., VLAN or flat), then this plugin will not work as intended. Step 6: Create Baremetal Port ----------------------------- Create a baremetal port on the overlay network: .. NOTE:: This step is intended for manually triggering the binding logic which demonstrates the mechanism driver creating lower binding segment. In normal usage flow of this plugin, Ironic manages the binding profile and vnic type attributes of ports. .. code-block:: bash openstack port create \ --network overlay-network \ --vnic-type baremetal \ --binding-profile physical_network=physnet1 \ baremetal-port The driver will automatically: - Allocate a dynamic VLAN segment (e.g., VLAN 150) on physnet1 - Create an OVN localnet port to bridge VXLAN ↔ VLAN - Bind the port using the VLAN segment Troubleshooting =============== Port Binding Fails ------------------ **Symptom**: Port remains in ``DOWN`` state or binding fails. **Possible Causes**: 1. **Missing physical_network**: Port binding profile doesn't specify ``physical_network`` and no ``default_physical_network`` is configured. **Solution**: Either specify physical_network in binding profile or configure ``default_physical_network``. 2. **Physical network not found**: No chassis has the specified physical network in bridge-mappings. **Solution**: Check logs for error message and verify OVN bridge-mappings configuration on all chassis. 3. **VLAN exhaustion**: No available VLANs in the configured range. **Solution**: Expand VLAN range in ``ml2_type_vlan`` configuration. Localnet Port Not Created ------------------------- **Symptom**: Port binds but traffic doesn't flow. **Possible Causes**: 1. **Localnet creation disabled**: ``create_localnet_ports = False`` **Solution**: Set ``create_localnet_ports = True`` or manage localnet ports externally. 2. **OVN not available**: Driver cannot connect to OVN. **Solution**: Check Neutron logs for OVN connection errors. Verify OVN mechanism driver is loaded. 3. **Chassis without physnet**: No chassis has the physical network configured. **Solution**: Configure ``ovn-bridge-mappings`` on at least one chassis. Router Attachment Breaks Connectivity ------------------------------------- **Symptom**: Adding a router to the network breaks baremetal connectivity. **Possible Causes**: 1. **Router without external gateway**: Router has no gateway port, causing OVN to remove external port bindings. **Solution**: Configure an external gateway for the router, or ensure the gateway interface is up. 2. **Gateway chassis mismatch**: Router gateway is on a different chassis than the localnet port. **Solution**: The driver handles this automatically. Check logs for gateway chassis binding messages. Checking Logs ------------- Enable debug logging for detailed information: .. code-block:: ini [DEFAULT] debug = True Check Neutron server logs: .. code-block:: bash journalctl -u neutron-server -f Look for messages containing: - ``L2vniMechanismDriver`` - General driver operations - ``localnet port`` - Localnet port creation/deletion - ``physical_network`` - Physical network validation - ``allocate dynamic segment`` - VLAN segment allocation Verifying OVN State ------------------- Check OVN Northbound database: .. code-block:: bash # List logical switches and ports ovn-nbctl show # Look for localnet ports (format: neutron--localnet-) ovn-nbctl list Logical_Switch_Port | grep localnet Check OVN Southbound database: .. code-block:: bash # List chassis and their bridge-mappings ovn-sbctl list Chassis # Check port bindings ovn-sbctl list Port_Binding Advanced Topics =============== Multiple Physical Networks -------------------------- You can use different physical networks for different ports: .. code-block:: bash openstack port create \ --network overlay-network \ --vnic-type baremetal \ --binding-profile physical_network=physnet1 \ port-on-physnet1 openstack port create \ --network overlay-network \ --vnic-type baremetal \ --binding-profile physical_network=physnet2 \ port-on-physnet2 The driver will create separate VLAN segments and localnet ports for each physical network. VLAN Segment Reuse ------------------ The driver is idempotent - if a VLAN segment already exists for a given overlay network + physical network combination, it will reuse the existing segment rather than allocating a new one. Segment Cleanup --------------- When the last baremetal port using a dynamic VLAN segment is deleted or unbound, the driver automatically: 1. Removes the OVN localnet port 2. Releases the dynamic VLAN segment back to the pool Performance Considerations ========================== VLAN Pool Sizing ---------------- Plan your VLAN ranges carefully. Each overlay network that has baremetal ports on a given physical network requires one VLAN from the pool. For example, with 100 tenant overlay networks and baremetal nodes on 2 physical networks, you need up to 200 VLANs. OVN Database Load ----------------- The driver queries the OVN Southbound database to validate physical network availability. In very large deployments (1000+ chassis), this query may add latency to port binding operations. See Also ======== * :doc:`/configuration/ml2/index` - ML2 Plugin Configuration * :doc:`/contributor/index` - Contributing Guide * OpenStack Neutron Documentation: https://docs.openstack.org/neutron/ * OVN Documentation: https://www.ovn.org/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8349946 networking_baremetal-7.2.0/doc/source/contributor/0000775000175000017500000000000015157004110021156 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/contributor/index.rst0000664000175000017500000000436615157004031023032 0ustar00zuulzuul============ Contributing ============ This document provides some necessary points for developers to consider when writing and reviewing networking-baremetal code. Getting Started =============== If you're completely new to OpenStack and want to contribute to the networking-baremetal project, please start by familiarizing yourself with the `Infra Team's Developer Guide `_. This will help you get your accounts set up in Launchpad and Gerrit, familiarize you with the workflow for the OpenStack continuous integration and testing systems, and help you with your first commit. LaunchPad Project ----------------- Most of the tools used for OpenStack require a launchpad.net ID for authentication. .. seealso:: * https://launchpad.net * https://launchpad.net/ironic Related Projects ---------------- Networking Baremetal is tightly integrated with the ironic and neutron projects. Ironic and its related projects are developed by the same community. .. seealso:: * https://launchpad.net/ironic * https://launchpad.net/neutron Project Hosting Details ----------------------- Bug tracker https://bugs.launchpad.net/networking-baremetal Mailing list (prefix Subject line with ``[ironic][networking-baremetal]``) http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss Code Hosting https://opendev.org/openstack/networking-baremetal Code Review https://review.opendev.org/#/q/status:open+project:openstack/networking-baremetal,n,z Developer quick-starts ====================== These are quick walk throughs to get you started developing code for networking-baremetal. These assume you are already familiar with submitting code reviews to an OpenStack project. .. toctree:: :maxdepth: 2 Deploying networking-baremetal with DevStack Deploying networking-baremetal and multi-tenant networking with DevStack Virtual lab with virtual switch and netconf-openconfig Device Driver Full networking-baremetal python API reference ============================================== * :ref:`modindex` .. # api/modules is hidden since it's in the modindex link above. .. toctree:: :hidden: api/modules ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/contributor/quickstart-multitenant.rst0000664000175000017500000001126015157004031026446 0ustar00zuulzuulDeploying networking-baremetal and multi-tenant networking with DevStack ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DevStack may be configured to deploy networking-baremetal Networking service plugin together with networking-generic-switch for multi-tenant networking. It is highly recommended to deploy on an expendable virtual machine and not on your personal work station. Deploying networking-baremetal with DevStack requires a machine running Ubuntu 14.04 (or later) or Fedora 20 (or later). .. seealso:: http://docs.openstack.org/devstack/latest Create ``devstack/local.conf`` with minimal settings required to enable networking-baremetal with ironic and networking-generic-switch for multi-tenant networking. Here is an example of local.conf:: [[local|localrc]] # Credentials ADMIN_PASSWORD=password DATABASE_PASSWORD=password RABBIT_PASSWORD=password SERVICE_PASSWORD=password SERVICE_TOKEN=password SWIFT_HASH=password SWIFT_TEMPURL_KEY=password # Install networking-generic-switch Neutron ML2 driver that interacts with OVS enable_plugin networking-generic-switch https://opendev.org/openstack/networking-generic-switch # Enable networking-baremetal plugin enable_plugin networking-baremetal https://opendev.org/openstack/networking-baremetal.git enable_service networking_baremetal enable_service ir-neutronagt # Add link local info when registering Ironic node IRONIC_USE_LINK_LOCAL=True IRONIC_ENABLED_NETWORK_INTERFACES=flat,neutron IRONIC_NETWORK_INTERFACE=neutron #Networking configuration OVS_PHYSICAL_BRIDGE=brbm PHYSICAL_NETWORK=mynetwork IRONIC_PROVISION_NETWORK_NAME=ironic-provision IRONIC_PROVISION_PROVIDER_NETWORK_TYPE=vlan IRONIC_PROVISION_SUBNET_PREFIX=10.0.5.0/24 IRONIC_PROVISION_SUBNET_GATEWAY=10.0.5.1 Q_PLUGIN=ml2 ENABLE_TENANT_VLANS=True Q_ML2_TENANT_NETWORK_TYPE=vlan TENANT_VLAN_RANGE=100:150 Q_USE_PROVIDERNET_FOR_PUBLIC=False # Enable segments service_plugin for routed networks Q_SERVICE_PLUGIN_CLASSES=neutron.services.l3_router.l3_router_plugin.L3RouterPlugin,segments IRONIC_USE_NEUTRON_SEGMENTS=True # Configure ironic from ironic devstack plugin. enable_plugin ironic https://opendev.org/openstack/ironic # Enable Ironic API and Ironic Conductor enable_service ironic enable_service ir-api enable_service ir-cond # Enable Neutron which is required by Ironic and disable nova-network. disable_service n-net disable_service n-novnc enable_service q-svc enable_service q-agt enable_service q-dhcp enable_service q-l3 enable_service q-meta enable_service neutron # Enable Swift for agent_* drivers enable_service s-proxy enable_service s-object enable_service s-container enable_service s-account # Disable Horizon disable_service horizon # Disable Heat disable_service heat h-api h-api-cfn h-api-cw h-eng # Disable Cinder disable_service cinder c-sch c-api c-vol # Swift temp URL's are required for agent_* drivers. SWIFT_ENABLE_TEMPURLS=True # Create 3 virtual machines to pose as Ironic's baremetal nodes. IRONIC_VM_COUNT=3 IRONIC_BAREMETAL_BASIC_OPS=True DEFAULT_INSTANCE_TYPE=baremetal # Enable additional hardware types, if needed. #IRONIC_ENABLED_HARDWARE_TYPES=ipmi,fake-hardware # Don't forget that many hardware types require enabling of additional # interfaces, most often power and management: #IRONIC_ENABLED_MANAGEMENT_INTERFACES=ipmitool,fake #IRONIC_ENABLED_POWER_INTERFACES=ipmitool,fake # The 'ipmi' hardware type's default deploy interface is 'iscsi'. # This would change the default to 'direct': #IRONIC_DEFAULT_DEPLOY_INTERFACE=direct # Change this to alter the default driver for nodes created by devstack. # This driver should be in the enabled list above. IRONIC_DEPLOY_DRIVER=ipmi # The parameters below represent the minimum possible values to create # functional nodes. IRONIC_VM_SPECS_RAM=1024 IRONIC_VM_SPECS_DISK=10 # Size of the ephemeral partition in GB. Use 0 for no ephemeral partition. IRONIC_VM_EPHEMERAL_DISK=0 # To build your own IPA ramdisk from source, set this to True IRONIC_BUILD_DEPLOY_RAMDISK=False VIRT_DRIVER=ironic # By default, DevStack creates a 10.0.0.0/24 network for instances. # If this overlaps with the hosts network, you may adjust with the # following. NETWORK_GATEWAY=10.1.0.1 FIXED_RANGE=10.1.0.0/24 FIXED_NETWORK_SIZE=256 # Log all output to files LOGFILE=$HOME/devstack.log LOGDIR=$HOME/logs IRONIC_VM_LOG_DIR=$HOME/ironic-bm-logs ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/contributor/quickstart-netconf-openconfig.rst0000664000175000017500000000047515157004031027671 0ustar00zuulzuulVirtual lab with virtual switch and netconf-openconfig Device Driver #################################################################### Ansible playbooks that can be used to set up a lab for developing networking-baremetal network device integration is hosted on `GitHub `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/contributor/quickstart.rst0000664000175000017500000000673015157004031024112 0ustar00zuulzuulDeploying networking-baremetal with DevStack ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DevStack may be configured to deploy networking-baremetal Networking service plugin. It is highly recommended to deploy on an expendable virtual machine and not on your personal work station. Deploying networking-baremetal with DevStack requires a machine running Ubuntu 14.04 (or later) or Fedora 20 (or later). .. seealso:: http://docs.openstack.org/devstack/latest Create ``devstack/local.conf`` with minimal settings required to enable networking-baremetal with ironic. Here is an example of local.conf:: cd devstack cat >local.conf <`_ Indices and tables ================== * :ref:`genindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8349946 networking_baremetal-7.2.0/doc/source/install/0000775000175000017500000000000015157004110020252 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/doc/source/install/index.rst0000664000175000017500000001065315157004031022122 0ustar00zuulzuul============ Installation ============ This section describes how to install and configure the ``networking-baremetal`` plugin and ``ironic-neutron-agent``. The ``ironic-neutron-agent`` is a neutron agent that populates the host to physical network mapping for baremetal nodes in neutron. Neutron uses this to calculate the segment to host mapping information. Install the networking-baremetal plugin and agent ------------------------------------------------- At the command line: .. code-block:: shell $ pip install networking-baremetal Or, if you have neutron installed in a virtualenv, install the ``networking-baremetal`` plugin to the same virtualenv: .. code-block:: shell $ . /bin/activate $ pip install networking-baremetal Or, use the package from your distribution. For RHEL7/CentOS7: .. code-block:: shell $ yum install python2-networking-baremetal python2-ironic-neutron-agent Enable baremetal mechanism driver in the Networking service ----------------------------------------------------------- To enable mechanism drivers in the ML2 plug-in, edit the ``/etc/neutron/plugins/ml2/ml2_conf.ini`` configuration file. For example, this enables the ``openvswitch`` and ``baremetal`` mechanism drivers: .. code-block:: ini [ml2] mechanism_drivers = openvswitch,baremetal Add devices (switches) to manage -------------------------------- The baremetal mechanism ML2 plug-in provides a device driver plug-in interface. If a device driver for the switch model exist the baremetal ML2 plug-in can be configured to manage switch configuration, adding tenant VLANs and setting switch port VLAN configuration etc. To add a device to manage, edit the ``/etc/neutron/plugins/ml2/ml2_conf.ini`` configuration file. The example below enables devices: ``device_a.example.net`` and ``device_b.example.net``. Both devices in the example is using the ``netconf-openconfig`` device driver. For each device a separate section in configuration defines the device and driver specific configuration. .. code-block:: ini [networking_baremetal] enabled_devices = device_a.example.net,device_b.example.net [device_a.example.net] driver = netconf-openconfig switch_info = device_a switch_id = 00:53:00:0a:0a:0a host = device_a.example.net username = user key_filename = /etc/neutron/ssh_keys/device_a_sshkey hostkey_verify = false [device_b.example.net] driver = netconf-openconfig switch_info = device_b switch_id = 00:53:00:0b:0b:0b host = device_a.example.net username = user key_filename = /etc/neutron/ssh_keys/device_a_sshkey hostkey_verify = false Configure ironic-neutron-agent ------------------------------ To configure the baremetal neutron agent, edit the neutron configuration ``/etc/neutron/plugins/ml2/ironic_neutron_agent.ini`` file. Add an ``[ironic]`` section. For example: .. code-block:: ini [ironic] project_domain_name = Default project_name = service user_domain_name = Default password = password username = ironic auth_url = http://identity-server.example.com/identity auth_type = password os_region = RegionOne Start ironic-neutron-agent service ---------------------------------- To start the agent either run it from the command line like in the example below or add it to the init system. .. code-block:: shell $ ironic-neutron-agent \ --config-dir /etc/neutron \ --config-file /etc/neutron/plugins/ml2/ironic_neutron_agent.ini \ --log-file /var/log/neutron/ironic_neutron_agent.log You can create a systemd service file ``/etc/systemd/system/ironic-neutron-agent.service`` for ``ironic-neutron-agent`` for systemd based distributions. For example: .. code-block:: ini [Unit] Description=OpenStack Ironic Neutron Agent After=syslog.target network.target [Service] Type=simple User=neutron PermissionsStartOnly=true TimeoutStartSec=0 Restart=on-failure ExecStart=/usr/bin/ironic-neutron-agent --config-dir /etc/neutron --config-file /etc/neutron/plugins/ml2/ironic_neutron_agent.ini --log-file /var/log/neutron/ironic-neutron-agent.log PrivateTmp=true KillMode=process [Install] WantedBy=multi-user.target .. Note:: systemd service file may be already available if you are installing from package released by linux distributions. Enable and start the ``ironic-neutron-agent`` service: .. code-block:: shell $ sudo systemctl enable ironic-neutron-agent.service $ sudo systemctl start ironic-neutron-agent.service ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8359947 networking_baremetal-7.2.0/networking_baremetal/0000775000175000017500000000000015157004110020742 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/__init__.py0000664000175000017500000000124415157004031023056 0ustar00zuulzuul# -*- 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( 'networking_baremetal').version_string() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/_i18n.py0000664000175000017500000000140715157004031022236 0ustar00zuulzuul# # 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 oslo_i18n DOMAIN = "networking_baremetal" _translators = oslo_i18n.TranslatorFactory(domain=DOMAIN) # The primary translation function using the well-known name "_" _ = _translators.primary ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1773930567.837995 networking_baremetal-7.2.0/networking_baremetal/agent/0000775000175000017500000000000015157004110022040 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/agent/__init__.py0000664000175000017500000000000015157004031024141 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/agent/agent_config.py0000664000175000017500000003171315157004031025044 0ustar00zuulzuul# Copyright (c) 2026 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. """Configuration options for ironic-neutron-agent.""" from oslo_config import cfg # L2VNI trunk reconciliation options L2VNI_OPTS = [ cfg.BoolOpt( 'enable_l2vni_trunk_reconciliation', # TODO(TheJulia): We might want this to be False by default. default=True, help='Enable L2VNI trunk port reconciliation based on OVN ' 'ha_chassis_group membership. When enabled, the agent will ' 'automatically manage trunk subports for network nodes to ' 'ensure only required VLANs are trunked to each chassis. ' 'This feature creates anchor ports and trunk configurations ' 'to bridge overlay networks to physical network infrastructure.'), cfg.IntOpt( 'l2vni_reconciliation_interval', default=180, min=30, help='Interval in seconds between L2VNI trunk reconciliation runs. ' 'Default is 180 seconds (3 minutes).'), cfg.StrOpt( 'l2vni_network_nodes_config', default='/etc/neutron/l2vni_network_nodes.yaml', help='Path to YAML file containing network node trunk port ' 'configuration. Used as fallback when trunk configuration is ' 'not available from OVN LLDP data or Ironic. The file should ' 'define system_id or hostname, physical_network, and ' 'local_link_information for each network node. ' 'Network nodes can be identified by either system_id (OVN ' 'chassis UUID) or hostname (OVN chassis hostname) for easier ' 'configuration.'), cfg.BoolOpt( 'l2vni_auto_create_networks', default=True, help='Automatically create Neutron networks for ha_chassis_groups ' 'and subport anchors if they do not exist. These networks are ' 'used for metadata and modeling, not for passing traffic. If ' 'disabled, networks must be pre-created with names matching ' 'the expected patterns.'), cfg.StrOpt( 'l2vni_subport_anchor_network', default='l2vni-subport-anchor', help='Name of the shared network used for all trunk subports. This ' 'network is used to signal VLAN bindings to ML2 switch plugins ' 'and does not pass actual traffic. Will be auto-created if ' 'l2vni_auto_create_networks is enabled.'), cfg.StrOpt( 'l2vni_subport_anchor_network_type', default='geneve', choices=['geneve', 'vxlan'], help='Network type to use for L2VNI anchor networks (both subport ' 'anchor and ha_chassis_group networks). These networks are used ' 'for metadata and modeling only, not for passing traffic. Must ' 'match the overlay network type configured in your environment. ' 'If the specified type is not available, network creation will ' 'fail with an error rather than falling back to an alternative ' 'type.'), cfg.IntOpt( 'l2vni_startup_jitter_max', default=60, min=0, help='Maximum random delay in seconds to add to initial ' 'reconciliation start time. This prevents thundering herd ' 'issues when multiple agents restart simultaneously (e.g., ' 'post-upgrade). A value of 60 means each agent will start ' 'reconciliation within 0-60 seconds of startup.'), cfg.BoolOpt( 'enable_l2vni_trunk_reconciliation_events', default=True, help='Enable event-driven L2VNI trunk reconciliation. When enabled, ' 'the agent watches OVN Northbound database for localnet port ' 'creation and deletion events and triggers immediate ' 'reconciliation. This eliminates the stale IDL cache issue and ' 'provides sub-second reconciliation latency. Periodic ' 'reconciliation still runs as a safety net. Requires ' 'enable_l2vni_trunk_reconciliation to be enabled. If disabled, ' 'only periodic reconciliation will be used.'), cfg.ListOpt( 'ovn_nb_connection', default=None, help='OVN Northbound database connection string(s). For HA ' 'deployments, specify multiple comma-separated connection ' 'strings. Used to query ha_chassis_groups, logical switches, ' 'and router ports for L2VNI trunk reconciliation. If not ' 'specified, reads from [ovn] ovn_nb_connection (shared with ' 'Neutron ML2). Defaults to tcp:127.0.0.1:6641 if neither is ' 'configured.'), cfg.ListOpt( 'ovn_sb_connection', default=None, help='OVN Southbound database connection string(s). For HA ' 'deployments, specify multiple comma-separated connection ' 'strings. Used to query chassis information and LLDP data for ' 'L2VNI trunk reconciliation. If not specified, reads from ' '[ovn] ovn_sb_connection (shared with Neutron ML2). ' 'Defaults to tcp:127.0.0.1:6642 if neither is configured.'), cfg.IntOpt( 'ovn_ovsdb_timeout', default=None, help='Timeout in seconds for OVN OVSDB connections. If not ' 'specified, reads from [ovn] ovsdb_connection_timeout ' '(shared with Neutron ML2). Defaults to 180 if neither ' 'is configured.'), cfg.IntOpt( 'ironic_cache_ttl', default=3600, min=300, help='Time-to-live in seconds for cached Ironic node and port data. ' 'Each system_id entry is cached independently and expires after ' 'this duration from when it was fetched. This avoids thundering ' 'herd issues when multiple agents are running. A small amount of ' 'jitter (10-20%%) is automatically added to spread cache refresh ' 'times. Default is 3600 seconds (1 hour). Minimum is 300 seconds ' '(5 minutes) to avoid excessive API load.'), cfg.StrOpt( 'ironic_conductor_group', default=None, help='Ironic conductor group to filter nodes when querying for ' 'local_link_information data. This allows the agent to only ' 'query nodes managed by a specific conductor group, reducing API ' 'load in large deployments. If not specified, all nodes are ' 'queried.'), cfg.StrOpt( 'ironic_shard', default=None, help='Ironic shard to filter nodes when querying for ' 'local_link_information data. This allows the agent to only ' 'query nodes in a specific shard, reducing API load in large ' 'sharded deployments. If not specified, all nodes are queried.'), ] # HA chassis group alignment options BAREMETAL_AGENT_OPTS = [ cfg.BoolOpt( 'enable_ha_chassis_group_alignment', default=True, help='Enable HA chassis group alignment reconciliation for router ' 'ports on networks with baremetal external ports. This fixes ' 'Launchpad bug #1995078 where mismatched HA chassis group ' 'priorities between router gateway ports and baremetal external ' 'ports cause intermittent connectivity issues. When enabled, the ' 'agent ensures router ports use the same ha_chassis_group as ' 'baremetal external ports on the same network.'), cfg.IntOpt( 'ha_chassis_group_alignment_interval', default=600, min=60, help='Interval in seconds between HA chassis group alignment ' 'reconciliation runs. This controls how frequently the agent ' 'checks for and fixes mismatched HA chassis groups. Default is ' '600 seconds (10 minutes). Minimum is 60 seconds to avoid ' 'excessive API load.'), cfg.BoolOpt( 'limit_ha_chassis_group_alignment_to_recent_changes_only', default=True, help='When enabled, HA chassis group alignment only checks resources ' 'created or updated within the time window specified by ' 'ha_chassis_group_alignment_window. This reduces reconciliation ' 'overhead by focusing on recently created resources that may ' 'have mismatched HA chassis groups. When disabled, performs full ' 'reconciliation of all resources on each run, which is more ' 'thorough but has higher API and database load.'), cfg.IntOpt( 'ha_chassis_group_alignment_window', default=1200, min=0, help='Time window in seconds for checking recent resources when ' 'limit_ha_chassis_group_alignment_to_recent_changes_only is ' 'enabled. Default is 1200 seconds (20 minutes), which is 2x the ' 'default alignment interval. Resources created or updated within ' 'this window will be checked for HA chassis group alignment. ' 'Setting to 0 effectively disables windowing even if the limit ' 'flag is enabled.'), cfg.BoolOpt( 'enable_router_ha_binding', default=True, help='Enable router HA binding for router interface ports on ' 'networks with baremetal nodes. When enabled, the agent ' 'automatically binds router interface ports to the same HA ' 'chassis group as the network\'s external ports, enabling ' 'proper ARP resolution and connectivity between baremetal nodes ' 'and their router gateway on VLAN networks. This fixes ' 'Launchpad bug #2144458 where baremetal nodes experience ' 'persistent connectivity failures to their router gateway. ' 'Uses both event-driven binding (for immediate response) and ' 'periodic reconciliation (for edge cases).'), cfg.BoolOpt( 'enable_router_ha_binding_events', default=True, help='Enable event-driven router HA binding. When enabled, the agent ' 'responds immediately to HA chassis group creation events by ' 'binding router interface ports on the affected network. This ' 'provides instant connectivity when networks are created. ' 'Requires enable_router_ha_binding to be enabled. If disabled, ' 'only periodic reconciliation will be used, which may result in ' 'connectivity delays until the next reconciliation cycle.'), cfg.IntOpt( 'router_ha_binding_interval', default=600, min=60, help='Interval in seconds for periodic router HA binding ' 'reconciliation. This ensures router interface ports are ' 'bound to network HA chassis groups even if events are ' 'missed or routers are added after the fact. Default is ' '600 seconds (10 minutes). Minimum is 60 seconds.'), cfg.IntOpt( 'router_ha_binding_startup_jitter_max', default=60, min=0, help='Maximum random delay in seconds to add to initial ' 'reconciliation start time. This prevents thundering herd ' 'issues when multiple agents restart simultaneously (e.g., ' 'post-upgrade). A value of 60 means each agent will start ' 'reconciliation within 0-60 seconds of startup. Matches ' 'l2vni_startup_jitter_max for consistency.'), ] def register_agent_opts(conf): """Register all agent configuration options. :param conf: oslo_config.cfg.ConfigOpts instance """ conf.register_opts(L2VNI_OPTS, group='l2vni') conf.register_opts(BAREMETAL_AGENT_OPTS, group='baremetal_agent') # Legacy function names for backwards compatibility def register_l2vni_opts(conf): """Register L2VNI configuration options (deprecated). Prefer register_agent_opts() instead. :param conf: oslo_config.cfg.ConfigOpts instance """ conf.register_opts(L2VNI_OPTS, group='l2vni') def register_baremetal_agent_opts(conf): """Register baremetal agent configuration options (deprecated). Prefer register_agent_opts() instead. :param conf: oslo_config.cfg.ConfigOpts instance """ conf.register_opts(BAREMETAL_AGENT_OPTS, group='baremetal_agent') def list_opts(): """Return a list of oslo_config options for config generation. :returns: list of (group_name, options) tuples """ return [ ('l2vni', L2VNI_OPTS), ('baremetal_agent', BAREMETAL_AGENT_OPTS) ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/agent/ironic_neutron_agent.py0000664000175000017500000012537615157004031026645 0ustar00zuulzuul# Copyright 2017 Cisco Systems, 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 os import secrets import socket import sys import threading from urllib import parse as urlparse from neutron.agent import rpc as agent_rpc from neutron.common import config as common_config from neutron.common.ovn import utils as ovn_utils from neutron.conf.agent import common as neutron_agent_config try: from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf except ImportError: ovn_conf = None from neutron_lib.agent import topics from neutron_lib import constants as n_const from neutron_lib import context from openstack import exceptions as sdk_exc from oslo_config import cfg from oslo_log import log as logging import oslo_messaging from oslo_service import loopingcall from oslo_service import service from oslo_utils import timeutils from oslo_utils import uuidutils from ovsdbapp.backend.ovs_idl import idlutils from ovsdbapp import exceptions as ovs_exc from tooz import hashring from networking_baremetal.agent import agent_config from networking_baremetal.agent import l2vni_trunk_manager from networking_baremetal.agent import ovn_client from networking_baremetal.agent import ovn_events from networking_baremetal.agent import router_ha_binding from networking_baremetal import constants from networking_baremetal import ironic_client from networking_baremetal import neutron_client CONF = cfg.CONF LOG = logging.getLogger(__name__) CONF.import_group('AGENT', 'neutron.plugins.ml2.drivers.agent.config') def list_opts(): return [ ('agent', neutron_agent_config.AGENT_STATE_OPTS), ] + agent_config.list_opts() + neutron_client.list_opts() def _get_notification_transport_url(): url = urlparse.urlparse(CONF.transport_url) if (CONF.oslo_messaging_rabbit.amqp_auto_delete is False and not getattr(CONF.oslo_messaging_rabbit, 'rabbit_quorum_queue', None)): q = urlparse.parse_qs(url.query) q.update({'amqp_auto_delete': ['true']}) query = urlparse.urlencode({k: v[0] for k, v in q.items()}) url = url._replace(query=query) return urlparse.urlunparse(url) def _set_up_notifier(transport, uuid): return oslo_messaging.Notifier( transport, publisher_id='ironic-neutron-agent-' + uuid, driver='messagingv2', topics=['ironic-neutron-agent-member-manager']) def _set_up_listener(transport, agent_id): targets = [ oslo_messaging.Target(topic='ironic-neutron-agent-member-manager')] endpoints = [HashRingMemberManagerNotificationEndpoint()] return oslo_messaging.get_notification_listener( transport, targets, endpoints, pool=agent_id) class HashRingMemberManagerNotificationEndpoint(object): """Class variables members and hashring is shared by all instances""" filter_rule = oslo_messaging.NotificationFilter( publisher_id='^ironic-neutron-agent.*') members = [] hashring = hashring.HashRing([]) def info(self, ctxt, publisher_id, event_type, payload, metadata): timestamp = timeutils.utcnow_ts() # Add members or update timestamp for existing members if payload['id'] not in [x['id'] for x in self.members]: try: LOG.info('Adding member id %s on host %s to hashring.', payload['id'], payload['host']) self.hashring.add_node(payload['id']) self.members.append(payload) except Exception: LOG.exception('Failed to add member %s to hash ring!', payload['id']) else: for member in self.members: if payload['id'] == member['id']: member['timestamp'] = payload['timestamp'] # Remove members that have not checked in for a while for member in self.members: if (timestamp - member['timestamp']) > ( CONF.AGENT.report_interval * 3): try: LOG.info('Removing member %s on host %s from hashring.', member['id'], member['host']) self.hashring.remove_node(member['id']) self.members.remove(member) except Exception: LOG.exception('Failed to remove member %s from hash ring!', member['id']) return oslo_messaging.NotificationResult.HANDLED class BaremetalNeutronAgent(service.ServiceBase): def __init__(self): self.context = context.get_admin_context_without_session() self.agent_id = uuidutils.generate_uuid(dashed=True) LOG.info('Agent ID generated: %s', self.agent_id) self.agent_host = socket.gethostname() self.heartbeat = None self.notify_agents = None # Set up oslo_messaging notifier and listener to keep track of other # members # NOTE(hjensas): Override the control_exchange for the notification # transport to allow setting amqp_auto_delete = true. # TODO(hjensas): Remove this and override the exchange when setting up # the notifier once the fix for bug is available. # https://bugs.launchpad.net/oslo.messaging/+bug/1814797 CONF.set_override('control_exchange', 'ironic-neutron-agent') self.transport = oslo_messaging.get_notification_transport( CONF, url=_get_notification_transport_url()) self.notifier = _set_up_notifier(self.transport, self.agent_id) # Note(hjensas): We need to have listener consuming the non-pool queue. # See bug: https://bugs.launchpad.net/oslo.messaging/+bug/1814544 self.listener = _set_up_listener(self.transport, None) self.pool_listener = _set_up_listener(self.transport, '-'.join( ['ironic-neutron-agent-member-manager-pool', self.agent_id])) self.member_manager = HashRingMemberManagerNotificationEndpoint() self.state_rpc = agent_rpc.PluginReportStateAPI(topics.REPORTS) self.ironic_client = ironic_client.get_client() self.reported_nodes = {} # Initialize OVN connections and Neutron client if any OVN-based # features are enabled (L2VNI, router HA binding, or HA alignment) ovn_nb_idl = None ovn_sb_idl = None neutron = None if (CONF.l2vni.enable_l2vni_trunk_reconciliation or CONF.l2vni.enable_l2vni_trunk_reconciliation_events or CONF.baremetal_agent.enable_ha_chassis_group_alignment or CONF.baremetal_agent.enable_router_ha_binding): if CONF.l2vni.enable_l2vni_trunk_reconciliation: LOG.info('L2VNI trunk reconciliation enabled, initializing...') if CONF.l2vni.enable_l2vni_trunk_reconciliation_events: LOG.info('Event-driven L2VNI trunk reconciliation enabled') neutron = self._get_neutron_client() # Try to connect to OVN, but allow startup if OVN is unavailable # The reconciliation loop will retry connecting try: ovn_nb_idl = ovn_client.get_ovn_nb_idl() ovn_sb_idl = ovn_client.get_ovn_sb_idl() LOG.info('Successfully connected to OVN databases') except Exception: LOG.warning( 'Failed to connect to OVN databases during startup. ' 'This is expected if OVN is restarting or not yet ' 'available. The agent will retry connecting during ' 'reconciliation cycles.', exc_info=True) # L2VNI trunk reconciliation (optional feature) self.trunk_manager = None self.l2vni_reconcile = None self._l2vni_reconciliation_lock = threading.Lock() if (CONF.l2vni.enable_l2vni_trunk_reconciliation or CONF.l2vni.enable_l2vni_trunk_reconciliation_events): if CONF.l2vni.enable_l2vni_trunk_reconciliation: LOG.info('L2VNI trunk reconciliation enabled, initializing...') if CONF.l2vni.enable_l2vni_trunk_reconciliation_events: LOG.info('Event-driven L2VNI trunk reconciliation enabled') self.trunk_manager = ( l2vni_trunk_manager.L2VNITrunkManager( neutron_client=neutron, ovn_nb_idl=ovn_nb_idl, ovn_sb_idl=ovn_sb_idl, ironic_client=self.ironic_client, member_manager=self.member_manager, agent_id=self.agent_id )) LOG.info('L2VNI trunk manager initialized') # Register OVN event handlers for L2VNI reconciliation if (CONF.l2vni.enable_l2vni_trunk_reconciliation_events and self.trunk_manager.ovn_nb_idl): from networking_baremetal.agent import ovn_events # Use dedicated event-only connection for event watching # This connection has selective table registration to minimize # event notification overhead try: ovn_nb_event_idl = ovn_client.get_ovn_nb_event_idl() self._localnet_event = ovn_events.LocalnetPortEvent(self) LOG.info('Created LocalnetPortEvent with agent_id: %s', self._localnet_event.agent_id) ovn_nb_event_idl.idl.notify_handler.watch_event( self._localnet_event) LOG.info('Registered OVN event handler for L2VNI localnet ' 'port changes (CREATE/DELETE) using dedicated ' 'event-only connection') except Exception: LOG.exception( 'Failed to create OVN event-only connection, ' 'OVN event-driven reconciliation disabled. Using ' 'periodic reconciliation only.') elif CONF.l2vni.enable_l2vni_trunk_reconciliation_events: LOG.error('OVN connection not available, event-driven L2VNI ' 'trunk reconciliation disabled. Using periodic ' 'reconciliation only. The agent will retry OVN ' 'connection on subsequent reconciliation cycles.') # HA chassis group alignment reconciliation (optional feature) self.ha_alignment_reconcile = None self._ha_alignment_lock = threading.Lock() if CONF.baremetal_agent.enable_ha_chassis_group_alignment: LOG.info('HA chassis group alignment reconciliation enabled') # Router HA binding manager (event-driven + periodic reconciliation) self.router_ha_binding = None self.router_ha_reconcile = None if CONF.baremetal_agent.enable_router_ha_binding and ovn_nb_idl: LOG.info('Router HA binding enabled, initializing manager') if not neutron: neutron = self._get_neutron_client() self.router_ha_binding = router_ha_binding.RouterHABindingManager( neutron_client=neutron, ovn_nb_idl=ovn_nb_idl, member_manager=self.member_manager, agent_id=self.agent_id ) LOG.info('Router HA binding manager initialized') # Register OVN event handlers for enabled features self._register_ovn_event_handlers() LOG.info('Agent networking-baremetal initialized.') def _register_ovn_event_handlers(self): """Register OVN event handlers for L2VNI and router HA binding. Creates a dedicated event-only OVN connection and registers: - LocalnetPortEvent for L2VNI trunk reconciliation (if enabled) - HAChassisGroupNetworkEvent for router HA binding (if initialized) """ # Check if any event-driven features are enabled needs_l2vni_events = ( CONF.l2vni.enable_l2vni_trunk_reconciliation_events and self.trunk_manager) needs_router_ha_events = ( self.router_ha_binding is not None and CONF.baremetal_agent.enable_router_ha_binding_events) if not needs_l2vni_events and not needs_router_ha_events: return # Use dedicated event-only connection for event watching # This connection has selective table registration to minimize # event notification overhead try: ovn_nb_event_idl = ovn_client.get_ovn_nb_event_idl() # Register localnet port event for L2VNI trunk reconciliation if needs_l2vni_events: self._localnet_event = ovn_events.LocalnetPortEvent(self) LOG.info('Created LocalnetPortEvent with agent_id: %s', self._localnet_event.agent_id) ovn_nb_event_idl.idl.notify_handler.watch_event( self._localnet_event) LOG.info('Registered OVN event handler for L2VNI localnet ' 'port changes (CREATE/DELETE) using dedicated ' 'event-only connection') # Register HA chassis group event for router HA binding if needs_router_ha_events: self._ha_chassis_group_event = ( ovn_events.HAChassisGroupNetworkEvent(self)) LOG.info('Created HAChassisGroupNetworkEvent with ' 'agent_id: %s', self._ha_chassis_group_event.agent_id) ovn_nb_event_idl.idl.notify_handler.watch_event( self._ha_chassis_group_event) LOG.info('Registered OVN event handler for HA chassis ' 'group changes (CREATE/UPDATE) using dedicated ' 'event-only connection') except Exception: LOG.exception( 'Failed to create OVN event-only connection, ' 'OVN event-driven reconciliation disabled. Using ' 'periodic reconciliation only.') def start(self): LOG.info('Starting agent networking-baremetal.') cfg.CONF.log_opt_values(LOG, logging.INFO) self.pool_listener.start() self.listener.start() self.notify_agents = loopingcall.FixedIntervalLoopingCall( self._notify_peer_agents) self.notify_agents.start(interval=(CONF.AGENT.report_interval / 3)) self.heartbeat = loopingcall.FixedIntervalLoopingCall( self._report_state) self.heartbeat.start(interval=CONF.AGENT.report_interval, initial_delay=CONF.AGENT.report_interval) self.cleanup_stale_agents() # Start L2VNI trunk reconciliation loop if periodic reconciliation # is enabled (event-driven reconciliation works without the loop) if self.trunk_manager and CONF.l2vni.enable_l2vni_trunk_reconciliation: # Add random jitter to prevent thundering herd on restart # First run happens after jitter only, subsequent runs at interval jitter = secrets.randbelow( CONF.l2vni.l2vni_startup_jitter_max + 1) self.l2vni_reconcile = loopingcall.FixedIntervalLoopingCall( self._reconcile_l2vni_trunks) self.l2vni_reconcile.start( interval=CONF.l2vni.l2vni_reconciliation_interval, initial_delay=jitter) LOG.info('Started L2VNI trunk reconciliation loop ' '(interval: %ds, first run in %ds)', CONF.l2vni.l2vni_reconciliation_interval, jitter) # Start HA chassis group alignment reconciliation if enabled if CONF.baremetal_agent.enable_ha_chassis_group_alignment: # Add random jitter to prevent thundering herd on restart # NOTE: Using pseudo-random is acceptable for jitter (S311) jitter = secrets.randbelow( CONF.baremetal_agent.ha_chassis_group_alignment_interval + 1) self.ha_alignment_reconcile = loopingcall.FixedIntervalLoopingCall( self._reconcile_ha_chassis_group_alignment) self.ha_alignment_reconcile.start( interval=CONF.baremetal_agent .ha_chassis_group_alignment_interval, initial_delay=jitter) LOG.info('Started HA chassis group alignment reconciliation loop ' '(interval: %ds, first run in %ds)', CONF.baremetal_agent .ha_chassis_group_alignment_interval, jitter) # Start router HA binding reconciliation if manager is initialized # This catches: # - Routers added to networks after HA chassis group exists # - Missed events or race conditions # - Any edge cases not covered by events if self.router_ha_binding: jitter = secrets.randbelow( CONF.baremetal_agent.router_ha_binding_startup_jitter_max + 1) self.router_ha_reconcile = loopingcall.FixedIntervalLoopingCall( self._reconcile_router_ha_binding) self.router_ha_reconcile.start( interval=CONF.baremetal_agent.router_ha_binding_interval, initial_delay=jitter) LOG.info('Started router HA binding reconciliation loop ' '(interval: %ds, first run in %ds)', CONF.baremetal_agent.router_ha_binding_interval, jitter) def stop(self, failure=False): LOG.info('Stopping agent networking-baremetal.') if self.heartbeat: self.heartbeat.stop() if self.notify_agents: self.notify_agents.stop() if self.l2vni_reconcile: self.l2vni_reconcile.stop() LOG.info('Stopped L2VNI trunk reconciliation loop') if self.ha_alignment_reconcile: self.ha_alignment_reconcile.stop() LOG.info('Stopped HA chassis group alignment reconciliation loop') if self.router_ha_reconcile: self.router_ha_reconcile.stop() LOG.info('Stopped router HA binding reconciliation loop') self.listener.stop() self.pool_listener.stop() self.listener.wait() self.pool_listener.wait() if failure: # This will generate a SIGABORT for the process which forces it # to exit, which seems cleaner to force the process to exit # than os.exit and avoids threading constraints. os.abort() def reset(self): LOG.info('Resetting agent networking-baremetal.') if self.heartbeat: self.heartbeat.stop() if self.notify_agents: self.notify_agents.stop() self.listener.stop() self.pool_listener.stop() self.listener.wait() self.pool_listener.wait() def wait(self): pass def _notify_peer_agents(self): try: self.notifier.info({ 'ironic-neutron-agent': 'heartbeat'}, 'ironic-neutron-agent-member-manager', {'id': self.agent_id, 'host': self.agent_host, 'timestamp': timeutils.utcnow_ts()}) except Exception: LOG.exception('Failed to send hash ring membership heartbeat!') def get_template_node_state(self, node_uuid): return { 'binary': constants.BAREMETAL_BINARY, 'host': node_uuid, 'topic': n_const.L2_AGENT_TOPIC, 'configurations': { 'bridge_mappings': {}, 'log_agent_heartbeats': CONF.AGENT.log_agent_heartbeats, }, 'start_flag': False, 'agent_type': constants.BAREMETAL_AGENT_TYPE, 'action': 'update'} def _report_state(self): node_states = {} conductor_groups_config = getattr(CONF, 'conductor_groups', None) conductor_groups = getattr( conductor_groups_config, 'conductor_groups', None) or [] if conductor_groups: LOG.info("Using conductor groups filter: %s", conductor_groups) ironic_ports = self.ironic_client.ports( details=True, conductor_groups=conductor_groups) # NOTE: the above calls returns a generator, so we need to handle # exceptions that happen just before the first loop iteration, when # the actual request to ironic happens try: for port in ironic_ports: node = port.node_id if (self.agent_id not in self.member_manager.hashring[node.encode('utf-8')]): continue template_node_state = self.get_template_node_state(node) node_states.setdefault(node, template_node_state) mapping = node_states[ node]["configurations"]["bridge_mappings"] if port.physical_network is not None: mapping[port.physical_network] = "yes" except sdk_exc.OpenStackCloudException: LOG.exception("Failed to get ironic ports data! " "Not reporting state.") try: # Replace the client, just to be on the safe side in # the event there was some sort of hard/breaking failure. self.ironic_client = ironic_client.get_client() except Exception: # Failed to re-launch a new client, aborting. self.stop(failure=True) return abort_operation = False for state in node_states.values(): # If the node was not previously reported with current # configuration set the start_flag True. # NOTE(TheJulia) reported_nodes is an internal list of nodes # we *have* updated. if not state['configurations'] == self.reported_nodes.get( state['host']): state.update({'start_flag': True}) LOG.info('Reporting state for host agent %s with new ' 'configuration: %s', state['host'], state['configurations']) try: LOG.debug('Reporting state for host: %s with configuration: ' '%s', state['host'], state['configurations']) self.state_rpc.report_state(self.context, state) except AttributeError: # This means the server does not support report_state LOG.exception("Neutron server does not support state report. " "State report for this agent will be disabled.") # Don't continue reporting the remaining agents in this case. abort_operation = True break except Exception: LOG.exception("Failed reporting state!") # Don't continue reporting the remaining nodes if one failed. return self.reported_nodes.update( {state['host']: state['configurations']}) # Identify nodes that are no longer present in Ironic by subtracting # the keys of `node_states` from the keys of `reported_nodes`. Then # delete agents for nodes that are no longer present. deleted_nodes = self.reported_nodes.keys() - node_states.keys() deleted_agents = self._delete_agents(deleted_nodes) for node in deleted_agents: self.reported_nodes.pop(node) if abort_operation: # We don't expect the agent to work, and as such we should call # stop so the program unwinds and begins to exit. self.stop(failure=True) def _get_down_agents(self): """Retrieves a list of inactive Baremetal agents. Fetch a list of inactive Baremetal agents. It interacts with the state_rpc object to call the 'get_agents' method, which retrieves agents based on the provided parameters. :returns: (list) Inactive Baremetal agents. """ down_bm_agents = [] try: down_bm_agents = self.state_rpc.get_agents( self.context, agent_type=constants.BAREMETAL_AGENT_TYPE, is_active=False) except oslo_messaging.NoSuchMethod: LOG.warning("Neutron server doesn't support " "`get_agents` endpoint.") return down_bm_agents def _get_nodes_not_found(self, down_bm_agents): """Identifies nodes that are not found in the Ironic The method iterates over each agent in the 'down_bm_agents' list. For each agent, it attempts to retrieve the corresponding node using the Ironic client's 'get_node' method. If the node is not found the node is appended to the 'nodes_not_found' list. :param down_bm_agents: (list) Agents that are down in Neutron. :return: (list) Nodes that are not found in Ironic. """ nodes_not_found = [] for agent in down_bm_agents: node = agent['host'] try: self.ironic_client.get_node(node) except sdk_exc.NotFoundException: nodes_not_found.append(node) return nodes_not_found def _delete_agents(self, nodes, log=True): """Delete agents for nodes that are not found in ironic Clean up agent records in neutron for ironic nodes that have been removed from the system. :param nodes_not_found: (list) Nodes that are not found in Ironic. :log: (bool) Log the actions taken. :return: (list) Agents that have been deleted in Neutron. """ deleted_agents = [] for node in nodes: if log: LOG.info('Removing agent for host: %s', node) try: kwargs = {'host': node, 'agent_type': constants.BAREMETAL_AGENT_TYPE} self.state_rpc.delete_agent(self.context, **kwargs) deleted_agents.append(node) except oslo_messaging.NoSuchMethod: LOG.warning("Neutron server doesn't support " "`delete_agent` endpoint.") break return deleted_agents def cleanup_stale_agents(self): """Cleans up stale baremetal agents This method identifies baremetal agents that are marked as inactive in the Neutron server and are not associated with any nodes in Ironic. It then deletes these stale agents. """ down_bm_agents = self._get_down_agents() nodes_not_found = self._get_nodes_not_found(down_bm_agents) deleted_agents = self._delete_agents(nodes_not_found, log=False) if deleted_agents: LOG.info("Stale baremetal agent for hosts was removed: %s", ", ".join(deleted_agents)) def _get_neutron_client(self): """Get Neutron client using OpenStack SDK. Uses Neutron-specific credentials from [neutron] section if configured, otherwise falls back to [ironic] section credentials for backwards compatibility. :returns: OpenStack SDK Connection object for accessing network APIs """ return neutron_client.get_client() def _reconcile_single_vlan_blocking( self, network_id, physnet, vlan_id, action): """Targeted reconciliation for a single VLAN (blocking lock). Called by OVN event handlers. Uses blocking lock acquisition to ensure the event is processed (unlike periodic reconciliation which skips if locked). :param network_id: Neutron network UUID :param physnet: Physical network name :param vlan_id: VLAN ID to add or remove :param action: 'add' or 'remove' """ LOG.debug("Acquiring lock for targeted VLAN reconciliation...") with self._l2vni_reconciliation_lock: LOG.debug("Lock acquired, processing targeted reconciliation for " "VLAN %d on physnet %s", vlan_id, physnet) try: self.trunk_manager.reconcile_single_vlan( network_id, physnet, vlan_id, action) except Exception: LOG.exception("Failed targeted reconciliation for VLAN %d", vlan_id) def _reconcile_l2vni_trunks(self): """Periodic L2VNI trunk reconciliation""" if not self._l2vni_reconciliation_lock.acquire(blocking=False): LOG.debug("L2VNI reconciliation already in progress, skipping") return try: LOG.debug("L2VNI reconciliation triggered.") # Retry OVN connection if not established if (self.trunk_manager.ovn_nb_idl is None or self.trunk_manager.ovn_sb_idl is None): LOG.debug("OVN connection not established, attempting to " "connect...") try: if self.trunk_manager.ovn_nb_idl is None: self.trunk_manager.ovn_nb_idl = ( ovn_client.get_ovn_nb_idl()) LOG.info("Successfully connected to OVN Northbound " "database") if self.trunk_manager.ovn_sb_idl is None: self.trunk_manager.ovn_sb_idl = ( ovn_client.get_ovn_sb_idl()) LOG.info("Successfully connected to OVN Southbound " "database") except Exception: LOG.info("OVN databases not available, skipping L2VNI " "reconciliation cycle. Will retry on next cycle.") return self.trunk_manager.reconcile() LOG.debug("L2VNI trunk reconciliation completed.") except Exception: LOG.exception("Failed to reconcile L2VNI trunks") finally: self._l2vni_reconciliation_lock.release() def _reconcile_router_ha_binding(self): """Periodic router HA binding reconciliation. Ensures router interface ports on networks with HA chassis groups are bound to those groups. This catches edge cases such as: - Routers added to networks after HA chassis group exists - Missed events (agent down/restarting) - Race conditions or out-of-order event processing Fixes LP#2144458 by ensuring eventual consistency even if event-driven binding fails. """ if not self.router_ha_binding: return try: self.router_ha_binding.reconcile() except Exception: LOG.exception("Failed to reconcile router HA binding") def _reconcile_ha_chassis_group_alignment(self): """Periodic HA chassis group alignment reconciliation. This reconciliation ensures that router ports on networks with baremetal external ports use the same ha_chassis_group as those baremetal ports. This fixes LP#1995078 where mismatched priorities cause intermittent connectivity issues. """ if not self._ha_alignment_lock.acquire(blocking=False): LOG.debug("HA alignment reconciliation already in progress, " "skipping") return try: LOG.debug("HA chassis group alignment reconciliation triggered.") neutron = self._get_neutron_client() # Get OVN connection (reuse from trunk manager if available, # otherwise create new connection) ovn_nb_idl = None if self.trunk_manager and self.trunk_manager.ovn_nb_idl: ovn_nb_idl = self.trunk_manager.ovn_nb_idl else: try: ovn_nb_idl = ovn_client.get_ovn_nb_idl() except (ovs_exc.OvsdbAppException, RuntimeError): LOG.warning("Failed to connect to OVN Northbound " "database, skipping reconciliation cycle. " "Will retry on next cycle.", exc_info=True) return # Determine time window for filtering recent resources cutoff_time = None if (CONF.baremetal_agent .limit_ha_chassis_group_alignment_to_recent_changes_only): window = CONF.baremetal_agent.ha_chassis_group_alignment_window if window > 0: cutoff_time = timeutils.utcnow_ts() - window LOG.debug("Filtering to resources updated after %s " "(window: %ds)", cutoff_time, window) # Get all baremetal external ports from Neutron # device_owner='baremetal:none' indicates external baremetal ports filters = {'device_owner': constants.BAREMETAL_NONE} bm_ports = list(neutron.network.ports(**filters)) LOG.debug("Found %d baremetal external ports", len(bm_ports)) if not bm_ports: LOG.debug("No baremetal external ports found, nothing to do") return # Group ports by network networks_with_bm_ports = {} for port in bm_ports: network_id = port.network_id # Apply time window filtering if enabled if cutoff_time is not None: port_updated = timeutils.parse_isotime( port.updated_at).timestamp() if port_updated < cutoff_time: LOG.debug("Skipping port %s (updated %s, before " "cutoff %s)", port.id, port.updated_at, cutoff_time) continue # Check if this agent should handle this network via hash ring # Use network_id as the key for consistent hashing network_key = network_id.encode('utf-8') if self.agent_id not in self.member_manager.hashring[ network_key]: LOG.debug("Network %s not managed by this agent " "(hash ring)", network_id) continue if network_id not in networks_with_bm_ports: networks_with_bm_ports[network_id] = [] networks_with_bm_ports[network_id].append(port) LOG.debug("Processing %d networks with baremetal ports managed " "by this agent", len(networks_with_bm_ports)) # Process each network for network_id, ports in networks_with_bm_ports.items(): try: self._align_ha_chassis_group_for_network( network_id, ports, neutron, ovn_nb_idl) except (ovs_exc.OvsdbAppException, sdk_exc.OpenStackCloudException, RuntimeError): LOG.exception("Failed to align HA chassis group for " "network %s", network_id) LOG.debug("HA chassis group alignment reconciliation completed.") except (sdk_exc.OpenStackCloudException, ovs_exc.OvsdbAppException, ValueError, AttributeError): LOG.exception("Failed to reconcile HA chassis group alignment") finally: self._ha_alignment_lock.release() def _align_ha_chassis_group_for_network(self, network_id, bm_ports, neutron, ovn_nb_idl): """Align HA chassis groups for a specific network. :param network_id: Neutron network UUID :param bm_ports: List of baremetal external ports on this network :param neutron: Neutron client :param ovn_nb_idl: OVN Northbound IDL connection """ LOG.debug("Aligning HA chassis group for network %s with %d " "baremetal ports", network_id, len(bm_ports)) # Find the HA chassis group used by baremetal ports via OVN # All baremetal ports on the same network should use the same # HA chassis group bm_ha_chassis_group = None found_any_lsp = False for port in bm_ports: try: lsp = ovn_nb_idl.lsp_get( ovn_utils.ovn_name(port.id)).execute(check_error=True) except idlutils.RowNotFound: LOG.debug("Baremetal port %s not found in OVN (may not be " "bound to OVN driver), skipping", port.id) continue except (ovs_exc.OvsdbAppException, RuntimeError, AttributeError): LOG.debug("Could not get HA chassis group from port %s", port.id, exc_info=True) continue found_any_lsp = True if hasattr(lsp, 'ha_chassis_group'): ha_group = lsp.ha_chassis_group if ha_group: bm_ha_chassis_group = ha_group[0] if isinstance( ha_group, list) else ha_group LOG.debug("Found HA chassis group %s from port %s", bm_ha_chassis_group, port.id) break if not found_any_lsp: LOG.debug("Could not find any baremetal ports in OVN for " "network %s, skipping HA chassis group alignment", network_id) return if not bm_ha_chassis_group: LOG.debug("Baremetal ports on network %s have no HA chassis " "group set, nothing to align router ports to", network_id) return LOG.debug("Target HA chassis group for network %s: %s", network_id, bm_ha_chassis_group) # Find all router ports on this network router_ports = list(neutron.network.ports( network_id=network_id, device_owner=n_const.DEVICE_OWNER_ROUTER_INTF)) if not router_ports: LOG.debug("No router ports found on network %s", network_id) return LOG.debug("Found %d router ports on network %s", len(router_ports), network_id) # Check and update each router port's HA chassis group for rport in router_ports: try: lrp_name = ovn_utils.ovn_lrouter_port_name(rport.id) lrp = ovn_nb_idl.lrp_get(lrp_name).execute(check_error=True) except idlutils.RowNotFound: LOG.debug("Logical router port %s not found in OVN", lrp_name) continue except (ovs_exc.OvsdbAppException, RuntimeError, AttributeError): LOG.debug("Could not get router port %s", lrp_name, exc_info=True) continue try: current_ha_group = None if hasattr(lrp, 'ha_chassis_group'): ha_group = lrp.ha_chassis_group if ha_group: current_ha_group = ha_group[0] if isinstance( ha_group, list) else ha_group if current_ha_group == bm_ha_chassis_group: LOG.debug("Router port %s already has correct HA " "chassis group %s", rport.id, bm_ha_chassis_group) continue # Update the router port's HA chassis group LOG.info("Updating router port %s HA chassis group from " "%s to %s (network %s)", rport.id, current_ha_group, bm_ha_chassis_group, network_id) ovn_nb_idl.lrp_set_ha_chassis_group( lrp_name, bm_ha_chassis_group).execute(check_error=True) LOG.info("Successfully updated router port %s HA chassis " "group", rport.id) except (ovs_exc.OvsdbAppException, RuntimeError, AttributeError): LOG.exception("Failed to update HA chassis group for " "router port %s", rport.id) def _unregiser_deprecated_opts(): CONF.reset() CONF.unregister_opts( [CONF._groups[ironic_client.IRONIC_GROUP]._opts[opt]['opt'] for opt in ironic_client._deprecated_opts], group=ironic_client.IRONIC_GROUP) def main(): common_config.register_common_config_options() # Register agent configuration options (L2VNI and baremetal agent) agent_config.register_agent_opts(CONF) # Register Neutron client configuration options neutron_client.get_session(neutron_client.NEUTRON_GROUP) # Register Neutron OVN options so we can read [ovn] section as fallback # for L2VNI OVN connection settings if ovn_conf is not None: try: ovn_conf.register_opts() except Exception as e: # If neutron OVN config can't be registered, L2VNI will use # its own config or defaults LOG.debug('Could not register Neutron OVN config options: %s', e) # TODO(hjensas): Imports from neutron in ironic_neutron_agent registers the # client options. We need to unregister the options we are deprecating # first to avoid DuplicateOptError. Remove this when dropping deprecations. _unregiser_deprecated_opts() # Add ML2 OVN config file to search path for OVN connection settings # This allows L2VNI to read Neutron's OVN configuration if available # Only include files that actually exist to avoid startup failures candidate_config_files = [ '/etc/neutron/neutron.conf', '/etc/neutron/plugins/ml2/ml2_conf.ini', '/etc/neutron/plugins/ml2/ovn_agent.ini' ] default_config_files = [f for f in candidate_config_files if os.path.exists(f)] if len(default_config_files) != len(candidate_config_files): missing = set(candidate_config_files) - set(default_config_files) LOG.warning('Config files not found (skipping): %s', ', '.join(missing)) common_config.init(sys.argv[1:], default_config_files=default_config_files) common_config.setup_logging() agent = BaremetalNeutronAgent() # Use service.Launcher for single-process execution to ensure hash ring # class variables are shared across all threads. service.launch() spawns # worker processes via fork, which isolates class variables and breaks # hash ring synchronization. See bug LP#2144384 launcher = service.Launcher(cfg.CONF, restart_method='mutate') launcher.launch_service(agent) launcher.wait() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/agent/l2vni_trunk_manager.py0000664000175000017500000020720715157004031026373 0ustar00zuulzuul# Copyright (c) 2026 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. """L2VNI Trunk Reconciliation Manager. Manages trunk ports and subports for OVN network nodes to ensure only required VLANs are trunked to each chassis based on ha_chassis_group membership and network requirements. Architecture: - One Neutron network per OVN ha_chassis_group for anchor port modeling - One shared subport anchor network for all trunk subports - Anchor ports (trunk parents) attach to ha_chassis_group networks - Subports signal VLAN bindings to ML2 switch plugins - Stateless reconciliation based on current OVN/Neutron state """ import random import time import yaml from neutron.common.ovn import utils as ovn_utils from neutron_lib.api.definitions import portbindings from neutron_lib import constants as n_const from openstack import exceptions as sdkexc from oslo_config import cfg from oslo_log import log as logging from ovsdbapp import exceptions as ovs_exc CONF = cfg.CONF LOG = logging.getLogger(__name__) # Device owner types for L2VNI infrastructure ports # TODO(TheJulia): Propose these and move these to neutron-lib # if they accept them. # # Terminology clarification: # - ANCHOR ports = trunk parent ports (attach to HA group networks) # - SUBPORT ports = trunk child ports (attach to subport anchor network) # - Despite confusing naming, "subport anchor network" hosts SUBPORTS, # while ANCHOR ports attach to separate HA group networks DEVICE_OWNER_L2VNI_ANCHOR = 'baremetal:l2vni_anchor' DEVICE_OWNER_L2VNI_SUBPORT = 'baremetal:l2vni_subport' DEVICE_OWNER_L2VNI_NETWORK = 'baremetal:l2vni_network' def _get_trunk_name(system_id, physnet): """Generate consistent trunk name. :param system_id: OVN chassis system-id :param physnet: Physical network name :returns: Trunk name string """ return f"l2vni-trunk-{system_id}-{physnet}" def _get_anchor_port_name(system_id, physnet): """Generate consistent anchor port name. :param system_id: OVN chassis system-id :param physnet: Physical network name :returns: Port name string """ return f"l2vni-anchor-{system_id}-{physnet}" def _get_subport_name(system_id, physnet, vlan_id): """Generate consistent subport name. :param system_id: OVN chassis system-id :param physnet: Physical network name :param vlan_id: VLAN ID for segmentation :returns: Port name string """ return f"l2vni-subport-{system_id}-{physnet}-vlan{vlan_id}" class L2VNITrunkManager: """Manages L2VNI trunk ports and subports for network nodes.""" def __init__(self, neutron_client, ovn_nb_idl, ovn_sb_idl, ironic_client, member_manager=None, agent_id=None): """Initialize L2VNI trunk manager. :param neutron_client: Neutron client for trunk/port operations :param ovn_nb_idl: OVN Northbound IDL connection :param ovn_sb_idl: OVN Southbound IDL connection :param ironic_client: Ironic client for node/port data :param member_manager: Member manager for hash ring filtering (optional, for distributed work) :param agent_id: This agent's ID for hash ring membership checks """ self.neutron = neutron_client self.ovn_nb_idl = ovn_nb_idl self.ovn_sb_idl = ovn_sb_idl self.ironic = ironic_client self.member_manager = member_manager self.agent_id = agent_id self._config_cache = None # Per-record cache: system_id -> data # Thread safety: This cache is only accessed from reconcile() and its # helper methods. The reconcile() method is protected by # _l2vni_reconciliation_lock in the agent (see # ironic_neutron_agent.py:_reconcile_l2vni_trunks), which prevents # concurrent execution. Therefore, no additional locking is needed. self._ironic_cache = {} def _should_manage_chassis(self, system_id): """Check if this agent should manage this chassis based on hash ring. :param system_id: OVN chassis system-id :returns: bool - True if this agent should manage the chassis """ if not self.member_manager or not self.agent_id: # No hash ring - manage all chassis (single agent mode) return True # Check if this agent is responsible for this chassis # Hashring requires bytes for md5 hashing return (self.agent_id in self.member_manager.hashring[system_id.encode('utf-8')]) def reconcile(self): """Main reconciliation entry point. Performs stateless reconciliation of trunk infrastructure: 1. Ensure ha_chassis_group networks exist 2. Ensure subport anchor network exists 3. Discover/create trunk ports for chassis 4. Calculate required VLANs per chassis from OVN state 5. Reconcile subports to match requirements 6. Clean up unused infrastructure """ try: # Skip reconciliation if OVN connections are not available if self.ovn_nb_idl is None or self.ovn_sb_idl is None: LOG.debug("OVN connections not available, skipping " "reconciliation") return # Ensure infrastructure networks exist self._ensure_infrastructure_networks() # Build trunk map: {(system_id, physnet): trunk_id} trunk_map = self._discover_trunks() # Calculate required VLANs with VNI info: # {(system_id, physnet): {vlan_id: vni}} required_vlans = self._calculate_required_vlans() # Reconcile subports self._reconcile_subports(trunk_map, required_vlans) # Clean up unused infrastructure self._cleanup_unused_infrastructure() except (sdkexc.SDKException, AttributeError, KeyError, TypeError, ValueError, IndexError): LOG.exception("Failed to reconcile L2VNI trunks") # Don't re-raise - let reconciliation continue on next interval except Exception: # This broad exception handler is intentional. The reconcile() # method is called periodically by the agent and must be resilient # to any unexpected errors to prevent the reconciliation loop from # crashing. The specific exception handlers above catch known error # types; this catches anything else that slips through. LOG.exception("Unexpected error during L2VNI trunk " "reconciliation.") # Don't re-raise - let reconciliation continue on next interval def reconcile_single_vlan(self, network_id, physnet, vlan_id, action='add'): """Targeted reconciliation for a single VLAN. Called by OVN event handlers when a specific localnet port is created or deleted. Much faster than full reconciliation because we already know which VLAN to add/remove. :param network_id: Neutron network UUID (tenant network with VLAN) :param physnet: Physical network name :param vlan_id: VLAN ID to add or remove :param action: 'add' for CREATE events, 'remove' for DELETE events """ try: LOG.debug("Starting targeted reconciliation: %s VLAN %d on " "physnet %s for network %s", action, vlan_id, physnet, network_id) # Skip reconciliation if OVN connections are not available if self.ovn_nb_idl is None or self.ovn_sb_idl is None: LOG.error("OVN connections not available, cannot perform " "targeted reconciliation") return # Ensure infrastructure networks exist (creates if missing) self._ensure_infrastructure_networks() # Get subport anchor network anchor_network_id = self._get_subport_anchor_network_id() if not anchor_network_id: LOG.error("Cannot reconcile VLAN without anchor network") return # Get VNI for this network if adding vni = None if action == 'add': vni = self._get_vni_for_network(network_id) if not vni: LOG.warning( "No VNI found for network %s, subport will " "not have L2VNI mapping configured", network_id) # Find all chassis that have this physnet chassis_set = self._get_all_chassis_with_physnet(physnet) if not chassis_set: LOG.debug("No chassis found with physnet %s", physnet) return # For each chassis, add or remove the subport for system_id in chassis_set: if not self._should_manage_chassis(system_id): continue # Ensure trunk exists trunk_id = self._find_or_create_trunk(system_id, physnet) if not trunk_id: LOG.error("Cannot reconcile VLAN %d for chassis %s: " "trunk not found/created", vlan_id, system_id) continue # Add or remove this specific VLAN if action == 'add': self._ensure_single_subport( trunk_id, system_id, physnet, vlan_id, anchor_network_id, vni=vni) elif action == 'remove': self._remove_single_subport( trunk_id, system_id, physnet, vlan_id) LOG.info("Completed targeted reconciliation for %s VLAN %d " "(VNI: %s) on physnet %s", action, vlan_id, vni if vni else 'none', physnet) except (sdkexc.SDKException, ovs_exc.OvsdbAppException): LOG.exception( "Failed targeted reconciliation for VLAN %d on " "physnet %s, will retry on next periodic reconciliation", vlan_id, physnet) def _ensure_infrastructure_networks(self): """Ensure ha_chassis_group and subport anchor networks exist. Infrastructure networks are metadata/modeling networks that don't pass actual traffic but are used for trunk port management: 1. Subport Anchor Network (singular, shared): - All trunk SUBPORTS attach to this network - Used to signal VLAN bindings to ML2 switch plugins - Config: l2vni_subport_anchor_network 2. HA Chassis Group Networks (multiple, one per group): - Trunk ANCHOR PORTS (parents) attach to these networks - One network created per OVN ha_chassis_group - Named: l2vni-ha-group-{group_name} Note: Despite the name "subport anchor network", it hosts subports, NOT anchor ports. Anchor ports attach to HA group networks. """ if not CONF.l2vni.l2vni_auto_create_networks: LOG.debug("Auto-creation of L2VNI networks is disabled") return # Ensure subport anchor network exists (for trunk subports) self._ensure_subport_anchor_network() # Ensure network per ha_chassis_group (for trunk anchor ports), # but only for groups that contain chassis we manage for ha_group in self._get_ha_chassis_groups(): # Check if this group contains any chassis we manage if self._ha_group_has_managed_chassis(ha_group): self._ensure_ha_group_network(ha_group) def _ha_group_has_managed_chassis(self, ha_group): """Check if HA group contains any chassis this agent manages. :param ha_group: OVN HA_Chassis_Group row :returns: bool """ for ha_chassis in ha_group.ha_chassis: chassis = self._get_chassis_by_name(ha_chassis.chassis_name) if chassis: # Chassis name IS the system-id if self._should_manage_chassis(chassis.name): return True return False def _ensure_network(self, network_name, description): """Ensure a network exists, creating if necessary. :param network_name: Name of the network :param description: Description for the network :returns: Network ID :raises: Exception if network creation fails """ # Check if network exists networks = self.neutron.network.networks(name=network_name) for network in networks: return network.id # Create network with configured type network_type = CONF.l2vni.l2vni_subport_anchor_network_type LOG.info("Creating L2VNI network '%s' with type '%s'", network_name, network_type) try: network = self.neutron.network.create_network( name=network_name, description=description, admin_state_up=True, shared=False, is_default=False, provider_network_type=network_type ) LOG.info("Created L2VNI network '%s' (%s) with type '%s'", network_name, network.id, network_type) return network.id except sdkexc.BadRequestException as e: LOG.error( "Failed to create L2VNI network '%s' with type '%s'. This " "indicates a misconfiguration - the requested network type " "is not available in your environment. Please verify that " "'%s' is configured in ml2_type_drivers and enabled in your " "Neutron deployment. Error: %s", network_name, network_type, network_type, e) raise def _ensure_subport_anchor_network(self): """Ensure the shared subport anchor network exists. This network is where all trunk SUBPORTS are attached. It is used to signal VLAN bindings to ML2 switch plugins but does not pass actual traffic. Note: Despite the name, this network does NOT host "anchor ports" (trunk parents). Those attach to HA chassis group networks. :returns: Network ID """ network_name = CONF.l2vni.l2vni_subport_anchor_network return self._ensure_network( network_name, 'L2VNI subport anchor network for VLAN signaling' ) def _ensure_ha_group_network(self, ha_group): """Ensure network exists for an ha_chassis_group. Creates a network for modeling the ha_chassis_group. This network is where trunk ANCHOR PORTS (trunk parents) are attached. It does not pass actual traffic, only used for modeling/metadata. Note: Trunk SUBPORTS attach to the separate "subport anchor network", not to these HA group networks. :param ha_group: OVN HA_Chassis_Group row :returns: Network ID """ network_name = f"l2vni-ha-group-{ha_group.name}" description = f'L2VNI network for ha_chassis_group {ha_group.name}' return self._ensure_network(network_name, description) def _get_ha_chassis_groups(self): """Get all ha_chassis_groups from OVN. :returns: List of OVN HA_Chassis_Group rows """ try: if not hasattr(self.ovn_nb_idl, 'tables'): LOG.warning("OVN NB IDL not available") return [] ha_groups = [] # Access IDL tables: tables['TableName'].rows.values() if 'HA_Chassis_Group' in self.ovn_nb_idl.tables: table = self.ovn_nb_idl.tables['HA_Chassis_Group'] for row in table.rows.values(): ha_groups.append(row) return ha_groups except (AttributeError, KeyError): LOG.exception("Failed to get ha_chassis_groups from OVN") return [] def _discover_trunks(self): """Discover existing trunk ports for network nodes. Builds a map of (chassis_system_id, physnet) -> trunk_id by: 1. Finding all chassis in ha_chassis_groups 2. Getting physnets from bridge-mappings 3. Looking up or creating trunk ports :returns: dict {(system_id, physnet): trunk_id} """ trunk_map = {} # Get all chassis in ha_chassis_groups chassis_physnets = self._get_chassis_physnets() for (system_id, physnet) in chassis_physnets: # Skip chassis this agent doesn't manage (hash ring filtering) if not self._should_manage_chassis(system_id): continue trunk_id = self._find_or_create_trunk(system_id, physnet) if trunk_id: trunk_map[(system_id, physnet)] = trunk_id return trunk_map def _get_chassis_physnets(self): """Get all (chassis_system_id, physnet) combinations. Finds chassis in ha_chassis_groups and extracts their physical networks from bridge-mappings. :returns: set of (system_id, physnet) tuples """ chassis_physnets = set() try: # Get all chassis referenced in ha_chassis_groups chassis_names = set() for ha_group in self._get_ha_chassis_groups(): for ha_chassis in ha_group.ha_chassis: chassis_names.add(ha_chassis.chassis_name) # Get physnets for each chassis if not hasattr(self.ovn_sb_idl, 'tables'): LOG.warning("OVN SB IDL not available") return chassis_physnets if 'Chassis' in self.ovn_sb_idl.tables: for chassis in self.ovn_sb_idl.tables['Chassis'].rows.values(): if chassis.name not in chassis_names: continue # The chassis name IS the system-id (UUID) system_id = chassis.name bridge_mappings = chassis.other_config.get( 'ovn-bridge-mappings', '') physnets = self._parse_bridge_mappings(bridge_mappings) for physnet in physnets: chassis_physnets.add((system_id, physnet)) except (AttributeError, KeyError): LOG.exception("Failed to get chassis physnets") return chassis_physnets def _parse_bridge_mappings(self, bridge_mappings_str): """Parse OVN bridge-mappings string to extract physnets. :param bridge_mappings_str: String like "physnet1:br-ex,physnet2:br2" :returns: List of physical network names """ physnets = [] if not bridge_mappings_str: return physnets for mapping in bridge_mappings_str.split(','): if ':' not in mapping: continue physnet = mapping.split(':')[0].strip() if physnet: physnets.append(physnet) return physnets def _find_or_create_trunk(self, system_id, physnet): """Find existing trunk or create new one. :param system_id: OVN chassis system-id :param physnet: Physical network name :returns: Trunk ID or None """ # Always reconcile anchor port first (creates or updates existing) anchor_port_id = self._find_or_create_anchor_port(system_id, physnet) if not anchor_port_id: LOG.warning("Cannot find or create anchor port for " "chassis %s physnet %s", system_id, physnet) return None # Try to find existing trunk trunk_name = _get_trunk_name(system_id, physnet) trunks = self.neutron.network.trunks(name=trunk_name) for trunk in trunks: return trunk.id # Create trunk try: LOG.debug("Creating trunk %s for chassis %s physnet %s", trunk_name, system_id, physnet) trunk = self.neutron.network.create_trunk( name=trunk_name, description=f'trunk for chassis {system_id} ' f'on {physnet}', port_id=anchor_port_id, admin_state_up=True ) LOG.debug("Created trunk %s", trunk.id) return trunk.id except sdkexc.SDKException: LOG.exception("Failed to create trunk for chassis %s physnet %s", system_id, physnet) return None def _find_or_create_anchor_port(self, system_id, physnet): """Find or create anchor port for trunk. The anchor port is the trunk PARENT port. It attaches to an HA chassis group network (NOT the "subport anchor network"). Terminology clarification: - Anchor port = trunk parent (attaches to HA group network) - Subports = trunk children (attach to subport anchor network) :param system_id: OVN chassis system-id :param physnet: Physical network name :returns: Port ID or None """ port_name = _get_anchor_port_name(system_id, physnet) # Try to find existing port ports = self.neutron.network.ports(name=port_name) for port in ports: binding_profile = port.binding_profile or {} current_local_link_info = binding_profile.get( 'local_link_information') # Early return if already configured correctly if current_local_link_info: return port.id # Port exists but missing local_link_information local_link_list = self._get_local_link_information( system_id, physnet) if local_link_list: binding_profile['local_link_information'] = local_link_list self.neutron.network.update_port( port.id, binding_profile=binding_profile) LOG.info("Updated anchor port %s with missing " "local_link_information (%d link(s))", port.id, len(local_link_list)) else: LOG.warning( "Anchor port %s missing local_link_information for " "chassis %s physnet %s", port.id, system_id, physnet) return port.id # Need to create - find ha_group network for this chassis network_id = self._find_ha_group_network_for_chassis(system_id) if not network_id: LOG.warning("Cannot find ha_chassis_group network for " "chassis %s", system_id) return None # Get local_link_information for new anchor port local_link_list = self._get_local_link_information(system_id, physnet) if not local_link_list: LOG.warning("Could not determine local_link_information for " "chassis %s physnet %s. Port will be created but " "may not bind properly when subports are added.", system_id, physnet) # Build binding profile binding_profile = { 'system_id': system_id, 'physical_network': physnet } if local_link_list: binding_profile['local_link_information'] = local_link_list if len(local_link_list) > 1: LOG.info("Creating anchor port with %d links (LAG/bonding) " "for chassis %s physnet %s", len(local_link_list), system_id, physnet) # Create anchor port try: LOG.debug("Creating anchor port %s for chassis %s physnet %s", port_name, system_id, physnet) port = self.neutron.network.create_port( name=port_name, network_id=network_id, device_owner=DEVICE_OWNER_L2VNI_ANCHOR, admin_state_up=True, binding_vnic_type=portbindings.VNIC_BAREMETAL, binding_profile=binding_profile ) LOG.debug("Created anchor port %s", port.id) return port.id except sdkexc.SDKException: LOG.exception("Failed to create anchor port for chassis %s " "physnet %s", system_id, physnet) return None def _find_ha_group_network_for_chassis(self, system_id): """Find ha_chassis_group network that contains this chassis. :param system_id: OVN chassis system-id (same as chassis name) :returns: Network ID or None """ # Find ha_chassis_group containing this chassis for ha_group in self._get_ha_chassis_groups(): for ha_chassis in ha_group.ha_chassis: chassis = self._get_chassis_by_name(ha_chassis.chassis_name) # Chassis name IS the system-id if chassis and chassis.name == system_id: # Found the group, find its network network_name = f"l2vni-ha-group-{ha_group.name}" networks = self.neutron.network.networks( name=network_name) # networks should be a list, because were asking the api # for a list of networks matching the name with a single # resulting entry if found, otherwise an empty list. for network in networks: # If we have a match, return the first entry. return network.id return None def _get_chassis_by_name(self, chassis_name): """Get OVN chassis by name. :param chassis_name: Chassis name :returns: Chassis row or None """ try: if not hasattr(self.ovn_sb_idl, 'tables'): return None if 'Chassis' in self.ovn_sb_idl.tables: for chassis in self.ovn_sb_idl.tables['Chassis'].rows.values(): if chassis.name == chassis_name: return chassis except (AttributeError, KeyError): LOG.exception("Failed to get chassis %s", chassis_name) return None def _calculate_required_vlans(self): """Calculate which VLANs each chassis needs. A chassis needs a VLAN if: - There's a localnet port for a network on that physnet - The chassis is in the ha_chassis_group for a router on that network For each required VLAN, also captures the associated VNI from the network's overlay segment (if present) to enable L2VNI mapping configuration on switches. :returns: dict {(system_id, physnet): {vlan_id: vni}} where vni is an integer for L2VNI networks or None for pure VLAN networks """ chassis_vlan_vni_map = {} networks_with_segments = self._get_networks_with_segments() for network_id, segment_info in networks_with_segments.items(): # Extract VNI from overlay segments vni_segments = segment_info['vni_segments'] vni = None if vni_segments: # Use the first overlay segment vni = vni_segments[0].segmentation_id # Warn if multiple overlay segments exist (unusual config) if len(vni_segments) > 1: LOG.warning( "Network %s has %d overlay segments. Only the first " "(VNI %s, type %s) will be used for L2VNI mapping. " "Multiple overlay segments per network is not " "supported.", network_id, len(vni_segments), vni, vni_segments[0].network_type) for segment in segment_info['vlan_segments']: physnet = segment.physical_network vlan_id = segment.segmentation_id chassis_set = self._find_chassis_for_network( network_id, physnet) for system_id in chassis_set: if not self._should_manage_chassis(system_id): continue key = (system_id, physnet) if key not in chassis_vlan_vni_map: chassis_vlan_vni_map[key] = {} chassis_vlan_vni_map[key][vlan_id] = vni return chassis_vlan_vni_map def _get_networks_with_segments(self): """Get networks with their VLAN and overlay segments. :returns: dict {network_id: { 'vlan_segments': [segment objects], 'vni_segments': [segment objects] }} """ networks = {} try: segments = self.neutron.network.segments() for segment in segments: network_id = segment.network_id if network_id not in networks: networks[network_id] = { 'vlan_segments': [], 'vni_segments': [] } if segment.network_type == n_const.TYPE_VLAN: networks[network_id]['vlan_segments'].append(segment) elif segment.network_type in [n_const.TYPE_VXLAN, n_const.TYPE_GENEVE]: networks[network_id]['vni_segments'].append(segment) except sdkexc.SDKException: LOG.exception("Failed to get networks with VLAN and overlay " "segments") return networks def _get_vni_for_network(self, network_id): """Get VNI from network's overlay segment. :param network_id: Neutron network UUID :returns: VNI (int) or None """ try: segments = self.neutron.network.segments(network_id=network_id) for segment in segments: if segment.network_type in [n_const.TYPE_VXLAN, n_const.TYPE_GENEVE]: return segment.segmentation_id except sdkexc.SDKException: LOG.exception("Failed to get VNI for network %s", network_id) return None def _find_chassis_for_network(self, network_id, physnet): """Find chassis that need a specific network's VLAN. :param network_id: Neutron network UUID :param physnet: Physical network name :returns: set of system_ids """ chassis_set = set() # Check for localnet ports - if they exist, all chassis with # this physnet need the VLAN if self._has_localnet_port(network_id, physnet): chassis_set.update(self._get_all_chassis_with_physnet(physnet)) # Check for router ports with ha_chassis_group chassis_set.update( self._get_chassis_for_router_ports(network_id, physnet)) return chassis_set def _has_localnet_port(self, network_id, physnet): """Check if network has a localnet port on physnet. :param network_id: Neutron network UUID :param physnet: Physical network name :returns: bool """ try: ls_name = ovn_utils.ovn_name(network_id) if not hasattr(self.ovn_nb_idl, 'tables'): return False if 'Logical_Switch_Port' in self.ovn_nb_idl.tables: lsp_table = self.ovn_nb_idl.tables['Logical_Switch_Port'] for lsp in lsp_table.rows.values(): if (lsp.type == 'localnet' and lsp.options.get('network_name') == physnet): # Check if this port is on our logical switch if 'Logical_Switch' in self.ovn_nb_idl.tables: ls_table = self.ovn_nb_idl.tables['Logical_Switch'] for ls in ls_table.rows.values(): if ls.name == ls_name and lsp in ls.ports: return True except (AttributeError, KeyError): LOG.exception("Failed to check for localnet port on network %s.", network_id) return False def _get_all_chassis_with_physnet(self, physnet): """Get all chassis that have a specific physnet. :param physnet: Physical network name :returns: set of system_ids (chassis names) """ chassis_set = set() try: if not hasattr(self.ovn_sb_idl, 'tables'): return chassis_set if 'Chassis' in self.ovn_sb_idl.tables: for chassis in self.ovn_sb_idl.tables['Chassis'].rows.values(): bridge_mappings = chassis.other_config.get( 'ovn-bridge-mappings', '') physnets = self._parse_bridge_mappings(bridge_mappings) if physnet in physnets: # Chassis name IS the system-id chassis_set.add(chassis.name) except (AttributeError, KeyError): LOG.exception("Failed to get chassis with physnet %s", physnet) return chassis_set def _get_chassis_for_router_ports(self, network_id, physnet): """Get chassis hosting router ports on this network. :param network_id: Neutron network UUID :param physnet: Physical network name :returns: set of system_ids """ chassis_set = set() try: ls_name = ovn_utils.ovn_name(network_id) if not hasattr(self.ovn_nb_idl, 'tables'): return chassis_set # Find logical router ports connected to this network if 'Logical_Router_Port' in self.ovn_nb_idl.tables: lrp_table = self.ovn_nb_idl.tables['Logical_Router_Port'] for lrp in lrp_table.rows.values(): # Check if this LRP is connected to our logical switch # via its peer LSP if 'Logical_Switch_Port' in self.ovn_nb_idl.tables: lsp_table = ( self.ovn_nb_idl.tables['Logical_Switch_Port']) for lsp in lsp_table.rows.values(): if (lsp.type == 'router' and lsp.options.get('router-port') == lrp.name): # Find the logical switch if 'Logical_Switch' in self.ovn_nb_idl.tables: ls_table = ( self.ovn_nb_idl.tables[ 'Logical_Switch']) for ls in ls_table.rows.values(): if (ls.name == ls_name and lsp in ls.ports): # Found a router port on network chassis_set.update( self._get_chassis_for_lrp(lrp)) except (AttributeError, KeyError): LOG.exception("Failed to get chassis for router ports on " "network %s", network_id) return chassis_set def _get_chassis_for_lrp(self, lrp): """Get chassis assigned to a logical router port. :param lrp: Logical_Router_Port row :returns: set of system_ids (chassis names) """ chassis_set = set() try: # Check ha_chassis_group (preferred) if hasattr(lrp, 'ha_chassis_group') and lrp.ha_chassis_group: for ha_chassis in lrp.ha_chassis_group.ha_chassis: chassis = self._get_chassis_by_name( ha_chassis.chassis_name) if chassis: # Chassis name IS the system-id chassis_set.add(chassis.name) # Check legacy gateway_chassis elif hasattr(lrp, 'gateway_chassis') and lrp.gateway_chassis: for gw_chassis in lrp.gateway_chassis: chassis = self._get_chassis_by_name( gw_chassis.chassis_name) if chassis: # Chassis name IS the system-id chassis_set.add(chassis.name) except (AttributeError, KeyError): LOG.exception("Failed to get chassis for LRP %s", lrp.name) return chassis_set def _reconcile_subports(self, trunk_map, required_vlans): """Reconcile subports to match required VLAN state. :param trunk_map: dict {(system_id, physnet): trunk_id} :param required_vlans: dict {(system_id, physnet): {vlan_id: vni}} where vni may be None for non-L2VNI networks """ subport_anchor_net = self._get_subport_anchor_network_id() if not subport_anchor_net: LOG.error("Cannot reconcile subports without anchor network") return for (system_id, physnet), trunk_id in trunk_map.items(): vlan_vni_map = required_vlans.get((system_id, physnet), {}) self._reconcile_trunk_subports( trunk_id, system_id, physnet, vlan_vni_map, subport_anchor_net) def _get_subport_anchor_network_id(self): """Get the subport anchor network ID. :returns: Network ID or None """ network_name = CONF.l2vni.l2vni_subport_anchor_network networks = self.neutron.network.networks(name=network_name) for network in networks: return network.id return None def _reconcile_trunk_subports(self, trunk_id, system_id, physnet, vlan_vni_map, anchor_network_id): """Reconcile subports for a single trunk. :param trunk_id: Trunk UUID :param system_id: Chassis system-id :param physnet: Physical network name :param vlan_vni_map: dict {vlan_id: vni} :param anchor_network_id: Subport anchor network UUID """ # Get current subports try: trunk = self.neutron.network.get_trunk(trunk_id) current_subports = {sp['segmentation_id']: sp['port_id'] for sp in trunk.sub_ports} except sdkexc.SDKException: LOG.exception("Failed to get trunk %s", trunk_id) return # Add missing subports with VNI for vlan_id in vlan_vni_map.keys() - set(current_subports.keys()): vni = vlan_vni_map.get(vlan_id) self._add_subport(trunk_id, system_id, physnet, vlan_id, anchor_network_id, vni=vni) # Remove extra subports for vlan_id in set(current_subports.keys()) - vlan_vni_map.keys(): self._remove_subport(trunk_id, current_subports[vlan_id], system_id, physnet, vlan_id) def _add_subport(self, trunk_id, system_id, physnet, vlan_id, anchor_network_id, vni=None): """Add a subport to a trunk. Subports are the trunk CHILDREN that attach to the shared "subport anchor network". They signal VLAN bindings to ML2 switch plugins. :param trunk_id: Trunk UUID :param system_id: Chassis system-id :param physnet: Physical network name :param vlan_id: VLAN ID for segmentation :param anchor_network_id: Subport anchor network UUID (the shared network all subports attach to) :param vni: VNI for L2VNI mapping (optional) """ port_name = _get_subport_name(system_id, physnet, vlan_id) # Get chassis hostname for binding hostname = self._get_chassis_hostname(system_id) if not hostname: LOG.warning("Could not determine hostname for chassis %s. " "Subport will not be bound.", system_id) try: # Create port LOG.debug("Creating subport %s for trunk %s (VNI: %s)", port_name, trunk_id, vni if vni else 'none') # Build binding profile with VNI if available binding_profile = {'physical_network': physnet} if vni: binding_profile['vni'] = vni port = self.neutron.network.create_port( name=port_name, network_id=anchor_network_id, device_owner=DEVICE_OWNER_L2VNI_SUBPORT, admin_state_up=True, binding_vnic_type='baremetal', binding_profile=binding_profile ) # Set binding:host_id on subport if we have a hostname if hostname: self.neutron.network.update_port( port.id, **{'binding:host_id': hostname} ) LOG.debug("Set binding:host_id=%s for subport %s", hostname, port.id) # Add as subport self.neutron.network.add_trunk_subports( trunk_id, [{'port_id': port.id, 'segmentation_type': 'vlan', 'segmentation_id': vlan_id}] ) LOG.debug("Added subport %s (VLAN %d, VNI: %s) to trunk %s", port.id, vlan_id, vni if vni else 'none', trunk_id) except sdkexc.SDKException: LOG.exception("Failed to add subport for trunk %s VLAN %d", trunk_id, vlan_id) def _remove_subport(self, trunk_id, port_id, system_id, physnet, vlan_id): """Remove a subport from a trunk. :param trunk_id: Trunk UUID :param port_id: Subport UUID :param system_id: Chassis system-id :param physnet: Physical network name :param vlan_id: VLAN ID """ try: LOG.debug("Removing subport %s (VLAN %d) from trunk %s", port_id, vlan_id, trunk_id) # Remove from trunk self.neutron.network.delete_trunk_subports( trunk_id, [{'port_id': port_id}]) # Delete port self.neutron.network.delete_port(port_id) LOG.debug("Removed subport %s from trunk %s", port_id, trunk_id) except sdkexc.SDKException: LOG.exception("Failed to remove subport %s from trunk %s", port_id, trunk_id) def _ensure_single_subport(self, trunk_id, system_id, physnet, vlan_id, anchor_network_id, vni=None): """Ensure a single subport exists on a trunk. Idempotent - checks if subport already exists before creating. :param trunk_id: Trunk UUID :param system_id: Chassis system-id :param physnet: Physical network name :param vlan_id: VLAN ID for segmentation :param anchor_network_id: Subport anchor network UUID :param vni: VNI for L2VNI mapping (optional) """ try: trunk = self.neutron.network.get_trunk(trunk_id) existing_vlans = {sp['segmentation_id'] for sp in trunk.sub_ports} if vlan_id in existing_vlans: LOG.debug("Subport for VLAN %d already exists on trunk %s", vlan_id, trunk_id) return # Add the subport self._add_subport(trunk_id, system_id, physnet, vlan_id, anchor_network_id, vni=vni) except sdkexc.SDKException: LOG.exception("Failed to ensure subport for VLAN %d on trunk %s", vlan_id, trunk_id) def _remove_single_subport(self, trunk_id, system_id, physnet, vlan_id): """Remove a single subport from a trunk if it exists. Idempotent - checks if subport exists before removing. :param trunk_id: Trunk UUID :param system_id: Chassis system-id :param physnet: Physical network name :param vlan_id: VLAN ID to remove """ try: trunk = self.neutron.network.get_trunk(trunk_id) subport_to_remove = None for sp in trunk.sub_ports: if sp['segmentation_id'] == vlan_id: subport_to_remove = sp['port_id'] break if not subport_to_remove: LOG.debug("Subport for VLAN %d does not exist on trunk %s", vlan_id, trunk_id) return # Remove the subport self._remove_subport(trunk_id, subport_to_remove, system_id, physnet, vlan_id) except sdkexc.SDKException: LOG.exception("Failed to remove subport for VLAN %d from " "trunk %s", vlan_id, trunk_id) def _get_local_link_information(self, system_id, physnet): """Get local_link_information data using tiered approach. Tries in order: 1. OVN LLDP data 2. Ironic port data 3. YAML configuration file Aggregates multiple links for LAG/bonding configurations where multiple physical ports connect to the same physical network. :param system_id: Chassis system-id :param physnet: Physical network name :returns: list of dicts with local_link_information, or None if no connection data found. List may contain multiple entries for LAG/bonding scenarios. """ # Try OVN LLDP data first lldp_data = self._get_lldp_from_ovn(system_id, physnet) if lldp_data: return lldp_data # Try Ironic discovery ironic_data = self._get_local_link_from_ironic(system_id, physnet) if ironic_data: return ironic_data # Fall back to YAML config return self._get_local_link_from_config(system_id, physnet) def _get_lldp_from_ovn(self, system_id, physnet): """Get local_link_information from OVN LLDP data. Extracts LLDP information from OVN Southbound Port table for the chassis and physical network. Aggregates all ports on the bridge to support LAG/bonding configurations. :param system_id: Chassis system-id (same as chassis name) :param physnet: Physical network name :returns: list of dicts with local_link_information, or None if no LLDP data found """ try: if not hasattr(self.ovn_sb_idl, 'tables'): LOG.debug("OVN SB IDL not available for LLDP lookup") return None # Find the chassis - chassis name IS the system-id chassis = None if 'Chassis' in self.ovn_sb_idl.tables: for c in self.ovn_sb_idl.tables['Chassis'].rows.values(): if c.name == system_id: chassis = c break if not chassis: return None # Find port on this chassis that maps to the physnet bridge_mappings = chassis.other_config.get( 'ovn-bridge-mappings', '') physnet_to_bridge = {} for mapping in bridge_mappings.split(','): if ':' not in mapping: continue pnet, bridge = mapping.split(':', 1) physnet_to_bridge[pnet.strip()] = bridge.strip() bridge_name = physnet_to_bridge.get(physnet) if not bridge_name: return None # Aggregate all ports on this chassis with LLDP for this bridge # Supports LAG/bonding with multiple ports to same bridge local_links = [] if 'Port' in self.ovn_sb_idl.tables: for port in self.ovn_sb_idl.tables['Port'].rows.values(): # Check if port belongs to this chassis if port.chassis != chassis: continue # Check if this port is on the correct bridge # Port.interfaces is a list of Interface objects if not hasattr(port, 'interfaces') or not port.interfaces: continue port_on_bridge = False for iface in port.interfaces: # Interface.name typically matches the OVS interface # which should contain the bridge name for physical # interfaces (e.g., "br-physnet1", "eth0", etc.) if not hasattr(iface, 'name'): continue iface_name = iface.name # Check if interface name matches or is on bridge if (iface_name == bridge_name or iface_name.startswith(bridge_name)): port_on_bridge = True break if not port_on_bridge: continue # Get LLDP data from external_ids lldp = port.external_ids chassis_id = lldp.get('lldp_chassis_id') port_id = lldp.get('lldp_port_id') system_name = lldp.get('lldp_system_name') if chassis_id and port_id: LOG.debug("Found LLDP data for chassis %s physnet %s " "bridge %s: switch_id=%s, port_id=%s, " "switch_info=%s", system_id, physnet, bridge_name, chassis_id, port_id, system_name) local_links.append({ 'switch_id': chassis_id, 'port_id': port_id, 'switch_info': system_name or '' }) if local_links: if len(local_links) > 1: LOG.info("Found %d links for chassis %s physnet %s " "(LAG/bonding configuration)", len(local_links), system_id, physnet) return local_links return None except (AttributeError, KeyError): LOG.exception("Failed to get LLDP data from OVN for chassis %s " "physnet %s.", system_id, physnet) return None def _fetch_ironic_data_for_system_id(self, system_id): """Fetch node and ports for a specific system_id from Ironic. Queries Ironic efficiently by: 1. Filtering nodes by conductor_group/shard if configured 2. Requesting only minimal fields needed 3. Only fetching ports for the matched node :param system_id: Chassis system-id :returns: dict with cached_at, node_uuid, and ports list, or None """ try: # Build query filters query_params = {} if CONF.l2vni.ironic_conductor_group: query_params['conductor_group'] = ( CONF.l2vni.ironic_conductor_group) if CONF.l2vni.ironic_shard: query_params['shard'] = CONF.l2vni.ironic_shard # Query nodes with minimal fields for performance nodes = self.ironic.nodes( fields=['uuid', 'properties'], **query_params ) # Find the node with matching system_id for node in nodes: if node.properties.get('system_id') == system_id: # Found the node - fetch its ports with minimal fields LOG.debug("Found Ironic node %s for system_id %s, " "fetching ports", node.uuid, system_id) ports = self.ironic.ports( node_uuid=node.uuid, fields=['physical_network', 'local_link_connection'] ) # Build cache entry cache_entry = { 'cached_at': time.time(), 'node_uuid': node.uuid, 'ports': [] } for port in ports: if port.local_link_connection: cache_entry['ports'].append({ 'physnet': port.physical_network, 'local_link': port.local_link_connection }) LOG.debug("Cached Ironic data for system_id %s: " "node %s with %d ports", system_id, node.uuid, len(cache_entry['ports'])) return cache_entry LOG.debug("No Ironic node found with system_id %s", system_id) return None except (sdkexc.SDKException, AttributeError, KeyError): LOG.exception("Failed to fetch Ironic data for system_id %s", system_id) return None def _aggregate_ironic_ports_for_physnet(self, cache_entry, physnet, system_id, source_label): """Aggregate Ironic ports matching physnet from cache entry. Helper method to extract local_link_information data for all ports matching a physical network from a cached Ironic data entry. :param cache_entry: Cached Ironic data dict with 'ports' list :param physnet: Physical network name to filter by :param system_id: Chassis system-id (for logging) :param source_label: Label for log messages (e.g., "Ironic cache", "Ironic") :returns: list of local_link_information dicts, or None if no matches """ local_links = [] for port in cache_entry['ports']: if port['physnet'] == physnet: local_links.append(port['local_link']) if local_links: if len(local_links) > 1: LOG.debug("Found %d links from %s for chassis %s physnet %s " "(LAG/bonding)", len(local_links), source_label, system_id, physnet) else: LOG.debug("Found local_link_information from %s for " "chassis %s physnet %s", source_label, system_id, physnet) return local_links return None def _get_local_link_from_ironic(self, system_id, physnet): """Get local_link_information from Ironic, using per-record cache. Uses a per-record cache with TTL and jitter to avoid thundering herd issues when multiple agents are running. Each system_id is cached independently and only refreshed when its TTL expires. Aggregates all Ironic ports matching the physnet to support LAG/bonding configurations where multiple ports share the same physical_network. :param system_id: Chassis system-id :param physnet: Physical network name :returns: list of dicts with local_link_information, or None if no ports found """ try: # Check if we have a valid cached entry for this system_id if system_id in self._ironic_cache: cached_entry = self._ironic_cache[system_id] age = time.time() - cached_entry['cached_at'] # Add jitter (10-20%) to TTL to spread refresh times # across multiple agents and avoid thundering herd jitter = 0.9 + random.random() * 0.2 # noqa: S311 ttl_with_jitter = CONF.l2vni.ironic_cache_ttl * jitter if age < ttl_with_jitter: # Cache hit - use cached data LOG.debug("Using cached Ironic data for system_id %s " "(age: %.1fs, TTL: %.1fs)", system_id, age, ttl_with_jitter) return self._aggregate_ironic_ports_for_physnet( cached_entry, physnet, system_id, "Ironic cache") else: LOG.debug("Ironic cache expired for system_id %s " "(age: %.1fs, TTL: %.1fs)", system_id, age, ttl_with_jitter) # Cache miss or expired - fetch data for this system_id LOG.debug("Fetching Ironic data for system_id %s (cache miss)", system_id) cache_entry = self._fetch_ironic_data_for_system_id(system_id) if cache_entry: # Update cache with new entry self._ironic_cache[system_id] = cache_entry return self._aggregate_ironic_ports_for_physnet( cache_entry, physnet, system_id, "Ironic") return None except (KeyError, AttributeError): LOG.exception("Failed to get local_link_information from Ironic " "for chassis %s physnet %s.", system_id, physnet) return None def _get_local_link_from_node_config(self, node, physnet): """Get local_link_information from a network node config entry. Supports both single-link and multi-link (LAG/bonding) configurations: - Single dict: local_link_information: {switch_id: ..., port_id: ...} - List of dicts: local_link_information: [{...}, {...}] For backward compatibility, also accepts 'local_link_connection' as an alias for 'local_link_information'. :param node: Network node config dict from YAML :param physnet: Physical network name :returns: list of dicts with local_link_information, or None """ for trunk_config in node.get('trunks', []): if trunk_config.get('physical_network') == physnet: # Try new name first, fallback to old name for backward compat local_link = trunk_config.get('local_link_information') if not local_link: # Check for deprecated name local_link = trunk_config.get('local_link_connection') if local_link: LOG.warning( "Configuration uses deprecated " "'local_link_connection' field for physnet %s. " "Please update to 'local_link_information' (as a " "list) to match Neutron API naming.", physnet) if not local_link: return None # Support both single dict and list of dicts if isinstance(local_link, list): # Already a list - return as-is if len(local_link) > 1: LOG.debug("Found %d links in config for physnet %s " "(LAG/bonding)", len(local_link), physnet) return local_link elif isinstance(local_link, dict): # Single dict - wrap in list for consistency return [local_link] else: LOG.warning("Invalid local_link_information format in " "config for physnet %s: expected dict or list", physnet) return None return None def _get_local_link_from_config(self, system_id, physnet): """Get local_link_information from YAML config file. Matches network nodes by system_id (chassis UUID) or hostname. This allows the YAML config to use either the predictable hostname or the exact chassis UUID. :param system_id: Chassis system-id (UUID) :param physnet: Physical network name :returns: list of dicts with local_link_information or None """ if self._config_cache is None: self._load_config() if not self._config_cache: return None # Try to match by system_id first (exact UUID match) for node in self._config_cache.get('network_nodes', []): if node.get('system_id') == system_id: return self._get_local_link_from_node_config(node, physnet) # No system_id match found, try hostname fallback chassis_hostname = self._get_chassis_hostname(system_id) if chassis_hostname: for node in self._config_cache.get('network_nodes', []): if node.get('hostname') == chassis_hostname: LOG.debug("Matched chassis %s by hostname %s in config", system_id, chassis_hostname) return self._get_local_link_from_node_config(node, physnet) return None def _get_chassis_hostname(self, system_id): """Get hostname for a chassis by system-id. :param system_id: Chassis system-id (UUID) :returns: Hostname string or None """ try: if not hasattr(self.ovn_sb_idl, 'tables'): return None if 'Chassis' not in self.ovn_sb_idl.tables: return None for chassis in self.ovn_sb_idl.tables['Chassis'].rows.values(): if chassis.name == system_id and hasattr(chassis, 'hostname'): return chassis.hostname except (AttributeError, KeyError): LOG.debug("Failed to get hostname for chassis %s", system_id) return None def _load_config(self): """Load configuration from YAML file.""" try: config_file = CONF.l2vni.l2vni_network_nodes_config with open(config_file, 'r') as f: self._config_cache = yaml.safe_load(f) LOG.debug("Loaded L2VNI configuration from %s", config_file) except FileNotFoundError: self._config_cache = {} except (IOError, OSError, yaml.YAMLError): LOG.exception("Failed to load L2VNI config file") self._config_cache = {} def _cleanup_unused_infrastructure(self): """Clean up unused L2VNI infrastructure. Removes: - Trunks with no subports for deleted chassis - Anchor ports for deleted trunks - Networks for deleted ha_chassis_groups """ try: # Get current chassis/physnet combinations that should exist valid_chassis_physnets = self._get_chassis_physnets() # Clean up orphaned trunks and anchor ports self._cleanup_orphaned_trunks(valid_chassis_physnets) # Clean up orphaned ha_chassis_group networks self._cleanup_orphaned_networks() except (sdkexc.SDKException, AttributeError, KeyError): LOG.exception("Failed to clean up unused L2VNI infrastructure.") def _cleanup_orphaned_trunks(self, valid_chassis_physnets): """Clean up trunks and anchor ports for deleted chassis. :param valid_chassis_physnets: set of (system_id, physnet) that should have trunks """ try: # Find all L2VNI trunks trunks = self.neutron.network.trunks() for trunk in trunks: if not trunk.name or not trunk.name.startswith( 'l2vni-trunk-'): continue # Parse trunk name: l2vni-trunk-{system_id}-{physnet} # System_id is a UUID with dashes, so split from right name_without_prefix = trunk.name[len('l2vni-trunk-'):] parts = name_without_prefix.rsplit('-', 1) if len(parts) != 2: continue system_id = parts[0] physnet = parts[1] # Check if this trunk should still exist if (system_id, physnet) not in valid_chassis_physnets: LOG.info("Cleaning up orphaned trunk %s for chassis %s " "physnet %s", trunk.id, system_id, physnet) # Get anchor port before deleting trunk anchor_port_id = trunk.port_id # Delete all subports first if trunk.sub_ports: for subport in trunk.sub_ports: try: port_id = subport['port_id'] subport_spec = [{'port_id': port_id}] self.neutron.network.delete_trunk_subports( trunk.id, subport_spec) self.neutron.network.delete_port( subport['port_id']) except sdkexc.SDKException: LOG.warning("Failed to delete subport %s", subport['port_id']) # Delete trunk try: self.neutron.network.delete_trunk(trunk.id) LOG.info("Deleted orphaned trunk %s", trunk.id) except sdkexc.SDKException: LOG.exception("Failed to delete trunk %s", trunk.id) continue # Delete anchor port if anchor_port_id: try: self.neutron.network.delete_port(anchor_port_id) LOG.info("Deleted orphaned anchor port %s", anchor_port_id) except sdkexc.SDKException: LOG.warning("Failed to delete anchor port %s", anchor_port_id) except (sdkexc.SDKException, AttributeError): LOG.exception("Failed to cleanup orphaned trunks") def _cleanup_orphaned_networks(self): """Clean up ha_chassis_group networks that no longer have groups.""" try: # Get all current ha_chassis_groups ha_groups = self._get_ha_chassis_groups() valid_group_names = {group.name for group in ha_groups} # Find all L2VNI ha_chassis_group networks networks = self.neutron.network.networks() for network in networks: if not network.name or not network.name.startswith( 'l2vni-ha-group-'): continue # Parse network name: l2vni-ha-group-{group_name} parts = network.name.split('-', 3) if len(parts) < 4: continue group_name = parts[3] # Check if this ha_chassis_group still exists if group_name not in valid_group_names: # Check if network has any ports (besides DHCP/router) ports = list(self.neutron.network.ports( network_id=network.id)) l2vni_ports = [ p for p in ports if p.device_owner == DEVICE_OWNER_L2VNI_ANCHOR] if not l2vni_ports: LOG.info("Cleaning up orphaned ha_chassis_group " "network %s for group %s", network.id, group_name) try: self.neutron.network.delete_network(network.id) LOG.info("Deleted orphaned network %s", network.id) except sdkexc.SDKException: LOG.exception("Failed to delete network %s", network.id) except (sdkexc.SDKException, AttributeError): LOG.exception("Failed to cleanup orphaned networks") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/agent/ovn_client.py0000664000175000017500000003071015157004031024555 0ustar00zuulzuul# Copyright (c) 2026 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """OVN client connections for agent.""" from oslo_config import cfg from oslo_log import log as logging from ovs import stream from ovsdbapp.backend.ovs_idl import connection from ovsdbapp.backend.ovs_idl import event as row_event from ovsdbapp.backend.ovs_idl import idlutils from ovsdbapp.schema.ovn_northbound import impl_idl as nb_impl_idl from ovsdbapp.schema.ovn_southbound import impl_idl as sb_impl_idl CONF = cfg.CONF LOG = logging.getLogger(__name__) _OVN_NB_IDL = None _OVN_SB_IDL = None _OVN_NB_EVENT_IDL = None class AgentOvnNbIdl(connection.OvsdbIdl): """Custom OVN NB IDL with event handler support for agent. Extends the standard OvsdbIdl to add RowEvent notification support. This allows the agent to watch for specific OVN database changes (e.g., localnet port creation) and trigger immediate reconciliation. """ def __init__(self, remote, schema, **kwargs): self.notify_handler = row_event.RowEventHandler() super().__init__(remote, schema, **kwargs) def notify(self, event, row, updates=None): """Called by IDL when database changes occur. Forwards notifications to registered RowEvent handlers. """ self.notify_handler.notify(event, row, updates) class AgentOvnSbIdl(connection.OvsdbIdl): """Custom OVN SB IDL with event handler support for agent. Extends the standard OvsdbIdl to add RowEvent notification support. This allows the agent to watch for specific OVN database changes and trigger immediate reconciliation if needed. """ def __init__(self, remote, schema, **kwargs): self.notify_handler = row_event.RowEventHandler() super().__init__(remote, schema, **kwargs) def notify(self, event, row, updates=None): """Called by IDL when database changes occur. Forwards notifications to registered RowEvent handlers. """ self.notify_handler.notify(event, row, updates) def _configure_ovn_ssl(): """Configure SSL settings for OVN connections. Reads SSL certificate configuration from [ovn] section and sets up the OVS Stream SSL parameters. This must be called before creating any IDL connections when using SSL. """ try: if not hasattr(CONF, 'ovn'): return # Set SSL CA certificate if configured if hasattr(CONF.ovn, 'ovn_sb_ca_cert') and CONF.ovn.ovn_sb_ca_cert: stream.Stream.ssl_set_ca_cert_file(CONF.ovn.ovn_sb_ca_cert) LOG.debug("Configured OVN SSL CA cert: %s", CONF.ovn.ovn_sb_ca_cert) # Set SSL client certificate if configured if (hasattr(CONF.ovn, 'ovn_sb_certificate') and CONF.ovn.ovn_sb_certificate): stream.Stream.ssl_set_certificate_file( CONF.ovn.ovn_sb_certificate) LOG.debug("Configured OVN SSL certificate: %s", CONF.ovn.ovn_sb_certificate) # Set SSL private key if configured if (hasattr(CONF.ovn, 'ovn_sb_private_key') and CONF.ovn.ovn_sb_private_key): stream.Stream.ssl_set_private_key_file( CONF.ovn.ovn_sb_private_key) LOG.debug("Configured OVN SSL private key: %s", CONF.ovn.ovn_sb_private_key) except (cfg.NoSuchOptError, AttributeError) as e: LOG.debug("Could not configure OVN SSL settings: %s", e) def _get_ovn_nb_connection(): """Get OVN NB connection string from config with fallback. Priority order: 1. [l2vni] ovn_nb_connection (explicit agent override) 2. [ovn] ovn_nb_connection (shared Neutron ML2 config) 3. Hardcoded default: tcp:127.0.0.1:6641 :returns: OVN NB connection string (comma-separated if multiple) """ # If explicitly set in [l2vni], use it if CONF.l2vni.ovn_nb_connection is not None: conn = CONF.l2vni.ovn_nb_connection # Convert list to comma-separated string for ovsdbapp if isinstance(conn, list): return ','.join(conn) return conn # Try to read from [ovn] section (Neutron ML2 config) try: if hasattr(CONF, 'ovn') and hasattr(CONF.ovn, 'ovn_nb_connection'): ovn_conn = CONF.ovn.ovn_nb_connection if ovn_conn: # Neutron's ovn_nb_connection is a ListOpt for HA support # Convert to comma-separated string if it's a list if isinstance(ovn_conn, list): ovn_conn = ','.join(ovn_conn) LOG.debug("Using OVN NB connection from [ovn] section: %s", ovn_conn) return ovn_conn except (cfg.NoSuchOptError, AttributeError): pass # Fallback to hardcoded default LOG.debug("Using default OVN NB connection: tcp:127.0.0.1:6641") return 'tcp:127.0.0.1:6641' def _get_ovn_sb_connection(): """Get OVN SB connection string from config with fallback. Priority order: 1. [l2vni] ovn_sb_connection (explicit agent override) 2. [ovn] ovn_sb_connection (shared Neutron ML2 config) 3. Hardcoded default: tcp:127.0.0.1:6642 :returns: OVN SB connection string (comma-separated if multiple) """ # If explicitly set in [l2vni], use it if CONF.l2vni.ovn_sb_connection is not None: conn = CONF.l2vni.ovn_sb_connection # Convert list to comma-separated string for ovsdbapp if isinstance(conn, list): return ','.join(conn) return conn # Try to read from [ovn] section (Neutron ML2 config) try: if hasattr(CONF, 'ovn') and hasattr(CONF.ovn, 'ovn_sb_connection'): ovn_conn = CONF.ovn.ovn_sb_connection if ovn_conn: # Neutron's ovn_sb_connection is a ListOpt for HA support # Convert to comma-separated string if it's a list if isinstance(ovn_conn, list): ovn_conn = ','.join(ovn_conn) LOG.debug("Using OVN SB connection from [ovn] section: %s", ovn_conn) return ovn_conn except (cfg.NoSuchOptError, AttributeError): pass # Fallback to hardcoded default LOG.debug("Using default OVN SB connection: tcp:127.0.0.1:6642") return 'tcp:127.0.0.1:6642' def _get_ovn_ovsdb_timeout(): """Get OVN OVSDB timeout from config with fallback. Priority order: 1. [l2vni] ovn_ovsdb_timeout (explicit agent override) 2. [ovn] ovsdb_connection_timeout (shared Neutron ML2 config) 3. Hardcoded default: 180 :returns: OVN OVSDB timeout in seconds """ # If explicitly set in [l2vni], use it if CONF.l2vni.ovn_ovsdb_timeout is not None: return CONF.l2vni.ovn_ovsdb_timeout # Try to read from [ovn] section (Neutron ML2 config) try: if (hasattr(CONF, 'ovn') and hasattr(CONF.ovn, 'ovsdb_connection_timeout')): ovn_timeout = CONF.ovn.ovsdb_connection_timeout if ovn_timeout: LOG.debug("Using OVN OVSDB timeout from [ovn] section: %d", ovn_timeout) return ovn_timeout except (cfg.NoSuchOptError, AttributeError): pass # Fallback to hardcoded default LOG.debug("Using default OVN OVSDB timeout: 180") return 180 def get_ovn_nb_idl(): """Get OVN Northbound IDL connection. :returns: OVN NB API instance """ global _OVN_NB_IDL if _OVN_NB_IDL is None: try: # Get connection string from config (with fallback to [ovn]) conn_string = _get_ovn_nb_connection() timeout = _get_ovn_ovsdb_timeout() LOG.debug("Connecting to OVN NB: %s", conn_string) # Configure SSL if using SSL connections _configure_ovn_ssl() # Create IDL connection helper = idlutils.get_schema_helper(conn_string, 'OVN_Northbound') helper.register_all() # Create custom IDL instance with event handler support idl = AgentOvnNbIdl(conn_string, helper) ovn_conn = connection.Connection( idl, timeout=timeout ) ovn_conn.start() # Create and store the NB API implementation _OVN_NB_IDL = nb_impl_idl.OvnNbApiIdlImpl(ovn_conn) LOG.info("Connected to OVN Northbound database") except Exception: LOG.info("Unable to connect to OVN Northbound database at %s: " "(OVN may not be configured)", conn_string, exc_info=True) raise return _OVN_NB_IDL def get_ovn_sb_idl(): """Get OVN Southbound IDL connection. :returns: OVN SB API instance """ global _OVN_SB_IDL if _OVN_SB_IDL is None: try: # Get connection string from config (with fallback to [ovn]) conn_string = _get_ovn_sb_connection() timeout = _get_ovn_ovsdb_timeout() LOG.debug("Connecting to OVN SB: %s", conn_string) # Configure SSL if using SSL connections _configure_ovn_ssl() # Create IDL connection helper = idlutils.get_schema_helper(conn_string, 'OVN_Southbound') helper.register_all() # Create custom IDL instance with event handler support idl = AgentOvnSbIdl(conn_string, helper) ovn_conn = connection.Connection( idl, timeout=timeout ) ovn_conn.start() # Create and store the SB API implementation _OVN_SB_IDL = sb_impl_idl.OvnSbApiIdlImpl(ovn_conn) LOG.info("Connected to OVN Southbound database") except Exception: LOG.info("Unable to connect to OVN Southbound database at %s: " "(OVN may not be configured)", conn_string, exc_info=True) raise return _OVN_SB_IDL def get_ovn_nb_event_idl(): """Get OVN Northbound IDL connection for event watching only. This connection registers only the minimal set of tables needed for event watching, significantly reducing event notification overhead. Use this connection for registering RowEvent handlers. For queries and updates, use get_ovn_nb_idl() instead. :returns: OVN NB API instance (event-watching connection) """ global _OVN_NB_EVENT_IDL if _OVN_NB_EVENT_IDL is None: try: # Get connection string from config (with fallback to [ovn]) conn_string = _get_ovn_nb_connection() timeout = _get_ovn_ovsdb_timeout() LOG.debug("Connecting to OVN NB (event-only): %s", conn_string) # Configure SSL if using SSL connections _configure_ovn_ssl() # Create IDL connection with selective table registration helper = idlutils.get_schema_helper(conn_string, 'OVN_Northbound') # Only register tables needed for event watching # This dramatically reduces event notification overhead helper.register_table('Logical_Switch_Port') # Create custom IDL instance with event handler support idl = AgentOvnNbIdl(conn_string, helper) ovn_conn = connection.Connection( idl, timeout=timeout ) ovn_conn.start() # Create and store the NB API implementation _OVN_NB_EVENT_IDL = nb_impl_idl.OvnNbApiIdlImpl(ovn_conn) LOG.info("Connected to OVN Northbound database (event-only " "connection with selective table registration)") except Exception: LOG.info("Unable to connect to OVN Northbound database at %s: " "(OVN may not be configured)", conn_string, exc_info=True) raise return _OVN_NB_EVENT_IDL ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/agent/ovn_events.py0000664000175000017500000002560415157004031024611 0ustar00zuulzuul# Copyright (c) 2026 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """OVN RowEvent handlers for L2VNI reconciliation.""" from neutron.common.ovn import constants as ovn_const from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovsdb_monitor from oslo_log import log as logging from ovsdbapp.backend.ovs_idl import event as row_event LOG = logging.getLogger(__name__) class LocalnetPortEvent(ovsdb_monitor.BaseEvent): """Trigger L2VNI reconciliation when localnet ports are created or deleted. Watches for CREATE and DELETE events on Logical_Switch_Port table where type=localnet and the port name follows L2VNI naming convention (contains '-localnet-'). Uses hash ring to filter events so only the agent responsible for the network processes the event. CREATE events trigger immediate reconciliation to add required subports. DELETE events trigger immediate reconciliation to remove obsolete subports, ensuring fast cleanup for security and resource isolation. """ table = 'Logical_Switch_Port' events = (row_event.RowEvent.ROW_CREATE, row_event.RowEvent.ROW_DELETE) def __init__(self, agent): """Initialize LocalnetPortEvent. :param agent: BaremetalNeutronAgent instance """ self.agent = agent self.hashring = agent.member_manager.hashring self.agent_id = agent.agent_id super().__init__() def match_fn(self, event, row, old=None): """Filter for L2VNI localnet ports owned by this agent. Returns True only if: 1. Port type is localnet 2. Port name follows L2VNI naming: neutron--localnet- 3. This agent owns the network (hash ring check) Note: Event type filtering (CREATE/DELETE only) is handled by the parent BaseEvent.matches() method, which ensures UPDATE events are filtered out before this method is called. :param event: Event type (ROW_CREATE or ROW_DELETE) :param row: OVN Logical_Switch_Port row :param old: Previous row state (unused for CREATE events) :returns: True if event should be processed, False otherwise """ # Filter for localnet port type if not hasattr(row, 'type') or row.type != 'localnet': return False if not hasattr(row, 'name') or not row.name: return False # L2VNI localnet ports have format: # neutron--localnet- if '-localnet-' not in row.name: return False # Extract network_id from port name try: parts = row.name.split('-localnet-') if len(parts) != 2: return False ls_name = parts[0] # neutron- if not ls_name.startswith('neutron-'): return False network_id = ls_name.replace('neutron-', '', 1) # Hash ring check: Do I own this network? hashring_members = list(self.hashring[network_id.encode('utf-8')]) if self.agent_id not in hashring_members: LOG.debug( "Localnet port %s on network %s not owned by this " "agent (hash ring), ignoring. Agent ID: %s, " "Hash ring members for network: %s", row.name, network_id, self.agent_id, hashring_members) return False LOG.debug("Localnet port %s matches: L2VNI port on network %s " "owned by this agent", row.name, network_id) return True except (ValueError, AttributeError, KeyError) as e: LOG.debug("Failed to parse localnet port name %s: %s", row.name, e) return False def run(self, event, row, old): """Trigger targeted L2VNI reconciliation for specific VLAN. :param event: Event type (ROW_CREATE or ROW_DELETE) :param row: OVN Logical_Switch_Port row :param old: Previous row state (used for DELETE events) """ # Debug logging at the very start before locking LOG.debug("LocalnetPortEvent.run called: event=%s, row.name=%s, " "row.tag=%s, row.options=%s, old=%s", event, row.name, getattr(row, 'tag', None), getattr(row, 'options', None), old) try: # Extract info from event network_id = self._extract_network_id(row.name) physnet = row.options.get('network_name') vlan_id = int(row.tag[0]) if row.tag else None if not all([network_id, physnet, vlan_id]): LOG.warning( "Incomplete info from localnet port event " "(network_id=%s, physnet=%s, vlan_id=%s), falling " "back to full reconciliation", network_id, physnet, vlan_id) self.agent._reconcile_l2vni_trunks() return action = 'add' if event == self.ROW_CREATE else 'remove' LOG.info("Localnet port %s for network %s (physnet=%s, vlan=%s), " "triggering targeted reconciliation", action, network_id, physnet, vlan_id) # Call targeted reconciliation (blocks until lock available) self.agent._reconcile_single_vlan_blocking( network_id, physnet, vlan_id, action) except AttributeError: LOG.exception( "Malformed OVN row data in localnet port event, falling " "back to full reconciliation") self.agent._reconcile_l2vni_trunks() def _extract_network_id(self, port_name): """Extract network UUID from localnet port name. Localnet port names follow the pattern: neutron--localnet- :param port_name: OVN localnet port name :returns: Network UUID or None """ try: parts = port_name.split('-localnet-') if len(parts) != 2: return None ls_name = parts[0] return ls_name.replace('neutron-', '', 1) except (ValueError, AttributeError): return None class HAChassisGroupNetworkEvent(ovsdb_monitor.BaseEvent): """Trigger router HA binding for HA Chassis group create/update. Watches for CREATE and UPDATE events on HA_Chassis_Group table where the group is network-level (has neutron:network_id in external_ids but not neutron:router_id). Uses hash ring to filter events so only the agent responsible for the network processes the event. Fixes LP#2144458 by enabling immediate router interface binding instead of waiting for periodic reconciliation. Related to LP#1995078 (networking-baremetal side of the solution). """ table = 'HA_Chassis_Group' events = (row_event.RowEvent.ROW_CREATE, row_event.RowEvent.ROW_UPDATE) def __init__(self, agent): """Initialize HAChassisGroupNetworkEvent. :param agent: BaremetalNeutronAgent instance """ self.agent = agent self.hashring = agent.member_manager.hashring self.agent_id = agent.agent_id super().__init__() def match_fn(self, event, row, old=None): """Filter for HA chassis groups with network_id owned by this agent. Returns True only if: 1. HA chassis group has neutron:network_id in external_ids 2. This agent owns the network (hash ring check) Note: We process groups with network_id regardless of whether they also have router_id. In unified HA chassis group scenarios, the same group is used for both the network and router. :param event: Event type (ROW_CREATE or ROW_UPDATE) :param row: OVN HA_Chassis_Group row :param old: Previous row state (for UPDATE events) :returns: True if event should be processed, False otherwise """ if not hasattr(row, 'external_ids') or not row.external_ids: return False external_ids = row.external_ids network_id = external_ids.get(ovn_const.OVN_NETWORK_ID_EXT_ID_KEY) if not network_id: return False try: hashring_members = list(self.hashring[network_id.encode('utf-8')]) if self.agent_id not in hashring_members: LOG.debug( "HA chassis group %s for network %s not owned by this " "agent (hash ring), ignoring. Agent ID: %s, " "Hash ring members for network: %s", row.uuid, network_id, self.agent_id, hashring_members) return False LOG.debug("HA chassis group %s matches: network-level group for " "network %s owned by this agent", row.uuid, network_id) return True except (ValueError, AttributeError, KeyError) as e: LOG.debug("Failed to check hash ring for network %s: %s", network_id, e) return False def run(self, event, row, old): """Trigger router interface binding for the network. :param event: Event type (ROW_CREATE or ROW_UPDATE) :param row: OVN HA_Chassis_Group row :param old: Previous row state (for UPDATE events) """ LOG.debug("HAChassisGroupNetworkEvent.run called: event=%s, " "row.uuid=%s, row.external_ids=%s", event, row.uuid, getattr(row, 'external_ids', None)) try: network_id = row.external_ids.get( ovn_const.OVN_NETWORK_ID_EXT_ID_KEY) ha_chassis_group = row.uuid LOG.info("Network HA chassis group %s created/updated for " "network %s, triggering router interface binding", event, network_id) if hasattr(self.agent, 'router_ha_binding') and \ self.agent.router_ha_binding: binding = self.agent.router_ha_binding binding.bind_router_interfaces_for_network( network_id, ha_chassis_group) else: LOG.warning("Router HA binding manager not available, " "skipping router interface binding for network %s", network_id) except (AttributeError, KeyError): LOG.exception("Failed to process HA chassis group event for " "row %s", row.uuid) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/agent/router_ha_binding.py0000664000175000017500000003112315157004031026076 0ustar00zuulzuul# Copyright (c) 2026 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. """Router HA Binding Manager. Manages HA chassis group binding for router interface ports on VLAN networks with baremetal nodes. Ensures router interface ports are bound to the same HA chassis group as the network's external ports, enabling router-to-baremetal communication on physical networks. This is related to LP#1995078 where baremetal nodes on VLAN networks cannot communicate with their router gateway because the router's internal interface port (LRP) is not bound to any chassis. This fixes LP#2144458 by providing event-driven HA chassis group binding, eliminating the multi-minute connectivity delays caused by periodic-only reconciliation. """ from neutron.common.ovn import constants as ovn_const from neutron.common.ovn import utils as ovn_utils from neutron_lib import constants as n_const from openstack import exceptions as sdk_exc from oslo_log import log as logging from ovsdbapp.backend.ovs_idl import idlutils from ovsdbapp import exceptions as ovs_exc LOG = logging.getLogger(__name__) class RouterHABindingManager: """Manages HA chassis group binding for router interface ports. Ensures router interface ports on VLAN networks are bound to the same HA chassis group as the network's external ports, enabling router-to- baremetal communication on physical networks. This manager is event-driven, responding to HA_Chassis_Group creation events to immediately bind router interface ports when network-level HA chassis groups are created. """ def __init__(self, neutron_client, ovn_nb_idl, member_manager, agent_id): """Initialize router HA binding manager. :param neutron_client: Neutron client (OpenStack SDK Connection) for port queries :param ovn_nb_idl: OVN Northbound IDL connection :param member_manager: Hash ring member manager for agent coordination :param agent_id: Agent ID for hash ring filtering """ self.neutron_client = neutron_client self.ovn_nb_idl = ovn_nb_idl self.member_manager = member_manager self.agent_id = agent_id def bind_router_interfaces_for_network(self, network_id, ha_chassis_group): """Bind router interface ports to network's HA chassis group. This is the main entry point called by event handlers when a network HA chassis group is created or updated. It finds all router interface ports on the network and binds them to the specified HA chassis group. :param network_id: Neutron network UUID :param ha_chassis_group: OVN HA_Chassis_Group UUID or name """ if not self._should_manage_network(network_id): return try: router_ports = self._get_router_interface_ports(network_id) if not router_ports: return for port in router_ports: try: self._bind_lrp_to_ha_group( port.id, ha_chassis_group, network_id) except (ovs_exc.OvsdbAppException, RuntimeError, AttributeError): LOG.exception("Failed to bind router port %s to HA " "chassis group %s", port.id, ha_chassis_group) LOG.info("Completed router HA binding for network %s: processed " "%d router ports to HA chassis group %s", network_id, len(router_ports), ha_chassis_group) except sdk_exc.OpenStackCloudException: LOG.exception("Failed to query router ports for network %s", network_id) def _get_router_interface_ports(self, network_id): """Query Neutron for router interface ports on a network. :param network_id: Neutron network UUID :returns: List of router interface port objects """ try: router_ports = list(self.neutron_client.network.ports( network_id=network_id, device_owner=n_const.DEVICE_OWNER_ROUTER_INTF)) return router_ports except sdk_exc.OpenStackCloudException: LOG.exception("Failed to get router interface ports for " "network %s", network_id) raise def _get_current_ha_chassis_group(self, lrp): """Extract current HA chassis group from LRP. :param lrp: OVN Logical_Router_Port row :returns: Current HA chassis group UUID/name or None """ current_ha_group = None if hasattr(lrp, 'ha_chassis_group'): ha_group = lrp.ha_chassis_group if ha_group: current_ha_group = ha_group[0] if isinstance( ha_group, list) else ha_group return current_ha_group def _update_lrp_ha_chassis_group(self, port_id, ha_chassis_group, network_id): """Update a single router port's HA chassis group if needed. Checks if the router port already has the correct HA chassis group (idempotent operation) and only updates if needed. :param port_id: Neutron port UUID :param ha_chassis_group: OVN HA_Chassis_Group UUID or name :param network_id: Neutron network UUID (for logging) :returns: True if port was updated, False if already correct or skipped :raises: OvsdbAppException, RuntimeError, AttributeError on errors """ lrp_name = ovn_utils.ovn_lrouter_port_name(port_id) try: lrp = self.ovn_nb_idl.lrp_get(lrp_name).execute(check_error=True) except idlutils.RowNotFound: return False current_ha_group = self._get_current_ha_chassis_group(lrp) if current_ha_group == ha_chassis_group: return False self.ovn_nb_idl.lrp_set_ha_chassis_group( lrp_name, ha_chassis_group).execute(check_error=True) LOG.info("Updated router port %s HA chassis group from %s to %s " "(network %s)", port_id, current_ha_group, ha_chassis_group, network_id) return True def _bind_lrp_to_ha_group(self, port_id, ha_chassis_group, network_id): """Set LRP ha_chassis_group in OVN. Checks if the router port already has the correct HA chassis group (idempotent operation) and only updates if needed. :param port_id: Neutron port UUID :param ha_chassis_group: OVN HA_Chassis_Group UUID or name :param network_id: Neutron network UUID (for logging) """ try: self._update_lrp_ha_chassis_group( port_id, ha_chassis_group, network_id) except (ovs_exc.OvsdbAppException, RuntimeError, AttributeError): LOG.exception("Failed to update HA chassis group for router " "port %s", port_id) raise def _should_manage_network(self, network_id): """Check if this agent should manage the network via hash ring. Uses consistent hashing to determine if this agent is responsible for managing the network. This ensures only one agent processes events for each network in a multi-agent deployment. :param network_id: Neutron network UUID :returns: True if this agent owns the network, False otherwise """ try: network_key = network_id.encode('utf-8') hashring_members = list( self.member_manager.hashring[network_key]) return self.agent_id in hashring_members except (KeyError, AttributeError, TypeError): LOG.exception("Hash ring lookup failed for network %s, skipping " "network management", network_id) return False def _get_networks_with_ha_chassis_groups(self): """Find all networks that have HA chassis groups. Queries OVN's HA_Chassis_Group table for network-level groups (identified by having neutron:network_id in external_ids). Filters out router-level groups (which have neutron:router_id). :returns: Dict mapping network_id -> ha_chassis_group_uuid """ network_ha_groups = {} try: if not hasattr(self.ovn_nb_idl, 'tables'): LOG.error("OVN NB IDL not available, router HA binding " "cannot function. Baremetal nodes may not have " "connectivity to router gateways.") return network_ha_groups if 'HA_Chassis_Group' not in self.ovn_nb_idl.tables: LOG.debug("HA_Chassis_Group table not found in OVN") return network_ha_groups table = self.ovn_nb_idl.tables['HA_Chassis_Group'] for row in table.rows.values(): if not hasattr(row, 'external_ids'): continue external_ids = row.external_ids network_id = external_ids.get( ovn_const.OVN_NETWORK_ID_EXT_ID_KEY) if network_id: # Use any HA chassis group that has a network_id, # regardless of whether it also has a router_id. In # unified HA chassis group scenarios, the same group is # used for both the network and the router. network_ha_groups[network_id] = row.uuid LOG.debug("Found %d network-level HA chassis groups", len(network_ha_groups)) return network_ha_groups except (AttributeError, KeyError): LOG.exception("Failed to get networks with HA chassis groups " "from OVN") return network_ha_groups def reconcile(self): """Periodic reconciliation of router HA binding. Discovers all networks with HA chassis groups and ensures their router interface ports are bound to those groups. This catches: 1. Routers added to networks after HA chassis group exists 2. Missed events (agent down/restarting during event) 3. Race conditions or out-of-order event processing 4. Manual changes to LRP ha_chassis_group settings This method is called periodically (default: 600s / 10 minutes) to ensure eventual consistency even if event-driven binding fails. """ LOG.info("Starting router HA binding reconciliation") try: network_ha_groups = self._get_networks_with_ha_chassis_groups() if not network_ha_groups: return networks_processed = 0 ports_updated = 0 for network_id, ha_chassis_group in network_ha_groups.items(): if not self._should_manage_network(network_id): continue networks_processed += 1 try: router_ports = self._get_router_interface_ports(network_id) if not router_ports: continue for port in router_ports: try: updated = self._update_lrp_ha_chassis_group( port.id, ha_chassis_group, network_id) if updated: ports_updated += 1 except (ovs_exc.OvsdbAppException, RuntimeError, AttributeError): LOG.exception("Failed to reconcile router port %s", port.id) except sdk_exc.OpenStackCloudException: LOG.exception("Failed to query router ports for network " "%s " "during reconciliation", network_id) LOG.info("Router HA binding reconciliation complete: processed %d " "networks, updated %d router ports", networks_processed, ports_updated) except Exception: LOG.exception("Router HA binding reconciliation failed") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/common.py0000664000175000017500000000417415157004031022614 0ustar00zuulzuul# 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 xml.etree import ElementTree from oslo_config import cfg from oslo_log import log as logging import stevedore from networking_baremetal import constants from networking_baremetal import exceptions DRIVER_NAMESPACE = 'networking_baremetal.drivers' CONF = cfg.CONF LOG = logging.getLogger(__name__) def txt_subelement(parent, tag, text, *args, **kwargs): element = ElementTree.SubElement(parent, tag, *args, **kwargs) element.text = text return element def config_to_xml(config): element = ElementTree.Element(constants.CFG_ELEMENT) for conf in config: element.append(conf.to_xml_element()) return ElementTree.tostring(element).decode("utf-8") def driver_mgr(device_id): driver = CONF[device_id].driver try: mgr = stevedore.driver.DriverManager( namespace=DRIVER_NAMESPACE, name=driver, invoke_on_load=True, invoke_args=(device_id,), on_load_failure_callback=_load_failure_hook ) except stevedore.exception.NoUniqueMatch as exc: raise exceptions.DriverEntrypointLoadError( entry_point=f'{DRIVER_NAMESPACE}.{driver}', err=exc) return mgr.driver def _load_failure_hook(manager, entrypoint, exception): LOG.error("Driver manager %(manager)s failed to load device plugin " "%(entrypoint)s: %(exp)s", {'manager': manager, 'entrypoint': entrypoint, 'exp': exception}) raise exceptions.DriverEntrypointLoadError(entry_point=entrypoint, err=exception) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/config.py0000664000175000017500000000757315157004031022577 0ustar00zuulzuul# 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 CONF = cfg.CONF LOG = logging.getLogger(__name__) _opts = [ cfg.ListOpt('enabled_devices', default=[], sample_default=['common-example', 'netconf-openconfig-example'], help=('Enabled devices for which the plugin should manage' 'configuration. Driver specific configuration for each ' 'device must be added in separate sections.')), ] _device_opts = [ cfg.StrOpt('driver', help='The driver to use when configuring the device'), cfg.StrOpt('switch_id', help='The switch ID, MAC address of the device.'), cfg.StrOpt('switch_info', help=('Optional string field to be used to store any ' 'vendor-specific information.')), cfg.ListOpt('physical_networks', default=[], help='A list of physical networks mapped to this device.'), cfg.BoolOpt('manage_vlans', default=True, help=('Set this to False for the device if VLANs should not ' 'be create and deleted on the device.')), ] _conductor_groups_opts = [ cfg.ListOpt('conductor_groups', default=[], help=('List of conductor groups this networking-baremetal ' 'instance should manage. If empty, all ports will be ' 'queried.')), ] networking_baremetal_group = cfg.OptGroup( name='networking_baremetal', title='ML2 networking-baremetal options') CONF.register_group(networking_baremetal_group) CONF.register_opts(_opts, group=networking_baremetal_group) conductor_groups_opt_group = cfg.OptGroup( name='conductor_groups', title='ML2 networking-baremetal conductor groups filtering options') CONF.register_group(conductor_groups_opt_group) CONF.register_opts(_conductor_groups_opts, group=conductor_groups_opt_group) for device in CONF.networking_baremetal.enabled_devices: group = cfg.OptGroup( name=device, title=f'{device} Device options') CONF.register_group(group) CONF.register_opts(_device_opts, group=group) def list_opts(): return [('networking_baremetal', _opts), ('conductor_groups', _conductor_groups_opts)] def list_common_device_driver_opts(): return [('networking_baremetal', _opts), ('common-example', _device_opts), ('conductor_groups', _conductor_groups_opts)] def get_devices(): """Get enabled network devices from configuration This is called during driver initialization, during initialization additional driver specific configuration is loaded and the drivers validation method is called. """ devices = dict() for dev in CONF.networking_baremetal.enabled_devices: if not CONF[dev].driver: LOG.error('IGNORING invalid device %s, driver not specified.', dev) if not CONF[dev].switch_id and not CONF[dev].switch_info: LOG.error('IGNORING invalid device %s, switch_id and/or ' 'switch_info is required', dev) if CONF[dev].switch_id: devices[CONF[dev].switch_id] = dev if CONF[dev].switch_info: devices[CONF[dev].switch_info] = dev return devices ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/constants.py0000664000175000017500000001167315157004031023342 0ustar00zuulzuul# Copyright 2017 Cisco Systems, 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 enum BAREMETAL_AGENT_TYPE = "Baremetal Node" BAREMETAL_BINARY = 'ironic-neutron-agent' BAREMETAL_NONE = 'baremetal:none' # External baremetal port device_owner LOCAL_LINK_INFO = 'local_link_information' LOCAL_GROUP_INFO = 'local_group_information' IFACE_TYPE_ETHERNET = 'ethernet' IFACE_TYPE_AGGREGATE = 'aggregate' IFACE_TYPE_BASE = 'base' LAG_TYPE_LACP = 'LACP' LAG_TYPE_SATIC = 'SATIC' LACP_TIMEOUT_LONG = 'LONG' LACP_TIMEOUT_SHORT = 'SHORT' LACP_PERIOD_FAST = 'FAST' LACP_PERIOD_SLOW = 'SLOW' LACP_ACTIVITY_ACTIVE = 'ACTIVE' LACP_ACTIVITY_PASSIVE = 'PASSIVE' LACP_MIN_LINKS = 'bond_min_links' LACP_INTERVAL = 'bond_lacp_rate' # These bond modes require switch configuration the plugin cannot create. PRE_CONF_ONLY_BOND_MODES = {'balance-rr', '0', 'balance-xor', '2', 'broadcast', '3'} LACP_BOND_MODES = {'802.3ad', '4'} NON_SWITCH_BOND_MODES = {'active-backup', '1', 'balance-tlb', '5', 'balance-alb', '6'} VLAN_ACTIVE = 'ACTIVE' VLAN_SUSPENDED = 'SUSPENDED' VLAN_MODE_TRUNK = 'TRUNK' VLAN_MODE_ACCESS = 'ACCESS' VLAN_RANGE = range(1, 4094) PORT_ID = 'port_id' SWITCH_ID = 'switch_id' SWITCH_INFO = 'switch_info' class NetconfEditConfigOperation(enum.Enum): """RFC 6241 - operation attribute The "operation" attribute has one of the following values: merge: The configuration data identified by the element containing this attribute is merged with the configuration at the corresponding level in the configuration datastore identified by the parameter. This is the default behavior. replace: The configuration data identified by the element containing this attribute replaces any related configuration in the configuration datastore identified by the parameter. If no such configuration data exists in the configuration datastore, it is created. Unlike a operation, which replaces the entire target configuration, only the configuration actually present in the parameter is affected. create: The configuration data identified by the element containing this attribute is added to the configuration if and only if the configuration data does not already exist in the configuration datastore. If the configuration data exists, an element is returned with an value of "data-exists". delete: The configuration data identified by the element containing this attribute is deleted from the configuration if and only if the configuration data currently exists in the configuration datastore. If the configuration data does not exist, an element is returned with an value of "data-missing". remove: The configuration data identified by the element containing this attribute is deleted from the configuration if the configuration data currently exists in the configuration datastore. If the configuration data does not exist, the "remove" operation is silently ignored by the server. """ MERGE = 'merge' REPLACE = 'replace' CREATE = 'create' DELETE = 'delete' REMOVE = 'remove' CFG_ELEMENT = 'config' IANA_NETCONF_CAPABILITIES = { # [RFC4741][RFC6241] ':base:1.0': 'urn:ietf:params:netconf:base:1.0', # [RFC4741] ':confirmed-commit': 'urn:ietf:params:netconf:capability:confirmed-commit:1.0', ':validate': 'urn:ietf:params:netconf:capability:validate:1.0', # [RFC6241] ':base:1.1': 'urn:ietf:params:netconf:base:1.1', ':writable-running': 'urn:ietf:params:netconf:capability:writable-running:1.0', ':candidate': 'urn:ietf:params:netconf:capability:candidate:1.0', ':confirmed-commit:1.1': 'urn:ietf:params:netconf:capability:confirmed-commit:1.1', ':rollback-on-error': 'urn:ietf:params:netconf:capability:rollback-on-error:1.0', ':validate:1.1': 'urn:ietf:params:netconf:capability:validate:1.1', ':startup': 'urn:ietf:params:netconf:capability:startup:1.0', } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1773930567.837995 networking_baremetal-7.2.0/networking_baremetal/drivers/0000775000175000017500000000000015157004110022420 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/drivers/__init__.py0000664000175000017500000000000015157004031024521 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/drivers/base.py0000664000175000017500000000677515157004031023725 0ustar00zuulzuul# 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 class BaseDeviceClient(object, metaclass=abc.ABCMeta): def __init__(self, device): self.device = device def get_client_args(self): """Get client connection arguments from configuration""" def get(self, **kwargs): """Get current configuration/state from device""" def edit_config(self, config): """Edit configuration on the device :param config: The configuration to apply to the device """ class BaseDeviceDriver(object, metaclass=abc.ABCMeta): SUPPORTED_BOND_MODES = set() def __init__(self, device): self.client = BaseDeviceClient(device) self.device = device def load_config(self): """Register driver specific configuration All drivers should register driver specific options in the device specific config group. This method will be called during mechanism driver initialization. """ def validate(self): """Driver validation This method will be called during mechanism driver initialization. Raising any exception other than DriverValidationError will cause service initialization failure. :raises DriverValidationError: On validation failure. """ def create_network(self, context): """Create network on device :param context: NetworkContext instance describing the new network. """ def update_network(self, context): """Update network on device :param context: NetworkContext instance describing the new network. """ def delete_network(self, context): """Delete network on device :param context: NetworkContext instance describing the new network. """ def create_port(self, context, segment, links): """Create/Configure port on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param segment: segment dictionary describing segment to bind :param links: Local link information filtered for the device. """ def update_port(self, context, links): """Update port on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param links: Local link information filtered for the device. """ def delete_port(self, context, links, current=True): """Delete/Un-configure port on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param links: Local link information filtered for the device. :param current: Boolean, when true use context.current, when false use context.original """ ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1773930567.837995 networking_baremetal-7.2.0/networking_baremetal/drivers/netconf/0000775000175000017500000000000015157004110024054 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/drivers/netconf/openconfig.py0000664000175000017500000012056015157004031026563 0ustar00zuulzuul# 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 random import re from urllib.parse import parse_qs as urlparse_qs from urllib.parse import urlparse import uuid from xml.etree import ElementTree from ncclient import manager from ncclient.operations.rpc import RPCError from ncclient.transport.errors import AuthenticationError from ncclient.transport.errors import SessionCloseError from ncclient.transport.errors import SSHError from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import provider_net from neutron_lib import constants as n_const from neutron_lib import exceptions as n_exec from neutron_lib.plugins.ml2 import api from oslo_config import cfg from oslo_log import log as logging import tenacity from networking_baremetal import common from networking_baremetal import config from networking_baremetal import constants from networking_baremetal.constants import NetconfEditConfigOperation as nc_op from networking_baremetal.drivers import base from networking_baremetal import exceptions from networking_baremetal.openconfig.interfaces import interfaces from networking_baremetal.openconfig.lacp import lacp from networking_baremetal.openconfig.network_instance import network_instance from networking_baremetal.openconfig.vlan import vlan CONF = cfg.CONF LOG = logging.getLogger(__name__) LOCK_DENIED_TAG = 'lock-denied' # [RFC 4741] CANDIDATE = 'candidate' RUNNING = 'running' DEFERRED = 'deferred' # Options for the device, maps to the local_link_information in the # port binding profile. _DEVICE_OPTS = [ cfg.StrOpt('network_instance', default='default', advanced=True, help=('The L2, L3, or L2+L3 forwarding instance to use when ' 'defining VLANs on the device.')), cfg.DictOpt('port_id_re_sub', default={}, sample_default={'pattern': 'Ethernet', 'repl': 'eth'}, help=('Regular expression pattern and replacement string. ' 'Some devices do not use the port description from ' 'LLDP in Netconf configuration. If the regular ' 'expression pattern and replacement string is set the ' 'port_id will be modified before passing configuration ' 'to the device.')), cfg.ListOpt('disabled_properties', item_type=cfg.types.String( choices=['port_mtu']), default=[], help=('A list of properties that should not be used, ' 'currently only "port_mtu" is valid')), cfg.BoolOpt('manage_lacp_aggregates', default=True, help=('When set to true the driver will manage LACP ' 'aggregates if link_group_information is defined in ' 'the binding:profile. When this is false the driver ' 'expect the link aggregation to be pre-configured on ' 'the device, and only perform vlan plugging.')), cfg.StrOpt('link_aggregate_prefix', default='Port-Channel', help=('The device specific prefix used for link-aggregation ' 'ports. Common values: "po", "port-channel" or ' '"Port-Channel".')), cfg.StrOpt('link_aggregate_range', default='1000..2000', help=('Range of link aggregation interface IDs that the driver ' 'can use when managing link aggregates.')), ] # Configuration option for Netconf client connection _NCCLIENT_OPTS = [ cfg.StrOpt('host', help=('The hostname or IP address to use for connecting to the ' 'netconf device.'), sample_default='device.example.com'), cfg.StrOpt('username', help='The username to use for SSH authentication.', sample_default='netconf'), cfg.IntOpt('port', default=830, help=('The port to use for connection to the netconf ' 'device.')), cfg.StrOpt('password', help=('The password used if using password authentication, or ' 'the passphrase to use for unlocking keys that require ' 'it. (To disable attempting key authentication ' 'altogether, set options *allow_agent* and ' '*look_for_keys* to `False`.'), sample_default='secret'), cfg.StrOpt('key_filename', help='Private key filename', default='~/.ssh/id_rsa'), cfg.BoolOpt('hostkey_verify', default=True, help=('Enables hostkey verification from ' '~/.ssh/known_hosts')), cfg.DictOpt('device_params', default={'name': 'default'}, help=('ncclient device handler parameters, see ncclient ' 'documentation for supported device handlers.')), cfg.BoolOpt('allow_agent', default=True, help='Enables querying SSH agent (if found) for keys.'), cfg.BoolOpt('look_for_keys', default=True, help=('Enables looking in the usual locations for ssh keys ' '(e.g. :file:`~/.ssh/id_*`)')), ] def list_driver_opts(): return [('networking_baremetal', config._opts), ('netconf-openconfig-example', config._device_opts + _DEVICE_OPTS + _NCCLIENT_OPTS)] class NetconfLockDenied(n_exec.NeutronException): message = ('Access to the requested lock is denied because the' 'lock is currently held by another entity.') class NetconfOpenConfigClient(base.BaseDeviceClient): def __init__(self, device): super().__init__(device) self.device = device self.capabilities = set() # Reduce the log level for ncclient, it is very chatty by default netconf_logger = logging.getLogger('ncclient') netconf_logger.setLevel(logging.WARNING) @staticmethod def _get_lock_session_id(err_info): """Parse lock-denied error [RFC6241] error-tag: lock-denied error-type: protocol error-severity: error error-info: : session ID of session holding the requested lock, or zero to indicate a non-NETCONF entity holds the lock Description: Access to the requested lock is denied because the lock is currently held by another entity. """ root = ElementTree.fromstring(err_info) session_id = root.find( "./{urn:ietf:params:xml:ns:netconf:base:1.0}session-id").text return session_id @staticmethod def process_capabilities(server_capabilities): capabilities = set() for capability in server_capabilities: for k, v in constants.IANA_NETCONF_CAPABILITIES.items(): if v in capability: capabilities.add(k) if capability.startswith('http://openconfig.net/yang'): openconfig_module = urlparse_qs( urlparse(capability).query).get('module').pop() capabilities.add(openconfig_module) return capabilities def get_capabilities(self): # https://github.com/ncclient/ncclient/issues/525 _ignore_close_issue_525 = False args = self.get_client_args() try: with manager.connect(**args) as nc_client: server_capabilities = nc_client.server_capabilities _ignore_close_issue_525 = True except SessionCloseError as e: if not _ignore_close_issue_525: raise e except (SSHError, AuthenticationError) as e: raise exceptions.DeviceConnectionError(device=self.device, err=e) return self.process_capabilities(server_capabilities) def get_client_args(self): """Get client connection arguments from configuration :param device: Device identifier """ args = dict( host=CONF[self.device].host, port=CONF[self.device].port, username=CONF[self.device].username, hostkey_verify=CONF[self.device].hostkey_verify, device_params=CONF[self.device].device_params, keepalive=True, allow_agent=CONF[self.device].allow_agent, look_for_keys=CONF[self.device].look_for_keys, ) if CONF[self.device].key_filename: args['key_filename'] = CONF[self.device].key_filename if CONF[self.device].password: args['password'] = CONF[self.device].password return args def get(self, **kwargs): """Get current configuration/staate from device""" # https://github.com/ncclient/ncclient/issues/525 _ignore_close_issue_525 = False query = kwargs.get('query') q_filter = ElementTree.tostring(query.to_xml_element()).decode('utf-8') try: with manager.connect(**self.get_client_args()) as client: reply = client.get(filter=('subtree', q_filter)) _ignore_close_issue_525 = True except SessionCloseError as e: # https://github.com/ncclient/ncclient/issues/525 if not _ignore_close_issue_525: raise e except RPCError as e: LOG.error('Netconf XML: %s', q_filter) raise e return reply.data_xml @tenacity.retry( reraise=True, retry=tenacity.retry_if_exception_type(NetconfLockDenied), wait=tenacity.wait_random_exponential(multiplier=1, min=2, max=10), stop=tenacity.stop_after_attempt(5)) def get_lock_and_configure(self, client, source, config, deferred_allocations): try: with client.locked(source): # Aggregate ID deferred until we have config lock # Get free aggregate ID by querying the device and update conf if deferred_allocations: aggregate_id = self.get_free_aggregate_id(client) self.allocate_deferred(aggregate_id, config) xml_config = common.config_to_xml(config) LOG.info( 'Sending configuration to Netconf device %(dev)s: ' '%(conf)s', {'dev': self.device, 'conf': xml_config}) if source == CANDIDATE: # Revert the candidate configuration to the current # running configuration. Any uncommitted changes are # discarded. client.discard_changes() # Edit the candidate configuration client.edit_config(target=source, config=xml_config) # Validate the candidate configuration if (':validate' in self.capabilities or ':validate:1.1' in self.capabilities): client.validate(source='candidate') # Commit the candidate config, 30 seconds timeout if (':confirmed-commit' in self.capabilities or ':confirmed-commit:1.1' in self.capabilities): client.commit(confirmed=True, timeout=str(30)) # Confirm the commit, if this commit does not # succeed the device will revert the config after # 30 seconds. client.commit() elif source == RUNNING: client.edit_config(target=source, config=xml_config) # TODO(hjensas): persist config. except RPCError as err: if err.tag == LOCK_DENIED_TAG: # If the candidate config is modified, some vendors do not # permit a new session to take a lock. This is per the RFC, # in this case a lock-denied error where session-id == 0 is # returned, because no session is actually holding the # lock we can discard changes which will release the lock. if (source == CANDIDATE and self._get_lock_session_id(err.info) == '0'): client.discard_changes() raise NetconfLockDenied() else: LOG.error('Netconf XML: %s', common.config_to_xml(config)) raise err def edit_config(self, config, deferred_allocations=False): """Edit configuration on the device :param config: Configuration, or list of configurations :param deferred_allocations: Used for link aggregates, the aggregate id cannot be allocated before device config is locked. When this is true an available aggregate id is identified by querying the device, and the configuration objects are updated accordingly before configuration is sent to the device. """ # https://github.com/ncclient/ncclient/issues/525 _ignore_close_issue_525 = False if not isinstance(config, list): config = [config] try: with manager.connect(**self.get_client_args()) as client: self.capabilities = self.process_capabilities( client.server_capabilities) if ':candidate' in self.capabilities: self.get_lock_and_configure( client, CANDIDATE, config, deferred_allocations) _ignore_close_issue_525 = True elif ':writable-running' in self.capabilities: self.get_lock_and_configure( client, RUNNING, config, deferred_allocations) _ignore_close_issue_525 = True except SessionCloseError as e: if not _ignore_close_issue_525: raise e def get_aggregation_ids(self): """Get aggregation IDs and aggregation prefix from config""" prefix = CONF[self.device].link_aggregate_prefix aggregate_id_range = CONF[self.device].link_aggregate_range.split('..') aggregate_ids = {f'{prefix}{x}' for x in range(int(aggregate_id_range[0]), int(aggregate_id_range[1]) + 1)} return aggregate_ids @staticmethod def allocate_deferred(aggregate_id, config): """Set aggregation id where it was deferred :param aggregate_id: Aggregation ID for the link aggregate, for example 'po123' :param config: Configuration objects to update """ for conf in config: if isinstance(conf, interfaces.Interfaces): for iface in conf: if isinstance(iface, interfaces.InterfaceAggregate): if iface.name == DEFERRED: iface.name = aggregate_id if iface.config.name == DEFERRED: iface.config.name = aggregate_id elif isinstance(iface, interfaces.InterfaceEthernet): if iface.ethernet.config.aggregate_id == DEFERRED: iface.ethernet.config.aggregate_id = aggregate_id if isinstance(conf, lacp.LACP): for lacp_iface in conf.interfaces.interfaces: if lacp_iface.name == DEFERRED: lacp_iface.name = aggregate_id def get_free_aggregate_id(self, client_locked): """Get free aggregate id by querying device config :param client_locked: Netconf client with active configuration lock """ aggregate_prefix = CONF[self.device].link_aggregate_prefix aggregate_ids = self.get_aggregation_ids() # Create a interfaces query oc_ifaces = interfaces.Interfaces() # Use empty string for the name, so the 'get' return all interfaces oc_iface = oc_ifaces.add('', interface_type=constants.IFACE_TYPE_BASE) # Don't need the config group del oc_iface.config # Get interfaces from device element = oc_ifaces.to_xml_element() device_interfaces = client_locked.get(filter=( 'subtree', ElementTree.tostring(element).decode("utf-8"))) # Find all interface names and filter on aggregate_prefix root = ElementTree.fromstring(device_interfaces.data_xml) used_aggregate_ids = { x.text for x in root.findall(f'.//{{{oc_ifaces.NAMESPACE}}}name') if x.text.startswith(aggregate_prefix)} # Get the difference, and make a random choice available_aggregate_ids = aggregate_ids.difference(used_aggregate_ids) return random.choice(list(available_aggregate_ids)) class NetconfOpenConfigDriver(base.BaseDeviceDriver): SUPPORTED_BOND_MODES = set().union(constants.NON_SWITCH_BOND_MODES, constants.LACP_BOND_MODES, constants.PRE_CONF_ONLY_BOND_MODES) def __init__(self, device): super().__init__(device) self.client = NetconfOpenConfigClient(device) self.device = device def validate(self): try: LOG.info('Device %(device)s was loaded. Device capabilities: ' '%(caps)s', {'device': self.device, 'caps': self.client.get_capabilities()}) except exceptions.DeviceConnectionError as e: raise exceptions.DriverValidationError(device=self.device, err=e) def load_config(self): """Register driver specific configuration""" CONF.register_opts(_DEVICE_OPTS, group=self.device) CONF.register_opts(_NCCLIENT_OPTS, group=self.device) def create_network(self, context): """Create network on device :param context: NetworkContext instance describing the new network. """ network = context.current segmentation_id = network[provider_net.SEGMENTATION_ID] net_instances = network_instance.NetworkInstances() net_instance = net_instances.add(CONF[self.device].network_instance) _vlan = net_instance.vlans.add(segmentation_id) # Devices has limitations for vlan names, use the hex variant of the # network UUID which is shorter. _vlan.config.name = self._uuid_as_hex(network[api.ID]) _vlan.config.status = constants.VLAN_ACTIVE self.client.edit_config(net_instances) def update_network(self, context): """Update network on device :param context: NetworkContext instance describing the new network. """ network = context.current network_orig = context.original segmentation_id = network[provider_net.SEGMENTATION_ID] segmentation_id_orig = network_orig[provider_net.SEGMENTATION_ID] admin_state = network['admin_state_up'] admin_state_orig = network_orig['admin_state_up'] add_net_instances = network_instance.NetworkInstances() add_net_instance = add_net_instances.add( CONF[self.device].network_instance) del_net_instances = None need_update = False if segmentation_id: _vlan = add_net_instance.vlans.add(segmentation_id) # Devices has limitations for vlan names, use the hex variant of # the network UUID which is shorter. _vlan.config.name = self._uuid_as_hex(network[api.ID]) if network['admin_state_up']: _vlan.config.status = constants.VLAN_ACTIVE else: _vlan.config.status = constants.VLAN_SUSPENDED if admin_state != admin_state_orig: need_update = True if segmentation_id_orig and segmentation_id != segmentation_id_orig: need_update = True del_net_instances = network_instance.NetworkInstances() del_net_instance = del_net_instances.add( CONF[self.device].network_instance) vlan_orig = del_net_instance.vlans.remove(segmentation_id_orig) # Not all devices support removing a VLAN, in that case lets # make sure the VLAN is suspended and set a name to indicate the # network was deleted. vlan_orig.config.name = f'neutron-DELETED-{segmentation_id_orig}' vlan_orig.config.status = constants.VLAN_SUSPENDED if not need_update: return # If the segmentation ID changed, delete the old VLAN first to avoid # vlan name conflict. if del_net_instances is not None: self.client.edit_config(del_net_instances) self.client.edit_config(add_net_instances) def delete_network(self, context): """Delete network on device :param context: NetworkContext instance describing the new network. """ network = context.current segmentation_id = network[provider_net.SEGMENTATION_ID] net_instances = network_instance.NetworkInstances() net_instance = net_instances.add(CONF[self.device].network_instance) _vlan = net_instance.vlans.remove(segmentation_id) # Not all devices support removing a VLAN, in that case lets # make sure the VLAN is suspended and set a name to indicate the # network was deleted. _vlan.config.name = f'neutron-DELETED-{segmentation_id}' _vlan.config.status = constants.VLAN_SUSPENDED self.client.edit_config(net_instances) def create_port(self, context, segment, links): """Create/Configure port on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param segment: segment dictionary describing segment to bind :param links: Local link information filtered for the device. """ port = context.current binding_profile = port[portbindings.PROFILE] local_group_information = binding_profile.get( constants.LOCAL_GROUP_INFO, {}) bond_mode = local_group_information.get('bond_mode') if segment[api.NETWORK_TYPE] != n_const.TYPE_VLAN: switched_vlan = None else: switched_vlan = vlan.VlanSwitchedVlan() switched_vlan.config.operation = nc_op.REPLACE switched_vlan.config.interface_mode = constants.VLAN_MODE_ACCESS switched_vlan.config.access_vlan = segment[api.SEGMENTATION_ID] if not bond_mode or bond_mode in constants.NON_SWITCH_BOND_MODES: self.create_non_bond(context, switched_vlan, links) elif bond_mode in constants.LACP_BOND_MODES: if CONF[self.device].manage_lacp_aggregates: self.create_lacp_aggregate(context, switched_vlan, links) else: self.create_pre_conf_aggregate(context, switched_vlan, links) elif bond_mode in constants.PRE_CONF_ONLY_BOND_MODES: self.create_pre_conf_aggregate(context, switched_vlan, links) def create_non_bond(self, context, switched_vlan, links): """Create/Configure ports on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param switched_vlan: switched_vlan OpenConfig object :param links: Local link information filtered for the device. """ port = context.current network = context.network.current ifaces = interfaces.Interfaces() for link in links: link_port_id = link.get(constants.PORT_ID) link_port_id = self._port_id_resub(link_port_id) iface = ifaces.add(link_port_id) iface.config.enabled = port['admin_state_up'] if 'port_mtu' not in CONF[self.device].disabled_properties: iface.config.mtu = network[api.MTU] iface.config.description = f'neutron-{port[api.ID]}' if switched_vlan is not None: iface.ethernet.switched_vlan = switched_vlan else: del iface.ethernet self.client.edit_config(ifaces) def create_lacp_aggregate(self, context, switched_vlan, links): """Create/Configure LACP aggregate on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param switched_vlan: switched_vlan OpenConfig object :param links: Local link information filtered for the device. """ port = context.current network = context.network.current binding_profile = port[portbindings.PROFILE] local_group_information = binding_profile.get( constants.LOCAL_GROUP_INFO, {}) dev_type = CONF[self.device].device_params.get('name') bond_properties = local_group_information.get('bond_properties', {}) lacp_interval = bond_properties.get(constants.LACP_INTERVAL) min_links = bond_properties.get(constants.LACP_MIN_LINKS) ifaces = interfaces.Interfaces() _lacp = lacp.LACP() lacp_iface = _lacp.interfaces.add(DEFERRED) lacp_iface.operation = nc_op.REPLACE lacp_iface.config.interval = (constants.LACP_PERIOD_FAST if lacp_interval in {'fast', 1, '1'} else constants.LACP_PERIOD_SLOW) # NX-API only allows configuring LACP interval rate on a port-channel # member which is not in shutdown state. Support would require a two # commit approach. if dev_type in {'nexus'}: LOG.warning('IGNORING LACP interval (bond_lacp_rate). The driver ' 'does not support LACP interval for this device type. ' 'Device: %(device)s, Port: %(port)s', {'device': self.device, 'port': port[api.ID]}) del lacp_iface.config.interval for link in links: link_port_id = link.get(constants.PORT_ID) link_port_id = self._port_id_resub(link_port_id) iface = ifaces.add( link_port_id, interface_type=constants.IFACE_TYPE_ETHERNET) iface.config.operation = nc_op.MERGE iface.config.enabled = port['admin_state_up'] if 'port_mtu' not in CONF[self.device].disabled_properties: iface.config.mtu = network[api.MTU] iface.config.description = f'neutron-{port[api.ID]}' iface.ethernet.config.aggregate_id = DEFERRED iface = ifaces.add(DEFERRED, interface_type=constants.IFACE_TYPE_AGGREGATE) iface.config.operation = nc_op.MERGE iface.config.name = DEFERRED iface.config.enabled = port['admin_state_up'] iface.config.description = f'neutron-{port[api.ID]}' iface.aggregation.config.lag_type = constants.LAG_TYPE_LACP if min_links: iface.aggregation.config.min_links = int(min_links) if switched_vlan is not None: iface.aggregation.switched_vlan = switched_vlan else: del iface.aggregation.switched_vlan self.client.edit_config([ifaces, _lacp], deferred_allocations=True) def create_pre_conf_aggregate(self, context, switched_vlan, links): """Create/Configure pre-configured aggregate on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param switched_vlan: switched_vlan OpenConfig object :param links: Local link information filtered for the device. """ port = context.current aggregate_ids = self.get_aggregate_ids(links) if not aggregate_ids: raise exceptions.PreConfiguredAggrergateNotFound( links=links, device=self.device) ifaces = interfaces.Interfaces() for aggregate_id in aggregate_ids: iface = ifaces.add(aggregate_id, interface_type=constants.IFACE_TYPE_AGGREGATE) iface.operation = nc_op.MERGE iface.config.enabled = port['admin_state_up'] if switched_vlan is not None: iface.aggregation.switched_vlan = switched_vlan else: del iface.aggregation.switched_vlan self.client.edit_config(ifaces) def update_port(self, context, links): """Update port on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param links: Local link information filtered for the device. """ if (not self.admin_state_changed(context) and not self.network_mtu_changed(context)): return port = context.current binding_profile = port[portbindings.PROFILE] local_group_information = binding_profile.get( constants.LOCAL_GROUP_INFO, {}) bond_mode = local_group_information.get('bond_mode') if not bond_mode or bond_mode in constants.NON_SWITCH_BOND_MODES: self.update_non_bond(context, links) elif bond_mode in constants.LACP_BOND_MODES: if CONF[self.device].manage_lacp_aggregates: self.update_lacp_aggregate(context, links) else: self.update_pre_conf_aggregate(context, links) elif bond_mode in constants.PRE_CONF_ONLY_BOND_MODES: self.update_pre_conf_aggregate(context, links) def update_non_bond(self, context, links): """Update port on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param links: Local link information filtered for the device. """ network = context.network.current ifaces = interfaces.Interfaces() port = context.current for link in links: link_port_id = link.get(constants.PORT_ID) link_port_id = self._port_id_resub(link_port_id) iface = ifaces.add(link_port_id) iface.config.enabled = port['admin_state_up'] if 'port_mtu' not in CONF[self.device].disabled_properties: iface.config.mtu = network[api.MTU] del iface.ethernet self.client.edit_config(ifaces) def update_lacp_aggregate(self, context, links): """Update LACP aggregate on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param links: Local link information filtered for the device. """ port = context.current network = context.network.current aggregate_ids = self.get_aggregate_ids(links) ifaces = interfaces.Interfaces() for link in links: link_port_id = link.get(constants.PORT_ID) link_port_id = self._port_id_resub(link_port_id) iface = ifaces.add(link_port_id, interface_type=constants.IFACE_TYPE_ETHERNET) iface.config.enabled = port['admin_state_up'] if 'port_mtu' not in CONF[self.device].disabled_properties: iface.config.mtu = network[api.MTU] del iface.ethernet for aggregate_id in aggregate_ids: iface = ifaces.add(aggregate_id, interface_type=constants.IFACE_TYPE_AGGREGATE) iface.operation = nc_op.MERGE iface.config.enabled = port['admin_state_up'] del iface.aggregation self.client.edit_config(ifaces) def update_pre_conf_aggregate(self, context, links): """Update pre-configured aggregate on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param links: Local link information filtered for the device. """ port = context.current aggregate_ids = self.get_aggregate_ids(links) if not aggregate_ids: raise exceptions.PreConfiguredAggrergateNotFound( links=links, device=self.device) ifaces = interfaces.Interfaces() for aggregate_id in aggregate_ids: iface = ifaces.add(aggregate_id, interface_type=constants.IFACE_TYPE_AGGREGATE) iface.operation = nc_op.MERGE iface.config.enabled = port['admin_state_up'] self.client.edit_config(ifaces) def delete_port(self, context, links, current=True): """Delete/Un-configure port on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param links: Local link information filtered for the device. :param current: Boolean, when true use context.current, when false use context.original """ port = context.current if current else context.original binding_profile = port[portbindings.PROFILE] local_group_information = binding_profile.get( constants.LOCAL_GROUP_INFO, {}) bond_mode = local_group_information.get('bond_mode') if not bond_mode or bond_mode in constants.NON_SWITCH_BOND_MODES: self.delete_non_bond(context, links) elif bond_mode in constants.LACP_BOND_MODES: if CONF[self.device].manage_lacp_aggregates: self.delete_lacp_aggregate(context, links) else: self.delete_pre_conf_aggregate(links) elif bond_mode in constants.PRE_CONF_ONLY_BOND_MODES: self.delete_pre_conf_aggregate(links) def delete_non_bond(self, context, links): """Delete/Un-configure port on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param links: Local link information filtered for the device. """ network = context.network.current ifaces = interfaces.Interfaces() for link in links: link_port_id = link.get(constants.PORT_ID) link_port_id = self._port_id_resub(link_port_id) iface = ifaces.add(link_port_id) iface.config.operation = nc_op.REMOVE # Not possible mark entire config for removal due to name leaf-ref # Set dummy values for properties to remove iface.config.description = '' iface.config.enabled = False if 'port_mtu' not in CONF[self.device].disabled_properties: iface.config.mtu = 0 if network[provider_net.NETWORK_TYPE] == n_const.TYPE_VLAN: iface.ethernet.switched_vlan.config.operation = nc_op.REMOVE else: del iface.ethernet self.client.edit_config(ifaces) def delete_lacp_aggregate(self, context, links): """Delete/Un-configure LACP aggregate on device :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param links: Local link information filtered for the device. """ network = context.network.current aggregate_ids = self.get_aggregate_ids(links) ifaces = interfaces.Interfaces() for link in links: link_port_id = link.get(constants.PORT_ID) link_port_id = self._port_id_resub(link_port_id) # Set up interface links for config remove iface = ifaces.add(link_port_id, interface_type=constants.IFACE_TYPE_ETHERNET) iface.config.operation = nc_op.REMOVE iface.config.description = '' iface.config.enabled = False if 'port_mtu' not in CONF[self.device].disabled_properties: iface.config.mtu = 0 iface.ethernet.config.operation = nc_op.REMOVE if network[provider_net.NETWORK_TYPE] == n_const.TYPE_VLAN: iface.ethernet.switched_vlan.config.operation = nc_op.REMOVE else: del iface.ethernet.switched_vlan # Set up lacp and aggregate interface for removal _lacp = lacp.LACP() for aggregate_id in aggregate_ids: # Remove LACP interface lacp_iface = _lacp.interfaces.add(aggregate_id) lacp_iface.operation = nc_op.REMOVE del lacp_iface.config # Remove Aggregate interface iface = ifaces.add(aggregate_id, interface_type=constants.IFACE_TYPE_AGGREGATE) iface.operation = nc_op.REMOVE del iface.config del iface.aggregation self.client.edit_config([_lacp, ifaces]) def delete_pre_conf_aggregate(self, links): """Delete/Un-configure pre-configured aggregate on device :param links: Local link information filtered for the device. """ aggregate_ids = self.get_aggregate_ids(links) if not aggregate_ids: raise exceptions.PreConfiguredAggrergateNotFound( links=links, device=self.device) ifaces = interfaces.Interfaces() for aggregate_id in aggregate_ids: iface = ifaces.add(aggregate_id, interface_type=constants.IFACE_TYPE_AGGREGATE) iface.config.enabled = False iface.aggregation.switched_vlan.config.operation = nc_op.REMOVE self.client.edit_config(ifaces) @staticmethod def _uuid_as_hex(_uuid): return uuid.UUID(_uuid).hex def _port_id_resub(self, link_port_id): """Replace pattern Regular expression pattern and replacement string. Some devices don not use the port description from LLDP in Netconf configuration. If the regular expression pattern and replacement string is set the port_id will be modified before passing configuration to the device. Replacing the leftmost non-overlapping occurrences of pattern in string by the replacement repl. """ if CONF[self.device].port_id_re_sub: pattern = CONF[self.device].port_id_re_sub.get('pattern') repl = CONF[self.device].port_id_re_sub.get('repl') link_port_id = re.sub(pattern, repl, link_port_id) return link_port_id def get_aggregate_ids(self, links): query = interfaces.Interfaces() for link in links: link_port_id = link.get(constants.PORT_ID) link_port_id = self._port_id_resub(link_port_id) # Set up query q_iface = query.add(link_port_id, interface_type=constants.IFACE_TYPE_ETHERNET) # Remove config and ethernet for broad filter. del q_iface.config del q_iface.ethernet # Get aggregate ids by querying the link interfaces xml_result = self.client.get(query=query) root = ElementTree.fromstring(xml_result) xpath_query_result = root.findall( './/{http://openconfig.net/yang/interfaces/aggregate}' 'aggregate-id') aggregate_ids = {x.text for x in xpath_query_result} return aggregate_ids @staticmethod def admin_state_changed(context): port = context.current port_orig = context.original return (port and port_orig and port['admin_state_up'] != port_orig['admin_state_up']) @staticmethod def network_mtu_changed(context): network = context.network.current network_orig = context.network.original return (network and network_orig and network[api.MTU] != network_orig[api.MTU]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/exceptions.py0000664000175000017500000000230015157004031023472 0ustar00zuulzuul# 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 import exceptions as n_exc class DriverEntrypointLoadError(n_exc.NeutronException): message = 'Failed to load entrypoint %(entry_point)s: %(err)s' class DriverValidationError(n_exc.NeutronException): message = 'Failed driver validation for device %(device)s: %(err)s' class DeviceConnectionError(n_exc.NeutronException): message = 'Driver failed connecting to device %(device)s: %(err)s' class PreConfiguredAggrergateNotFound(n_exc.NeutronException): message = ('Driver could not find the aggregate ID for the pre-configured ' 'link aggregate for links %(links)s on device %(device)s.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/ironic_client.py0000664000175000017500000000642315157004031024144 0ustar00zuulzuul# 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 keystoneauth1 import loading import openstack from oslo_config import cfg from oslo_log import log as logging import tenacity CONF = cfg.CONF LOG = logging.getLogger(__name__) _IRONIC_SESSION = None IRONIC_GROUP = 'ironic' _deprecated_opts = {} _deprecated_opts['endpoint_override'] = [ cfg.DeprecatedOpt('ironic_url', group=IRONIC_GROUP)] _deprecated_opts['region_name'] = [ cfg.DeprecatedOpt('os_region', group=IRONIC_GROUP)] _deprecated_opts['status_code_retries'] = [ cfg.DeprecatedOpt('max_retries', group=IRONIC_GROUP)] _deprecated_opts['status_code_retry_delay'] = [ cfg.DeprecatedOpt('retry_interval', group=IRONIC_GROUP)] IRONIC_OPTS = [ cfg.StrOpt('auth_strategy', default='keystone', deprecated_for_removal=True, deprecated_reason='This option is no longer used, please use ' 'the [ironic]/auth_type option instead.', choices=('keystone', 'noauth'), help='Method to use for authentication: noauth or keystone.'), ] def list_opts(): return [ (IRONIC_GROUP, IRONIC_OPTS + loading.get_adapter_conf_options(deprecated_opts=_deprecated_opts) + loading.get_session_conf_options(deprecated_opts=_deprecated_opts) + loading.get_auth_plugin_conf_options('v3password'))] def get_session(group): loading.register_adapter_conf_options(CONF, group, deprecated_opts=_deprecated_opts) loading.register_session_conf_options(CONF, group, deprecated_opts=_deprecated_opts) loading.register_auth_conf_options(CONF, group) CONF.register_opts(IRONIC_OPTS, group=group) auth = loading.load_auth_from_conf_options(CONF, group) session = loading.load_session_from_conf_options(CONF, group, auth=auth) return session def _get_ironic_session(): global _IRONIC_SESSION if not _IRONIC_SESSION: _IRONIC_SESSION = get_session(IRONIC_GROUP) return _IRONIC_SESSION @tenacity.retry( retry=tenacity.retry_if_exception_type(openstack.exceptions.NotSupported), wait=tenacity.wait_exponential(max=30)) def get_client(): """Get an ironic client connection.""" session = _get_ironic_session() try: return openstack.connection.Connection( session=session, oslo_conf=CONF).baremetal except openstack.exceptions.NotSupported as exc: LOG.error('Ironic API might not be running, failed to establish a ' 'connection with ironic, reason: %s. Retrying ...', exc) raise except Exception as exc: LOG.error('Failed to establish a connection with ironic, reason: %s', exc) raise ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/neutron_client.py0000664000175000017500000000617115157004031024353 0ustar00zuulzuul# 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 keystoneauth1 import loading import openstack from oslo_config import cfg from oslo_log import log as logging from networking_baremetal import ironic_client CONF = cfg.CONF LOG = logging.getLogger(__name__) _NEUTRON_SESSION = None NEUTRON_GROUP = 'neutron' def list_opts(): """Return neutron client configuration options.""" return [ (NEUTRON_GROUP, loading.get_adapter_conf_options() + loading.get_session_conf_options() + loading.get_auth_plugin_conf_options('v3password'))] def get_session(group): """Get a session for the specified config group. :param group: Configuration group name :returns: keystoneauth1 session """ loading.register_adapter_conf_options(CONF, group) loading.register_session_conf_options(CONF, group) loading.register_auth_conf_options(CONF, group) auth = loading.load_auth_from_conf_options(CONF, group) session = loading.load_session_from_conf_options(CONF, group, auth=auth) return session def _get_neutron_session(): """Get cached Neutron session, creating if needed. Returns Neutron-specific session if configured, otherwise falls back to ironic session for backwards compatibility. :returns: keystoneauth1 session """ global _NEUTRON_SESSION if not _NEUTRON_SESSION: # Check if neutron-specific auth is configured # If auth_type is set in [neutron], use neutron credentials # Otherwise fall back to ironic credentials for backwards compat if CONF[NEUTRON_GROUP].auth_type: LOG.info('Using Neutron-specific authentication credentials ' 'from [%s] section', NEUTRON_GROUP) _NEUTRON_SESSION = get_session(NEUTRON_GROUP) else: LOG.info('No Neutron-specific credentials configured, falling ' 'back to [ironic] section credentials') _NEUTRON_SESSION = ironic_client._get_ironic_session() return _NEUTRON_SESSION def get_client(): """Get a Neutron client connection via OpenStack SDK. :returns: OpenStack SDK Connection object for accessing network APIs """ session = _get_neutron_session() try: # Don't pass oslo_conf - let SDK discover services from # service catalog. This allows the same session to access # both Neutron and other services without config conflicts. return openstack.connection.Connection(session=session) except Exception as exc: LOG.error('Failed to establish a connection with Neutron, ' 'reason: %s', exc) raise ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8389952 networking_baremetal-7.2.0/networking_baremetal/openconfig/0000775000175000017500000000000015157004110023071 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/__init__.py0000664000175000017500000000000015157004031025172 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8389952 networking_baremetal-7.2.0/networking_baremetal/openconfig/interfaces/0000775000175000017500000000000015157004110025214 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/interfaces/__init__.py0000664000175000017500000000000015157004031027315 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/interfaces/aggregate.py0000664000175000017500000001101215157004031027511 0ustar00zuulzuul# 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 xml.etree import ElementTree from networking_baremetal import common from networking_baremetal import constants from networking_baremetal.openconfig.interfaces import types from networking_baremetal.openconfig.vlan import vlan class InterfacesAggregationConfig: NAMESPACE = 'http://openconfig.net/yang/interfaces/aggregate' PARENT = 'aggregation' TAG = 'config' def __init__(self, operation: str = constants.NetconfEditConfigOperation.MERGE): self.operation = operation self._lag_type = None self._min_links = None @property def operation(self): """RFC 6241 - operation attribute""" return self._operation.value if self._operation else None @operation.setter def operation(self, value): """RFC 6241 - operation attribute""" if isinstance(value, constants.NetconfEditConfigOperation): self._operation = value elif isinstance(value, str): self._operation = constants.NetconfEditConfigOperation(value) else: raise TypeError('Invalid type {} for config operation attribute.' .format(type(value))) @operation.deleter def operation(self): self._operation = None @property def lag_type(self): return self._lag_type.value if self._lag_type else None @lag_type.setter def lag_type(self, value: str): """the type of LAG, i.e., how it is configured / maintained""" self._lag_type = types.AggregationType(value) @lag_type.deleter def lag_type(self): self._lag_type = None @property def min_links(self): return self._min_links @min_links.setter def min_links(self, value: int): self._min_links = value @min_links.deleter def min_links(self): self._min_links = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) if self.operation: elem.set('operation', self.operation) if self.lag_type is not None: common.txt_subelement(elem, 'lag-type', self.lag_type) if self.min_links is not None: common.txt_subelement(elem, 'min-links', str(self.min_links)) return elem class InterfacesAggregation: """Options for logical interfaces representing aggregates""" NAMESPACE = 'http://openconfig.net/yang/interfaces/aggregate' PARENT = 'interface' TAG = 'aggregation' def __init__(self): self._switched_vlan = vlan.VlanSwitchedVlan() self._config = InterfacesAggregationConfig() @property def switched_vlan(self): return self._switched_vlan @switched_vlan.setter def switched_vlan(self, value): if not isinstance(value, vlan.VlanSwitchedVlan): raise TypeError('switched_vlan must be ' 'OpenConfigVlanSwitchedVlan, got {}' .format(type(value))) self._switched_vlan = value @switched_vlan.deleter def switched_vlan(self): self._switched_vlan = None @property def config(self): return self._config @config.setter def config(self, value): if not isinstance(value, InterfacesAggregationConfig): raise TypeError('config must be InterfacesAggregationConfig, got ' '{}'.format(type(value))) self._config = value @config.deleter def config(self): self._config = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) elem.set('xmlns', self.NAMESPACE) if self.config: elem.append(self.config.to_xml_element()) if self.switched_vlan: elem.append(self.switched_vlan.to_xml_element()) return elem ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/interfaces/ethernet.py0000664000175000017500000001066515157004031027416 0ustar00zuulzuul# 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 xml.etree import ElementTree from networking_baremetal import common from networking_baremetal import constants from networking_baremetal.openconfig.vlan import vlan class InterfacesEthernetConfig: """OpenConfig interface ethernet configuration""" NAMESPACE = 'http://openconfig.net/yang/interfaces' PARENT = 'interface' TAG = 'config' def __init__(self, operation=constants.NetconfEditConfigOperation.MERGE): self.operation = operation self._aggregate_id = None self._aggregate_id_namespace = ( 'http://openconfig.net/yang/interfaces/aggregate') @property def operation(self): """RFC 6241 - operation attribute""" return self._operation.value if self._operation else None @operation.setter def operation(self, value): """RFC 6241 - operation attribute""" if isinstance(value, constants.NetconfEditConfigOperation): self._operation = value elif isinstance(value, str): self._operation = constants.NetconfEditConfigOperation(value) else: raise TypeError('Invalid type {} for config operation attribute.' .format(type(value))) @operation.deleter def operation(self): self._operation = None @property def aggregate_id(self): """Logical aggregate interface for interface""" return self._aggregate_id @aggregate_id.setter def aggregate_id(self, value: str): """Set logical aggregate interface for interface""" if not isinstance(value, str): raise TypeError('aggregate_id must be string, got {}' .format(type(value))) self._aggregate_id = value @aggregate_id.deleter def aggregate_id(self): self._aggregate_id = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ element = ElementTree.Element(self.TAG) if self.operation: element.set('operation', self.operation) if self.aggregate_id is not None: common.txt_subelement(element, 'aggregate-id', self.aggregate_id, xmlns=self._aggregate_id_namespace) return element class InterfacesEthernet: """Ethernet configuration and state""" NAMESPACE = 'http://openconfig.net/yang/interfaces/ethernet' PARENT = 'interface' TAG = 'ethernet' def __init__(self): self._switched_vlan = vlan.VlanSwitchedVlan() self._config = InterfacesEthernetConfig() @property def switched_vlan(self): return self._switched_vlan @switched_vlan.setter def switched_vlan(self, value): if not isinstance(value, vlan.VlanSwitchedVlan): raise TypeError('switched_vlan must be VlanSwitchedVlan, got {}' .format(type(value))) self._switched_vlan = value @switched_vlan.deleter def switched_vlan(self): self._switched_vlan = None @property def config(self): """Configuration parameters for interface""" return self._config @config.setter def config(self, value): if not isinstance(value, InterfacesEthernetConfig): raise TypeError('config must be InterfacesEthernetConfig, got {}' .format(type(value))) self._config = value @config.deleter def config(self): self._config = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) elem.set('xmlns', self.NAMESPACE) if self.config: elem.append(self.config.to_xml_element()) if self.switched_vlan: elem.append(self.switched_vlan.to_xml_element()) return elem ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/interfaces/interfaces.py0000664000175000017500000002616515157004031027725 0ustar00zuulzuul# 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 collections import abc from typing import Optional from xml.etree import ElementTree from networking_baremetal import common from networking_baremetal import constants from networking_baremetal.openconfig.interfaces import aggregate from networking_baremetal.openconfig.interfaces import ethernet class InterfaceConfig: """OpenConfig interface configuration""" NAMESPACE = 'http://openconfig.net/yang/interfaces' PARENT = 'interface' TAG = 'config' def __init__(self, operation=constants.NetconfEditConfigOperation.MERGE, name: Optional[str] = None, description: Optional[str] = None, enabled: Optional[bool] = None, mtu: Optional[int] = None): self.operation = operation self._name = name self._description = None self._enabled = None self._mtu = None if description: self.description = description if enabled is not None: self.enabled = enabled if mtu: self.mtu = mtu @property def operation(self): """RFC 6241 - operation attribute""" return self._operation.value if self._operation else None @operation.setter def operation(self, value): """RFC 6241 - operation attribute""" if isinstance(value, constants.NetconfEditConfigOperation): self._operation = value elif isinstance(value, str): self._operation = constants.NetconfEditConfigOperation(value) else: raise TypeError('Invalid type {} for config operation attribute.' .format(type(value))) @operation.deleter def operation(self): """RFC 6241 - operation attribute""" self._operation = None @property def name(self): """The name of the interface.""" return self._name @name.setter def name(self, value: str): """The name of the interface.""" if not isinstance(value, str): raise TypeError('name must be string, got {}'.format(type(value))) self._name = value @name.deleter def name(self): """The name of the interface.""" self._name = None @property def description(self): """A textual description of the interface""" return self._description @description.setter def description(self, value: str): """A textual description of the interface""" if not isinstance(value, str): raise TypeError('description must be string, got {}' .format(type(value))) self._description = value @description.deleter def description(self): self._description = None @property def enabled(self): """The configured, desired state of the interface""" return self._enabled @enabled.setter def enabled(self, value: bool): """The configured, desired state of the interface""" if not isinstance(value, bool): raise TypeError('enabled must be boolean, got {}' .format(type(value))) self._enabled = value @enabled.deleter def enabled(self): self._enabled = None @property def mtu(self): """The max transmission unit size in octets""" return self._mtu @mtu.setter def mtu(self, value: int): """Set the max transmission unit size in octets""" if not isinstance(value, int): raise TypeError(f'mtu must be integer, got {type(value)}') self._mtu = value @mtu.deleter def mtu(self): self._mtu = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) if self.name is not None: common.txt_subelement(elem, 'name', self.name, attrib={'operation': self.operation}) if self.description is not None: common.txt_subelement(elem, 'description', self.description, attrib={'operation': self.operation}) if self.enabled is not None: common.txt_subelement(elem, 'enabled', str(self.enabled).lower(), attrib={'operation': self.operation}) if self.mtu is not None: common.txt_subelement(elem, 'mtu', str(self.mtu), attrib={'operation': self.operation}) return elem class BaseInterface: """Base interface""" NAMESPACE = 'http://openconfig.net/yang/interfaces' PARENT = 'interfaces' TAG = 'interface' def __init__(self, name: str): self.name = name self._config = InterfaceConfig() @property def name(self): """The name of the interface.""" return self._name @name.setter def name(self, value: str): """The name of the interface.""" if not isinstance(value, str): raise TypeError('name must be string, got {}'.format(type(value))) self._name = value @name.deleter def name(self): self._name = None @property def config(self): """Configuration parameters for interface""" return self._config @config.setter def config(self, value): if not isinstance(value, InterfaceConfig): raise TypeError('config must be InterfaceConfig, got {}' .format(type(value))) self._config = value @config.deleter def config(self): self._config = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) common.txt_subelement(elem, 'name', self.name) if self._config: elem.append(self.config.to_xml_element()) return elem class InterfaceEthernet(BaseInterface): def __init__(self, name: str): super(InterfaceEthernet, self).__init__(name) self._ethernet = ethernet.InterfacesEthernet() @property def ethernet(self): """Ethernet configuration and state""" return self._ethernet @ethernet.setter def ethernet(self, value): if not isinstance(value, ethernet.InterfacesEthernet): raise TypeError('ethernet must be InterfacesEthernet, got {}' .format(type(value))) self._ethernet = value @ethernet.deleter def ethernet(self): self._ethernet = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) common.txt_subelement(elem, 'name', self.name) if self.config: elem.append(self.config.to_xml_element()) if self.ethernet: elem.append(self.ethernet.to_xml_element()) return elem class InterfaceAggregate(BaseInterface): def __init__(self, name: str, operation: str = constants.NetconfEditConfigOperation.MERGE): super(InterfaceAggregate, self).__init__(name) self.operation = operation self._aggregation = aggregate.InterfacesAggregation() @property def operation(self): """RFC 6241 - operation attribute""" return self._operation.value if self._operation else None @operation.setter def operation(self, value): """RFC 6241 - operation attribute""" if isinstance(value, constants.NetconfEditConfigOperation): self._operation = value elif isinstance(value, str): self._operation = constants.NetconfEditConfigOperation(value) else: raise TypeError('Invalid type {} for config operation attribute.' .format(type(value))) @operation.deleter def operation(self): self._operation = None @property def aggregation(self): """Ethernet configuration and state""" return self._aggregation @aggregation.setter def aggregation(self, value): if not isinstance(value, aggregate.InterfacesAggregation): raise TypeError('ethernet must be OpenConfigInterfacesAggregation,' 'got {}'.format(type(value))) self._aggregation = value @aggregation.deleter def aggregation(self): self._aggregation = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) if self.operation: elem.set('operation', self.operation) common.txt_subelement(elem, 'name', self.name) if self.config: elem.append(self.config.to_xml_element()) if self.aggregation: elem.append(self.aggregation.to_xml_element()) return elem class Interfaces(abc.Collection): """Group/List of interfaces""" NAMESPACE = 'http://openconfig.net/yang/interfaces' TAG = 'interfaces' def __init__(self): # List of interfaces of type Interface self._interfaces = list() def __iter__(self): return iter(self._interfaces) def __len__(self): return len(self._interfaces) def __contains__(self, item): return item in self._interfaces @property def interfaces(self): """List of interfaces""" return self._interfaces def add(self, name: str, interface_type: str = constants.IFACE_TYPE_ETHERNET): """Add interface :param name: Interface name :type: str :param interface_type: Interface type ('ethernet', 'aggregate', 'base') :type: str """ if interface_type == constants.IFACE_TYPE_ETHERNET: interface = InterfaceEthernet(name) elif interface_type == constants.IFACE_TYPE_AGGREGATE: interface = InterfaceAggregate(name) elif interface_type == constants.IFACE_TYPE_BASE: interface = BaseInterface(name) else: raise ValueError('Invalid interface type {}'.format(type(name))) self._interfaces.append(interface) return interface def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ eleme = ElementTree.Element(self.TAG) eleme.set('xmlns', self.NAMESPACE) for interface in self.interfaces: eleme.append(interface.to_xml_element()) return eleme ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/interfaces/types.py0000664000175000017500000000144515157004031026740 0ustar00zuulzuul# 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 enum from networking_baremetal import constants class AggregationType(enum.Enum): # LAG managed by LACP LACP = constants.LAG_TYPE_LACP # Statically configured bundle / LAG STATIC = constants.LAG_TYPE_SATIC ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8399954 networking_baremetal-7.2.0/networking_baremetal/openconfig/lacp/0000775000175000017500000000000015157004110024010 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/lacp/__init__.py0000664000175000017500000000000015157004031026111 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/lacp/lacp.py0000664000175000017500000002122715157004031025307 0ustar00zuulzuul# 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 collections import abc from xml.etree import ElementTree from networking_baremetal import common from networking_baremetal import constants from networking_baremetal.openconfig.lacp import types class LACP: """LACP Top level LACP configuration and state variable containers """ NAMESPACE = 'http://openconfig.net/yang/lacp' TAG = 'lacp' def __init__(self): self._config = None self._interfaces = LACPInterfaces() @property def interfaces(self): return self._interfaces @interfaces.setter def interfaces(self, value): if not isinstance(value, LACPInterfaces): raise TypeError('interfaces must be OpenConfigLACPInterfaces,' 'got {}'.format(type(value))) self._interfaces = value @interfaces.deleter def interfaces(self): self._interfaces = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) elem.set('xmlns', self.NAMESPACE) if self.interfaces: elem.append(self.interfaces.to_xml_element()) return elem class LACPInterfaces(abc.Collection): """Top-level grouping for LACP-enabled interfaces""" NAMESPACE = 'http://openconfig.net/yang/lacp' PARENT = 'lacp' TAG = 'interfaces' def __init__(self): # List of interfaces of type OpenconfigInterface self._interfaces = list() def __iter__(self): return iter(self._interfaces) def __len__(self): return len(self._interfaces) def __contains__(self, item): return item in self._interfaces @property def interfaces(self): """List of interfaces""" return self._interfaces def add(self, name: str): """Add interface :param name: Interface name :type: str """ interface = LACPInterface(name) self._interfaces.append(interface) return interface def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) for interface in self.interfaces: elem.append(interface.to_xml_element()) return elem class LACPInterface: """Base LACP aggregate interface""" NAMESPACE = 'http://openconfig.net/yang/lacp' PARENT = 'interfaces' TAG = 'interface' def __init__(self, name: str, operation=constants.NetconfEditConfigOperation.MERGE): self.operation = operation self._config = LACPInterfaceConfig(name) self.name = name @property def operation(self): """RFC 6241 - operation attribute""" return self._operation.value @operation.setter def operation(self, value): """RFC 6241 - operation attribute""" if isinstance(value, constants.NetconfEditConfigOperation): self._operation = value elif isinstance(value, str): self._operation = constants.NetconfEditConfigOperation(value) else: raise TypeError('Invalid type {} for config operation attribute.' .format(type(value))) @property def name(self): """The name of the LACP aggregate interface.""" return self._name @name.setter def name(self, value: str): """The name of the LACP aggregate interface.""" if not isinstance(value, str): raise TypeError('name must be string, got {}'.format(type(value))) self._name = value # 'name' in the configuration is leaf-ref, should match. if self.config is not None: self.config.name = self.name @name.deleter def name(self): self._name = None @property def config(self): """Configuration data for each LACP aggregate interface""" return self._config @config.setter def config(self, value): if not isinstance(value, LACPInterfaceConfig): raise TypeError('config must be LACPInterfaceConfig,' 'got {}'.format(type(value))) self._config = value @config.deleter def config(self): self._config = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) elem.set('operation', self.operation) if self.name: common.txt_subelement(elem, 'name', self.name) if self.config: elem.append(self.config.to_xml_element()) return elem class LACPInterfaceConfig: """OpenConfig LACP aggregate interface configuration""" NAMESPACE = 'http://openconfig.net/yang/lacp' PARENT = 'interface' TAG = 'config' def __init__(self, name: str, operation=constants.NetconfEditConfigOperation.MERGE, interval=types.LACPPeriod.SLOW, lacp_mode=types.LACPActivity.ACTIVE): self._name = name self.operation = operation self.interval = interval self.lacp_mode = lacp_mode @property def operation(self): """RFC 6241 - operation attribute""" return self._operation.value @operation.setter def operation(self, value): """RFC 6241 - operation attribute""" if isinstance(value, constants.NetconfEditConfigOperation): self._operation = value elif isinstance(value, str): self._operation = constants.NetconfEditConfigOperation(value) else: raise TypeError('Invalid type {} for config operation attribute.' .format(type(value))) @property def name(self): """The name of the interface.""" return self._name @name.setter def name(self, value: str): if not isinstance(value, str): raise TypeError('name must be string, got {}'.format(type(value))) self._name = value @name.deleter def name(self): self._name = None @property def interval(self): """The period between LACP messages""" return self._interval.value if self._interval else None @interval.setter def interval(self, value: str): """Set the period between LACP messages (SLOW or FAST)""" if isinstance(value, types.LACPPeriod): self._interval = value elif isinstance(value, str): self._interval = types.LACPPeriod(value) else: raise TypeError('Invalid type {} for LACP interface interval.' .format(type(value))) @interval.deleter def interval(self): self._interval = None @property def lacp_mode(self): """The LACP mode if the aggregate interface""" return self._lacp_mode.value @lacp_mode.setter def lacp_mode(self, value: str): """Set the LACP mode if the aggregate interface ACTIVE: is to initiate the transmission of LACP packets. PASSIVE: is to wait for peer to initiate the transmission of LACP packets """ if isinstance(value, types.LACPActivity): self._lacp_mode = value elif isinstance(value, str): self._lacp_mode = types.LACPActivity(value) else: raise TypeError('Invalid type {} for LACP interface mode.' .format(type(value))) @lacp_mode.deleter def lacp_mode(self): self._lacp_mode = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) elem.set('operation', self.operation) if self.name is not None: common.txt_subelement(elem, 'name', self.name) if self.interval is not None: common.txt_subelement(elem, 'interval', self.interval) if self.lacp_mode is not None: common.txt_subelement(elem, 'lacp-mode', self.lacp_mode) return elem ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/lacp/types.py0000664000175000017500000000254515157004031025536 0ustar00zuulzuul# 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 enum from networking_baremetal import constants class LACPPeriod(enum.Enum): """Defines the time between sending LACP messages reference "IEEE 802.3ad" FAST: Send LACP packets every second SLOW: Send LACP packets every 30 seconds """ FAST = constants.LACP_PERIOD_FAST SLOW = constants.LACP_PERIOD_SLOW class LACPActivity(enum.Enum): """Describes the LACP membership type Active or passive, of the interface in the aggregate. reference "IEEE 802.1AX-2008" ACTIVE: Interface is an active member, i.e., will detect and maintain aggregates PASSIVE: Interface is a passive member, i.e., it participates with an active partner """ ACTIVE = constants.LACP_ACTIVITY_ACTIVE PASSIVE = constants.LACP_ACTIVITY_PASSIVE ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8399954 networking_baremetal-7.2.0/networking_baremetal/openconfig/network_instance/0000775000175000017500000000000015157004110026446 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/network_instance/__init__.py0000664000175000017500000000000015157004031030547 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/network_instance/network_instance.py0000664000175000017500000000713315157004031032403 0ustar00zuulzuul# 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 collections import abc from xml.etree import ElementTree from networking_baremetal import common from networking_baremetal.openconfig.vlan import vlan class NetworkInstances(abc.Collection): """Top-level grouping containing a list of network instances.""" NAMESPACE = 'http://openconfig.net/yang/network-instance' TAG = 'network-instances' def __init__(self): self._network_instances = list() def __iter__(self): return iter(self._network_instances) def __len__(self): return len(self._network_instances) def __contains__(self, item): return item in self._network_instances @property def network_instances(self): return self._network_instances def add(self, name: str): """Add network instance :param name: A unique name identifying the network instance :type: str :Keyword arguments: Network instance arguments """ network_instance = NetworkInstance(name) self._network_instances.append(network_instance) return network_instance def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) elem.set('xmlns', self.NAMESPACE) for instance in self.network_instances: elem.append(instance.to_xml_element()) return elem class NetworkInstance: """An OpenConfig description of a network_instance. This may be a Layer 3 forwarding construct such as a virtual routing and forwarding (VRF) instance, or a Layer 2 instance such as a virtual switch instance (VSI). Mixed Layer 2 and Layer 3 instances are also supported. """ NAMESPACE = 'http://openconfig.net/yang/network-instance' TAG = 'network-instance' def __init__(self, name): self.name = name self._vlans = vlan.Vlans() @property def name(self): """A unique name identifying the network instance""" return self._name @name.setter def name(self, value: str): """A unique name identifying the network instance""" if not isinstance(value, str): raise TypeError('name must be string, got {}'.format(type(value))) self._name = value @name.deleter def name(self): self._name = None @property def vlans(self): """Group/List of VLANs - keyed by id""" return self._vlans @vlans.setter def vlans(self, value): if not isinstance(value, vlan.Vlans): raise TypeError('vlans must be Vlans, got {}'.format(type(value))) self._vlans = value @vlans.deleter def vlans(self): self._vlans = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) if self.name: common.txt_subelement(elem, 'name', self.name) if self.vlans: elem.append(self.vlans.to_xml_element()) return elem ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8399954 networking_baremetal-7.2.0/networking_baremetal/openconfig/vlan/0000775000175000017500000000000015157004110024031 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/vlan/__init__.py0000664000175000017500000000000015157004031026132 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/vlan/types.py0000664000175000017500000000533415157004031025556 0ustar00zuulzuul# 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 enum import re from networking_baremetal import constants class VlanStatus(enum.Enum): """VLAN Admin state ACTIVE: VLAN is active SUSPENDED: VLAN is inactive / suspended """ ACTIVE = constants.VLAN_ACTIVE SUSPENDED = constants.VLAN_SUSPENDED class VlanInterfaceMode(enum.Enum): """VLAN interface mode (trunk or access)""" TRUNK = constants.VLAN_MODE_TRUNK ACCESS = constants.VLAN_MODE_ACCESS class VlanId: """Type definition representing a single-tagged VLAN""" def __init__(self, vlan_id: int): if not isinstance(vlan_id, int): raise TypeError('vlan_id must be integer, got {}' .format(type(vlan_id))) if vlan_id not in constants.VLAN_RANGE: raise ValueError('Invalid vlan id: {vlan_id} not in {range}' .format(vlan_id=vlan_id, range=constants.VLAN_RANGE)) self._vlan_id = vlan_id @property def vlan_id(self): return self._vlan_id class VlanRange: """Type definition representing a range of single-tagged VLANs. A range is specified as x..y where x and y are valid VLAN IDs (1 <= vlan-id <= 4094). The range is assumed to be inclusive, such that any VLAN-ID matching x <= VLAN-ID <= y falls within the range." """ # range specified as [lower]..[upper] pattern = re.compile( ('^(409[0-4]|40[0-8][0-9]|[1-3][0-9]{3}|' '[1-9][0-9]{1,2}|[1-9])\\.\\.(409[0-4]|' '40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{1,2}|' '[1-9])$')) def __init__(self, vlan_range: str): if not isinstance(vlan_range, str): raise TypeError('vlan_range must be string, got {}' .format(type(vlan_range))) if not self.pattern.match(vlan_range): raise ValueError('Invalid VLAN range {}'.format(vlan_range)) lower, _, upper = vlan_range.partition('..') if not int(lower) <= int(upper): raise ValueError('Invalid VLAN range {}'.format(vlan_range)) self._vlan_range = vlan_range @property def vlan_range(self): return self._vlan_range ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/openconfig/vlan/vlan.py0000664000175000017500000003260115157004031025347 0ustar00zuulzuul# 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 collections import abc from typing import Optional from xml.etree import ElementTree from networking_baremetal import common from networking_baremetal import constants from networking_baremetal.openconfig.vlan import types class TrunkVlans(abc.Collection): def __init__(self): self._trunk_vlans = [] def __iter__(self): return iter(self._trunk_vlans) def __len__(self): return len(self._trunk_vlans) def __contains__(self, item): return item in self._trunk_vlans def add(self, value): """Add vlan or range of vlans (range: 100..200)""" try: value = int(value) if value not in self._trunk_vlans: self._trunk_vlans.append(types.VlanId(value).vlan_id) except ValueError: if value not in self._trunk_vlans: self._trunk_vlans.append(types.VlanRange(value).vlan_range) class VlanSwitchedConfig: """Ethernet interface VLAN config VLAN related configuration that is part of the physical Ethernet interface. """ NAMESPACE = 'http://openconfig.net/yang/vlan' PARENT = 'switched-vlan' TAG = 'config' def __init__(self, operation: str = constants.NetconfEditConfigOperation.MERGE, interface_mode: Optional[str] = None, native_vlan: Optional[int] = None, access_vlan: Optional[int] = None): self.operation = operation self._interface_mode = None self._native_vlan = None self._access_vlan = None self._trunk_vlans = TrunkVlans() if interface_mode: self.interface_mode = interface_mode if native_vlan: self.native_vlan = native_vlan if access_vlan: self.access_vlan = access_vlan @property def operation(self): """RFC 6241 - operation attribute""" return self._operation.value if self._operation else None @operation.setter def operation(self, value): """RFC 6241 - operation attribute""" if isinstance(value, constants.NetconfEditConfigOperation): self._operation = value elif isinstance(value, str): self._operation = constants.NetconfEditConfigOperation(value) else: raise TypeError('Invalid type {} for config operation attribute.' .format(type(value))) @operation.deleter def operation(self): self._operation = None @property def interface_mode(self): """Get the interface to access or trunk mode for VLANs""" return self._interface_mode.value if self._interface_mode else None @interface_mode.setter def interface_mode(self, value): """Set the interface to access or trunk mode for VLANs""" self._interface_mode = types.VlanInterfaceMode(value) @interface_mode.deleter def interface_mode(self): """Delete the interface to access or trunk mode for VLANs""" self._interface_mode = None @property def native_vlan(self): """Native VLAN is valid for trunk mode interfaces """ return self._native_vlan.vlan_id if self._native_vlan else None # TODO(hjensas): Only allow if interface_mode == trunk @native_vlan.setter def native_vlan(self, value: int): """Set native VLAN is valid for trunk mode interfaces """ self._native_vlan = types.VlanId(value) @native_vlan.deleter def native_vlan(self): """Delete native VLAN""" self._native_vlan = None # TODO(hjensas): Only allow if interface_mode == access @property def access_vlan(self): """Access VLAN assigned to the interfaces""" return self._access_vlan.vlan_id if self._access_vlan else None @access_vlan.setter def access_vlan(self, value: int): """Set access VLAN assigned to the interfaces""" self._access_vlan = types.VlanId(value) @access_vlan.deleter def access_vlan(self): """Unset access VLAN assigned to the interfaces""" self._access_vlan = None @property def trunk_vlans(self): """Allowed VLANs may be specified for trunk mode interfaces""" return self._trunk_vlans # TODO(hjensas): Only allow if interface_mode == trunk @trunk_vlans.setter def trunk_vlans(self, value: str): """Set allowed VLANs may be specified for trunk mode interfaces""" self._trunk_vlans.add(value) @trunk_vlans.deleter def trunk_vlans(self): self._trunk_vlans = TrunkVlans() def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) if self.operation: elem.set('operation', self.operation) if self.interface_mode: common.txt_subelement(elem, 'interface-mode', self.interface_mode) if self.access_vlan is not None: common.txt_subelement(elem, 'access-vlan', str(self.access_vlan)) if self.native_vlan is not None: common.txt_subelement(elem, 'native-vlan', str(self.native_vlan)) if self.trunk_vlans is not None: for item in self.trunk_vlans: common.txt_subelement(elem, 'trunk-vlans', str(item)) return elem class VlanSwitchedVlan: """VLAN interface-specific data on Ethernet interfaces. Enclosing container for VLAN interface-specific data on Ethernet interfaces. These are for standard L2, switched-style VLANs. """ NAMESPACE = 'http://openconfig.net/yang/vlan' PARENT = 'ethernet' TAG = 'switched-vlan' def __init__(self): self._config = VlanSwitchedConfig() @property def config(self): """Configuration parameters for VLANs""" return self._config @config.setter def config(self, value): if not isinstance(value, VlanSwitchedConfig): raise TypeError('config must be VlanSwitchedConfig, got {}' .format(type(value))) self._config = value @config.deleter def config(self): self._config = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) elem.set('xmlns', self.NAMESPACE) if self.config: elem.append(self.config.to_xml_element()) return elem class VlanConfig: """OpenConfig VLAN configuration""" NAMESPACE = 'http://openconfig.net/yang/vlan' PARENT = 'vlan' TAG = 'config' def __init__(self, operation=constants.NetconfEditConfigOperation.MERGE, vlan_id: int = None, name: str = None, status: str = None): self.operation = operation self._vlan_id = None self._name = None self._status = None if vlan_id: self.vlan_id = vlan_id if name: self.name = name if status: self.status = status @property def operation(self): """RFC 6241 - operation attribute""" return self._operation.value @operation.setter def operation(self, value): """RFC 6241 - operation attribute""" if isinstance(value, constants.NetconfEditConfigOperation): self._operation = value elif isinstance(value, str): self._operation = constants.NetconfEditConfigOperation(value) else: raise TypeError('Invalid type {} for config operation attribute.' .format(type(value))) @property def vlan_id(self): """The id of the VLAN""" return self._vlan_id.vlan_id if self._vlan_id else None @vlan_id.setter def vlan_id(self, value: int): self._vlan_id = types.VlanId(value) @vlan_id.deleter def vlan_id(self): self._vlan_id = None @property def name(self): """Interface VLAN name.""" return self._name @name.setter def name(self, value: str): if not isinstance(value, str): raise TypeError('name must be string, got {}' .format(type(value))) self._name = value @name.deleter def name(self): self._name = None @property def status(self): """Admin state of the VLAN""" return self._status.value if self._status else None @status.setter def status(self, value: str): """Admin state of the VLAN""" self._status = types.VlanStatus(value) @status.deleter def status(self): self._status = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) elem.set('operation', self.operation) if self.vlan_id is not None: common.txt_subelement(elem, 'vlan-id', str(self.vlan_id)) if self.name is not None: common.txt_subelement(elem, 'name', self.name) if self.status is not None: common.txt_subelement(elem, 'status', self.status) return elem class Vlan: """Base vlan""" NAMESPACE = 'http://openconfig.net/yang/vlan' PARENT = 'vlans' TAG = 'vlan' def __init__(self, vlan_id: int, operation=constants.NetconfEditConfigOperation.MERGE): self.operation = operation self.vlan_id = vlan_id self._config = VlanConfig(vlan_id=self.vlan_id) @property def operation(self): """RFC 6241 - operation attribute""" return self._operation.value @operation.setter def operation(self, value): """RFC 6241 - operation attribute""" if isinstance(value, constants.NetconfEditConfigOperation): self._operation = value elif isinstance(value, str): self._operation = constants.NetconfEditConfigOperation(value) else: raise TypeError('Invalid type {} for config operation ' 'attribute.'.format(type(value))) @property def vlan_id(self): """The id of the VLAN""" return self._vlan_id.vlan_id @vlan_id.setter def vlan_id(self, value: int): if not isinstance(value, int): raise TypeError(f'vlan_id must be integer, got {type(value)}') self._vlan_id = types.VlanId(value) @vlan_id.deleter def vlan_id(self): self._vlan_id = None @property def config(self): """Configuration parameters for VLAN""" return self._config @config.setter def config(self, value): """Configuration parameters for VLAN""" if not isinstance(value, VlanConfig): raise TypeError('config must be VlanConfig, got {}' .format(type(value))) self._config = value if self.vlan_id: self.config.vlan_id = self.vlan_id @config.deleter def config(self): self._config = None def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) if self.vlan_id: common.txt_subelement(elem, 'vlan-id', str(self.vlan_id), attrib={'operation': self.operation}) if self.config: elem.append(self.config.to_xml_element()) return elem class Vlans(abc.Collection): """Group/List of VLANs""" NAMESPACE = 'http://openconfig.net/yang/vlan' TAG = 'vlans' def __init__(self): # List of vlans of type OpenconfigVlan self._vlans = list() def __iter__(self): return iter(self._vlans) def __len__(self): return len(self._vlans) def __contains__(self, item): return item in self._vlans @property def vlans(self): """List of VLANs""" return self._vlans def add(self, vlan_id: int): """Add VLAN :param vlan_id: VLAN ID :type: int :Keyword arguments: VLAN configuration """ vlan = Vlan(vlan_id) self._vlans.append(vlan) return vlan def remove(self, vlan_id: int): """Remove VLAN :param vlan_id: VLAN ID :type: int """ vlan = Vlan(vlan_id) vlan.operation = constants.NetconfEditConfigOperation.REMOVE self._vlans.append(vlan) return vlan def to_xml_element(self): """Create XML Element :return: ElementTree Element with SubElements """ elem = ElementTree.Element(self.TAG) elem.set('xmlns', self.NAMESPACE) for vlan in self.vlans: elem.append(vlan.to_xml_element()) return elem ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8399954 networking_baremetal-7.2.0/networking_baremetal/plugins/0000775000175000017500000000000015157004110022423 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/plugins/__init__.py0000664000175000017500000000000015157004031024524 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8409956 networking_baremetal-7.2.0/networking_baremetal/plugins/ml2/0000775000175000017500000000000015157004110023115 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/plugins/ml2/__init__.py0000664000175000017500000000000015157004031025216 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/plugins/ml2/baremetal_l2vni_mapping.py0000664000175000017500000007172315157004031030264 0ustar00zuulzuul# Copyright (c) 2025 Rackspace Technology, Inc. # Copyright (c) 2026 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 socket from typing import cast from neutron.common.ovn import constants as ovn_const from neutron.common.ovn import utils as ovn_utils from neutron.db import provisioning_blocks from neutron.objects import ports from neutron_lib.api.definitions import portbindings from neutron_lib.callbacks import resources from neutron_lib import constants as p_const from neutron_lib import exceptions as exc from neutron_lib.plugins import directory from neutron_lib.plugins.ml2 import api from oslo_config import cfg from oslo_log import log as logging LOG = logging.getLogger(__name__) baremetal_l2vni_opts = [ cfg.BoolOpt('create_localnet_ports', default=True, help='Automatically create OVN localnet ports to bridge ' 'VXLAN overlay networks to physical networks for ' 'baremetal. Disable if localnet ports are managed ' 'externally or not needed. You likely need this ' 'set to True unless your doing a pure BGP EVPN ' 'setup with Neutron.'), cfg.StrOpt('default_physical_network', default=None, help='Default physical network name to use for baremetal ' 'L2VNI bindings when the port binding profile does not ' 'specify a physical_network. If not set and the port ' 'lacks physical_network in its binding profile, port ' 'binding will fail.'), ] # L2VNI trunk configuration options (shared with agent) l2vni_opts = [ cfg.StrOpt('l2vni_subport_anchor_network', default='l2vni-subport-anchor', help='Name of the shared network used for all trunk ' 'subports. This network is used to signal VLAN bindings ' 'to ML2 switch plugins and does not pass actual traffic. ' 'Ports on this network should not be bound by the L2VNI ' 'mechanism driver.'), ] cfg.CONF.register_opts(baremetal_l2vni_opts, group='baremetal_l2vni') cfg.CONF.register_opts(l2vni_opts, group='l2vni') SUPPORTED_VNIC_TYPES = [portbindings.VNIC_BAREMETAL] # NOTE(cardoe) This is where we want to use the TYPE from # https://review.opendev.org/c/openstack/neutron-specs/+/952166 # Supports both VXLAN and Geneve overlay protocols for EVPN L2VNI EVPN_TYPES = [p_const.TYPE_VXLAN, p_const.TYPE_GENEVE] def _get_port_name(ls_name, physnet): """Helper to ensure consistent naming of ports.""" return f"{ls_name}-localnet-{physnet}" class L2vniMechanismDriver(api.MechanismDriver): """ML2 mechanism driver for L2VNI binding This mechanism driver is called on port binding to facilitate the VTEP to VLAN binding necessary for EVPN networks to attach to baremetal ports, which may then connect to the environment through an EVPN connection, or through direct port attachments """ @property def connectivity(self): return portbindings.CONNECTIVITY_L2 def initialize(self): pass def get_allowed_network_types(self, agent): """Return the agent's or driver's allowed network types. L2VNI handles hierarchical port binding for overlay networks only. Returns vxlan and geneve, which are handled by creating dynamic VLAN segments. Flat and VLAN networks are handled by the base baremetal mechanism driver. """ return [p_const.TYPE_VXLAN, p_const.TYPE_GENEVE] @functools.cached_property def _get_ovn_client(self): """Get OVN client from the OVN mechanism driver. :returns: OVN client instance or None if OVN driver not available """ try: # Get the mechanism driver manager plugin = directory.get_plugin() if not hasattr(plugin, 'mechanism_manager'): LOG.warning("ML2 plugin does not have mechanism_manager") return None # Find the OVN mechanism driver for driver in plugin.mechanism_manager.ordered_mech_drivers: if hasattr(driver.obj, '_ovn_client'): return driver.obj._ovn_client LOG.warning("OVN mechanism driver not found") return None except Exception as e: LOG.error("Failed to get OVN client: %s", e) return None def _get_local_chassis_name(self, ovn_client): """Get the local OVN chassis name. :param ovn_client: OVN client instance :returns: Chassis name string or None """ try: # Try to get from OVN mech driver if hasattr(ovn_client, 'chassis'): return ovn_client.chassis # Try to get from config if hasattr(cfg.CONF, 'ovn') and hasattr(cfg.CONF.ovn, 'ovn_chassis_name'): return cfg.CONF.ovn.ovn_chassis_name # Fall back to hostname hostname = socket.gethostname() LOG.debug("Using hostname as chassis name: %s", hostname) return hostname except Exception as e: LOG.error("Failed to determine local chassis name: %s", e) return None def _get_local_chassis(self, ovn_client): """Get the local OVN chassis object. :param ovn_client: OVN client instance :returns: Chassis object or None """ try: local_chassis_name = self._get_local_chassis_name(ovn_client) if not local_chassis_name: return None # Get chassis from OVN Southbound if not hasattr(ovn_client, '_sb_idl'): LOG.debug("No southbound connection available") return None # TODO(TheJulia): At some point soon, once we have a CI job # validating all of this, we should look at a different query # pattern. See: # https://review.opendev.org/c/openstack/networking-baremetal/+/973889/9/networking_baremetal/plugins/ml2/baremetal_l2vni_mapping.py # Query chassis from Southbound database for ch in ovn_client._sb_idl.tables['Chassis'].rows.values(): if ch.name == local_chassis_name or \ ch.hostname == local_chassis_name: return ch LOG.warning("Local chassis %s not found in OVN", local_chassis_name) return None except Exception as e: LOG.error("Error getting local chassis: %s", e) return None def _chassis_can_forward_physnet(self, ovn_client, physnet): """Check if any chassis in the cluster can forward for this physnet. Checks all chassis in the OVN cluster to see if at least one has the physnet configured in its ovn-bridge-mappings. Since localnet ports are realized on all chassis with the matching bridge-mapping, we only need one chassis to have the physnet available. :param ovn_client: OVN client instance :param physnet: Physical network name :returns: True if any chassis has physnet, False otherwise """ # TODO(TheJulia): We should look at simplifying this logic, see # https://review.opendev.org/c/openstack/networking-baremetal/+/973889/9/networking_baremetal/plugins/ml2/baremetal_l2vni_mapping.py try: if not hasattr(ovn_client, '_sb_idl'): LOG.warning("No southbound connection available, cannot " "verify physnet %s exists", physnet) # Return True to allow creation - fail open rather than # closed return True # Check all chassis in the cluster chassis_table = ovn_client._sb_idl.tables['Chassis'] chassis_list = list(chassis_table.rows.values()) LOG.debug("Checking %d chassis for physnet %s", len(chassis_list), physnet) for chassis in chassis_list: # Get bridge mappings from other_config (OVN 20.06+) bridge_mappings = chassis.other_config.get( 'ovn-bridge-mappings', '') # Format is "physnet1:br-provider,physnet2:br-ex" physnets = [mapping.split(':')[0].strip() for mapping in bridge_mappings.split(',') if ':' in mapping] if physnet in physnets: LOG.debug("Found physnet %s on chassis %s with bridge " "mappings: %s", physnet, chassis.name, bridge_mappings) return True # We've hit the bottom of the chassis check loop, and # did not find what we are looking for, meaning there # is an input mismatch, or misconfiguration someplace... # or the operator is intentionally partitioning everything # apart. # TODO(TheJulia): Maybe one-day make this configurable? found = ', '.join(physnets) LOG.warning( "Evaluated chassis %s and did not find physnet %s, " "this may be acceptable with complex environments " "or indication of a misconfiguration. Found: %s.", chassis.name, physnet, found) # No chassis has this physnet - this is an error condition LOG.error("Physical network %s not found in bridge-mappings " "on any chassis in the OVN cluster. Check OVN " "configuration.", physnet) return False except Exception as e: LOG.error("Error checking chassis bridge mappings: %s. " "Failing open - allowing localnet port creation.", e) # Fail open - let the creation proceed and let OVN handle it return True def _ensure_localnet_port(self, context, network_id, physnet, vlan_id: int): """Ensure a localnet port exists in OVN to bridge overlay to physnet. Creates a localnet port in OVN's logical switch that bridges the VXLAN overlay network to the physical network via the dynamic VLAN segment. This is idempotent - if the port already exists, it will not be recreated. :param context: PortContext instance :param network_id: Neutron network UUID :param physnet: Physical network name :param vlan_id: VLAN tag for the physical network (None for untagged) """ if not cfg.CONF.baremetal_l2vni.create_localnet_ports: LOG.debug("Localnet port creation disabled by config") return ovn_client = self._get_ovn_client if not ovn_client: LOG.warning("Cannot create localnet port - OVN client unavailable") return # TODO(TheJulia): We should consider simplifying and just using # ovn_client on the class method directly on helper methods as # opposed to pasisng a variable. Refactoring for later. # Check if this chassis can forward traffic for the physnet if not self._chassis_can_forward_physnet(ovn_client, physnet): LOG.debug("Chassis cannot forward physnet %s, skipping " "localnet port creation", physnet) return try: ls_name = ovn_utils.ovn_name(network_id) # Localnet port name includes physnet for uniqueness port_name = _get_port_name(ls_name, physnet) # Check if localnet port already exists existing_port = ovn_client._nb_idl.lookup( 'Logical_Switch_Port', port_name, default=None) if existing_port: # Get local chassis name for validation chassis_name = self._get_local_chassis_name(ovn_client) # Verify the VLAN tag matches - it may have changed if the # segment was released and a new one allocated existing_tag = existing_port.tag if hasattr( existing_port, 'tag') else None # OVN returns tag as a list [vlan_id] or empty list [] if isinstance(existing_tag, list): existing_tag = existing_tag[0] if existing_tag else None # Verify requested-chassis matches existing_options = existing_port.options if hasattr( existing_port, 'options') else {} if isinstance(existing_options, list): existing_options = {k: v for k, v in existing_options} elif not isinstance(existing_options, dict): existing_options = {} existing_chassis = existing_options.get('requested-chassis') tag_mismatch = existing_tag != vlan_id chassis_mismatch = (chassis_name and existing_chassis != chassis_name) if not tag_mismatch and not chassis_mismatch: LOG.debug("Localnet port %s already exists for network " "%s on physnet %s with correct VLAN tag %s " "and chassis %s", port_name, network_id, physnet, vlan_id, chassis_name) return else: # VLAN tag or chassis mismatch - delete and recreate if tag_mismatch and chassis_mismatch: LOG.warning("Localnet port %s exists with stale " "VLAN tag %s (expected %s) and " "chassis %s (expected %s), recreating", port_name, existing_tag, vlan_id, existing_chassis, chassis_name) elif tag_mismatch: LOG.warning("Localnet port %s exists with stale " "VLAN tag %s (expected %s), recreating", port_name, existing_tag, vlan_id) else: LOG.warning("Localnet port %s exists with stale " "chassis %s (expected %s), recreating", port_name, existing_chassis, chassis_name) ovn_client._nb_idl.lsp_del(port_name).execute( check_error=True) # Create the localnet port using atomic create_lswitch_port LOG.info("Creating localnet port %s for network %s to bridge " "to physnet %s with VLAN %s", port_name, network_id, physnet, vlan_id) # Build options for localnet port options = { "network_name": physnet, ovn_const.LSP_OPTIONS_LOCALNET_LEARN_FDB: 'true', ovn_const.LSP_OPTIONS_MCAST_FLOOD: 'true', ovn_const.LSP_OPTIONS_MCAST_FLOOD_REPORTS: 'true', } # Get the local chassis name to pin the localnet port # This ensures the localnet port stays on the same chassis, # preventing disjointed behavior. Similar to trunk reconciler # approach in: # https://review.opendev.org/c/openstack/networking-baremetal/+/975333 # noqa: E501 chassis_name = self._get_local_chassis_name(ovn_client) if not chassis_name: LOG.warning("Cannot determine local chassis name, " "localnet port will not be chassis-pinned") # Create localnet port atomically cmd = ovn_client._nb_idl.create_lswitch_port( lport_name=port_name, lswitch_name=ls_name, addresses=[ovn_const.UNKNOWN_ADDR], external_ids={}, type=ovn_const.LSP_TYPE_LOCALNET, tag=vlan_id if vlan_id else [], options=options, enabled=True ) # Pin localnet port to local chassis if we have a chassis name if chassis_name: cmd_set_options = ovn_client._nb_idl.db_set( 'Logical_Switch_Port', port_name, ('options', {'requested-chassis': chassis_name, **options}) ) ovn_client._transaction([cmd, cmd_set_options]) else: ovn_client._transaction([cmd]) LOG.info("Successfully created localnet port %s with VLAN tag %s", port_name, vlan_id) except Exception as e: LOG.error("Failed to create localnet port for network %s " "on physnet %s: %s", network_id, physnet, e) # Don't raise - this is an optimization, binding can still work def _remove_localnet_port(self, context, network_id, physnet): """Remove localnet port from OVN when dynamic segment is released. Cleans up the localnet port that bridges the VXLAN overlay to the physical network when the dynamic VLAN segment is no longer needed. :param context: PortContext instance :param network_id: Neutron network UUID :param physnet: Physical network name """ if not cfg.CONF.baremetal_l2vni.create_localnet_ports: return ovn_client = self._get_ovn_client if not ovn_client: LOG.debug("Cannot remove localnet port - OVN client unavailable") return try: ls_name = ovn_utils.ovn_name(network_id) # Localnet port name includes physnet for uniqueness port_name = _get_port_name(ls_name, physnet) # Check if localnet port exists existing_port = ovn_client._nb_idl.lookup( 'Logical_Switch_Port', port_name, default=None) if not existing_port: LOG.debug("Localnet port %s does not exist, nothing to " "remove", port_name) return # Remove the localnet port LOG.info("Removing localnet port %s for network %s on physnet %s " "as dynamic segment is being released", port_name, network_id, physnet) ovn_client._nb_idl.lsp_del(port_name).execute(check_error=True) LOG.info("Successfully removed localnet port %s", port_name) except Exception as e: LOG.error("Failed to remove localnet port for network %s " "on physnet %s: %s", network_id, physnet, e) # Don't raise - segment cleanup should continue def update_port_postcommit(self, context): vnic_type = context.current[portbindings.VNIC_TYPE] if vnic_type not in SUPPORTED_VNIC_TYPES: return vif_type = context.current[portbindings.VIF_TYPE] if vif_type == portbindings.VIF_TYPE_UNBOUND: # The lowest bound segment should be our dynamic segment segment = context.original_bottom_bound_segment if segment and segment[api.NETWORK_TYPE] == p_const.TYPE_VLAN: # If no host is bound to this segment now, release it if not ports.PortBindingLevel.get_objects( context.plugin_context, segment_id=segment[api.ID] ): # Clean up the localnet port before releasing the segment physnet = segment.get(api.PHYSICAL_NETWORK) if physnet: self._remove_localnet_port( context, context.network.current['id'], physnet ) context.release_dynamic_segment(segment[api.ID]) if vif_type == portbindings.VIF_TYPE_OTHER: # Complete OVN's L2 provisioning block for baremetal # This is really a workaround for odd OVN behavior which # could be a misconfiguration, we're not 100% sure yet. # Without it, the port never moves to ACTIVE, but realistically # the binding is still incomplete and the port doesn't entirely # work yet on the controller side because the created port is # declared shutdown which triggers the config reconcile which # creates this entry. provisioning_blocks.provisioning_complete( context._plugin_context, context.current['id'], resources.PORT, 'L2') def delete_port_postcommit(self, context): """Clean up localnet port when last baremetal port is deleted. When a baremetal port is deleted, check if it was the last port using the dynamic VLAN segment. If so, remove the localnet port and release the segment to prevent VLAN ID reuse conflicts. """ vnic_type = context.current[portbindings.VNIC_TYPE] if vnic_type not in SUPPORTED_VNIC_TYPES: return # Check if this port had a bound segment segment = context.bottom_bound_segment if not segment or segment[api.NETWORK_TYPE] != p_const.TYPE_VLAN: return # Check if any other ports are still using this segment if ports.PortBindingLevel.get_objects( context.plugin_context, segment_id=segment[api.ID] ): # Other ports still using this segment, don't clean up return # This was the last port - clean up localnet port and release segment physnet = segment.get(api.PHYSICAL_NETWORK) if physnet: self._remove_localnet_port( context, context.network.current['id'], physnet ) context.release_dynamic_segment(segment[api.ID]) def bind_port(self, context): if context.current[portbindings.VNIC_TYPE] not in SUPPORTED_VNIC_TYPES: return # Skip binding for ports on the L2VNI subport anchor network. # The anchor network is a dummy/metadata network used to hold trunk # subports. These ports don't need actual network connectivity or # hierarchical binding - they just need to exist as Neutron ports # so they can be added to trunks and trigger networking-generic-switch # callbacks for switch configuration. anchor_network_name = cfg.CONF.l2vni.l2vni_subport_anchor_network if context.network.current['name'] == anchor_network_name: LOG.debug("Skipping L2VNI binding for port %s on anchor " "network %s (anchor network ports are metadata only)", context.current['id'], anchor_network_name) return # Only bind overlay segments (vxlan, geneve) at the current level. # Check segments_to_bind rather than all network segments to avoid # re-binding the overlay segment when we're at level 2 binding the # VLAN segment. for segment in context.segments_to_bind: if segment[api.NETWORK_TYPE] in EVPN_TYPES: LOG.debug("L2VNI binding overlay segment %s for port %s", segment[api.ID], context.current['id']) self._bind_port_segment(context, segment) # Fast out to avoid walking the rest of the list break else: LOG.debug("L2VNI no overlay segments to bind for port %s", context.current['id']) def _bind_port_segment(self, context, bind_segment): """Dynamically allocates a VLAN segment to bind the segment to.""" # This will only be set by # https://review.opendev.org/c/openstack/ironic/+/964570 # Get physical network from port binding profile, fallback to config physnet = context.current[portbindings.PROFILE].get( api.PHYSICAL_NETWORK) if not physnet: # Fallback to configured default physical network physnet = cfg.CONF.baremetal_l2vni.default_physical_network if physnet: LOG.debug("Port %s does not specify physical_network in " "binding profile, using default: %s", context.current['id'], physnet) if not physnet: # No physnet from profile or config - cannot bind LOG.error("Port %s cannot be bound: no physical_network " "specified in binding profile and no default " "physical network configured. Set " "[baremetal_l2vni]default_physical_network or ensure " "ports have physical_network in binding profile.", context.current['id']) raise exc.InvalidInput( error_message="Port binding requires physical_network in " "binding profile or default_physical_network " "configuration.") lower_segment = None for segment in context.network.network_segments: if (segment[api.NETWORK_TYPE] == p_const.TYPE_VLAN and segment[api.PHYSICAL_NETWORK] == physnet): lower_segment = segment break if lower_segment: # NOTE(TheJulia): This may be overkill logging wise, but it makes # it pretty clear logging wise. LOG.debug("A lower segment (%s) is already exists in physical " "network %s to attach to segmentation id %s.", lower_segment.get(api.SEGMENTATION_ID), physnet, bind_segment.get(api.SEGMENTATION_ID)) if context._plugin.type_manager.is_partial_segment(lower_segment): LOG.error("Lower segment in physical network %s is lacking a " "segmentation ID.", physnet) raise exc.InvalidInput( error_message="Lower segment is lacking a " "segmentation id.") else: # If we do not have a lower segment, we need to allocate it. lower_segment = context.allocate_dynamic_segment( { api.PHYSICAL_NETWORK: physnet, api.NETWORK_TYPE: p_const.TYPE_VLAN, } ) if not lower_segment: LOG.error("Failed to allocate dynamic VLAN segment for " "physical network %s on port %s", physnet, context.current['id']) raise exc.InvalidInput( error_message=f"Failed to allocate dynamic VLAN segment " f"for physical network {physnet}") LOG.debug("A lower_segment was not found to bind segmentation id " "%s to physical network %s. Allocated: %s", bind_segment.get(api.SEGMENTATION_ID), physnet, lower_segment.get(api.SEGMENTATION_ID)) # Validate lower segment has a segmentation ID before proceeding vlan_id = lower_segment.get(api.SEGMENTATION_ID) if not vlan_id: LOG.error("Lower segment for physical network %s is missing " "segmentation ID on port %s", physnet, context.current['id']) raise exc.InvalidInput( error_message=f"Lower segment on physical network {physnet} " f"is missing segmentation ID") # Ensure OVN has a localnet port to bridge the overlay to the physnet vlan_id = cast(int, vlan_id) self._ensure_localnet_port( context, context.network.current['id'], physnet, vlan_id ) LOG.debug("Calling continue_binding for overlay segment %s with " "lower VLAN segment %s (vlan_id=%s) on physical network %s", bind_segment[api.ID], lower_segment[api.ID], lower_segment.get(api.SEGMENTATION_ID), physnet) # record the current segment as bound and move on to binding # the VLAN segment. Under no circumstances, should we call # context.set_binding because this mech driver cannot bind # the entirety of the port structure, only part. context.continue_binding(bind_segment[api.ID], [lower_segment]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/plugins/ml2/baremetal_mech.py0000664000175000017500000007272315157004031026434 0ustar00zuulzuul# Copyright 2015 Mirantis, 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.db import provisioning_blocks from neutron.plugins.ml2.drivers import mech_agent from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import provider_net from neutron_lib.callbacks import resources from neutron_lib import constants as n_const from neutron_lib.plugins.ml2 import api from oslo_config import cfg from oslo_log import log as logging from networking_baremetal import common from networking_baremetal import config from networking_baremetal import constants from networking_baremetal import exceptions CONF = cfg.CONF LOG = logging.getLogger(__name__) BAREMETAL_DRV_ENTITY = 'BAREMETAL_DRV_ENTITIY' class BaremetalMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase): def __init__(self): super(BaremetalMechanismDriver, self).__init__( agent_type=constants.BAREMETAL_AGENT_TYPE, vif_type=portbindings.VIF_TYPE_OTHER, vif_details={ portbindings.VIF_DETAILS_CONNECTIVITY: self.connectivity}, supported_vnic_types=[portbindings.VNIC_BAREMETAL]) self.devices = config.get_devices() # Use set to remove duplicates, # i.e device has both switch_id and switch_info for device_id in set(self.devices.values()): device_driver = common.driver_mgr(device_id) device_driver.load_config() try: device_driver.validate() except exceptions.DriverValidationError: LOG.exception("Failed to validate device driver %s", device_id) @property def connectivity(self): return portbindings.CONNECTIVITY_L2 def get_allowed_network_types(self, agent): """Return the agent's or driver's allowed network types. For example: return ('flat', ...). You can also refer to the configuration the given agent exposes. """ return [n_const.TYPE_FLAT, n_const.TYPE_VLAN] def get_mappings(self, agent): """Return the agent's bridge or interface mappings. For example: agent['configurations'].get('bridge_mappings', {}). """ return agent['configurations'].get('bridge_mappings', {}) def bind_port(self, context): """Bind port, skipping hierarchical overlay→VLAN scenarios. Check if this is a hierarchical binding scenario where an overlay segment exists on the network. If so, skip binding to allow other drivers like genericswitch to handle the VLAN binding. """ # Check if this is hierarchical binding by seeing if any static # network segment is overlay type. If so, skip binding entirely # to let baremetal-l2vni handle overlay and genericswitch handle # VLAN levels. network_segments = context.network.network_segments has_overlay = any( seg[api.NETWORK_TYPE] in [n_const.TYPE_VXLAN, n_const.TYPE_GENEVE] for seg in network_segments ) if has_overlay: # This is hierarchical binding - skip to let other drivers # handle it (baremetal-l2vni for overlay, genericswitch for # VLAN) LOG.debug("Skipping baremetal driver bind_port for port %s " "- network has overlay segment, hierarchical " "binding scenario", context.current['id']) return # Not a hierarchical scenario, proceed with normal agent binding super(BaremetalMechanismDriver, self).bind_port(context) def create_network_precommit(self, context): """Allocate resources for a new network. Create a new network, allocating resources as necessary in the database. Called inside transaction context on session. Call cannot block. Raising an exception will result in a rollback of the current transaction. :param context: NetworkContext instance describing the new network. """ pass def create_network_postcommit(self, context): """Create a network. Called after the transaction commits. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Raising an exception will cause the deletion of the resource. :param context: NetworkContext instance describing the new network. """ network = context.current network_type = network[provider_net.NETWORK_TYPE] segmentation_id = network[provider_net.SEGMENTATION_ID] physical_network = network[provider_net.PHYSICAL_NETWORK] # If not VLAN network, or no segmentation_id - nothing to do. if network_type != n_const.TYPE_VLAN or not segmentation_id: return # TODO(hjensas): This should be parallelized for device in CONF.networking_baremetal.enabled_devices: # VLAN management is disabled for this device if not CONF[device].manage_vlans: continue # Skip device if not on physical network if not self._is_device_on_physnet(device, physical_network): continue driver = common.driver_mgr(device) driver.create_network(context) def update_network_precommit(self, context): """Update resources of a network. Update values of a network, updating the associated resources in the database. Called inside transaction context on session. Raising an exception will result in rollback of the transaction. update_network_precommit is called for all changes to the network state. It is up to the mechanism driver to ignore state or state changes that it does not know or care about. :param context: NetworkContext instance describing the new state of the network, as well as the original state prior to the update_network call. """ pass def update_network_postcommit(self, context): """Update a network. Called after the transaction commits. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Raising an exception will cause the deletion of the resource. update_network_postcommit is called for all changes to the network state. It is up to the mechanism driver to ignore state or state changes that it does not know or care about. :param context: NetworkContext instance describing the new state of the network, as well as the original state prior to the update_network call. """ network = context.current network_orig = context.original network_type = network[provider_net.NETWORK_TYPE] segmentation_id = network[provider_net.SEGMENTATION_ID] network_type_orig = network_orig[provider_net.NETWORK_TYPE] physical_network = network[provider_net.PHYSICAL_NETWORK] if (network_type != n_const.TYPE_VLAN and network_type_orig != n_const.TYPE_VLAN): return if not segmentation_id and not network_type_orig: return # TODO(hjensas): This should be parallelized for device in CONF.networking_baremetal.enabled_devices: # VLAN management is disabled for this device if not CONF[device].manage_vlans: continue # Skip device if not on physical network if not self._is_device_on_physnet(device, physical_network): continue driver = common.driver_mgr(device) driver.update_network(context) def delete_network_precommit(self, context): """Delete resources for a network. Delete network resources previously allocated by this mechanism driver for a network. Called inside transaction context on session. Runtime errors are not expected, but raising an exception will result in rollback of the transaction. :param context: NetworkContext instance describing the current state of the network, prior to the call to delete it. """ pass def delete_network_postcommit(self, context): """Delete a network. Called after the transaction commits. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Runtime errors are not expected, and will not prevent the resource from being deleted. :param context: NetworkContext instance describing the current state of the network, prior to the call to delete it. """ network = context.current network_type = network[provider_net.NETWORK_TYPE] segmentation_id = network[provider_net.SEGMENTATION_ID] physical_network = network[provider_net.PHYSICAL_NETWORK] # If not VLAN network, or no segmentation_id - nothing to do. if network_type != n_const.TYPE_VLAN or not segmentation_id: return # TODO(hjensas): This should be parallelized for device in CONF.networking_baremetal.enabled_devices: # VLAN management is disabled for this device if not CONF[device].manage_vlans: continue # Skip device if not on physical network if not self._is_device_on_physnet(device, physical_network): continue driver = common.driver_mgr(device) driver.delete_network(context) def create_subnet_precommit(self, context): """Allocate resources for a new subnet. Create a new subnet, allocating resources as necessary in the database. Called inside transaction context on session. Call cannot block. Raising an exception will result in a rollback of the current transaction. :param context: SubnetContext instance describing the new subnet. """ pass def create_subnet_postcommit(self, context): """Create a subnet. Called after the transaction commits. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Raising an exception will cause the deletion of the resource. :param context: SubnetContext instance describing the new subnet. """ pass def update_subnet_precommit(self, context): """Update resources of a subnet. Update values of a subnet, updating the associated resources in the database. Called inside transaction context on session. Raising an exception will result in rollback of the transaction. update_subnet_precommit is called for all changes to the subnet state. It is up to the mechanism driver to ignore state or state changes that it does not know or care about. :param context: SubnetContext instance describing the new state of the subnet, as well as the original state prior to the update_subnet call. """ pass def update_subnet_postcommit(self, context): """Update a subnet. Called after the transaction commits. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Raising an exception will cause the deletion of the resource. update_subnet_postcommit is called for all changes to the subnet state. It is up to the mechanism driver to ignore state or state changes that it does not know or care about. :param context: SubnetContext instance describing the new state of the subnet, as well as the original state prior to the update_subnet call. """ pass def delete_subnet_precommit(self, context): """Delete resources for a subnet. Delete subnet resources previously allocated by this mechanism driver for a subnet. Called inside transaction context on session. Runtime errors are not expected, but raising an exception will result in rollback of the transaction. :param context: SubnetContext instance describing the current state of the subnet, prior to the call to delete it. """ pass def delete_subnet_postcommit(self, context): """Delete a subnet.a Called after the transaction commits. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Runtime errors are not expected, and will not prevent the resource from being deleted. :param context: SubnetContext instance describing the current state of the subnet, prior to the call to delete it. """ pass def create_port_precommit(self, context): """Allocate resources for a new port. Create a new port, allocating resources as necessary in the database. Called inside transaction context on session. Call cannot block. Raising an exception will result in a rollback of the current transaction. :param context: PortContext instance describing the port. """ pass def create_port_postcommit(self, context): """Create a port. Called after the transaction completes. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Raising an exception will result in the deletion of the resource. :param context: PortContext instance describing the port. """ pass def update_port_precommit(self, context): """Update resources of a port. Called inside transaction context on session to complete a port update as defined by this mechanism driver. Raising an exception will result in rollback of the transaction. update_port_precommit is called for all changes to the port state. It is up to the mechanism driver to ignore state or state changes that it does not know or care about. :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. """ pass def update_port_postcommit(self, context): """Update a port. Called after the transaction completes. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Raising an exception will result in the deletion of the resource. update_port_postcommit is called for all changes to the port state. It is up to the mechanism driver to ignore state or state changes that it does not know or care about. :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. """ port = context.current port_orig = context.original if self._is_bound(port): if port_orig: self._update_port(context) provisioning_blocks.provisioning_complete( context._plugin_context, port['id'], resources.PORT, BAREMETAL_DRV_ENTITY) elif self._is_bound(port_orig): # The port has been unbound. This will cause the local link # information to be lost, so remove the port from the network on # the switch now while we have the required information. self._unplug_port(context, current=False) def delete_port_precommit(self, context): """Delete resources of a port. Called inside transaction context on session. Runtime errors are not expected, but raising an exception will result in rollback of the transaction. :param context: PortContext instance describing the current state of the port, prior to the call to delete it. """ pass def delete_port_postcommit(self, context): """Delete a port. state of the port, prior to the call to delete it. Called after the transaction completes. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Runtime errors are not expected, and will not prevent the resource from being deleted. :param context: PortContext instance describing the current state of the port, prior to the call to delete it. """ self._unplug_port(context) def try_to_bind_segment_for_agent(self, context, segment, agent): """Try to bind with segment for agent. :param context: PortContext instance describing the port :param segment: segment dictionary describing segment to bind :param agent: agents_db entry describing agent to bind :returns: True iff segment has been bound for agent Neutron segments api-ref: https://docs.openstack.org/api-ref/network/v2/#segments Example segment dictionary: {'segmentation_id': 'segmentation_id', 'network_type': 'network_type', 'id': 'segment_uuid'} Called outside any transaction during bind_port() so that derived MechanismDrivers can use agent_db data along with built-in knowledge of the corresponding agent's capabilities to attempt to bind to the specified network segment for the agent. If the segment can be bound for the agent, this function must call context.set_binding() with appropriate values and then return True. Otherwise, it must return False. """ # Skip hierarchical overlay→VLAN binding scenarios. # If we're being asked to bind a VLAN segment and the first segment # on the network is overlay (vxlan/geneve), this is hierarchical # binding and other drivers like genericswitch should handle it. network_segments = context.network.network_segments if (segment[api.NETWORK_TYPE] == n_const.TYPE_VLAN and network_segments and network_segments[0][api.NETWORK_TYPE] in [n_const.TYPE_VXLAN, n_const.TYPE_GENEVE]): LOG.debug("Skipping baremetal driver binding for VLAN " "segment %s on port %s - first network segment is " "overlay type %s (hierarchical binding scenario)", segment[api.ID], context.current['id'], network_segments[0][api.NETWORK_TYPE]) return False if not self.check_segment_for_agent(segment, agent): return False port = context.current binding_profile = port[portbindings.PROFILE] or {} local_link_information = binding_profile.get( constants.LOCAL_LINK_INFO, []) local_group_information = binding_profile.get( constants.LOCAL_GROUP_INFO, {}) bond_mode = local_group_information.get('bond_mode') by_device = {} for link in local_link_information: device = self._get_device(link) # If there is no device for all links the port will be bound, this # keeps backward compatibility. if not device: continue # Device was found, but no port_id in link - fail port binding if not link.get(constants.PORT_ID): LOG.warning('Cannot bind port %(port)s. no port_id in link ' 'information: %(link)s', {'port': port[api.ID], 'link': link}) return False # Check device on physnet, if not fail port binding if not self._is_device_on_physnet(device, segment[api.PHYSICAL_NETWORK]): LOG.warning( 'Cannot bind port %(port)s, device %(device)s is ' 'not on physical network %(physnet)s', {'port': port[api.ID], 'device': device, 'physnet': segment[api.PHYSICAL_NETWORK]}) return False by_device.setdefault(device, {}) by_device[device].setdefault('links', []) if 'driver' not in by_device[device]: # Load the driver, fail port binding on load error try: driver = common.driver_mgr(device) by_device[device]['driver'] = driver except exceptions.DriverEntrypointLoadError as e: LOG.warning('Cannot bind port %(port)s, failed to load ' 'driver for device %(device)s', {'link': link, 'port': port[api.ID], 'device': device}) LOG.debug(e.message) return False by_device[device]['links'].append(link) if not by_device: # NOTE(vsaienko): we can call set_binding ONLY when we complete # binding for the port in the segment. We do not handle the port # and want to let other drivers to bind it. return False # Check driver(s) support the bond_mode - if not fail port binding if (bond_mode and by_device and not self._is_bond_mode_supported(bond_mode, by_device)): LOG.warning('Cannot bind port %(port)s, unsupported ' 'bond_mode %(bond_mode)s', {'port': port[api.ID], 'bond_mode': bond_mode}) return False # Call each drivers create_port method to plug the device links for device, args in by_device.items(): driver = args['driver'] driver.create_port(context, segment, args['links']) # Complete the port binding provisioning_blocks.add_provisioning_component( context._plugin_context, port[api.ID], resources.PORT, BAREMETAL_DRV_ENTITY) context.set_binding(segment[api.ID], self.get_vif_type(context, agent, segment), self.get_vif_details(context, agent, segment)) return True def _is_bound(self, context): """Check if port is currently bound by this driver :param context: Port context :returns: True/False """ return (context[portbindings.VNIC_TYPE] in self.supported_vnic_types and context[portbindings.VIF_TYPE] == self.vif_type) @staticmethod def _is_device_on_physnet(device, physical_network): """Check if Device is connected to physical network If the device is not configured to any physical networks, return True so that all networks are created on the switch. :param device: Netconf device in config :param physical_network: Physical network :returns: True or False """ if (CONF[device].physical_networks and physical_network not in CONF[device].physical_networks): return False return True def _get_device(self, link): """Get device identifier from link information :param link: Link information :returns: Device identifier (switch_id or switch_info) """ device = None switch_id = link.get(constants.SWITCH_ID) switch_info = link.get(constants.SWITCH_INFO) if switch_id and switch_id in self.devices: device = self.devices[switch_id] elif switch_info and switch_info in self.devices: device = self.devices[switch_info] return device def _update_port(self, context): """Update port :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. """ port = context.current binding_profile = port[portbindings.PROFILE] local_link_information = binding_profile.get( constants.LOCAL_LINK_INFO, []) local_group_information = binding_profile.get( constants.LOCAL_GROUP_INFO, {}) bond_mode = local_group_information.get('bond_mode') by_device = {} for link in local_link_information: device = self._get_device(link) # No device == noop if not device: continue by_device.setdefault(device, {}) by_device[device].setdefault('links', []) if 'driver' not in by_device[device]: # Load the driver, if this fails the link cannot be updated try: driver = common.driver_mgr(device) by_device[device]['driver'] = driver except exceptions.DriverEntrypointLoadError as e: LOG.warning('Cannot update link %(link)s on port ' '%(port)s, failed to load driver for device ' '%(device)s', {'link': link, 'port': port[api.ID], 'device': device}) LOG.debug(e.message) continue by_device[device]['links'].append(link) # Check driver(s) support the bond_mode if (bond_mode and by_device and not self._is_bond_mode_supported(bond_mode, by_device)): LOG.error('Cannot update port %(port)s on device, unsupported ' 'bond_mode %(bond_mode)s', {'port': port[api.ID], 'bond_mode': bond_mode}) return # Call each drivers update_port method for device, args in by_device.items(): driver = args['driver'] driver.update_port(context, args['links']) def _unplug_port(self, context, current=True): """Unplug/Unbind/Delete port :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. :param current: Boolean, when true use context.current, when false use context.original """ if current: port = context.current else: port = context.original binding_profile = port[portbindings.PROFILE] or {} local_link_information = binding_profile.get( constants.LOCAL_LINK_INFO, []) local_group_information = binding_profile.get( constants.LOCAL_GROUP_INFO, {}) bond_mode = local_group_information.get('bond_mode') by_device = {} for link in local_link_information: device = self._get_device(link) # No device == noop if not device: continue by_device.setdefault(device, {}) by_device[device].setdefault('links', []) if 'driver' not in by_device[device]: # Load the driver, if this fails the link cannot be unbound try: driver = common.driver_mgr(device) by_device[device]['driver'] = driver except exceptions.DriverEntrypointLoadError as e: LOG.warning('Cannot delete link %(link)s for port ' '%(port)s, failed to load driver for device ' '%(device)s', {'link': link, 'port': port[api.ID], 'device': device}) LOG.debug(e.message) continue by_device[device]['links'].append(link) # Check driver(s) support the bond_mode if (bond_mode and by_device and not self._is_bond_mode_supported(bond_mode, by_device)): LOG.warning('Cannot delete port %(port)s on device, unsupported ' 'bond_mode %(bond_mode)s', {'port': port[api.ID], 'bond_mode': bond_mode}) return # Call each drivers delete_port method to unplug the device links for device, args in by_device.items(): driver = args['driver'] driver.delete_port(context, args['links'], current=current) @staticmethod def _is_bond_mode_supported(bond_mode, by_device): """Check if drivers support the bond mode :param bond_mode: The bond mode :param by_device: Dictionary of driver and links per-device. """ for device, args in by_device.items(): driver = args['driver'] if bond_mode and bond_mode not in driver.SUPPORTED_BOND_MODES: return False return True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8409956 networking_baremetal-7.2.0/networking_baremetal/tests/0000775000175000017500000000000015157004110022104 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/__init__.py0000664000175000017500000000627215157004031024226 0ustar00zuulzuul# Copyright (c) 2026 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. """Test package initialization. This module is imported before any test modules, allowing us to set up test-wide configuration and monkey-patches. """ from oslo_config import cfg # Store original register_opts methods _original_register_opts = cfg.ConfigOpts.register_opts _original_register_opt = cfg.ConfigOpts.register_opt # Options that are legitimately shared between agent and ML2 plugin # In production they run in separate processes, but in tests they share # cfg.CONF SHARED_OPTIONS = { ('l2vni', 'l2vni_subport_anchor_network'), } def _register_opts_ignore_known_duplicates(self, opts, group=None): """Wrapper that registers options individually, ignoring known duplicates. In production, agent and ML2 run in separate processes with separate config registries. In tests, they share cfg.CONF, causing DuplicateOptError for legitimately shared options. This wrapper only ignores duplicates for options in SHARED_OPTIONS. Any other DuplicateOptError will be raised to catch test bugs. """ for opt in opts: try: _original_register_opt(self, opt, group=group) except cfg.DuplicateOptError as e: # Only ignore if this is a known shared option if (group, opt.name) not in SHARED_OPTIONS: # This is an unexpected duplicate - raise it to catch bugs raise e # Known shared option, skip it def _register_opt_ignore_known_duplicates(self, opt, group=None, cli=False, **kwargs): """Wrapper that ignores duplicate registration for known shared options. In production, agent and ML2 run in separate processes with separate config registries. In tests, they share cfg.CONF, causing DuplicateOptError for legitimately shared options. This wrapper only ignores duplicates for options in SHARED_OPTIONS. Any other DuplicateOptError will be raised to catch test bugs. """ try: return _original_register_opt(self, opt, group=group, cli=cli, **kwargs) except cfg.DuplicateOptError as e: # Only ignore if this is a known shared option if (group, opt.name) not in SHARED_OPTIONS: # This is an unexpected duplicate - raise it to catch bugs raise e # Known shared option, that's fine return False # Monkey-patch the registration methods to handle known shared options in tests cfg.ConfigOpts.register_opts = _register_opts_ignore_known_duplicates cfg.ConfigOpts.register_opt = _register_opt_ignore_known_duplicates ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/base.py0000664000175000017500000000143215157004031023372 0ustar00zuulzuul# -*- 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 oslotest import base class TestCase(base.BaseTestCase): """Test case base class for all unit tests.""" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8409956 networking_baremetal-7.2.0/networking_baremetal/tests/unit/0000775000175000017500000000000015157004110023063 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/__init__.py0000664000175000017500000000000015157004031025164 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8419957 networking_baremetal-7.2.0/networking_baremetal/tests/unit/agent/0000775000175000017500000000000015157004110024161 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/agent/__init__.py0000664000175000017500000000000015157004031026262 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/agent/test_agent_config.py0000664000175000017500000001636115157004031030226 0ustar00zuulzuul# Copyright (c) 2026 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 neutron.tests import base as tests_base from oslo_config import cfg from networking_baremetal.agent import agent_config CONF = cfg.CONF class TestAgentConfig(tests_base.BaseTestCase): """Test cases for agent configuration options.""" def setUp(self): super(TestAgentConfig, self).setUp() # Register options for testing agent_config.register_agent_opts(CONF) def test_register_agent_opts(self): """Test agent options are registered correctly.""" self.assertIn('l2vni', CONF) self.assertIn('baremetal_agent', CONF) def test_enable_l2vni_trunk_reconciliation_default(self): """Test enable_l2vni_trunk_reconciliation default value.""" self.assertTrue(CONF.l2vni.enable_l2vni_trunk_reconciliation) def test_enable_l2vni_trunk_reconciliation_can_be_set(self): """Test enable_l2vni_trunk_reconciliation can be set.""" CONF.set_override('enable_l2vni_trunk_reconciliation', True, group='l2vni') self.assertTrue(CONF.l2vni.enable_l2vni_trunk_reconciliation) def test_l2vni_reconciliation_interval_default(self): """Test l2vni_reconciliation_interval default value.""" self.assertEqual(180, CONF.l2vni.l2vni_reconciliation_interval) def test_l2vni_reconciliation_interval_can_be_set(self): """Test l2vni_reconciliation_interval can be set.""" CONF.set_override('l2vni_reconciliation_interval', 600, group='l2vni') self.assertEqual(600, CONF.l2vni.l2vni_reconciliation_interval) def test_l2vni_reconciliation_interval_minimum(self): """Test l2vni_reconciliation_interval respects minimum.""" # Should raise error if set below minimum (min is 30) self.assertRaises(ValueError, CONF.set_override, 'l2vni_reconciliation_interval', 10, group='l2vni') def test_l2vni_network_nodes_config_default(self): """Test l2vni_network_nodes_config default value.""" self.assertEqual('/etc/neutron/l2vni_network_nodes.yaml', CONF.l2vni.l2vni_network_nodes_config) def test_l2vni_network_nodes_config_can_be_set(self): """Test l2vni_network_nodes_config can be set.""" CONF.set_override('l2vni_network_nodes_config', '/custom/path/config.yaml', group='l2vni') self.assertEqual('/custom/path/config.yaml', CONF.l2vni.l2vni_network_nodes_config) def test_l2vni_auto_create_networks_default(self): """Test l2vni_auto_create_networks default value.""" self.assertTrue(CONF.l2vni.l2vni_auto_create_networks) def test_l2vni_auto_create_networks_can_be_set(self): """Test l2vni_auto_create_networks can be set.""" CONF.set_override('l2vni_auto_create_networks', False, group='l2vni') self.assertFalse(CONF.l2vni.l2vni_auto_create_networks) def test_enable_l2vni_trunk_reconciliation_events_default(self): """Test enable_l2vni_trunk_reconciliation_events default value.""" self.assertTrue(CONF.l2vni.enable_l2vni_trunk_reconciliation_events) def test_enable_l2vni_trunk_reconciliation_events_can_be_set(self): """Test enable_l2vni_trunk_reconciliation_events can be set.""" CONF.set_override('enable_l2vni_trunk_reconciliation_events', False, group='l2vni') self.assertFalse(CONF.l2vni.enable_l2vni_trunk_reconciliation_events) def test_l2vni_subport_anchor_network_default(self): """Test l2vni_subport_anchor_network default value.""" self.assertEqual('l2vni-subport-anchor', CONF.l2vni.l2vni_subport_anchor_network) def test_l2vni_subport_anchor_network_can_be_set(self): """Test l2vni_subport_anchor_network can be set.""" CONF.set_override('l2vni_subport_anchor_network', 'custom-anchor-network', group='l2vni') self.assertEqual('custom-anchor-network', CONF.l2vni.l2vni_subport_anchor_network) def test_enable_ha_chassis_group_alignment_default(self): """Test enable_ha_chassis_group_alignment default value.""" self.assertTrue(CONF.baremetal_agent .enable_ha_chassis_group_alignment) def test_ha_chassis_group_alignment_interval_default(self): """Test ha_chassis_group_alignment_interval default value.""" self.assertEqual(600, CONF.baremetal_agent .ha_chassis_group_alignment_interval) def test_enable_router_ha_binding_default(self): """Test enable_router_ha_binding default value.""" self.assertTrue(CONF.baremetal_agent.enable_router_ha_binding) def test_enable_router_ha_binding_events_default(self): """Test enable_router_ha_binding_events default value.""" self.assertTrue(CONF.baremetal_agent.enable_router_ha_binding_events) def test_router_ha_binding_interval_default(self): """Test router_ha_binding_interval default value.""" self.assertEqual(600, CONF.baremetal_agent.router_ha_binding_interval) def test_list_opts(self): """Test list_opts returns correct format.""" opts = agent_config.list_opts() self.assertIsInstance(opts, list) self.assertEqual(2, len(opts)) # Check L2VNI options l2vni_group_name, l2vni_options = opts[0] self.assertEqual('l2vni', l2vni_group_name) self.assertEqual(agent_config.L2VNI_OPTS, l2vni_options) # Check baremetal agent options bm_group_name, bm_options = opts[1] self.assertEqual('baremetal_agent', bm_group_name) self.assertEqual(agent_config.BAREMETAL_AGENT_OPTS, bm_options) def test_all_options_have_help_text(self): """Test all configuration options have help text.""" for opt in agent_config.L2VNI_OPTS: self.assertIsNotNone(opt.help) self.assertGreater(len(opt.help), 0) for opt in agent_config.BAREMETAL_AGENT_OPTS: self.assertIsNotNone(opt.help) self.assertGreater(len(opt.help), 0) def test_boolean_options_have_defaults(self): """Test boolean options have explicit default values.""" boolean_opts = [opt for opt in agent_config.L2VNI_OPTS if isinstance(opt, cfg.BoolOpt)] boolean_opts += [opt for opt in agent_config.BAREMETAL_AGENT_OPTS if isinstance(opt, cfg.BoolOpt)] for opt in boolean_opts: self.assertIsNotNone(opt.default) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/agent/test_ironic_neutron_agent.py0000664000175000017500000005552615157004031032024 0ustar00zuulzuul# Copyright (c) 2026 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 datetime from unittest import mock from neutron.tests import base as tests_base from neutron_lib import constants as n_const from oslo_config import cfg from oslo_utils import timeutils from tooz import hashring from networking_baremetal.agent import agent_config from networking_baremetal.agent import ironic_neutron_agent from networking_baremetal import constants CONF = cfg.CONF class FakePort: """Fake Neutron Port object.""" def __init__(self, port_id, network_id, device_owner, updated_at=None): self.id = port_id self.network_id = network_id self.device_owner = device_owner # updated_at should be ISO8601 string like real Neutron ports self.updated_at = updated_at or timeutils.utcnow().isoformat() class FakeLogicalSwitchPort: """Fake OVN Logical Switch Port object.""" def __init__(self, name, ha_chassis_group=None): self.name = name self.ha_chassis_group = ( [ha_chassis_group] if ha_chassis_group else []) class FakeLogicalRouterPort: """Fake OVN Logical Router Port object.""" def __init__(self, name, ha_chassis_group=None): self.name = name self.ha_chassis_group = ( [ha_chassis_group] if ha_chassis_group else []) class FakeOVNCommand: """Fake OVN IDL command result.""" def __init__(self, result): self.result = result def execute(self, check_error=False): if check_error and self.result is None: from ovsdbapp.backend.ovs_idl import idlutils raise idlutils.RowNotFound(table='Unknown', col='name', match='unknown') return self.result class TestHAChassisGroupAlignment(tests_base.BaseTestCase): """Test cases for HA chassis group alignment reconciliation.""" def setUp(self): super(TestHAChassisGroupAlignment, self).setUp() # Register config options agent_config.register_baremetal_agent_opts(CONF) # Set required config overrides CONF.set_override('enable_ha_chassis_group_alignment', True, group='baremetal_agent') # Create mock agent with minimal setup self.agent = mock.MagicMock(spec=ironic_neutron_agent .BaremetalNeutronAgent) self.agent._ha_alignment_lock = mock.MagicMock() self.agent._ha_alignment_lock.acquire.return_value = True # Setup agent_id and member_manager with real hashring self.agent.agent_id = 'test-agent-id' self.agent.member_manager = mock.MagicMock() # Create a hashring with our test agent as the only member # This means our agent will be responsible for all keys self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) self.agent.trunk_manager = None # Bind the methods we're testing self.agent._reconcile_ha_chassis_group_alignment = ( ironic_neutron_agent.BaremetalNeutronAgent ._reconcile_ha_chassis_group_alignment.__get__(self.agent)) self.agent._align_ha_chassis_group_for_network = ( ironic_neutron_agent.BaremetalNeutronAgent ._align_ha_chassis_group_for_network.__get__(self.agent)) self.agent._get_neutron_client = mock.MagicMock() @mock.patch('networking_baremetal.agent.ovn_client.get_ovn_nb_idl', autospec=True) def test_reconcile_no_baremetal_ports(self, mock_get_ovn_nb): """Test reconciliation when no baremetal ports exist.""" # Setup mocks mock_neutron = mock.MagicMock() mock_neutron.network.ports.return_value = [] self.agent._get_neutron_client.return_value = mock_neutron mock_ovn_nb = mock.MagicMock() mock_get_ovn_nb.return_value = mock_ovn_nb # Execute self.agent._reconcile_ha_chassis_group_alignment() # Verify - should query for baremetal ports but do nothing else mock_neutron.network.ports.assert_called_once_with( device_owner=constants.BAREMETAL_NONE) # Should not query for router ports self.assertEqual(1, mock_neutron.network.ports.call_count) @mock.patch('networking_baremetal.agent.ovn_client.get_ovn_nb_idl', autospec=True) def test_reconcile_with_baremetal_ports(self, mock_get_ovn_nb): """Test reconciliation processes baremetal ports correctly.""" # Setup mocks bm_port = FakePort('bm-port-1', 'net-1', constants.BAREMETAL_NONE) router_port = FakePort('router-port-1', 'net-1', n_const.DEVICE_OWNER_ROUTER_INTF) mock_neutron = mock.MagicMock() mock_neutron.network.ports.side_effect = [ [bm_port], # First call for baremetal ports [router_port] # Second call for router ports on network ] self.agent._get_neutron_client.return_value = mock_neutron # Setup OVN mocks lsp = FakeLogicalSwitchPort('neutron-bm-port-1', 'ha-group-1') lrp = FakeLogicalRouterPort('lrp-router-port-1', 'ha-group-2') mock_ovn_nb = mock.MagicMock() mock_ovn_nb.lsp_get.return_value = FakeOVNCommand(lsp) mock_ovn_nb.lrp_get.return_value = FakeOVNCommand(lrp) mock_ovn_nb.lrp_set_ha_chassis_group.return_value = ( FakeOVNCommand(None)) mock_get_ovn_nb.return_value = mock_ovn_nb # Execute self.agent._reconcile_ha_chassis_group_alignment() # Verify - should update router port's HA chassis group mock_ovn_nb.lrp_set_ha_chassis_group.assert_called_once_with( 'lrp-router-port-1', 'ha-group-1') @mock.patch('networking_baremetal.agent.ovn_client.get_ovn_nb_idl', autospec=True) def test_reconcile_already_aligned(self, mock_get_ovn_nb): """Test reconciliation when HA groups already match.""" # Setup mocks bm_port = FakePort('bm-port-1', 'net-1', constants.BAREMETAL_NONE) router_port = FakePort('router-port-1', 'net-1', n_const.DEVICE_OWNER_ROUTER_INTF) mock_neutron = mock.MagicMock() mock_neutron.network.ports.side_effect = [ [bm_port], [router_port] ] self.agent._get_neutron_client.return_value = mock_neutron # Both use the same HA chassis group lsp = FakeLogicalSwitchPort('neutron-bm-port-1', 'ha-group-1') lrp = FakeLogicalRouterPort('lrp-router-port-1', 'ha-group-1') mock_ovn_nb = mock.MagicMock() mock_ovn_nb.lsp_get.return_value = FakeOVNCommand(lsp) mock_ovn_nb.lrp_get.return_value = FakeOVNCommand(lrp) mock_get_ovn_nb.return_value = mock_ovn_nb # Execute self.agent._reconcile_ha_chassis_group_alignment() # Verify - should NOT update since already aligned mock_ovn_nb.lrp_set_ha_chassis_group.assert_not_called() @mock.patch('networking_baremetal.agent.ovn_client.get_ovn_nb_idl', autospec=True) def test_reconcile_filters_by_hash_ring(self, mock_get_ovn_nb): """Test reconciliation respects hash ring filtering.""" # Setup mocks bm_port = FakePort('bm-port-1', 'net-1', constants.BAREMETAL_NONE) mock_neutron = mock.MagicMock() mock_neutron.network.ports.return_value = [bm_port] self.agent._get_neutron_client.return_value = mock_neutron # Setup hashring with multiple agents, but find a network that # our test agent doesn't manage other_agent_id = 'other-agent-id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id, other_agent_id]) # Verify that net-1 is NOT managed by our agent # (if by chance it is, the test setup is invalid) responsible_agents = self.agent.member_manager.hashring[ 'net-1'.encode('utf-8')] self.assertNotIn(self.agent.agent_id, responsible_agents, "Test setup error: net-1 should not be managed " "by test agent") mock_ovn_nb = mock.MagicMock() mock_get_ovn_nb.return_value = mock_ovn_nb # Execute self.agent._reconcile_ha_chassis_group_alignment() # Verify - should not query for router ports (only baremetal ports) self.assertEqual(1, mock_neutron.network.ports.call_count) @mock.patch('networking_baremetal.agent.ovn_client.get_ovn_nb_idl', autospec=True) def test_reconcile_with_time_window(self, mock_get_ovn_nb): """Test reconciliation respects time window filtering.""" # Enable time window filtering CONF.set_override( 'limit_ha_chassis_group_alignment_to_recent_changes_only', True, group='baremetal_agent') CONF.set_override('ha_chassis_group_alignment_window', 600, group='baremetal_agent') # Setup ports - one recent, one old now = timeutils.utcnow() recent_port = FakePort( 'bm-port-recent', 'net-1', constants.BAREMETAL_NONE, updated_at=now.isoformat()) old_dt = now - datetime.timedelta(seconds=700) old_port = FakePort( 'bm-port-old', 'net-2', constants.BAREMETAL_NONE, updated_at=old_dt.isoformat()) mock_neutron = mock.MagicMock() mock_neutron.network.ports.return_value = [recent_port, old_port] self.agent._get_neutron_client.return_value = mock_neutron mock_ovn_nb = mock.MagicMock() lsp = FakeLogicalSwitchPort('neutron-bm-port-recent', 'ha-group-1') mock_ovn_nb.lsp_get.return_value = FakeOVNCommand(lsp) mock_get_ovn_nb.return_value = mock_ovn_nb # Execute self.agent._reconcile_ha_chassis_group_alignment() # Verify - should only process recent port (net-1), old port (net-2) # should be filtered out by time window # We verify this indirectly by checking OVN queries # Note: OVN prefixes port names with "neutron-" mock_ovn_nb.lsp_get.assert_called_once_with('neutron-bm-port-recent') @mock.patch('networking_baremetal.agent.ovn_client.get_ovn_nb_idl', autospec=True) def test_reconcile_lock_already_held(self, mock_get_ovn_nb): """Test reconciliation skips when lock is held.""" # Lock is already held self.agent._ha_alignment_lock.acquire.return_value = False mock_neutron = mock.MagicMock() self.agent._get_neutron_client.return_value = mock_neutron # Execute self.agent._reconcile_ha_chassis_group_alignment() # Verify - should not query Neutron at all mock_neutron.network.ports.assert_not_called() @mock.patch('networking_baremetal.agent.ovn_client.get_ovn_nb_idl', autospec=True) def test_reconcile_ovn_connection_failure(self, mock_get_ovn_nb): """Test reconciliation handles OVN connection failure.""" mock_get_ovn_nb.side_effect = RuntimeError("Connection failed") # Execute - should not raise self.agent._reconcile_ha_chassis_group_alignment() # Verify lock is released self.agent._ha_alignment_lock.release.assert_called_once() def test_align_network_no_ha_group_on_bm_port(self): """Test alignment skips when baremetal port has no HA group.""" bm_port = FakePort('bm-port-1', 'net-1', constants.BAREMETAL_NONE) mock_neutron = mock.MagicMock() mock_ovn_nb = mock.MagicMock() # Baremetal port has no HA chassis group lsp = FakeLogicalSwitchPort('neutron-bm-port-1', None) mock_ovn_nb.lsp_get.return_value = FakeOVNCommand(lsp) # Execute self.agent._align_ha_chassis_group_for_network( 'net-1', [bm_port], mock_neutron, mock_ovn_nb) # Verify - should not query for router ports mock_neutron.network.ports.assert_not_called() def test_align_network_no_router_ports(self): """Test alignment when network has no router ports.""" bm_port = FakePort('bm-port-1', 'net-1', constants.BAREMETAL_NONE) mock_neutron = mock.MagicMock() mock_neutron.network.ports.return_value = [] # No router ports mock_ovn_nb = mock.MagicMock() lsp = FakeLogicalSwitchPort('neutron-bm-port-1', 'ha-group-1') mock_ovn_nb.lsp_get.return_value = FakeOVNCommand(lsp) # Execute self.agent._align_ha_chassis_group_for_network( 'net-1', [bm_port], mock_neutron, mock_ovn_nb) # Verify - should query for router ports but not update anything mock_neutron.network.ports.assert_called_once_with( network_id='net-1', device_owner=n_const.DEVICE_OWNER_ROUTER_INTF) mock_ovn_nb.lrp_set_ha_chassis_group.assert_not_called() def test_align_network_router_port_not_in_ovn(self): """Test alignment when router port not found in OVN.""" bm_port = FakePort('bm-port-1', 'net-1', constants.BAREMETAL_NONE) router_port = FakePort('router-port-1', 'net-1', n_const.DEVICE_OWNER_ROUTER_INTF) mock_neutron = mock.MagicMock() mock_neutron.network.ports.return_value = [router_port] mock_ovn_nb = mock.MagicMock() lsp = FakeLogicalSwitchPort('neutron-bm-port-1', 'ha-group-1') mock_ovn_nb.lsp_get.return_value = FakeOVNCommand(lsp) # Router port not found in OVN mock_ovn_nb.lrp_get.return_value = FakeOVNCommand(None) # Execute self.agent._align_ha_chassis_group_for_network( 'net-1', [bm_port], mock_neutron, mock_ovn_nb) # Verify - should not try to update mock_ovn_nb.lrp_set_ha_chassis_group.assert_not_called() def test_align_network_handles_exceptions(self): """Test alignment handles exceptions gracefully.""" bm_port = FakePort('bm-port-1', 'net-1', constants.BAREMETAL_NONE) router_port = FakePort('router-port-1', 'net-1', n_const.DEVICE_OWNER_ROUTER_INTF) mock_neutron = mock.MagicMock() mock_neutron.network.ports.return_value = [router_port] mock_ovn_nb = mock.MagicMock() lsp = FakeLogicalSwitchPort('neutron-bm-port-1', 'ha-group-1') mock_ovn_nb.lsp_get.return_value = FakeOVNCommand(lsp) # Simulate error when updating lrp = FakeLogicalRouterPort('lrp-router-port-1', 'ha-group-2') mock_ovn_nb.lrp_get.return_value = FakeOVNCommand(lrp) mock_ovn_nb.lrp_set_ha_chassis_group.side_effect = RuntimeError( "Update failed") # Execute - should not raise self.agent._align_ha_chassis_group_for_network( 'net-1', [bm_port], mock_neutron, mock_ovn_nb) # Verify - attempted to update mock_ovn_nb.lrp_set_ha_chassis_group.assert_called_once() def test_align_network_continues_after_missing_ports(self): """Test alignment continues when some BM ports missing from OVN. This tests LP#2144061 - when some baremetal ports don't exist in OVN (RowNotFound), the reconciliation should continue checking other ports rather than short-circuiting. """ from ovsdbapp.backend.ovs_idl import idlutils # Create multiple baremetal ports bm_port_1 = FakePort('bm-port-1', 'net-1', constants.BAREMETAL_NONE) bm_port_2 = FakePort('bm-port-2', 'net-1', constants.BAREMETAL_NONE) router_port = FakePort('router-port-1', 'net-1', n_const.DEVICE_OWNER_ROUTER_INTF) mock_neutron = mock.MagicMock() mock_neutron.network.ports.return_value = [router_port] mock_ovn_nb = mock.MagicMock() # First port lookup fails (port missing from OVN) # Second port lookup succeeds and has HA chassis group # Note: OVN prefixes port names with "neutron-" def lsp_get_side_effect(port_name): if port_name == 'neutron-bm-port-1': # Simulate RowNotFound for missing port raise idlutils.RowNotFound(table='Logical_Switch_Port', col='name', match=port_name) elif port_name == 'neutron-bm-port-2': # Second port exists and has HA chassis group lsp = FakeLogicalSwitchPort('neutron-bm-port-2', 'ha-group-1') return FakeOVNCommand(lsp) mock_ovn_nb.lsp_get.side_effect = lsp_get_side_effect # Router port needs alignment lrp = FakeLogicalRouterPort('lrp-router-port-1', 'ha-group-2') mock_ovn_nb.lrp_get.return_value = FakeOVNCommand(lrp) mock_ovn_nb.lrp_set_ha_chassis_group.return_value = FakeOVNCommand( None) # Execute - should not raise self.agent._align_ha_chassis_group_for_network( 'net-1', [bm_port_1, bm_port_2], mock_neutron, mock_ovn_nb) # Verify - should have found HA chassis group from second port # and updated router port mock_ovn_nb.lrp_set_ha_chassis_group.assert_called_once_with( 'lrp-router-port-1', 'ha-group-1') def test_align_network_skips_when_all_ports_missing(self): """Test alignment skips when all BM ports missing from OVN.""" from ovsdbapp.backend.ovs_idl import idlutils bm_port = FakePort('bm-port-1', 'net-1', constants.BAREMETAL_NONE) mock_neutron = mock.MagicMock() mock_ovn_nb = mock.MagicMock() # Port lookup fails - port missing from OVN # Note: OVN prefixes port names with "neutron-" mock_ovn_nb.lsp_get.side_effect = idlutils.RowNotFound( table='Logical_Switch_Port', col='name', match='neutron-bm-port-1') # Execute self.agent._align_ha_chassis_group_for_network( 'net-1', [bm_port], mock_neutron, mock_ovn_nb) # Verify - should not query for router ports since we couldn't # find any baremetal ports in OVN mock_neutron.network.ports.assert_not_called() class TestL2VNITargetedReconciliation(tests_base.BaseTestCase): """Tests for targeted single-VLAN reconciliation in agent.""" def test_reconcile_single_vlan_blocking_acquires_lock(self): """Test wrapper method acquires lock and calls trunk manager.""" agent = mock.Mock(spec=ironic_neutron_agent.BaremetalNeutronAgent) agent.trunk_manager = mock.Mock() agent._l2vni_reconciliation_lock = mock.MagicMock() ironic_neutron_agent.BaremetalNeutronAgent.\ _reconcile_single_vlan_blocking( agent, 'net-1', 'physnet1', 100, 'add') agent._l2vni_reconciliation_lock.__enter__.assert_called_once() agent.trunk_manager.reconcile_single_vlan.assert_called_once_with( 'net-1', 'physnet1', 100, 'add') def test_reconcile_single_vlan_blocking_handles_exception(self): """Test wrapper method handles exceptions gracefully.""" agent = mock.Mock(spec=ironic_neutron_agent.BaremetalNeutronAgent) agent.trunk_manager = mock.Mock() agent._l2vni_reconciliation_lock = mock.MagicMock() agent.trunk_manager.reconcile_single_vlan.side_effect = \ Exception("Test error") ironic_neutron_agent.BaremetalNeutronAgent.\ _reconcile_single_vlan_blocking( agent, 'net-1', 'physnet1', 100, 'add') class TestBaremetalAgentConfig(tests_base.BaseTestCase): """Test cases for baremetal agent configuration options.""" def setUp(self): super(TestBaremetalAgentConfig, self).setUp() # Register options for testing agent_config.register_baremetal_agent_opts(CONF) def test_register_baremetal_agent_opts(self): """Test baremetal agent options are registered correctly.""" self.assertIn('baremetal_agent', CONF) def test_enable_ha_chassis_group_alignment_default(self): """Test enable_ha_chassis_group_alignment default value.""" self.assertTrue(CONF.baremetal_agent .enable_ha_chassis_group_alignment) def test_ha_chassis_group_alignment_interval_default(self): """Test ha_chassis_group_alignment_interval default value.""" self.assertEqual(600, CONF.baremetal_agent .ha_chassis_group_alignment_interval) def test_ha_chassis_group_alignment_interval_minimum(self): """Test ha_chassis_group_alignment_interval respects minimum.""" # Should raise error if set below minimum (min is 60) self.assertRaises(ValueError, CONF.set_override, 'ha_chassis_group_alignment_interval', 30, group='baremetal_agent') def test_limit_ha_alignment_to_recent_changes_default(self): """Test limit_ha_alignment_to_recent_changes_only default.""" self.assertTrue( CONF.baremetal_agent .limit_ha_chassis_group_alignment_to_recent_changes_only) def test_ha_chassis_group_alignment_window_default(self): """Test ha_chassis_group_alignment_window default value.""" self.assertEqual(1200, CONF.baremetal_agent .ha_chassis_group_alignment_window) def test_list_opts(self): """Test list_opts returns correct format.""" opts = agent_config.list_opts() self.assertIsInstance(opts, list) self.assertEqual(2, len(opts)) # Verify l2vni group group_name, options = opts[0] self.assertEqual('l2vni', group_name) self.assertEqual(agent_config.L2VNI_OPTS, options) # Verify baremetal_agent group group_name, options = opts[1] self.assertEqual('baremetal_agent', group_name) self.assertEqual(agent_config.BAREMETAL_AGENT_OPTS, options) def test_all_options_have_help_text(self): """Test all configuration options have help text.""" for opt in agent_config.BAREMETAL_AGENT_OPTS: self.assertIsNotNone(opt.help) self.assertGreater(len(opt.help), 0) def test_boolean_options_have_defaults(self): """Test boolean options have explicit default values.""" boolean_opts = [opt for opt in agent_config.BAREMETAL_AGENT_OPTS if isinstance(opt, cfg.BoolOpt)] for opt in boolean_opts: self.assertIsNotNone(opt.default) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/agent/test_l2vni_trunk_manager.py0000664000175000017500000024244615157004031031557 0ustar00zuulzuul# Copyright (c) 2026 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 neutron.tests import base as tests_base from neutron_lib import constants as n_const from openstack import exceptions as sdkexc from oslo_config import cfg from networking_baremetal.agent import agent_config from networking_baremetal.agent import l2vni_trunk_manager CONF = cfg.CONF class FakeHAChassis: """Fake OVN HA_Chassis object (member of HA_Chassis_Group).""" def __init__(self, chassis_name): self.chassis_name = chassis_name class FakeHAChassisGroup: """Fake OVN HA Chassis Group object.""" def __init__(self, name, chassis_list, uuid=None): self.name = name self.ha_chassis = chassis_list class FakeChassis: """Fake OVN Chassis object. In real OVN, the chassis name IS the system-id (UUID). For test backwards compatibility, we accept both parameters but use system_id as the name since that's what code expects. """ def __init__(self, name, system_id, external_ids=None, other_config=None, hostname=None): # In real OVN, chassis.name IS the system-id # Use system_id as the name to match real behavior self.name = system_id self.external_ids = external_ids or {} self.other_config = other_config or {} self.hostname = hostname # Don't store system-id in external_ids - that's not where it is # in real OVN (it's the name field) class FakeLogicalRouterPort: """Fake OVN Logical Router Port object.""" def __init__(self, name, gateway_chassis_list, networks=None): self.name = name self.gateway_chassis = gateway_chassis_list self.networks = networks or [] self.ha_chassis_group = [] class FakeLogicalSwitchPort: """Fake OVN Logical Switch Port object.""" def __init__(self, name, lsp_type, options=None, external_ids=None): self.name = name self.type = lsp_type self.options = options or {} self.external_ids = external_ids or {} class FakeLogicalSwitch: """Fake OVN Logical Switch object.""" def __init__(self, name, external_ids=None): self.name = name self.external_ids = external_ids or {} class FakePort: """Fake Neutron Port object.""" def __init__(self, port_id, device_owner, binding_profile=None, device_id=None): self.id = port_id self.device_owner = device_owner self.binding_profile = binding_profile or {} self.binding = {'profile': binding_profile or {}} self.device_id = device_id class FakeTrunk: """Fake Neutron Trunk object.""" def __init__(self, trunk_id, port_id, name='', sub_ports=None): self.id = trunk_id self.port_id = port_id self.name = name self.sub_ports = sub_ports or [] class FakeNetwork: """Fake Neutron Network object.""" def __init__(self, network_id, name=''): self.id = network_id self.name = name class FakeSegment: """Fake Neutron Segment object.""" def __init__(self, network_id, network_type, segmentation_id, physical_network): self.network_id = network_id self.network_type = network_type self.segmentation_id = segmentation_id self.physical_network = physical_network class FakeIronicPort: """Fake Ironic Port object.""" def __init__(self, node_id, physical_network, local_link_connection): self.node_id = node_id self.physical_network = physical_network self.local_link_connection = local_link_connection class FakeIronicNode: """Fake Ironic Node object.""" def __init__(self, node_id, system_id): self.id = node_id self.uuid = node_id self.properties = {'system_id': system_id} class TestL2VNITrunkManager(tests_base.BaseTestCase): """Test cases for L2VNI Trunk Manager.""" def setUp(self): super(TestL2VNITrunkManager, self).setUp() # Register L2VNI config options agent_config.register_l2vni_opts(cfg.CONF) self.mock_neutron = mock.Mock() self.mock_ovn_nb = mock.Mock() self.mock_ovn_sb = mock.Mock() self.mock_ironic = mock.Mock() # Setup OVN tables structure # Tables now use .rows.values() pattern self.mock_ovn_nb.tables = { 'HA_Chassis_Group': mock.Mock( rows=mock.Mock(values=mock.Mock(return_value=[]))), 'Logical_Router_Port': mock.Mock( rows=mock.Mock(values=mock.Mock(return_value=[]))), 'Logical_Switch_Port': mock.Mock( rows=mock.Mock(values=mock.Mock(return_value=[]))), 'Logical_Switch': mock.Mock( rows=mock.Mock(values=mock.Mock(return_value=[]))), } self.mock_ovn_sb.tables = { 'Chassis': mock.Mock( rows=mock.Mock(values=mock.Mock(return_value=[]))), 'Port': mock.Mock( rows=mock.Mock(values=mock.Mock(return_value=[]))), } # Note: member_manager is None, so _should_manage_chassis returns True # for all chassis (single agent mode) self.manager = l2vni_trunk_manager.L2VNITrunkManager( neutron_client=self.mock_neutron, ovn_nb_idl=self.mock_ovn_nb, ovn_sb_idl=self.mock_ovn_sb, ironic_client=self.mock_ironic, member_manager=None, agent_id=None ) def test_initialize(self): """Test trunk manager initialization.""" self.assertIsNotNone(self.manager.neutron) self.assertIsNotNone(self.manager.ovn_nb_idl) self.assertIsNotNone(self.manager.ovn_sb_idl) self.assertIsNotNone(self.manager.ironic) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_ensure_infrastructure_networks', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_discover_trunks', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_calculate_required_vlans', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_reconcile_subports', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_cleanup_unused_infrastructure', autospec=True) def test_reconcile_full_workflow(self, mock_cleanup, mock_reconcile_subports, mock_calculate_vlans, mock_discover_trunks, mock_ensure_infra): """Test full reconciliation workflow.""" mock_discover_trunks.return_value = {} mock_calculate_vlans.return_value = {} self.manager.reconcile() mock_ensure_infra.assert_called_once() mock_discover_trunks.assert_called_once() mock_calculate_vlans.assert_called_once() mock_reconcile_subports.assert_called_once() mock_cleanup.assert_called_once() def test_ensure_infrastructure_networks_auto_create_enabled(self): """Test infrastructure network creation when auto-create enabled.""" cfg.CONF.set_override('l2vni_auto_create_networks', True, group='l2vni') # Mock chassis and add to SB table chassis = FakeChassis('chassis-1', 'system-id-1') self.mock_ovn_sb.tables['Chassis'].rows.values.return_value = [ chassis] # Mock HA chassis group with proper structure ha_chassis = FakeHAChassis('system-id-1') ha_group = FakeHAChassisGroup('ha_group_1', [ha_chassis]) self.mock_ovn_nb.tables['HA_Chassis_Group'].rows.values\ .return_value = [ha_group] # Mock network doesn't exist self.mock_neutron.network.networks.return_value = [] self.mock_neutron.network.create_network.return_value = FakeNetwork( 'network-id-1', 'l2vni-ha-group-ha_group_1') self.manager._ensure_infrastructure_networks() # Should create ha_chassis_group network self.mock_neutron.network.create_network.assert_called() # Check that ha_group network was created (might be multiple calls) calls = self.mock_neutron.network.create_network.call_args_list network_names = [call.kwargs['name'] for call in calls] self.assertIn('l2vni-ha-group-ha_group_1', network_names) for call in calls: if call.kwargs['name'] == 'l2vni-ha-group-ha_group_1': self.assertEqual('geneve', call.kwargs.get('provider_network_type')) def test_ensure_infrastructure_networks_auto_create_disabled(self): """Test infrastructure network creation when auto-create disabled.""" cfg.CONF.set_override('l2vni_auto_create_networks', False, group='l2vni') # Mock chassis and add to SB table chassis = FakeChassis('chassis-1', 'system-id-1') self.mock_ovn_sb.tables['Chassis'].rows.values.return_value = [ chassis] # Mock HA chassis group with proper structure ha_chassis = FakeHAChassis('system-id-1') ha_group = FakeHAChassisGroup('ha_group_1', [ha_chassis]) self.mock_ovn_nb.tables['HA_Chassis_Group'].rows.values\ .return_value = [ha_group] self.mock_neutron.network.networks.return_value = [] self.manager._ensure_infrastructure_networks() # Should not create network self.mock_neutron.network.create_network.assert_not_called() def test_ensure_subport_anchor_network_creates_when_missing(self): """Test subport anchor network creation when it doesn't exist.""" cfg.CONF.set_override('l2vni_auto_create_networks', True, group='l2vni') cfg.CONF.set_override('l2vni_subport_anchor_network', 'anchor-network', group='l2vni') # Mock network doesn't exist self.mock_neutron.network.networks.return_value = [] self.mock_neutron.network.create_network.return_value = FakeNetwork( 'anchor-net-id', 'anchor-network') self.manager._ensure_subport_anchor_network() # Should create network self.mock_neutron.network.create_network.assert_called_once() call_kwargs = self.mock_neutron.network.create_network.call_args.kwargs self.assertEqual('anchor-network', call_kwargs['name']) self.assertEqual('geneve', call_kwargs['provider_network_type']) def test_ensure_subport_anchor_network_reuses_existing(self): """Test subport anchor network reuses existing network.""" cfg.CONF.set_override('l2vni_subport_anchor_network', 'anchor-network', group='l2vni') # Mock network exists existing_network = FakeNetwork('existing-id', 'anchor-network') self.mock_neutron.network.networks.return_value = [existing_network] result = self.manager._ensure_subport_anchor_network() # Should not create new network self.mock_neutron.network.create_network.assert_not_called() self.assertEqual('existing-id', result) def test_ensure_subport_anchor_network_fails_on_misconfiguration(self): """Test subport anchor network fails with error on type mismatch.""" from openstack import exceptions as sdkexc cfg.CONF.set_override('l2vni_auto_create_networks', True, group='l2vni') cfg.CONF.set_override('l2vni_subport_anchor_network', 'anchor-network', group='l2vni') cfg.CONF.set_override('l2vni_subport_anchor_network_type', 'geneve', group='l2vni') # Mock network doesn't exist self.mock_neutron.network.networks.return_value = [] # Network creation fails due to misconfiguration self.mock_neutron.network.create_network.side_effect = \ sdkexc.BadRequestException("geneve not supported") # Should raise the exception rather than fallback self.assertRaises(sdkexc.BadRequestException, self.manager._ensure_subport_anchor_network) # Should only attempt once (no fallback) self.assertEqual(1, self.mock_neutron.network.create_network.call_count) # Verify it attempted with configured type call_kwargs = ( self.mock_neutron.network.create_network.call_args.kwargs) self.assertEqual('anchor-network', call_kwargs['name']) self.assertEqual('geneve', call_kwargs['provider_network_type']) def test_ensure_ha_group_network_fails_on_misconfiguration(self): """Test HA group network fails with error on type mismatch.""" from openstack import exceptions as sdkexc cfg.CONF.set_override('l2vni_auto_create_networks', True, group='l2vni') cfg.CONF.set_override('l2vni_subport_anchor_network_type', 'geneve', group='l2vni') # Create fake HA chassis group ha_chassis = FakeHAChassis('system-1') ha_group = FakeHAChassisGroup('ha_group_test', [ha_chassis]) # Mock network doesn't exist self.mock_neutron.network.networks.return_value = [] # Network creation fails due to misconfiguration self.mock_neutron.network.create_network.side_effect = \ sdkexc.BadRequestException("geneve not supported") # Should raise the exception rather than fallback self.assertRaises(sdkexc.BadRequestException, self.manager._ensure_ha_group_network, ha_group) # Should only attempt once (no fallback) self.assertEqual(1, self.mock_neutron.network.create_network.call_count) # Verify it attempted with configured type call_kwargs = ( self.mock_neutron.network.create_network.call_args.kwargs) self.assertEqual('l2vni-ha-group-ha_group_test', call_kwargs['name']) self.assertEqual('geneve', call_kwargs['provider_network_type']) def test_discover_trunks_finds_existing_trunks(self): """Test trunk discovery finds existing L2VNI trunks.""" # Setup HA chassis group # In real OVN, chassis name IS the system-id chassis = FakeChassis( 'chassis-1', 'system-1', other_config={'ovn-bridge-mappings': 'physnet1:br-ex'}) ha_chassis = FakeHAChassis('system-1') ha_group = FakeHAChassisGroup('ha_group_1', [ha_chassis]) self.mock_ovn_nb.tables['HA_Chassis_Group'].rows.values\ .return_value = [ha_group] # Setup Southbound chassis self.mock_ovn_sb.tables['Chassis'].rows.values\ .return_value = [chassis] # Mock trunks with anchor port that has local_link_information local_link = { 'switch_id': '00:11:22:33:44:55', 'port_id': 'Ethernet1/5', 'switch_info': 'switch1' } anchor_port = FakePort( 'anchor-port-id', l2vni_trunk_manager.DEVICE_OWNER_L2VNI_ANCHOR, binding_profile={ 'system_id': 'system-1', 'physical_network': 'physnet1', 'local_link_information': [local_link] } ) trunk = FakeTrunk('trunk-id-1', 'anchor-port-id', name='l2vni-trunk-system-1-physnet1') self.mock_neutron.network.ports.return_value = [anchor_port] self.mock_neutron.network.trunks.return_value = [trunk] result = self.manager._discover_trunks() self.assertEqual(1, len(result)) self.assertIn(('system-1', 'physnet1'), result) self.assertEqual('trunk-id-1', result[('system-1', 'physnet1')]) def test_discover_trunks_ignores_non_l2vni_trunks(self): """Test trunk discovery ignores non-L2VNI device owners.""" # Mock port with wrong device owner port = FakePort('port-id', 'network:dhcp') trunk = FakeTrunk('trunk-id', 'port-id') self.mock_neutron.ports.return_value = [port] self.mock_neutron.trunks.return_value = [trunk] result = self.manager._discover_trunks() self.assertEqual(0, len(result)) @mock.patch('neutron.common.ovn.utils.ovn_name', autospec=True) def test_calculate_required_vlans_from_ha_groups(self, mock_ovn_name): """Test VLAN calculation from HA chassis groups.""" # Mock ovn_name to return the expected logical switch name mock_ovn_name.return_value = 'neutron-network-id-1' # Setup HA chassis group chassis1 = FakeChassis( 'chassis-1', 'system-id-1', other_config={'ovn-bridge-mappings': 'physnet1:br-ex,physnet2:br-data'}) ha_chassis = FakeHAChassis('system-id-1') ha_group = FakeHAChassisGroup('ha_group_1', [ha_chassis]) self.mock_ovn_nb.tables['HA_Chassis_Group'].rows.values\ .return_value = [ha_group] # Setup Southbound chassis (needed for _get_all_chassis_with_physnet) self.mock_ovn_sb.tables['Chassis'].rows.values\ .return_value = [chassis1] # Setup router port with gateway chassis lrp = FakeLogicalRouterPort( 'lrp-1', [mock.Mock(chassis=chassis1)], networks=['192.168.1.1/24'] ) self.mock_ovn_nb.tables['Logical_Router_Port'].rows.values\ .return_value = [lrp] # Mock logical switch port (localnet) lsp = FakeLogicalSwitchPort( 'provnet-physnet1', 'localnet', options={'network_name': 'physnet1'}, external_ids={'neutron:network_id': 'network-id-1'} ) self.mock_ovn_nb.tables['Logical_Switch_Port'].rows.values\ .return_value = [lsp] # Mock logical switch ls = FakeLogicalSwitch( 'neutron-network-id-1', external_ids={'neutron:network_id': 'network-id-1'} ) ls.ports = [lsp] self.mock_ovn_nb.tables['Logical_Switch'].rows.values\ .return_value = [ls] # Mock segment segment = FakeSegment('network-id-1', n_const.TYPE_VLAN, 100, 'physnet1') self.mock_neutron.network.segments.return_value = [segment] result = self.manager._calculate_required_vlans() # Should find VLAN 100 on physnet1 for system-id-1 self.assertIn(('system-id-1', 'physnet1'), result) self.assertIn(100, result[('system-id-1', 'physnet1')]) # VNI should be None since no overlay segment exists self.assertIsNone(result[('system-id-1', 'physnet1')][100]) @mock.patch('neutron.common.ovn.utils.ovn_name', autospec=True) def test_calculate_required_vlans_captures_vni(self, mock_ovn_name): """Test VLAN calculation captures VNI from overlay segments.""" # Mock ovn_name to return the expected logical switch name mock_ovn_name.return_value = 'neutron-network-id-1' # Setup HA chassis group chassis1 = FakeChassis( 'chassis-1', 'system-id-1', other_config={'ovn-bridge-mappings': 'physnet1:br-ex,physnet2:br-data'}) ha_chassis = FakeHAChassis('system-id-1') ha_group = FakeHAChassisGroup('ha_group_1', [ha_chassis]) self.mock_ovn_nb.tables['HA_Chassis_Group'].rows.values\ .return_value = [ha_group] # Setup Southbound chassis self.mock_ovn_sb.tables['Chassis'].rows.values\ .return_value = [chassis1] # Setup router port lrp = FakeLogicalRouterPort( 'lrp-1', [mock.Mock(chassis=chassis1)], networks=['192.168.1.1/24'] ) self.mock_ovn_nb.tables['Logical_Router_Port'].rows.values\ .return_value = [lrp] # Mock logical switch port (localnet) lsp = FakeLogicalSwitchPort( 'provnet-physnet1', 'localnet', options={'network_name': 'physnet1'}, external_ids={'neutron:network_id': 'network-id-1'} ) self.mock_ovn_nb.tables['Logical_Switch_Port'].rows.values\ .return_value = [lsp] # Mock logical switch ls = FakeLogicalSwitch( 'neutron-network-id-1', external_ids={'neutron:network_id': 'network-id-1'} ) ls.ports = [lsp] self.mock_ovn_nb.tables['Logical_Switch'].rows.values\ .return_value = [ls] # Mock segments: VLAN + VXLAN overlay vlan_segment = FakeSegment('network-id-1', n_const.TYPE_VLAN, 100, 'physnet1') vxlan_segment = FakeSegment('network-id-1', n_const.TYPE_VXLAN, 5000, None) self.mock_neutron.network.segments.return_value = [vlan_segment, vxlan_segment] result = self.manager._calculate_required_vlans() # Should find VLAN 100 on physnet1 for system-id-1 with VNI 5000 self.assertIn(('system-id-1', 'physnet1'), result) self.assertIn(100, result[('system-id-1', 'physnet1')]) self.assertEqual(5000, result[('system-id-1', 'physnet1')][100]) def test_reconcile_subports_adds_missing_subports(self): """Test subport reconciliation adds missing subports.""" # Setup trunk with no subports trunk = FakeTrunk('trunk-id', 'anchor-port-id', sub_ports=[]) trunk_map = {('system-1', 'physnet1'): 'trunk-id'} # Mock get_trunk to return the trunk self.mock_neutron.network.get_trunk.return_value = trunk # Setup required VLANs with VNI mapping required_vlans = {('system-1', 'physnet1'): {100: 5000, 200: 5001}} # Mock anchor network cfg.CONF.set_override('l2vni_subport_anchor_network', 'anchor-network', group='l2vni') anchor_network = FakeNetwork('anchor-net-id', 'anchor-network') self.mock_neutron.network.networks.return_value = [anchor_network] # Mock port creation self.mock_neutron.network.create_port.return_value = FakePort( 'new-port-id', l2vni_trunk_manager.DEVICE_OWNER_L2VNI_SUBPORT ) # Mock local link connection discovery with mock.patch.object( self.manager, '_get_local_link_information', autospec=True, return_value={'switch_id': '00:11:22:33:44:55', 'port_id': 'Ethernet1'}): self.manager._reconcile_subports(trunk_map, required_vlans) # Should create 2 subports self.assertEqual(2, self.mock_neutron.network.create_port.call_count) # Verify binding_profile contains VNI for both subports calls = self.mock_neutron.network.create_port.call_args_list for call in calls: kwargs = call[1] self.assertIn('binding_profile', kwargs) binding_profile = kwargs['binding_profile'] self.assertIn('physical_network', binding_profile) self.assertEqual('physnet1', binding_profile['physical_network']) self.assertIn('vni', binding_profile) # VNI should be either 5000 or 5001 self.assertIn(binding_profile['vni'], [5000, 5001]) # Should add subports to trunk (one call per VLAN) add_subports = self.mock_neutron.network.add_trunk_subports self.assertEqual(2, add_subports.call_count) def test_reconcile_subports_removes_extra_subports(self): """Test subport reconciliation removes extra subports.""" # Setup trunk with extra subport existing_subport1 = {'port_id': 'subport-1', 'segmentation_id': 100, 'segmentation_type': 'vlan'} existing_subport2 = {'port_id': 'subport-2', 'segmentation_id': 200, 'segmentation_type': 'vlan'} trunk = FakeTrunk('trunk-id', 'anchor-port-id', sub_ports=[existing_subport1, existing_subport2]) trunk_map = {('system-1', 'physnet1'): 'trunk-id'} # Mock get_trunk to return the trunk self.mock_neutron.network.get_trunk.return_value = trunk # Only VLAN 100 is required, VLAN 200 should be removed required_vlans = {('system-1', 'physnet1'): {100: 5000}} cfg.CONF.set_override('l2vni_subport_anchor_network', 'anchor-network', group='l2vni') anchor_network = FakeNetwork('anchor-net-id', 'anchor-network') self.mock_neutron.network.networks.return_value = [anchor_network] self.manager._reconcile_subports(trunk_map, required_vlans) # Should remove subport-2 self.mock_neutron.network.delete_trunk_subports.assert_called_once() remove_args = ( self.mock_neutron.network.delete_trunk_subports.call_args[0][0]) self.assertEqual('trunk-id', remove_args) def test_reconcile_subports_without_vni(self): """Test subport creation without VNI for pure VLAN networks.""" # Setup trunk with no subports trunk = FakeTrunk('trunk-id', 'anchor-port-id', sub_ports=[]) trunk_map = {('system-1', 'physnet1'): 'trunk-id'} # Mock get_trunk to return the trunk self.mock_neutron.network.get_trunk.return_value = trunk # Setup required VLANs without VNI (None values) required_vlans = {('system-1', 'physnet1'): {100: None, 200: None}} # Mock anchor network cfg.CONF.set_override('l2vni_subport_anchor_network', 'anchor-network', group='l2vni') anchor_network = FakeNetwork('anchor-net-id', 'anchor-network') self.mock_neutron.network.networks.return_value = [anchor_network] # Mock port creation self.mock_neutron.network.create_port.return_value = FakePort( 'new-port-id', l2vni_trunk_manager.DEVICE_OWNER_L2VNI_SUBPORT ) # Mock local link connection discovery with mock.patch.object( self.manager, '_get_local_link_information', autospec=True, return_value={'switch_id': '00:11:22:33:44:55', 'port_id': 'Ethernet1'}): self.manager._reconcile_subports(trunk_map, required_vlans) # Should create 2 subports self.assertEqual(2, self.mock_neutron.network.create_port.call_count) # Verify binding_profile does NOT contain VNI for pure VLAN networks calls = self.mock_neutron.network.create_port.call_args_list for call in calls: kwargs = call[1] self.assertIn('binding_profile', kwargs) binding_profile = kwargs['binding_profile'] self.assertIn('physical_network', binding_profile) self.assertEqual('physnet1', binding_profile['physical_network']) # VNI should NOT be in binding_profile when it's None self.assertNotIn('vni', binding_profile) def test_get_local_link_from_ovn_lldp_success(self): """Test local_link_information retrieval from OVN LLDP data.""" # Mock chassis with bridge mappings chassis = FakeChassis('chassis-1', 'system-id-1') chassis.other_config['ovn-bridge-mappings'] = 'physnet1:br-physnet1' self.mock_ovn_sb.tables['Chassis'].rows.values\ .return_value = [chassis] # Mock port with LLDP data on the correct bridge port = mock.Mock() port.chassis = chassis port.external_ids = { 'lldp_chassis_id': '00:11:22:33:44:55', 'lldp_port_id': 'Ethernet1/1', 'lldp_system_name': 'switch.example.com' } # Mock interface on the bridge iface = mock.Mock() iface.name = 'br-physnet1' port.interfaces = [iface] self.mock_ovn_sb.tables['Port'].rows.values\ .return_value = [port] result = self.manager._get_lldp_from_ovn( 'system-id-1', 'physnet1') self.assertIsNotNone(result) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual('00:11:22:33:44:55', result[0]['switch_id']) self.assertEqual('Ethernet1/1', result[0]['port_id']) self.assertEqual('switch.example.com', result[0]['switch_info']) def test_get_local_link_from_ovn_lldp_multiple_ports_lag(self): """Test OVN LLDP aggregates multiple ports for LAG/bonding.""" # Mock chassis with bridge mappings chassis = FakeChassis('chassis-1', 'system-id-1') chassis.other_config['ovn-bridge-mappings'] = 'physnet1:br-physnet1' self.mock_ovn_sb.tables['Chassis'].rows.values\ .return_value = [chassis] # Mock two ports with LLDP data on the same bridge (LAG scenario) port1 = mock.Mock() port1.chassis = chassis port1.external_ids = { 'lldp_chassis_id': '00:11:22:33:44:55', 'lldp_port_id': 'Ethernet1/3', 'lldp_system_name': 'switch.example.com' } iface1 = mock.Mock() iface1.name = 'br-physnet1' port1.interfaces = [iface1] port2 = mock.Mock() port2.chassis = chassis port2.external_ids = { 'lldp_chassis_id': '00:11:22:33:44:55', 'lldp_port_id': 'Ethernet1/5', 'lldp_system_name': 'switch.example.com' } iface2 = mock.Mock() iface2.name = 'br-physnet1' port2.interfaces = [iface2] self.mock_ovn_sb.tables['Port'].rows.values\ .return_value = [port1, port2] result = self.manager._get_lldp_from_ovn( 'system-id-1', 'physnet1') self.assertIsNotNone(result) self.assertIsInstance(result, list) self.assertEqual(2, len(result)) self.assertEqual('00:11:22:33:44:55', result[0]['switch_id']) self.assertEqual('Ethernet1/3', result[0]['port_id']) self.assertEqual('00:11:22:33:44:55', result[1]['switch_id']) self.assertEqual('Ethernet1/5', result[1]['port_id']) def test_get_local_link_from_ovn_lldp_chassis_not_found(self): """Test OVN LLDP when chassis not found.""" # Mock empty chassis list self.mock_ovn_sb.tables['Chassis'].rows.values\ .return_value = [] result = self.manager._get_lldp_from_ovn( 'nonexistent-system-id', 'physnet1') self.assertIsNone(result) def test_get_local_link_from_ovn_lldp_filters_wrong_bridge(self): """Test OVN LLDP filters out ports on different bridges.""" # Mock chassis with multiple bridge mappings chassis = FakeChassis('chassis-1', 'system-id-1') chassis.other_config['ovn-bridge-mappings'] = ( 'physnet1:br-physnet1,physnet2:br-physnet2') self.mock_ovn_sb.tables['Chassis'].rows.values\ .return_value = [chassis] # Mock ports on different bridges port1 = mock.Mock() port1.chassis = chassis port1.external_ids = { 'lldp_chassis_id': '00:11:22:33:44:55', 'lldp_port_id': 'Ethernet1/1', 'lldp_system_name': 'switch1.example.com' } iface1 = mock.Mock() iface1.name = 'br-physnet1' port1.interfaces = [iface1] port2 = mock.Mock() port2.chassis = chassis port2.external_ids = { 'lldp_chassis_id': 'aa:bb:cc:dd:ee:ff', 'lldp_port_id': 'Ethernet2/1', 'lldp_system_name': 'switch2.example.com' } iface2 = mock.Mock() iface2.name = 'br-physnet2' port2.interfaces = [iface2] self.mock_ovn_sb.tables['Port'].rows.values\ .return_value = [port1, port2] # Query for physnet1 should only return port1 result = self.manager._get_lldp_from_ovn( 'system-id-1', 'physnet1') self.assertIsNotNone(result) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual('00:11:22:33:44:55', result[0]['switch_id']) self.assertEqual('Ethernet1/1', result[0]['port_id']) self.assertEqual('switch1.example.com', result[0]['switch_info']) def test_get_local_link_from_ironic_multiple_ports_lag(self): """Test Ironic aggregates multiple ports for LAG/bonding.""" # Mock two Ironic ports with same physnet (LAG scenario) local_link_conn1 = { 'switch_id': 'aa:bb:cc:dd:ee:ff', 'port_id': 'GigabitEthernet1/0/1', 'switch_info': 'ironic-switch' } local_link_conn2 = { 'switch_id': 'aa:bb:cc:dd:ee:ff', 'port_id': 'GigabitEthernet1/0/2', 'switch_info': 'ironic-switch' } ironic_port1 = FakeIronicPort( 'node-id-1', 'physnet1', local_link_conn1) ironic_port2 = FakeIronicPort( 'node-id-1', 'physnet1', local_link_conn2) ironic_node = FakeIronicNode('node-id-1', 'system-id-1') self.mock_ironic.nodes.return_value = [ironic_node] self.mock_ironic.ports.return_value = [ironic_port1, ironic_port2] result = self.manager._get_local_link_from_ironic( 'system-id-1', 'physnet1') self.assertIsNotNone(result) self.assertIsInstance(result, list) self.assertEqual(2, len(result)) self.assertEqual('aa:bb:cc:dd:ee:ff', result[0]['switch_id']) self.assertEqual('GigabitEthernet1/0/1', result[0]['port_id']) self.assertEqual('aa:bb:cc:dd:ee:ff', result[1]['switch_id']) self.assertEqual('GigabitEthernet1/0/2', result[1]['port_id']) def test_get_local_link_from_ironic_success(self): """Test local_link_information retrieval from Ironic.""" # Mock Ironic port and node local_link_conn = { 'switch_id': 'aa:bb:cc:dd:ee:ff', 'port_id': 'GigabitEthernet1/0/1', 'switch_info': 'ironic-switch' } ironic_port = FakeIronicPort( 'node-id-1', 'physnet1', local_link_conn) ironic_node = FakeIronicNode('node-id-1', 'system-id-1') # Mock the new efficient query pattern self.mock_ironic.nodes.return_value = [ironic_node] self.mock_ironic.ports.return_value = [ironic_port] result = self.manager._get_local_link_from_ironic( 'system-id-1', 'physnet1') self.assertIsNotNone(result) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual('aa:bb:cc:dd:ee:ff', result[0]['switch_id']) self.assertEqual('GigabitEthernet1/0/1', result[0]['port_id']) # Verify efficient querying - nodes() called with fields filter self.mock_ironic.nodes.assert_called_once_with( fields=['uuid', 'properties']) # Verify ports() called with node_uuid and fields filter self.mock_ironic.ports.assert_called_once_with( node_uuid='node-id-1', fields=['physical_network', 'local_link_connection']) def test_aggregate_ironic_ports_for_physnet_single_port(self): """Test port aggregation helper with single port.""" cache_entry = { 'cached_at': 1234567890.0, 'node_uuid': 'node-1', 'ports': [ { 'physnet': 'physnet1', 'local_link': { 'switch_id': 'aa:bb:cc:dd:ee:ff', 'port_id': 'Ethernet1/1' } } ] } result = self.manager._aggregate_ironic_ports_for_physnet( cache_entry, 'physnet1', 'system-1', 'test-source') self.assertIsNotNone(result) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual('aa:bb:cc:dd:ee:ff', result[0]['switch_id']) def test_aggregate_ironic_ports_for_physnet_multiple_ports(self): """Test port aggregation helper with multiple ports (LAG).""" cache_entry = { 'cached_at': 1234567890.0, 'node_uuid': 'node-1', 'ports': [ { 'physnet': 'physnet1', 'local_link': { 'switch_id': 'aa:bb:cc:dd:ee:ff', 'port_id': 'Ethernet1/1' } }, { 'physnet': 'physnet1', 'local_link': { 'switch_id': 'aa:bb:cc:dd:ee:ff', 'port_id': 'Ethernet1/2' } }, { 'physnet': 'physnet2', 'local_link': { 'switch_id': 'aa:bb:cc:dd:ee:ff', 'port_id': 'Ethernet2/1' } } ] } result = self.manager._aggregate_ironic_ports_for_physnet( cache_entry, 'physnet1', 'system-1', 'test-source') self.assertIsNotNone(result) self.assertIsInstance(result, list) self.assertEqual(2, len(result)) self.assertEqual('Ethernet1/1', result[0]['port_id']) self.assertEqual('Ethernet1/2', result[1]['port_id']) def test_aggregate_ironic_ports_for_physnet_no_match(self): """Test port aggregation helper with no matching physnet.""" cache_entry = { 'cached_at': 1234567890.0, 'node_uuid': 'node-1', 'ports': [ { 'physnet': 'physnet2', 'local_link': { 'switch_id': 'aa:bb:cc:dd:ee:ff', 'port_id': 'Ethernet1/1' } } ] } result = self.manager._aggregate_ironic_ports_for_physnet( cache_entry, 'physnet1', 'system-1', 'test-source') self.assertIsNone(result) def test_get_local_link_from_ironic_node_not_found(self): """Test Ironic fallback when node not found.""" # Mock empty nodes list - no node with matching system_id self.mock_ironic.nodes.return_value = [] result = self.manager._get_local_link_from_ironic( 'nonexistent-system-id', 'physnet1') self.assertIsNone(result) # Should not call ports() if no node found self.mock_ironic.ports.assert_not_called() def test_get_local_link_from_ironic_uses_cache(self): """Test that Ironic data is cached per system_id.""" cfg.CONF.set_override('ironic_cache_ttl', 3600, group='l2vni') local_link_conn = { 'switch_id': 'aa:bb:cc:dd:ee:ff', 'port_id': 'GigabitEthernet1/0/1', } ironic_port = FakeIronicPort( 'node-id-1', 'physnet1', local_link_conn) ironic_node = FakeIronicNode('node-id-1', 'system-id-1') self.mock_ironic.nodes.return_value = [ironic_node] self.mock_ironic.ports.return_value = [ironic_port] # First call - should query Ironic result1 = self.manager._get_local_link_from_ironic( 'system-id-1', 'physnet1') self.assertIsNotNone(result1) self.assertIsInstance(result1, list) self.assertEqual(1, self.mock_ironic.nodes.call_count) self.assertEqual(1, self.mock_ironic.ports.call_count) # Second call - should use cache result2 = self.manager._get_local_link_from_ironic( 'system-id-1', 'physnet1') self.assertIsNotNone(result2) self.assertEqual(result1, result2) # Still only 1 call - cache was used self.assertEqual(1, self.mock_ironic.nodes.call_count) self.assertEqual(1, self.mock_ironic.ports.call_count) def test_get_local_link_from_ironic_cache_expires(self): """Test that Ironic cache expires after TTL.""" import time # Manually inject an expired cache entry to test expiration # (we can't set TTL < 300 due to config validation) local_link_conn = { 'switch_id': 'aa:bb:cc:dd:ee:ff', 'port_id': 'GigabitEthernet1/0/1', } ironic_port = FakeIronicPort( 'node-id-1', 'physnet1', local_link_conn) ironic_node = FakeIronicNode('node-id-1', 'system-id-1') self.mock_ironic.nodes.return_value = [ironic_node] self.mock_ironic.ports.return_value = [ironic_port] # Manually create an expired cache entry (timestamped in the past) self.manager._ironic_cache['system-id-1'] = { 'cached_at': time.time() - 4000, # Expired (> 3600s default) 'node_uuid': 'node-id-1', 'ports': [{'physnet': 'physnet1', 'local_link': local_link_conn}] } # Call should detect expired cache and refresh result = self.manager._get_local_link_from_ironic( 'system-id-1', 'physnet1') self.assertIsNotNone(result) # Should have queried Ironic to refresh expired cache self.assertEqual(1, self.mock_ironic.nodes.call_count) self.assertEqual(1, self.mock_ironic.ports.call_count) def test_get_local_link_from_ironic_with_conductor_group_filter(self): """Test Ironic query uses conductor_group filter when configured.""" cfg.CONF.set_override('ironic_conductor_group', 'group1', group='l2vni') ironic_node = FakeIronicNode('node-id-1', 'system-id-1') self.mock_ironic.nodes.return_value = [ironic_node] self.mock_ironic.ports.return_value = [] self.manager._get_local_link_from_ironic('system-id-1', 'physnet1') # Verify conductor_group filter was passed self.mock_ironic.nodes.assert_called_once_with( fields=['uuid', 'properties'], conductor_group='group1') def test_get_local_link_from_ironic_with_shard_filter(self): """Test Ironic query uses shard filter when configured.""" cfg.CONF.set_override('ironic_shard', 'shard1', group='l2vni') ironic_node = FakeIronicNode('node-id-1', 'system-id-1') self.mock_ironic.nodes.return_value = [ironic_node] self.mock_ironic.ports.return_value = [] self.manager._get_local_link_from_ironic('system-id-1', 'physnet1') # Verify shard filter was passed self.mock_ironic.nodes.assert_called_once_with( fields=['uuid', 'properties'], shard='shard1') def test_get_local_link_information_tiered_fallback(self): """Test tiered local_link_information discovery.""" # Setup mocks for tiered fallback with mock.patch.object( self.manager, '_get_lldp_from_ovn', autospec=True, return_value=None), \ mock.patch.object( self.manager, '_get_local_link_from_ironic', autospec=True, return_value=[{'switch_id': 'from-ironic', 'port_id': 'port1'}]), \ mock.patch.object( self.manager, '_get_local_link_from_config', autospec=True, return_value=[{'switch_id': 'from-config', 'port_id': 'port2'}]): # OVN returns None, should fall back to Ironic result = self.manager._get_local_link_information( 'system-1', 'physnet1') self.assertIsInstance(result, list) self.assertEqual('from-ironic', result[0]['switch_id']) @mock.patch('builtins.open', new_callable=mock.mock_open, read_data=''' network_nodes: - system_id: system-1 trunks: - physical_network: physnet1 local_link_information: switch_id: "11:22:33:44:55:66" port_id: "Ethernet1" switch_info: "config-switch" ''') def test_get_local_link_from_config_success(self, mock_file): """Test local_link_information retrieval from YAML config.""" cfg.CONF.set_override('l2vni_network_nodes_config', '/etc/neutron/l2vni_network_nodes.yaml', group='l2vni') result = self.manager._get_local_link_from_config( 'system-1', 'physnet1') self.assertIsNotNone(result) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual('11:22:33:44:55:66', result[0]['switch_id']) self.assertEqual('Ethernet1', result[0]['port_id']) @mock.patch('builtins.open', new_callable=mock.mock_open, read_data=''' network_nodes: - system_id: system-1 trunks: - physical_network: physnet1 local_link_information: - switch_id: "22:57:f8:dd:03:01" port_id: "Ethernet1/3" switch_info: "leaf01.netlab.example.com" - switch_id: "22:57:f8:dd:03:01" port_id: "Ethernet1/5" switch_info: "leaf01.netlab.example.com" ''') def test_get_local_link_from_config_list_format_lag(self, mock_file): """Test YAML config with list of links for LAG/bonding.""" cfg.CONF.set_override('l2vni_network_nodes_config', '/etc/neutron/l2vni_network_nodes.yaml', group='l2vni') result = self.manager._get_local_link_from_config( 'system-1', 'physnet1') self.assertIsNotNone(result) self.assertIsInstance(result, list) self.assertEqual(2, len(result)) self.assertEqual('22:57:f8:dd:03:01', result[0]['switch_id']) self.assertEqual('Ethernet1/3', result[0]['port_id']) self.assertEqual('22:57:f8:dd:03:01', result[1]['switch_id']) self.assertEqual('Ethernet1/5', result[1]['port_id']) @mock.patch('builtins.open', new_callable=mock.mock_open, read_data=''' network_nodes: - hostname: test-hostname trunks: - physical_network: physnet1 local_link_information: switch_id: "aa:bb:cc:dd:ee:ff" port_id: "Ethernet2" switch_info: "hostname-switch" ''') def test_get_local_link_from_config_hostname_fallback(self, mock_file): """Test local_link_information retrieval using hostname fallback.""" cfg.CONF.set_override('l2vni_network_nodes_config', '/etc/neutron/l2vni_network_nodes.yaml', group='l2vni') # Mock chassis with hostname chassis = FakeChassis('chassis-1', 'system-uuid-123') chassis.hostname = 'test-hostname' self.mock_ovn_sb.tables['Chassis'].rows.values.return_value = [chassis] result = self.manager._get_local_link_from_config( 'system-uuid-123', 'physnet1') self.assertIsNotNone(result) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual('aa:bb:cc:dd:ee:ff', result[0]['switch_id']) self.assertEqual('Ethernet2', result[0]['port_id']) @mock.patch('builtins.open', new_callable=mock.mock_open, read_data=''' network_nodes: - system_id: system-1 trunks: - physical_network: physnet1 local_link_connection: switch_id: "11:22:33:44:55:66" port_id: "Ethernet1" ''') def test_get_local_link_from_config_deprecated_name_warning( self, mock_file): """Test deprecation warning for old local_link_connection name.""" cfg.CONF.set_override('l2vni_network_nodes_config', '/etc/neutron/l2vni_network_nodes.yaml', group='l2vni') with self.assertLogs( 'networking_baremetal.agent.l2vni_trunk_manager', level='WARNING') as log: result = self.manager._get_local_link_from_config( 'system-1', 'physnet1') self.assertIsNotNone(result) self.assertIn("deprecated 'local_link_connection'", log.output[0]) self.assertIn("'local_link_information'", log.output[0]) def test_cleanup_orphaned_trunks_removes_deleted_chassis(self): """Test cleanup removes trunks for deleted chassis.""" # Mock existing trunk for chassis that no longer exists trunk = FakeTrunk('orphan-trunk-id', 'orphan-port-id', name='l2vni-trunk-deleted-system-physnet1') self.mock_neutron.network.trunks.return_value = [trunk] # No valid chassis/physnet combinations valid_chassis_physnets = set() self.manager._cleanup_orphaned_trunks(valid_chassis_physnets) # Should delete trunk and port self.mock_neutron.network.delete_trunk.assert_called_once_with( 'orphan-trunk-id') self.mock_neutron.network.delete_port.assert_called_once_with( 'orphan-port-id') def test_cleanup_orphaned_networks_removes_unused_ha_networks(self): """Test cleanup removes ha_chassis_group networks with no groups.""" # Mock network with l2vni-ha prefix orphan_network = FakeNetwork('orphan-net-id', 'l2vni-ha-group-deleted_group') self.mock_neutron.network.networks.return_value = [orphan_network] # No HA chassis groups exist self.mock_ovn_nb.tables['HA_Chassis_Group'].rows.values\ .return_value = [] # Mock no L2VNI ports on network self.mock_neutron.network.ports.return_value = [] self.manager._cleanup_orphaned_networks() # Should delete network self.mock_neutron.network.delete_network.assert_called_once_with( 'orphan-net-id') def test_cleanup_orphaned_networks_skips_networks_with_ports(self): """Test cleanup skips networks that have active L2VNI ports.""" # Mock network with L2VNI anchor port network = FakeNetwork('net-id', 'l2vni-ha-group-group1') port = FakePort('port-id', l2vni_trunk_manager.DEVICE_OWNER_L2VNI_ANCHOR) self.mock_neutron.network.networks.return_value = [network] self.mock_neutron.network.ports.return_value = [port] # No HA chassis groups self.mock_ovn_nb.tables['HA_Chassis_Group'].rows.values\ .return_value = [] self.manager._cleanup_orphaned_networks() # Should not delete network with L2VNI ports self.mock_neutron.network.delete_network.assert_not_called() class TestL2VNITrunkManagerEdgeCases(tests_base.BaseTestCase): """Test edge cases and error handling.""" def setUp(self): super(TestL2VNITrunkManagerEdgeCases, self).setUp() # Register L2VNI config options agent_config.register_l2vni_opts(cfg.CONF) self.mock_neutron = mock.Mock() self.mock_ovn_nb = mock.Mock() self.mock_ovn_sb = mock.Mock() self.mock_ironic = mock.Mock() # Setup OVN tables structure # Tables now use .rows.values() pattern self.mock_ovn_nb.tables = { 'HA_Chassis_Group': mock.Mock( rows=mock.Mock(values=mock.Mock(return_value=[]))), 'Logical_Router_Port': mock.Mock( rows=mock.Mock(values=mock.Mock(return_value=[]))), 'Logical_Switch_Port': mock.Mock( rows=mock.Mock(values=mock.Mock(return_value=[]))), 'Logical_Switch': mock.Mock( rows=mock.Mock(values=mock.Mock(return_value=[]))), } self.mock_ovn_sb.tables = { 'Chassis': mock.Mock( rows=mock.Mock(values=mock.Mock(return_value=[]))), 'Port': mock.Mock( rows=mock.Mock(values=mock.Mock(return_value=[]))), } # Note: member_manager is None, so _should_manage_chassis returns True # for all chassis (single agent mode) self.manager = l2vni_trunk_manager.L2VNITrunkManager( neutron_client=self.mock_neutron, ovn_nb_idl=self.mock_ovn_nb, ovn_sb_idl=self.mock_ovn_sb, ironic_client=self.mock_ironic, member_manager=None, agent_id=None ) def test_reconcile_handles_exception_gracefully(self): """Test reconciliation handles exceptions without crashing.""" with mock.patch.object( self.manager, '_ensure_infrastructure_networks', autospec=True, side_effect=Exception('Test error')): # Should log exception but not raise try: self.manager.reconcile() except Exception: self.fail('reconcile() raised exception unexpectedly') def test_create_trunk_handles_neutron_error(self): """Test trunk creation handles Neutron API errors.""" # Mock trunk doesn't exist self.mock_neutron.network.trunks.return_value = [] # Mock anchor port creation succeeds but trunk creation fails self.mock_neutron.network.ports.return_value = [] self.mock_neutron.network.create_port.return_value = mock.Mock( id='anchor-port-id') self.mock_neutron.network.create_trunk.side_effect = ( sdkexc.SDKException('Neutron error')) # Mock ha_group network lookup and local_link_information with mock.patch.object( self.manager, '_find_ha_group_network_for_chassis', autospec=True, return_value='network-id'), \ mock.patch.object( self.manager, '_get_local_link_information', autospec=True, return_value=None): result = self.manager._find_or_create_trunk( 'system-1', 'physnet1') self.assertIsNone(result) def test_get_local_link_from_config_file_not_found(self): """Test config file fallback when file doesn't exist.""" cfg.CONF.set_override('l2vni_network_nodes_config', '/nonexistent/path/config.yaml', group='l2vni') result = self.manager._get_local_link_from_config( 'system-1', 'physnet1') self.assertIsNone(result) def test_calculate_required_vlans_handles_missing_segment(self): """Test VLAN calculation handles missing segment data.""" # Setup minimal OVN data FakeChassis('chassis-1', 'system-1', other_config={'ovn-bridge-mappings': 'physnet1:br-ex'}) ha_chassis = FakeHAChassis('system-1') ha_group = FakeHAChassisGroup('group1', [ha_chassis]) self.mock_ovn_nb.tables['HA_Chassis_Group'].rows.values\ .return_value = [ha_group] # Mock empty segments self.mock_neutron.network.segments.return_value = [] result = self.manager._calculate_required_vlans() # Should handle gracefully and return empty or minimal result self.assertIsInstance(result, dict) def test_anchor_port_creation_includes_local_link_information(self): """Test anchor port creation includes local_link_information.""" system_id = 'system-1' physnet = 'physnet1' # Mock no existing port self.mock_neutron.network.ports.return_value = [] # Mock ha_group network ha_network = FakeNetwork('ha-net-id', 'l2vni-ha-group-group1') self.mock_neutron.network.networks.return_value = [ha_network] # Setup OVN data for ha_group lookup chassis = FakeChassis('chassis-1', system_id, hostname='host1') ha_chassis = FakeHAChassis(system_id) ha_group = FakeHAChassisGroup('group1', [ha_chassis]) self.mock_ovn_nb.tables['HA_Chassis_Group'].rows.values\ .return_value = [ha_group] self.mock_ovn_sb.tables['Chassis'].rows.values\ .return_value = [chassis] # Mock local_link_information discovery local_link = { 'switch_id': '00:11:22:33:44:55', 'port_id': 'Ethernet1/5', 'switch_info': 'switch1' } with mock.patch.object( self.manager, '_get_local_link_information', autospec=True, return_value=[local_link]): # Mock port creation created_port = FakePort( 'anchor-port-id', l2vni_trunk_manager.DEVICE_OWNER_L2VNI_ANCHOR) self.mock_neutron.network.create_port.return_value = created_port result = self.manager._find_or_create_anchor_port( system_id, physnet) # Should create port with local_link_information in binding profile self.assertEqual('anchor-port-id', result) self.mock_neutron.network.create_port.assert_called_once() call_kwargs = self.mock_neutron.network.create_port.call_args[1] self.assertIn('binding_profile', call_kwargs) self.assertIn('local_link_information', call_kwargs['binding_profile']) self.assertEqual( [local_link], call_kwargs['binding_profile']['local_link_information']) def test_anchor_port_creation_with_multiple_links_lag(self): """Test anchor port creation with multiple links for LAG/bonding.""" system_id = 'system-1' physnet = 'physnet1' # Mock no existing port self.mock_neutron.network.ports.return_value = [] # Mock ha_group network ha_network = FakeNetwork('ha-net-id', 'l2vni-ha-group-group1') self.mock_neutron.network.networks.return_value = [ha_network] # Setup OVN data for ha_group lookup chassis = FakeChassis('chassis-1', system_id, hostname='host1') ha_chassis = FakeHAChassis(system_id) ha_group = FakeHAChassisGroup('group1', [ha_chassis]) self.mock_ovn_nb.tables['HA_Chassis_Group'].rows.values\ .return_value = [ha_group] self.mock_ovn_sb.tables['Chassis'].rows.values\ .return_value = [chassis] # Mock local_link_information discovery with multiple links local_links = [ { 'switch_id': '00:11:22:33:44:55', 'port_id': 'Ethernet1/3', 'switch_info': 'switch1' }, { 'switch_id': '00:11:22:33:44:55', 'port_id': 'Ethernet1/5', 'switch_info': 'switch1' } ] with mock.patch.object( self.manager, '_get_local_link_information', autospec=True, return_value=local_links): # Mock port creation created_port = FakePort( 'anchor-port-id', l2vni_trunk_manager.DEVICE_OWNER_L2VNI_ANCHOR) self.mock_neutron.network.create_port.return_value = created_port result = self.manager._find_or_create_anchor_port( system_id, physnet) # Should create port with multiple links in binding profile self.assertEqual('anchor-port-id', result) self.mock_neutron.network.create_port.assert_called_once() call_kwargs = self.mock_neutron.network.create_port.call_args[1] self.assertIn('binding_profile', call_kwargs) self.assertIn('local_link_information', call_kwargs['binding_profile']) self.assertEqual( local_links, call_kwargs['binding_profile']['local_link_information']) def test_anchor_port_creation_without_local_link_information(self): """Test anchor port creation when local_link_information is N/A.""" system_id = 'system-1' physnet = 'physnet1' # Mock no existing port self.mock_neutron.network.ports.return_value = [] # Mock ha_group network ha_network = FakeNetwork('ha-net-id', 'l2vni-ha-group-group1') self.mock_neutron.network.networks.return_value = [ha_network] # Setup OVN data chassis = FakeChassis('chassis-1', system_id, hostname='host1') ha_chassis = FakeHAChassis(system_id) ha_group = FakeHAChassisGroup('group1', [ha_chassis]) self.mock_ovn_nb.tables['HA_Chassis_Group'].rows.values\ .return_value = [ha_group] self.mock_ovn_sb.tables['Chassis'].rows.values\ .return_value = [chassis] # Mock local_link_information discovery returns None with mock.patch.object( self.manager, '_get_local_link_information', autospec=True, return_value=None): # Mock port creation created_port = FakePort( 'anchor-port-id', l2vni_trunk_manager.DEVICE_OWNER_L2VNI_ANCHOR) self.mock_neutron.network.create_port.return_value = created_port result = self.manager._find_or_create_anchor_port( system_id, physnet) # Should still create port, but without local_link_information self.assertEqual('anchor-port-id', result) self.mock_neutron.network.create_port.assert_called_once() call_kwargs = self.mock_neutron.network.create_port.call_args[1] self.assertIn('binding_profile', call_kwargs) self.assertNotIn('local_link_information', call_kwargs['binding_profile']) def test_anchor_port_reconciliation_adds_missing_local_link(self): """Test reconciliation updates anchor port missing LLC. Updates existing anchor ports that are missing local_link_information in their binding profile. """ system_id = 'system-1' physnet = 'physnet1' # Mock existing port WITHOUT local_link_information existing_port = FakePort('anchor-port-id', l2vni_trunk_manager.DEVICE_OWNER_L2VNI_ANCHOR, binding_profile={'system_id': system_id, 'physical_network': physnet}) self.mock_neutron.network.ports.return_value = [existing_port] # Mock local_link_information discovery local_link = { 'switch_id': '00:11:22:33:44:55', 'port_id': 'Ethernet1/5', 'switch_info': 'switch1' } with mock.patch.object( self.manager, '_get_local_link_information', autospec=True, return_value=[local_link]): result = self.manager._find_or_create_anchor_port( system_id, physnet) # Should return existing port and update it self.assertEqual('anchor-port-id', result) self.mock_neutron.network.update_port.assert_called_once() call_args = self.mock_neutron.network.update_port.call_args self.assertEqual('anchor-port-id', call_args[0][0]) updated_profile = call_args[1]['binding_profile'] self.assertIn('local_link_information', updated_profile) self.assertEqual([local_link], updated_profile['local_link_information']) def test_anchor_port_reconciliation_skips_correct_ports(self): """Test reconciliation skips correctly configured anchor ports. Verifies that anchor ports with local_link_information already set are not updated. """ system_id = 'system-1' physnet = 'physnet1' local_link = { 'switch_id': '00:11:22:33:44:55', 'port_id': 'Ethernet1/5', 'switch_info': 'switch1' } # Mock existing port WITH local_link_information existing_port = FakePort('anchor-port-id', l2vni_trunk_manager.DEVICE_OWNER_L2VNI_ANCHOR, binding_profile={ 'system_id': system_id, 'physical_network': physnet, 'local_link_information': [local_link] }) self.mock_neutron.network.ports.return_value = [existing_port] result = self.manager._find_or_create_anchor_port(system_id, physnet) # Should return existing port without updating self.assertEqual('anchor-port-id', result) self.mock_neutron.network.update_port.assert_not_called() def test_existing_trunk_reconciles_anchor_port(self): """Test that existing trunks still reconcile their anchor ports. Verifies that when a trunk already exists, _find_or_create_trunk() still calls _find_or_create_anchor_port() to reconcile the anchor port's binding profile. This ensures existing trunks created before the local_link_information fix get updated. """ system_id = 'system-1' physnet = 'physnet1' # Mock existing anchor port WITHOUT local_link_information existing_anchor_port = FakePort( 'anchor-port-id', l2vni_trunk_manager.DEVICE_OWNER_L2VNI_ANCHOR, binding_profile={ 'system_id': system_id, 'physical_network': physnet }) # Mock existing trunk existing_trunk = FakeTrunk( 'trunk-id', 'anchor-port-id', name='l2vni-trunk-system-1-physnet1') self.mock_neutron.network.ports.return_value = [existing_anchor_port] self.mock_neutron.network.trunks.return_value = [existing_trunk] # Mock local_link_information discovery local_link = { 'switch_id': '00:11:22:33:44:55', 'port_id': 'Ethernet1/5', 'switch_info': 'switch1' } with mock.patch.object( self.manager, '_get_local_link_information', autospec=True, return_value=[local_link]): result = self.manager._find_or_create_trunk(system_id, physnet) # Should return existing trunk self.assertEqual('trunk-id', result) # Should have updated the anchor port with local_link_information self.mock_neutron.network.update_port.assert_called_once() call_args = self.mock_neutron.network.update_port.call_args self.assertEqual('anchor-port-id', call_args[0][0]) updated_profile = call_args[1]['binding_profile'] self.assertIn('local_link_information', updated_profile) self.assertEqual([local_link], updated_profile['local_link_information']) def test_subport_creation_sets_binding_host_id(self): """Test subport creation sets binding:host_id to chassis hostname.""" trunk_id = 'trunk-id' system_id = 'system-1' physnet = 'physnet1' vlan_id = 100 anchor_network_id = 'anchor-net-id' # Setup chassis with hostname chassis = FakeChassis('chassis-1', system_id, hostname='devstack') self.mock_ovn_sb.tables['Chassis'].rows.values\ .return_value = [chassis] # Mock port creation created_port = FakePort('subport-id', l2vni_trunk_manager.DEVICE_OWNER_L2VNI_SUBPORT) self.mock_neutron.network.create_port.return_value = created_port self.manager._add_subport(trunk_id, system_id, physnet, vlan_id, anchor_network_id) # Should create port and set binding:host_id self.mock_neutron.network.create_port.assert_called_once() self.mock_neutron.network.update_port.assert_called_once() # Check update_port was called with binding:host_id update_call = self.mock_neutron.network.update_port.call_args self.assertEqual('subport-id', update_call[0][0]) self.assertIn('binding:host_id', update_call[1]) self.assertEqual('devstack', update_call[1]['binding:host_id']) def test_subport_creation_without_hostname(self): """Test subport creation when hostname cannot be determined.""" trunk_id = 'trunk-id' system_id = 'system-1' physnet = 'physnet1' vlan_id = 100 anchor_network_id = 'anchor-net-id' # Mock empty chassis table (hostname lookup fails) self.mock_ovn_sb.tables['Chassis'].rows.values.return_value = [] # Mock port creation created_port = FakePort('subport-id', l2vni_trunk_manager.DEVICE_OWNER_L2VNI_SUBPORT) self.mock_neutron.network.create_port.return_value = created_port self.manager._add_subport(trunk_id, system_id, physnet, vlan_id, anchor_network_id) # Should create port but NOT call update_port (no hostname) self.mock_neutron.network.create_port.assert_called_once() # update_port should not be called since we have no hostname self.mock_neutron.network.update_port.assert_not_called() class TestL2VNITrunkManagerTargetedReconciliation(tests_base.BaseTestCase): """Tests for targeted single-VLAN reconciliation.""" def setUp(self): super().setUp() agent_config.register_agent_opts(CONF) CONF.set_override('l2vni_subport_anchor_network', 'l2vni-subports', group='l2vni') CONF.set_override('l2vni_auto_create_networks', True, group='l2vni') def _create_manager(self): """Create L2VNITrunkManager with mocked dependencies.""" neutron = mock.Mock() ovn_nb_idl = mock.Mock() ovn_sb_idl = mock.Mock() ironic = mock.Mock() ovn_nb_idl.tables = { 'HA_Chassis_Group': mock.Mock(rows=mock.Mock(values=mock.Mock( return_value=[]))), 'Logical_Switch_Port': mock.Mock(rows=mock.Mock(values=mock.Mock( return_value=[]))), 'Logical_Router_Port': mock.Mock(rows=mock.Mock(values=mock.Mock( return_value=[]))) } ovn_sb_idl.tables = { 'Chassis': mock.Mock(rows=mock.Mock(values=mock.Mock( return_value=[]))) } return l2vni_trunk_manager.L2VNITrunkManager( neutron, ovn_nb_idl, ovn_sb_idl, ironic) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_ensure_infrastructure_networks', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_get_subport_anchor_network_id', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_get_all_chassis_with_physnet', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_find_or_create_trunk', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_ensure_single_subport', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_get_vni_for_network', autospec=True) def test_reconcile_single_vlan_add_action( self, mock_get_vni, mock_ensure_subport, mock_find_trunk, mock_get_chassis, mock_get_anchor, mock_ensure_infra): """Test targeted reconciliation adds subport for single VLAN.""" manager = self._create_manager() mock_get_anchor.return_value = 'anchor-net-id' mock_get_chassis.return_value = {'chassis-1', 'chassis-2'} mock_get_vni.return_value = 5000 trunk_map = {} def find_trunk_side_effect(self, system_id, physnet): trunk_id = f'trunk-{system_id}' trunk_map[system_id] = trunk_id return trunk_id mock_find_trunk.side_effect = find_trunk_side_effect manager.reconcile_single_vlan('net-1', 'physnet1', 100, action='add') mock_ensure_infra.assert_called_once_with(manager) mock_get_anchor.assert_called_once_with(manager) mock_get_chassis.assert_called_once_with(manager, 'physnet1') mock_get_vni.assert_called_once_with(manager, 'net-1') self.assertEqual(2, mock_find_trunk.call_count) self.assertEqual(2, mock_ensure_subport.call_count) for system_id in ['chassis-1', 'chassis-2']: trunk_id = trunk_map[system_id] mock_ensure_subport.assert_any_call( manager, trunk_id, system_id, 'physnet1', 100, 'anchor-net-id', vni=5000) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_ensure_infrastructure_networks', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_get_subport_anchor_network_id', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_get_all_chassis_with_physnet', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_find_or_create_trunk', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_remove_single_subport', autospec=True) def test_reconcile_single_vlan_remove_action( self, mock_remove_subport, mock_find_trunk, mock_get_chassis, mock_get_anchor, mock_ensure_infra): """Test targeted reconciliation removes subport for single VLAN.""" manager = self._create_manager() mock_get_anchor.return_value = 'anchor-net-id' mock_get_chassis.return_value = {'chassis-1'} mock_find_trunk.return_value = 'trunk-1' manager.reconcile_single_vlan( 'net-1', 'physnet1', 200, action='remove') mock_ensure_infra.assert_called_once_with(manager) mock_get_anchor.assert_called_once_with(manager) mock_get_chassis.assert_called_once_with(manager, 'physnet1') mock_find_trunk.assert_called_once_with( manager, 'chassis-1', 'physnet1') mock_remove_subport.assert_called_once_with( manager, 'trunk-1', 'chassis-1', 'physnet1', 200) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_ensure_infrastructure_networks', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_get_subport_anchor_network_id', autospec=True) def test_reconcile_single_vlan_no_anchor_network( self, mock_get_anchor, mock_ensure_infra): """Test reconciliation exits early if anchor network missing.""" manager = self._create_manager() mock_get_anchor.return_value = None manager.reconcile_single_vlan('net-1', 'physnet1', 100, action='add') mock_ensure_infra.assert_called_once_with(manager) mock_get_anchor.assert_called_once_with(manager) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_ensure_infrastructure_networks', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_get_subport_anchor_network_id', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_get_all_chassis_with_physnet', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_get_vni_for_network', autospec=True) def test_reconcile_single_vlan_no_chassis_with_physnet( self, mock_get_vni, mock_get_chassis, mock_get_anchor, mock_ensure_infra): """Test reconciliation exits early if no chassis with physnet.""" manager = self._create_manager() mock_get_anchor.return_value = 'anchor-net-id' mock_get_chassis.return_value = set() mock_get_vni.return_value = 5000 manager.reconcile_single_vlan('net-1', 'physnet1', 100, action='add') mock_ensure_infra.assert_called_once_with(manager) mock_get_chassis.assert_called_once_with(manager, 'physnet1') mock_get_vni.assert_called_once_with(manager, 'net-1') @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_ensure_infrastructure_networks', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_get_subport_anchor_network_id', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_get_all_chassis_with_physnet', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_find_or_create_trunk', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_ensure_single_subport', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_get_vni_for_network', autospec=True) def test_reconcile_single_vlan_trunk_creation_fails( self, mock_get_vni, mock_ensure_subport, mock_find_trunk, mock_get_chassis, mock_get_anchor, mock_ensure_infra): """Test reconciliation continues if trunk creation fails.""" manager = self._create_manager() mock_get_anchor.return_value = 'anchor-net-id' mock_get_chassis.return_value = {'chassis-1', 'chassis-2'} mock_get_vni.return_value = 5000 def find_trunk_side_effect(self, system_id, physnet): if system_id == 'chassis-1': return None return f'trunk-{system_id}' mock_find_trunk.side_effect = find_trunk_side_effect manager.reconcile_single_vlan('net-1', 'physnet1', 100, action='add') self.assertEqual(2, mock_find_trunk.call_count) mock_ensure_subport.assert_called_once_with( manager, 'trunk-chassis-2', 'chassis-2', 'physnet1', 100, 'anchor-net-id', vni=5000) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_ensure_infrastructure_networks', autospec=True) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_get_subport_anchor_network_id', autospec=True) def test_reconcile_single_vlan_handles_sdk_exception( self, mock_get_anchor, mock_ensure_infra): """Test reconciliation handles SDK exceptions gracefully.""" manager = self._create_manager() mock_get_anchor.side_effect = sdkexc.SDKException("API error") manager.reconcile_single_vlan('net-1', 'physnet1', 100, action='add') mock_ensure_infra.assert_called_once_with(manager) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_add_subport', autospec=True) def test_ensure_single_subport_creates_when_missing(self, mock_add): """Test _ensure_single_subport creates subport if missing.""" manager = self._create_manager() trunk = mock.Mock() trunk.sub_ports = [ {'port_id': 'port-1', 'segmentation_id': 100}, {'port_id': 'port-2', 'segmentation_id': 200} ] manager.neutron.network.get_trunk.return_value = trunk manager._ensure_single_subport( 'trunk-1', 'chassis-1', 'physnet1', 300, 'anchor-net-id', vni=5000) mock_add.assert_called_once_with( manager, 'trunk-1', 'chassis-1', 'physnet1', 300, 'anchor-net-id', vni=5000) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_add_subport', autospec=True) def test_ensure_single_subport_skips_if_exists(self, mock_add): """Test _ensure_single_subport is idempotent.""" manager = self._create_manager() trunk = mock.Mock() trunk.sub_ports = [ {'port_id': 'port-1', 'segmentation_id': 100}, {'port_id': 'port-2', 'segmentation_id': 200} ] manager.neutron.network.get_trunk.return_value = trunk manager._ensure_single_subport( 'trunk-1', 'chassis-1', 'physnet1', 100, 'anchor-net-id') mock_add.assert_not_called() @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_remove_subport', autospec=True) def test_remove_single_subport_removes_when_exists(self, mock_remove): """Test _remove_single_subport removes existing subport.""" manager = self._create_manager() trunk = mock.Mock() trunk.sub_ports = [ {'port_id': 'port-1', 'segmentation_id': 100}, {'port_id': 'port-2', 'segmentation_id': 200} ] manager.neutron.network.get_trunk.return_value = trunk manager._remove_single_subport('trunk-1', 'chassis-1', 'physnet1', 100) mock_remove.assert_called_once_with( manager, 'trunk-1', 'port-1', 'chassis-1', 'physnet1', 100) @mock.patch.object(l2vni_trunk_manager.L2VNITrunkManager, '_remove_subport', autospec=True) def test_remove_single_subport_skips_if_not_exists(self, mock_remove): """Test _remove_single_subport is idempotent.""" manager = self._create_manager() trunk = mock.Mock() trunk.sub_ports = [ {'port_id': 'port-1', 'segmentation_id': 100} ] manager.neutron.network.get_trunk.return_value = trunk manager._remove_single_subport('trunk-1', 'chassis-1', 'physnet1', 200) mock_remove.assert_not_called() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/agent/test_ovn_client.py0000664000175000017500000004127715157004031027747 0ustar00zuulzuul# Copyright (c) 2026 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 neutron.tests import base as tests_base from oslo_config import cfg from networking_baremetal.agent import ovn_client class TestOVNClient(tests_base.BaseTestCase): """Test cases for OVN Client connections.""" def setUp(self): super(TestOVNClient, self).setUp() # Reset global IDL instances before each test ovn_client._OVN_NB_IDL = None ovn_client._OVN_SB_IDL = None # Register L2VNI config options (includes OVN connection settings) from networking_baremetal.agent import agent_config agent_config.register_l2vni_opts(cfg.CONF) # Register Neutron OVN config options for testing fallback behavior try: from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf ovn_conf.register_opts() except (ImportError, cfg.DuplicateOptError): # OVN config may not be available or already registered pass def tearDown(self): super(TestOVNClient, self).tearDown() # Clean up global IDL instances after each test ovn_client._OVN_NB_IDL = None ovn_client._OVN_SB_IDL = None def test_module_has_required_functions(self): """Test OVN client module has required functions.""" self.assertTrue(callable(ovn_client.get_ovn_nb_idl)) self.assertTrue(callable(ovn_client.get_ovn_sb_idl)) def test_module_has_global_idl_variables(self): """Test OVN client module has global IDL variables.""" # Check that module has the expected global variables self.assertIsNone(ovn_client._OVN_NB_IDL) self.assertIsNone(ovn_client._OVN_SB_IDL) @mock.patch.object(ovn_client, 'nb_impl_idl', autospec=True) @mock.patch.object(ovn_client, 'AgentOvnNbIdl', autospec=True) @mock.patch.object(ovn_client, 'connection', autospec=True) @mock.patch.object(ovn_client, 'idlutils', autospec=True) def test_get_ovn_nb_idl_creates_connection( self, mock_idlutils, mock_connection, mock_agent_idl, mock_nb_impl): """Test OVN Northbound IDL connection creation.""" mock_helper = mock.Mock() mock_idlutils.get_schema_helper = mock.Mock(return_value=mock_helper) mock_idl = mock.Mock() mock_agent_idl.return_value = mock_idl mock_conn = mock.Mock() mock_connection.Connection.return_value = mock_conn mock_api = mock.Mock() mock_nb_impl.OvnNbApiIdlImpl.return_value = mock_api result = ovn_client.get_ovn_nb_idl() # Should call connection setup functions mock_idlutils.get_schema_helper.assert_called_once_with( 'tcp:127.0.0.1:6641', 'OVN_Northbound') mock_helper.register_all.assert_called_once() mock_agent_idl.assert_called_once_with( 'tcp:127.0.0.1:6641', mock_helper) mock_conn.start.assert_called_once() # Should return NB API instance self.assertEqual(result, mock_api) @mock.patch.object(ovn_client, 'nb_impl_idl', autospec=True) @mock.patch.object(ovn_client, 'AgentOvnNbIdl', autospec=True) @mock.patch.object(ovn_client, 'connection', autospec=True) @mock.patch.object(ovn_client, 'idlutils', autospec=True) def test_get_ovn_nb_idl_returns_cached_instance( self, mock_idlutils, mock_connection, mock_agent_idl, mock_nb_impl): """Test OVN Northbound IDL returns cached instance on second call.""" mock_helper = mock.Mock() mock_idlutils.get_schema_helper = mock.Mock(return_value=mock_helper) mock_idl = mock.Mock() mock_agent_idl.return_value = mock_idl mock_conn = mock.Mock() mock_connection.Connection.return_value = mock_conn mock_api = mock.Mock() mock_nb_impl.OvnNbApiIdlImpl.return_value = mock_api # First call creates connection result1 = ovn_client.get_ovn_nb_idl() # Second call should return cached instance result2 = ovn_client.get_ovn_nb_idl() # Should only create connection once self.assertEqual(1, mock_idlutils.get_schema_helper.call_count) self.assertEqual(1, mock_agent_idl.call_count) # Both calls should return same instance self.assertEqual(result1, result2) @mock.patch.object(ovn_client, 'idlutils', autospec=True) def test_get_ovn_nb_idl_handles_connection_failure(self, mock_idlutils): """Test OVN Northbound IDL handles connection failures.""" mock_idlutils.get_schema_helper = mock.Mock( side_effect=RuntimeError('Connection failed')) self.assertRaises(RuntimeError, ovn_client.get_ovn_nb_idl) @mock.patch.object(ovn_client, 'sb_impl_idl', autospec=True) @mock.patch.object(ovn_client, 'AgentOvnSbIdl', autospec=True) @mock.patch.object(ovn_client, 'connection', autospec=True) @mock.patch.object(ovn_client, 'idlutils', autospec=True) def test_get_ovn_sb_idl_creates_connection( self, mock_idlutils, mock_connection, mock_agent_idl, mock_sb_impl): """Test OVN Southbound IDL connection creation.""" mock_helper = mock.Mock() mock_idlutils.get_schema_helper = mock.Mock(return_value=mock_helper) mock_idl = mock.Mock() mock_agent_idl.return_value = mock_idl mock_conn = mock.Mock() mock_connection.Connection.return_value = mock_conn mock_api = mock.Mock() mock_sb_impl.OvnSbApiIdlImpl.return_value = mock_api result = ovn_client.get_ovn_sb_idl() # Should call connection setup functions mock_idlutils.get_schema_helper.assert_called_once_with( 'tcp:127.0.0.1:6642', 'OVN_Southbound') mock_helper.register_all.assert_called_once() mock_agent_idl.assert_called_once_with( 'tcp:127.0.0.1:6642', mock_helper) mock_conn.start.assert_called_once() # Should return SB API instance self.assertEqual(result, mock_api) @mock.patch.object(ovn_client, 'sb_impl_idl', autospec=True) @mock.patch.object(ovn_client, 'AgentOvnSbIdl', autospec=True) @mock.patch.object(ovn_client, 'connection', autospec=True) @mock.patch.object(ovn_client, 'idlutils', autospec=True) def test_get_ovn_sb_idl_returns_cached_instance( self, mock_idlutils, mock_connection, mock_agent_idl, mock_sb_impl): """Test OVN Southbound IDL returns cached instance on second call.""" mock_helper = mock.Mock() mock_idlutils.get_schema_helper = mock.Mock(return_value=mock_helper) mock_idl = mock.Mock() mock_agent_idl.return_value = mock_idl mock_conn = mock.Mock() mock_connection.Connection.return_value = mock_conn mock_api = mock.Mock() mock_sb_impl.OvnSbApiIdlImpl.return_value = mock_api # First call creates connection result1 = ovn_client.get_ovn_sb_idl() # Second call should return cached instance result2 = ovn_client.get_ovn_sb_idl() # Should only create connection once self.assertEqual(1, mock_idlutils.get_schema_helper.call_count) self.assertEqual(1, mock_agent_idl.call_count) # Both calls should return same instance self.assertEqual(result1, result2) @mock.patch.object(ovn_client, 'idlutils', autospec=True) def test_get_ovn_sb_idl_handles_connection_failure(self, mock_idlutils): """Test OVN Southbound IDL handles connection failures.""" mock_idlutils.get_schema_helper = mock.Mock( side_effect=RuntimeError('Connection failed')) self.assertRaises(RuntimeError, ovn_client.get_ovn_sb_idl) @mock.patch.object(ovn_client, 'sb_impl_idl', autospec=True) @mock.patch.object(ovn_client, 'nb_impl_idl', autospec=True) @mock.patch.object(ovn_client, 'AgentOvnSbIdl', autospec=True) @mock.patch.object(ovn_client, 'AgentOvnNbIdl', autospec=True) @mock.patch.object(ovn_client, 'connection', autospec=True) @mock.patch.object(ovn_client, 'idlutils', autospec=True) def test_both_idls_independent( self, mock_idlutils, mock_connection, mock_agent_nb_idl, mock_agent_sb_idl, mock_nb_impl, mock_sb_impl): """Test NB and SB IDL connections are independent.""" mock_helper = mock.Mock() mock_idlutils.get_schema_helper = mock.Mock(return_value=mock_helper) # Create different mock IDL instances for NB and SB mock_idl_nb = mock.Mock() mock_idl_sb = mock.Mock() mock_agent_nb_idl.return_value = mock_idl_nb mock_agent_sb_idl.return_value = mock_idl_sb mock_conn_nb = mock.Mock() mock_conn_sb = mock.Mock() mock_connection.Connection.side_effect = [mock_conn_nb, mock_conn_sb] mock_api_nb = mock.Mock() mock_api_sb = mock.Mock() mock_nb_impl.OvnNbApiIdlImpl.return_value = mock_api_nb mock_sb_impl.OvnSbApiIdlImpl.return_value = mock_api_sb # Get both IDLs nb_idl = ovn_client.get_ovn_nb_idl() sb_idl = ovn_client.get_ovn_sb_idl() # Both should be created self.assertEqual(2, mock_idlutils.get_schema_helper.call_count) self.assertEqual(1, mock_agent_nb_idl.call_count) self.assertEqual(1, mock_agent_sb_idl.call_count) # Should be different instances self.assertIsNotNone(nb_idl) self.assertIsNotNone(sb_idl) self.assertNotEqual(nb_idl, sb_idl) def test_get_ovn_nb_connection_from_l2vni_config(self): """Test getting NB connection from [l2vni] section.""" cfg.CONF.set_override('ovn_nb_connection', ['ssl:10.0.0.1:6641'], group='l2vni') result = ovn_client._get_ovn_nb_connection() self.assertEqual('ssl:10.0.0.1:6641', result) def test_get_ovn_nb_connection_from_l2vni_list(self): """Test getting NB connection list from [l2vni] converts to string.""" cfg.CONF.set_override('ovn_nb_connection', ['ssl:10.0.0.1:6641', 'ssl:10.0.0.2:6641'], group='l2vni') result = ovn_client._get_ovn_nb_connection() self.assertEqual('ssl:10.0.0.1:6641,ssl:10.0.0.2:6641', result) def test_get_ovn_nb_connection_fallback_to_ovn_section(self): """Test NB connection falls back to [ovn] section.""" # Don't set l2vni config, should read from [ovn] cfg.CONF.set_override('ovn_nb_connection', ['ssl:192.168.1.1:6641'], group='ovn') result = ovn_client._get_ovn_nb_connection() self.assertEqual('ssl:192.168.1.1:6641', result) def test_get_ovn_nb_connection_fallback_to_default(self): """Test NB connection falls back to default.""" # Neither l2vni nor ovn configured, should use default result = ovn_client._get_ovn_nb_connection() self.assertEqual('tcp:127.0.0.1:6641', result) def test_get_ovn_sb_connection_from_l2vni_config(self): """Test getting SB connection from [l2vni] section.""" cfg.CONF.set_override('ovn_sb_connection', ['ssl:10.0.0.1:6642'], group='l2vni') result = ovn_client._get_ovn_sb_connection() self.assertEqual('ssl:10.0.0.1:6642', result) def test_get_ovn_sb_connection_from_l2vni_list(self): """Test getting SB connection list from [l2vni] converts to string.""" cfg.CONF.set_override('ovn_sb_connection', ['ssl:10.0.0.1:6642', 'ssl:10.0.0.2:6642'], group='l2vni') result = ovn_client._get_ovn_sb_connection() self.assertEqual('ssl:10.0.0.1:6642,ssl:10.0.0.2:6642', result) def test_get_ovn_sb_connection_fallback_to_ovn_section(self): """Test SB connection falls back to [ovn] section.""" # Don't set l2vni config, should read from [ovn] cfg.CONF.set_override('ovn_sb_connection', ['ssl:192.168.1.1:6642'], group='ovn') result = ovn_client._get_ovn_sb_connection() self.assertEqual('ssl:192.168.1.1:6642', result) def test_get_ovn_sb_connection_fallback_to_default(self): """Test SB connection falls back to default.""" # Neither l2vni nor ovn configured, should use default result = ovn_client._get_ovn_sb_connection() self.assertEqual('tcp:127.0.0.1:6642', result) def test_get_ovn_ovsdb_timeout_from_l2vni_config(self): """Test getting timeout from [l2vni] section.""" cfg.CONF.set_override('ovn_ovsdb_timeout', 120, group='l2vni') result = ovn_client._get_ovn_ovsdb_timeout() self.assertEqual(120, result) def test_get_ovn_ovsdb_timeout_fallback_to_ovn_section(self): """Test timeout falls back to [ovn] section.""" # Don't set l2vni config, should read from [ovn] cfg.CONF.set_override('ovsdb_connection_timeout', 90, group='ovn') result = ovn_client._get_ovn_ovsdb_timeout() self.assertEqual(90, result) def test_get_ovn_ovsdb_timeout_fallback_to_default(self): """Test timeout falls back to default.""" # Neither l2vni nor ovn configured, should use default result = ovn_client._get_ovn_ovsdb_timeout() self.assertEqual(180, result) @mock.patch.object(ovn_client.stream.Stream, 'ssl_set_ca_cert_file', autospec=True) @mock.patch.object(ovn_client.stream.Stream, 'ssl_set_certificate_file', autospec=True) @mock.patch.object(ovn_client.stream.Stream, 'ssl_set_private_key_file', autospec=True) def test_configure_ovn_ssl_with_all_certs( self, mock_set_key, mock_set_cert, mock_set_ca): """Test SSL configuration with all certificates.""" cfg.CONF.set_override('ovn_sb_ca_cert', '/path/to/ca.pem', group='ovn') cfg.CONF.set_override('ovn_sb_certificate', '/path/to/cert.pem', group='ovn') cfg.CONF.set_override('ovn_sb_private_key', '/path/to/key.pem', group='ovn') ovn_client._configure_ovn_ssl() mock_set_ca.assert_called_once_with('/path/to/ca.pem') mock_set_cert.assert_called_once_with('/path/to/cert.pem') mock_set_key.assert_called_once_with('/path/to/key.pem') @mock.patch.object(ovn_client.stream.Stream, 'ssl_set_ca_cert_file', autospec=True) def test_configure_ovn_ssl_with_ca_only(self, mock_set_ca): """Test SSL configuration with only CA certificate.""" cfg.CONF.set_override('ovn_sb_ca_cert', '/path/to/ca.pem', group='ovn') ovn_client._configure_ovn_ssl() mock_set_ca.assert_called_once_with('/path/to/ca.pem') @mock.patch.object(ovn_client.stream.Stream, 'ssl_set_ca_cert_file', autospec=True) def test_configure_ovn_ssl_no_ovn_section(self, mock_set_ca): """Test SSL configuration handles missing [ovn] attributes.""" # Don't set any ovn SSL config - function should handle gracefully # The function checks hasattr(CONF.ovn, 'ovn_sb_ca_cert') which # will be False if the option isn't set # Should not raise exception ovn_client._configure_ovn_ssl() # Should not call SSL functions if attributes don't exist # (in reality ovn_conf registers these, but they'd be None/empty) # This test validates the defensive checks in the function @mock.patch.object(ovn_client.stream.Stream, 'ssl_set_ca_cert_file', autospec=True) def test_configure_ovn_ssl_with_empty_config(self, mock_set_ca): """Test SSL configuration with empty certificate paths.""" cfg.CONF.set_override('ovn_sb_ca_cert', '', group='ovn') ovn_client._configure_ovn_ssl() # Should not call SSL functions for empty paths mock_set_ca.assert_not_called() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/agent/test_ovn_events.py0000664000175000017500000002575315157004031027776 0ustar00zuulzuul# Copyright (c) 2026 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. """Unit tests for OVN event handlers.""" from unittest import mock from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovsdb_monitor from neutron.tests import base as tests_base from ovsdbapp.backend.ovs_idl import event as row_event from tooz import hashring from networking_baremetal.agent import ovn_events class TestLocalnetPortEvent(tests_base.BaseTestCase): """Test cases for LocalnetPortEvent.""" def setUp(self): super(TestLocalnetPortEvent, self).setUp() # Create mock agent with required attributes self.mock_agent = mock.MagicMock() self.mock_agent.agent_id = 'test-agent-id' # Create mock member manager with hash ring self.mock_member_manager = mock.MagicMock() self.mock_hashring = hashring.HashRing(['test-agent-id']) self.mock_member_manager.hashring = self.mock_hashring self.mock_agent.member_manager = self.mock_member_manager # Create event instance self.event = ovn_events.LocalnetPortEvent(self.mock_agent) def _create_mock_row(self, **kwargs): """Helper to create a mock row with required OVN attributes. BaseEvent.matches() checks row._table.name, so we need to ensure all mock rows have this attribute set correctly. """ row = mock.MagicMock() row._table.name = 'Logical_Switch_Port' for key, value in kwargs.items(): if key == 'tag' and value is not None: # tag should be a list row.tag = [value] if not isinstance(value, list) else value else: setattr(row, key, value) return row def test_event_initialization(self): """Test LocalnetPortEvent initialization.""" self.assertEqual(self.event.agent, self.mock_agent) self.assertEqual(self.event.agent_id, 'test-agent-id') self.assertEqual(self.event.hashring, self.mock_hashring) self.assertEqual(self.event.event_name, 'LocalnetPortEvent') # Verify event is watching CREATE and DELETE on Logical_Switch_Port self.assertIn(row_event.RowEvent.ROW_CREATE, self.event.events) self.assertIn(row_event.RowEvent.ROW_DELETE, self.event.events) self.assertEqual(self.event.table, 'Logical_Switch_Port') def test_event_inherits_from_base_event(self): """Test LocalnetPortEvent inherits from BaseEvent.""" self.assertIsInstance(self.event, ovsdb_monitor.BaseEvent) self.assertIsInstance(self.event, row_event.RowEvent) def test_matches_l2vni_localnet_port_owned_by_agent(self): """Test event matches L2VNI localnet port owned by this agent.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( type='localnet', name=f'neutron-{network_id}-localnet-physnet1' ) result = self.event.matches(row_event.RowEvent.ROW_CREATE, row) self.assertTrue(result) def test_matches_rejects_update_events(self): """Test event rejects UPDATE events (only CREATE/DELETE allowed).""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( type='localnet', name=f'neutron-{network_id}-localnet-physnet1' ) # BaseEvent.matches() filters out UPDATE events result = self.event.matches(row_event.RowEvent.ROW_UPDATE, row) self.assertFalse(result) def test_matches_rejects_wrong_table(self): """Test event rejects rows from wrong table.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( type='localnet', name=f'neutron-{network_id}-localnet-physnet1' ) row._table.name = 'Logical_Router_Port' # Wrong table result = self.event.matches(row_event.RowEvent.ROW_CREATE, row) self.assertFalse(result) def test_matches_rejects_non_localnet_port(self): """Test event rejects ports that are not type=localnet.""" row = self._create_mock_row( type='patch', name='neutron-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee-localnet-' 'physnet1' ) result = self.event.matches(row_event.RowEvent.ROW_CREATE, row) self.assertFalse(result) def test_matches_rejects_port_without_name(self): """Test event rejects ports without a name attribute.""" row = self._create_mock_row(type='localnet') del row.name result = self.event.matches(row_event.RowEvent.ROW_CREATE, row) self.assertFalse(result) def test_matches_rejects_non_l2vni_localnet_port(self): """Test event rejects localnet ports without L2VNI naming.""" row = self._create_mock_row( type='localnet', name='provnet-physnet1' # No '-localnet-' in name ) result = self.event.matches(row_event.RowEvent.ROW_CREATE, row) self.assertFalse(result) def test_matches_rejects_port_not_owned_by_agent(self): """Test event rejects ports not owned by agent (hash ring).""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( type='localnet', name=f'neutron-{network_id}-localnet-physnet1' ) # Create a new hashring with a different agent other_hashring = hashring.HashRing(['other-agent-id']) self.event.hashring = other_hashring result = self.event.matches(row_event.RowEvent.ROW_CREATE, row) self.assertFalse(result) def test_run_triggers_targeted_reconciliation_on_create(self): """Test run() triggers targeted reconciliation on CREATE.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( type='localnet', name=f'neutron-{network_id}-localnet-physnet1', options={'network_name': 'physnet1'}, tag=105 ) self.event.run(row_event.RowEvent.ROW_CREATE, row, None) # Verify targeted reconciliation was triggered mock_reconcile = self.mock_agent._reconcile_single_vlan_blocking mock_reconcile.assert_called_once_with( network_id, 'physnet1', 105, 'add' ) def test_run_triggers_targeted_reconciliation_on_delete(self): """Test run() triggers targeted reconciliation on DELETE.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( type='localnet', name=f'neutron-{network_id}-localnet-physnet1', options={'network_name': 'physnet1'}, tag=105 ) self.event.run(row_event.RowEvent.ROW_DELETE, row, None) # Verify targeted reconciliation was triggered with 'remove' action mock_reconcile = self.mock_agent._reconcile_single_vlan_blocking mock_reconcile.assert_called_once_with( network_id, 'physnet1', 105, 'remove' ) def test_run_falls_back_to_full_reconciliation_on_missing_vlan(self): """Test run() falls back to full reconciliation if VLAN missing.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( type='localnet', name=f'neutron-{network_id}-localnet-physnet1', options={'network_name': 'physnet1'}, tag=None # No VLAN tag ) self.event.run(row_event.RowEvent.ROW_CREATE, row, None) # Should fall back to full reconciliation self.mock_agent._reconcile_l2vni_trunks.assert_called_once() self.mock_agent._reconcile_single_vlan_blocking.assert_not_called() def test_run_falls_back_to_full_reconciliation_on_missing_physnet(self): """Test run() falls back to full reconciliation if physnet missing.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( type='localnet', name=f'neutron-{network_id}-localnet-physnet1', options={}, # No network_name tag=105 ) self.event.run(row_event.RowEvent.ROW_CREATE, row, None) # Should fall back to full reconciliation self.mock_agent._reconcile_l2vni_trunks.assert_called_once() self.mock_agent._reconcile_single_vlan_blocking.assert_not_called() def test_run_handles_attribute_error_gracefully(self): """Test run() handles AttributeError. Falls back to full reconciliation. """ row = self._create_mock_row( type='localnet', name='neutron-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee-localnet-' 'physnet1' ) # Missing options attribute will cause AttributeError del row.options # Should not raise exception self.event.run(row_event.RowEvent.ROW_CREATE, row, None) # Should fall back to full reconciliation self.mock_agent._reconcile_l2vni_trunks.assert_called_once() def test_extract_network_id(self): """Test _extract_network_id() helper method.""" test_cases = [ ('neutron-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee-localnet-physnet1', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'), ('neutron-11111111-2222-3333-4444-555555555555-localnet-provider', '11111111-2222-3333-4444-555555555555'), ] for port_name, expected_network_id in test_cases: result = self.event._extract_network_id(port_name) self.assertEqual(result, expected_network_id) def test_extract_network_id_handles_malformed_name(self): """Test _extract_network_id() handles malformed port names.""" # Name without '-localnet-' separator result = self.event._extract_network_id('invalid-name') self.assertIsNone(result) # Name with '-localnet-' but missing network UUID part # Returns 'neutron' because replace doesn't match 'neutron-' result = self.event._extract_network_id('neutron-localnet-physnet1') self.assertEqual(result, 'neutron') def test_extract_network_id_handles_empty_network_id(self): """Test _extract_network_id() returns empty string. For empty network ID. """ # This edge case has empty network ID but valid format port_name = 'neutron--localnet-physnet1' result = self.event._extract_network_id(port_name) # Returns empty string (after replacing 'neutron-' from 'neutron-') self.assertEqual(result, '') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/agent/test_router_ha_binding.py0000664000175000017500000011347315157004031031267 0ustar00zuulzuul# Copyright (c) 2026 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. """Unit tests for Router HA Binding Manager.""" from unittest import mock from neutron.common.ovn import constants as ovn_const from neutron.tests import base as tests_base from neutron_lib import constants as n_const from openstack import exceptions as sdk_exc from ovsdbapp.backend.ovs_idl import idlutils from ovsdbapp import exceptions as ovs_exc from tooz import hashring from networking_baremetal.agent import router_ha_binding class FakePort: """Fake Neutron Port object.""" def __init__(self, port_id, device_owner=n_const.DEVICE_OWNER_ROUTER_INTF): self.id = port_id self.device_owner = device_owner class FakeLogicalRouterPort: """Fake OVN Logical Router Port object.""" def __init__(self, name, ha_chassis_group=None): self.name = name if ha_chassis_group is None: self.ha_chassis_group = [] elif isinstance(ha_chassis_group, list): self.ha_chassis_group = ha_chassis_group else: self.ha_chassis_group = [ha_chassis_group] class FakeHAChassisGroup: """Fake OVN HA_Chassis_Group object.""" def __init__(self, uuid, external_ids=None): self.uuid = uuid self.external_ids = external_ids or {} class TestRouterHABindingManager(tests_base.BaseTestCase): """Test cases for RouterHABindingManager.""" def setUp(self): super(TestRouterHABindingManager, self).setUp() self.mock_neutron = mock.Mock() self.mock_ovn_nb = mock.Mock() self.mock_member_manager = mock.Mock() self.agent_id = 'test-agent-id' # Setup hash ring self.mock_hashring = hashring.HashRing([self.agent_id]) self.mock_member_manager.hashring = self.mock_hashring # Setup OVN tables structure self.mock_ovn_nb.tables = { 'HA_Chassis_Group': mock.Mock( rows=mock.Mock(values=mock.Mock(return_value=[]))), } self.manager = router_ha_binding.RouterHABindingManager( neutron_client=self.mock_neutron, ovn_nb_idl=self.mock_ovn_nb, member_manager=self.mock_member_manager, agent_id=self.agent_id ) def test_initialize(self): """Test manager initialization.""" self.assertEqual(self.manager.neutron_client, self.mock_neutron) self.assertEqual(self.manager.ovn_nb_idl, self.mock_ovn_nb) self.assertEqual(self.manager.member_manager, self.mock_member_manager) self.assertEqual(self.manager.agent_id, self.agent_id) def test_should_manage_network_owned_by_agent(self): """Test _should_manage_network returns True for owned network.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' result = self.manager._should_manage_network(network_id) self.assertTrue(result) def test_should_manage_network_not_owned_by_agent(self): """Test _should_manage_network returns False for non-owned network.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' # Create hashring with different agent other_hashring = hashring.HashRing(['other-agent-id']) self.mock_member_manager.hashring = other_hashring result = self.manager._should_manage_network(network_id) self.assertFalse(result) def test_should_manage_network_handles_hash_ring_error(self): """Test _should_manage_network handles hash ring lookup errors.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' # Simulate hash ring error self.mock_member_manager.hashring = None result = self.manager._should_manage_network(network_id) self.assertFalse(result) def test_get_router_interface_ports_success(self): """Test _get_router_interface_ports returns router ports.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' port1 = FakePort('port-1') port2 = FakePort('port-2') self.mock_neutron.network.ports.return_value = iter([port1, port2]) result = self.manager._get_router_interface_ports(network_id) self.assertEqual(len(result), 2) self.assertEqual(result[0].id, 'port-1') self.assertEqual(result[1].id, 'port-2') self.mock_neutron.network.ports.assert_called_once_with( network_id=network_id, device_owner=n_const.DEVICE_OWNER_ROUTER_INTF ) def test_get_router_interface_ports_empty(self): """Test _get_router_interface_ports returns empty list.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' self.mock_neutron.network.ports.return_value = iter([]) result = self.manager._get_router_interface_ports(network_id) self.assertEqual(len(result), 0) def test_get_router_interface_ports_handles_exception(self): """Test _get_router_interface_ports handles SDK exceptions.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' self.mock_neutron.network.ports.side_effect = \ sdk_exc.OpenStackCloudException("API error") with self.assertRaises(sdk_exc.OpenStackCloudException): self.manager._get_router_interface_ports(network_id) def test_get_current_ha_chassis_group_with_list(self): """Test _get_current_ha_chassis_group with list value.""" lrp = FakeLogicalRouterPort( 'lrp-test', ha_chassis_group=['ha-group-1']) result = self.manager._get_current_ha_chassis_group(lrp) self.assertEqual(result, 'ha-group-1') def test_get_current_ha_chassis_group_with_single_value(self): """Test _get_current_ha_chassis_group with single value.""" lrp = FakeLogicalRouterPort( 'lrp-test', ha_chassis_group='ha-group-1') result = self.manager._get_current_ha_chassis_group(lrp) self.assertEqual(result, 'ha-group-1') def test_get_current_ha_chassis_group_empty(self): """Test _get_current_ha_chassis_group with empty list.""" lrp = FakeLogicalRouterPort('lrp-test', ha_chassis_group=[]) result = self.manager._get_current_ha_chassis_group(lrp) self.assertIsNone(result) def test_get_current_ha_chassis_group_no_attribute(self): """Test _get_current_ha_chassis_group without ha_chassis_group attr.""" lrp = mock.Mock(spec=['name']) lrp.name = 'lrp-test' result = self.manager._get_current_ha_chassis_group(lrp) self.assertIsNone(result) def test_update_lrp_ha_chassis_group_success(self): """Test _update_lrp_ha_chassis_group updates LRP successfully.""" port_id = 'port-1' ha_chassis_group = 'ha-group-1' network_id = 'network-1' # Mock LRP with no HA chassis group lrp = FakeLogicalRouterPort('lrp-port-1', ha_chassis_group=[]) mock_lrp_get = mock.Mock() mock_lrp_get.execute.return_value = lrp self.mock_ovn_nb.lrp_get.return_value = mock_lrp_get # Mock lrp_set_ha_chassis_group mock_set_ha = mock.Mock() mock_set_ha.execute.return_value = None self.mock_ovn_nb.lrp_set_ha_chassis_group.return_value = mock_set_ha result = self.manager._update_lrp_ha_chassis_group( port_id, ha_chassis_group, network_id) self.assertTrue(result) self.mock_ovn_nb.lrp_get.assert_called_once_with('lrp-port-1') self.mock_ovn_nb.lrp_set_ha_chassis_group.assert_called_once_with( 'lrp-port-1', ha_chassis_group) def test_update_lrp_ha_chassis_group_already_correct(self): """Test _update_lrp_ha_chassis_group skips if already correct.""" port_id = 'port-1' ha_chassis_group = 'ha-group-1' network_id = 'network-1' lrp = FakeLogicalRouterPort( 'lrp-port-1', ha_chassis_group=['ha-group-1']) mock_lrp_get = mock.Mock() mock_lrp_get.execute.return_value = lrp self.mock_ovn_nb.lrp_get.return_value = mock_lrp_get result = self.manager._update_lrp_ha_chassis_group( port_id, ha_chassis_group, network_id) self.assertFalse(result) self.mock_ovn_nb.lrp_get.assert_called_once_with('lrp-port-1') self.mock_ovn_nb.lrp_set_ha_chassis_group.assert_not_called() def test_update_lrp_ha_chassis_group_not_found(self): """Test _update_lrp_ha_chassis_group handles LRP not found.""" port_id = 'port-1' ha_chassis_group = 'ha-group-1' network_id = 'network-1' # Mock LRP not found mock_lrp_get = mock.Mock() mock_lrp_get.execute.side_effect = idlutils.RowNotFound( table='Logical_Router_Port', col='name', match='lrp-port-1') self.mock_ovn_nb.lrp_get.return_value = mock_lrp_get result = self.manager._update_lrp_ha_chassis_group( port_id, ha_chassis_group, network_id) self.assertFalse(result) self.mock_ovn_nb.lrp_set_ha_chassis_group.assert_not_called() def test_update_lrp_ha_chassis_group_updates_from_different_group(self): """Test _update_lrp_ha_chassis_group updates from different group.""" port_id = 'port-1' ha_chassis_group = 'ha-group-2' network_id = 'network-1' lrp = FakeLogicalRouterPort( 'lrp-port-1', ha_chassis_group=['ha-group-1']) mock_lrp_get = mock.Mock() mock_lrp_get.execute.return_value = lrp self.mock_ovn_nb.lrp_get.return_value = mock_lrp_get # Mock lrp_set_ha_chassis_group mock_set_ha = mock.Mock() mock_set_ha.execute.return_value = None self.mock_ovn_nb.lrp_set_ha_chassis_group.return_value = mock_set_ha result = self.manager._update_lrp_ha_chassis_group( port_id, ha_chassis_group, network_id) self.assertTrue(result) self.mock_ovn_nb.lrp_set_ha_chassis_group.assert_called_once_with( 'lrp-port-1', ha_chassis_group) def test_bind_lrp_to_ha_group_success(self): """Test _bind_lrp_to_ha_group calls update method.""" port_id = 'port-1' ha_chassis_group = 'ha-group-1' network_id = 'network-1' with mock.patch.object( self.manager, '_update_lrp_ha_chassis_group', autospec=True, return_value=True) as mock_update: self.manager._bind_lrp_to_ha_group( port_id, ha_chassis_group, network_id) mock_update.assert_called_once_with( port_id, ha_chassis_group, network_id) def test_bind_lrp_to_ha_group_handles_ovsdb_exception(self): """Test _bind_lrp_to_ha_group handles OvsdbAppException.""" port_id = 'port-1' ha_chassis_group = 'ha-group-1' network_id = 'network-1' with mock.patch.object( self.manager, '_update_lrp_ha_chassis_group', autospec=True, side_effect=ovs_exc.OvsdbAppException()): with self.assertRaises(ovs_exc.OvsdbAppException): self.manager._bind_lrp_to_ha_group( port_id, ha_chassis_group, network_id) def test_bind_lrp_to_ha_group_handles_runtime_error(self): """Test _bind_lrp_to_ha_group handles RuntimeError.""" port_id = 'port-1' ha_chassis_group = 'ha-group-1' network_id = 'network-1' with mock.patch.object( self.manager, '_update_lrp_ha_chassis_group', autospec=True, side_effect=RuntimeError("Runtime error")): with self.assertRaises(RuntimeError): self.manager._bind_lrp_to_ha_group( port_id, ha_chassis_group, network_id) def test_bind_lrp_to_ha_group_handles_attribute_error(self): """Test _bind_lrp_to_ha_group handles AttributeError.""" port_id = 'port-1' ha_chassis_group = 'ha-group-1' network_id = 'network-1' with mock.patch.object( self.manager, '_update_lrp_ha_chassis_group', autospec=True, side_effect=AttributeError("Attribute error")): with self.assertRaises(AttributeError): self.manager._bind_lrp_to_ha_group( port_id, ha_chassis_group, network_id) def test_bind_router_interfaces_for_network_success(self): """Test bind_router_interfaces_for_network happy path.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' ha_chassis_group = 'ha-group-1' port1 = FakePort('port-1') port2 = FakePort('port-2') with mock.patch.object( self.manager, '_should_manage_network', autospec=True, return_value=True): with mock.patch.object( self.manager, '_get_router_interface_ports', autospec=True, return_value=[port1, port2]): with mock.patch.object( self.manager, '_bind_lrp_to_ha_group', autospec=True) as mock_bind: self.manager.bind_router_interfaces_for_network( network_id, ha_chassis_group) self.assertEqual(mock_bind.call_count, 2) mock_bind.assert_any_call('port-1', ha_chassis_group, network_id) mock_bind.assert_any_call('port-2', ha_chassis_group, network_id) def test_bind_router_interfaces_for_network_not_managed(self): """Test skips non-managed network.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' ha_chassis_group = 'ha-group-1' with mock.patch.object( self.manager, '_should_manage_network', autospec=True, return_value=False): with mock.patch.object( self.manager, '_get_router_interface_ports', autospec=True) as mock_get_ports: self.manager.bind_router_interfaces_for_network( network_id, ha_chassis_group) mock_get_ports.assert_not_called() def test_bind_router_interfaces_for_network_no_router_ports(self): """Test bind_router_interfaces_for_network with no router ports.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' ha_chassis_group = 'ha-group-1' with mock.patch.object( self.manager, '_should_manage_network', autospec=True, return_value=True): with mock.patch.object( self.manager, '_get_router_interface_ports', autospec=True, return_value=[]): with mock.patch.object( self.manager, '_bind_lrp_to_ha_group', autospec=True) as mock_bind: self.manager.bind_router_interfaces_for_network( network_id, ha_chassis_group) mock_bind.assert_not_called() def test_bind_router_interfaces_for_network_handles_port_bind_error( self): """Test handles port bind errors.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' ha_chassis_group = 'ha-group-1' port1 = FakePort('port-1') port2 = FakePort('port-2') with mock.patch.object( self.manager, '_should_manage_network', autospec=True, return_value=True): with mock.patch.object( self.manager, '_get_router_interface_ports', autospec=True, return_value=[port1, port2]): with mock.patch.object( self.manager, '_bind_lrp_to_ha_group', autospec=True) as mock_bind: mock_bind.side_effect = [ ovs_exc.OvsdbAppException(), None ] self.manager.bind_router_interfaces_for_network( network_id, ha_chassis_group) self.assertEqual(mock_bind.call_count, 2) def test_bind_router_interfaces_for_network_handles_query_error(self): """Test handles query errors.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' ha_chassis_group = 'ha-group-1' with mock.patch.object( self.manager, '_should_manage_network', autospec=True, return_value=True): with mock.patch.object( self.manager, '_get_router_interface_ports', autospec=True, side_effect=sdk_exc.OpenStackCloudException( "API error")): self.manager.bind_router_interfaces_for_network( network_id, ha_chassis_group) def test_get_networks_with_ha_chassis_groups_success(self): """Test _get_networks_with_ha_chassis_groups returns groups.""" network1_id = 'network-1' network2_id = 'network-2' ha_group1 = FakeHAChassisGroup( 'ha-group-1', external_ids={ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network1_id} ) ha_group2 = FakeHAChassisGroup( 'ha-group-2', external_ids={ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network2_id} ) table = self.mock_ovn_nb.tables['HA_Chassis_Group'] table.rows.values.return_value = [ha_group1, ha_group2] result = self.manager._get_networks_with_ha_chassis_groups() self.assertEqual(len(result), 2) self.assertEqual(result[network1_id], 'ha-group-1') self.assertEqual(result[network2_id], 'ha-group-2') def test_get_networks_with_ha_chassis_groups_accepts_unified_groups( self): """Test accepts unified HA groups (network + router). When both network-level and unified groups exist for the same network, the last one found is used. In unified HA chassis group scenarios, the same group is used for both the network and router, which is the correct behavior for proper router interface binding. """ network_id = 'network-1' router_id = 'router-1' network_group = FakeHAChassisGroup( 'ha-group-network', external_ids={ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network_id} ) router_group = FakeHAChassisGroup( 'ha-group-router', external_ids={ ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network_id, ovn_const.OVN_ROUTER_ID_EXT_ID_KEY: router_id } ) table = self.mock_ovn_nb.tables['HA_Chassis_Group'] table.rows.values.return_value = [network_group, router_group] result = self.manager._get_networks_with_ha_chassis_groups() self.assertEqual(len(result), 1) # The last group found is used (router_group in this case) self.assertEqual(result[network_id], 'ha-group-router') def test_get_networks_with_ha_chassis_groups_empty(self): """Test _get_networks_with_ha_chassis_groups with no groups.""" table = self.mock_ovn_nb.tables['HA_Chassis_Group'] table.rows.values.return_value = [] result = self.manager._get_networks_with_ha_chassis_groups() self.assertEqual(len(result), 0) def test_get_networks_with_ha_chassis_groups_no_table(self): """Test _get_networks_with_ha_chassis_groups when table missing.""" self.mock_ovn_nb.tables = {} result = self.manager._get_networks_with_ha_chassis_groups() self.assertEqual(len(result), 0) def test_get_networks_with_ha_chassis_groups_no_tables_attr(self): """Test without tables attribute.""" delattr(self.mock_ovn_nb, 'tables') result = self.manager._get_networks_with_ha_chassis_groups() self.assertEqual(len(result), 0) def test_get_networks_with_ha_chassis_groups_handles_row_no_ext_ids( self): """Test handles rows without external_ids.""" network_id = 'network-1' good_group = FakeHAChassisGroup( 'ha-group-1', external_ids={ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network_id} ) bad_group = mock.Mock(spec=['uuid']) bad_group.uuid = 'ha-group-2' table = self.mock_ovn_nb.tables['HA_Chassis_Group'] table.rows.values.return_value = [good_group, bad_group] result = self.manager._get_networks_with_ha_chassis_groups() self.assertEqual(len(result), 1) self.assertEqual(result[network_id], 'ha-group-1') def test_reconcile_success(self): """Test reconcile processes networks and updates ports.""" network1_id = 'network-1' network2_id = 'network-2' ha_group1 = 'ha-group-1' ha_group2 = 'ha-group-2' port1 = FakePort('port-1') port2 = FakePort('port-2') with mock.patch.object( self.manager, '_get_networks_with_ha_chassis_groups', autospec=True, return_value={network1_id: ha_group1, network2_id: ha_group2}): with mock.patch.object( self.manager, '_should_manage_network', autospec=True, return_value=True): with mock.patch.object( self.manager, '_get_router_interface_ports', autospec=True, return_value=[port1, port2]): with mock.patch.object( self.manager, '_update_lrp_ha_chassis_group', autospec=True, return_value=True) as mock_update: self.manager.reconcile() self.assertEqual(mock_update.call_count, 4) def test_reconcile_no_networks(self): """Test reconcile with no networks.""" with mock.patch.object( self.manager, '_get_networks_with_ha_chassis_groups', autospec=True, return_value={}): with mock.patch.object( self.manager, '_should_manage_network', autospec=True) as mock_should_manage: self.manager.reconcile() mock_should_manage.assert_not_called() def test_reconcile_skips_non_managed_networks(self): """Test reconcile skips networks not managed by agent.""" network1_id = 'network-1' network2_id = 'network-2' ha_group1 = 'ha-group-1' ha_group2 = 'ha-group-2' with mock.patch.object( self.manager, '_get_networks_with_ha_chassis_groups', autospec=True, return_value={network1_id: ha_group1, network2_id: ha_group2}): with mock.patch.object( self.manager, '_should_manage_network', autospec=True, side_effect=lambda nid: nid == network1_id): with mock.patch.object( self.manager, '_get_router_interface_ports', autospec=True, return_value=[FakePort('port-1')]) as mock_get_ports: with mock.patch.object( self.manager, '_update_lrp_ha_chassis_group', autospec=True, return_value=True): self.manager.reconcile() mock_get_ports.assert_called_once_with(network1_id) def test_reconcile_skips_networks_without_router_ports(self): """Test reconcile skips networks without router ports.""" network_id = 'network-1' ha_group = 'ha-group-1' with mock.patch.object( self.manager, '_get_networks_with_ha_chassis_groups', autospec=True, return_value={network_id: ha_group}): with mock.patch.object( self.manager, '_should_manage_network', autospec=True, return_value=True): with mock.patch.object( self.manager, '_get_router_interface_ports', autospec=True, return_value=[]): with mock.patch.object( self.manager, '_update_lrp_ha_chassis_group', autospec=True) as mock_update: self.manager.reconcile() mock_update.assert_not_called() def test_reconcile_handles_port_update_errors(self): """Test reconcile continues after port update errors.""" network_id = 'network-1' ha_group = 'ha-group-1' port1 = FakePort('port-1') port2 = FakePort('port-2') with mock.patch.object( self.manager, '_get_networks_with_ha_chassis_groups', autospec=True, return_value={network_id: ha_group}): with mock.patch.object( self.manager, '_should_manage_network', autospec=True, return_value=True): with mock.patch.object( self.manager, '_get_router_interface_ports', autospec=True, return_value=[port1, port2]): with mock.patch.object( self.manager, '_update_lrp_ha_chassis_group', autospec=True) as mock_update: mock_update.side_effect = [ ovs_exc.OvsdbAppException(), True ] self.manager.reconcile() self.assertEqual(mock_update.call_count, 2) def test_reconcile_handles_query_errors(self): """Test reconcile handles Neutron query errors.""" network_id = 'network-1' ha_group = 'ha-group-1' with mock.patch.object( self.manager, '_get_networks_with_ha_chassis_groups', autospec=True, return_value={network_id: ha_group}): with mock.patch.object( self.manager, '_should_manage_network', autospec=True, return_value=True): with mock.patch.object( self.manager, '_get_router_interface_ports', autospec=True, side_effect=sdk_exc.OpenStackCloudException( "API error")): self.manager.reconcile() def test_reconcile_handles_general_exception(self): """Test reconcile handles general exceptions.""" with mock.patch.object( self.manager, '_get_networks_with_ha_chassis_groups', autospec=True, side_effect=Exception("Unexpected error")): self.manager.reconcile() def test_reconcile_counts_updated_ports(self): """Test reconcile correctly counts updated ports.""" network_id = 'network-1' ha_group = 'ha-group-1' port1 = FakePort('port-1') port2 = FakePort('port-2') port3 = FakePort('port-3') with mock.patch.object( self.manager, '_get_networks_with_ha_chassis_groups', autospec=True, return_value={network_id: ha_group}): with mock.patch.object( self.manager, '_should_manage_network', autospec=True, return_value=True): with mock.patch.object( self.manager, '_get_router_interface_ports', autospec=True, return_value=[port1, port2, port3]): with mock.patch.object( self.manager, '_update_lrp_ha_chassis_group', autospec=True) as mock_update: mock_update.side_effect = [True, False, True] self.manager.reconcile() self.assertEqual(mock_update.call_count, 3) class TestHAChassisGroupNetworkEvent(tests_base.BaseTestCase): """Test cases for HAChassisGroupNetworkEvent.""" def setUp(self): super(TestHAChassisGroupNetworkEvent, self).setUp() # Create mock agent with required attributes self.mock_agent = mock.MagicMock() self.mock_agent.agent_id = 'test-agent-id' # Create mock member manager with hash ring self.mock_member_manager = mock.MagicMock() self.mock_hashring = hashring.HashRing(['test-agent-id']) self.mock_member_manager.hashring = self.mock_hashring self.mock_agent.member_manager = self.mock_member_manager # Create mock router HA binding manager self.mock_router_ha_binding = mock.Mock() self.mock_agent.router_ha_binding = self.mock_router_ha_binding # Create event instance from networking_baremetal.agent.ovn_events import \ HAChassisGroupNetworkEvent self.event = HAChassisGroupNetworkEvent(self.mock_agent) def _create_mock_row(self, **kwargs): """Helper to create a mock HA_Chassis_Group row.""" row = mock.MagicMock() row._table.name = 'HA_Chassis_Group' for key, value in kwargs.items(): setattr(row, key, value) return row def test_event_initialization(self): """Test HAChassisGroupNetworkEvent initialization.""" self.assertEqual(self.event.agent, self.mock_agent) self.assertEqual(self.event.agent_id, 'test-agent-id') self.assertEqual(self.event.hashring, self.mock_hashring) self.assertEqual(self.event.event_name, 'HAChassisGroupNetworkEvent') # Verify event is watching CREATE and UPDATE on HA_Chassis_Group from ovsdbapp.backend.ovs_idl import event as row_event self.assertIn(row_event.RowEvent.ROW_CREATE, self.event.events) self.assertIn(row_event.RowEvent.ROW_UPDATE, self.event.events) self.assertEqual(self.event.table, 'HA_Chassis_Group') def test_event_inherits_from_base_event(self): """Test HAChassisGroupNetworkEvent inherits from BaseEvent.""" from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import \ ovsdb_monitor from ovsdbapp.backend.ovs_idl import event as row_event self.assertIsInstance(self.event, ovsdb_monitor.BaseEvent) self.assertIsInstance(self.event, row_event.RowEvent) def test_match_fn_network_level_group_owned_by_agent(self): """Test match_fn returns True for network-level group.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( uuid='ha-group-1', external_ids={ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network_id} ) from ovsdbapp.backend.ovs_idl import event as row_event result = self.event.matches(row_event.RowEvent.ROW_CREATE, row) self.assertTrue(result) def test_match_fn_accepts_unified_ha_group(self): """Test match_fn accepts unified HA groups (network + router).""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' router_id = 'router-1' row = self._create_mock_row( uuid='ha-group-1', external_ids={ ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network_id, ovn_const.OVN_ROUTER_ID_EXT_ID_KEY: router_id } ) from ovsdbapp.backend.ovs_idl import event as row_event result = self.event.matches(row_event.RowEvent.ROW_CREATE, row) self.assertTrue(result) def test_match_fn_rejects_group_without_network_id(self): """Test match_fn rejects groups without network_id.""" row = self._create_mock_row( uuid='ha-group-1', external_ids={} ) from ovsdbapp.backend.ovs_idl import event as row_event result = self.event.matches(row_event.RowEvent.ROW_CREATE, row) self.assertFalse(result) def test_match_fn_rejects_group_without_external_ids(self): """Test match_fn rejects groups without external_ids attribute.""" row = self._create_mock_row(uuid='ha-group-1') del row.external_ids from ovsdbapp.backend.ovs_idl import event as row_event result = self.event.matches(row_event.RowEvent.ROW_CREATE, row) self.assertFalse(result) def test_match_fn_rejects_group_not_owned_by_agent(self): """Test match_fn rejects groups not owned by agent (hash ring).""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( uuid='ha-group-1', external_ids={ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network_id} ) # Create hashring with different agent other_hashring = hashring.HashRing(['other-agent-id']) self.event.hashring = other_hashring from ovsdbapp.backend.ovs_idl import event as row_event result = self.event.matches(row_event.RowEvent.ROW_CREATE, row) self.assertFalse(result) def test_match_fn_rejects_wrong_table(self): """Test match_fn rejects rows from wrong table.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( uuid='ha-group-1', external_ids={ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network_id} ) row._table.name = 'Logical_Router_Port' # Wrong table from ovsdbapp.backend.ovs_idl import event as row_event result = self.event.matches(row_event.RowEvent.ROW_CREATE, row) self.assertFalse(result) def test_match_fn_accepts_create_events(self): """Test match_fn accepts CREATE events.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( uuid='ha-group-1', external_ids={ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network_id} ) from ovsdbapp.backend.ovs_idl import event as row_event result = self.event.matches(row_event.RowEvent.ROW_CREATE, row) self.assertTrue(result) def test_match_fn_accepts_update_events(self): """Test match_fn accepts UPDATE events.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( uuid='ha-group-1', external_ids={ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network_id} ) from ovsdbapp.backend.ovs_idl import event as row_event result = self.event.matches(row_event.RowEvent.ROW_UPDATE, row) self.assertTrue(result) def test_match_fn_rejects_delete_events(self): """Test match_fn rejects DELETE events.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( uuid='ha-group-1', external_ids={ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network_id} ) from ovsdbapp.backend.ovs_idl import event as row_event result = self.event.matches(row_event.RowEvent.ROW_DELETE, row) self.assertFalse(result) def test_run_triggers_router_interface_binding(self): """Test run() triggers router interface binding.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' ha_group_uuid = 'ha-group-1' row = self._create_mock_row( uuid=ha_group_uuid, external_ids={ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network_id} ) from ovsdbapp.backend.ovs_idl import event as row_event self.event.run(row_event.RowEvent.ROW_CREATE, row, None) mock_bind = (self.mock_router_ha_binding. bind_router_interfaces_for_network) mock_bind.assert_called_once_with(network_id, ha_group_uuid) def test_run_handles_missing_router_ha_binding_manager(self): """Test run() handles missing router HA binding manager.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( uuid='ha-group-1', external_ids={ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network_id} ) # Remove router_ha_binding attribute self.mock_agent.router_ha_binding = None from ovsdbapp.backend.ovs_idl import event as row_event # Should not raise exception self.event.run(row_event.RowEvent.ROW_CREATE, row, None) def test_run_handles_missing_router_ha_binding_attribute(self): """Test run() handles missing router_ha_binding attribute.""" network_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' row = self._create_mock_row( uuid='ha-group-1', external_ids={ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: network_id} ) # Remove router_ha_binding attribute entirely delattr(self.mock_agent, 'router_ha_binding') from ovsdbapp.backend.ovs_idl import event as row_event # Should not raise exception self.event.run(row_event.RowEvent.ROW_CREATE, row, None) def test_run_handles_attribute_error(self): """Test run() handles AttributeError gracefully.""" row = self._create_mock_row(uuid='ha-group-1') # Missing external_ids will cause AttributeError del row.external_ids from ovsdbapp.backend.ovs_idl import event as row_event # Should not raise exception self.event.run(row_event.RowEvent.ROW_CREATE, row, None) def test_run_handles_key_error(self): """Test run() handles KeyError gracefully.""" row = self._create_mock_row( uuid='ha-group-1', external_ids={} # No network_id key ) from ovsdbapp.backend.ovs_idl import event as row_event # Should not raise exception (network_id will be None) self.event.run(row_event.RowEvent.ROW_CREATE, row, None) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8419957 networking_baremetal-7.2.0/networking_baremetal/tests/unit/drivers/0000775000175000017500000000000015157004110024541 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/drivers/__init__.py0000664000175000017500000000000015157004031026642 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1773930567.842996 networking_baremetal-7.2.0/networking_baremetal/tests/unit/drivers/netconf/0000775000175000017500000000000015157004110026175 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/drivers/netconf/__init__.py0000664000175000017500000000000015157004031030276 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/drivers/netconf/test_openconfig.py0000664000175000017500000015773715157004031031763 0ustar00zuulzuul# 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 xml.etree import ElementTree from ncclient import manager from neutron.plugins.ml2 import driver_context from neutron_lib import constants as n_const from neutron_lib.plugins.ml2 import api from oslo_config import fixture as config_fixture from oslo_utils import uuidutils from networking_baremetal import config from networking_baremetal import constants from networking_baremetal.constants import NetconfEditConfigOperation as nc_op from networking_baremetal.drivers.netconf import openconfig from networking_baremetal.openconfig.interfaces import interfaces from networking_baremetal.openconfig.lacp import lacp from networking_baremetal.tests import base from networking_baremetal.tests.unit.plugins.ml2 import utils as ml2_utils OC_IF_NS = 'http://openconfig.net/yang/interfaces' OC_IF_ETH_NS = 'http://openconfig.net/yang/interfaces/ethernet' OC_IF_AGG_NS = 'http://openconfig.net/yang/interfaces/aggregate' XML_IFACES_AGGREDATE_ID = f''' foo1/1 Po10 foo1/2 Po10 ''' XML_AGGREGATE_IFACES = f''' Po5 Po7 Po9 foo1/1 ''' class TestNetconfOpenConfigClient(base.TestCase): def setUp(self): super(TestNetconfOpenConfigClient, self).setUp() self.device = 'foo' self.conf = self.useFixture(config_fixture.Config()) self.conf.register_opts(config._opts + config._device_opts, group='foo') self.conf.register_opts((openconfig._DEVICE_OPTS + openconfig._NCCLIENT_OPTS), group='foo') self.conf.config(enabled_devices=['foo'], group='networking_baremetal') self.conf.config(driver='test-driver', switch_id='aa:bb:cc:dd:ee:ff', switch_info='foo', physical_networks=['fake_physical_network'], device_params={'name': 'default'}, host='foo.example.com', key_filename='/test/test_key_file', username='foo_user', group='foo') self.client = openconfig.NetconfOpenConfigClient(self.device) def test_get_lock_session_id(self): err_info = ( '' '' '{}' '') self.assertEqual('0', self.client._get_lock_session_id( err_info.format(0))) self.assertEqual('abc-123', self.client._get_lock_session_id( err_info.format('abc-123'))) def test_get_client_args(self): self.assertEqual( {'device_params': {'name': 'default'}, 'host': 'foo.example.com', 'hostkey_verify': True, 'keepalive': True, 'key_filename': '/test/test_key_file', 'port': 830, 'username': 'foo_user', 'allow_agent': True, 'look_for_keys': True}, self.client.get_client_args()) @mock.patch.object(manager, 'connect', autospec=True) def test_get_capabilities(self, mock_manager): fake_caps = set(constants.IANA_NETCONF_CAPABILITIES.values()) fake_caps.add('http://openconfig.net/yang/' 'network-instance?' 'module=openconfig-network-instance&' 'revision=2021-07-22') fake_caps.add('http://openconfig.net/yang/' 'interfaces?' 'module=openconfig-interfaces&' 'revision=2021-04-06') mock_ncclient = mock.Mock() mock_ncclient.server_capabilities = fake_caps mock_manager.return_value.__enter__.return_value = mock_ncclient self.assertEqual({ ':base:1.0', ':base:1.1', ':candidate', ':confirmed-commit', ':confirmed-commit:1.1', ':rollback-on-error', ':startup', ':validate', ':validate:1.1', ':writable-running', 'openconfig-network-instance', 'openconfig-interfaces'}, self.client.get_capabilities()) @mock.patch.object(manager, 'connect', autospec=True) @mock.patch.object(openconfig.NetconfOpenConfigClient, 'get_lock_and_configure', autospec=True) def test_edit_config_writable_running(self, mock_lock_config, mock_manager): fake_config = mock.Mock() fake_config.to_xml_element.return_value = ElementTree.Element('fake') mock_ncclient = mock.Mock() fake_caps = {constants.IANA_NETCONF_CAPABILITIES[':writable-running']} mock_ncclient.server_capabilities = fake_caps mock_manager.return_value.__enter__.return_value = mock_ncclient self.client.edit_config(fake_config) mock_lock_config.assert_called_once_with(self.client, mock_ncclient, openconfig.RUNNING, [fake_config], False) @mock.patch.object(manager, 'connect', autospec=True) @mock.patch.object(openconfig.NetconfOpenConfigClient, 'get_lock_and_configure', autospec=True) def test_edit_config_candidate(self, mock_lock_config, mock_manager): fake_config = mock.Mock() fake_config.to_xml_element.return_value = ElementTree.Element('fake') mock_ncclient = mock.Mock() fake_caps = {constants.IANA_NETCONF_CAPABILITIES[':candidate']} mock_ncclient.server_capabilities = fake_caps mock_manager.return_value.__enter__.return_value = mock_ncclient self.client.edit_config(fake_config) mock_lock_config.assert_called_once_with(self.client, mock_ncclient, openconfig.CANDIDATE, [fake_config], False) def test_get_lock_and_configure_confirmed_commit(self): self.client.capabilities = {':candidate', ':writable-running', ':confirmed-commit'} fake_config = mock.Mock() fake_config.to_xml_element.return_value = ElementTree.Element('fake') mock_client = mock.MagicMock() self.client.get_lock_and_configure(mock_client, openconfig.CANDIDATE, [fake_config], False) mock_client.locked.assert_called_with(openconfig.CANDIDATE) mock_client.discard_changes.assert_called_once() mock_client.edit_config.assert_called_with( target=openconfig.CANDIDATE, config='') mock_client.validate.assert_not_called() mock_client.commit.assert_has_calls([ mock.call(confirmed=True, timeout=str(30)), mock.call()]) def test_get_lock_and_configure_validate(self): self.client.capabilities = {':candidate', ':writable-running', ':validate'} fake_config = mock.Mock() fake_config.to_xml_element.return_value = ElementTree.Element('fake') mock_client = mock.MagicMock() self.client.get_lock_and_configure(mock_client, openconfig.CANDIDATE, [fake_config], False) mock_client.locked.assert_called_with(openconfig.CANDIDATE) mock_client.discard_changes.assert_called_once() mock_client.edit_config.assert_called_with( target=openconfig.CANDIDATE, config='') mock_client.validate.assert_called_once_with( source=openconfig.CANDIDATE) mock_client.commit.assert_called_once_with() def test_get_lock_and_configure_writeable_running(self): self.client.capabilities = {':writable-running'} fake_config = mock.Mock() fake_config.to_xml_element.return_value = ElementTree.Element('fake') mock_client = mock.MagicMock() self.client.get_lock_and_configure(mock_client, openconfig.RUNNING, [fake_config], False) mock_client.locked.assert_called_with(openconfig.RUNNING) mock_client.discard_changes.assert_not_called() mock_client.validate.assert_not_called() mock_client.commit.assert_not_called() mock_client.edit_config.assert_called_with( target=openconfig.RUNNING, config='') @mock.patch.object(manager, 'connect', autospec=True) def test_get(self, mock_manager): fake_query = interfaces.Interfaces() fake_query.add('foo1/1') mock_ncclient = mock.Mock() mock_manager.return_value.__enter__.return_value = mock_ncclient self.client.get(query=fake_query) mock_ncclient.get.assert_called_with(filter=('subtree', mock.ANY)) def test_get_aggregation_ids(self): self.conf.config(link_aggregate_prefix='foo', link_aggregate_range='5..10', group=self.device) self.assertEqual({'foo5', 'foo6', 'foo7', 'foo8', 'foo9', 'foo10'}, self.client.get_aggregation_ids()) def test_allocate_deferred(self): aggregate_id = 'foo5' _config = [] ifaces = interfaces.Interfaces() iface_a = ifaces.add('foo1/1', interface_type=constants.IFACE_TYPE_ETHERNET) iface_a.ethernet.config.aggregate_id = openconfig.DEFERRED iface_b = ifaces.add('foo1/2', interface_type=constants.IFACE_TYPE_ETHERNET) iface_b.ethernet.config.aggregate_id = openconfig.DEFERRED iface_agg = ifaces.add(openconfig.DEFERRED, interface_type=constants.IFACE_TYPE_AGGREGATE) iface_agg.config.name = openconfig.DEFERRED _config.append(ifaces) _lacp = lacp.LACP() lacp_ifaces = lacp.LACPInterfaces() lacp_ifaces.add(openconfig.DEFERRED) _config.append(_lacp) self.client.allocate_deferred(aggregate_id, _config) for conf in _config: if isinstance(conf, interfaces.Interfaces): for iface in ifaces: if isinstance(iface, interfaces.InterfaceAggregate): self.assertEqual(iface.name, aggregate_id) self.assertEqual(iface.config.name, aggregate_id) elif isinstance(iface, interfaces.InterfaceEthernet): self.assertEqual(iface.ethernet.config.aggregate_id, aggregate_id) if isinstance(conf, lacp.LACP): for lacp_iface in _lacp.interfaces.interfaces: self.assertEqual(lacp_iface.name, aggregate_id) self.assertEqual(lacp_iface.config.name, aggregate_id) def test_get_free_aggregate_id(self): self.conf.config(link_aggregate_prefix='Po', link_aggregate_range='5..10', group=self.device) mock_get_result = mock.Mock() mock_get_result.data_xml = XML_AGGREGATE_IFACES mock_client_locked = mock.Mock() mock_client_locked.get.return_value = mock_get_result used_aggregate_ids = {'Po5', 'Po7', 'Po9'} all_aggregate_ids = self.client.get_aggregation_ids() result = self.client.get_free_aggregate_id(mock_client_locked) self.assertNotIn(result, used_aggregate_ids) self.assertIn(result, all_aggregate_ids) class TestNetconfOpenConfigDriver(base.TestCase): def setUp(self): super(TestNetconfOpenConfigDriver, self).setUp() self.device = 'foo' self.conf = self.useFixture(config_fixture.Config()) self.conf.register_opts(config._opts + config._device_opts, group='foo') self.conf.register_opts((openconfig._DEVICE_OPTS + openconfig._NCCLIENT_OPTS), group='foo') self.conf.config(enabled_devices=['foo'], group='networking_baremetal') self.conf.config(driver='test-driver', switch_id='aa:bb:cc:dd:ee:ff', switch_info='foo', physical_networks=['fake_physical_network'], device_params={'name': 'default'}, host='foo.example.com', key_filename='/test/test_key_file', username='foo_user', group='foo') mock_client = mock.patch.object(openconfig, 'NetconfOpenConfigClient', autospec=True) self.mock_client = mock_client.start() self.addCleanup(mock_client.stop) self.driver = openconfig.NetconfOpenConfigDriver(self.device) self.mock_client.assert_called_once_with('foo') self.mock_client.reset_mock() def test_validate(self): self.driver.validate() self.driver.client.get_capabilities.assert_called_once_with() @mock.patch.object(openconfig, 'CONF', autospec=True) def test_load_config(self, mock_conf): self.driver.load_config() mock_conf.register_opts.assert_has_calls( [mock.call(openconfig._DEVICE_OPTS, group=self.driver.device), mock.call(openconfig._NCCLIENT_OPTS, group=self.driver.device)]) def test_create_network(self): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network() self.driver.create_network(m_nc) net_instances = self.driver.client.edit_config.call_args[0][0] for net_instance in net_instances: self.assertEqual(net_instance.name, 'default') vlans = net_instance.vlans for vlan in vlans: self.assertEqual(vlan.config.operation, nc_op.MERGE.value) self.assertEqual(vlan.config.name, self.driver._uuid_as_hex(m_nc.current['id'])) self.assertEqual(vlan.config.status, constants.VLAN_ACTIVE) def test_update_network_no_changes(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN) m_nc.original = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN) self.assertEqual(m_nc.current, m_nc.original) self.driver.update_network(m_nc) self.driver.client.edit_config.assert_not_called() def test_update_network_change_vlan_id(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=10) m_nc.original = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=20) self.driver.update_network(m_nc) call_args_list = self.driver.client.edit_config.call_args_list del_net_instances = call_args_list[0][0][0] add_net_instances = call_args_list[1][0][0] self.driver.client.edit_config.assert_has_calls( [mock.call(del_net_instances), mock.call(add_net_instances)]) for net_instance in del_net_instances: self.assertEqual(net_instance.name, 'default') for vlan in net_instance.vlans: self.assertEqual(vlan.operation, nc_op.REMOVE.value) self.assertEqual(vlan.vlan_id, 20) self.assertEqual(vlan.config.status, constants.VLAN_SUSPENDED) self.assertEqual(vlan.config.name, 'neutron-DELETED-20') for net_instance in add_net_instances: self.assertEqual(net_instance.name, 'default') for vlan in net_instance.vlans: self.assertEqual(vlan.operation, nc_op.MERGE.value) self.assertEqual(vlan.config.name, self.driver._uuid_as_hex(network_id)) self.assertEqual(vlan.vlan_id, 10) def test_update_network_change_admin_state(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=10, admin_state_up=False) m_nc.original = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=10, admin_state_up=True) self.driver.update_network(m_nc) call_args_list = self.driver.client.edit_config.call_args_list add_net_instances = call_args_list[0][0][0] self.driver.client.edit_config.assert_called_once_with( add_net_instances) for net_instance in add_net_instances: self.assertEqual(net_instance.name, 'default') for vlan in net_instance.vlans: self.assertEqual(vlan.operation, nc_op.MERGE.value) self.assertEqual(vlan.config.status, constants.VLAN_SUSPENDED) self.assertEqual(vlan.config.name, self.driver._uuid_as_hex(network_id)) self.assertEqual(vlan.vlan_id, 10) def test_delete_network(self): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN, segmentation_id=15) self.driver.delete_network(m_nc) self.driver.client.edit_config.assert_called_once() call_args_list = self.driver.client.edit_config.call_args_list net_instances = call_args_list[0][0][0] for net_instance in net_instances: self.assertEqual(net_instance.name, 'default') for vlan in net_instance.vlans: self.assertEqual(vlan.operation, nc_op.REMOVE.value) self.assertEqual(vlan.vlan_id, 15) self.assertEqual(vlan.config.status, constants.VLAN_SUSPENDED) self.assertEqual(vlan.config.name, 'neutron-DELETED-15') def test_create_port_vlan(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=15) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id) m_pc.network = m_nc segment = { api.ID: uuidutils.generate_uuid(), api.PHYSICAL_NETWORK: m_nc.current['provider:physical_network'], api.NETWORK_TYPE: m_nc.current['provider:network_type'], api.SEGMENTATION_ID: m_nc.current['provider:segmentation_id']} links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.create_port(m_pc, segment, links) self.driver.client.edit_config.assert_called_once() call_args_list = self.driver.client.edit_config.call_args_list ifaces = call_args_list[0][0][0] for iface in ifaces: self.assertEqual(iface.name, links[0]['port_id']) self.assertEqual(iface.config.enabled, m_pc.current['admin_state_up']) self.assertEqual(iface.config.mtu, m_nc.current[api.MTU]) self.assertEqual(iface.config.description, f'neutron-{m_pc.current[api.ID]}') self.assertEqual(iface.ethernet.switched_vlan.config.operation, nc_op.REPLACE.value) self.assertEqual( iface.ethernet.switched_vlan.config.interface_mode, constants.VLAN_MODE_ACCESS) self.assertEqual( iface.ethernet.switched_vlan.config.access_vlan, segment[api.SEGMENTATION_ID]) def test_create_port_flat(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_FLAT) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id) m_pc.network = m_nc segment = { api.ID: uuidutils.generate_uuid(), api.PHYSICAL_NETWORK: m_nc.current['provider:physical_network'], api.NETWORK_TYPE: m_nc.current['provider:network_type']} links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.create_port(m_pc, segment, links) self.driver.client.edit_config.assert_called_once() call_args_list = self.driver.client.edit_config.call_args_list ifaces = call_args_list[0][0][0] for iface in ifaces: self.assertEqual(iface.name, links[0]['port_id']) self.assertEqual(iface.config.enabled, m_pc.current['admin_state_up']) self.assertEqual(iface.config.mtu, m_nc.current[api.MTU]) self.assertEqual(iface.config.description, f'neutron-{m_pc.current[api.ID]}') self.assertIsNone(iface.ethernet) def test_update_port(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=15, mtu=9000) m_nc.original = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=15, mtu=1500) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, admin_state_up=False) m_pc.original = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, admin_state_up=True) m_pc.network = m_nc links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.update_port(m_pc, links) self.driver.client.edit_config.assert_called_once() call_args_list = self.driver.client.edit_config.call_args_list ifaces = call_args_list[0][0][0] for iface in ifaces: self.assertEqual(iface.name, links[0]['port_id']) self.assertEqual(iface.config.enabled, m_pc.current['admin_state_up']) self.assertEqual(iface.config.mtu, m_nc.current[api.MTU]) self.assertIsNone(iface.ethernet) def test_update_port_no_supported_attrib_changed(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=15) m_nc.original = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=15) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, name='current') m_pc.original = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, name='original') m_pc.network = m_nc links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.update_port(m_pc, links) self.driver.client.edit_config.assert_not_called() def test_delete_port_vlan(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=15) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id) m_pc.network = m_nc links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.delete_port(m_pc, links) self.driver.client.edit_config.assert_called_once() call_args_list = self.driver.client.edit_config.call_args_list ifaces = call_args_list[0][0][0] for iface in ifaces: self.assertEqual(iface.name, links[0]['port_id']) self.assertEqual(iface.config.operation, nc_op.REMOVE.value) self.assertEqual(iface.config.description, '') self.assertFalse(iface.config.enabled) self.assertEqual(iface.config.mtu, 0) self.assertEqual(iface.ethernet.switched_vlan.config.operation, nc_op.REMOVE.value) def test_delete_port_flat(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_FLAT) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id) m_pc.network = m_nc links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.delete_port(m_pc, links) self.driver.client.edit_config.assert_called_once() call_args_list = self.driver.client.edit_config.call_args_list ifaces = call_args_list[0][0][0] for iface in ifaces: self.assertEqual(iface.name, links[0]['port_id']) self.assertEqual(iface.config.operation, nc_op.REMOVE.value) self.assertEqual(iface.config.description, '') self.assertFalse(iface.config.enabled) self.assertEqual(iface.config.mtu, 0) self.assertIsNone(iface.ethernet) def test_create_lacp_port_flat(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) binding_profile = { constants.LOCAL_LINK_INFO: [ {'port_id': 'foo1/1', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}, {'port_id': 'foo1/2', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}], constants.LOCAL_GROUP_INFO: { 'id': uuidutils.generate_uuid(), 'name': 'PortGroup1', 'bond_mode': '802.3ad', 'bond_properties': { constants.LACP_INTERVAL: 'fast', constants.LACP_MIN_LINKS: 2} } } m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_FLAT) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, binding_profile=binding_profile) m_pc.network = m_nc segment = { api.ID: uuidutils.generate_uuid(), api.PHYSICAL_NETWORK: m_nc.current['provider:physical_network'], api.NETWORK_TYPE: m_nc.current['provider:network_type']} links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.create_port(m_pc, segment, links) self.driver.client.edit_config.assert_called_once_with( [mock.ANY, mock.ANY], deferred_allocations=True) call_args_list = self.driver.client.edit_config.call_args_list ifaces = list(call_args_list[0][0][0][0]) _lacp = call_args_list[0][0][0][1] lacp_iface = list(_lacp.interfaces)[0] if_link_a = ifaces[0] if_link_b = ifaces[1] if_agg = ifaces[2] assert isinstance(if_link_a, interfaces.InterfaceEthernet) assert isinstance(if_link_b, interfaces.InterfaceEthernet) assert isinstance(if_agg, interfaces.InterfaceAggregate) assert isinstance(lacp_iface, lacp.LACPInterface) self.assertEqual(if_link_a.name, 'foo1/1') self.assertEqual(if_link_b.name, 'foo1/2') self.assertEqual(if_agg.name, openconfig.DEFERRED) for iface in (if_link_a, if_link_b): self.assertEqual(iface.config.operation, nc_op.MERGE.value) self.assertEqual(iface.config.enabled, m_pc.current['admin_state_up']) self.assertEqual(iface.config.mtu, m_nc.current['mtu']) self.assertEqual(iface.config.description, f'neutron-{m_pc.current[api.ID]}') self.assertEqual(iface.ethernet.config.aggregate_id, openconfig.DEFERRED) self.assertEqual(if_agg.name, openconfig.DEFERRED) self.assertEqual(if_agg.config.name, openconfig.DEFERRED) self.assertEqual(if_agg.config.operation, nc_op.MERGE.value) self.assertEqual(if_agg.config.description, f'neutron-{m_pc.current[api.ID]}') self.assertEqual(if_agg.aggregation.config.lag_type, constants.LAG_TYPE_LACP) self.assertEqual(if_agg.aggregation.config.min_links, binding_profile[ constants.LOCAL_GROUP_INFO]['bond_properties'][ constants.LACP_MIN_LINKS]) self.assertIsNone(if_agg.aggregation.switched_vlan) self.assertEqual(lacp_iface.name, openconfig.DEFERRED) self.assertEqual(lacp_iface.operation, nc_op.REPLACE.value) self.assertEqual(lacp_iface.config.name, openconfig.DEFERRED) self.assertEqual(lacp_iface.config.interval, constants.LACP_PERIOD_FAST) def test_create_lacp_port_vlan(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) binding_profile = { constants.LOCAL_LINK_INFO: [ {'port_id': 'foo1/1', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}, {'port_id': 'foo1/2', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}], constants.LOCAL_GROUP_INFO: { 'id': uuidutils.generate_uuid(), 'name': 'PortGroup1', 'bond_mode': '802.3ad', 'bond_properties': { constants.LACP_INTERVAL: 'fast', constants.LACP_MIN_LINKS: 2} } } m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=40) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, binding_profile=binding_profile) m_pc.network = m_nc segment = { api.ID: uuidutils.generate_uuid(), api.PHYSICAL_NETWORK: m_nc.current['provider:physical_network'], api.NETWORK_TYPE: m_nc.current['provider:network_type'], api.SEGMENTATION_ID: m_nc.current['provider:segmentation_id']} links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.create_port(m_pc, segment, links) self.driver.client.edit_config.assert_called_once_with( [mock.ANY, mock.ANY], deferred_allocations=True) call_args_list = self.driver.client.edit_config.call_args_list ifaces = list(call_args_list[0][0][0][0]) _lacp = call_args_list[0][0][0][1] lacp_iface = list(_lacp.interfaces)[0] if_link_a = ifaces[0] if_link_b = ifaces[1] if_agg = ifaces[2] assert isinstance(if_link_a, interfaces.InterfaceEthernet) assert isinstance(if_link_b, interfaces.InterfaceEthernet) assert isinstance(if_agg, interfaces.InterfaceAggregate) assert isinstance(lacp_iface, lacp.LACPInterface) self.assertEqual(if_link_a.name, 'foo1/1') self.assertEqual(if_link_b.name, 'foo1/2') self.assertEqual(if_agg.name, openconfig.DEFERRED) for iface in (if_link_a, if_link_b): self.assertEqual(iface.config.operation, nc_op.MERGE.value) self.assertEqual(iface.config.enabled, m_pc.current['admin_state_up']) self.assertEqual(iface.config.mtu, m_nc.current['mtu']) self.assertEqual(iface.config.description, f'neutron-{m_pc.current[api.ID]}') self.assertEqual(iface.ethernet.config.aggregate_id, openconfig.DEFERRED) self.assertEqual(if_agg.name, openconfig.DEFERRED) self.assertEqual(if_agg.config.name, openconfig.DEFERRED) self.assertEqual(if_agg.config.operation, nc_op.MERGE.value) self.assertEqual(if_agg.config.description, f'neutron-{m_pc.current[api.ID]}') self.assertEqual(if_agg.aggregation.config.lag_type, constants.LAG_TYPE_LACP) self.assertEqual(if_agg.aggregation.config.min_links, binding_profile[ constants.LOCAL_GROUP_INFO]['bond_properties'][ constants.LACP_MIN_LINKS]) self.assertEqual(if_agg.aggregation.switched_vlan.config.operation, nc_op.REPLACE.value) self.assertEqual( if_agg.aggregation.switched_vlan.config.interface_mode, constants.VLAN_MODE_ACCESS) self.assertEqual(if_agg.aggregation.switched_vlan.config.access_vlan, segment[api.SEGMENTATION_ID]) self.assertEqual(lacp_iface.name, openconfig.DEFERRED) self.assertEqual(lacp_iface.operation, nc_op.REPLACE.value) self.assertEqual(lacp_iface.config.name, openconfig.DEFERRED) self.assertEqual(lacp_iface.config.interval, constants.LACP_PERIOD_FAST) def test_update_lacp_port(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) binding_profile = { constants.LOCAL_LINK_INFO: [ {'port_id': 'foo1/1', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}, {'port_id': 'foo1/2', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}], constants.LOCAL_GROUP_INFO: { 'id': uuidutils.generate_uuid(), 'name': 'PortGroup1', 'bond_mode': '802.3ad', 'bond_properties': { constants.LACP_INTERVAL: 'fast', constants.LACP_MIN_LINKS: 2} } } m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=15, mtu=9000) m_nc.original = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=15, mtu=1500) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, admin_state_up=False, binding_profile=binding_profile) m_pc.original = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, admin_state_up=True, binding_profile=binding_profile) m_pc.network = m_nc links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.client.get.return_value = XML_IFACES_AGGREDATE_ID self.driver.update_port(m_pc, links) self.driver.client.get.assert_called_once() self.driver.client.edit_config.assert_called_once() # Validate the query to get aggregate id query_call_args = self.driver.client.get.call_args ifaces = query_call_args[1]['query'] iface_a = list(ifaces)[0] iface_b = list(ifaces)[1] self.assertEqual(iface_a.name, 'foo1/1') self.assertEqual(iface_b.name, 'foo1/2') self.assertIsNone(iface_a.config) self.assertIsNone(iface_a.ethernet) self.assertIsNone(iface_b.config) self.assertIsNone(iface_b.ethernet) # Validate the edit config call edit_call_args_list = self.driver.client.edit_config.call_args_list ifaces = list(edit_call_args_list[0][0][0]) if_link_a = ifaces[0] if_link_b = ifaces[1] if_agg = ifaces[2] assert isinstance(if_link_a, interfaces.InterfaceEthernet) assert isinstance(if_link_b, interfaces.InterfaceEthernet) assert isinstance(if_agg, interfaces.InterfaceAggregate) self.assertEqual(if_link_a.name, 'foo1/1') self.assertEqual(if_link_b.name, 'foo1/2') self.assertEqual(if_agg.name, 'Po10') for iface in (if_link_a, if_link_b): self.assertEqual(iface.config.operation, nc_op.MERGE.value) self.assertEqual(False, iface.config.enabled) self.assertEqual(9000, iface.config.mtu) self.assertEqual(if_agg.name, 'Po10') self.assertEqual(if_agg.operation, nc_op.MERGE.value) self.assertEqual(False, if_agg.config.enabled) def test_delete_lacp_port_flat(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) binding_profile = { constants.LOCAL_LINK_INFO: [ {'port_id': 'foo1/1', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}, {'port_id': 'foo1/2', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}], constants.LOCAL_GROUP_INFO: { 'id': uuidutils.generate_uuid(), 'name': 'PortGroup1', 'bond_mode': '802.3ad', 'bond_properties': { constants.LACP_INTERVAL: 'fast', constants.LACP_MIN_LINKS: 2} } } m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_FLAT) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, binding_profile=binding_profile) m_pc.network = m_nc links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.client.get.return_value = XML_IFACES_AGGREDATE_ID self.driver.delete_port(m_pc, links) self.driver.client.get.assert_called_once_with(query=mock.ANY) self.driver.client.edit_config.assert_called_once_with([mock.ANY, mock.ANY]) # Validate the query to get aggregate id query_call_args = self.driver.client.get.call_args ifaces = query_call_args[1]['query'] iface_a = list(ifaces)[0] iface_b = list(ifaces)[1] self.assertEqual(iface_a.name, 'foo1/1') self.assertEqual(iface_b.name, 'foo1/2') self.assertIsNone(iface_a.config) self.assertIsNone(iface_a.ethernet) self.assertIsNone(iface_b.config) self.assertIsNone(iface_b.ethernet) # Validate the edit config call edit_call_args_list = self.driver.client.edit_config.call_args_list _lacp = edit_call_args_list[0][0][0][0] ifaces = list(edit_call_args_list[0][0][0][1]) lacp_iface = list(_lacp.interfaces)[0] if_link_a = ifaces[0] if_link_b = ifaces[1] if_agg = ifaces[2] assert isinstance(if_link_a, interfaces.InterfaceEthernet) assert isinstance(if_link_b, interfaces.InterfaceEthernet) assert isinstance(if_agg, interfaces.InterfaceAggregate) assert isinstance(lacp_iface, lacp.LACPInterface) self.assertEqual(if_link_a.name, 'foo1/1') self.assertEqual(if_link_b.name, 'foo1/2') self.assertEqual(if_agg.name, 'Po10') for iface in (if_link_a, if_link_b): self.assertEqual(iface.config.operation, nc_op.REMOVE.value) self.assertEqual(iface.config.enabled, False) self.assertEqual(iface.config.mtu, 0) self.assertEqual(iface.config.description, '') self.assertIsNone(iface.ethernet.switched_vlan) self.assertIsNone(iface.ethernet.config.aggregate_id) self.assertEqual(iface.ethernet.config.operation, nc_op.REMOVE.value) self.assertEqual(if_agg.name, 'Po10') self.assertEqual(if_agg.operation, nc_op.REMOVE.value) self.assertIsNone(if_agg.config) self.assertIsNone(if_agg.aggregation) self.assertEqual(lacp_iface.name, 'Po10') self.assertEqual(lacp_iface.operation, nc_op.REMOVE.value) self.assertIsNone(lacp_iface.config) def test_delete_lacp_port_vlan(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) binding_profile = { constants.LOCAL_LINK_INFO: [ {'port_id': 'foo1/1', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}, {'port_id': 'foo1/2', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}], constants.LOCAL_GROUP_INFO: { 'id': uuidutils.generate_uuid(), 'name': 'PortGroup1', 'bond_mode': '802.3ad', 'bond_properties': { constants.LACP_INTERVAL: 'fast', constants.LACP_MIN_LINKS: 2} } } m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=40) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, binding_profile=binding_profile) m_pc.network = m_nc links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.client.get.return_value = XML_IFACES_AGGREDATE_ID self.driver.delete_port(m_pc, links) self.driver.client.get.assert_called_once_with(query=mock.ANY) self.driver.client.edit_config.assert_called_once_with([mock.ANY, mock.ANY]) # Validate the query to get aggregate id query_call_args = self.driver.client.get.call_args ifaces = query_call_args[1]['query'] iface_a = list(ifaces)[0] iface_b = list(ifaces)[1] self.assertEqual(iface_a.name, 'foo1/1') self.assertEqual(iface_b.name, 'foo1/2') self.assertIsNone(iface_a.config) self.assertIsNone(iface_a.ethernet) self.assertIsNone(iface_b.config) self.assertIsNone(iface_b.ethernet) # Validate the edit config call edit_call_args_list = self.driver.client.edit_config.call_args_list _lacp = edit_call_args_list[0][0][0][0] ifaces = list(edit_call_args_list[0][0][0][1]) lacp_iface = list(_lacp.interfaces)[0] if_link_a = ifaces[0] if_link_b = ifaces[1] if_agg = ifaces[2] assert isinstance(if_link_a, interfaces.InterfaceEthernet) assert isinstance(if_link_b, interfaces.InterfaceEthernet) assert isinstance(if_agg, interfaces.InterfaceAggregate) assert isinstance(lacp_iface, lacp.LACPInterface) self.assertEqual(if_link_a.name, 'foo1/1') self.assertEqual(if_link_b.name, 'foo1/2') self.assertEqual(if_agg.name, 'Po10') for iface in (if_link_a, if_link_b): self.assertEqual(iface.config.operation, nc_op.REMOVE.value) self.assertEqual(iface.config.enabled, False) self.assertEqual(iface.config.mtu, 0) self.assertEqual(iface.config.description, '') self.assertEqual(iface.ethernet.switched_vlan.config.operation, nc_op.REMOVE.value) self.assertIsNone(iface.ethernet.config.aggregate_id) self.assertEqual(if_agg.name, 'Po10') self.assertEqual(if_agg.operation, nc_op.REMOVE.value) self.assertIsNone(if_agg.config) self.assertIsNone(if_agg.aggregation) self.assertEqual(lacp_iface.name, 'Po10') self.assertEqual(lacp_iface.operation, nc_op.REMOVE.value) self.assertIsNone(lacp_iface.config) def test_create_pre_configured_aggregate_port_vlan(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) binding_profile = { constants.LOCAL_LINK_INFO: [ {'port_id': 'foo1/1', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}, {'port_id': 'foo1/2', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}], constants.LOCAL_GROUP_INFO: { 'id': uuidutils.generate_uuid(), 'name': 'PortGroup1', 'bond_mode': 'balance-rr', } } m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=40) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, binding_profile=binding_profile) m_pc.network = m_nc segment = { api.ID: uuidutils.generate_uuid(), api.PHYSICAL_NETWORK: m_nc.current['provider:physical_network'], api.NETWORK_TYPE: m_nc.current['provider:network_type'], api.SEGMENTATION_ID: m_nc.current['provider:segmentation_id']} links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.client.get.return_value = XML_IFACES_AGGREDATE_ID self.driver.create_port(m_pc, segment, links) self.driver.client.get.assert_called_once() self.driver.client.edit_config.assert_called_once() # Validate the query to get aggregate id query_call_args = self.driver.client.get.call_args ifaces = query_call_args[1]['query'] iface_a = list(ifaces)[0] iface_b = list(ifaces)[1] self.assertEqual(iface_a.name, 'foo1/1') self.assertEqual(iface_b.name, 'foo1/2') self.assertIsNone(iface_a.config) self.assertIsNone(iface_a.ethernet) self.assertIsNone(iface_b.config) self.assertIsNone(iface_b.ethernet) # Validate the edit config call edit_call_args_list = self.driver.client.edit_config.call_args_list ifaces = list(edit_call_args_list[0][0][0]) if_agg = ifaces[0] assert isinstance(if_agg, interfaces.InterfaceAggregate) self.assertEqual(if_agg.name, 'Po10') self.assertEqual(if_agg.operation, nc_op.MERGE.value) self.assertEqual(True, if_agg.config.enabled) self.assertEqual(if_agg.aggregation.switched_vlan.config.operation, nc_op.REPLACE.value) self.assertEqual( if_agg.aggregation.switched_vlan.config.interface_mode, constants.VLAN_MODE_ACCESS) self.assertEqual(if_agg.aggregation.switched_vlan.config.access_vlan, segment[api.SEGMENTATION_ID]) def test_update_pre_configured_aggregate_port_vlan(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) binding_profile = { constants.LOCAL_LINK_INFO: [ {'port_id': 'foo1/1', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}, {'port_id': 'foo1/2', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}], constants.LOCAL_GROUP_INFO: { 'id': uuidutils.generate_uuid(), 'name': 'PortGroup1', 'bond_mode': 'balance-rr', } } m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=15) m_nc.original = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=15) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, admin_state_up=False, binding_profile=binding_profile) m_pc.original = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, admin_state_up=True, binding_profile=binding_profile) m_pc.network = m_nc links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.client.get.return_value = XML_IFACES_AGGREDATE_ID self.driver.update_port(m_pc, links) self.driver.client.get.assert_called_once() self.driver.client.edit_config.assert_called_once() # Validate the edit config call edit_call_args_list = self.driver.client.edit_config.call_args_list ifaces = list(edit_call_args_list[0][0][0]) if_agg = ifaces[0] assert isinstance(if_agg, interfaces.InterfaceAggregate) self.assertEqual(if_agg.name, 'Po10') self.assertEqual(if_agg.operation, nc_op.MERGE.value) self.assertEqual(False, if_agg.config.enabled) def test_delete_pre_configured_aggregate_port_vlan(self): tenant_id = uuidutils.generate_uuid() network_id = uuidutils.generate_uuid() project_id = uuidutils.generate_uuid() m_nc = mock.create_autospec(driver_context.NetworkContext) m_pc = mock.create_autospec(driver_context.PortContext) binding_profile = { constants.LOCAL_LINK_INFO: [ {'port_id': 'foo1/1', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}, {'port_id': 'foo1/2', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}], constants.LOCAL_GROUP_INFO: { 'id': uuidutils.generate_uuid(), 'name': 'PortGroup1', 'bond_mode': 'balance-rr', } } m_nc.current = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=15) m_nc.original = ml2_utils.get_test_network( id=network_id, tenant_id=tenant_id, project_id=project_id, network_type=n_const.TYPE_VLAN, segmentation_id=15) m_pc.current = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, admin_state_up=False, binding_profile=binding_profile) m_pc.original = ml2_utils.get_test_port( network_id=network_id, tenant_id=tenant_id, project_id=project_id, admin_state_up=True, binding_profile=binding_profile) m_pc.network = m_nc links = m_pc.current['binding:profile'][constants.LOCAL_LINK_INFO] self.driver.client.get.return_value = XML_IFACES_AGGREDATE_ID self.driver.delete_port(m_pc, links) self.driver.client.get.assert_called_once() self.driver.client.edit_config.assert_called_once() # Validate the edit config call edit_call_args_list = self.driver.client.edit_config.call_args_list ifaces = list(edit_call_args_list[0][0][0]) if_agg = ifaces[0] assert isinstance(if_agg, interfaces.InterfaceAggregate) self.assertEqual(if_agg.name, 'Po10') self.assertEqual(if_agg.operation, nc_op.MERGE.value) self.assertEqual(False, if_agg.config.enabled) self.assertEqual(if_agg.aggregation.switched_vlan.config.operation, nc_op.REMOVE.value) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1773930567.842996 networking_baremetal-7.2.0/networking_baremetal/tests/unit/ironic_agent/0000775000175000017500000000000015157004110025524 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/ironic_agent/__init__.py0000664000175000017500000000000015157004031027625 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000020700000000000010214 xustar00113 path=networking_baremetal-7.2.0/networking_baremetal/tests/unit/ironic_agent/test_hashring_member_manager.py 22 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/ironic_agent/test_hashring_member_manager0000664000175000017500000000713115157004031033336 0ustar00zuulzuul# Copyright 2017 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 copy from unittest import mock from oslo_utils import timeutils from oslotest import base from networking_baremetal.agent import ironic_neutron_agent def fake_notification(): return (mock.Mock(), 'publisher_id', 'event_type', {'id': 'agent_id', 'host': 'agent_host', 'timestamp': timeutils.utcnow_ts()}, 'metadata') class TestHashRingMemberManagerNotificationEndpoint(base.BaseTestCase): def setUp(self): super(TestHashRingMemberManagerNotificationEndpoint, self).setUp() # Create instance without agent_id for backward compatibility tests self.member_manager = ( ironic_neutron_agent.HashRingMemberManagerNotificationEndpoint()) self.member_manager.members = [] self.old_timestamp = 1517874977 @mock.patch.object(ironic_neutron_agent.LOG, 'info', autospec=True) def test_notification_info_add_new_agent(self, mock_log): self.member_manager.hashring = mock.Mock() ctxt, publisher_id, event_type, payload, metadata = fake_notification() self.member_manager.info(ctxt, publisher_id, event_type, payload, metadata) self.member_manager.hashring.add_node.assert_called_with(payload['id']) self.assertEqual(payload, self.member_manager.members[0]) self.assertEqual(1, mock_log.call_count) def test_notification_info_update_timestamp(self): self.member_manager.hashring = mock.Mock() ctxt, publisher_id, event_type, payload, metadata = fake_notification() # Set an old timestamp, and insert into members payload['timestamp'] = self.old_timestamp self.member_manager.members.append(copy.deepcopy(payload)) # Reset timestamp, and simulate notification, add_node not called # Timestamp in member manager is updated. payload['timestamp'] = timeutils.utcnow_ts() self.assertNotEqual(payload['timestamp'], self.member_manager.members[0]['timestamp']) self.member_manager.info(ctxt, publisher_id, event_type, payload, metadata) self.member_manager.hashring.add_node.assert_not_called() self.assertEqual(payload['timestamp'], self.member_manager.members[0]['timestamp']) @mock.patch.object(ironic_neutron_agent.LOG, 'info', autospec=True) def test_remove_old_members(self, mock_log): self.member_manager.hashring = mock.Mock() # Add a member with an old timestamp, it is removed. ctxt, publisher_id, event_type, payload, metadata = fake_notification() payload['timestamp'] = self.old_timestamp self.member_manager.info(ctxt, publisher_id, event_type, payload, metadata) self.member_manager.hashring.remove_node.assert_called_with( payload['id']) self.assertEqual(0, len(self.member_manager.members)) self.assertEqual(2, mock_log.call_count) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/ironic_agent/test_ironic_agent.py0000664000175000017500000006264615157004031031616 0ustar00zuulzuul# Copyright 2017 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 urllib import parse as urlparse from neutron.agent import rpc as agent_rpc from neutron.tests import base from neutron_lib import constants as n_const from openstack import connection from openstack import exceptions as sdk_exc from oslo_config import fixture as config_fixture from tooz import hashring from networking_baremetal.agent import ironic_neutron_agent from networking_baremetal import constants from networking_baremetal import ironic_client class FakePort1(object): def __init__(self, physnet='physnet1'): self.uuid = '11111111-2222-3333-4444-555555555555' self.node_id = '55555555-4444-3333-2222-111111111111' self.physical_network = physnet class FakePort2(object): def __init__(self, physnet='physnet2'): self.uuid = '11111111-aaaa-3333-4444-555555555555' self.node_id = '55555555-4444-3333-aaaa-111111111111' self.physical_network = physnet @mock.patch.object(ironic_client, '_get_ironic_session', autospec=True) @mock.patch.object(connection.Connection, 'baremetal', autospec=True) class TestBaremetalNeutronAgent(base.BaseTestCase): def setUp(self): super(TestBaremetalNeutronAgent, self).setUp() self.context = object() self.conf = self.useFixture(config_fixture.Config()) self.conf.config(transport_url='rabbit://user:password@host/') # Register agent config options (L2VNI and baremetal_agent) from networking_baremetal.agent import agent_config agent_config.register_agent_opts(self.conf.conf) # Disable L2VNI, HA alignment, and router HA binding for these tests self.conf.config(group='l2vni', enable_l2vni_trunk_reconciliation=False, enable_l2vni_trunk_reconciliation_events=False) self.conf.config(group='baremetal_agent', enable_ha_chassis_group_alignment=False, enable_router_ha_binding=False) def test_get_template_node_state(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() # Verify agent binary expected = constants.BAREMETAL_BINARY self.assertEqual(expected, self.agent.get_template_node_state( 'uuid')['binary']) # Verify agent_type is Baremetal Node expected = constants.BAREMETAL_AGENT_TYPE self.assertEqual(expected, self.agent.get_template_node_state( 'uuid')['agent_type']) # Verify topic expected = n_const.L2_AGENT_TOPIC self.assertEqual(expected, self.agent.get_template_node_state( 'uuid')['topic']) # Verify host expected = 'the_node_uuid' self.assertEqual(expected, self.agent.get_template_node_state( 'the_node_uuid')['host']) def test_report_state_one_node_one_port(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() with mock.patch.object(self.agent.state_rpc, 'report_state', autospec=True) as mock_report_state: self.agent.ironic_client = mock_conn mock_conn.ports.return_value = iter([FakePort1()]) self.agent.agent_id = 'agent_id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) expected = { 'topic': n_const.L2_AGENT_TOPIC, 'start_flag': True, 'binary': constants.BAREMETAL_BINARY, 'host': '55555555-4444-3333-2222-111111111111', 'configurations': { 'bridge_mappings': { 'physnet1': 'yes' }, 'log_agent_heartbeats': False, }, 'agent_type': constants.BAREMETAL_AGENT_TYPE, 'action': 'update' } self.agent._report_state() mock_report_state.assert_called_with(self.agent.context, expected) def test_report_state_with_log_agent_heartbeats(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() with mock.patch.object(self.agent.state_rpc, 'report_state', autospec=True) as mock_report_state: self.conf.config(log_agent_heartbeats=True, group='AGENT') self.agent.ironic_client = mock_conn mock_conn.ports.return_value = iter([FakePort1()]) self.agent.agent_id = 'agent_id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) expected = { 'topic': n_const.L2_AGENT_TOPIC, 'start_flag': True, 'binary': constants.BAREMETAL_BINARY, 'host': '55555555-4444-3333-2222-111111111111', 'configurations': { 'bridge_mappings': { 'physnet1': 'yes' }, 'log_agent_heartbeats': True, }, 'agent_type': constants.BAREMETAL_AGENT_TYPE, 'action': 'update' } self.agent._report_state() mock_report_state.assert_called_with(self.agent.context, expected) def test_start_flag_false_on_update_no_config_change(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() with mock.patch.object(self.agent.state_rpc, 'report_state', autospec=True) as mock_report_state: self.agent.ironic_client = mock_conn mock_conn.ports.return_value = iter([FakePort1()]) self.agent.agent_id = 'agent_id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) expected = { 'topic': n_const.L2_AGENT_TOPIC, 'start_flag': 'PLACEHOLDER', 'binary': constants.BAREMETAL_BINARY, 'host': '55555555-4444-3333-2222-111111111111', 'configurations': { 'bridge_mappings': { 'physnet1': 'yes' }, 'log_agent_heartbeats': False, }, 'agent_type': constants.BAREMETAL_AGENT_TYPE, 'action': 'update' } # First time report start_flag is True expected.update({'start_flag': True}) self.agent._report_state() mock_report_state.assert_called_with(self.agent.context, expected) # Subsequent times report start_flag is False mock_conn.ports.return_value = iter([FakePort1()]) expected.update({'start_flag': False}) self.agent._report_state() mock_report_state.assert_called_with(self.agent.context, expected) def test_start_flag_true_on_update_after_config_change(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() with mock.patch.object(self.agent.state_rpc, 'report_state', autospec=True) as mock_report_state: self.agent.ironic_client = mock_conn mock_conn.ports.return_value = iter([FakePort1()]) self.agent.agent_id = 'agent_id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) expected = { 'topic': n_const.L2_AGENT_TOPIC, 'start_flag': 'PLACEHOLDER', 'binary': constants.BAREMETAL_BINARY, 'host': '55555555-4444-3333-2222-111111111111', 'configurations': { 'bridge_mappings': { 'physnet1': 'yes' }, 'log_agent_heartbeats': False, }, 'agent_type': constants.BAREMETAL_AGENT_TYPE, 'action': 'update' } # First time report start_flag is True expected.update({'start_flag': True}) self.agent._report_state() mock_report_state.assert_called_with(self.agent.context, expected) # Subsequent times report start_flag is False mock_conn.ports.return_value = iter([FakePort1()]) expected.update({'start_flag': False}) self.agent._report_state() mock_report_state.assert_called_with(self.agent.context, expected) # After bridge_mapping config change start_flag is True once mock_conn.ports.return_value = iter( [FakePort1(physnet='new_physnet')]) expected.update({'configurations': { 'bridge_mappings': {'new_physnet': 'yes'}, 'log_agent_heartbeats': False}}) expected.update({'start_flag': True}) self.agent._report_state() mock_report_state.assert_called_with(self.agent.context, expected) # Subsequent times report start_flag is False mock_conn.ports.return_value = iter( [FakePort1(physnet='new_physnet')]) expected.update({'start_flag': False}) self.agent._report_state() mock_report_state.assert_called_with(self.agent.context, expected) def test_report_state_two_nodes_two_ports(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() with mock.patch.object(self.agent.state_rpc, 'report_state', autospec=True) as mock_report_state: self.agent.ironic_client = mock_conn mock_conn.ports.return_value = iter([FakePort1(), FakePort2()]) self.agent.agent_id = 'agent_id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) expected1 = { 'topic': n_const.L2_AGENT_TOPIC, 'start_flag': True, 'binary': constants.BAREMETAL_BINARY, 'host': '55555555-4444-3333-2222-111111111111', 'configurations': { 'bridge_mappings': { 'physnet1': 'yes' }, 'log_agent_heartbeats': False, }, 'agent_type': constants.BAREMETAL_AGENT_TYPE, 'action': 'update' } expected2 = { 'topic': n_const.L2_AGENT_TOPIC, 'start_flag': True, 'binary': constants.BAREMETAL_BINARY, 'host': '55555555-4444-3333-aaaa-111111111111', 'configurations': { 'bridge_mappings': { 'physnet2': 'yes' }, 'log_agent_heartbeats': False, }, 'agent_type': constants.BAREMETAL_AGENT_TYPE, 'action': 'update' } self.agent._report_state() mock_report_state.assert_has_calls( [mock.call(self.agent.context, expected1), mock.call(self.agent.context, expected2)], any_order=True) def test_report_state_deleted_node(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() with (mock.patch.object(self.agent.state_rpc, 'report_state', autospec=True) as mock_report_state, mock.patch.object(self.agent.state_rpc, 'delete_agent', autospec=True) as mock_delete_agent): self.agent.ironic_client = mock_conn mock_conn.ports.return_value = iter([FakePort1()]) self.agent.agent_id = 'agent_id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) expected1 = { 'topic': n_const.L2_AGENT_TOPIC, 'start_flag': True, 'binary': constants.BAREMETAL_BINARY, 'host': '55555555-4444-3333-2222-111111111111', 'configurations': { 'bridge_mappings': { 'physnet1': 'yes' }, 'log_agent_heartbeats': False, }, 'agent_type': constants.BAREMETAL_AGENT_TYPE, 'action': 'update' } self.agent.reported_nodes = { '55555555-4444-3333-2222-111111111111': {}, '55555555-4444-3333-aaaa-111111111111': {}} self.agent._report_state() mock_report_state.assert_has_calls( [mock.call(self.agent.context, expected1)], any_order=True) mock_delete_agent.assert_has_calls( [mock.call(self.agent.context, host='55555555-4444-3333-aaaa-111111111111', agent_type=constants.BAREMETAL_AGENT_TYPE)], any_order=True) @mock.patch.object(ironic_client, 'get_client', autospec=True) @mock.patch.object(ironic_neutron_agent.LOG, 'exception', autospec=True) def test_ironic_port_list_fail(self, mock_log, mock_get_client, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() self.agent.ironic_client = mock_conn def mock_generator(details=None, conductor_groups=None): raise sdk_exc.OpenStackCloudException() yield mock_conn.ports.side_effect = mock_generator self.agent._report_state() self.assertEqual(1, mock_log.call_count) # Test initialization triggers the client call once # before _report_state is triggered, hence call # count below of 2. self.assertEqual(2, mock_get_client.call_count) @mock.patch.object(ironic_neutron_agent.BaremetalNeutronAgent, 'stop', autospec=True) @mock.patch.object(ironic_client, 'get_client', autospec=True) @mock.patch.object(ironic_neutron_agent.LOG, 'exception', autospec=True) def test_ironic_port_list_fail_breakage(self, mock_log, mock_get_client, mock_stop, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() self.agent.ironic_client = mock_conn mock_get_client.side_effect = Exception def mock_generator(details=None, conductor_groups=None): raise sdk_exc.OpenStackCloudException() yield mock_conn.ports.side_effect = mock_generator self.agent._report_state() self.assertEqual(1, mock_log.call_count) # Checking the count on stop to see if it is called, as # opposed to the get_client method as it is the exception # root cause. mock_stop.assert_called_once_with(mock.ANY, failure=True) @mock.patch.object(ironic_neutron_agent.LOG, 'exception', autospec=True) @mock.patch.object(agent_rpc, 'PluginReportStateAPI', autospec=True) def test_state_rpc_report_state_fail(self, mock_report_state, mock_log, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() self.agent.agent_id = 'agent_id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) self.agent.ironic_client = mock_conn self.agent.state_rpc = mock_report_state mock_conn.ports.return_value = iter([FakePort1(), FakePort2()]) mock_report_state.report_state.side_effect = Exception() self.agent._report_state() self.assertEqual(1, mock_log.call_count) @mock.patch.object(ironic_neutron_agent.BaremetalNeutronAgent, 'stop', autospec=True) @mock.patch.object(ironic_neutron_agent.LOG, 'exception', autospec=True) @mock.patch.object(agent_rpc, 'PluginReportStateAPI', autospec=True) def test_state_rpc_report_state_fail_attribute(self, mock_report_state, mock_log, mock_stop, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() self.agent.agent_id = 'agent_id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) self.agent.ironic_client = mock_conn self.agent.state_rpc = mock_report_state mock_conn.ports.return_value = iter([FakePort1(), FakePort2()]) mock_report_state.report_state.side_effect = AttributeError() self.agent._report_state() self.assertEqual(1, mock_log.call_count) mock_stop.assert_called_once_with(mock.ANY, failure=True) def test__get_notification_transport_url(self, mock_conn, mock_ir_client): self.assertEqual( 'rabbit://user:password@host/?amqp_auto_delete=true', ironic_neutron_agent._get_notification_transport_url()) self.conf.config(transport_url='rabbit://user:password@host:5672/') self.assertEqual( 'rabbit://user:password@host:5672/?amqp_auto_delete=true', ironic_neutron_agent._get_notification_transport_url()) self.conf.config(transport_url='rabbit://host:5672/') self.assertEqual( 'rabbit://host:5672/?amqp_auto_delete=true', ironic_neutron_agent._get_notification_transport_url()) self.conf.config(transport_url='rabbit://user:password@host/vhost') self.assertEqual( 'rabbit://user:password@host/vhost?amqp_auto_delete=true', ironic_neutron_agent._get_notification_transport_url()) self.conf.config( transport_url='rabbit://user:password@host/vhost?foo=bar') self.assertEqual( # NOTE(hjensas): Parse the url's when comparing, different versions # may sort the query different. urlparse.urlparse('rabbit://user:password@host/' 'vhost?foo=bar&amqp_auto_delete=true'), urlparse.urlparse( ironic_neutron_agent._get_notification_transport_url())) self.conf.config( transport_url=('rabbit://user:password@host/vhost?foo=bar&' 'amqp_auto_delete=false')) self.assertEqual( # NOTE(hjensas): Parse the url's when comparing, different versions # may sort the query different. urlparse.urlparse('rabbit://user:password@host' '/vhost?foo=bar&amqp_auto_delete=true'), urlparse.urlparse( ironic_neutron_agent._get_notification_transport_url())) def test__get_notification_transport_url_auto_delete_enabled( self, mock_conn, mock_ir_client): self.conf.config(amqp_auto_delete=True, group='oslo_messaging_rabbit') self.assertEqual( 'rabbit://user:password@host/', ironic_neutron_agent._get_notification_transport_url()) def test__get_down_agents(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() with mock.patch.object(self.agent.state_rpc, 'get_agents', return_value=[], autospec=True): self.assertEqual([], self.agent._get_down_agents()) with mock.patch.object(self.agent.state_rpc, 'get_agents', return_value=[{'host': 'deleted_host'}], autospec=True): self.assertEqual( [{'host': 'deleted_host'}], self.agent._get_down_agents()) def test__get_nodes_not_found(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() self.agent.ironic_client = mock_conn down_agents = [{'host': 'deleted_host'}, {'host': 'existing_host'}] expected = ['deleted_host'] mock_conn.get_node.side_effect = [ sdk_exc.NotFoundException(), mock.Mock()] self.assertEqual(expected, self.agent._get_nodes_not_found(down_agents)) def test__delete_agents(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() self.agent.agent_id = 'agent_id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) nodes_not_found = ['host0', 'host1'] calls = [ mock.call(self.agent.context, host=nodes_not_found[0], agent_type=constants.BAREMETAL_AGENT_TYPE), mock.call(self.agent.context, host=nodes_not_found[1], agent_type=constants.BAREMETAL_AGENT_TYPE) ] with mock.patch.object(self.agent.state_rpc, 'delete_agent', autospec=True) as mock_delete_agent: self.agent._delete_agents(nodes_not_found) mock_delete_agent.assert_has_calls(calls, any_order=True) def test_cleanup_stale_agents(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() with (mock.patch.object(self.agent.state_rpc, 'delete_agent', autospec=True) as mock_delete_agent, mock.patch.object(self.agent.state_rpc, 'get_agents', return_value=[{'host': 'deleted_host'}], autospec=True)): self.agent.ironic_client = mock_conn mock_conn.get_node.side_effect = sdk_exc.NotFoundException() self.agent.agent_id = 'agent_id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) self.agent.cleanup_stale_agents() kwargs = {'host': 'deleted_host', 'agent_type': constants.BAREMETAL_AGENT_TYPE} mock_delete_agent.assert_called_with(self.agent.context, **kwargs) def test_report_state_with_conductor_groups(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() with mock.patch.object(self.agent.state_rpc, 'report_state', autospec=True): self.conf.config(conductor_groups=['group1', 'group2'], group='conductor_groups') self.agent.ironic_client = mock_conn mock_conn.ports.return_value = iter([FakePort1()]) self.agent.agent_id = 'agent_id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) self.agent._report_state() # Verify conductor_groups parameter was passed correctly mock_conn.ports.assert_called_once_with( details=True, conductor_groups=['group1', 'group2']) def test_report_state_with_empty_conductor_groups(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() with mock.patch.object(self.agent.state_rpc, 'report_state', autospec=True): self.conf.config(conductor_groups=[], group='conductor_groups') self.agent.ironic_client = mock_conn mock_conn.ports.return_value = iter([FakePort1()]) self.agent.agent_id = 'agent_id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) self.agent._report_state() # Verify empty list is passed (should query all ports) mock_conn.ports.assert_called_once_with( details=True, conductor_groups=[]) def test_report_state_without_conductor_groups_config(self, mock_conn, mock_ir_client): self.agent = ironic_neutron_agent.BaremetalNeutronAgent() with mock.patch.object(self.agent.state_rpc, 'report_state', autospec=True): # Don't set conductor_groups config self.agent.ironic_client = mock_conn mock_conn.ports.return_value = iter([FakePort1()]) self.agent.agent_id = 'agent_id' self.agent.member_manager.hashring = hashring.HashRing( [self.agent.agent_id]) self.agent._report_state() # Verify empty list is passed when config is not set mock_conn.ports.assert_called_once_with( details=True, conductor_groups=[]) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1773930567.843996 networking_baremetal-7.2.0/networking_baremetal/tests/unit/openconfig/0000775000175000017500000000000015157004110025212 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/openconfig/__init__.py0000664000175000017500000000000015157004031027313 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/openconfig/test_interfaces.py0000664000175000017500000002221515157004031030752 0ustar00zuulzuul# 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 xml.etree import ElementTree from oslotest import base from networking_baremetal import constants from networking_baremetal.openconfig.interfaces import aggregate from networking_baremetal.openconfig.interfaces import ethernet from networking_baremetal.openconfig.interfaces import interfaces from networking_baremetal.openconfig.vlan import vlan class TestInterfaces(base.BaseTestCase): @mock.patch.object(ethernet, 'InterfacesEthernetConfig', autospec=True) @mock.patch.object(vlan, 'VlanSwitchedVlan', autospec=True) def test_interfaces_ethernet(self, mock_sw_vlan, mock_eth_conf): mock_sw_vlan.return_value.to_xml_element.return_value = ( ElementTree.Element('fake-switched-vlan')) mock_eth_conf.return_value.to_xml_element.return_value = ( ElementTree.Element('fake-ethernet-config')) if_eth = ethernet.InterfacesEthernet() mock_sw_vlan.assert_called_with() element = if_eth.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = (f'' '' '' '') self.assertEqual(expected, xml_str) @mock.patch.object(aggregate, 'InterfacesAggregationConfig', autospec=True) @mock.patch.object(vlan, 'VlanSwitchedVlan', autospec=True) def test_interfaces_aggregate(self, mock_sw_vlan, mock_agg_conf): mock_sw_vlan.return_value.to_xml_element.return_value = ( ElementTree.Element('fake-switched-vlan')) mock_agg_conf.return_value.to_xml_element.return_value = ( ElementTree.Element('fake-aggregate-config')) if_aggregate = aggregate.InterfacesAggregation() mock_sw_vlan.assert_called_with() element = if_aggregate.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = (f'' '' '' '') self.assertEqual(expected, xml_str) @mock.patch.object(interfaces, 'InterfaceAggregate', autospec=True) @mock.patch.object(interfaces, 'InterfaceEthernet', autospec=True) def test_interfaces_interfaces(self, mock_iface_eth, mock_iface_aggregate): mock_iface_eth.return_value.to_xml_element.return_value = ( ElementTree.Element('fake-ethernet')) mock_iface_aggregate.return_value.to_xml_element.return_value = ( ElementTree.Element('fake-aggregate')) ifaces = interfaces.Interfaces() iface = ifaces.add('eth0/1') iface2 = ifaces.add('po10', interface_type='aggregate') mock_iface_eth.assert_called_with('eth0/1') mock_iface_aggregate.assert_called_with('po10') self.assertEqual([iface, iface2], ifaces.interfaces) element = ifaces.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = (f'' '' '' '') self.assertEqual(expected, xml_str) @mock.patch.object(ethernet, 'InterfacesEthernet', autospec=True) @mock.patch.object(interfaces, 'InterfaceConfig', autospec=True) def test_interfaces_interface_ethernet(self, mock_if_conf, mock_if_eth): mock_if_conf.return_value.to_xml_element.return_value = ( ElementTree.Element('fake_config')) mock_if_eth.return_value.to_xml_element.return_value = ( ElementTree.Element('fake_ethernet')) interface = interfaces.InterfaceEthernet('eth0/1') mock_if_conf.assert_called_with() mock_if_eth.assert_called_with() self.assertEqual('eth0/1', interface.name) self.assertEqual(mock_if_conf(), interface.config) self.assertEqual(mock_if_eth(), interface.ethernet) element = interface.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' 'eth0/1' '' '' '') self.assertEqual(expected, xml_str) not_string = 10 self.assertRaises(TypeError, interfaces.InterfaceEthernet, not_string) @mock.patch.object(aggregate, 'InterfacesAggregation', autospec=True) @mock.patch.object(interfaces, 'InterfaceConfig', autospec=True) def test_interfaces_interface_aggregate(self, mock_if_conf, mock_if_aggr): mock_if_conf.return_value.to_xml_element.return_value = ( ElementTree.Element('fake_config')) mock_if_aggr.return_value.to_xml_element.return_value = ( ElementTree.Element('fake_aggregation')) interface = interfaces.InterfaceAggregate('po10') mock_if_conf.assert_called_with() mock_if_aggr.assert_called_with() self.assertEqual('po10', interface.name) self.assertEqual(mock_if_conf(), interface.config) self.assertEqual(mock_if_aggr(), interface.aggregation) element = interface.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' 'po10' '' '' '') self.assertEqual(expected, xml_str) not_string = 10 self.assertRaises(TypeError, interfaces.InterfaceEthernet, not_string) def test_interfaces_interface_config(self): if_conf = interfaces.InterfaceConfig() self.assertEqual(constants.NetconfEditConfigOperation.MERGE.value, if_conf.operation) self.assertRaises(ValueError, interfaces.InterfaceConfig, **dict(operation='invalid')) self.assertRaises(TypeError, interfaces.InterfaceConfig, **dict(enabled='not_bool')) self.assertRaises(TypeError, interfaces.InterfaceConfig, **dict(description=10)) # Not string self.assertRaises(TypeError, interfaces.InterfaceConfig, **dict(mtu='not_int')) if_conf.name = 'test1' if_conf.enabled = True if_conf.description = 'Description' if_conf.mtu = 9000 element = if_conf.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' 'test1' 'Description' 'true' '9000' '') self.assertEqual(expected, xml_str) del if_conf.name if_conf.operation = 'remove' if_conf.description = '' if_conf.mtu = 0 if_conf.enabled = False element = if_conf.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' '' 'false' '0' '') self.assertEqual(expected, xml_str) def test_interfaces_interface_ethernet_config(self): eth_conf = ethernet.InterfacesEthernetConfig() self.assertEqual(constants.NetconfEditConfigOperation.MERGE.value, eth_conf.operation) self.assertRaises(ValueError, ethernet.InterfacesEthernetConfig, **dict(operation='invalid')) eth_conf.aggregate_id = 'po100' element = eth_conf.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' 'po100' '') self.assertEqual(expected, xml_str) eth_conf = ethernet.InterfacesEthernetConfig() eth_conf.operation = 'remove' element = eth_conf.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = '' self.assertEqual(expected, xml_str) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/openconfig/test_lacp.py0000664000175000017500000001154315157004031027550 0ustar00zuulzuul# 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 xml.etree import ElementTree from oslotest import base from networking_baremetal import constants from networking_baremetal.openconfig.lacp import lacp class TestOpenConfigLACP(base.BaseTestCase): @mock.patch.object(lacp, 'LACPInterfaces', autospec=True) def test_openconfig_lacp(self, mock_lcap_ifaces): mock_lcap_ifaces.return_value.to_xml_element.return_value = ( ElementTree.Element('fake-lacp-interfaces')) oc_lacp = lacp.LACP() mock_lcap_ifaces.assert_called_with() mock_lcap_ifaces.return_value.__len__.return_value = 1 element = oc_lacp.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = (f'' '' '') self.assertEqual(expected, xml_str) @mock.patch.object(lacp, 'LACPInterface', autospec=True) def test_openconfig_lacp_interfaces(self, mock_lacp_iface): mock_lacp_iface.return_value.to_xml_element.return_value = ( ElementTree.Element('fake-lacp-interface')) oc_lacp_ifaces = lacp.LACPInterfaces() self.assertEqual([], oc_lacp_ifaces.interfaces) oc_lacp_iface = oc_lacp_ifaces.add('lacp-iface-name') mock_lacp_iface.assert_called_with('lacp-iface-name') self.assertEqual([oc_lacp_iface], oc_lacp_ifaces.interfaces) element = oc_lacp_ifaces.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' '' '') self.assertEqual(expected, xml_str) @mock.patch.object(lacp, 'LACPInterfaceConfig', autospec=True) def test_openconfig_lacp_interface(self, mock_lacp_if_conf): mock_lacp_if_conf.return_value.to_xml_element.return_value = ( ElementTree.Element('fake-lacp-interface-config')) self.assertRaises(TypeError, lacp.LACPInterface, int(20)) oc_lacp_iface = lacp.LACPInterface('lacp-iface-name') self.assertEqual('lacp-iface-name', oc_lacp_iface.name) element = oc_lacp_iface.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' 'lacp-iface-name' '' '') self.assertEqual(expected, xml_str) oc_lacp_iface.operation = constants.NetconfEditConfigOperation.REMOVE element = oc_lacp_iface.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' 'lacp-iface-name' '' '') self.assertEqual(expected, xml_str) def test_openconfig_lacp_interface_config(self): self.assertRaises(ValueError, lacp.LACPInterfaceConfig, 'name', **dict(operation='invalid')) self.assertRaises(ValueError, lacp.LACPInterfaceConfig, 'name', **dict(interval='invalid')) self.assertRaises(ValueError, lacp.LACPInterfaceConfig, 'name', **dict(lacp_mode='invalid')) lacp_if_conf = lacp.LACPInterfaceConfig('lacp-iface-name') element = lacp_if_conf.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' 'lacp-iface-name' 'SLOW' 'ACTIVE' '') self.assertEqual(expected, xml_str) lacp_if_conf.interval = constants.LACP_PERIOD_FAST lacp_if_conf.lacp_mode = constants.LACP_ACTIVITY_PASSIVE element = lacp_if_conf.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' 'lacp-iface-name' 'FAST' 'PASSIVE' '') self.assertEqual(expected, xml_str) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/openconfig/test_network_instance.py0000664000175000017500000000444515157004031032211 0ustar00zuulzuul# 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 xml.etree import ElementTree from oslotest import base from networking_baremetal.openconfig.network_instance import network_instance from networking_baremetal.openconfig.vlan import vlan class TestNetworkInstance(base.BaseTestCase): @mock.patch.object(network_instance, 'NetworkInstance', autospec=True) def test_network_instances(self, mock_net_instance): mock_net_instance.return_value.to_xml_element.return_value = ( ElementTree.Element('fake-net-instance')) net_instances = network_instance.NetworkInstances() net_instance = net_instances.add('default') self.assertEqual([net_instance], net_instances.network_instances) element = net_instances.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = (f'' '' '') self.assertEqual(expected, xml_str) @mock.patch.object(vlan, 'Vlans', autospec=True) def test_network_instance(self, mock_oc_vlans): mock_oc_vlans.return_value.to_xml_element.return_value = ( ElementTree.Element('fake-oc-vlans')) mock_oc_vlans.return_value.__len__.return_value = 1 net_instance = network_instance.NetworkInstance('default') self.assertEqual(mock_oc_vlans(), net_instance.vlans) element = net_instance.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' 'default' '' '') self.assertEqual(expected, xml_str) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/openconfig/test_vlan.py0000664000175000017500000001600415157004031027566 0ustar00zuulzuul# 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 xml.etree import ElementTree from oslotest import base from networking_baremetal import constants from networking_baremetal.openconfig.vlan import vlan class TestVlan(base.BaseTestCase): @mock.patch.object(vlan, 'Vlan', autospec=True) def test_vlans(self, mock_vlan): mock_vlan.return_value.to_xml_element.side_effect = [ ElementTree.Element('fake-vlan-10'), ElementTree.Element('fake-vlan-20') ] oc_vlans = vlan.Vlans() oc_vlan = oc_vlans.add(10) mock_vlan.assert_called_with(10) self.assertEqual([oc_vlan], oc_vlans.vlans) remove_vlan = oc_vlans.remove(20) self.assertEqual([oc_vlan, remove_vlan], oc_vlans.vlans) element = oc_vlans.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = (f'' '' '' '') self.assertEqual(expected, xml_str) @mock.patch.object(vlan, 'VlanConfig', autospec=True) def test_vlan(self, mock_vlan_conf): mock_vlan_conf.return_value.to_xml_element.return_value = ( ElementTree.Element('fake-vlan-conf')) oc_vlan = vlan.Vlan(10) self.assertEqual(constants.NetconfEditConfigOperation.MERGE.value, oc_vlan.operation) self.assertEqual(10, oc_vlan.vlan_id) self.assertRaises(TypeError, vlan.Vlan, 'not-int') self.assertRaises(ValueError, vlan.Vlan, 20, **dict(operation='invalid')) element = oc_vlan.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' '10' '' '') self.assertEqual(expected, xml_str) oc_vlan.operation = 'remove' element = oc_vlan.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' '10' '' '') self.assertEqual(expected, xml_str) def test_vlan_config(self): vlan_conf = vlan.VlanConfig() self.assertEqual(constants.NetconfEditConfigOperation.MERGE.value, vlan_conf.operation) self.assertRaises(ValueError, vlan.VlanConfig, **dict(operation='invalid')) self.assertRaises(TypeError, vlan.VlanConfig, **dict(vlan_id='not-int')) self.assertRaises(TypeError, vlan.VlanConfig, **dict(name=20)) # Not str self.assertRaises(ValueError, vlan.VlanConfig, **dict(status='invalid')) vlan_conf.vlan_id = 10 vlan_conf.name = 'Vlan10' vlan_conf.status = 'ACTIVE' element = vlan_conf.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' '10' 'Vlan10' 'ACTIVE' '') self.assertEqual(expected, xml_str) vlan_conf.operation = 'delete' vlan_conf.status = 'SUSPENDED' element = vlan_conf.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' '10' 'Vlan10' 'SUSPENDED' '') self.assertEqual(expected, xml_str) @mock.patch.object(vlan, 'VlanSwitchedConfig', autospec=True) def test_switched_vlan(self, mock_switched_vlan_conf): mock_switched_vlan_conf.return_value.to_xml_element.return_value = ( ElementTree.Element('fake-switched-vlan-config')) switched_vlan = vlan.VlanSwitchedVlan() element = switched_vlan.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = (f'' '' '') self.assertEqual(expected, xml_str) def test_switched_vlan_config(self): swithced_vlan_conf = vlan.VlanSwitchedConfig() self.assertEqual(constants.NetconfEditConfigOperation.MERGE.value, swithced_vlan_conf.operation) self.assertRaises(ValueError, vlan.VlanSwitchedConfig, **dict(operation='invalid')) self.assertRaises(ValueError, vlan.VlanSwitchedConfig, **dict(interface_mode='invalid')) self.assertRaises(TypeError, vlan.VlanSwitchedConfig, **dict(native_vlan='not-int')) self.assertRaises(TypeError, vlan.VlanSwitchedConfig, **dict(access_vlan='not-int')) swithced_vlan_conf.interface_mode = 'ACCESS' swithced_vlan_conf.access_vlan = 20 element = swithced_vlan_conf.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' 'ACCESS' '20' '') self.assertEqual(expected, xml_str) del swithced_vlan_conf.access_vlan swithced_vlan_conf.interface_mode = 'TRUNK' swithced_vlan_conf.native_vlan = 30 swithced_vlan_conf.trunk_vlans.add('10..50') swithced_vlan_conf.trunk_vlans.add('99') swithced_vlan_conf.trunk_vlans.add(88) swithced_vlan_conf.trunk_vlans.add('200..300') element = swithced_vlan_conf.to_xml_element() xml_str = ElementTree.tostring(element).decode("utf-8") expected = ('' 'TRUNK' '30' '10..50' '99' '88' '200..300' '') self.assertEqual(expected, xml_str) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1773930567.843996 networking_baremetal-7.2.0/networking_baremetal/tests/unit/plugins/0000775000175000017500000000000015157004110024544 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/plugins/__init__.py0000664000175000017500000000000015157004031026645 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1773930567.843996 networking_baremetal-7.2.0/networking_baremetal/tests/unit/plugins/ml2/0000775000175000017500000000000015157004110025236 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/plugins/ml2/__init__.py0000664000175000017500000000000015157004031027337 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/plugins/ml2/test_baremetal_l2vni_mech.py0000664000175000017500000007376315157004031032733 0ustar00zuulzuul# 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 neutron.db import provisioning_blocks from neutron.objects import ports as port_objects from neutron.tests import base as tests_base from neutron_lib.api.definitions import portbindings from neutron_lib.callbacks import resources from neutron_lib import constants as n_const from neutron_lib import exceptions as n_exc from neutron_lib.plugins.ml2 import api from oslo_config import cfg from networking_baremetal.plugins.ml2 import baremetal_l2vni_mapping from networking_baremetal.tests.unit.plugins.ml2 import utils as ml2_utils class TestL2vniMechanismDriver(tests_base.BaseTestCase): """Test cases for L2VNI Mechanism Driver""" def setUp(self): super(TestL2vniMechanismDriver, self).setUp() self.driver = baremetal_l2vni_mapping.L2vniMechanismDriver() self.driver.initialize() def test_initialize(self): """Test driver initialization""" self.assertEqual(portbindings.CONNECTIVITY_L2, self.driver.connectivity) def test_get_allowed_network_types(self): """Test that L2VNI driver only reports overlay network types""" agent_mock = mock.Mock() allowed_network_types = self.driver.get_allowed_network_types( agent_mock) self.assertEqual(allowed_network_types, [n_const.TYPE_VXLAN, n_const.TYPE_GENEVE]) def test_get_ovn_client_success(self): """Test successful OVN client retrieval""" mock_plugin = mock.Mock() mock_driver = mock.Mock() mock_driver.obj._ovn_client = mock.Mock() mock_plugin.mechanism_manager.ordered_mech_drivers = [mock_driver] with mock.patch('neutron_lib.plugins.directory.get_plugin', autospec=True, return_value=mock_plugin): result = self.driver._get_ovn_client self.assertIsNotNone(result) self.assertEqual(mock_driver.obj._ovn_client, result) def test_get_ovn_client_no_mechanism_manager(self): """Test OVN client retrieval when plugin has no mechanism_manager""" mock_plugin = mock.Mock(spec=[]) with mock.patch('neutron_lib.plugins.directory.get_plugin', autospec=True, return_value=mock_plugin): result = self.driver._get_ovn_client self.assertIsNone(result) def test_get_ovn_client_no_ovn_driver(self): """Test OVN client retrieval when OVN driver not found""" mock_plugin = mock.Mock() mock_driver = mock.Mock(spec=[]) mock_plugin.mechanism_manager.ordered_mech_drivers = [mock_driver] with mock.patch('neutron_lib.plugins.directory.get_plugin', autospec=True, return_value=mock_plugin): result = self.driver._get_ovn_client self.assertIsNone(result) def test_chassis_can_forward_physnet_true(self): """Test chassis can forward physnet returns True""" mock_ovn_client = mock.Mock() # Mock chassis 1 with physnet1 mock_chassis1 = mock.Mock() mock_chassis1.name = 'chassis-1' mock_chassis1.external_ids = { 'ovn-bridge-mappings': 'physnet1:br-provider,physnet2:br-ex' } # Mock chassis 2 without our physnet mock_chassis2 = mock.Mock() mock_chassis2.name = 'chassis-2' mock_chassis2.external_ids = { 'ovn-bridge-mappings': 'physnet3:br-other' } mock_ovn_client._sb_idl.tables = { 'Chassis': mock.Mock(rows=mock.Mock( values=mock.Mock(return_value=[mock_chassis1, mock_chassis2]) )) } result = self.driver._chassis_can_forward_physnet( mock_ovn_client, 'physnet1') self.assertTrue(result) def test_chassis_can_forward_physnet_false(self): """Test chassis can forward physnet returns False""" mock_ovn_client = mock.Mock() # Mock chassis without the requested physnet mock_chassis = mock.Mock() mock_chassis.name = 'test-chassis' mock_chassis.other_config = { 'ovn-bridge-mappings': 'physnet1:br-provider' } mock_ovn_client._sb_idl.tables = { 'Chassis': mock.Mock(rows=mock.Mock( values=mock.Mock(return_value=[mock_chassis]) )) } result = self.driver._chassis_can_forward_physnet( mock_ovn_client, 'physnet2') self.assertFalse(result) def test_chassis_can_forward_physnet_no_sb_connection(self): """Test chassis can forward physnet when no SB connection""" mock_ovn_client = mock.Mock(spec=[]) # No _sb_idl attribute result = self.driver._chassis_can_forward_physnet( mock_ovn_client, 'physnet1') # Should fail open and return True self.assertTrue(result) class TestL2vniPortBinding(tests_base.BaseTestCase): """Test cases for port binding functionality""" def setUp(self): super(TestL2vniPortBinding, self).setUp() self.driver = baremetal_l2vni_mapping.L2vniMechanismDriver() self.driver.initialize() self.context = self._create_port_context() def _create_port_context(self, vnic_type=portbindings.VNIC_BAREMETAL, network_type=n_const.TYPE_VXLAN, physnet='physnet1', vlan_id=100, network_name=None): """Create a mock port context""" mock_context = mock.Mock() # Setup current port network = ml2_utils.get_test_network() if network_name: network['name'] = network_name port = ml2_utils.get_test_port( network['id'], vnic_type=vnic_type, binding_profile={'physical_network': physnet} ) mock_context.current = port # Setup network segments overlay_segment = { api.ID: 'overlay-segment-id', api.NETWORK_TYPE: network_type, api.SEGMENTATION_ID: 5000, api.PHYSICAL_NETWORK: None } vlan_segment = { api.ID: 'vlan-segment-id', api.NETWORK_TYPE: n_const.TYPE_VLAN, api.SEGMENTATION_ID: vlan_id, api.PHYSICAL_NETWORK: physnet } mock_network = mock.Mock() mock_network.network_segments = [overlay_segment] mock_network.current = network mock_context.network = mock_network mock_context.segments_to_bind = [overlay_segment] mock_context.top_bound_segment = None mock_context.bottom_bound_segment = None mock_context.original_bottom_bound_segment = None # Mock methods mock_context.allocate_dynamic_segment.return_value = vlan_segment mock_context.continue_binding = mock.Mock() # Mock plugin and type_manager mock_context._plugin = mock.Mock() mock_context._plugin.type_manager = mock.Mock() mock_context._plugin.type_manager.is_partial_segment.return_value = ( False) return mock_context def test_bind_port_unsupported_vnic_type(self): """Test bind_port skips unsupported VNIC types""" context = self._create_port_context( vnic_type=portbindings.VNIC_NORMAL) self.driver.bind_port(context) context.continue_binding.assert_not_called() def test_bind_port_non_overlay_network(self): """Test bind_port skips non-overlay networks (flat, vlan, etc)""" context = self._create_port_context( network_type=n_const.TYPE_FLAT) self.driver.bind_port(context) context.continue_binding.assert_not_called() def test_bind_port_skips_anchor_network(self): """Test bind_port skips ports on L2VNI subport anchor network""" cfg.CONF.set_override('l2vni_subport_anchor_network', 'l2vni-subport-anchor', group='l2vni') context = self._create_port_context( network_type=n_const.TYPE_VXLAN, network_name='l2vni-subport-anchor') self.driver.bind_port(context) # Should not attempt to allocate segment or continue binding context.allocate_dynamic_segment.assert_not_called() context.continue_binding.assert_not_called() @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_ensure_localnet_port', autospec=True) def test_bind_port_geneve_network(self, mock_ensure_localnet): """Test bind_port processes Geneve networks""" context = self._create_port_context( network_type=n_const.TYPE_GENEVE) self.driver.bind_port(context) # Should allocate dynamic segment for Geneve context.allocate_dynamic_segment.assert_called_once() call_args = context.allocate_dynamic_segment.call_args[0][0] self.assertEqual('physnet1', call_args[api.PHYSICAL_NETWORK]) self.assertEqual(n_const.TYPE_VLAN, call_args[api.NETWORK_TYPE]) # Should ensure localnet port mock_ensure_localnet.assert_called_once() # Should continue binding context.continue_binding.assert_called_once() @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_ensure_localnet_port', autospec=True) def test_bind_port_allocates_new_segment(self, mock_ensure_localnet): """Test bind_port allocates new VLAN segment""" context = self._create_port_context() self.driver.bind_port(context) # Should allocate dynamic segment context.allocate_dynamic_segment.assert_called_once() call_args = context.allocate_dynamic_segment.call_args[0][0] self.assertEqual('physnet1', call_args[api.PHYSICAL_NETWORK]) self.assertEqual(n_const.TYPE_VLAN, call_args[api.NETWORK_TYPE]) # Should ensure localnet port mock_ensure_localnet.assert_called_once() # Should continue binding context.continue_binding.assert_called_once() @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_ensure_localnet_port', autospec=True) def test_bind_port_reuses_existing_segment(self, mock_ensure_localnet): """Test bind_port reuses existing VLAN segment""" context = self._create_port_context() # Add existing VLAN segment to network vlan_segment = { api.ID: 'existing-vlan-segment', api.NETWORK_TYPE: n_const.TYPE_VLAN, api.SEGMENTATION_ID: 200, api.PHYSICAL_NETWORK: 'physnet1' } context.network.network_segments.append(vlan_segment) self.driver.bind_port(context) # Should NOT allocate new segment context.allocate_dynamic_segment.assert_not_called() # Should still ensure localnet port mock_ensure_localnet.assert_called_once() # Should continue binding context.continue_binding.assert_called_once() def test_bind_port_partial_segment_error(self): """Test bind_port raises error for partial segment""" context = self._create_port_context() # Add partial VLAN segment (no segmentation_id) partial_segment = { api.ID: 'partial-vlan-segment', api.NETWORK_TYPE: n_const.TYPE_VLAN, api.SEGMENTATION_ID: None, api.PHYSICAL_NETWORK: 'physnet1' } context.network.network_segments.append(partial_segment) context._plugin.type_manager.is_partial_segment.return_value = True self.assertRaises(n_exc.InvalidInput, self.driver.bind_port, context) def test_bind_port_missing_physnet_no_default(self): """Test bind_port raises error when physnet missing and no default""" context = self._create_port_context() context.current[portbindings.PROFILE] = {} # No default configured cfg.CONF.set_override('default_physical_network', None, group='baremetal_l2vni') # Should raise InvalidInput exception self.assertRaises(n_exc.InvalidInput, self.driver.bind_port, context) @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_ensure_localnet_port', autospec=True) def test_bind_port_uses_default_physnet(self, mock_ensure_localnet): """Test bind_port uses default physnet when missing from profile""" context = self._create_port_context() context.current[portbindings.PROFILE] = {} # Configure default physical network cfg.CONF.set_override('default_physical_network', 'default-physnet', group='baremetal_l2vni') self.driver.bind_port(context) # Should allocate segment with default physnet context.allocate_dynamic_segment.assert_called_once() call_args = context.allocate_dynamic_segment.call_args[0][0] self.assertEqual('default-physnet', call_args[api.PHYSICAL_NETWORK]) self.assertEqual(n_const.TYPE_VLAN, call_args[api.NETWORK_TYPE]) # Should ensure localnet port mock_ensure_localnet.assert_called_once() # Should continue binding context.continue_binding.assert_called_once() def test_bind_port_profile_physnet_overrides_default(self): """Test bind_port prefers profile physnet over default""" context = self._create_port_context(physnet='profile-physnet') # Configure default physical network cfg.CONF.set_override('default_physical_network', 'default-physnet', group='baremetal_l2vni') with mock.patch.object(self.driver, '_ensure_localnet_port', autospec=True): self.driver.bind_port(context) # Should use profile physnet, not default context.allocate_dynamic_segment.assert_called_once() call_args = context.allocate_dynamic_segment.call_args[0][0] self.assertEqual('profile-physnet', call_args[api.PHYSICAL_NETWORK]) def test_bind_port_segment_allocation_fails(self): """Test bind_port raises error when segment allocation fails""" context = self._create_port_context() # Mock allocation returning None (failure) context.allocate_dynamic_segment.return_value = None # Should raise InvalidInput exception self.assertRaises(n_exc.InvalidInput, self.driver.bind_port, context) def test_bind_port_allocated_segment_missing_vlan_id(self): """Test bind_port raises error when segment lacks segmentation_id""" context = self._create_port_context() # Mock allocation returning segment without segmentation_id invalid_segment = { api.ID: 'invalid-segment', api.NETWORK_TYPE: n_const.TYPE_VLAN, api.SEGMENTATION_ID: None, api.PHYSICAL_NETWORK: 'physnet1' } context.allocate_dynamic_segment.return_value = invalid_segment # Should raise InvalidInput exception self.assertRaises(n_exc.InvalidInput, self.driver.bind_port, context) @mock.patch.object(baremetal_l2vni_mapping.LOG, 'debug', autospec=True) def test_bind_port_logs_debug_info(self, mock_debug): """Verify bind_port logs debug information.""" context = self._create_port_context() segments = [{ api.ID: 'test-segment-id', api.NETWORK_TYPE: n_const.TYPE_GENEVE, api.SEGMENTATION_ID: 12345 }] context.segments_to_bind = segments with mock.patch.object(self.driver, '_bind_port_segment', autospec=True): self.driver.bind_port(context) # Verify debug logging was called multiple times # (once for entry, once for binding overlay segment) self.assertGreater(mock_debug.call_count, 0) class TestL2vniLocalnetPort(tests_base.BaseTestCase): """Test cases for localnet port management""" def setUp(self): super(TestL2vniLocalnetPort, self).setUp() self.driver = baremetal_l2vni_mapping.L2vniMechanismDriver() self.driver.initialize() @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_get_ovn_client', new_callable=mock.PropertyMock) @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_chassis_can_forward_physnet', autospec=True) @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_get_local_chassis_name', autospec=True) def test_ensure_localnet_port_success(self, mock_get_chassis_name, mock_can_forward, mock_get_client): """Test successful localnet port creation""" mock_context = mock.Mock() mock_ovn_client = mock.Mock() mock_get_client.return_value = mock_ovn_client mock_can_forward.return_value = True mock_get_chassis_name.return_value = 'test-chassis' # Mock that port doesn't exist (lookup returns None) mock_ovn_client._nb_idl.lookup.return_value = None network_id = 'test-network-id' physnet = 'physnet1' vlan_id = 100 self.driver._ensure_localnet_port( mock_context, network_id, physnet, vlan_id) # Should create localnet port with requested-chassis mock_ovn_client._nb_idl.create_lswitch_port.assert_called_once() mock_ovn_client._nb_idl.db_set.assert_called_once() mock_ovn_client._transaction.assert_called_once() @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_get_ovn_client', new_callable=mock.PropertyMock) def test_ensure_localnet_port_disabled_by_config(self, mock_get_client): """Test localnet port creation skipped when disabled by config""" cfg.CONF.set_override('create_localnet_ports', False, group='baremetal_l2vni') mock_context = mock.Mock() network_id = 'test-network-id' physnet = 'physnet1' vlan_id = 100 self.driver._ensure_localnet_port( mock_context, network_id, physnet, vlan_id) # Should not get OVN client mock_get_client.assert_not_called() @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_get_ovn_client', new_callable=mock.PropertyMock) def test_ensure_localnet_port_no_ovn_client(self, mock_get_client): """Test localnet port creation when OVN client unavailable""" mock_get_client.return_value = None mock_context = mock.Mock() self.driver._ensure_localnet_port( mock_context, 'network-id', 'physnet1', 100) # Should return early without error @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_get_ovn_client', new_callable=mock.PropertyMock) @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_chassis_can_forward_physnet', autospec=True) def test_ensure_localnet_port_chassis_cannot_forward( self, mock_can_forward, mock_get_client): """Test localnet port creation skipped when chassis can't forward""" mock_ovn_client = mock.Mock() mock_get_client.return_value = mock_ovn_client mock_can_forward.return_value = False mock_context = mock.Mock() self.driver._ensure_localnet_port( mock_context, 'network-id', 'physnet1', 100) # Should not attempt to create port mock_ovn_client._nb_idl.create_lswitch_port.assert_not_called() @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_get_ovn_client', new_callable=mock.PropertyMock) @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_chassis_can_forward_physnet', autospec=True) @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_get_local_chassis_name', autospec=True) def test_ensure_localnet_port_already_exists(self, mock_get_chassis_name, mock_can_forward, mock_get_client): """Test localnet port creation when port already exists""" mock_ovn_client = mock.Mock() mock_get_client.return_value = mock_ovn_client mock_can_forward.return_value = True mock_get_chassis_name.return_value = 'test-chassis' # Mock that port already exists with matching VLAN tag and chassis existing_port = mock.Mock() existing_port.tag = [100] # OVN stores tags as list existing_port.options = {'requested-chassis': 'test-chassis'} mock_ovn_client._nb_idl.lookup.return_value = existing_port mock_context = mock.Mock() self.driver._ensure_localnet_port( mock_context, 'network-id', 'physnet1', 100) # Should not create new port mock_ovn_client._nb_idl.create_lswitch_port.assert_not_called() @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_get_ovn_client', new_callable=mock.PropertyMock) @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_chassis_can_forward_physnet', autospec=True) @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_get_local_chassis_name', autospec=True) def test_ensure_localnet_port_vlan_tag_mismatch( self, mock_get_chassis_name, mock_can_forward, mock_get_client): """Test localnet port recreation when VLAN tag changes""" mock_ovn_client = mock.Mock() mock_get_client.return_value = mock_ovn_client mock_can_forward.return_value = True mock_get_chassis_name.return_value = 'test-chassis' # Mock that port exists with old VLAN tag 107 existing_port = mock.Mock() existing_port.tag = [107] existing_port.options = {'requested-chassis': 'test-chassis'} mock_ovn_client._nb_idl.lookup.return_value = existing_port mock_context = mock.Mock() # Try to ensure port with new VLAN tag 135 self.driver._ensure_localnet_port( mock_context, 'network-id', 'physnet1', 135) # Should delete old port mock_ovn_client._nb_idl.lsp_del.assert_called_once() # Should create new port with correct VLAN tag mock_ovn_client._nb_idl.create_lswitch_port.assert_called_once() call_args = ( mock_ovn_client._nb_idl.create_lswitch_port.call_args) self.assertEqual(call_args.kwargs['tag'], 135) @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_get_ovn_client', new_callable=mock.PropertyMock) def test_remove_localnet_port_success(self, mock_get_client): """Test successful localnet port removal""" mock_ovn_client = mock.Mock() mock_get_client.return_value = mock_ovn_client # Mock that port exists mock_ovn_client._nb_idl.lookup.return_value = mock.Mock() mock_context = mock.Mock() network_id = 'test-network-id' physnet = 'physnet1' self.driver._remove_localnet_port(mock_context, network_id, physnet) # Should delete the port mock_ovn_client._nb_idl.lsp_del.return_value.execute\ .assert_called_once() @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_get_ovn_client', new_callable=mock.PropertyMock) def test_remove_localnet_port_not_exists(self, mock_get_client): """Test localnet port removal when port doesn't exist""" mock_ovn_client = mock.Mock() mock_get_client.return_value = mock_ovn_client # Mock that port doesn't exist (lookup returns None) mock_ovn_client._nb_idl.lookup.return_value = None mock_context = mock.Mock() self.driver._remove_localnet_port( mock_context, 'network-id', 'physnet1') # Should not attempt to delete mock_ovn_client._nb_idl.lsp_del.assert_not_called() class TestL2vniPortUpdate(tests_base.BaseTestCase): """Test cases for port update functionality""" def setUp(self): super(TestL2vniPortUpdate, self).setUp() self.driver = baremetal_l2vni_mapping.L2vniMechanismDriver() self.driver.initialize() def test_update_port_postcommit_unsupported_vnic_type(self): """Test update_port_postcommit skips unsupported VNIC types""" mock_context = mock.Mock() mock_context.current = { portbindings.VNIC_TYPE: portbindings.VNIC_NORMAL, portbindings.VIF_TYPE: portbindings.VIF_TYPE_UNBOUND } self.driver.update_port_postcommit(mock_context) # Should return early without error @mock.patch.object(port_objects.PortBindingLevel, 'get_objects', autospec=True) @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_remove_localnet_port', autospec=True) def test_update_port_postcommit_release_segment(self, mock_remove_port, mock_get_objects): """Test update_port_postcommit releases dynamic segment""" mock_context = mock.Mock() network = ml2_utils.get_test_network() mock_context.current = { portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL, portbindings.VIF_TYPE: portbindings.VIF_TYPE_UNBOUND } segment = { api.ID: 'segment-id', api.NETWORK_TYPE: n_const.TYPE_VLAN, api.SEGMENTATION_ID: 100, api.PHYSICAL_NETWORK: 'physnet1' } mock_context.original_bottom_bound_segment = segment mock_network = mock.Mock() mock_network.current = network mock_context.network = mock_network # No other ports using this segment mock_get_objects.return_value = [] self.driver.update_port_postcommit(mock_context) # Should remove localnet port and release segment mock_remove_port.assert_called_once() mock_context.release_dynamic_segment.assert_called_once_with( 'segment-id') @mock.patch.object(provisioning_blocks, 'provisioning_complete', autospec=True) def test_update_port_postcommit_complete_provisioning(self, mock_pc): """Test update_port_postcommit completes provisioning blocks""" mock_context = mock.Mock() port = ml2_utils.get_test_port( 'network-id', vnic_type=portbindings.VNIC_BAREMETAL, vif_type=portbindings.VIF_TYPE_OTHER ) mock_context.current = port self.driver.update_port_postcommit(mock_context) # Should complete provisioning mock_pc.assert_called_once_with( mock_context._plugin_context, port['id'], resources.PORT, 'L2' ) @mock.patch.object(port_objects.PortBindingLevel, 'get_objects', autospec=True) @mock.patch.object(baremetal_l2vni_mapping.L2vniMechanismDriver, '_remove_localnet_port', autospec=True) def test_delete_port_postcommit_cleanup_segment( self, mock_remove_port, mock_get_objects): """Test delete_port_postcommit cleans up when last port deleted""" mock_context = mock.Mock() network = ml2_utils.get_test_network() mock_context.current = { portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL, } segment = { api.ID: 'segment-id', api.NETWORK_TYPE: n_const.TYPE_VLAN, api.SEGMENTATION_ID: 107, api.PHYSICAL_NETWORK: 'physnet1' } mock_context.bottom_bound_segment = segment mock_network = mock.Mock() mock_network.current = network mock_context.network = mock_network # No other ports using this segment (last port deleted) mock_get_objects.return_value = [] self.driver.delete_port_postcommit(mock_context) # Should remove localnet port and release segment mock_remove_port.assert_called_once() mock_context.release_dynamic_segment.assert_called_once_with( 'segment-id') @mock.patch.object(port_objects.PortBindingLevel, 'get_objects', autospec=True) def test_delete_port_postcommit_other_ports_remain( self, mock_get_objects): """Test delete_port_postcommit preserves segment when ports remain""" mock_context = mock.Mock() mock_context.current = { portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL, } segment = { api.ID: 'segment-id', api.NETWORK_TYPE: n_const.TYPE_VLAN, api.SEGMENTATION_ID: 107, api.PHYSICAL_NETWORK: 'physnet1' } mock_context.bottom_bound_segment = segment # Other ports still using this segment mock_get_objects.return_value = [mock.Mock()] self.driver.delete_port_postcommit(mock_context) # Should NOT release segment mock_context.release_dynamic_segment.assert_not_called() class TestL2vniRouterGateway(tests_base.BaseTestCase): """Test cases for router gateway chassis management""" def setUp(self): super(TestL2vniRouterGateway, self).setUp() self.driver = baremetal_l2vni_mapping.L2vniMechanismDriver() self.driver.initialize() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/plugins/ml2/test_baremetal_mech.py0000664000175000017500000007231515157004031031611 0ustar00zuulzuul# Copyright 2017 Mirantis, 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 neutron.db import provisioning_blocks from neutron.plugins.ml2 import driver_context from neutron.tests.unit.plugins.ml2 import _test_mech_agent as base from neutron_lib.api.definitions import portbindings from neutron_lib import constants as n_const from neutron_lib.plugins.ml2 import api from oslo_config import fixture as config_fixture from networking_baremetal import common from networking_baremetal import config from networking_baremetal import constants from networking_baremetal import exceptions from networking_baremetal.plugins.ml2 import baremetal_mech from networking_baremetal.tests.unit.plugins.ml2 import utils as ml2_utils class TestBaremetalMechDriver(base.AgentMechanismBaseTestCase): VIF_TYPE = portbindings.VIF_TYPE_OTHER VIF_DETAILS = None AGENT_TYPE = constants.BAREMETAL_AGENT_TYPE GOOD_CONFIGS = { 'bridge_mappings': {'fake_physical_network': 'fake_physnet'} } BAD_CONFIGS = { 'bridge_mappings': {'wrong_physical_network': 'wrong_physnet'} } AGENTS = [{'agent_type': AGENT_TYPE, 'alive': True, 'configurations': GOOD_CONFIGS, 'host': 'host'}] AGENTS_DEAD = [ {'agent_type': AGENT_TYPE, 'alive': False, 'configurations': GOOD_CONFIGS, 'host': 'dead_host'} ] AGENTS_BAD = [ {'agent_type': AGENT_TYPE, 'alive': False, 'configurations': GOOD_CONFIGS, 'host': 'bad_host_1'}, {'agent_type': AGENT_TYPE, 'alive': True, 'configurations': BAD_CONFIGS, 'host': 'bad_host_2'} ] VNIC_TYPE = portbindings.VNIC_BAREMETAL def setUp(self): super(TestBaremetalMechDriver, self).setUp() self.driver = baremetal_mech.BaremetalMechanismDriver() self.driver.initialize() def _make_port_ctx(self, agents): segments = [{api.ID: 'local_segment_id', api.PHYSICAL_NETWORK: 'fake_physical_network', api.NETWORK_TYPE: n_const.TYPE_FLAT}] return base.FakePortContext(self.AGENT_TYPE, agents, segments, vnic_type=self.VNIC_TYPE) def test_initialize(self): self.assertEqual([portbindings.VNIC_BAREMETAL], self.driver.supported_vnic_types) self.assertEqual(portbindings.VIF_TYPE_OTHER, self.driver.vif_type) def test_get_allowed_network_types(self): agent_mock = mock.Mock() allowed_network_types = self.driver.get_allowed_network_types( agent_mock) self.assertEqual(allowed_network_types, [n_const.TYPE_FLAT, n_const.TYPE_VLAN]) @mock.patch.object(provisioning_blocks, 'provisioning_complete', autospec=True) def test_update_port_postcommit_not_bound(self, mpb_pc): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network() m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = ml2_utils.get_test_port(network_id=m_nc.current['id']) m_pc.network = m_nc self.driver.update_port_postcommit(m_pc) self.assertFalse(mpb_pc.called) @mock.patch.object(provisioning_blocks, 'provisioning_complete', autospec=True) def test_update_port_postcommit_unsupported_vnic_type_not_bound( self, mpb_pc): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network() m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = ml2_utils.get_test_port( network_id=m_nc.current['id'], vnic_type=portbindings.VNIC_MACVTAP, vif_type=portbindings.VIF_TYPE_OTHER) m_pc.network = m_nc self.driver.update_port_postcommit(m_pc) self.assertFalse(mpb_pc.called) @mock.patch.object(provisioning_blocks, 'provisioning_complete', autospec=True) def test_update_port_postcommit_supported_vnic_type_bound( self, mpb_pc): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network() m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = ml2_utils.get_test_port( network_id=m_nc.current['id'], vnic_type=portbindings.VNIC_BAREMETAL, vif_type=portbindings.VIF_TYPE_OTHER) m_pc._plugin_context = 'plugin_context' m_pc.network = m_nc self.driver.update_port_postcommit(m_pc) mpb_pc.assert_called_once_with('plugin_context', m_pc.current['id'], 'port', 'BAREMETAL_DRV_ENTITIY') @mock.patch.object(provisioning_blocks, 'add_provisioning_component', autospec=True) def test_bind_port_unsupported_network_type(self, mpb_pc): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_VXLAN) m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = ml2_utils.get_test_port( network_id=m_nc.current['id'], vnic_type=portbindings.VNIC_BAREMETAL, vif_type=portbindings.VIF_TYPE_OTHER) m_pc.network = m_nc m_pc.segments_to_bind = [ ml2_utils.get_test_segment(network_type=n_const.TYPE_VXLAN)] self.driver.bind_port(m_pc) self.assertFalse(mpb_pc.called) @mock.patch.object(provisioning_blocks, 'add_provisioning_component', autospec=True) def test_bind_port_unsupported_vnic_type(self, mpb_pc): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_FLAT) m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = ml2_utils.get_test_port( network_id=m_nc.current['id'], vnic_type='unsupported') m_pc.network = m_nc m_pc.segments_to_bind = [ ml2_utils.get_test_segment(network_type=n_const.TYPE_FLAT)] self.driver.bind_port(m_pc) self.assertFalse(mpb_pc.called) def test_empty_methods(self): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network() m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = ml2_utils.get_test_port(network_id=m_nc.current['id']) m_pc.network = m_nc.current m_sc = mock.create_autospec(driver_context.SubnetContext) m_sc.current = ml2_utils.get_test_subnet( network_id=m_nc.current['id']) m_sc.network = m_nc self.driver.create_network_precommit(m_nc) self.driver.create_network_postcommit(m_nc) self.driver.update_network_precommit(m_nc) self.driver.update_network_postcommit(m_nc) self.driver.delete_network_precommit(m_nc) self.driver.delete_network_postcommit(m_nc) self.driver.create_subnet_precommit(m_sc) self.driver.create_subnet_postcommit(m_sc) self.driver.update_subnet_precommit(m_sc) self.driver.update_subnet_postcommit(m_sc) self.driver.delete_subnet_precommit(m_sc) self.driver.delete_subnet_postcommit(m_sc) self.driver.create_port_precommit(m_pc) self.driver.create_port_postcommit(m_pc) self.driver.update_port_precommit(m_pc) self.driver.update_port_postcommit(m_pc) self.driver.delete_port_precommit(m_pc) self.driver.delete_port_postcommit(m_pc) def test_bind_port_skips_when_overlay_segment_exists(self): """Verify bind_port skips binding when network has overlay segment.""" m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = {'id': 'test-port-id'} mock_network = mock.Mock() mock_network.network_segments = [ {api.NETWORK_TYPE: n_const.TYPE_GENEVE, api.ID: 'geneve-seg-id'} ] m_pc.network = mock_network with mock.patch.object(baremetal_mech.mech_agent. SimpleAgentMechanismDriverBase, 'bind_port', autospec=True) as mock_super_bind: self.driver.bind_port(m_pc) # Should NOT call parent bind_port when overlay exists mock_super_bind.assert_not_called() def test_bind_port_proceeds_when_no_overlay_segment(self): """Verify bind_port calls parent when no overlay segments exist.""" m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = {'id': 'test-port-id'} mock_network = mock.Mock() mock_network.network_segments = [ {api.NETWORK_TYPE: n_const.TYPE_FLAT, api.ID: 'flat-seg-id'} ] m_pc.network = mock_network with mock.patch.object(baremetal_mech.mech_agent. SimpleAgentMechanismDriverBase, 'bind_port', autospec=True) as mock_super_bind: self.driver.bind_port(m_pc) # Should call parent bind_port for flat/VLAN networks # With autospec, first arg is self mock_super_bind.assert_called_once_with(self.driver, m_pc) def test_try_to_bind_segment_for_agent_skips_vlan_with_overlay(self): """Verify VLAN binding skipped when overlay is first segment.""" m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = {'id': 'test-port-id'} mock_network = mock.Mock() mock_network.network_segments = [ {api.NETWORK_TYPE: n_const.TYPE_GENEVE, api.ID: 'geneve-seg-id'} ] m_pc.network = mock_network segment = {api.NETWORK_TYPE: n_const.TYPE_VLAN, api.ID: 'vlan-seg-id'} agent = mock.Mock() result = self.driver.try_to_bind_segment_for_agent( m_pc, segment, agent) # Should return False to skip VLAN binding self.assertFalse(result) def test_try_to_bind_segment_for_agent_proceeds_with_flat(self): """Verify normal binding proceeds with flat network.""" m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = {'id': 'test-port-id'} mock_network = mock.Mock() mock_network.network_segments = [ {api.NETWORK_TYPE: n_const.TYPE_FLAT, api.ID: 'flat-seg-id'} ] m_pc.network = mock_network segment = {api.NETWORK_TYPE: n_const.TYPE_FLAT, api.ID: 'flat-seg-id'} agent = mock.Mock() with mock.patch.object(self.driver, 'check_segment_for_agent', return_value=False, autospec=True) as mock_check: self.driver.try_to_bind_segment_for_agent( m_pc, segment, agent) # Should proceed to check_segment_for_agent mock_check.assert_called_once_with(segment, agent) class TestBaremetalMechDriverFakeDriver(base.AgentMechanismBaseTestCase): VIF_TYPE = portbindings.VIF_TYPE_OTHER VIF_DETAILS = None AGENT_TYPE = constants.BAREMETAL_AGENT_TYPE AGENT_CONF = {'bridge_mappings': {'fake_physical_network': 'fake_physnet'}} AGENTS = [{'agent_type': AGENT_TYPE, 'alive': True, 'configurations': AGENT_CONF, 'host': 'host'}] VNIC_TYPE = portbindings.VNIC_BAREMETAL def setUp(self): super(TestBaremetalMechDriverFakeDriver, self).setUp() mock_manager = mock.patch.object(common, 'driver_mgr', autospec=True) self.mock_manager = mock_manager.start() self.addCleanup(mock_manager.stop) self.mock_driver = mock.MagicMock() self.mock_manager.return_value = self.mock_driver self.conf = self.useFixture(config_fixture.Config()) self.conf.config(enabled_devices=['foo'], group='networking_baremetal') self.conf.register_opts(config._opts + config._device_opts, group='foo') self.conf.config(driver='test-driver', switch_id='aa:bb:cc:dd:ee:ff', switch_info='foo', physical_networks=['fake_physical_network'], group='foo') self.driver = baremetal_mech.BaremetalMechanismDriver() self.driver.initialize() self.mock_manager.assert_called_once_with('foo') self.mock_driver.load_config.assert_called_once() self.mock_driver.validate.assert_called_once() self.mock_manager.reset_mock() self.mock_driver.reset_mock() def _make_port_ctx(self, agents, profile): segments = [{api.ID: 'local_segment_id', api.PHYSICAL_NETWORK: 'fake_physical_network', api.NETWORK_TYPE: n_const.TYPE_FLAT}] return base.FakePortContext(self.AGENT_TYPE, agents, segments, vnic_type=self.VNIC_TYPE, profile=profile) def test__is_bound(self): m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = ml2_utils.get_test_port( network_id='network-id', vnic_type=portbindings.VNIC_BAREMETAL, vif_type=None) self.assertFalse(self.driver._is_bound(m_pc.current)) m_pc.current[portbindings.VIF_TYPE] = portbindings.VIF_TYPE_OTHER self.assertTrue(self.driver._is_bound(m_pc.current)) def test_create_network_postcommit_flat(self): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_FLAT) self.driver.create_network_postcommit(m_nc) self.mock_manager.assert_not_called() self.mock_driver.assert_not_called() def test_update_network_postcommit_flat(self): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_FLAT) self.driver.update_network_postcommit(m_nc) self.mock_manager.assert_not_called() self.mock_driver.assert_not_called() def test_delete_network_postcommit_flat(self): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_FLAT) self.driver.delete_network_postcommit(m_nc) self.mock_manager.assert_not_called() self.mock_driver.assert_not_called() def test_create_network_postcommit_vlan(self): # VLAN but no segmentation ID m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN) self.driver.create_network_postcommit(m_nc) self.mock_manager.assert_not_called() self.mock_manager.assert_not_called() # VLAN with segmentation ID, but not on physical network self.mock_manager.reset_mock() self.mock_driver.reset_mock() m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN, segmentation_id=10) self.driver.create_network_postcommit(m_nc) self.mock_manager.assert_not_called() self.mock_driver.assert_not_called() # VLAN with segmentation ID, on physical network self.mock_manager.reset_mock() self.mock_driver.reset_mock() m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN, segmentation_id=10, physical_network='fake_physical_network') self.driver.create_network_postcommit(m_nc) self.mock_manager.assert_called_once_with('foo') self.mock_driver.create_network.assert_called_once_with(m_nc) # Device VLAN management disabled in config self.conf.config(manage_vlans=False, group='foo') self.mock_manager.reset_mock() self.mock_driver.reset_mock() m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN, segmentation_id=10, physical_network='fake_physical_network') self.driver.create_network_postcommit(m_nc) self.mock_manager.assert_not_called() self.mock_driver.assert_not_called() def test_update_network_postcommit_vlan(self): # VLAN but no segmentation ID m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN) m_nc.original = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN) self.driver.update_network_postcommit(m_nc) self.mock_manager.assert_not_called() self.mock_driver.assert_not_called() # With physical network self.mock_manager.reset_mock() self.mock_driver.reset_mock() m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN, segmentation_id=10, physical_network='fake_physical_network') self.driver.update_network_postcommit(m_nc) self.mock_manager.assert_called_once_with('foo') self.mock_driver.update_network.assert_called_once_with(m_nc) # VLAN management disabled self.mock_manager.reset_mock() self.mock_driver.reset_mock() self.conf.config(manage_vlans=False, group='foo') self.driver.update_network_postcommit(m_nc) self.mock_manager.assert_not_called() self.mock_driver.assert_not_called() # Device not on physical network self.mock_manager.reset_mock() self.mock_driver.reset_mock() m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN, segmentation_id=10, physical_network='fake_physical_network') self.conf.config(physical_networks=['not-connected-physnet'], manage_vlans=True, group='foo') self.driver.update_network_postcommit(m_nc) self.mock_manager.assert_not_called() self.mock_driver.assert_not_called() def test_delete_network_postcommit_vlan(self): # VLAN but no segmentation ID m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN) m_nc.original = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN) self.driver.delete_network_postcommit(m_nc) self.mock_manager.assert_not_called() self.mock_driver.assert_not_called() # VLAN ID and matching physnet self.mock_manager.reset_mock() self.mock_driver.reset_mock() m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN, segmentation_id=10, physical_network='fake_physical_network') self.driver.delete_network_postcommit(m_nc) self.mock_manager.assert_called_once_with('foo') self.mock_driver.delete_network.assert_called_once_with(m_nc) # Not on physnet self.mock_manager.reset_mock() self.mock_driver.reset_mock() m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN, segmentation_id=10, physical_network='not-on-physnet') self.driver.delete_network_postcommit(m_nc) self.mock_manager.assert_not_called() self.mock_driver.assert_not_called() # VLAN management disabled self.mock_manager.reset_mock() self.mock_driver.reset_mock() self.conf.config(manage_vlans=False, group='foo') m_nc.current = ml2_utils.get_test_network( network_type=n_const.TYPE_VLAN, segmentation_id=10, physical_network='fake_physical_network') self.driver.delete_network_postcommit(m_nc) self.mock_manager.assert_not_called() self.mock_driver.assert_not_called() @mock.patch.object(provisioning_blocks, 'add_provisioning_component', autospec=True) def test_bind_port(self, mock_p_blocks): binding_profile = {} lli = binding_profile['local_link_information'] = [] lli.append({'port_id': 'test1/1', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}) context = self._make_port_ctx(self.AGENTS, binding_profile) context._plugin_context = 'plugin_context' self.assertIsNone(context._bound_vif_type) self.driver.bind_port(context) self.mock_manager.assert_called_once_with('foo') self.mock_driver.create_port.assert_called_once_with( context, context.segments_to_bind[0], lli) self.assertEqual(context._bound_vif_type, self.driver.vif_type) @mock.patch.object(provisioning_blocks, 'add_provisioning_component', autospec=True) def test_no_device_does_bind_port(self, mock_p_blocks): binding_profile = {} lli = binding_profile['local_link_information'] = [] lli.append({'port_id': 'test1/1', 'switch_id': '11:11:11:11:11:11', 'switch_info': 'not-such-device'}) context = self._make_port_ctx(self.AGENTS, binding_profile) context._plugin_context = 'plugin_context' self.assertIsNone(context._bound_vif_type) self.driver.bind_port(context) self.mock_manager.assert_not_called() self.mock_driver.create_port.assert_not_called() self.assertIsNone(context._bound_vif_type) mock_p_blocks.assert_not_called() @mock.patch.object(provisioning_blocks, 'add_provisioning_component', autospec=True) def test_driver_load_error_does_not_bind_port(self, mock_p_blocks): binding_profile = {} lli = binding_profile['local_link_information'] = [] lli.append({'port_id': 'test1/1', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}) context = self._make_port_ctx(self.AGENTS, binding_profile) context._plugin_context = 'plugin_context' self.mock_manager.side_effect = exceptions.DriverEntrypointLoadError( entry_point='entry_point', err='ERROR_MSG' ) self.assertIsNone(context._bound_vif_type) self.driver.bind_port(context) self.mock_manager.assert_called_once_with('foo') self.mock_driver.create_port.assert_not_called() # The port will not bind self.assertIsNone(context._bound_vif_type) @mock.patch.object(provisioning_blocks, 'add_provisioning_component', autospec=True) def test_not_on_physnet_does_not_bind_port(self, mock_p_blocks): binding_profile = {} lli = binding_profile['local_link_information'] = [] lli.append({'port_id': 'test1/1', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}) context = self._make_port_ctx(self.AGENTS, binding_profile) context._plugin_context = 'plugin_context' self.conf.config(physical_networks='other_physnet', group='foo') self.assertIsNone(context._bound_vif_type) self.driver.bind_port(context) self.mock_manager.assert_not_called() self.mock_driver.create_port.assert_not_called() # The port will not bind self.assertIsNone(context._bound_vif_type) @mock.patch.object(provisioning_blocks, 'add_provisioning_component', autospec=True) def test_bond_mode_supported_bind_port(self, mock_p_blocks): binding_profile = {} lli = binding_profile['local_link_information'] = [] llg = binding_profile['local_group_information'] = {} llg['bond_mode'] = '802.3ad' lli.append({'port_id': 'test1/1', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}) context = self._make_port_ctx(self.AGENTS, binding_profile) context._plugin_context = 'plugin_context' self.mock_driver.SUPPORTED_BOND_MODES = {'802.3ad'} self.assertIsNone(context._bound_vif_type) self.driver.bind_port(context) self.mock_manager.assert_called_once_with('foo') self.mock_driver.create_port.assert_called_once_with( context, context.segments_to_bind[0], lli) self.assertEqual(context._bound_vif_type, self.driver.vif_type) @mock.patch.object(provisioning_blocks, 'add_provisioning_component', autospec=True) def test_bond_mode_unsupported_does_not_bind_port(self, mock_p_blocks): binding_profile = {} lli = binding_profile['local_link_information'] = [] llg = binding_profile['local_group_information'] = {} llg['bond_mode'] = 'unsupported' lli.append({'port_id': 'test1/1', 'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}) context = self._make_port_ctx(self.AGENTS, binding_profile) context._plugin_context = 'plugin_context' self.mock_driver.SUPPORTED_BOND_MODES = {'802.3ad'} self.assertIsNone(context._bound_vif_type) self.driver.bind_port(context) self.mock_manager.assert_called_once_with('foo') self.mock_driver.create_port.assert_not_called() self.assertIsNone(context._bound_vif_type) @mock.patch.object(provisioning_blocks, 'add_provisioning_component', autospec=True) def test_no_port_id_does_not_bind_port(self, mock_p_blocks): binding_profile = {} lli = binding_profile['local_link_information'] = [] lli.append({'switch_id': 'aa:bb:cc:dd:ee:ff', 'switch_info': 'foo'}) context = self._make_port_ctx(self.AGENTS, binding_profile) context._plugin_context = 'plugin_context' self.assertIsNone(context._bound_vif_type) self.driver.bind_port(context) self.mock_manager.assert_not_called() self.mock_driver.create_port.assert_not_called() self.assertIsNone(context._bound_vif_type) @mock.patch.object(provisioning_blocks, 'provisioning_complete', autospec=True) def test_port_bound_update_port(self, mock_p_blocks): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network() m_nc.original = ml2_utils.get_test_network() m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = ml2_utils.get_test_port( network_id=m_nc.current['id'], vnic_type=portbindings.VNIC_BAREMETAL, vif_type=portbindings.VIF_TYPE_OTHER) m_pc.original = ml2_utils.get_test_port( network_id=m_nc.current['id'], vnic_type=portbindings.VNIC_BAREMETAL, vif_type=portbindings.VIF_TYPE_OTHER) m_pc.network = m_nc m_pc._plugin_context = 'plugin_context' m_pc._bound_vif_type = self.driver.vif_type self.driver.update_port_postcommit(m_pc) self.mock_manager.assert_called_once_with('foo') self.mock_driver.update_port.assert_called_once_with( m_pc, m_pc.current['binding:profile']['local_link_information']) def test_port_unbound_unplug_port(self): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network() m_nc.original = ml2_utils.get_test_network() m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = ml2_utils.get_test_port( network_id=m_nc.current['id'], vnic_type=None, vif_type=None) m_pc.original = ml2_utils.get_test_port( network_id=m_nc.current['id'], vnic_type=portbindings.VNIC_BAREMETAL, vif_type=portbindings.VIF_TYPE_OTHER) m_pc.network = m_nc self.driver.update_port_postcommit(m_pc) self.mock_manager.assert_called_once_with('foo') self.mock_driver.delete_port.assert_called_once_with( m_pc, m_pc.current['binding:profile']['local_link_information'], current=False) def test_delete_port(self): m_nc = mock.create_autospec(driver_context.NetworkContext) m_nc.current = ml2_utils.get_test_network() m_pc = mock.create_autospec(driver_context.PortContext) m_pc.current = ml2_utils.get_test_port( network_id=m_nc.current['id'], vnic_type=portbindings.VNIC_BAREMETAL, vif_type=portbindings.VIF_TYPE_OTHER) m_pc.network = m_nc self.driver.delete_port_postcommit(m_pc) self.mock_manager.assert_called_once_with('foo') self.mock_driver.delete_port.assert_called_once_with( m_pc, m_pc.current['binding:profile']['local_link_information'], current=True) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/networking_baremetal/tests/unit/plugins/ml2/utils.py0000664000175000017500000001213715157004031026756 0ustar00zuulzuul# Copyright (c) 2017 Mirantis, 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 oslo_utils import uuidutils def get_test_network(**kw): """Return a network object with appropriate attributes.""" result = { "provider:physical_network": kw.get("physical_network", "mynetwork"), "ipv6_address_scope": kw.get("ipv6_address_scope", None), "revision_number": kw.get("revision_number", 7), "port_security_enabled": kw.get("port_security_enabled", True), "mtu": kw.get("mtu", 1500), "id": kw.get("id", uuidutils.generate_uuid()), "router:external": kw.get("router:external", False), "availability_zone_hints": kw.get("availability_zone_hints", []), "availability_zones": kw.get("availability_zones", ["nova"]), "ipv4_address_scope": kw.get("ipv4_address_scope", None), "shared": kw.get("shared", False), "project_id": kw.get("project_id", uuidutils.generate_uuid()), "status": kw.get("status", "ACTIVE"), "subnets": kw.get("subnets", []), "description": kw.get("description", ""), "tags": kw.get("tags", []), "provider:segmentation_id": kw.get("segmentation_id", 113), "name": kw.get("name", "private"), "admin_state_up": kw.get("admin_state_up", True), "tenant_id": kw.get("tenant_id", uuidutils.generate_uuid()), "provider:network_type": kw.get("network_type", "flat") } return result def get_test_port(network_id, **kw): """Return a port object with appropriate attributes.""" result = { "status": kw.get("status", "DOWN"), "binding:host_id": kw.get("host_id", "aaa.host"), "description": kw.get("description", ""), "allowed_address_pairs": kw.get("allowed_address_pairs", []), "tags": kw.get("tags", []), "extra_dhcp_opts": kw.get("extra_dhcp_opts", []), "device_owner": kw.get("device_owner", "baremetal:host"), "revision_number": kw.get("revision_number", 7), "port_security_enabled": kw.get("port_security_enabled", False), "binding:profile": kw.get("binding_profile", {'local_link_information': [ {'switch_info': 'foo', 'port_id': 'Gig0/1', 'switch_id': 'aa:bb:cc:dd:ee:ff'}]}), "fixed_ips": kw.get("fixed_ips", []), "id": kw.get("id", uuidutils.generate_uuid()), "security_groups": kw.get("security_groups", []), "device_id": kw.get("device_id", ""), "name": kw.get("name", "Port1"), "admin_state_up": kw.get("admin_state_up", True), "network_id": network_id, "tenant_id": kw.get("tenant_id", uuidutils.generate_uuid()), "binding:vif_details": kw.get("vif_details", {}), "binding:vnic_type": kw.get("vnic_type", "baremetal"), "binding:vif_type": kw.get("vif_type", "unbound"), "mac_address": kw.get("mac_address", "fa:16:3e:c2:2a:8f"), "project_id": kw.get("project_id", uuidutils.generate_uuid()) } return result def get_test_subnet(network_id, **kw): """Return a subnet object with appropriate attributes.""" result = { "service_types": kw.get("service_types", []), "description": kw.get("description", ""), "enable_dhcp": kw.get("enable_dhcp", True), "tags": kw.get("tags", []), "network_id": network_id, "tenant_id": kw.get("tenant_id", uuidutils.generate_uuid()), "dns_nameservers": kw.get("dns_nameservers", []), "gateway_ip": kw.get("gateway_ip", "10.1.0.1"), "ipv6_ra_mode": kw.get("ipv6_ra_mode", None), "allocation_pools": kw.get("allocation_pools", [{"start": "10.1.0.2", "end": "10.1.0.62"}]), "host_routes": kw.get("host_routes", []), "revision_number": kw.get("revision_number", 7), "ip_version": kw.get("ip_version", 4), "ipv6_address_mode": kw.get("ipv6_address_mode", None), "cidr": kw.get("cidr", "10.1.0.0/26"), "project_id": kw.get("project_id", uuidutils.generate_uuid()), "id": kw.get("id", uuidutils.generate_uuid()), "subnetpool_id": kw.get("subnetpool_id", uuidutils.generate_uuid()), "name": kw.get("name", "subnet0") } return result def get_test_segment(**kw): result = { 'segmentation_id': kw.get('segmentation_id', '123'), 'network_type': kw.get('network_type', 'flat'), 'id': uuidutils.generate_uuid() } return result ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8539977 networking_baremetal-7.2.0/networking_baremetal.egg-info/0000775000175000017500000000000015157004110022434 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930567.0 networking_baremetal-7.2.0/networking_baremetal.egg-info/PKG-INFO0000644000175000017500000000510715157004107023540 0ustar00zuulzuulMetadata-Version: 2.4 Name: networking-baremetal Version: 7.2.0 Summary: Neutron plugin that provides deep Ironic/Neutron integration. Author-email: OpenStack License: Apache-2.0 Project-URL: Homepage, https://docs.openstack.org/networking-baremetal/latest/ 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 :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: ncclient>=0.6.9 Requires-Dist: neutron-lib>=1.28.0 Requires-Dist: oslo.config>=9.7.1 Requires-Dist: oslo.i18n>=6.5.1 Requires-Dist: oslo.log>=7.1.0 Requires-Dist: oslo.utils>=8.2.0 Requires-Dist: oslo.messaging>=16.1.0 Requires-Dist: oslo.service[threading]>=4.2.0 Requires-Dist: pbr>=6.0.0 Requires-Dist: openstacksdk>=4.9.0 Requires-Dist: tooz>=6.3.0 Requires-Dist: neutron>=27.0.0.0rc1 Requires-Dist: tenacity>=6.0.0 Requires-Dist: keystoneauth1>=3.14.0 Dynamic: license-file Dynamic: requires-dist networking-baremetal plugin --------------------------- This project's goal is to provide deep integration between the Networking service and the Bare Metal service and advanced networking features like notifications of port status changes and routed networks support in clouds with Bare Metal service. Features -------- * **L2VNI Mechanism Driver**: Enables baremetal servers to connect to VXLAN and Geneve overlay networks by dynamically allocating VLAN segments and creating OVN localnet ports. See documentation for configuration details. * **Port Status Notifications**: Real-time notifications of port status changes from the Bare Metal service to the Networking service. * **Multi-tenant Network Support**: Advanced networking features for baremetal deployments with tenant isolation. * Free software: Apache license * Documentation: http://docs.openstack.org/networking-baremetal/latest * Source: http://opendev.org/openstack/networking-baremetal * Bugs: https://bugs.launchpad.net/networking-baremetal * Release notes: https://docs.openstack.org/releasenotes/networking-baremetal/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930567.0 networking_baremetal-7.2.0/networking_baremetal.egg-info/SOURCES.txt0000664000175000017500000002101315157004107024323 0ustar00zuulzuul.pre-commit-config.yaml .stestr.conf AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE README.rst pyproject.toml requirements.txt setup.cfg setup.py test-requirements.txt tox.ini devstack/plugin.sh devstack/settings devstack/lib/networking-baremetal doc/requirements.txt doc/source/conf.py doc/source/index.rst doc/source/admin/ha-chassis-group-alignment.rst doc/source/admin/index.rst doc/source/admin/l2vni-trunk-reconciliation.rst doc/source/admin/router-ha-binding.rst doc/source/configuration/index.rst doc/source/configuration/ironic-neutron-agent/config.rst doc/source/configuration/ironic-neutron-agent/index.rst doc/source/configuration/ironic-neutron-agent/sample-config.rst doc/source/configuration/ml2/index.rst doc/source/configuration/ml2/l2vni-example.ini doc/source/configuration/ml2/l2vni-mechanism-driver.rst doc/source/configuration/ml2/device_drivers/common_config.rst doc/source/configuration/ml2/device_drivers/index.rst doc/source/configuration/ml2/device_drivers/netconf-openconfig.rst doc/source/contributor/index.rst doc/source/contributor/quickstart-multitenant.rst doc/source/contributor/quickstart-netconf-openconfig.rst doc/source/contributor/quickstart.rst doc/source/install/index.rst networking_baremetal/__init__.py networking_baremetal/_i18n.py networking_baremetal/common.py networking_baremetal/config.py networking_baremetal/constants.py networking_baremetal/exceptions.py networking_baremetal/ironic_client.py networking_baremetal/neutron_client.py networking_baremetal.egg-info/PKG-INFO networking_baremetal.egg-info/SOURCES.txt networking_baremetal.egg-info/dependency_links.txt networking_baremetal.egg-info/entry_points.txt networking_baremetal.egg-info/not-zip-safe networking_baremetal.egg-info/pbr.json networking_baremetal.egg-info/requires.txt networking_baremetal.egg-info/top_level.txt networking_baremetal/agent/__init__.py networking_baremetal/agent/agent_config.py networking_baremetal/agent/ironic_neutron_agent.py networking_baremetal/agent/l2vni_trunk_manager.py networking_baremetal/agent/ovn_client.py networking_baremetal/agent/ovn_events.py networking_baremetal/agent/router_ha_binding.py networking_baremetal/drivers/__init__.py networking_baremetal/drivers/base.py networking_baremetal/drivers/netconf/openconfig.py networking_baremetal/openconfig/__init__.py networking_baremetal/openconfig/interfaces/__init__.py networking_baremetal/openconfig/interfaces/aggregate.py networking_baremetal/openconfig/interfaces/ethernet.py networking_baremetal/openconfig/interfaces/interfaces.py networking_baremetal/openconfig/interfaces/types.py networking_baremetal/openconfig/lacp/__init__.py networking_baremetal/openconfig/lacp/lacp.py networking_baremetal/openconfig/lacp/types.py networking_baremetal/openconfig/network_instance/__init__.py networking_baremetal/openconfig/network_instance/network_instance.py networking_baremetal/openconfig/vlan/__init__.py networking_baremetal/openconfig/vlan/types.py networking_baremetal/openconfig/vlan/vlan.py networking_baremetal/plugins/__init__.py networking_baremetal/plugins/ml2/__init__.py networking_baremetal/plugins/ml2/baremetal_l2vni_mapping.py networking_baremetal/plugins/ml2/baremetal_mech.py networking_baremetal/tests/__init__.py networking_baremetal/tests/base.py networking_baremetal/tests/unit/__init__.py networking_baremetal/tests/unit/agent/__init__.py networking_baremetal/tests/unit/agent/test_agent_config.py networking_baremetal/tests/unit/agent/test_ironic_neutron_agent.py networking_baremetal/tests/unit/agent/test_l2vni_trunk_manager.py networking_baremetal/tests/unit/agent/test_ovn_client.py networking_baremetal/tests/unit/agent/test_ovn_events.py networking_baremetal/tests/unit/agent/test_router_ha_binding.py networking_baremetal/tests/unit/drivers/__init__.py networking_baremetal/tests/unit/drivers/netconf/__init__.py networking_baremetal/tests/unit/drivers/netconf/test_openconfig.py networking_baremetal/tests/unit/ironic_agent/__init__.py networking_baremetal/tests/unit/ironic_agent/test_hashring_member_manager.py networking_baremetal/tests/unit/ironic_agent/test_ironic_agent.py networking_baremetal/tests/unit/openconfig/__init__.py networking_baremetal/tests/unit/openconfig/test_interfaces.py networking_baremetal/tests/unit/openconfig/test_lacp.py networking_baremetal/tests/unit/openconfig/test_network_instance.py networking_baremetal/tests/unit/openconfig/test_vlan.py networking_baremetal/tests/unit/plugins/__init__.py networking_baremetal/tests/unit/plugins/ml2/__init__.py networking_baremetal/tests/unit/plugins/ml2/test_baremetal_l2vni_mech.py networking_baremetal/tests/unit/plugins/ml2/test_baremetal_mech.py networking_baremetal/tests/unit/plugins/ml2/utils.py releasenotes/notes/.placeholder releasenotes/notes/add-initial-note-8f08fd95b0149b2c.yaml releasenotes/notes/add-l2vni-mechanism-driver-a7f9e2c4b8d3a1f5.yaml releasenotes/notes/agent-notification-auto-delete-queues-a3782fbeea2b57b1.yaml releasenotes/notes/auto-detect-l2vni-overlay-support-93785319735a4cc8.yaml releasenotes/notes/cleanup-orphan-agents-236721a7sh844315.yaml releasenotes/notes/conductor-groups-filtering-support.yaml releasenotes/notes/device-manager-driver-interface-741703fbfc063780.yaml releasenotes/notes/distributed-agent-hashring-6b623a7a9caf0425.yaml releasenotes/notes/drop-py-2-7-2249129e616bb1e5.yaml releasenotes/notes/fix-conflict-with-ngs-41a862c292718c3b.yaml releasenotes/notes/fix-exception-handling-openstacksdk-d3eff6f9fe9ea42f.yaml releasenotes/notes/fix-ha-reconcile-missing-ports-3d7e4c81bfd466b9.yaml releasenotes/notes/fix-hash-ring-after-eventlet-removal-b3e1967d57bea523.yaml releasenotes/notes/fix-is-partial-segment-call-bug2144505-63609991e8aff0c6.yaml releasenotes/notes/fix-l2vni-multi-link-lag-support-bug2144476-488d19606296d61e.yaml releasenotes/notes/fix-l2vni-trunk-port-binding-ef14191976236305.yaml releasenotes/notes/fix-l2vni-trunk-subport-vni-mapping-2cb690055917a6da.yaml releasenotes/notes/fix-member-manager-notification-queue-not-consumed-449738d4fd799634.yaml releasenotes/notes/fix-orphaned-localnet-ports-943b2ea6ebe40f2a.yaml releasenotes/notes/fix-ovn-idl-api-bug-2143988-8f3d9a2c1e5b7d4a.yaml releasenotes/notes/fix-router-interface-ha-binding-6629cb075748eed4.yaml releasenotes/notes/fix-snat-gateway-chassis-a8f91c2d4e7b3fa1.yaml releasenotes/notes/fix_autodelete_for_quorum_queues-e001f2d8d8ae780b.yaml releasenotes/notes/force-exit-on-comm-failure-d0a584af6a3bb373.yaml releasenotes/notes/ha-chassis-group-alignment-8f2d1e5c9a4b6e3d.yaml releasenotes/notes/ironic-neutron-agent-291f8aad7d53f06c.yaml releasenotes/notes/l2vni-targeted-single-vlan-reconciliation-14d87599a0055136.yaml releasenotes/notes/l2vni-trunk-reconciliation-4dced39222f4657e.yaml releasenotes/notes/mech-agent-driver-ffc361e528668f8e.yaml releasenotes/notes/netconf-openconfig-device-driver-8fc15c9c2dc4bf17.yaml releasenotes/notes/netconf-openconfig-device-driver-lacp-support-ed9e1bc0eb784c9b.yaml releasenotes/notes/netconf-openconfig-device-driver-pre-configured-link-aggregates-2dcba8f96500d159.yaml releasenotes/notes/no-explicit-eventlet-use-19a2287e1f15ab71.yaml releasenotes/notes/openconfig-library-5ecd1f158666c6c5.yaml releasenotes/notes/ovn-event-driven-reconciliation-a62685c58a778927.yaml releasenotes/notes/pre-commit-a3de910e07fde16e.yaml releasenotes/notes/remove-py38-f406f765564942b7.yaml releasenotes/notes/remove-py39-5749e4aa121787c3.yaml releasenotes/notes/replace-ironicclient-with-openstacksdk-75d1edd705571f94.yaml releasenotes/notes/sighup-service-reloads-configs-11cd374cc33aac83.yaml releasenotes/notes/skip-l2vni-anchor-network-binding-a658fedb85c8818e.yaml releasenotes/notes/vlan-type-support-mech-driver-31f907c76dc44923.yaml releasenotes/source/2023.1.rst releasenotes/source/2023.2.rst releasenotes/source/2024.1.rst releasenotes/source/2024.2.rst releasenotes/source/2025.1.rst releasenotes/source/2025.2.rst releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/pike.rst releasenotes/source/queens.rst releasenotes/source/rocky.rst releasenotes/source/stein.rst releasenotes/source/train.rst releasenotes/source/unreleased.rst releasenotes/source/ussuri.rst releasenotes/source/victoria.rst releasenotes/source/wallaby.rst releasenotes/source/xena.rst releasenotes/source/yoga.rst releasenotes/source/zed.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder tools/flake8wrap.sh tools/run_bashate.sh tools/config/networking-baremetal-common-device-driver-opts.conf tools/config/networking-baremetal-ironic-neutron-agent.conf tools/config/networking-baremetal-netconf-openconfig-driver-opts.conf zuul.d/networking-baremetal-jobs.yaml zuul.d/project.yaml././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930567.0 networking_baremetal-7.2.0/networking_baremetal.egg-info/dependency_links.txt0000664000175000017500000000000115157004107026510 0ustar00zuulzuul ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930567.0 networking_baremetal-7.2.0/networking_baremetal.egg-info/entry_points.txt0000664000175000017500000000147615157004107025750 0ustar00zuulzuul[console_scripts] ironic-neutron-agent = networking_baremetal.agent.ironic_neutron_agent:main [networking_baremetal.drivers] netconf-openconfig = networking_baremetal.drivers.netconf.openconfig:NetconfOpenConfigDriver [neutron.ml2.mechanism_drivers] baremetal = networking_baremetal.plugins.ml2.baremetal_mech:BaremetalMechanismDriver baremetal-l2vni = networking_baremetal.plugins.ml2.baremetal_l2vni_mapping:L2vniMechanismDriver [oslo.config.opts] baremetal = networking_baremetal.config:list_opts common-device-driver-opts = networking_baremetal.config:list_common_device_driver_opts ironic-client = networking_baremetal.ironic_client:list_opts ironic-neutron-agent = networking_baremetal.agent.ironic_neutron_agent:list_opts netconf-openconfig-driver-opts = networking_baremetal.drivers.netconf.openconfig:list_driver_opts ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930567.0 networking_baremetal-7.2.0/networking_baremetal.egg-info/not-zip-safe0000664000175000017500000000000115157004107024670 0ustar00zuulzuul ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930567.0 networking_baremetal-7.2.0/networking_baremetal.egg-info/pbr.json0000664000175000017500000000005615157004107024121 0ustar00zuulzuul{"git_version": "8356d4f", "is_release": true}././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930567.0 networking_baremetal-7.2.0/networking_baremetal.egg-info/requires.txt0000664000175000017500000000040615157004107025042 0ustar00zuulzuulncclient>=0.6.9 neutron-lib>=1.28.0 oslo.config>=9.7.1 oslo.i18n>=6.5.1 oslo.log>=7.1.0 oslo.utils>=8.2.0 oslo.messaging>=16.1.0 oslo.service[threading]>=4.2.0 pbr>=6.0.0 openstacksdk>=4.9.0 tooz>=6.3.0 neutron>=27.0.0.0rc1 tenacity>=6.0.0 keystoneauth1>=3.14.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930567.0 networking_baremetal-7.2.0/networking_baremetal.egg-info/top_level.txt0000664000175000017500000000002515157004107025171 0ustar00zuulzuulnetworking_baremetal ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/pyproject.toml0000664000175000017500000000567215157004031017467 0ustar00zuulzuul[build-system] requires = ["pbr>=6.1.1"] build-backend = "pbr.build" [project] name = "networking-baremetal" description = "Neutron plugin that provides deep Ironic/Neutron integration." authors = [ {name = "OpenStack", email = "openstack-discuss@lists.openstack.org"}, ] readme = {file = "README.rst", content-type = "text/x-rst"} license = {text = "Apache-2.0"} dynamic = ["version", "dependencies"] requires-python = ">=3.10" classifiers = [ "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 :: Implementation :: CPython", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] [project.urls] Homepage = "https://docs.openstack.org/networking-baremetal/latest/" [project.entry-points."oslo.config.opts"] ironic-neutron-agent = "networking_baremetal.agent.ironic_neutron_agent:list_opts" ironic-client = "networking_baremetal.ironic_client:list_opts" baremetal = "networking_baremetal.config:list_opts" common-device-driver-opts = "networking_baremetal.config:list_common_device_driver_opts" netconf-openconfig-driver-opts = "networking_baremetal.drivers.netconf.openconfig:list_driver_opts" [project.scripts] ironic-neutron-agent = "networking_baremetal.agent.ironic_neutron_agent:main" [project.entry-points."neutron.ml2.mechanism_drivers"] baremetal = "networking_baremetal.plugins.ml2.baremetal_mech:BaremetalMechanismDriver" baremetal-l2vni = "networking_baremetal.plugins.ml2.baremetal_l2vni_mapping:L2vniMechanismDriver" [project.entry-points."networking_baremetal.drivers"] netconf-openconfig = "networking_baremetal.drivers.netconf.openconfig:NetconfOpenConfigDriver" [tool.setuptools] packages = [ "networking_baremetal" ] [tool.codespell] quiet-level = 4 # Words to ignore: # assertIn: Python's unittest method ignore-words-list = "assertIn" skip = "releasenotes/notes/force-exit-on-comm-failure-d0a584af6a3bb373.yaml" [tool.doc8] ignore = ["D001"] [tool.ruff] line-length = 79 target-version = "py310" [tool.ruff.lint] select = [ "E", # pycodestyle (error) "F", # pyflakes "G", # flake8-logging-format "LOG", # flake8-logging "S", # flake8-bandit ] [tool.ruff.lint.per-file-ignores] "networking_baremetal/agent/ironic_neutron_agent.py" = [ "E402", # we need to monkey patch before import ] "networking_baremetal/drivers/netconf/openconfig.py" = [ "S311", # we don't need a secure random choice here "S314", # keep using xml since that's been in use ] "networking_baremetal/tests/unit/drivers/netconf/test_openconfig.py" = [ "S101", # test uses assert ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8289936 networking_baremetal-7.2.0/releasenotes/0000775000175000017500000000000015157004110017230 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1773930567.849997 networking_baremetal-7.2.0/releasenotes/notes/0000775000175000017500000000000015157004110020360 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/.placeholder0000664000175000017500000000000015157004031022633 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/add-initial-note-8f08fd95b0149b2c.yaml0000664000175000017500000000052615157004031026615 0ustar00zuulzuul--- prelude: > This is the initial release of the networking-baremetal. The project includes the ``baremetal`` ml2 mechanism driver performing binding of the Networking service ports with ``binding_vnic_type=baremetal`` in flat networks. It also includes the devstack plugin to simplify the development setup and testing. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/add-l2vni-mechanism-driver-a7f9e2c4b8d3a1f5.yaml0000664000175000017500000000135415157004031030644 0ustar00zuulzuul--- features: - | Adds a new L2VNI (Layer 2 Virtual Network Identifier) mechanism driver that enables baremetal servers to connect to VXLAN and Geneve overlay networks. The driver automatically allocates dynamic VLAN segments on physical networks and creates OVN localnet ports to bridge overlay traffic to baremetal nodes. To enable the driver, add ``baremetal_l2vni`` to the mechanism_drivers list in ``ml2_conf.ini``: .. code-block:: ini [ml2] mechanism_drivers = ovn,baremetal_l2vni [baremetal_l2vni] create_localnet_ports = True default_physical_network = physnet1 See the L2VNI mechanism driver documentation for complete configuration and deployment details. ././@PaxHeader0000000000000000000000000000021100000000000010207 xustar00115 path=networking_baremetal-7.2.0/releasenotes/notes/agent-notification-auto-delete-queues-a3782fbeea2b57b1.yaml 22 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/agent-notification-auto-delete-queues-a3782fbeea2b57b10000664000175000017500000000303315157004031032151 0ustar00zuulzuul--- upgrade: - | To fix `bug: 2004933 `_ oslo.messaging notification queues are now renamed and created with ``amqp_auto_delete=true``. When upgrading the agent old queues should be deleted to free up message broker resources. Previous queue that can be deleted are named ``ironic-neutron-agent-heartbeat.info``. There may also be queues with uuid of previous agent instances as name, these can also safely be deleted. (Look in the agent logs for relevant agent uuids). On rabbitmq queues can be deleted via the web console. For example with curl:: curl -i -u username:password \ -H "content-type:application/json" -XDELETE \ http://:/api/queues// Another example with vhost: '/' deleting the ironic-neutron-agent-heartbeat.info queue:: curl -i -u username:password \ -H "content-type:application/json" \ -XDELETE \ http://172.20.0.1:15672/api/queues/%2F/ironic-neutron-agent-heartbeat.info .. Note:: In the example above the vhost is ``/``. To ensure the vhost is correctly encoded the use of ``%2F``, instead of ``/`` is required. fixes: - | Fixes an issue where old oslo.messaging notification pool queues remained in the broker without any consumer after agent restart. The notification queues will now be created with ``amqp_auto_delete=true``. See `bug: 2004933 `_. ././@PaxHeader0000000000000000000000000000020500000000000010212 xustar00111 path=networking_baremetal-7.2.0/releasenotes/notes/auto-detect-l2vni-overlay-support-93785319735a4cc8.yaml 22 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/auto-detect-l2vni-overlay-support-93785319735a4cc8.yam0000664000175000017500000000125615157004031031551 0ustar00zuulzuul--- fixes: - | Fixes an issue where Neutron would reject baremetal port binding to VXLAN and Geneve overlay networks with error "Host is not connected to any segments on routed provider network" when the segments service plugin was enabled. The baremetal-l2vni mechanism driver now reports support for overlay network types (vxlan, geneve) to enable hierarchical port binding. The base baremetal mechanism driver continues to handle only flat and vlan networks. This allows baremetal nodes to successfully bind to tenant overlay networks using the L2VNI hierarchical binding model without being incorrectly treated as routed provider networks. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/cleanup-orphan-agents-236721a7sh844315.yaml0000664000175000017500000000050715157004031027461 0ustar00zuulzuul--- fixes: - | Clean up orphan baremetal agents which are left after Ironic's node was deleted. Removes all agents without related node at service start up and when node is not found in port list but was reported before. See `bug: 2086640 `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/conductor-groups-filtering-support.yaml0000664000175000017500000000036615157004031030263 0ustar00zuulzuul--- features: - | Adds support for filtering ports by conductor groups. When ``[conductor_groups]conductor_groups`` is configured, the agent will only query ports associated with nodes in the specified list of conductor groups. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/device-manager-driver-interface-741703fbfc063780.yaml0000664000175000017500000000033015157004031031414 0ustar00zuulzuul--- features: - | A device management driver interface using stevedore for dynamic loading has been added. The base driver includes two abstract base classes `BaseDeviceDriver` and `BaseDeviceClient`. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/distributed-agent-hashring-6b623a7a9caf0425.yaml0000664000175000017500000000056715157004031030704 0ustar00zuulzuul--- features: - | Adds support for load distribution when multiple instances of the networking-baremetal agent are running. Each instance will manage a subset of bare metal nodes. In case one or more instances of networking-baremetal agent is lost, the remaining instances will take over the bare metal nodes previously managed by the lost instance(s). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/drop-py-2-7-2249129e616bb1e5.yaml0000664000175000017500000000034615157004031025221 0ustar00zuulzuul--- upgrade: - | Python 2.7 support has been dropped. Last release of Networking Baremetal to support Python 2.7 is OpenStack Train. The minimum version of Python now supported by Networking Baremetal is Python 3.6. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix-conflict-with-ngs-41a862c292718c3b.yaml0000664000175000017500000000024215157004031027442 0ustar00zuulzuul--- fixes: - | Fixes an issue when networking-baremetal try to bind port that should be handled by other ml2 driver like networking-generic-switch. ././@PaxHeader0000000000000000000000000000020700000000000010214 xustar00113 path=networking_baremetal-7.2.0/releasenotes/notes/fix-exception-handling-openstacksdk-d3eff6f9fe9ea42f.yaml 22 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix-exception-handling-openstacksdk-d3eff6f9fe9ea42f.y0000664000175000017500000000050715157004031032351 0ustar00zuulzuul--- fixes: - | Fixed the incorrect handling of exceptions from openstacksdk when querying the list of ports from ironic, that caused the agent to stop reporting its state. Also when there are problems querying ports, agent now does not report an empty state, and rather waits for the next iteration to retry. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix-ha-reconcile-missing-ports-3d7e4c81bfd466b9.yaml0000664000175000017500000000056715157004031031516 0ustar00zuulzuul--- fixes: - | Fixed HA chassis group alignment reconciliation to properly look up ports in OVN using the correct "neutron-" prefixed naming convention. Port lookups now handle RowNotFound exceptions gracefully and prevent misleading ERROR logs for expected conditions. See `bug 2144061 `_. ././@PaxHeader0000000000000000000000000000021000000000000010206 xustar00114 path=networking_baremetal-7.2.0/releasenotes/notes/fix-hash-ring-after-eventlet-removal-b3e1967d57bea523.yaml 22 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix-hash-ring-after-eventlet-removal-b3e1967d57bea523.0000664000175000017500000000142515157004031031637 0ustar00zuulzuul--- fixes: - | Fixed hash ring synchronization broken after eventlet removal. The agent now uses ``service.Launcher`` for single-process execution instead of ``service.launch()`` which spawns worker processes via fork. With forked processes, the ``HashRingMemberManagerNotificationEndpoint`` class variables (``members`` and ``hashring``) were not shared between the parent process (handling OVN events and operations) and worker processes (receiving heartbeat notifications), causing hash ring checks to always fail in the parent process. Single-process execution ensures all threads share the same memory space and class variables work correctly. See `LP#2144384 `_ for details. ././@PaxHeader0000000000000000000000000000021200000000000010210 xustar00116 path=networking_baremetal-7.2.0/releasenotes/notes/fix-is-partial-segment-call-bug2144505-63609991e8aff0c6.yaml 22 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix-is-partial-segment-call-bug2144505-63609991e8aff0c0000664000175000017500000000101715157004031031113 0ustar00zuulzuul--- fixes: - | Fixed incorrect ``is_partial_segment()`` call in the baremetal_l2vni mechanism driver. The driver was calling ``context.is_partial_segment()`` which doesn't exist on PortContext. Changed to correctly call ``context._plugin.type_manager.is_partial_segment()`` to access the TypeDriver API. This bug was exposed when neutron started including dynamic segments in the network context. For more details see `bug 2144505 `_. ././@PaxHeader0000000000000000000000000000021700000000000010215 xustar00121 path=networking_baremetal-7.2.0/releasenotes/notes/fix-l2vni-multi-link-lag-support-bug2144476-488d19606296d61e.yaml 22 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix-l2vni-multi-link-lag-support-bug2144476-488d1960620000664000175000017500000000165415157004031031173 0ustar00zuulzuul--- fixes: - | Fixed L2VNI trunk manager to support multiple physical links per (chassis, physical_network) combination for LAG/bonding. Previously only the first link was used, preventing proper link aggregation. The fix aggregates all matching links from OVN LLDP, Ironic ports, and YAML config. See `bug 2144476 `_. - | Fixed OVN LLDP data collection to properly filter ports by bridge, preventing switch connection information from the wrong physical network being used in multi-physnet deployments. See `bug 2144482 `_. deprecations: - | The ``local_link_connection`` field in network node YAML configuration is deprecated in favor of ``local_link_information`` to match Neutron API naming. The old field name is still supported with a deprecation warning. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix-l2vni-trunk-port-binding-ef14191976236305.yaml0000664000175000017500000000063515157004031030544 0ustar00zuulzuul--- fixes: - | Fixed L2VNI trunk port binding to properly configure switch ports via networking-generic-switch. Anchor ports now include ``local_link_information`` in their binding profile, and subports have ``binding:host_id`` set to enable ML2 plugin binding. Existing trunks are automatically reconciled. See `bug 2144387 `_. ././@PaxHeader0000000000000000000000000000020700000000000010214 xustar00113 path=networking_baremetal-7.2.0/releasenotes/notes/fix-l2vni-trunk-subport-vni-mapping-2cb690055917a6da.yaml 22 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix-l2vni-trunk-subport-vni-mapping-2cb690055917a6da.y0000664000175000017500000000076015157004031031607 0ustar00zuulzuul--- fixes: - | Fixed L2VNI trunk subports to include VNI (VXLAN/Geneve segment ID) information in their ``binding:profile['vni']`` field, enabling ML2 mechanism drivers to configure complete VLAN-to-VNI mappings on physical network switches. Previously, subports only contained ``physical_network`` and VLAN ID, preventing proper EVPN/VXLAN configuration. For more information, see `bug 2144396 `_. ././@PaxHeader0000000000000000000000000000022600000000000010215 xustar00128 path=networking_baremetal-7.2.0/releasenotes/notes/fix-member-manager-notification-queue-not-consumed-449738d4fd799634.yaml 22 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix-member-manager-notification-queue-not-consumed-4490000664000175000017500000000125715157004031032512 0ustar00zuulzuul--- fixes: - | Fixes an issue causing heavy RAM (and/or-storage) usage on the message broker back-end. The ``ironic-neutron-agent`` uses oslo.messaging notifications, with all notification listeners using pools. Since all listeners are using pools the default notification queue in messaging is not consumed (only the pool queues are consumed). The default notification queue was continuously growing, consuming more and more resources on the messaging back-end. See `oslo.messaging bug: 1814544 `_ and `bug: 2004938 `_ for more details. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix-orphaned-localnet-ports-943b2ea6ebe40f2a.yaml0000664000175000017500000000171515157004031031150 0ustar00zuulzuul--- fixes: - | Fixes an issue in the baremetal-l2vni mechanism driver where OVN localnet ports could become orphaned with stale VLAN tags when dynamic segments were released and reallocated. This could result in VLAN tag mismatches causing traffic to be tagged with incorrect VLAN IDs. The driver now validates that existing localnet ports have the correct VLAN tag matching the current dynamic segment allocation. If a mismatch is detected (for example, the localnet port has VLAN 107 but the current segment uses VLAN 135), the stale port is automatically deleted and recreated with the correct tag. Additionally, the driver now implements proper cleanup in ``delete_port_postcommit()`` to remove localnet ports and release dynamic segments when the last baremetal port using a segment is deleted. This prevents orphaned localnet ports and ensures VLAN IDs can be safely reused without configuration conflicts. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix-ovn-idl-api-bug-2143988-8f3d9a2c1e5b7d4a.yaml0000664000175000017500000000073015157004031030142 0ustar00zuulzuul--- fixes: - | Fixed AttributeError in ironic-neutron-agent when attempting to align HA chassis groups for baremetal ports. The OVN client was returning raw IDL objects instead of API objects, causing "AttributeError: 'Idl' object has no attribute 'lsp_get'" errors. The get_ovn_nb_idl() and get_ovn_sb_idl() functions now correctly return OvnNbApiIdlImpl and OvnSbApiIdlImpl instances that provide the required API methods. See bug 2143988. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix-router-interface-ha-binding-6629cb075748eed4.yaml0000664000175000017500000000166515157004031031470 0ustar00zuulzuul--- features: - | Added event-driven router interface HA chassis group binding to fix connectivity issues between baremetal nodes and routers on VLAN networks. A new RouterHABindingManager monitors OVN HA_Chassis_Group creation events and automatically binds router interface ports to the network's HA chassis group, enabling proper ARP resolution on physical networks. The manager includes both event-driven binding (for immediate response) and periodic reconciliation (for routers added after the fact or missed events). fixes: - | Fixed persistent connectivity failures between baremetal nodes and routers on VLAN networks. Router interface ports are now automatically bound to the network's HA chassis group, enabling proper ARP resolution. See `bug 2144458 `_ and `bug 1995078 `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix-snat-gateway-chassis-a8f91c2d4e7b3fa1.yaml0000664000175000017500000000105615157004031030447 0ustar00zuulzuul--- fixes: - | Fixes an issue in the baremetal-l2vni mechanism driver where ``_ensure_router_gateway_chassis()`` was incorrectly setting gateway_chassis on all router ports connected to L2VNI networks, including internal router ports. This caused OVN to treat them as distributed gateway ports, which prevented NAT flows from being created when multiple such ports existed. The function has been removed and replaced with a ``requested-chassis`` option on localnet ports to ensure they remain pinned to the correct chassis. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/fix_autodelete_for_quorum_queues-e001f2d8d8ae780b.yaml0000664000175000017500000000041515157004031032406 0ustar00zuulzuul--- fixes: - | Fixes a bug which tried to force delete queues when quorum queues are enabled. Quorum queues do not support the auto-delete feature. See the `bugreport 2046962 `_ for details. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/force-exit-on-comm-failure-d0a584af6a3bb373.yaml0000664000175000017500000000074515157004031030576 0ustar00zuulzuul--- fixes: - | Fixes cases where the ``ironic-neturon-agent`` could enter a state where it is no longer operating properly by forcing the agent to exit when "hard" communication failures occur which cannot be retried automatically. This allows the service runner to understand a failure has occured and to restart the agent. The process exit is self-triggered by the agent through the use of the SIGABRT signal to indicate non-normal process termination. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/ha-chassis-group-alignment-8f2d1e5c9a4b6e3d.yaml0000664000175000017500000000421215157004031030757 0ustar00zuulzuul--- features: - | Added HA chassis group alignment reconciliation to ironic-neutron-agent to address connectivity issues for baremetal nodes in OVN deployments (Launchpad bug #1995078). When router gateway ports and baremetal external ports have mismatched HA chassis group priorities, baremetal nodes can experience intermittent external connectivity failures. This occurs when different chassis are active for the router vs. the baremetal port, causing inconsistent routing. The ironic-neutron-agent now includes a periodic reconciliation loop that ensures router ports use the same ha_chassis_group as baremetal external ports on the same network. This eliminates priority mismatches and prevents connectivity failures. Key features: * Automatic alignment of HA chassis groups between router and baremetal ports * Distributed across multiple agents via hash ring for scalability * Time-windowed reconciliation to minimize API load (only checks recently created/updated resources by default) * Configurable reconciliation interval (default: 10 minutes) * Enabled by default Configuration options in the ``[baremetal_agent]`` section: * ``enable_ha_chassis_group_alignment`` - Enable/disable the feature (default: True) * ``ha_chassis_group_alignment_interval`` - Reconciliation interval in seconds (default: 600, minimum: 60) * ``limit_ha_chassis_group_alignment_to_recent_changes_only`` - Only check recently updated resources (default: True) * ``ha_chassis_group_alignment_window`` - Time window for recent resources in seconds (default: 1200) See the HA Chassis Group Alignment documentation for detailed configuration guidance and troubleshooting information. fixes: - | Fixes intermittent external connectivity failures for baremetal nodes in OVN deployments caused by mismatched HA chassis group priorities between router gateway ports and baremetal external ports (Launchpad bug #1995078). The ironic-neutron-agent now automatically aligns HA chassis group configuration to ensure consistent routing behavior. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/ironic-neutron-agent-291f8aad7d53f06c.yaml0000664000175000017500000000107315157004031027615 0ustar00zuulzuul--- features: - Add neutron agent ``ironic-neutron-agent`` to enable integration with neutron routed provider networks. The ml2 agent reports the state of ironic ports associated with ironic nodes to neutron, it populates the bridge_mappings configuration for each ironic node. The agent data can be used by the neutron segments plug-in in conjunction with neutron ml2 mechanism driver to ensure that port binding and ipam ip address allocations are taken from subnets associated with physical network segments available to the ironic port. ././@PaxHeader0000000000000000000000000000021500000000000010213 xustar00119 path=networking_baremetal-7.2.0/releasenotes/notes/l2vni-targeted-single-vlan-reconciliation-14d87599a0055136.yaml 22 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/l2vni-targeted-single-vlan-reconciliation-14d87599a0050000664000175000017500000000064115157004031031754 0ustar00zuulzuul--- features: - | Added targeted single-VLAN reconciliation for OVN event-driven L2VNI trunk management. When localnet port events are received, the agent now updates only the specific VLAN from the event rather than scanning all VLANs, significantly reducing reconciliation time and API calls. This works with ``enable_l2vni_trunk_reconciliation_events = True`` in the ``[l2vni]`` section. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/l2vni-trunk-reconciliation-4dced39222f4657e.yaml0000664000175000017500000000104115157004031030661 0ustar00zuulzuul--- features: - | Added an experimental L2VNI trunk reconciliation pattern to ironic-neutron-agent. This feature automatically manages trunk ports on network nodes for bridging OVN overlay networks to physical infrastructure based on ha_chassis_group membership. The feature is enabled by default. Disable it by setting ``enable_l2vni_trunk_reconciliation = False`` in the ``[l2vni]`` section of neutron.conf. See the L2VNI Trunk Reconciliation documentation for configuration options and deployment guidance. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/mech-agent-driver-ffc361e528668f8e.yaml0000664000175000017500000000036615157004031027015 0ustar00zuulzuul--- features: - Baremetal ml2 mechanism driver integration with the L2 agent. This enables the ml2 mechanism driver to use the agent_db data when binding ports. E.g the bridge mappings to enable binding on routed provider networks. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/netconf-openconfig-device-driver-8fc15c9c2dc4bf17.yaml0000664000175000017500000000045115157004031032135 0ustar00zuulzuul--- features: - | Added an `OpenConfig `__ based device driver (driver name: ``netconf-openconfig``) using Network Configuration Protocol (**NETCONF**). Implements network create, delete and update functionality as well as port create, delete and update. ././@PaxHeader0000000000000000000000000000022100000000000010210 xustar00123 path=networking_baremetal-7.2.0/releasenotes/notes/netconf-openconfig-device-driver-lacp-support-ed9e1bc0eb784c9b.yaml 22 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/netconf-openconfig-device-driver-lacp-support-ed9e1bc00000664000175000017500000000020415157004031032531 0ustar00zuulzuul--- features: - | Added support to configure LACP (802.3ad) link-aggregates in the ``netconf-openconfig`` device driver. ././@PaxHeader0000000000000000000000000000024300000000000010214 xustar00141 path=networking_baremetal-7.2.0/releasenotes/notes/netconf-openconfig-device-driver-pre-configured-link-aggregates-2dcba8f96500d159.yaml 22 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/netconf-openconfig-device-driver-pre-configured-link-a0000664000175000017500000000035015157004031032672 0ustar00zuulzuul--- features: - | Added support for *pre-configured* link-aggregates in the ``netconf-openconfig`` device driver. This is useful for the following linux bond modes: * balance-rr * balance-xor * broadcast ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/no-explicit-eventlet-use-19a2287e1f15ab71.yaml0000664000175000017500000000050115157004031030243 0ustar00zuulzuul--- other: - | Explicit usage and monkey-patching with eventlet have been removed. Since this is a Neutron plugin, Neutron may still load eventlet modules, but networking-baremetal no longer explicitly uses it. Operators should be sensitive to potential performance changes, although none are expected. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/openconfig-library-5ecd1f158666c6c5.yaml0000664000175000017500000000160215157004031027264 0ustar00zuulzuul--- features: - | `OpenConfig `_ YANG data model python bindings. Bindings for a subset of the OpenConfig YANG models has been added, these bindings can be used to build a structured configuration that can be serialized and sent to a network device (switch) where it will be parsed and applied. Serialization to XML which can be used with Network Configuration Protocol (NETCONF) has been implemented. The bindings is only a small subset of the following YANG models, it implements what is required to provide a good feature-set for BMaaS use case. * \http://openconfig.net/yang/interfaces * \http://openconfig.net/yang/interfaces/ethernet * \http://openconfig.net/yang/vlan * \http://openconfig.net/yang/network-instance * \http://openconfig.net/yang/interfaces/aggregate * \http://openconfig.net/yang/lacp ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/ovn-event-driven-reconciliation-a62685c58a778927.yaml0000664000175000017500000000076015157004031031501 0ustar00zuulzuul--- features: - | Added OVN event-driven reconciliation for L2VNI trunk management. The ironic-neutron-agent now watches OVN Northbound database for localnet port events, triggering immediate reconciliation when baremetal ports are bound or unbound. This provides sub-second reconciliation latency compared to the periodic interval (default 300 seconds). The feature is enabled by default via ``enable_l2vni_trunk_reconciliation_events`` in the ``[l2vni]`` section. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/pre-commit-a3de910e07fde16e.yaml0000664000175000017500000000014215157004031025672 0ustar00zuulzuul--- other: - | Add support for using pre-commit to run checks more easily for contributors. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/remove-py38-f406f765564942b7.yaml0000664000175000017500000000016615157004031025363 0ustar00zuulzuul--- upgrade: - | Support for Python 3.8 has been removed. Now the minimum python version supported is 3.9 . ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/remove-py39-5749e4aa121787c3.yaml0000664000175000017500000000016615157004031025431 0ustar00zuulzuul--- upgrade: - | Support for Python 3.9 has been removed. Now Python 3.10 is the minimum version supported. ././@PaxHeader0000000000000000000000000000021200000000000010210 xustar00116 path=networking_baremetal-7.2.0/releasenotes/notes/replace-ironicclient-with-openstacksdk-75d1edd705571f94.yaml 22 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/replace-ironicclient-with-openstacksdk-75d1edd705571f90000664000175000017500000000170315157004031032124 0ustar00zuulzuul--- upgrade: - | Operators using ironic-neutron-agent with ``noauth`` authentication strategy (i.e standalone ironic without keystone) must update the configuration. Replace ``[ironic]/auth_strategy = noauth`` with ``[ironic]/auth_type = none`` and set the ``[ironic]/endpoint_override`` option accordingly. deprecations: - | With the switch from ironicclient to openstacksdk the following options has been deprecated. * ``[ironic]/ironic_url`` replaced by ``[ironic]/endpoint_override`` * ``[ironic]/os_region`` replaced by ``[ironic]/region_name`` * ``[ironic]/retry_interval`` replaced by ``[ironic]/status_code_retries`` * ``[ironic]/max_retries`` replaced by ``[ironic]/status_code_retry_delay`` * ``[ironic]/auth_strategy`` is **ignored**, please use ``[ironic]/auth_type`` instead. other: - | Communication with ironic is now using openstacksdk, removing the dependency on ironicclient. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/sighup-service-reloads-configs-11cd374cc33aac83.yaml0000664000175000017500000000103215157004031031534 0ustar00zuulzuul--- features: - | Issuing a SIGHUP (e.g. ``pkill -HUP ironic-neutron-agent``) to the agent service will cause the service to reload and use any changed values for *mutable* configuration options. Mutable configuration options are indicated as such in the `sample configuration file `_ by ``Note: This option can be changed without restarting``. A warning is logged for any changes to immutable configuration options. ././@PaxHeader0000000000000000000000000000020500000000000010212 xustar00111 path=networking_baremetal-7.2.0/releasenotes/notes/skip-l2vni-anchor-network-binding-a658fedb85c8818e.yaml 22 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/skip-l2vni-anchor-network-binding-a658fedb85c8818e.yam0000664000175000017500000000044615157004031031763 0ustar00zuulzuul--- other: - | The baremetal-l2vni ML2 driver now skips binding for ports on the L2VNI anchor network (l2vni-subport-anchor). This prevents unnecessary hierarchical binding attempts for metadata-only trunk subports. Switch configuration continues to work via trunk callbacks. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/notes/vlan-type-support-mech-driver-31f907c76dc44923.yaml0000664000175000017500000000133515157004031031170 0ustar00zuulzuul--- features: - | Add support for type ``vlan`` networks in baremetal ml2 mechanism driver. This enables binding on networks using vlans for segmentation. It is only setting type ``vlan`` as supported. The intent is to use this in combination with another neutron mechanism driver that actually knows how to configure the network devices. .. Note:: The driver will **not** do anything to **set up** the correct **vlan tagging** in the network infrastructure such as switches or baremetal node ports. Another ml2 mechanism driver, or some other implementation, must be enabled to perform the necessary configuration on network devices. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8519974 networking_baremetal-7.2.0/releasenotes/source/0000775000175000017500000000000015157004110020530 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/2023.1.rst0000664000175000017500000000021015157004031022002 0ustar00zuulzuul=========================== 2023.1 Series Release Notes =========================== .. release-notes:: :branch: unmaintained/2023.1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/2023.2.rst0000664000175000017500000000020215157004031022004 0ustar00zuulzuul=========================== 2023.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/2024.1.rst0000664000175000017500000000021015157004031022003 0ustar00zuulzuul=========================== 2024.1 Series Release Notes =========================== .. release-notes:: :branch: unmaintained/2024.1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/2024.2.rst0000664000175000017500000000020215157004031022005 0ustar00zuulzuul=========================== 2024.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2024.2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/2025.1.rst0000664000175000017500000000020215157004031022005 0ustar00zuulzuul=========================== 2025.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2025.1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/2025.2.rst0000664000175000017500000000020215157004031022006 0ustar00zuulzuul=========================== 2025.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2025.2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8519974 networking_baremetal-7.2.0/releasenotes/source/_static/0000775000175000017500000000000015157004110022156 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/_static/.placeholder0000664000175000017500000000000015157004031024431 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8529975 networking_baremetal-7.2.0/releasenotes/source/_templates/0000775000175000017500000000000015157004110022665 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/_templates/.placeholder0000664000175000017500000000000015157004031025140 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/conf.py0000664000175000017500000002162115157004031022033 0ustar00zuulzuul# -*- 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. # Networking Baremetal Release Notes documentation build configuration file, # created by sphinx-quickstart on Tue Nov 3 17:40:50 2015. # # 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. copyright = '2017, The Networking Baremetal team' # 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' # openstackdocstheme options openstackdocs_repo_name = 'openstack/networking-baremetal' openstackdocs_use_storyboard = False # 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 not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # 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 = 'NetworkingBaremetalReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- # 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', 'NetworkingBaremetalReleaseNotes.tex', 'Networking Baremetal Release Notes Documentation', 'Networking Baremetal Developers', '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', 'networkingbaremetalreleasenotes', 'Networking Baremetal Release Notes Documentation', ['Networking Baremetal Developers'], 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', 'NetworkingBaremetalReleaseNotes', 'Networking Baremetal Release Notes Documentation', 'networking Baremetal Developers', 'networkingbaremetalreleasenotes', 'Neutron plugin that provides deep Ironic/Neutron integration.', '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/'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/index.rst0000664000175000017500000000047215157004031022376 0ustar00zuulzuul=================================== networking_baremetal Release Notes =================================== .. toctree:: :maxdepth: 1 unreleased 2025.2 2025.1 2024.2 2024.1 2023.2 2023.1 zed yoga xena wallaby victoria ussuri train stein rocky queens pike ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/pike.rst0000664000175000017500000000021715157004031022214 0ustar00zuulzuul=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/queens.rst0000664000175000017500000000022315157004031022561 0ustar00zuulzuul=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/rocky.rst0000664000175000017500000000022115157004031022406 0ustar00zuulzuul=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/stein.rst0000664000175000017500000000022115157004031022401 0ustar00zuulzuul=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/train.rst0000664000175000017500000000026115157004031022400 0ustar00zuulzuul=========================================== Train Series (1.4.0 - 1.4.x) Release Notes =========================================== .. release-notes:: :branch: stable/train ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/unreleased.rst0000664000175000017500000000016015157004031023410 0ustar00zuulzuul============================== Current Series Release Notes ============================== .. release-notes:: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/ussuri.rst0000664000175000017500000000020215157004031022610 0ustar00zuulzuul=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/victoria.rst0000664000175000017500000000022015157004031023076 0ustar00zuulzuul============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: unmaintained/victoria ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/wallaby.rst0000664000175000017500000000021415157004031022714 0ustar00zuulzuul============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: unmaintained/wallaby ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/xena.rst0000664000175000017500000000020015157004031022207 0ustar00zuulzuul========================= Xena Series Release Notes ========================= .. release-notes:: :branch: unmaintained/xena ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/yoga.rst0000664000175000017500000000020015157004031022213 0ustar00zuulzuul========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: unmaintained/yoga ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/releasenotes/source/zed.rst0000664000175000017500000000017415157004031022050 0ustar00zuulzuul======================== Zed Series Release Notes ======================== .. release-notes:: :branch: unmaintained/zed ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/requirements.txt0000664000175000017500000000123615157004031020027 0ustar00zuulzuul# Requirements lower bounds listed here are our best effort to keep them up to # date but we do not test them so no guarantee of having them all correct. If # you find any incorrect lower bounds, let us know or propose a fix. ncclient>=0.6.9 # Apache-2.0 neutron-lib>=1.28.0 # Apache-2.0 oslo.config>=9.7.1 # Apache-2.0 oslo.i18n>=6.5.1 # Apache-2.0 oslo.log>=7.1.0 # Apache-2.0 oslo.utils>=8.2.0 # Apache-2.0 oslo.messaging>=16.1.0 # Apache-2.0 oslo.service[threading]>=4.2.0 # Apache-2.0 pbr>=6.0.0 # Apache-2.0 openstacksdk>=4.9.0 # Apache-2.0 tooz>=6.3.0 # Apache-2.0 neutron>=27.0.0.0rc1 # Apache-2.0 tenacity>=6.0.0 # Apache-2.0 keystoneauth1>=3.14.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8539977 networking_baremetal-7.2.0/setup.cfg0000664000175000017500000000011615157004110016356 0ustar00zuulzuul[metadata] name = networking-baremetal [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/setup.py0000664000175000017500000000127115157004031016254 0ustar00zuulzuul# 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. import setuptools setuptools.setup( setup_requires=['pbr>=6.0.0'], pbr=True) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/test-requirements.txt0000664000175000017500000000021615157004031021001 0ustar00zuulzuulcoverage>=4.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 testtools>=2.2.0 # MIT stestr>=2.0.0 # Apache-2.0 testscenarios>=0.4 # Apache-2.0/BSD ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8529975 networking_baremetal-7.2.0/tools/0000775000175000017500000000000015157004110015677 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8529975 networking_baremetal-7.2.0/tools/config/0000775000175000017500000000000015157004110017144 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/tools/config/networking-baremetal-common-device-driver-opts.conf0000664000175000017500000000020615157004031031133 0ustar00zuulzuul[DEFAULT] output_file = etc/neutron/plugins/ml2/common_device_driver.ini.sample wrap_width = 79 namespace = common-device-driver-opts ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/tools/config/networking-baremetal-ironic-neutron-agent.conf0000664000175000017500000000026015157004031030201 0ustar00zuulzuul[DEFAULT] output_file = etc/neutron/plugins/ml2/ironic_neutron_agent.ini.sample wrap_width = 79 namespace = ironic-neutron-agent namespace = oslo.log namespace = ironic-client ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/tools/config/networking-baremetal-netconf-openconfig-driver-opts.conf0000664000175000017500000000022715157004031032172 0ustar00zuulzuul[DEFAULT] output_file = etc/neutron/plugins/ml2/netconf_openconfig_device_driver.ini.sample wrap_width = 79 namespace = netconf-openconfig-driver-opts ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/tools/flake8wrap.sh0000775000175000017500000000073515157004031020311 0ustar00zuulzuul#!/bin/bash # # A simple wrapper around flake8 which makes it possible # to ask it to only verify files changed in the current # git HEAD patch. # # Intended to be invoked via tox: # # tox -epep8 -- -HEAD # if test "x$1" = "x-HEAD" ; then shift files=$(git diff --name-only HEAD~1 | tr '\n' ' ') echo "Running flake8 on ${files}" diff -u --from-file /dev/null ${files} | flake8 --diff "$@" else echo "Running flake8 on all files" exec flake8 "$@" fi ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/tools/run_bashate.sh0000775000175000017500000000242015157004031020531 0ustar00zuulzuul#!/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. find "$@" -not \( -type d -name .?\* -prune \) \ -type f \ -not -name \*.swp \ -not -name \*~ \ -not -name \*.xml \ -not -name \*.template \ -not -name \*.py \ \( \ -name \*.sh -or \ -wholename \*/devstack/lib/\* -or \ -wholename \*/tools/\* \ \) \ -print0 | xargs -0 bashate -v -iE006 -eE005,E042 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/tox.ini0000664000175000017500000000611515157004031016057 0ustar00zuulzuul[tox] minversion = 4.4.0 envlist = py3,pep8 [testenv] usedevelop = True setenv = PYTHONWARNINGS=default::DeprecationWarning deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = stestr run {posargs} passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY [testenv:pep8] deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure {posargs} [testenv:codespell] description = Run codespell to check spelling deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure {posargs} [testenv:venv] commands = {posargs} [testenv:cover] setenv = LANGUAGE=en_US PYTHON=coverage run --source networking_baremetal --parallel-mode commands = coverage erase stestr run {posargs} coverage combine coverage html -d cover coverage xml -o cover/coverage.xml coverage report --omit='*test*' [testenv:docs] setenv = PYTHONHASHSEED=0 sitepackages = False deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -b html doc/source doc/build/html [testenv:pdf-docs] allowlist_externals = make setenv = PYTHONHASHSEED=0 sitepackages = False deps = {[testenv:docs]deps} commands = sphinx-build -b latex doc/source doc/build/pdf make -C doc/build/pdf [testenv:releasenotes] usedevelop = False deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:genconfig] allowlist_externals = mkdir commands = mkdir -p {toxinidir}/etc/neutron/plugins/ml2 oslo-config-generator --config-file=tools/config/networking-baremetal-ironic-neutron-agent.conf oslo-config-generator --config-file=tools/config/networking-baremetal-common-device-driver-opts.conf oslo-config-generator --config-file=tools/config/networking-baremetal-netconf-openconfig-driver-opts.conf [testenv:debug] commands = oslo_debug_helper -t networking_baremetal/tests/unit {posargs} [flake8] show-source = True # E123, E125 skipped as they are invalid PEP-8. # [W503] Line break occurred before a binary operator. Conflicts with W504. ignore = E123,E125,W503 # [H106] Don't put vim configuration in source files. # [H203] Use assertIs(Not)None to check for None. # [H204] Use assert(Not)Equal to check for equality. # [H205] Use assert(Greater|Less)(Equal) for comparison. # [H210] Require 'autospec', 'spec', or 'spec_set' in mock.patch/mock.patch.object calls # [H904] Delay string interpolations at logging calls. enable-extensions=H106,H203,H204,H205,H210,H904 builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build import-order-style = pep8 application-import-names = networking_baremetal filename = *.py per-file-ignores = networking_baremetal/agent/ironic_neutron_agent.py:E402 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1773930567.8529975 networking_baremetal-7.2.0/zuul.d/0000775000175000017500000000000015157004110015760 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/zuul.d/networking-baremetal-jobs.yaml0000664000175000017500000001422715157004031023730 0ustar00zuulzuul- job: name: networking-baremetal-multitenant-vlans parent: ironic-base irrelevant-files: - ^.*\.rst$ - ^doc/.*$ - ^setup.cfg$ - ^test-requirements.txt$ - ^tools/.*$ - ^tox.ini$ required-projects: - openstack/networking-generic-switch - openstack/networking-baremetal - openstack/sushy-tools vars: tempest_test_timeout: 2400 devstack_plugins: networking-generic-switch: https://opendev.org/openstack/networking-generic-switch networking-baremetal: https://opendev.org/openstack/networking-baremetal devstack_localrc: IRONIC_DEPLOY_DRIVER: redfish IRONIC_ENABLED_BOOT_INTERFACES: redfish-virtual-media IRONIC_ENABLED_HARDWARE_TYPES: redfish IRONIC_ENABLED_MANAGEMENT_INTERFACES: redfish BUILD_TIMEOUT: 2400 ENABLE_TENANT_VLANS: True IRONIC_DEFAULT_DEPLOY_INTERFACE: direct IRONIC_DEFAULT_RESCUE_INTERFACE: "" IRONIC_ENABLED_NETWORK_INTERFACES: flat,neutron IRONIC_NETWORK_INTERFACE: neutron IRONIC_PROVISION_NETWORK_NAME: ironic-provision IRONIC_PROVISION_PROVIDER_NETWORK_TYPE: vlan IRONIC_PROVISION_SUBNET_GATEWAY: 10.0.5.1 IRONIC_PROVISION_SUBNET_PREFIX: 10.0.5.0/24 IRONIC_TEMPEST_BUILD_TIMEOUT: 2400 IRONIC_TEMPEST_WHOLE_DISK_IMAGE: True IRONIC_USE_LINK_LOCAL: True IRONIC_USE_NEUTRON_SEGMENTS: True IRONIC_VM_COUNT: 3 IRONIC_VM_EPHEMERAL_DISK: 0 IRONIC_AUTOMATED_CLEAN_ENABLED: False OVS_PHYSICAL_BRIDGE: brbm Q_USE_PROVIDERNET_FOR_PUBLIC: True PUBLIC_PHYSICAL_NETWORK: public OVS_BRIDGE_MAPPINGS: mynetwork:brbm,public:br-ex PHYSICAL_NETWORK: mynetwork Q_ML2_TENANT_NETWORK_TYPE: vlan Q_PLUGIN: ml2 Q_SERVICE_PLUGIN_CLASSES: neutron.services.l3_router.l3_router_plugin.L3RouterPlugin,segments Q_USE_DEBUG_COMMAND: True SWIFT_ENABLE_TEMPURLS: True SWIFT_TEMPURL_KEY: secretkey TENANT_VLAN_RANGE: 100:150 EBTABLES_RACE_FIX: True NEUTRON_PORT_SECURITY: False devstack_services: s-account: True s-container: True s-object: True s-proxy: True generic_switch: True networking_baremetal: True ir-neutronagt: True neutron-api: True neutron-agent: True neutron-dhcp: True neutron-l3: True neutron-metadata-agent: True neutron-metering: True - job: name: networking-baremetal-ovn-vxlan parent: ironic-base irrelevant-files: - ^.*\.rst$ - ^doc/.*$ - ^setup.cfg$ - ^test-requirements.txt$ - ^tools/.*$ - ^tox.ini$ required-projects: - openstack/networking-generic-switch - openstack/networking-baremetal - openstack/ironic - openstack/sushy-tools vars: tempest_test_timeout: 2400 tempest_test_regex: "BaremetalBasicOps" devstack_plugins: networking-generic-switch: https://opendev.org/openstack/networking-generic-switch networking-baremetal: https://opendev.org/openstack/networking-baremetal ironic: https://opendev.org/openstack/ironic devstack_localrc: IRONIC_DEPLOY_DRIVER: redfish # This uses vmedia to boot the node. TFTP is super sensitive to MTU # differences, and because we may end up running the job in multi-node # one day, we really don't want to deal with that headache. IRONIC_ENABLED_BOOT_INTERFACES: redfish-virtual-media IRONIC_ENABLED_HARDWARE_TYPES: redfish IRONIC_ENABLED_MANAGEMENT_INTERFACES: redfish BUILD_TIMEOUT: 2400 ENABLE_TENANT_VLANS: True IRONIC_DEFAULT_DEPLOY_INTERFACE: direct IRONIC_DEFAULT_RESCUE_INTERFACE: "" IRONIC_ENABLED_NETWORK_INTERFACES: neutron IRONIC_NETWORK_INTERFACE: neutron IRONIC_PROVISION_NETWORK_NAME: ironic-provision # TODO: Likely need to fix ironic-devstack to support this next value as none IRONIC_PROVISION_PROVIDER_NETWORK_TYPE: vxlan IRONIC_PROVISION_SUBNET_GATEWAY: 10.0.5.1 IRONIC_PROVISION_SUBNET_PREFIX: 10.0.5.0/24 IRONIC_TEMPEST_BUILD_TIMEOUT: 2400 IRONIC_TEMPEST_WHOLE_DISK_IMAGE: True IRONIC_USE_LINK_LOCAL: True IRONIC_USE_NEUTRON_SEGMENTS: False IRONIC_USE_VXLAN: True IRONIC_VM_COUNT: 3 IRONIC_VM_EPHEMERAL_DISK: 0 # TODO(TheJulia): we may want to force this to True. IRONIC_AUTOMATED_CLEAN_ENABLED: False OVS_PHYSICAL_BRIDGE: brbm OVN_BRIDGE_MAPPINGS: mynetwork:brbm,public:br-ex PHYSICAL_NETWORK: mynetwork Q_AGENT: ovn Q_ML2_TENANT_NETWORK_TYPE: vxlan Q_ML2_PLUGIN_TYPE_DRIVERS: vxlan,geneve,vlan,flat Q_ML2_PLUGIN_MECHANISM_DRIVERS: ovn,baremetal-l2vni,genericswitch,baremetal ML2_L3_PLUGIN: ovn-router,trunk,segments Q_PLUGIN: ml2 Q_USE_DEBUG_COMMAND: True SWIFT_ENABLE_TEMPURLS: True SWIFT_TEMPURL_KEY: secretkey TENANT_VLAN_RANGE: 100:150 EBTABLES_RACE_FIX: True NEUTRON_PORT_SECURITY: False devstack_services: s-account: True s-container: True s-object: True s-proxy: True generic_switch: True networking_baremetal: True ir-neutronagt: True neutron-api: True neutron-agent: False neutron-dhcp: False neutron-l3: False neutron-metadata-agent: False neutron-metering: False ovn-controller: True ovn-northd: True ovs-vswitchd: True ovsdb-server: True q-ovn-agent: True q-ovn-metadata-agent: True # Service names from ironic-base q-svc: True q-agt: False q-dhcp: False q-l3: False q-meta: False - job: name: networking-baremetal-ovn-geneve parent: networking-baremetal-ovn-vxlan vars: devstack_localrc: Q_ML2_TENANT_NETWORK_TYPE: geneve IRONIC_PROVISION_PROVIDER_NETWORK_TYPE: geneve - job: name: networking-baremetal-tox-codespell parent: openstack-tox timeout: 7200 vars: tox_envlist: codespell ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1773930521.0 networking_baremetal-7.2.0/zuul.d/project.yaml0000664000175000017500000000072715157004031020322 0ustar00zuulzuul- project: templates: - check-requirements - openstack-python3-jobs-neutron - publish-openstack-docs-pti - release-notes-jobs-python3 check: jobs: - networking-baremetal-multitenant-vlans - networking-baremetal-ovn-vxlan - networking-baremetal-ovn-geneve gate: jobs: - networking-baremetal-multitenant-vlans - networking-baremetal-ovn-vxlan - networking-baremetal-ovn-geneve