Skip to content

Commit 85a8742

Browse files
authored
Merge pull request #2578 from rackerlabs/netdev-firewall-split-nics
feat(neutron): wire a netdev router's gateway and subnets to separate NICs PUC-2115
2 parents e85aba1 + 739b719 commit 85a8742

10 files changed

Lines changed: 874 additions & 63 deletions

File tree

‎components/ironic/cm-trait-based-networking.yaml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ data:
3131
filter: port.category == 'network'
3232
max_count: 1
3333
34+
# Match each parent VIF's role tag to its NIC category.
35+
CUSTOM_NETDEV_ROLE_ATTACH:
36+
order: 1
37+
actions:
38+
- action: attach_port
39+
filter: port.category == 'outside' && vif.tags == 'outside'
40+
max_count: 1
41+
- action: attach_port
42+
filter: port.category == 'inside' && vif.tags == 'inside'
43+
max_count: 1
44+
3445
# Attaches a single category=storage port for a storage VIF. Single-port
3546
# only for now (A/B grouping is deferred). Require it on a flavor with:
3647
# openstack flavor set --property trait:CUSTOM_STORAGE_ATTACH=required <flavor>

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

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@
3535
_RECONCILE_PROVISION_TIMEOUT = 60
3636
_PANOS_DRIVER = "panos"
3737
_ROUTER_RELEASE_MARKER = "understack_router_release" # release is in progress
38+
# Adoption sets it in instance_info.traits when a port has a role.
39+
ROLE_ATTACH_TRAIT = "CUSTOM_NETDEV_ROLE_ATTACH"
40+
_TENANT_VIF_KEY = "tenant_vif_port_id"
41+
# Port categories that give a NIC a router role.
42+
_ROLE_CATEGORIES = frozenset({"outside", "inside"})
43+
44+
45+
def uses_port_roles(node) -> bool:
46+
"""Return True if Ironic will attach this node's VIFs by port role.
47+
48+
Reads instance_info, not node traits: Ironic applies a TBN trait only once
49+
it is in instance_info.traits, which adoption sets.
50+
"""
51+
instance_info = getattr(node, "instance_info", None)
52+
if not isinstance(instance_info, dict):
53+
return False
54+
traits = instance_info.get("traits")
55+
return isinstance(traits, list) and ROLE_ATTACH_TRAIT in traits
3856

3957

4058
@dataclass(frozen=True)
@@ -160,7 +178,7 @@ def router_id_for_release(node: BaremetalNode) -> str | None:
160178
return identities[0]
161179

162180
def attach_vif_to_node(self, node: str | BaremetalNode, vif_id: str) -> None:
163-
"""Attach a Neutron port (VIF) to the node."""
181+
"""Attach a VIF, forwarding optional role tags to Ironic's TBN rules."""
164182
node_id = node.id if isinstance(node, BaremetalNode) else node
165183
LOG.info("Attaching VIF %s to Ironic node %s", vif_id, node_id)
166184
self.irclient.attach_vif_to_node(node, vif_id)
@@ -184,6 +202,15 @@ def node_ports(self, node: str | BaremetalNode) -> list[BaremetalPort]:
184202
node_id = node.id if isinstance(node, BaremetalNode) else node
185203
return list(self.irclient.ports(details=True, node_id=node_id))
186204

205+
def port_for_vif(
206+
self, node: str | BaremetalNode, vif_id: str
207+
) -> BaremetalPort | None:
208+
"""Return the node's baremetal port that the VIF is attached to, or None."""
209+
for port in self.node_ports(node):
210+
if (port.internal_info or {}).get(_TENANT_VIF_KEY) == vif_id:
211+
return port
212+
return None
213+
187214
def adopt_node_for_router(
188215
self,
189216
node: str | BaremetalNode,
@@ -211,6 +238,8 @@ def adopt_node_for_router(
211238
router_name,
212239
project_id,
213240
)
241+
# Read before manage, so a failed lookup leaves the node untouched.
242+
role_instance_info = self._role_attach_instance_info(node)
214243
try:
215244
# available -> manageable, required before the adopt verb is valid.
216245
# Attempt identity-checked rollback if manage or a later step fails.
@@ -232,13 +261,19 @@ def adopt_node_for_router(
232261
router_id,
233262
router_name,
234263
)
235-
self.irclient.update_node(
236-
node,
237-
retry_on_conflict=False,
238-
lessee=project_id,
239-
instance_id=router_id,
240-
instance_name=router_name,
241-
)
264+
stamp = {
265+
"lessee": project_id,
266+
"instance_id": router_id,
267+
"instance_name": router_name,
268+
}
269+
if role_instance_info is not None:
270+
LOG.info(
271+
"Setting node %s instance_info.traits to %s",
272+
node_id,
273+
role_instance_info["traits"],
274+
)
275+
stamp["instance_info"] = role_instance_info
276+
self.irclient.update_node(node, retry_on_conflict=False, **stamp)
242277
LOG.info("Node %s: adopt (manageable -> active)", node_id)
243278
# Use wait=False: the SDK waits for available after adopt, not active.
244279
# https://review.opendev.org/c/openstack/openstacksdk/+/999686
@@ -265,6 +300,31 @@ def adopt_node_for_router(
265300
adopted.instance_name,
266301
)
267302

303+
def _role_attach_instance_info(self, node: str | BaremetalNode) -> dict | None:
304+
"""Return instance_info whose traits match the node's port roles, or None.
305+
306+
A node with an outside or inside port gets exactly the role trait, so
307+
no other attach rule can run first; a node without loses a stale one.
308+
None means no change.
309+
Undeploy clears instance_info, so release needs no matching step.
310+
"""
311+
current = (
312+
node if isinstance(node, BaremetalNode) else self.irclient.get_node(node)
313+
)
314+
if current is None:
315+
return None
316+
instance_info = dict(current.instance_info or {})
317+
traits = list(instance_info.get("traits") or [])
318+
ports = self.node_ports(current)
319+
if any(port.category in _ROLE_CATEGORIES for port in ports):
320+
wanted = [ROLE_ATTACH_TRAIT]
321+
else:
322+
wanted = [trait for trait in traits if trait != ROLE_ATTACH_TRAIT]
323+
if wanted == traits:
324+
return None
325+
instance_info["traits"] = wanted
326+
return instance_info
327+
268328
def _return_node_to_available(
269329
self,
270330
node: str | BaremetalNode,

0 commit comments

Comments
 (0)