-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathneutron_understack_mech.py
More file actions
431 lines (359 loc) · 16.2 KB
/
Copy pathneutron_understack_mech.py
File metadata and controls
431 lines (359 loc) · 16.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
import logging
from uuid import UUID
from neutron_lib import constants as p_const
from neutron_lib.api.definitions import portbindings
from neutron_lib.callbacks import events
from neutron_lib.callbacks import priority_group
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
from neutron_lib.plugins.ml2 import api
from neutron_lib.plugins.ml2.api import MechanismDriver
from oslo_config import cfg
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
from .ml2_type_annotations import NetworkContext
from .ml2_type_annotations import PortContext
LOG = logging.getLogger(__name__)
SUPPORTED_VNIC_TYPES = [portbindings.VNIC_BAREMETAL, portbindings.VNIC_NORMAL]
ROUTER_INTERFACE_OWNERS = (
p_const.DEVICE_OWNER_ROUTER_INTF,
p_const.DEVICE_OWNER_ROUTER_GW,
)
def _is_router_interface_port(port):
return port.get("device_owner") in ROUTER_INTERFACE_OWNERS
class UnderstackDriver(MechanismDriver):
# See MechanismDriver docs for resource_provider_uuid5_namespace
resource_provider_uuid5_namespace = UUID("6eae3046-4072-11ef-9bcf-d6be6370a162")
@property
def connectivity(self): # type: ignore
return portbindings.CONNECTIVITY_L2
def initialize(self):
config.register_ml2_understack_opts(cfg.CONF)
conf = cfg.CONF.ml2_understack
self.undersync = Undersync(conf.undersync_url)
self.ironic_client = IronicClient()
self.trunk_driver = UnderStackTrunkDriver.create(self)
self.subscribe()
def subscribe(self):
registry.subscribe(
routers.handle_router_interface_removal,
resources.PORT,
events.PRECOMMIT_DELETE,
cancellable=True,
)
# Runs after neutron's OVN handler (PRIORITY_DEFAULT) has created the
# internal LRP, so the unified network HCG and the LRP both exist.
# Smaller priority is called earlier, so use a larger value to run later.
registry.subscribe(
routers.link_vxlan_network_ha_chassis_group,
resources.ROUTER_INTERFACE,
events.AFTER_CREATE,
priority=priority_group.PRIORITY_DEFAULT + 1000,
)
# Handles uplink cleanup when a subnet is explicitly detached from a
# router (remove_router_interface). This path does not reliably fire
# PORT PRECOMMIT_DELETE, so we subscribe to the ROUTER_INTERFACE event
# directly. The PORT PRECOMMIT_DELETE handler remains for the router-
# deletion path (delete_router skips ROUTER_INTERFACE events).
registry.subscribe(
routers.handle_router_interface_after_delete,
resources.ROUTER_INTERFACE,
events.AFTER_DELETE,
)
def create_network_precommit(self, context):
pass
def create_network_postcommit(self, context: NetworkContext):
pass
def update_network_precommit(self, context: NetworkContext):
pass
def update_network_postcommit(self, context: NetworkContext):
pass
def delete_network_precommit(self, context: NetworkContext):
pass
def delete_network_postcommit(self, context: NetworkContext):
pass
def create_subnet_precommit(self, context):
pass
def create_subnet_postcommit(self, context):
pass
def update_subnet_precommit(self, context):
pass
def update_subnet_postcommit(self, context):
pass
def delete_subnet_precommit(self, context):
pass
def delete_subnet_postcommit(self, context):
pass
def create_port_precommit(self, context: PortContext):
# Early SVI address scope check fires before port is committed and
# before create_port_postcommit, so invalid subnets never reach the
# VLAN allocation / trunk / Undersync steps.
# Neutron surfaces the BadRequest back through the router-interface API.
# update_port_precommit covers existing-port attaches.
if utils.is_router_interface(context):
LOG.info(
"create_port_precommit: checking SVI scope validation for "
"router port %s device_id=%s",
context.current["id"],
context.current.get("device_id"),
)
checked = svi_router.validate_svi_router_port(
context.plugin_context, context.current
)
if checked:
LOG.info(
"create_port_precommit: SVI scope check passed for port %(port)s "
"network %(network)s router %(router)s",
{
"port": context.current["id"],
"network": context.current.get("network_id"),
"router": context.current.get("device_id"),
},
)
else:
LOG.debug(
"create_port_precommit: SVI scope check not applicable for "
"port %(port)s owner %(owner)s router %(router)s",
{
"port": context.current["id"],
"owner": context.current.get("device_owner"),
"router": context.current.get("device_id"),
},
)
def create_port_postcommit(self, context: PortContext) -> None:
# Provide network node(s) with connectivity to the networks where this
# router port is attached to.
#
port = context.current
LOG.debug(
"Created port %(port)s on network %(net)s",
{"port": port["id"], "net": port["network_id"]},
)
if utils.is_router_interface(context):
LOG.info(
"Router interface port %(port)s detected on network %(net)s "
"device_id=%(router)s owner=%(owner)s fixed_ips=%(fixed_ips)s "
"- handing off to routers.create_port_postcommit",
{
"port": port["id"],
"net": port["network_id"],
"router": port.get("device_id"),
"owner": port.get("device_owner"),
"fixed_ips": port.get("fixed_ips", []),
},
)
try:
routers.create_port_postcommit(context)
except Exception:
LOG.exception(
"Router interface postcommit failed for port %(port)s "
"network %(network)s router %(router)s owner %(owner)s "
"fixed_ips=%(fixed_ips)s",
{
"port": port["id"],
"network": port["network_id"],
"router": port.get("device_id"),
"owner": port.get("device_owner"),
"fixed_ips": port.get("fixed_ips", []),
},
)
raise
def update_port_precommit(self, context: PortContext):
if not utils.is_router_interface(context):
return
original = context.original
current = context.current
became_router_interface = not _is_router_interface_port(
original
) and _is_router_interface_port(current)
fixed_ips_changed = original.get("fixed_ips", []) != current.get(
"fixed_ips", []
)
if not (became_router_interface or fixed_ips_changed):
LOG.debug(
"update_port_precommit: SVI scope check not needed for port "
"%(port)s owner %(owner)s router %(router)s",
{
"port": current["id"],
"owner": current.get("device_owner"),
"router": current.get("device_id"),
},
)
return
LOG.info(
"update_port_precommit: checking SVI scope validation for router "
"port %(port)s device_id=%(router)s became_router_interface=%(became)s "
"fixed_ips_changed=%(fixed_ips_changed)s",
{
"port": current["id"],
"router": current.get("device_id"),
"became": became_router_interface,
"fixed_ips_changed": fixed_ips_changed,
},
)
checked = svi_router.validate_svi_router_port(context.plugin_context, current)
if checked:
LOG.info(
"update_port_precommit: SVI scope check passed for port %(port)s "
"network %(network)s router %(router)s",
{
"port": current["id"],
"network": current.get("network_id"),
"router": current.get("device_id"),
},
)
else:
LOG.debug(
"update_port_precommit: SVI scope check not applicable for "
"port %(port)s owner %(owner)s router %(router)s",
{
"port": current["id"],
"owner": current.get("device_owner"),
"router": current.get("device_id"),
},
)
def update_port_postcommit(self, context: PortContext) -> None:
if utils.is_baremetal_port(context):
self._update_port_baremetal(context)
def _update_port_baremetal(self, context: PortContext) -> None:
current_vif_unbound = context.vif_type == portbindings.VIF_TYPE_UNBOUND
original_vif_other = context.original_vif_type == portbindings.VIF_TYPE_OTHER
current_vif_other = context.vif_type == portbindings.VIF_TYPE_OTHER
if current_vif_unbound and original_vif_other:
port = context.original
else:
port = context.current
vlan_group_name = port[portbindings.PROFILE].get("physical_network")
if current_vif_unbound and original_vif_other:
self._tenant_network_port_cleanup(context)
if vlan_group_name:
self.undersync.sync(vlan_group_name)
elif current_vif_other and 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.
This is triggered in the update_port_postcommit call as in the
delete_port_postcommit call there is no binding profile information
anymore, hence there is no way for us to identify which baremetal port
needs cleanup.
Only in the update_port_postcommit do we have access to the original
context, from which we can access the binding information.
"""
trunk_details = context.current.get("trunk_details", {})
segment_id = context.original_top_bound_segment["id"]
original_binding = context.original[portbindings.PROFILE]
if not utils.ports_bound_to_segment(
segment_id
) and utils.is_dynamic_network_segment(segment_id):
context.release_dynamic_segment(segment_id)
networks_to_remove = {segment_id}
LOG.debug(
"update_port_postcommit removing vlans %s from interface",
networks_to_remove,
)
if trunk_details:
self.trunk_driver.clean_trunk(
trunk_details=trunk_details,
binding_profile=original_binding,
host=context.original_host,
)
def delete_port_precommit(self, context):
pass
def delete_port_postcommit(self, context: PortContext) -> None:
if utils.is_baremetal_port(context):
self._delete_port_baremetal(context)
def _delete_port_baremetal(self, context: PortContext) -> None:
# Only clean up provisioning ports. Ports with tenant networks are cleaned
# up in _tenant_network_port_cleanup
port = context.current
vlan_group_name = port[portbindings.PROFILE].get("physical_network")
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.undersync.sync(vlan_group_name)
def bind_port(self, context: PortContext) -> None:
"""Bind the VXLAN network segment and allocate dynamic VLAN segments.
Our "context" knows a Port, a Network and a list of Segments.
We find the first (hopefully only) segment of type vxlan. This is the
one we bind. There may be other segments, but we only bind the vxlan
one.
We obtain the dynamic segment for this (network, vlan_group) pair.
If there are no VXLAN segments, then bind a VLAN segment instead (this
is required for VLAN-type networks like the provisioning network).
Then make the required call in to the black box: context.set_binding
which tells the framework that we have dealt with this port and they
don't need to retry or handle this some other way.
We expect to receive a call to update_port_postcommit soon after this,
which means that changes made here will get pushed to the switch at that
time.
"""
port = context.current
LOG.debug(
"Attempting to bind port %(port)s on network %(net)s",
{"port": port["id"], "net": port["network_id"]},
)
vnic_type = port.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL)
if vnic_type not in SUPPORTED_VNIC_TYPES:
LOG.debug("Refusing to bind due to unsupported vnic_type: %s", vnic_type)
return
for segment in context.segments_to_bind:
if segment[api.NETWORK_TYPE] == p_const.TYPE_VXLAN:
self._bind_port_segment(context, segment)
return
def _bind_port_segment(self, context: PortContext, segment):
network_id = context.current["network_id"]
mac_address = context.current["mac_address"]
port = context.current
vlan_group_name = port[portbindings.PROFILE].get("physical_network")
if not vlan_group_name:
LOG.error(
"bind_port_segment: physical_network is required in binding_profile "
"for baremetal port binding, but was not found. "
"port_id=%(port_id)s mac_address=%(mac)s network_id=%(network_id)s. "
"Please ensure the port's binding_profile contains physical_network "
"in local_link_information.",
{
"port_id": port["id"],
"mac": mac_address,
"network_id": network_id,
},
)
return
LOG.debug(
"bind_port_segment: interface network %s vlan group %s",
network_id,
vlan_group_name,
)
current_vlan_segment = utils.vlan_segment_for_physnet(context, vlan_group_name)
if current_vlan_segment:
LOG.info(
"vlan segment: %(segment)s already preset for physnet: %(physnet)s",
{"segment": current_vlan_segment, "physnet": vlan_group_name},
)
dynamic_segment = current_vlan_segment
else:
dynamic_segment = context.allocate_dynamic_segment(
segment={
"network_type": p_const.TYPE_VLAN,
"physical_network": vlan_group_name,
},
)
LOG.debug("bind_port_segment: Native VLAN segment %s", dynamic_segment)
trunk_details = context.current.get("trunk_details") or {}
port_id = context.current["id"]
if trunk_details:
self.trunk_driver.configure_trunk(trunk_details, port_id)
LOG.debug("continue_binding for segment: %s", segment)
context.continue_binding(
segment_id=segment[api.ID],
next_segments_to_bind=[dynamic_segment],
)
def check_vlan_transparency(self, context):
pass
def is_provisioning_network(network_id: str) -> bool:
return network_id == cfg.CONF.ml2_understack.provisioning_network