Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
### Action required

Neutron no longer falls back to querying Ironic for a baremetal port's
`physical_network` when it is absent from the port's `binding_profile`. It is
now a hard requirement: without it, binding a baremetal port is refused and
trunk create and delete operations are rejected with a `BadRequest`.

Before upgrading, confirm no bound baremetal ports are missing
`physical_network`, and backfill any that are:

1. Take a report-only inventory of affected ports in each cloud:

```bash
./scripts/audit_ports_missing_physical_network.py --os-cloud $CLOUD
```

2. If the report lists any ports, backfill them from their Ironic baremetal
port:

```bash
./scripts/audit_ports_missing_physical_network.py --os-cloud $CLOUD --execute
```

3. Re-run step 1 and confirm it reports no matches.

A port whose trunk parent is still bound but has no `physical_network` cannot
have its trunk deleted until the profile is corrected. Trunks whose parent port
is unbound are unaffected and delete as before.
13 changes: 0 additions & 13 deletions python/neutron-understack/neutron_understack/ironic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from openstack import connection
from openstack.baremetal.baremetal_service import BaremetalService
from openstack.baremetal.v1.node import Node as BaremetalNode
from openstack.baremetal.v1.port import Port as BaremetalPort
from oslo_config import cfg

from neutron_understack import config
Expand Down Expand Up @@ -61,18 +60,6 @@ def _get_ironic_client(self) -> BaremetalService:
app_version=version,
).baremetal

def baremetal_port_physical_network(self, local_link_info: dict) -> str | None:
port = self._port_by_local_link(local_link_info)
return port.physical_network if port else None

def _port_by_local_link(self, local_link_info: dict) -> BaremetalPort | None:
try:
return next(
self.irclient.ports(details=True, local_link_connection=local_link_info)
)
except StopIteration:
return None

def baremetal_node_name(self, node_uuid: str) -> str | None:
try:
node = self.irclient.get_node(node_uuid)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from neutron_understack import config
from neutron_understack import routers
from neutron_understack import utils
from neutron_understack.ironic import IronicClient
from neutron_understack.l3_router import svi as svi_router
from neutron_understack.trunk import UnderStackTrunkDriver
from neutron_understack.undersync import Undersync
Expand Down Expand Up @@ -49,7 +48,6 @@ def initialize(self):
conf = cfg.CONF.ml2_understack

self.undersync = Undersync(conf.undersync_url)
self.ironic_client = IronicClient()
self.trunk_driver = UnderStackTrunkDriver.create(self)
self.subscribe()

Expand Down Expand Up @@ -266,13 +264,6 @@ def _update_port_baremetal(self, context: PortContext) -> None:
port = context.current

vlan_group_name = port[portbindings.PROFILE].get("physical_network")
if vlan_group_name is None:
local_link_info = utils.local_link_from_binding_profile(
port[portbindings.PROFILE]
)
vlan_group_name = self.ironic_client.baremetal_port_physical_network(
local_link_info
)

if current_vif_unbound and original_vif_other:
self._tenant_network_port_cleanup(context)
Expand Down Expand Up @@ -325,13 +316,6 @@ def _delete_port_baremetal(self, context: PortContext) -> None:
port = context.current

vlan_group_name = port[portbindings.PROFILE].get("physical_network")
if vlan_group_name is None:
local_link_info = utils.local_link_from_binding_profile(
port[portbindings.PROFILE]
)
vlan_group_name = self.ironic_client.baremetal_port_physical_network(
local_link_info
)

if not vlan_group_name:
return
Expand Down Expand Up @@ -400,19 +384,17 @@ def _bind_port_segment(self, context: PortContext, segment):
port = context.current

vlan_group_name = port[portbindings.PROFILE].get("physical_network")
if vlan_group_name is None:
local_link_info = utils.local_link_from_binding_profile(
port[portbindings.PROFILE]
)
vlan_group_name = self.ironic_client.baremetal_port_physical_network(
local_link_info
)

if not vlan_group_name:
LOG.error(
"bind_port_segment: no physical_network found for baremetal "
"port with mac address: %(mac)s",
{"mac": mac_address},
"bind_port_segment: physical_network is required in the "
"binding_profile for baremetal port binding, but was not found. "
"port_id=%(port_id)s mac_address=%(mac)s network_id=%(network_id)s.",
{
"port_id": port["id"],
"mac": mac_address,
"network_id": network_id,
},
)
return

Expand Down
46 changes: 18 additions & 28 deletions python/neutron-understack/neutron_understack/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
from oslo_config import fixture as config_fixture

from neutron_understack import config as understack_config
from neutron_understack.ironic import IronicClient
from neutron_understack.neutron_understack_mech import UnderstackDriver
from neutron_understack.tests.helpers import Ml2PluginNoInit
from neutron_understack.tests.helpers import extend_network_dict
Expand Down Expand Up @@ -196,18 +195,24 @@ def subnet_context(ml2_plugin, subnet_dict) -> SubnetContext:

