Skip to content

Commit e04e45c

Browse files
authored
Merge pull request #2464 from rackerlabs/fix-port-binding
fix(neutron-understack): select the ACTIVE port binding, not bindings[0]
2 parents 59da85a + c48fb5a commit e04e45c

4 files changed

Lines changed: 62 additions & 7 deletions

File tree

‎docs/design-guide/neutron-networking.md‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,49 @@ Neutron based on certain data provided to port creation and update API calls.
499499
- the port has a binding host
500500
- the port is either unbound or has previously failed to bind
501501

502+
### Port Representations
503+
504+
Neutron exposes a port in three different shapes and you will see all of them
505+
in the Neutron source and in our drivers:
506+
507+
| Representation | How you get it | Notes |
508+
| -------------- | -------------- | ----- |
509+
| Dict | `core_plugin.get_port()`, `PortContext.current` | API-shaped. Binding fields appear flattened as `binding:vif_type`, `binding:vnic_type`, `binding:host_id`, etc. |
510+
| OVO | `neutron.objects.ports.Port.get_object()` | Typed versioned object. Bindings are in `port.bindings`, binding levels in `port.binding_levels`. |
511+
| SQLAlchemy model | `payload.metadata["port_db"]`, `ml2.db.get_port()` | ML2 internal. Bindings are in `port.port_bindings`. Don't go looking for these; use them when Neutron hands one to you. |
512+
513+
Use the representation that matches where the port came from, and don't
514+
re-fetch a port just to change its shape:
515+
516+
- If Neutron hands you a dict (`PortContext.current`) or a SQLAlchemy model
517+
(for example `payload.metadata["port_db"]` in a registry callback), work with
518+
that directly.
519+
- When we have only a port ID, as with a trunk's parent port, fetch the **OVO**
520+
with `utils.fetch_port_object()`. It is typed, and it is what Neutron's own
521+
newer code uses when it needs binding details. We don't use `get_port()` for
522+
this.
523+
524+
#### Selecting a binding
525+
526+
A port can have several bindings (one `ACTIVE`, any number of `INACTIVE` ones
527+
during live migration), so never index with `port.bindings[0]`. Select the
528+
binding with `get_port_binding_by_status_and_host()` from
529+
`neutron_lib.plugins.utils`, then read `vif_type`, `vnic_type`, `profile` and
530+
`host` from the result. For an OVO port, `utils.active_port_binding()` wraps
531+
this and returns the `ACTIVE` binding, or `None` if there isn't one:
532+
533+
```python
534+
binding = utils.active_port_binding(port)
535+
if binding and binding.vnic_type == portbindings.VNIC_BAREMETAL:
536+
...
537+
```
538+
539+
For a SQLAlchemy model, call the neutron-lib helper directly with
540+
`port.port_bindings` (the model's attribute name) instead of `port.bindings`.
541+
The helper only subscripts each binding (`binding["status"]`,
542+
`binding["host"]`), so it works on either. Pass `raise_if_not_found=True` to get
543+
`PortBindingNotFound` instead of `None`.
544+
502545
### Router Interface Lifecycle
503546

504547
When a subnet is attached to a router, the understack ML2 driver sets up a path

‎python/neutron-understack/neutron_understack/trunk.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,9 @@ def _handle_segment_allocation(
160160
def _add_subports_networks_to_parent_port_switchport(
161161
self, parent_port: Port, subports: list[SubPort]
162162
) -> None:
163-
binding_profile = parent_port.bindings[0].profile
164-
binding_host = parent_port.bindings[0].host
163+
parent_binding = utils.active_port_binding(parent_port)
164+
binding_profile = parent_binding.profile
165+
binding_host = parent_binding.host
165166

166167
physnet = binding_profile.get("physical_network")
167168
if not physnet:
@@ -182,7 +183,8 @@ def _clean_parent_port_switchport_config(
182183
parent_port_obj = utils.fetch_port_object(trunk.port_id)
183184
if not utils.parent_port_is_bound(parent_port_obj):
184185
return
185-
self._handle_segment_deallocation(subports, parent_port_obj.bindings[0].host)
186+
parent_binding = utils.active_port_binding(parent_port_obj)
187+
self._handle_segment_deallocation(subports, parent_binding.host)
186188

187189
def _delete_unused_segment(self, segment_id: str) -> NetworkSegment:
188190
network_segment = utils.network_segment_by_id(segment_id)

‎python/neutron-understack/neutron_understack/undersync_mech.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def _validate_parent_physnet(self, resource, event, trunk_plugin, payload):
144144
parent_port = utils.fetch_port_object(trunk.port_id)
145145
if not utils.parent_port_is_bound(parent_port):
146146
return
147-
if not parent_port.bindings[0].profile.get("physical_network"):
147+
if not utils.active_port_binding(parent_port).profile.get(api.PHYSICAL_NETWORK):
148148
raise exc.BadRequest(
149149
resource="port", msg=_missing_physnet_msg(parent_port.id)
150150
)
@@ -165,7 +165,9 @@ def _sync_trunk_parent(self, trunk) -> None:
165165
if not utils.parent_port_is_bound(parent_port):
166166
return
167167

168-
physnet = parent_port.bindings[0].profile.get("physical_network")
168+
physnet = utils.active_port_binding(parent_port).profile.get(
169+
api.PHYSICAL_NETWORK
170+
)
169171
if not physnet:
170172
# _validate_parent_physnet rejects this while it is still
171173
# abortable, so the binding profile changed underneath us. Raising
@@ -192,7 +194,7 @@ def bind_port(self, context: PortContext) -> None:
192194
binding_profile = port.get(portbindings.PROFILE) or {}
193195
if not binding_profile.get("physical_network"):
194196
LOG.debug(
195-
"Skipping port %s without a physical_network in its " "binding_profile",
197+
"Skipping port %s without a physical_network in its binding_profile",
196198
port["id"],
197199
)
198200
return

‎python/neutron-understack/neutron_understack/utils.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from neutron_lib import context as n_context
1616
from neutron_lib.api.definitions import segment as segment_def
1717
from neutron_lib.plugins import directory
18+
from neutron_lib.plugins import utils as plugin_utils
1819
from neutron_lib.plugins.ml2 import api
1920
from oslo_config import cfg
2021

@@ -472,8 +473,15 @@ def release_segment_if_unused(segment: NetworkSegment) -> None:
472473
release_dynamic_segment(segment.id)
473474

474475

476+
def active_port_binding(port: port_obj.Port) -> port_obj.PortBinding | None:
477+
"""The port's ACTIVE binding, or None if it has none."""
478+
return plugin_utils.get_port_binding_by_status_and_host(
479+
port.bindings, constants.ACTIVE, port_id=port.id
480+
)
481+
482+
475483
def parent_port_is_bound(port: port_obj.Port) -> bool:
476-
port_binding = port.bindings[0]
484+
port_binding = active_port_binding(port)
477485
return bool(
478486
port_binding
479487
and port_binding.vif_type == portbindings.VIF_TYPE_OTHER

0 commit comments

Comments
 (0)