diff --git a/.gitignore b/.gitignore index 75a4404e8..29efa7800 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,9 @@ __pycache__ # see .github/workflows/mkdocs.yaaml docs/workflows/ +# auto-generated neutron-understack sample config docs +docs/design-guide/neutron-understack-config-sample.md + # devbox's envrc .envrc .direnv diff --git a/Makefile b/Makefile index ea5b08f24..0d5dc04ec 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,8 @@ else ACTIVATE := $(VENV_DIR)/bin/activate endif +NEUTRON_SAMPLE_CONFIG := docs/design-guide/neutron-understack-config-sample.md + WFTMPLS := $(wildcard components/*-workflows/*/workflowtemplates/*.yaml) .PHONY: help @@ -39,10 +41,17 @@ wftmpls: $(WFTMPLS) $(ACTIVATE) component-docs-check: ## Validate component docs coverage for ArgoCD app templates @$(PYTHON) scripts/check-component-docs.py +$(NEUTRON_SAMPLE_CONFIG): ## Generate neutron-understack sample configuration docs + @mkdir -p docs/design-guide + @{ printf '# neutron-understack Sample Configuration\n\n```ini\n'; \ + uv run --directory python/neutron-understack oslo-config-generator \ + --config-file tools/config/neutron-understack-config-generator.conf; \ + printf '\n```\n'; } > $(NEUTRON_SAMPLE_CONFIG) + .PHONY: docs -docs: $(ACTIVATE) wftmpls component-docs-check ## Builds the documentation +docs: $(ACTIVATE) wftmpls $(NEUTRON_SAMPLE_CONFIG) component-docs-check ## Builds the documentation NO_MKDOCS_2_WARNING=1 $(MKDOCS) build --strict .PHONY: docs-local -docs-local: $(ACTIVATE) wftmpls component-docs-check ## Build and locally host the documentation +docs-local: $(ACTIVATE) wftmpls $(NEUTRON_SAMPLE_CONFIG) component-docs-check ## Build and locally host the documentation $(MKDOCS) serve --strict --livereload diff --git a/mkdocs.yml b/mkdocs.yml index 7ce00d935..1341d6a47 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -135,6 +135,7 @@ nav: - design-guide/hardware-traits.md - design-guide/flavors.md - design-guide/neutron-networking.md + - design-guide/neutron-understack-config-sample.md - design-guide/argo-workflows.md - design-guide/argo-events.md - 'Deployment Guide': diff --git a/python/neutron-understack/neutron_understack/config.py b/python/neutron-understack/neutron_understack/config.py index 8926960ef..cbcbd440f 100644 --- a/python/neutron-understack/neutron_understack/config.py +++ b/python/neutron-understack/neutron_understack/config.py @@ -1,6 +1,12 @@ +from keystoneauth1 import loading as ks_loading +from keystoneauth1 import session as ks_session from oslo_config import cfg -mech_understack_opts = [ +_OPT_GRP_ML2_UNDERSTACK = "ml2_understack" +_OPT_GRP_IRONIC = "ironic" +_OPT_GRP_L3_SVC_CISCO_ASA = "l3_service_cisco_asa" + +_mech_understack_opts = [ cfg.StrOpt( "nb_url", help="Nautobot URL", @@ -71,7 +77,8 @@ ), ] -l3_svc_cisco_asa_opts = [ + +_l3_svc_cisco_asa_opts = [ cfg.StrOpt( "user_agent", help="User-Agent for requests to Cisco ASA", @@ -93,9 +100,46 @@ ] +def list_understack_opts(): + return [ + (_OPT_GRP_ML2_UNDERSTACK, _mech_understack_opts), + ] + + +def list_ironic_opts(): + return [ + ( + _OPT_GRP_IRONIC, + [ + *ks_loading.get_adapter_conf_options(include_deprecated=False), + *ks_loading.get_session_conf_options(), + *ks_loading.get_auth_plugin_conf_options("v3password"), + ], + ) + ] + + +def list_cisco_asa_opts(): + return [ + (_OPT_GRP_L3_SVC_CISCO_ASA, _l3_svc_cisco_asa_opts), + ] + + def register_ml2_understack_opts(config): - config.register_opts(mech_understack_opts, "ml2_understack") + config.register_opts(_mech_understack_opts, _OPT_GRP_ML2_UNDERSTACK) + + +def register_ironic_opts(config): + ks_loading.register_adapter_conf_options(config, _OPT_GRP_IRONIC) + ks_loading.register_session_conf_options(config, _OPT_GRP_IRONIC) + ks_loading.register_auth_conf_options(config, _OPT_GRP_IRONIC) def register_l3_svc_cisco_asa_opts(config): - config.register_opts(l3_svc_cisco_asa_opts, "l3_service_cisco_asa") + config.register_opts(_l3_svc_cisco_asa_opts, _OPT_GRP_L3_SVC_CISCO_ASA) + + +def get_session(group: str) -> ks_session.Session: + auth = ks_loading.load_auth_from_conf_options(cfg.CONF, group) + session = ks_loading.load_session_from_conf_options(cfg.CONF, group, auth=auth) + return session diff --git a/python/neutron-understack/neutron_understack/ironic.py b/python/neutron-understack/neutron_understack/ironic.py index 50f196b05..c182a2739 100644 --- a/python/neutron-understack/neutron_understack/ironic.py +++ b/python/neutron-understack/neutron_understack/ironic.py @@ -1,24 +1,29 @@ -from keystoneauth1 import loading as ks_loading +import importlib.metadata + from openstack import connection from openstack.baremetal.baremetal_service import BaremetalService from openstack.baremetal.v1.port import Port as BaremetalPort from oslo_config import cfg +from neutron_understack import config + class IronicClient: def __init__(self): + config.register_ironic_opts(cfg.CONF) self.irclient = self._get_ironic_client() - def _get_session(self, group: str) -> ks_loading.session.Session: - auth = ks_loading.load_auth_from_conf_options(cfg.CONF, group) - session = ks_loading.load_session_from_conf_options(cfg.CONF, group, auth=auth) - return session - def _get_ironic_client(self) -> BaremetalService: - session = self._get_session("ironic") + session = config.get_session(config._OPT_GRP_IRONIC) + + version = importlib.metadata.version("neutron_understack") return connection.Connection( - session=session, oslo_conf=cfg.CONF, connect_retries=cfg.CONF.http_retries + session=session, + oslo_conf=cfg.CONF, + connect_retries=cfg.CONF.http_retries, + app_name="neutron_understack", + app_version=version, ).baremetal def baremetal_port_physical_network(self, local_link_info: dict) -> str | None: diff --git a/python/neutron-understack/neutron_understack/neutron_understack_mech.py b/python/neutron-understack/neutron_understack/neutron_understack_mech.py index 850b56efd..c268945d5 100644 --- a/python/neutron-understack/neutron_understack/neutron_understack_mech.py +++ b/python/neutron-understack/neutron_understack/neutron_understack_mech.py @@ -1,8 +1,6 @@ import logging from uuid import UUID -from keystoneauth1 import loading as ks_loading -from keystoneauth1 import session as ks_session from neutron_lib import constants as p_const from neutron_lib.api.definitions import portbindings from neutron_lib.callbacks import events @@ -41,51 +39,11 @@ def initialize(self): config.register_ml2_understack_opts(cfg.CONF) conf = cfg.CONF.ml2_understack - session = self._get_keystone_session() - self.undersync = Undersync(session, conf.undersync_url) - + self.undersync = Undersync(conf.undersync_url) self.ironic_client = IronicClient() self.trunk_driver = UnderStackTrunkDriver.create(self) self.subscribe() - def _get_keystone_session(self) -> ks_session.Session: - """Get a Keystone session using the Neutron service credentials. - - This uses the existing [keystone_authtoken] configuration section - to create a session that can automatically refresh tokens. - - Returns: - ks_session.Session: The Keystone session for authenticating with Undersync. - - Raises: - Exception: If unable to create session from Keystone. - """ - try: - # Load credentials from the [keystone_authtoken] section - auth = ks_loading.load_auth_from_conf_options( - cfg.CONF, "keystone_authtoken" - ) - # Create session manually to avoid missing config options - sess = ks_session.Session(auth=auth, timeout=30) - - # Verify we can get a token (test session is working) - token = sess.get_token() - if not token: - raise ValueError("Unable to obtain initial token from session") - - LOG.info( - "Successfully created Keystone session for Undersync authentication" - ) - return sess - - except Exception as e: - LOG.error( - "Failed to create Keystone session: %(error)s. " - "Please check your [keystone_authtoken] configuration.", - {"error": e}, - ) - raise - def subscribe(self): registry.subscribe( routers.handle_router_interface_removal, @@ -184,9 +142,9 @@ def _update_port_baremetal(self, context: PortContext) -> None: if current_vif_unbound and original_vif_other: self._tenant_network_port_cleanup(context) if vlan_group_name: - self.invoke_undersync(vlan_group_name) + self.undersync.sync(vlan_group_name) elif current_vif_other and vlan_group_name: - self.invoke_undersync(vlan_group_name) + self.undersync.sync(vlan_group_name) def _tenant_network_port_cleanup(self, context: PortContext): """Tenant network port cleanup in the UnderCloud infrastructure. @@ -247,7 +205,7 @@ def _delete_port_baremetal(self, context: PortContext) -> None: if vlan_group_name and is_provisioning_network(port["network_id"]): # Signals end of the provisioning / cleaning cycle, so we # put the port back to its normal tenant mode: - self.invoke_undersync(vlan_group_name) + self.undersync.sync(vlan_group_name) def bind_port(self, context: PortContext) -> None: """Bind the VXLAN network segment and allocate dynamic VLAN segments. @@ -344,12 +302,6 @@ def _bind_port_segment(self, context: PortContext, segment): next_segments_to_bind=[dynamic_segment], ) - def invoke_undersync(self, vlan_group_name: str): - self.undersync.sync_devices( - vlan_group=vlan_group_name, - dry_run=cfg.CONF.ml2_understack.undersync_dry_run, - ) - def check_vlan_transparency(self, context): pass diff --git a/python/neutron-understack/neutron_understack/tests/conftest.py b/python/neutron-understack/neutron_understack/tests/conftest.py index 240ec8382..c96aca047 100644 --- a/python/neutron-understack/neutron_understack/tests/conftest.py +++ b/python/neutron-understack/neutron_understack/tests/conftest.py @@ -281,9 +281,7 @@ def ironic_client(mocker) -> IronicClient: @pytest.fixture def understack_driver(oslo_config, ironic_client) -> UnderstackDriver: driver = UnderstackDriver() - mock_session = MagicMock() - mock_session.get_token.return_value = "auth_token" - driver.undersync = Undersync(mock_session, "api_url") + driver.undersync = MagicMock(spec_set=Undersync) driver.ironic_client = ironic_client return driver @@ -302,11 +300,6 @@ def _ironic_baremetal_port_physical_network(mocker, understack_driver) -> None: ) -@pytest.fixture(autouse=True) -def _undersync_sync_devices_patch(mocker, understack_driver) -> None: - mocker.patch.object(understack_driver.undersync, "sync_devices") - - @pytest.fixture def _utils_fetch_subport_network_id_patch(mocker, network_id) -> None: mocker.patch( diff --git a/python/neutron-understack/neutron_understack/tests/test_keystone_token_refresh.py b/python/neutron-understack/neutron_understack/tests/test_keystone_token_refresh.py index 0edaa908e..d49d31159 100644 --- a/python/neutron-understack/neutron_understack/tests/test_keystone_token_refresh.py +++ b/python/neutron-understack/neutron_understack/tests/test_keystone_token_refresh.py @@ -1,66 +1,31 @@ -from itertools import cycle from unittest.mock import Mock -from neutron_understack.neutron_understack_mech import UnderstackDriver +import pytest +from neutron_understack.undersync import Undersync -class TestKeystoneTokenRefresh: - """Test that Keystone tokens can be refreshed when they expire.""" - - def test_undersync_refreshes_expired_keystone_token(self, mocker): - """Test that Undersync can handle token expiration by refreshing the token. - - This test simulates a scenario where: - 1. First call to get_token() returns token_1 - 2. First Undersync API call succeeds with token_1 - 3. Second call to get_token() returns token_2 (token_1 expired) - 4. Second Undersync API call succeeds with token_2 - - This proves the session can refresh tokens automatically. - """ - # Mock the keystone session to simulate token refresh - mock_session = Mock() - # Use cycle to provide different tokens on each call (simulating refresh) - mock_session.get_token.side_effect = cycle(["token_1", "token_2", "token_3"]) - - mocker.patch("keystoneauth1.loading.load_auth_from_conf_options") - mocker.patch( - "neutron_understack.neutron_understack_mech.ks_session.Session", - return_value=mock_session, - ) - # Mock IronicClient to avoid config issues - mocker.patch("neutron_understack.neutron_understack_mech.IronicClient") +@pytest.fixture +def undersync(mocker): + mock_session = Mock() + mock_session.post.return_value.status_code = 200 + mock_session.post.return_value.json.return_value = {"result": "success"} + mocker.patch("neutron_understack.config.get_session", return_value=mock_session) + return Undersync("http://test-api") - # Create driver - this should store the session, not extract token - driver = UnderstackDriver() - driver.initialize() - # Mock the HTTP requests to verify the correct token is used - mock_post = mocker.patch("requests.Session.post") - mock_post.return_value.status_code = 200 - mock_post.return_value.json.return_value = {"result": "success"} - - # First call - should use token_1 - result1 = driver.undersync.sync_devices("vlan-group-1") +class TestKeystoneTokenRefresh: + """Test that each Undersync request goes through the keystoneauth1 session.""" - # Second call - should refresh and use token_2 - result2 = driver.undersync.sync_devices("vlan-group-2") + def test_undersync_uses_session_per_request(self, undersync): + """Test that sync calls _session.post, for token refresh. - # Verify session.get_token() was called three times: - # 1. During initialization (verification) - # 2. First sync_devices call - # 3. Second sync_devices call - assert mock_session.get_token.call_count == 3 + By calling _session.post directly, keystoneauth1 handles token refresh + transparently rather than us caching a token manually. + """ + result1 = undersync.sync("vlan-group-1") + result2 = undersync.sync("vlan-group-2") - # Verify both calls succeeded + assert undersync._session.post.call_count == 2 assert result1.status_code == 200 assert result2.status_code == 200 - - # Verify HTTP calls were made - assert ( - len(mock_post.call_args_list) == 2 - ), f"Expected 2 calls, got {len(mock_post.call_args_list)}" - - # The key verification is that session.get_token() was called for each request, - # proving that tokens are refreshed rather than cached diff --git a/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py b/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py index 83edc0262..ed67108ca 100644 --- a/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py +++ b/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py @@ -3,7 +3,6 @@ import pytest from neutron_lib.api.definitions import portbindings from neutron_lib.plugins.ml2 import api -from oslo_config import cfg from neutron_understack.neutron_understack_mech import UnderstackDriver @@ -12,14 +11,14 @@ class TestUpdatePortPostCommit: def test_with_simple_port(self, understack_driver, port_context): understack_driver.update_port_postcommit(port_context) - understack_driver.undersync.sync_devices.assert_called_once() + understack_driver.undersync.sync.assert_called_once() def test_skips_non_baremetal_port(self, understack_driver, port_context): port_context.current[portbindings.VNIC_TYPE] = portbindings.VNIC_NORMAL understack_driver.update_port_postcommit(port_context) - understack_driver.undersync.sync_devices.assert_not_called() + understack_driver.undersync.sync.assert_not_called() class TestDeletePortPostCommit: @@ -28,7 +27,7 @@ def test_skips_non_baremetal_port(self, understack_driver, port_context): understack_driver.delete_port_postcommit(port_context) - understack_driver.undersync.sync_devices.assert_not_called() + understack_driver.undersync.sync.assert_not_called() @pytest.mark.usefixtures("_ironic_baremetal_port_physical_network") @@ -209,24 +208,16 @@ class FakeContext: class TestKeystoneAuthentication: def test_initialize_with_keystone_auth(self, mocker): - """Test that driver initializes with Keystone authentication.""" - mock_auth = mocker.patch("keystoneauth1.loading.load_auth_from_conf_options") - mock_session_class = mocker.patch( - "neutron_understack.neutron_understack_mech.ks_session.Session" - ) - mock_get_token = mocker.MagicMock(return_value="test_service_token") - + """Test that Undersync creates its own session using the ironic auth config.""" mock_session_instance = mocker.MagicMock() - mock_session_instance.get_token = mock_get_token - mock_session_class.return_value = mock_session_instance - - # Mock IronicClient to avoid config issues + mock_get_session = mocker.patch( + "neutron_understack.config.get_session", + return_value=mock_session_instance, + ) mocker.patch("neutron_understack.neutron_understack_mech.IronicClient") driver = UnderstackDriver() driver.initialize() - mock_auth.assert_called_once_with(cfg.CONF, "keystone_authtoken") - mock_session_class.assert_called_once() - mock_get_token.assert_called_once() - assert driver.undersync.session == mock_session_instance + mock_get_session.assert_called_once_with("ironic") + assert driver.undersync._session == mock_session_instance diff --git a/python/neutron-understack/neutron_understack/tests/test_trunk.py b/python/neutron-understack/neutron_understack/tests/test_trunk.py index 5166c4cb6..1d0b80110 100644 --- a/python/neutron-understack/neutron_understack/tests/test_trunk.py +++ b/python/neutron-understack/neutron_understack/tests/test_trunk.py @@ -1,6 +1,5 @@ import pytest from neutron.plugins.ml2.driver_context import portbindings -from oslo_config import cfg from neutron_understack import utils from neutron_understack.trunk import SubportSegmentationIDError @@ -116,10 +115,7 @@ def test_subports_add_post( None, None, None, mocker.Mock(states=[trunk]) ) - understack_trunk_driver.undersync.sync_devices.assert_called_once_with( - vlan_group="physnet", - dry_run=cfg.CONF.ml2_understack.undersync_dry_run, - ) + understack_trunk_driver.undersync.sync.assert_called_once_with("physnet") def test_when_parent_port_is_unbound( self, mocker, understack_trunk_driver, trunk, subport, port_object @@ -210,10 +206,7 @@ def test_when_parent_port_is_bound( understack_trunk_driver._clean_parent_port_switchport_config(trunk, [subport]) - understack_trunk_driver.undersync.sync_devices.assert_called_once_with( - vlan_group="physnet", - dry_run=cfg.CONF.ml2_understack.undersync_dry_run, - ) + understack_trunk_driver.undersync.sync.assert_called_once_with("physnet") def test_when_parent_port_is_unbound( self, mocker, understack_trunk_driver, port_object, trunk, subport diff --git a/python/neutron-understack/neutron_understack/tests/test_utils.py b/python/neutron-understack/neutron_understack/tests/test_utils.py index 5de9e7f21..f39e974d1 100644 --- a/python/neutron-understack/neutron_understack/tests/test_utils.py +++ b/python/neutron-understack/neutron_understack/tests/test_utils.py @@ -12,7 +12,6 @@ from sqlalchemy.orm import sessionmaker from neutron_understack import utils -from neutron_understack.undersync import Undersync class TestParentPortIsBound: @@ -636,18 +635,3 @@ def test_port_bound_to_uuid_when_agent_reports_hostname(self, mocker): assert result == "trunk-456" mock_ironic.baremetal_node_uuid.assert_called_once_with("gateway-host-1") - - -class TestUndersyncAuthentication: - def test_undersync_with_keystone_auth(self, mocker): - """Test that Undersync client uses X-Auth-Token header from session.""" - mock_session = MagicMock() - mock_session.get_token.return_value = "test_token" - - client = Undersync(mock_session, "http://test.api") - - session = client.client - - assert session.headers["Content-Type"] == "application/json" - assert session.headers["X-Auth-Token"] == "test_token" - assert "Authorization" not in session.headers diff --git a/python/neutron-understack/neutron_understack/trunk.py b/python/neutron-understack/neutron_understack/trunk.py index aa903c889..c09bf7010 100644 --- a/python/neutron-understack/neutron_understack/trunk.py +++ b/python/neutron-understack/neutron_understack/trunk.py @@ -241,12 +241,6 @@ def _handle_segment_deallocation(self, subports: list[SubPort], host: str): subport_binding_level = self._delete_binding_level(subport["port_id"], host) self._delete_unused_segment(subport_binding_level.segment_id) - def _trigger_undersync(self, vlan_group_name: str) -> None: - self.undersync.sync_devices( - vlan_group=vlan_group_name, - dry_run=cfg.CONF.ml2_understack.undersync_dry_run, - ) - def _handle_subports_removal( self, binding_profile: dict, @@ -257,7 +251,7 @@ def _handle_subports_removal( ) -> None: self._handle_segment_deallocation(subports, binding_host) if invoke_undersync and vlan_group_name: - self._trigger_undersync(vlan_group_name) + self.undersync.sync(vlan_group_name) def subports_added(self, resource, event, trunk_plugin, payload): trunk = payload.states[0] @@ -277,7 +271,7 @@ def subports_added_post(self, resource, event, trunk_plugin, payload): local_link_info ) LOG.debug("subports_added_post found vlan_group_name=%s", vlan_group_name) - self._trigger_undersync(vlan_group_name) + self.undersync.sync(vlan_group_name) def subports_deleted(self, resource, event, trunk_plugin, payload): trunk = payload.states[0] diff --git a/python/neutron-understack/neutron_understack/undersync.py b/python/neutron-understack/neutron_understack/undersync.py index bbdc7314c..6f963c37b 100644 --- a/python/neutron-understack/neutron_understack/undersync.py +++ b/python/neutron-understack/neutron_understack/undersync.py @@ -1,9 +1,13 @@ +import importlib.metadata import urllib.parse import requests +from oslo_config import cfg from oslo_log import log from requests.models import HTTPError +from neutron_understack import config + LOG = log.getLogger(__name__) @@ -14,15 +18,23 @@ class UndersyncError(Exception): class Undersync: def __init__( self, - session, api_url: str | None = None, timeout: int = 90, ) -> None: - self.session = session self.url = "http://undersync.undersync.svc.cluster.local:8080" self.api_url = api_url or self.url self.timeout = timeout + version = importlib.metadata.version("neutron_understack") + + # we use the [ironic] group here since we don't need to duplicate + # the credentials + config.register_ironic_opts(cfg.CONF) + config.register_ml2_understack_opts(cfg.CONF) + self._session = config.get_session(config._OPT_GRP_IRONIC) + self._session.app_name = "neutron_understack" + self._session.app_version = version + def _log_and_raise_for_status(self, response: requests.Response): try: response.raise_for_status() @@ -30,26 +42,9 @@ def _log_and_raise_for_status(self, response: requests.Response): LOG.error("Undersync error: %(error)s", {"error": error}) raise UndersyncError() from error - def sync_devices( - self, vlan_group: str, force=False, dry_run=False - ) -> requests.Response: - if dry_run: - return self.dry_run(vlan_group) - elif force: - return self.force(vlan_group) - else: - return self.sync(vlan_group) - - @property - def client(self): - session = requests.Session() - session.headers = {"Content-Type": "application/json"} - session.headers["X-Auth-Token"] = self.session.get_token() - return session - def _undersync_post(self, action: str, vlan_group: str) -> requests.Response: vlan_group = urllib.parse.quote(vlan_group, safe="") - response = self.client.post( + response = self._session.post( f"{self.api_url}/v1/vlan-group/{vlan_group}/{action}", timeout=self.timeout ) try: @@ -63,6 +58,8 @@ def _undersync_post(self, action: str, vlan_group: str) -> requests.Response: return response def sync(self, vlan_group: str) -> requests.Response: + if cfg.CONF.ml2_understack.undersync_dry_run: + return self._undersync_post("dry-run", vlan_group) return self._undersync_post("sync", vlan_group) def dry_run(self, vlan_group: str) -> requests.Response: diff --git a/python/neutron-understack/pyproject.toml b/python/neutron-understack/pyproject.toml index 16d62d7ad..9fa945cc6 100644 --- a/python/neutron-understack/pyproject.toml +++ b/python/neutron-understack/pyproject.toml @@ -27,8 +27,17 @@ dependencies = [ "requests>=2,<3", "neutron-lib>=3,<4", "neutron>=27,<29", + "oslo.config>=9.7.1", + "oslo.log>=7.1.0", + "openstacksdk>=4.9.0", + "keystoneauth1>=3.14.0", ] +[project.entry-points."oslo.config.opts"] +ironic = "neutron_understack.config:list_ironic_opts" +understack = "neutron_understack.config:list_understack_opts" +cisco-asa = "neutron_understack.config:list_cisco_asa_opts" + [project.entry-points."neutron.ml2.mechanism_drivers"] understack = "neutron_understack.neutron_understack_mech:UnderstackDriver" undersync = "neutron_understack.undersync_mech:UndersyncDriver" @@ -44,6 +53,7 @@ test = [ "pytest>=9.0.1,<10", "pytest-cov>=7,<8", "pytest-mock>=3.14.0,<4", + "sqlalchemy", ] [tool.uv] diff --git a/python/neutron-understack/tools/config/neutron-understack-config-generator.conf b/python/neutron-understack/tools/config/neutron-understack-config-generator.conf new file mode 100644 index 000000000..dd4bdaeab --- /dev/null +++ b/python/neutron-understack/tools/config/neutron-understack-config-generator.conf @@ -0,0 +1,5 @@ +[DEFAULT] +wrap_width = 62 +namespace = ironic +namespace = understack +namespace = cisco-asa diff --git a/python/neutron-understack/uv.lock b/python/neutron-understack/uv.lock index c9a219bd5..b71ad9747 100644 --- a/python/neutron-understack/uv.lock +++ b/python/neutron-understack/uv.lock @@ -771,8 +771,12 @@ name = "neutron-understack" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "keystoneauth1" }, { name = "neutron" }, { name = "neutron-lib" }, + { name = "openstacksdk" }, + { name = "oslo-config" }, + { name = "oslo-log" }, { name = "requests" }, ] @@ -781,12 +785,17 @@ test = [ { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-mock" }, + { name = "sqlalchemy" }, ] [package.metadata] requires-dist = [ + { name = "keystoneauth1", specifier = ">=3.14.0" }, { name = "neutron", specifier = ">=27,<29" }, { name = "neutron-lib", specifier = ">=3,<4" }, + { name = "openstacksdk", specifier = ">=4.9.0" }, + { name = "oslo-config", specifier = ">=9.7.1" }, + { name = "oslo-log", specifier = ">=7.1.0" }, { name = "requests", specifier = ">=2,<3" }, ] @@ -795,11 +804,12 @@ test = [ { name = "pytest", specifier = ">=9.0.1,<10" }, { name = "pytest-cov", specifier = ">=7,<8" }, { name = "pytest-mock", specifier = ">=3.14.0,<4" }, + { name = "sqlalchemy" }, ] [[package]] name = "openstacksdk" -version = "4.8.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, @@ -814,12 +824,10 @@ dependencies = [ { name = "platformdirs" }, { name = "psutil" }, { name = "pyyaml" }, - { name = "requestsexceptions" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/24/1167097740136e302c74043c1c6feecf8d757b052d7b457960e0dc60fa03/openstacksdk-4.8.0.tar.gz", hash = "sha256:4dc038e1c17d893005f3a0a8951456afd9d148f3f65d448f94adcceb278d7f31", size = 1309981, upload-time = "2025-11-13T13:59:59.614Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/43/49b126e9ccfa19647d2f0aff26321caded607522d29c3d9495d44f4b9471/openstacksdk-4.16.0.tar.gz", hash = "sha256:466640c6d2b813b782d4ad58a4f2960633e8e58f147ec0baa2019454de190d30", size = 1397114, upload-time = "2026-06-17T08:43:22.774Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/45/87fa873f35abdf191d66d4821fe965f781e2d3e37e58883c6e23e95fa794/openstacksdk-4.8.0-py3-none-any.whl", hash = "sha256:7f7c438d418a4ee0c8737b1ac0859b3c1e8e21401677782415d21bce3324b9dd", size = 1849694, upload-time = "2025-11-13T13:59:57.634Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ea/60a4a274c4991847a085ac4a36b19d09f44f04a22adcfda6ca631763322b/openstacksdk-4.16.0-py3-none-any.whl", hash = "sha256:0469939bd7fc1b80979e4440dd8f8026eea302bdd979fac5437b56ada3780a62", size = 1982817, upload-time = "2026-06-17T08:43:21.148Z" }, ] [[package]] @@ -1775,15 +1783,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] -[[package]] -name = "requestsexceptions" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/61b9652d3256503c99b0b8f145d9c8aa24c514caff6efc229989505937c1/requestsexceptions-1.4.0.tar.gz", hash = "sha256:b095cbc77618f066d459a02b137b020c37da9f46d9b057704019c9f77dba3065", size = 6880, upload-time = "2018-02-01T17:04:45.294Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/8c/49ca60ea8c907260da4662582c434bec98716177674e88df3fd340acf06d/requestsexceptions-1.4.0-py2.py3-none-any.whl", hash = "sha256:3083d872b6e07dc5c323563ef37671d992214ad9a32b0ca4a3d7f5500bf38ce3", size = 3802, upload-time = "2018-02-01T17:04:39.07Z" }, -] - [[package]] name = "rfc3986" version = "2.0.0"