@pytest.fixture
def binding_profile(request, port_id) -> str:
"""A baremetal port binding profile.

Pass ``{"physical_network": None}`` to omit that key entirely.
"""
req = getattr(request, "param", {})
return json.dumps(
{
"local_link_information": [
{
"port_id": req.get("port_id", str(port_id)),
"switch_id": "11:22:33:44:55:66",
"switch_info": "a1-1-1.iad3.rackspace.net",
}
]
}
)
profile = {
"local_link_information": [
{
"port_id": req.get("port_id", str(port_id)),
"switch_id": "11:22:33:44:55:66",
"switch_info": "a1-1-1.iad3.rackspace.net",
}
],
}
physical_network = req.get("physical_network", "physnet")
if physical_network is not None:
profile["physical_network"] = physical_network
return json.dumps(profile)


@pytest.fixture
Expand Down Expand Up @@ -274,15 +279,9 @@ def port_context(network_context, port_dict, port_binding, ml2_plugin) -> PortCo


@pytest.fixture
def ironic_client(mocker) -> IronicClient:
return mocker.MagicMock(spec_set=IronicClient)


@pytest.fixture
def understack_driver(oslo_config, ironic_client) -> UnderstackDriver:
def understack_driver(oslo_config) -> UnderstackDriver:
driver = UnderstackDriver()
driver.undersync = MagicMock(spec_set=Undersync)
driver.ironic_client = ironic_client
return driver


Expand All @@ -291,15 +290,6 @@ def understack_trunk_driver(understack_driver) -> UnderStackTrunkDriver:
return UnderStackTrunkDriver.create(understack_driver)


@pytest.fixture
def _ironic_baremetal_port_physical_network(mocker, understack_driver) -> None:
mocker.patch.object(
understack_driver.ironic_client,
"baremetal_port_physical_network",
return_value="physnet",
)


@pytest.fixture
def _utils_fetch_subport_network_id_patch(mocker, network_id) -> None:
mocker.patch(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from dataclasses import dataclass
from types import SimpleNamespace

Expand All @@ -6,6 +7,7 @@
from neutron_lib.api.definitions import portbindings
from neutron_lib.plugins.ml2 import api

from neutron_understack import neutron_understack_mech
from neutron_understack.neutron_understack_mech import UnderstackDriver


Expand Down Expand Up @@ -153,7 +155,6 @@ def test_skips_non_baremetal_port(self, understack_driver, port_context):
MECH_UTILS = "neutron_understack.neutron_understack_mech.utils"


@pytest.mark.usefixtures("_ironic_baremetal_port_physical_network")
class TestDeletePortPostCommit:
def test_skips_non_baremetal_port(self, understack_driver, port_context):
port_context.current[portbindings.VNIC_TYPE] = portbindings.VNIC_NORMAL
Expand Down Expand Up @@ -236,7 +237,6 @@ def test_syncs_and_skips_release_on_provisioning_network(
release.assert_not_called()


@pytest.mark.usefixtures("_ironic_baremetal_port_physical_network")
class TestBindPort:
def test_does_not_bind_vlan_only_segments(
self,
Expand Down Expand Up @@ -327,19 +327,26 @@ def test_refuses_unsupported_vnic_type(

port_context.continue_binding.assert_not_called()

@pytest.mark.usefixtures("_ironic_baremetal_port_physical_network")
def test_does_not_bind_when_physical_network_not_found(
self, mocker, port_context, understack_driver
@pytest.mark.parametrize(
"binding_profile", [{"physical_network": None}], indirect=True
)
def test_does_not_bind_when_physical_network_missing(
self, mocker, caplog, port_context, understack_driver
):
understack_driver.ironic_client.baremetal_port_physical_network.return_value = (
None
)
"""physical_network is required: there is no fallback lookup any more.

Without it we cannot tell which VLAN group to allocate a segment in, so
binding is refused and the reason is logged rather than failing silently.
"""
mocker.patch.object(port_context, "continue_binding")
port_context._prepare_to_bind(port_context.network.network_segments)
caplog.set_level(logging.ERROR, logger=neutron_understack_mech.LOG.name)

understack_driver.bind_port(port_context)

port_context.continue_binding.assert_not_called()
assert "physical_network is required" in caplog.text
assert port_context.current["id"] in caplog.text

@pytest.mark.parametrize("port_dict", [{"trunk": True}], indirect=True)
def test_with_trunk_details(
Expand Down Expand Up @@ -420,8 +427,6 @@ def test_initialize_with_keystone_auth(self, mocker):
"neutron_understack.config.get_session",
return_value=mock_session_instance,
)
mocker.patch("neutron_understack.neutron_understack_mech.IronicClient")

driver = UnderstackDriver()
driver.initialize()

Expand Down
Loading
Loading