|
| 1 | +import logging |
| 2 | +import re |
1 | 3 | from contextlib import contextmanager |
2 | 4 |
|
| 5 | +from neutron.common.ovn import constants as ovn_const |
3 | 6 | from neutron.db import models_v2 |
4 | 7 | from neutron.objects import ports as port_obj |
| 8 | +from neutron.objects import trunk as trunk_obj |
5 | 9 | from neutron.objects.network import NetworkSegment |
6 | 10 | from neutron.objects.network_segment_range import NetworkSegmentRange |
7 | 11 | from neutron.plugins.ml2.driver_context import portbindings |
|
14 | 18 | from neutron_lib.plugins.ml2 import api |
15 | 19 | from oslo_config import cfg |
16 | 20 |
|
| 21 | +from neutron_understack.ironic import IronicClient |
17 | 22 | from neutron_understack.ml2_type_annotations import NetworkSegmentDict |
18 | 23 | from neutron_understack.ml2_type_annotations import PortContext |
19 | 24 | from neutron_understack.ml2_type_annotations import PortDict |
20 | 25 |
|
| 26 | +LOG = logging.getLogger(__name__) |
| 27 | + |
21 | 28 |
|
22 | 29 | def fetch_port_object(port_id: str) -> port_obj.Port: |
23 | 30 | context = n_context.get_admin_context() |
@@ -101,6 +108,148 @@ def fetch_trunk_plugin() -> TrunkPlugin: |
101 | 108 | return trunk_plugin |
102 | 109 |
|
103 | 110 |
|
| 111 | +def _is_uuid(value: str) -> bool: |
| 112 | + """Check if a string is a UUID.""" |
| 113 | + uuid_pattern = re.compile( |
| 114 | + r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE |
| 115 | + ) |
| 116 | + return bool(uuid_pattern.match(value)) |
| 117 | + |
| 118 | + |
| 119 | +def fetch_network_node_trunk_id() -> str: |
| 120 | + """Dynamically discover the network node trunk ID via OVN Gateway agent. |
| 121 | +
|
| 122 | + This function discovers the network node trunk by: |
| 123 | + 1. Finding alive OVN Controller Gateway agents |
| 124 | + 2. Getting the host of the gateway agent |
| 125 | + 3. Finding trunks whose parent port is bound to that host |
| 126 | + (resolving baremetal node UUIDs to names via Ironic if needed) |
| 127 | + 4. Returning the first matching trunk |
| 128 | +
|
| 129 | + The network node trunk is used to connect router networks to the |
| 130 | + network node (OVN gateway) by adding subports for each VLAN. |
| 131 | +
|
| 132 | + Returns: |
| 133 | + str: The UUID of the network node trunk |
| 134 | +
|
| 135 | + Raises: |
| 136 | + Exception: If no gateway agent or suitable trunk is found |
| 137 | +
|
| 138 | + Example: |
| 139 | + >>> trunk_id = fetch_network_node_trunk_id() |
| 140 | + >>> print(trunk_id) |
| 141 | + '2e558202-0bd0-4971-a9f8-61d1adea0427' |
| 142 | + """ |
| 143 | + context = n_context.get_admin_context() |
| 144 | + core_plugin = directory.get_plugin() |
| 145 | + |
| 146 | + if not core_plugin: |
| 147 | + raise Exception("Unable to obtain core plugin") |
| 148 | + |
| 149 | + # Find OVN Controller Gateway agents that are alive |
| 150 | + LOG.debug("Looking for OVN Controller Gateway agents") |
| 151 | + gateway_agents = core_plugin.get_agents( |
| 152 | + context, |
| 153 | + filters={"agent_type": [ovn_const.OVN_CONTROLLER_GW_AGENT], "alive": [True]}, |
| 154 | + ) |
| 155 | + |
| 156 | + if not gateway_agents: |
| 157 | + raise Exception( |
| 158 | + "No alive OVN Controller Gateway agents found. " |
| 159 | + "Please ensure the network node is running and the " |
| 160 | + "OVN gateway agent is active." |
| 161 | + ) |
| 162 | + |
| 163 | + # Use the first gateway agent's host |
| 164 | + # TODO: In the future, support multiple gateway agents for HA |
| 165 | + gateway_host = gateway_agents[0]["host"] |
| 166 | + LOG.debug( |
| 167 | + "Found OVN Gateway agent on host: %s (agent_id: %s)", |
| 168 | + gateway_host, |
| 169 | + gateway_agents[0]["id"], |
| 170 | + ) |
| 171 | + |
| 172 | + # Get all trunks |
| 173 | + trunks = trunk_obj.Trunk.get_objects(context) |
| 174 | + |
| 175 | + if not trunks: |
| 176 | + raise Exception("No trunks found in the system") |
| 177 | + |
| 178 | + ironic_client = IronicClient() |
| 179 | + # Find trunk whose parent port is bound to the gateway host |
| 180 | + for trunk in trunks: |
| 181 | + try: |
| 182 | + parent_port = port_obj.Port.get_object(context, id=trunk.port_id) |
| 183 | + |
| 184 | + if not parent_port or not parent_port.bindings: |
| 185 | + LOG.debug( |
| 186 | + "Trunk %s: parent port %s has no bindings, skipping", |
| 187 | + trunk.id, |
| 188 | + trunk.port_id, |
| 189 | + ) |
| 190 | + continue |
| 191 | + |
| 192 | + binding = parent_port.bindings[0] |
| 193 | + port_host = binding.host |
| 194 | + |
| 195 | + # If port is bound to a UUID (baremetal node), resolve it to name |
| 196 | + if _is_uuid(port_host): |
| 197 | + LOG.debug( |
| 198 | + "Trunk %s: port bound to baremetal node UUID %s, resolving to name", |
| 199 | + trunk.id, |
| 200 | + port_host, |
| 201 | + ) |
| 202 | + resolved_name = ironic_client.baremetal_node_name(port_host) |
| 203 | + if resolved_name: |
| 204 | + LOG.debug( |
| 205 | + "Trunk %s: resolved baremetal node %s to name %s", |
| 206 | + trunk.id, |
| 207 | + port_host, |
| 208 | + resolved_name, |
| 209 | + ) |
| 210 | + port_host = resolved_name |
| 211 | + else: |
| 212 | + LOG.debug( |
| 213 | + "Trunk %s: failed to resolve baremetal node %s, skipping", |
| 214 | + trunk.id, |
| 215 | + port_host, |
| 216 | + ) |
| 217 | + continue |
| 218 | + |
| 219 | + # Compare resolved host with gateway host |
| 220 | + if port_host == gateway_host: |
| 221 | + LOG.info( |
| 222 | + "Found network node trunk: %s (parent_port: %s, host: %s)", |
| 223 | + trunk.id, |
| 224 | + trunk.port_id, |
| 225 | + gateway_host, |
| 226 | + ) |
| 227 | + return str(trunk.id) |
| 228 | + else: |
| 229 | + LOG.debug( |
| 230 | + "Trunk %s: parent port bound to host %s (looking for %s), skipping", |
| 231 | + trunk.id, |
| 232 | + port_host, |
| 233 | + gateway_host, |
| 234 | + ) |
| 235 | + |
| 236 | + except Exception as e: |
| 237 | + LOG.debug( |
| 238 | + "Error checking trunk %s: %s. Continuing to next trunk.", |
| 239 | + trunk.id, |
| 240 | + str(e), |
| 241 | + ) |
| 242 | + continue |
| 243 | + |
| 244 | + # If we get here, no suitable trunk was found |
| 245 | + raise Exception( |
| 246 | + f"Unable to find network node trunk on gateway host '{gateway_host}'. " |
| 247 | + f"Found {len(trunks)} trunk(s) but none have a parent port " |
| 248 | + f"bound to the gateway host. " |
| 249 | + "Please ensure a trunk exists with a parent port on the network node." |
| 250 | + ) |
| 251 | + |
| 252 | + |
104 | 253 | def allocate_dynamic_segment( |
105 | 254 | network_id: str, |
106 | 255 | network_type: str = "vlan", |
|
0 commit comments