Browse Source

Multiple port binding for ML2

Functionality is added to the ML2 plugin to handle multiple port
bindings

Co-Authored-By: Anindita Das <anindita.das@intel.com>
Co-Authored-By: Miguel Lavalle <miguel.lavalle@huawei.com>

Partial-Bug: #1580880

Change-Id: Ie31d4e27e3f55edfe334c4029ca9ed685e684c39
tags/13.0.0.0b3
Jakub Libosvar 1 year ago
parent
commit
f7b62a7f29

+ 5
- 1
neutron/agent/rpc.py View File

@@ -29,6 +29,7 @@ from neutron.agent import resource_cache
29 29
 from neutron.api.rpc.callbacks import resources
30 30
 from neutron.common import constants as n_const
31 31
 from neutron.common import rpc as n_rpc
32
+from neutron.common import utils
32 33
 from neutron import objects
33 34
 
34 35
 LOG = logging.getLogger(__name__)
@@ -239,6 +240,9 @@ class CacheBackedPluginApi(PluginApi):
239 240
         # match format of old RPC interface
240 241
         mac_addr = str(netaddr.EUI(str(port_obj.mac_address),
241 242
                                    dialect=netaddr.mac_unix_expanded))
243
+        binding = utils.get_port_binding_by_status_and_host(
244
+            port_obj.binding, constants.ACTIVE, raise_if_not_found=True,
245
+            port_id=port_obj.id)
242 246
         entry = {
243 247
             'device': device,
244 248
             'network_id': port_obj.network_id,
@@ -259,7 +263,7 @@ class CacheBackedPluginApi(PluginApi):
259 263
                                              'port_security_enabled', True),
260 264
             'qos_policy_id': port_obj.qos_policy_id,
261 265
             'network_qos_policy_id': net_qos_policy_id,
262
-            'profile': port_obj.binding.profile,
266
+            'profile': binding.profile,
263 267
             'security_groups': list(port_obj.security_group_ids)
264 268
         }
265 269
         LOG.debug("Returning: %s", entry)

+ 20
- 0
neutron/common/exceptions.py View File

@@ -334,3 +334,23 @@ class FilterIDForIPNotFound(e.NotFound):
334 334
 class FailedToAddQdiscToDevice(e.NeutronException):
335 335
     message = _("Failed to add %(direction)s qdisc "
336 336
                 "to device %(device)s.")
337
+
338
+
339
+class PortBindingNotFound(e.NotFound):
340
+    message = _("Binding for port %(port_id)s for host %(host)s could not be "
341
+                "found.")
342
+
343
+
344
+class PortBindingAlreadyActive(e.Conflict):
345
+    message = _("Binding for port %(port_id)s on host %(host)s is already "
346
+                "active.")
347
+
348
+
349
+class PortBindingAlreadyExists(e.Conflict):
350
+    message = _("Binding for port %(port_id)s on host %(host)s already "
351
+                "exists.")
352
+
353
+
354
+class PortBindingError(e.NeutronException):
355
+    message = _("Binding for port %(port_id)s on host %(host)s could not be "
356
+                "created or updated.")

+ 35
- 0
neutron/common/utils.py View File

@@ -33,6 +33,7 @@ import uuid
33 33
 import eventlet
34 34
 from eventlet.green import subprocess
35 35
 import netaddr
36
+from neutron_lib.api.definitions import portbindings_extended as pb_ext
36 37
 from neutron_lib import constants as n_const
37 38
 from neutron_lib.utils import helpers
38 39
 from oslo_config import cfg
@@ -43,6 +44,7 @@ import six
43 44
 
44 45
 import neutron
45 46
 from neutron._i18n import _
47
+from neutron.common import exceptions
46 48
 from neutron.db import api as db_api
47 49
 
48 50
 TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
@@ -783,3 +785,36 @@ def bytes_to_bits(value):
783 785
 def bits_to_kilobits(value, base):
784 786
     # NOTE(slaweq): round up that even 1 bit will give 1 kbit as a result
785 787
     return int((value + (base - 1)) / base)
788
+
789
+
790
+def get_port_binding_by_status_and_host(bindings, status, host='',
791
+                                        raise_if_not_found=False,
792
+                                        port_id=None):
793
+    """Returns from an iterable the binding with the specified status and host.
794
+
795
+    The input iterable can contain zero or one binding in status ACTIVE
796
+    and zero or many bindings in status INACTIVE. As a consequence, to
797
+    unequivocally retrieve an inactive binding, the caller must specify a non
798
+    empty value for host. If host is the empty string, the first binding
799
+    satisfying the specified status will be returned. If no binding is found
800
+    with the specified status and host, None is returned or PortBindingNotFound
801
+    is raised if raise_if_not_found is True
802
+
803
+    :param bindings: An iterable containing port bindings
804
+    :param status: The status of the port binding to return. Possible values
805
+                   are ACTIVE or INACTIVE as defined in
806
+                   :file:`neutron_lib/constants.py`.
807
+    :param host: str representing the host of the binding to return.
808
+    :param raise_if_not_found: If a binding is not found and this parameter is
809
+                               True, a PortBindingNotFound exception is raised
810
+    :param port_id: The id of the binding's port
811
+    :returns: The searched for port binding or None if it is not found
812
+    :raises: PortBindingNotFound if the binding is not found and
813
+             raise_if_not_found is True
814
+    """
815
+    for binding in bindings:
816
+        if binding[pb_ext.STATUS] == status:
817
+            if not host or binding[pb_ext.HOST] == host:
818
+                return binding
819
+    if raise_if_not_found:
820
+        raise exceptions.PortBindingNotFound(port_id=port_id, host=host)

+ 77
- 0
neutron/extensions/portbindings_extended.py View File

@@ -0,0 +1,77 @@
1
+# All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from neutron_lib.api.definitions import portbindings_extended as pbe_ext
16
+from neutron_lib.api import extensions as api_extensions
17
+from neutron_lib.plugins import directory
18
+
19
+from neutron.api import extensions
20
+from neutron.api.v2 import base
21
+
22
+EXT_ALIAS = pbe_ext.ALIAS
23
+
24
+
25
+class Portbindings_extended(api_extensions.ExtensionDescriptor):
26
+    """Extension class supporting port bindings.
27
+
28
+    This class is used by neutron's extension framework to make
29
+    metadata about the port bindings available to external applications.
30
+
31
+    With admin rights one will be able to update and read the values.
32
+    """
33
+    @classmethod
34
+    def get_name(cls):
35
+        return pbe_ext.NAME
36
+
37
+    @classmethod
38
+    def get_alias(cls):
39
+        return pbe_ext.ALIAS
40
+
41
+    @classmethod
42
+    def get_description(cls):
43
+        return pbe_ext.DESCRIPTION
44
+
45
+    @classmethod
46
+    def get_updated(cls):
47
+        return pbe_ext.UPDATED_TIMESTAMP
48
+
49
+    @classmethod
50
+    def get_resources(cls):
51
+        plugin = directory.get_plugin()
52
+
53
+        params = pbe_ext.SUB_RESOURCE_ATTRIBUTE_MAP[
54
+            pbe_ext.COLLECTION_NAME]['parameters']
55
+        parent = pbe_ext.SUB_RESOURCE_ATTRIBUTE_MAP[
56
+            pbe_ext.COLLECTION_NAME]['parent']
57
+        controller = base.create_resource(
58
+            pbe_ext.COLLECTION_NAME,
59
+            pbe_ext.RESOURCE_NAME,
60
+            plugin,
61
+            params,
62
+            member_actions=pbe_ext.ACTION_MAP[pbe_ext.RESOURCE_NAME],
63
+            parent=parent,
64
+            allow_pagination=True,
65
+            allow_sorting=True,
66
+        )
67
+        exts = [
68
+            extensions.ResourceExtension(
69
+                pbe_ext.COLLECTION_NAME,
70
+                controller,
71
+                parent,
72
+                member_actions=pbe_ext.ACTION_MAP[pbe_ext.RESOURCE_NAME],
73
+                attr_map=params,
74
+            ),
75
+        ]
76
+
77
+        return exts

+ 16
- 2
neutron/objects/ports.py View File

@@ -264,7 +264,8 @@ class Port(base.NeutronDbObject):
264 264
     # Version 1.1: Add data_plane_status field
265 265
     # Version 1.2: Added segment_id to binding_levels
266 266
     # Version 1.3: distributed_binding -> distributed_bindings
267
-    VERSION = '1.3'
267
+    # Version 1.4: Attribute binding becomes ListOfObjectsField
268
+    VERSION = '1.4'
268 269
 
269 270
     db_model = models_v2.Port
270 271
 
@@ -282,7 +283,7 @@ class Port(base.NeutronDbObject):
282 283
         'allowed_address_pairs': obj_fields.ListOfObjectsField(
283 284
             'AllowedAddressPair', nullable=True
284 285
         ),
285
-        'binding': obj_fields.ObjectField(
286
+        'binding': obj_fields.ListOfObjectsField(
286 287
             'PortBinding', nullable=True
287 288
         ),
288 289
         'data_plane_status': obj_fields.ObjectField(
@@ -473,6 +474,19 @@ class Port(base.NeutronDbObject):
473 474
             bindings = primitive.pop('distributed_bindings', [])
474 475
             primitive['distributed_binding'] = (bindings[0]
475 476
                                                 if bindings else None)
477
+        if _target_version < (1, 4):
478
+            # In version 1.4 we add support for multiple port bindings.
479
+            # Previous versions only support one port binding. The following
480
+            # lines look for the active port binding, which is the only one
481
+            # needed in previous versions
482
+            if 'binding' in primitive:
483
+                original_binding = primitive['binding']
484
+                primitive['binding'] = None
485
+                for a_binding in original_binding:
486
+                    if (a_binding['versioned_object.data']['status'] ==
487
+                            constants.ACTIVE):
488
+                        primitive['binding'] = a_binding
489
+                        break
476 490
 
477 491
     @classmethod
478 492
     def get_ports_by_router(cls, context, router_id, owner, subnet):

+ 5
- 0
neutron/plugins/ml2/driver_context.py View File

@@ -50,6 +50,11 @@ class InstanceSnapshot(object):
50 50
             session.add(self._model_class(**{col: getattr(self, col)
51 51
                                              for col in self._cols}))
52 52
 
53
+    def __getitem__(self, item):
54
+        if item not in self._cols:
55
+            raise KeyError(item)
56
+        return getattr(self, item)
57
+
53 58
 
54 59
 class MechanismDriverContext(object):
55 60
     """MechanismDriver context base class."""

+ 3
- 1
neutron/plugins/ml2/models.py View File

@@ -58,8 +58,10 @@ class PortBinding(model_base.BASEV2):
58 58
     port = orm.relationship(
59 59
         models_v2.Port,
60 60
         load_on_pending=True,
61
+        # TODO(mlavalle) change name of the relationship to reflect that it is
62
+        # now an iterable
61 63
         backref=orm.backref("port_binding",
62
-                            lazy='joined', uselist=False,
64
+                            lazy='joined',
63 65
                             cascade='delete'))
64 66
     revises_on_change = ('port', )
65 67
 

+ 284
- 40
neutron/plugins/ml2/plugin.py View File

@@ -23,6 +23,7 @@ from neutron_lib.api.definitions import network_mtu_writable as mtuw_apidef
23 23
 from neutron_lib.api.definitions import port as port_def
24 24
 from neutron_lib.api.definitions import port_security as psec
25 25
 from neutron_lib.api.definitions import portbindings
26
+from neutron_lib.api.definitions import portbindings_extended as pbe_ext
26 27
 from neutron_lib.api.definitions import subnet as subnet_def
27 28
 from neutron_lib.api.definitions import vlantransparent as vlan_apidef
28 29
 from neutron_lib.api import extensions
@@ -64,6 +65,7 @@ from neutron.api.rpc.handlers import metadata_rpc
64 65
 from neutron.api.rpc.handlers import resources_rpc
65 66
 from neutron.api.rpc.handlers import securitygroups_rpc
66 67
 from neutron.common import constants as n_const
68
+from neutron.common import exceptions as n_exc
67 69
 from neutron.common import rpc as n_rpc
68 70
 from neutron.common import utils
69 71
 from neutron.db import _model_query as model_query
@@ -87,6 +89,8 @@ from neutron.db import subnet_service_type_mixin
87 89
 from neutron.db import vlantransparent_db
88 90
 from neutron.extensions import providernet as provider
89 91
 from neutron.extensions import vlantransparent
92
+from neutron.objects import base as base_obj
93
+from neutron.objects import ports as ports_obj
90 94
 from neutron.plugins.ml2.common import exceptions as ml2_exc
91 95
 from neutron.plugins.ml2 import db
92 96
 from neutron.plugins.ml2 import driver_context
@@ -113,7 +117,7 @@ def _ml2_port_result_filter_hook(query, filters):
113 117
     if not values:
114 118
         return query
115 119
     bind_criteria = models.PortBinding.host.in_(values)
116
-    return query.filter(models_v2.Port.port_binding.has(bind_criteria))
120
+    return query.filter(models_v2.Port.port_binding.any(bind_criteria))
117 121
 
118 122
 
119 123
 @resource_extend.has_resource_extenders
@@ -161,7 +165,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
161 165
                                     "ip-substring-filtering",
162 166
                                     "port-security-groups-filtering",
163 167
                                     "empty-string-filtering",
164
-                                    "port-mac-address-regenerate"]
168
+                                    "port-mac-address-regenerate",
169
+                                    "binding-extended"]
165 170
 
166 171
     @property
167 172
     def supported_extension_aliases(self):
@@ -241,12 +246,14 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
241 246
                           **kwargs):
242 247
         port_id = object_id
243 248
         port = db.get_port(context, port_id)
244
-        if not port or not port.port_binding:
249
+        port_binding = utils.get_port_binding_by_status_and_host(
250
+            getattr(port, 'port_binding', []), const.ACTIVE)
251
+        if not port or not port_binding:
245 252
             LOG.debug("Port %s was deleted so its status cannot be updated.",
246 253
                       port_id)
247 254
             return
248
-        if port.port_binding.vif_type in (portbindings.VIF_TYPE_BINDING_FAILED,
249
-                                          portbindings.VIF_TYPE_UNBOUND):
255
+        if port_binding.vif_type in (portbindings.VIF_TYPE_BINDING_FAILED,
256
+                                     portbindings.VIF_TYPE_UNBOUND):
250 257
             # NOTE(kevinbenton): we hit here when a port is created without
251 258
             # a host ID and the dhcp agent notifies that its wiring is done
252 259
             LOG.debug("Port %s cannot update to ACTIVE because it "
@@ -320,13 +327,15 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
320 327
                                 new_mac=port['mac_address'])
321 328
         return mac_change
322 329
 
323
-    def _process_port_binding(self, mech_context, attrs):
324
-        plugin_context = mech_context._plugin_context
325
-        binding = mech_context._binding
326
-        port = mech_context.current
327
-        port_id = port['id']
328
-        changes = False
330
+    def _clear_port_binding(self, mech_context, binding, port, original_host):
331
+        binding.vif_type = portbindings.VIF_TYPE_UNBOUND
332
+        binding.vif_details = ''
333
+        db.clear_binding_levels(mech_context._plugin_context, port['id'],
334
+                                original_host)
335
+        mech_context._clear_binding_levels()
329 336
 
337
+    def _process_port_binding_attributes(self, binding, attrs):
338
+        changes = False
330 339
         host = const.ATTR_NOT_SPECIFIED
331 340
         if attrs and portbindings.HOST_ID in attrs:
332 341
             host = attrs.get(portbindings.HOST_ID) or ''
@@ -354,23 +363,28 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
354 363
                 msg = _("binding:profile value too large")
355 364
                 raise exc.InvalidInput(error_message=msg)
356 365
             changes = True
366
+        return changes, original_host
367
+
368
+    def _process_port_binding(self, mech_context, attrs):
369
+        plugin_context = mech_context._plugin_context
370
+        binding = mech_context._binding
371
+        port = mech_context.current
372
+        changes, original_host = self._process_port_binding_attributes(binding,
373
+                                                                       attrs)
357 374
 
358 375
         # Unbind the port if needed.
359 376
         if changes:
360
-            binding.vif_type = portbindings.VIF_TYPE_UNBOUND
361
-            binding.vif_details = ''
362
-            db.clear_binding_levels(plugin_context, port_id, original_host)
363
-            mech_context._clear_binding_levels()
377
+            self._clear_port_binding(mech_context, binding, port,
378
+                                     original_host)
364 379
             port['status'] = const.PORT_STATUS_DOWN
365 380
             super(Ml2Plugin, self).update_port(
366
-                mech_context._plugin_context, port_id,
367
-                {port_def.RESOURCE_NAME: {'status': const.PORT_STATUS_DOWN}})
381
+                mech_context._plugin_context, port['id'],
382
+                {port_def.RESOURCE_NAME:
383
+                    {'status': const.PORT_STATUS_DOWN}})
368 384
 
369 385
         if port['device_owner'] == const.DEVICE_OWNER_DVR_INTERFACE:
370
-            binding.vif_type = portbindings.VIF_TYPE_UNBOUND
371
-            binding.vif_details = ''
372
-            db.clear_binding_levels(plugin_context, port_id, original_host)
373
-            mech_context._clear_binding_levels()
386
+            self._clear_port_binding(mech_context, binding, port,
387
+                                     original_host)
374 388
             binding.host = ''
375 389
 
376 390
         self._update_port_dict_binding(port, binding)
@@ -379,7 +393,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
379 393
 
380 394
     @db_api.retry_db_errors
381 395
     def _bind_port_if_needed(self, context, allow_notify=False,
382
-                             need_notify=False):
396
+                             need_notify=False, allow_commit=True):
383 397
         if not context.network.network_segments:
384 398
             LOG.debug("Network %s has no segments, skipping binding",
385 399
                       context.network.current['id'])
@@ -399,7 +413,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
399 413
                 context, need_notify)
400 414
 
401 415
             if count == MAX_BIND_TRIES or not try_again:
402
-                if self._should_bind_port(context):
416
+                if self._should_bind_port(context) and allow_commit:
403 417
                     # At this point, we attempted to bind a port and reached
404 418
                     # its final binding state. Binding either succeeded or
405 419
                     # exhausted all attempts, thus no need to try again.
@@ -488,7 +502,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
488 502
             # mechanism driver update_port_*commit() calls.
489 503
             try:
490 504
                 port_db = self._get_port(plugin_context, port_id)
491
-                cur_binding = port_db.port_binding
505
+                cur_binding = utils.get_port_binding_by_status_and_host(
506
+                    port_db.port_binding, const.ACTIVE)
492 507
             except exc.PortNotFound:
493 508
                 port_db, cur_binding = None, None
494 509
             if not port_db or not cur_binding:
@@ -532,8 +547,14 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
532 547
                 # to optimize this code to avoid fetching it.
533 548
                 cur_binding = db.get_distributed_port_binding_by_host(
534 549
                     plugin_context, port_id, orig_binding.host)
550
+            cur_context_binding = cur_binding
551
+            if new_binding.status == const.INACTIVE:
552
+                cur_context_binding = (
553
+                    utils.get_port_binding_by_status_and_host(
554
+                        port_db.port_binding, const.INACTIVE,
555
+                        host=new_binding.host))
535 556
             cur_context = driver_context.PortContext(
536
-                self, plugin_context, port, network, cur_binding, None,
557
+                self, plugin_context, port, network, cur_context_binding, None,
537 558
                 original_port=oport)
538 559
 
539 560
             # Commit our binding results only if port has not been
@@ -549,20 +570,24 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
549 570
             if commit:
550 571
                 # Update the port's binding state with our binding
551 572
                 # results.
552
-                cur_binding.vif_type = new_binding.vif_type
553
-                cur_binding.vif_details = new_binding.vif_details
573
+                if new_binding.status == const.INACTIVE:
574
+                    cur_context_binding.status = const.ACTIVE
575
+                    cur_binding.status = const.INACTIVE
576
+                else:
577
+                    cur_context_binding.vif_type = new_binding.vif_type
578
+                    cur_context_binding.vif_details = new_binding.vif_details
554 579
                 db.clear_binding_levels(plugin_context, port_id,
555 580
                                         cur_binding.host)
556 581
                 db.set_binding_levels(plugin_context,
557 582
                                       bind_context._binding_levels)
558 583
                 # refresh context with a snapshot of updated state
559 584
                 cur_context._binding = driver_context.InstanceSnapshot(
560
-                    cur_binding)
585
+                    cur_context_binding)
561 586
                 cur_context._binding_levels = bind_context._binding_levels
562 587
 
563 588
                 # Update PortContext's port dictionary to reflect the
564 589
                 # updated binding state.
565
-                self._update_port_dict_binding(port, cur_binding)
590
+                self._update_port_dict_binding(port, cur_context_binding)
566 591
 
567 592
                 # Update the port status if requested by the bound driver.
568 593
                 if (bind_context._binding_levels and
@@ -632,9 +657,11 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
632 657
     @resource_extend.extends([port_def.COLLECTION_NAME])
633 658
     def _ml2_extend_port_dict_binding(port_res, port_db):
634 659
         plugin = directory.get_plugin()
660
+        port_binding = utils.get_port_binding_by_status_and_host(
661
+            port_db.port_binding, const.ACTIVE)
635 662
         # None when called during unit tests for other plugins.
636
-        if port_db.port_binding:
637
-            plugin._update_port_dict_binding(port_res, port_db.port_binding)
663
+        if port_binding:
664
+            plugin._update_port_dict_binding(port_res, port_binding)
638 665
 
639 666
     # ML2's resource extend functions allow extension drivers that extend
640 667
     # attributes for the resources to add those attributes to the result.
@@ -1304,7 +1331,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
1304 1331
                         original_port=original_port)
1305 1332
         with db_api.context_manager.writer.using(context):
1306 1333
             port_db = self._get_port(context, id)
1307
-            binding = port_db.port_binding
1334
+            binding = utils.get_port_binding_by_status_and_host(
1335
+                port_db.port_binding, const.ACTIVE)
1308 1336
             if not binding:
1309 1337
                 raise exc.PortNotFound(port_id=id)
1310 1338
             mac_address_updated = self._check_mac_update_allowed(
@@ -1522,7 +1550,9 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
1522 1550
         with db_api.context_manager.writer.using(context):
1523 1551
             try:
1524 1552
                 port_db = self._get_port(context, id)
1525
-                binding = port_db.port_binding
1553
+                binding = utils.get_port_binding_by_status_and_host(
1554
+                    port_db.port_binding, const.ACTIVE,
1555
+                    raise_if_not_found=True, port_id=id)
1526 1556
             except exc.PortNotFound:
1527 1557
                 LOG.debug("The port '%s' was deleted", id)
1528 1558
                 return
@@ -1640,14 +1670,15 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
1640 1670
                 # related attribute port_binding could disappear in
1641 1671
                 # concurrent port deletion.
1642 1672
                 # It's not an error condition.
1643
-                binding = port_db.port_binding
1673
+                binding = utils.get_port_binding_by_status_and_host(
1674
+                    port_db.port_binding, const.ACTIVE)
1644 1675
                 if not binding:
1645 1676
                     LOG.info("Binding info for port %s was not found, "
1646 1677
                              "it might have been deleted already.",
1647 1678
                              port_id)
1648 1679
                     return
1649 1680
                 levels = db.get_binding_levels(plugin_context, port_db.id,
1650
-                                               port_db.port_binding.host)
1681
+                                               binding.host)
1651 1682
                 port_context = driver_context.PortContext(
1652 1683
                     self, plugin_context, port, network, binding, levels)
1653 1684
 
@@ -1683,7 +1714,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
1683 1714
                         plugin_context, port['id'], host)
1684 1715
                     bindlevelhost_match = host
1685 1716
                 else:
1686
-                    binding = port_db.port_binding
1717
+                    binding = utils.get_port_binding_by_status_and_host(
1718
+                        port_db.port_binding, const.ACTIVE)
1687 1719
                     bindlevelhost_match = binding.host if binding else None
1688 1720
                 if not binding:
1689 1721
                     LOG.info("Binding info for port %s was not found, "
@@ -1768,11 +1800,13 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
1768 1800
                 # listening for db events can modify the port if necessary
1769 1801
                 context.session.flush()
1770 1802
                 updated_port = self._make_port_dict(port)
1771
-                levels = db.get_binding_levels(context, port.id,
1772
-                                               port.port_binding.host)
1803
+                binding = utils.get_port_binding_by_status_and_host(
1804
+                    port.port_binding, const.ACTIVE, raise_if_not_found=True,
1805
+                    port_id=port_id)
1806
+                levels = db.get_binding_levels(context, port.id, binding.host)
1773 1807
                 mech_context = driver_context.PortContext(
1774
-                    self, context, updated_port, network, port.port_binding,
1775
-                    levels, original_port=original_port)
1808
+                    self, context, updated_port, network, binding, levels,
1809
+                    original_port=original_port)
1776 1810
                 self.mechanism_manager.update_port_precommit(mech_context)
1777 1811
                 updated = True
1778 1812
             elif port['device_owner'] == const.DEVICE_OWNER_DVR_INTERFACE:
@@ -1964,3 +1998,213 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
1964 1998
             self.mechanism_manager.update_network_precommit(mech_context)
1965 1999
         elif event == events.AFTER_CREATE or event == events.AFTER_DELETE:
1966 2000
             self.mechanism_manager.update_network_postcommit(mech_context)
2001
+
2002
+    @staticmethod
2003
+    def _validate_compute_port(port):
2004
+        if not port['device_owner'].startswith(
2005
+                const.DEVICE_OWNER_COMPUTE_PREFIX):
2006
+            msg = _('Invalid port %s. Operation only valid on compute '
2007
+                    'ports') % port['id']
2008
+            raise exc.BadRequest(resource='port', msg=msg)
2009
+
2010
+    def _make_port_binding_dict(self, binding, fields=None):
2011
+        res = {key: binding[key] for key in (
2012
+                    pbe_ext.HOST, pbe_ext.VIF_TYPE, pbe_ext.VNIC_TYPE,
2013
+                    pbe_ext.STATUS)}
2014
+        if isinstance(binding, ports_obj.PortBinding):
2015
+            res[pbe_ext.PROFILE] = binding.profile or {}
2016
+            res[pbe_ext.VIF_DETAILS] = binding.vif_details or {}
2017
+        else:
2018
+            res[pbe_ext.PROFILE] = self._get_profile(binding)
2019
+            res[pbe_ext.VIF_DETAILS] = self._get_vif_details(binding)
2020
+        return db_utils.resource_fields(res, fields)
2021
+
2022
+    def _get_port_binding_attrs(self, binding, host=None):
2023
+        return {portbindings.VNIC_TYPE: binding.get(pbe_ext.VNIC_TYPE),
2024
+                portbindings.HOST_ID: binding.get(pbe_ext.HOST) or host,
2025
+                portbindings.PROFILE: binding.get(pbe_ext.PROFILE, {})}
2026
+
2027
+    def _process_active_binding_change(self, changes, mech_context, port_dict,
2028
+                                       original_host):
2029
+        if changes:
2030
+            self._clear_port_binding(mech_context,
2031
+                                     mech_context._binding, port_dict,
2032
+                                     original_host)
2033
+            port_dict['status'] = const.PORT_STATUS_DOWN
2034
+            super(Ml2Plugin, self).update_port(
2035
+                mech_context._plugin_context, port_dict['id'],
2036
+                {port_def.RESOURCE_NAME:
2037
+                    {'status': const.PORT_STATUS_DOWN}})
2038
+        self._update_port_dict_binding(port_dict,
2039
+                                       mech_context._binding)
2040
+        mech_context._binding.persist_state_to_session(
2041
+            mech_context._plugin_context.session)
2042
+
2043
+    @utils.transaction_guard
2044
+    @db_api.retry_if_session_inactive()
2045
+    def create_port_binding(self, context, port_id, binding):
2046
+        attrs = binding[pbe_ext.RESOURCE_NAME]
2047
+        with db_api.context_manager.writer.using(context):
2048
+            port_db = self._get_port(context, port_id)
2049
+            self._validate_compute_port(port_db)
2050
+            if self._get_binding_for_host(port_db.port_binding,
2051
+                                          attrs[pbe_ext.HOST]):
2052
+                raise n_exc.PortBindingAlreadyExists(
2053
+                    port_id=port_id, host=attrs[pbe_ext.HOST])
2054
+            status = const.ACTIVE
2055
+            is_active_binding = True
2056
+            active_binding = utils.get_port_binding_by_status_and_host(
2057
+                port_db.port_binding, const.ACTIVE)
2058
+            if active_binding:
2059
+                status = const.INACTIVE
2060
+                is_active_binding = False
2061
+            network = self.get_network(context, port_db['network_id'])
2062
+            port_dict = self._make_port_dict(port_db)
2063
+            new_binding = models.PortBinding(
2064
+                port_id=port_id,
2065
+                vif_type=portbindings.VIF_TYPE_UNBOUND,
2066
+                status=status)
2067
+            mech_context = driver_context.PortContext(self, context, port_dict,
2068
+                                                      network, new_binding,
2069
+                                                      None)
2070
+            changes, original_host = self._process_port_binding_attributes(
2071
+                mech_context._binding, self._get_port_binding_attrs(attrs))
2072
+            if is_active_binding:
2073
+                self._process_active_binding_change(changes, mech_context,
2074
+                                                    port_dict, original_host)
2075
+        bind_context = self._bind_port_if_needed(
2076
+            mech_context, allow_commit=is_active_binding)
2077
+        if (bind_context._binding.vif_type ==
2078
+                portbindings.VIF_TYPE_BINDING_FAILED):
2079
+            raise n_exc.PortBindingError(port_id=port_id,
2080
+                                         host=attrs[pbe_ext.HOST])
2081
+        bind_context._binding.port_id = port_id
2082
+        bind_context._binding.status = status
2083
+        if not is_active_binding:
2084
+            with db_api.context_manager.writer.using(context):
2085
+                bind_context._binding.persist_state_to_session(context.session)
2086
+                db.set_binding_levels(context, bind_context._binding_levels)
2087
+        return self._make_port_binding_dict(bind_context._binding)
2088
+
2089
+    @utils.transaction_guard
2090
+    @db_api.retry_if_session_inactive()
2091
+    def get_port_bindings(self, context, port_id, filters=None, fields=None,
2092
+                          sorts=None, limit=None, marker=None,
2093
+                          page_reverse=False):
2094
+        port = ports_obj.Port.get_object(context, id=port_id)
2095
+        if not port:
2096
+            raise exc.PortNotFound(port_id=port_id)
2097
+        self._validate_compute_port(port)
2098
+        filters = filters or {}
2099
+        pager = base_obj.Pager(sorts, limit, page_reverse, marker)
2100
+        bindings = ports_obj.PortBinding.get_objects(
2101
+            context, _pager=pager, port_id=port_id, **filters)
2102
+
2103
+        return [self._make_port_binding_dict(binding, fields)
2104
+                for binding in bindings]
2105
+
2106
+    @utils.transaction_guard
2107
+    @db_api.retry_if_session_inactive()
2108
+    def get_port_binding(self, context, host, port_id, fields=None):
2109
+        port = ports_obj.Port.get_object(context, id=port_id)
2110
+        if not port:
2111
+            raise exc.PortNotFound(port_id=port_id)
2112
+        self._validate_compute_port(port)
2113
+        binding = ports_obj.PortBinding.get_object(context, host=host,
2114
+                                                   port_id=port_id)
2115
+        if not binding:
2116
+            raise n_exc.PortBindingNotFound(port_id=port_id, host=host)
2117
+        return self._make_port_binding_dict(binding, fields)
2118
+
2119
+    def _get_binding_for_host(self, bindings, host):
2120
+        for binding in bindings:
2121
+            if binding.host == host:
2122
+                return binding
2123
+
2124
+    @utils.transaction_guard
2125
+    @db_api.retry_if_session_inactive()
2126
+    def update_port_binding(self, context, host, port_id, binding):
2127
+        attrs = binding[pbe_ext.RESOURCE_NAME]
2128
+        with db_api.context_manager.writer.using(context):
2129
+            port_db = self._get_port(context, port_id)
2130
+            self._validate_compute_port(port_db)
2131
+            original_binding = self._get_binding_for_host(port_db.port_binding,
2132
+                                                          host)
2133
+            if not original_binding:
2134
+                raise n_exc.PortBindingNotFound(port_id=port_id, host=host)
2135
+            is_active_binding = (original_binding.status == const.ACTIVE)
2136
+            network = self.get_network(context, port_db['network_id'])
2137
+            port_dict = self._make_port_dict(port_db)
2138
+            mech_context = driver_context.PortContext(self, context, port_dict,
2139
+                                                      network,
2140
+                                                      original_binding, None)
2141
+            changes, original_host = self._process_port_binding_attributes(
2142
+                mech_context._binding, self._get_port_binding_attrs(attrs,
2143
+                                                                    host=host))
2144
+            if is_active_binding:
2145
+                self._process_active_binding_change(changes, mech_context,
2146
+                                                    port_dict, original_host)
2147
+        bind_context = self._bind_port_if_needed(
2148
+            mech_context, allow_commit=is_active_binding)
2149
+        if (bind_context._binding.vif_type ==
2150
+                portbindings.VIF_TYPE_BINDING_FAILED):
2151
+            raise n_exc.PortBindingError(port_id=port_id, host=host)
2152
+        if not is_active_binding:
2153
+            with db_api.context_manager.writer.using(context):
2154
+                bind_context._binding.persist_state_to_session(context.session)
2155
+                db.set_binding_levels(context, bind_context._binding_levels)
2156
+        return self._make_port_binding_dict(bind_context._binding)
2157
+
2158
+    @utils.transaction_guard
2159
+    @db_api.retry_if_session_inactive()
2160
+    def activate(self, context, host, port_id):
2161
+        with db_api.context_manager.writer.using(context):
2162
+            # TODO(mlavalle) Next two lines can be removed when bug #1770267 is
2163
+            # fixed
2164
+            if isinstance(port_id, dict):
2165
+                port_id = port_id['port_id']
2166
+            port_db = self._get_port(context, port_id)
2167
+            self._validate_compute_port(port_db)
2168
+            active_binding = utils.get_port_binding_by_status_and_host(
2169
+                port_db.port_binding, const.ACTIVE)
2170
+            if host == (active_binding and active_binding.host):
2171
+                raise n_exc.PortBindingAlreadyActive(port_id=port_id,
2172
+                                                     host=host)
2173
+            inactive_binding = utils.get_port_binding_by_status_and_host(
2174
+                port_db.port_binding, const.INACTIVE, host=host)
2175
+            if not inactive_binding or inactive_binding.host != host:
2176
+                raise n_exc.PortBindingNotFound(port_id=port_id, host=host)
2177
+            network = self.get_network(context, port_db['network_id'])
2178
+            port_dict = self._make_port_dict(port_db)
2179
+            levels = db.get_binding_levels(context, port_id,
2180
+                                           active_binding.host)
2181
+            original_context = driver_context.PortContext(self, context,
2182
+                                                          port_dict, network,
2183
+                                                          active_binding,
2184
+                                                          levels)
2185
+            self._clear_port_binding(original_context, active_binding,
2186
+                                     port_dict, active_binding.host)
2187
+            port_dict['status'] = const.PORT_STATUS_DOWN
2188
+            super(Ml2Plugin, self).update_port(
2189
+                context, port_dict['id'],
2190
+                {port_def.RESOURCE_NAME:
2191
+                    {'status': const.PORT_STATUS_DOWN}})
2192
+            levels = db.get_binding_levels(context, port_id,
2193
+                                           inactive_binding.host)
2194
+            bind_context = driver_context.PortContext(self, context, port_dict,
2195
+                                                      network,
2196
+                                                      inactive_binding, levels)
2197
+        for count in range(MAX_BIND_TRIES):
2198
+            cur_context, _, try_again = self._commit_port_binding(
2199
+                original_context, bind_context, need_notify=True,
2200
+                try_again=True)
2201
+            if not try_again:
2202
+                self.notifier.port_delete(context, port_id)
2203
+                return self._make_port_binding_dict(cur_context._binding)
2204
+        raise n_exc.PortBindingError(port_id=port_id, host=host)
2205
+
2206
+    @utils.transaction_guard
2207
+    @db_api.retry_if_session_inactive()
2208
+    def delete_port_binding(self, context, host, port_id):
2209
+        ports_obj.PortBinding.delete_objects(context, host=host,
2210
+                                             port_id=port_id)

+ 7
- 2
neutron/services/logapi/common/validators.py View File

@@ -14,11 +14,13 @@
14 14
 #    under the License.
15 15
 
16 16
 from neutron_lib.api.definitions import portbindings
17
+from neutron_lib import constants
17 18
 from neutron_lib.plugins import constants as plugin_const
18 19
 from neutron_lib.plugins import directory
19 20
 from oslo_log import log as logging
20 21
 from sqlalchemy.orm import exc as orm_exc
21 22
 
23
+from neutron.common import utils
22 24
 from neutron.db import _utils as db_utils
23 25
 from neutron.db.models import securitygroup as sg_db
24 26
 from neutron.objects import ports
@@ -93,13 +95,16 @@ def validate_log_type_for_port(log_type, port):
93 95
 
94 96
     log_plugin = directory.get_plugin(alias=plugin_const.LOG_API)
95 97
     drivers = log_plugin.driver_manager.drivers
98
+    port_binding = utils.get_port_binding_by_status_and_host(
99
+        port.binding, constants.ACTIVE, raise_if_not_found=True,
100
+        port_id=port['id'])
96 101
     for driver in drivers:
97
-        vif_type = port.binding.vif_type
102
+        vif_type = port_binding.vif_type
98 103
         if vif_type not in SKIPPED_VIF_TYPES:
99 104
             if not _validate_vif_type(driver, vif_type, port['id']):
100 105
                 continue
101 106
         else:
102
-            vnic_type = port.binding.vnic_type
107
+            vnic_type = port_binding.vnic_type
103 108
             if not _validate_vnic_type(driver, vnic_type, port['id']):
104 109
                 continue
105 110
 

+ 7
- 2
neutron/services/qos/drivers/manager.py View File

@@ -13,6 +13,7 @@
13 13
 from neutron_lib.api.definitions import portbindings
14 14
 from neutron_lib.callbacks import events
15 15
 from neutron_lib.callbacks import registry
16
+from neutron_lib import constants as lib_constants
16 17
 from neutron_lib.services.qos import constants as qos_consts
17 18
 from oslo_log import log as logging
18 19
 
@@ -22,6 +23,7 @@ from neutron.api.rpc.callbacks import resources
22 23
 from neutron.api.rpc.handlers import resources_rpc
23 24
 from neutron.common import constants
24 25
 from neutron.common import exceptions
26
+from neutron.common import utils
25 27
 from neutron.objects.qos import policy as policy_object
26 28
 
27 29
 
@@ -134,13 +136,16 @@ class QosServiceDriverManager(object):
134 136
         self.rpc_notifications_required |= driver.requires_rpc_notifications
135 137
 
136 138
     def validate_rule_for_port(self, rule, port):
139
+        port_binding = utils.get_port_binding_by_status_and_host(
140
+            port.binding, lib_constants.ACTIVE, raise_if_not_found=True,
141
+            port_id=port['id'])
137 142
         for driver in self._drivers:
138
-            vif_type = port.binding.vif_type
143
+            vif_type = port_binding.vif_type
139 144
             if vif_type not in SKIPPED_VIF_TYPES:
140 145
                 if not self._validate_vif_type(driver, vif_type, port['id']):
141 146
                     continue
142 147
             else:
143
-                vnic_type = port.binding.vnic_type
148
+                vnic_type = port_binding.vnic_type
144 149
                 if not self._validate_vnic_type(driver, vnic_type, port['id']):
145 150
                     continue
146 151
 

+ 1
- 0
neutron/tests/contrib/hooks/api_all_extensions View File

@@ -6,6 +6,7 @@ NETWORK_API_EXTENSIONS+=",allowed-address-pairs"
6 6
 NETWORK_API_EXTENSIONS+=",auto-allocated-topology"
7 7
 NETWORK_API_EXTENSIONS+=",availability_zone"
8 8
 NETWORK_API_EXTENSIONS+=",binding"
9
+NETWORK_API_EXTENSIONS+=",binding-extended"
9 10
 NETWORK_API_EXTENSIONS+=",default-subnetpools"
10 11
 NETWORK_API_EXTENSIONS+=",dhcp_agent_scheduler"
11 12
 NETWORK_API_EXTENSIONS+=",dns-integration"

+ 32
- 0
neutron/tests/unit/common/test_utils.py View File

@@ -21,14 +21,18 @@ import ddt
21 21
 import eventlet
22 22
 import mock
23 23
 import netaddr
24
+from neutron_lib.api.definitions import portbindings_extended as pb_ext
24 25
 from neutron_lib import constants
25 26
 from oslo_log import log as logging
27
+from oslo_utils import uuidutils
26 28
 import six
27 29
 import testscenarios
28 30
 import testtools
29 31
 
30 32
 from neutron.common import constants as common_constants
33
+from neutron.common import exceptions
31 34
 from neutron.common import utils
35
+from neutron.objects import ports
32 36
 from neutron.tests import base
33 37
 from neutron.tests.unit import tests
34 38
 
@@ -524,3 +528,31 @@ class TestIECUnitConversions(BaseUnitConversionTest, base.BaseTestCase):
524 528
                 expected_kilobits,
525 529
                 utils.bits_to_kilobits(input_bits, self.base_unit)
526 530
             )
531
+
532
+
533
+class TestGetPortBindingByStatusAndHost(base.BaseTestCase):
534
+
535
+    def test_get_port_binding_by_status_and_host(self):
536
+        bindings = []
537
+        self.assertIsNone(utils.get_port_binding_by_status_and_host(
538
+                              bindings, constants.INACTIVE))
539
+        bindings.extend([ports.PortBinding(
540
+                             port_id=uuidutils.generate_uuid(), host='host-1',
541
+                             status=constants.INACTIVE),
542
+                         ports.PortBinding(
543
+                             port_id=uuidutils.generate_uuid(), host='host-2',
544
+                             status=constants.INACTIVE)])
545
+        self.assertEqual(
546
+            'host-1', utils.get_port_binding_by_status_and_host(
547
+                          bindings,
548
+                          constants.INACTIVE)[pb_ext.HOST])
549
+        self.assertEqual(
550
+            'host-2', utils.get_port_binding_by_status_and_host(
551
+                          bindings,
552
+                          constants.INACTIVE,
553
+                          host='host-2')[pb_ext.HOST])
554
+        self.assertIsNone(utils.get_port_binding_by_status_and_host(
555
+                              bindings, constants.ACTIVE))
556
+        self.assertRaises(exceptions.PortBindingNotFound,
557
+                          utils.get_port_binding_by_status_and_host, bindings,
558
+                          constants.ACTIVE, 'host', True, 'port_id')

+ 1
- 1
neutron/tests/unit/objects/test_objects.py View File

@@ -62,7 +62,7 @@ object_data = {
62 62
     'NetworkPortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3',
63 63
     'NetworkRBAC': '1.0-c8a67f39809c5a3c8c7f26f2f2c620b2',
64 64
     'NetworkSegment': '1.0-57b7f2960971e3b95ded20cbc59244a8',
65
-    'Port': '1.3-4cb798ffc8b08f2657c0bd8afa708e9e',
65
+    'Port': '1.4-c3937b92962d5b43a09a7de2f44e0ab7',
66 66
     'PortBinding': '1.0-3306deeaa6deb01e33af06777d48d578',
67 67
     'PortBindingLevel': '1.1-50d47f63218f87581b6cd9a62db574e5',
68 68
     'PortDataPlaneStatus': '1.0-25be74bda46c749653a10357676c0ab2',

+ 34
- 0
neutron/tests/unit/objects/test_ports.py View File

@@ -11,6 +11,7 @@
11 11
 #    under the License.
12 12
 
13 13
 import mock
14
+from neutron_lib import constants
14 15
 from oslo_utils import uuidutils
15 16
 import testscenarios
16 17
 
@@ -418,3 +419,36 @@ class PortDbObjectTestCase(obj_test_base.BaseDbObjectTestCase,
418 419
         binding_data = (
419 420
             port_data['distributed_binding']['versioned_object.data'])
420 421
         self.assertEqual(binding.host, binding_data['host'])
422
+
423
+    def test_v1_4_to_v1_3_converts_binding_to_portbinding_object(self):
424
+        port_v1_4 = self._create_test_port()
425
+        port_v1_3 = port_v1_4.obj_to_primitive(target_version='1.3')
426
+
427
+        # Port has no bindings, so binding attribute should be None
428
+        self.assertIsNone(port_v1_3['versioned_object.data']['binding'])
429
+        active_binding = ports.PortBinding(self.context, port_id=port_v1_4.id,
430
+                                           host='host1', vif_type='type')
431
+        inactive_binding = ports.PortBinding(
432
+            self.context, port_id=port_v1_4.id, host='host2', vif_type='type',
433
+            status=constants.INACTIVE)
434
+        active_binding.create()
435
+        inactive_binding.create()
436
+        port_v1_4 = ports.Port.get_object(self.context, id=port_v1_4.id)
437
+        port_v1_3 = port_v1_4.obj_to_primitive(target_version='1.3')
438
+        binding = port_v1_3['versioned_object.data']['binding']
439
+
440
+        # Port has active binding, so the binding attribute should point to it
441
+        self.assertEqual('host1', binding['versioned_object.data']['host'])
442
+        active_binding.delete()
443
+        port_v1_4 = ports.Port.get_object(self.context, id=port_v1_4.id)
444
+        port_v1_3 = port_v1_4.obj_to_primitive(target_version='1.3')
445
+
446
+        # Port has no active bindings, so binding attribute should be None
447
+        self.assertIsNone(port_v1_3['versioned_object.data']['binding'])
448
+
449
+        # Port with no binding attribute should be handled without raising
450
+        # exception
451
+        primitive = port_v1_4.obj_to_primitive()
452
+        primitive['versioned_object.data'].pop('binding')
453
+        port_v1_4_no_binding = port_v1_4.obj_from_primitive(primitive)
454
+        port_v1_4_no_binding.obj_to_primitive(target_version='1.3')

+ 28
- 13
neutron/tests/unit/plugins/ml2/test_plugin.py View File

@@ -738,9 +738,14 @@ class TestMl2PortsV2(test_plugin.TestPortsV2, Ml2PluginV2TestCase):
738 738
         plugin = directory.get_plugin()
739 739
         ups = mock.patch.object(plugin, 'update_port_status').start()
740 740
         port_id = 'fake_port_id'
741
-        binding = mock.Mock(vif_type=portbindings.VIF_TYPE_OVS)
742
-        port = mock.Mock(
743
-            id=port_id, admin_state_up=False, port_binding=binding)
741
+
742
+        def getitem(key):
743
+            return constants.ACTIVE
744
+
745
+        binding = mock.MagicMock(vif_type=portbindings.VIF_TYPE_OVS)
746
+        binding.__getitem__.side_effect = getitem
747
+        port = mock.MagicMock(
748
+            id=port_id, admin_state_up=False, port_binding=[binding])
744 749
         with mock.patch('neutron.plugins.ml2.plugin.db.get_port',
745 750
                         return_value=port):
746 751
             plugin._port_provisioned('port', 'evt', 'trigger',
@@ -1801,8 +1806,10 @@ class TestMl2PortBinding(Ml2PluginV2TestCase,
1801 1806
         # create a port and delete it so we have an expired mechanism context
1802 1807
         with self.port() as port:
1803 1808
             plugin = directory.get_plugin()
1804
-            binding = plugin._get_port(self.context,
1805
-                                       port['port']['id']).port_binding
1809
+            binding = utils.get_port_binding_by_status_and_host(
1810
+                plugin._get_port(self.context,
1811
+                                 port['port']['id']).port_binding,
1812
+                constants.ACTIVE)
1806 1813
             binding['host'] = 'test'
1807 1814
             mech_context = driver_context.PortContext(
1808 1815
                 plugin, self.context, port['port'],
@@ -1822,8 +1829,10 @@ class TestMl2PortBinding(Ml2PluginV2TestCase,
1822 1829
     def _create_port_and_bound_context(self, port_vif_type, bound_vif_type):
1823 1830
         with self.port() as port:
1824 1831
             plugin = directory.get_plugin()
1825
-            binding = plugin._get_port(
1826
-                self.context, port['port']['id']).port_binding
1832
+            binding = utils.get_port_binding_by_status_and_host(
1833
+                plugin._get_port(self.context,
1834
+                                 port['port']['id']).port_binding,
1835
+                constants.ACTIVE)
1827 1836
             binding['host'] = 'fake_host'
1828 1837
             binding['vif_type'] = port_vif_type
1829 1838
             # Generates port context to be used before the bind.
@@ -1937,8 +1946,10 @@ class TestMl2PortBinding(Ml2PluginV2TestCase,
1937 1946
     def test_update_port_binding_host_id_none(self):
1938 1947
         with self.port() as port:
1939 1948
             plugin = directory.get_plugin()
1940
-            binding = plugin._get_port(
1941
-                self.context, port['port']['id']).port_binding
1949
+            binding = utils.get_port_binding_by_status_and_host(
1950
+                plugin._get_port(self.context,
1951
+                                 port['port']['id']).port_binding,
1952
+                constants.ACTIVE)
1942 1953
             with self.context.session.begin(subtransactions=True):
1943 1954
                 binding.host = 'test'
1944 1955
             mech_context = driver_context.PortContext(
@@ -1957,8 +1968,10 @@ class TestMl2PortBinding(Ml2PluginV2TestCase,
1957 1968
     def test_update_port_binding_host_id_not_changed(self):
1958 1969
         with self.port() as port:
1959 1970
             plugin = directory.get_plugin()
1960
-            binding = plugin._get_port(
1961
-                self.context, port['port']['id']).port_binding
1971
+            binding = utils.get_port_binding_by_status_and_host(
1972
+                plugin._get_port(self.context,
1973
+                                 port['port']['id']).port_binding,
1974
+                constants.ACTIVE)
1962 1975
             binding['host'] = 'test'
1963 1976
             mech_context = driver_context.PortContext(
1964 1977
                 plugin, self.context, port['port'],
@@ -2944,8 +2957,10 @@ class TestML2Segments(Ml2PluginV2TestCase):
2944 2957
             # add writer here to make sure that the following operations are
2945 2958
             # performed in the same session
2946 2959
             with db_api.context_manager.writer.using(self.context):
2947
-                binding = plugin._get_port(
2948
-                    self.context, port['port']['id']).port_binding
2960
+                binding = utils.get_port_binding_by_status_and_host(
2961
+                    plugin._get_port(self.context,
2962
+                                     port['port']['id']).port_binding,
2963
+                    constants.ACTIVE)
2949 2964
                 binding['host'] = 'host-ovs-no_filter'
2950 2965
                 mech_context = driver_context.PortContext(
2951 2966
                     plugin, self.context, port['port'],

+ 313
- 0
neutron/tests/unit/plugins/ml2/test_port_binding.py View File

@@ -15,16 +15,23 @@
15 15
 
16 16
 import mock
17 17
 from neutron_lib.api.definitions import portbindings
18
+from neutron_lib.api.definitions import portbindings_extended as pbe_ext
18 19
 from neutron_lib import constants as const
19 20
 from neutron_lib import context
20 21
 from neutron_lib.plugins import directory
21 22
 from oslo_config import cfg
22 23
 from oslo_serialization import jsonutils
24
+import webob.exc
23 25
 
26
+from neutron.common import exceptions
27
+from neutron.common import utils
28
+from neutron.conf.plugins.ml2 import config
24 29
 from neutron.conf.plugins.ml2.drivers import driver_type
25 30
 from neutron.plugins.ml2 import driver_context
26 31
 from neutron.plugins.ml2 import models as ml2_models
32
+from neutron.plugins.ml2 import plugin as ml2_plugin
27 33
 from neutron.tests.unit.db import test_db_base_plugin_v2 as test_plugin
34
+from neutron.tests.unit.plugins.ml2.drivers import mechanism_test
28 35
 
29 36
 
30 37
 class PortBindingTestCase(test_plugin.NeutronDbPluginV2TestCase):
@@ -327,3 +334,309 @@ class PortBindingTestCase(test_plugin.NeutronDbPluginV2TestCase):
327 334
             # Get port and verify status is still DOWN.
328 335
             port = self._show('ports', port_id)
329 336
             self.assertEqual('DOWN', port['port']['status'])
337
+
338
+
339
+class ExtendedPortBindingTestCase(test_plugin.NeutronDbPluginV2TestCase):
340
+
341
+    host = 'host-ovs-no_filter'
342
+
343
+    def setUp(self):
344
+        # Enable the test mechanism driver to ensure that
345
+        # we can successfully call through to all mechanism
346
+        # driver apis.
347
+        config.register_ml2_plugin_opts()
348
+        cfg.CONF.set_override('mechanism_drivers',
349
+                              ['logger', 'test'],
350
+                              'ml2')
351
+
352
+        driver_type.register_ml2_drivers_vlan_opts()
353
+        cfg.CONF.set_override('network_vlan_ranges',
354
+                              ['physnet1:1000:1099'],
355
+                              group='ml2_type_vlan')
356
+        super(ExtendedPortBindingTestCase, self).setUp('ml2')
357
+        self.port_create_status = 'DOWN'
358
+        self.plugin = directory.get_plugin()
359
+        self.plugin.start_rpc_listeners()
360
+
361
+    def _create_port_binding(self, fmt, port_id, host, tenant_id=None,
362
+                             **kwargs):
363
+        tenant_id = tenant_id or self._tenant_id
364
+        data = {'binding': {'host': host, 'tenant_id': tenant_id}}
365
+        if kwargs:
366
+            data['binding'].update(kwargs)
367
+        binding_resource = 'ports/%s/bindings' % port_id
368
+        binding_req = self.new_create_request(binding_resource, data, fmt)
369
+        return binding_req.get_response(self.api)
370
+
371
+    def _make_port_binding(self, fmt, port_id, host, **kwargs):
372
+        res = self._create_port_binding(fmt, port_id, host, **kwargs)
373
+        if res.status_int >= webob.exc.HTTPClientError.code:
374
+            raise webob.exc.HTTPClientError(code=res.status_int)
375
+        return self.deserialize(fmt, res)
376
+
377
+    def _update_port_binding(self, fmt, port_id, host, **kwargs):
378
+        data = {'binding': kwargs}
379
+        binding_req = self.new_update_request('ports', data, port_id, fmt,
380
+                                              subresource='bindings',
381
+                                              sub_id=host)
382
+        return binding_req.get_response(self.api)
383
+
384
+    def _do_update_port_binding(self, fmt, port_id, host, **kwargs):
385
+        res = self._update_port_binding(fmt, port_id, host, **kwargs)
386
+        if res.status_int >= webob.exc.HTTPClientError.code:
387
+            raise webob.exc.HTTPClientError(code=res.status_int)
388
+        return self.deserialize(fmt, res)
389
+
390
+    def _activate_port_binding(self, port_id, host, raw_response=True):
391
+        response = self._req('PUT', 'ports', id=port_id,
392
+                             data={'port_id': port_id},
393
+                             subresource='bindings', sub_id=host,
394
+                             action='activate').get_response(self.api)
395
+        return self._check_code_and_serialize(response, raw_response)
396
+
397
+    def _check_code_and_serialize(self, response, raw_response):
398
+        if raw_response:
399
+            return response
400
+        if response.status_int >= webob.exc.HTTPClientError.code:
401
+            raise webob.exc.HTTPClientError(code=response.status_int)
402
+        return self.deserialize(self.fmt, response)
403
+
404
+    def _list_port_bindings(self, port_id, params=None, raw_response=True):
405
+        response = self._req(
406
+            'GET', 'ports', fmt=self.fmt, id=port_id, subresource='bindings',
407
+            params=params).get_response(self.api)
408
+        return self._check_code_and_serialize(response, raw_response)
409
+
410
+    def _show_port_binding(self, port_id, host, params=None,
411
+                           raw_response=True):
412
+        response = self._req(
413
+            'GET', 'ports', fmt=self.fmt, id=port_id, subresource='bindings',
414
+            sub_id=host, params=params).get_response(self.api)
415
+        return self._check_code_and_serialize(response, raw_response)
416
+
417
+    def _delete_port_binding(self, port_id, host):
418
+        response = self._req(
419
+            'DELETE', 'ports', fmt=self.fmt, id=port_id,
420
+            subresource='bindings', sub_id=host).get_response(self.api)
421
+        return response
422
+
423
+    def _create_port_and_binding(self, **kwargs):
424
+        device_owner = '%s%s' % (const.DEVICE_OWNER_COMPUTE_PREFIX, 'nova')
425
+        with self.port(device_owner=device_owner) as port:
426
+            port_id = port['port']['id']
427
+            binding = self._make_port_binding(self.fmt, port_id, self.host,
428
+                                              **kwargs)['binding']
429
+            self._assert_bound_port_binding(binding)
430
+            return port['port'], binding
431
+
432
+    def _assert_bound_port_binding(self, binding):
433
+        self.assertEqual(self.host, binding[pbe_ext.HOST])
434
+        self.assertEqual(portbindings.VIF_TYPE_OVS,
435
+                         binding[pbe_ext.VIF_TYPE])
436
+        self.assertEqual({'port_filter': False},
437
+                         binding[pbe_ext.VIF_DETAILS])
438
+
439
+    def _assert_unbound_port_binding(self, binding):
440
+        self.assertFalse(binding[pbe_ext.HOST])
441
+        self.assertEqual(portbindings.VIF_TYPE_UNBOUND,
442
+                         binding[pbe_ext.VIF_TYPE])
443
+        self.assertEqual({}, binding[pbe_ext.VIF_DETAILS])
444
+        self.assertEqual({}, binding[pbe_ext.PROFILE])
445
+
446
+    def test_create_port_binding(self):
447
+        profile = {'key1': 'value1'}
448
+        kwargs = {pbe_ext.PROFILE: profile}
449
+        port, binding = self._create_port_and_binding(**kwargs)
450
+        self._assert_bound_port_binding(binding)
451
+        self.assertEqual({"key1": "value1"}, binding[pbe_ext.PROFILE])
452
+
453
+    def test_create_duplicate_port_binding(self):
454
+        device_owner = '%s%s' % (const.DEVICE_OWNER_COMPUTE_PREFIX, 'nova')
455
+        host_arg = {portbindings.HOST_ID: self.host}
456
+        with self.port(device_owner=device_owner,
457
+                       arg_list=(portbindings.HOST_ID,),
458
+                       **host_arg) as port:
459
+            response = self._create_port_binding(self.fmt, port['port']['id'],
460
+                                                 self.host)
461
+            self.assertEqual(webob.exc.HTTPConflict.code,
462
+                             response.status_int)
463
+
464
+    def test_create_port_binding_failure(self):
465
+        device_owner = '%s%s' % (const.DEVICE_OWNER_COMPUTE_PREFIX, 'nova')
466
+        with self.port(device_owner=device_owner) as port:
467
+            port_id = port['port']['id']
468
+            response = self._create_port_binding(self.fmt, port_id,
469
+                                                 'host-fail')
470
+            self.assertEqual(webob.exc.HTTPInternalServerError.code,
471
+                             response.status_int)
472
+            self.assertTrue(exceptions.PortBindingError.__name__ in
473
+                            response.text)
474
+
475
+    def test_create_port_binding_for_non_compute_owner(self):
476
+        with self.port() as port:
477
+            port_id = port['port']['id']
478
+            response = self._create_port_binding(self.fmt, port_id,
479
+                                                 'host-ovs-no_filter')
480
+            self.assertEqual(webob.exc.HTTPBadRequest.code,
481
+                             response.status_int)
482
+
483
+    def test_update_port_binding(self):
484
+        port, binding = self._create_port_and_binding()
485
+        profile = {'key1': 'value1'}
486
+        kwargs = {pbe_ext.PROFILE: profile}
487
+        binding = self._do_update_port_binding(self.fmt, port['id'], self.host,
488
+                                               **kwargs)['binding']
489
+        self._assert_bound_port_binding(binding)
490
+        self.assertEqual({"key1": "value1"}, binding[pbe_ext.PROFILE])
491
+
492
+    def test_update_non_existing_binding(self):
493
+        device_owner = '%s%s' % (const.DEVICE_OWNER_COMPUTE_PREFIX, 'nova')
494
+        with self.port(device_owner=device_owner) as port:
495
+            port_id = port['port']['id']
496
+            profile = {'key1': 'value1'}
497
+            kwargs = {pbe_ext.PROFILE: profile}
498
+            response = self._update_port_binding(self.fmt, port_id, 'a_host',
499
+                                                 **kwargs)
500
+            self.assertEqual(webob.exc.HTTPNotFound.code, response.status_int)
501
+
502
+    def test_update_port_binding_for_non_compute_owner(self):
503
+        with self.port() as port:
504
+            port_id = port['port']['id']
505
+            profile = {'key1': 'value1'}
506
+            kwargs = {pbe_ext.PROFILE: profile}
507
+            response = self._update_port_binding(self.fmt, port_id, 'a_host',
508
+                                                 **kwargs)
509
+            self.assertEqual(webob.exc.HTTPBadRequest.code,
510
+                             response.status_int)
511
+
512
+    def test_update_port_binding_failure(self):
513
+        class FakeBinding(object):
514
+            vif_type = portbindings.VIF_TYPE_BINDING_FAILED
515
+
516
+        class FakePortContext(object):
517
+            _binding = FakeBinding()
518
+
519
+        port, binding = self._create_port_and_binding()
520
+        profile = {'key1': 'value1'}
521
+        kwargs = {pbe_ext.PROFILE: profile}
522
+        with mock.patch.object(
523
+                self.plugin, '_bind_port_if_needed',
524
+                return_value=FakePortContext()):
525
+            response = self._update_port_binding(self.fmt, port['id'],
526
+                                                 self.host, **kwargs)
527
+            self.assertEqual(webob.exc.HTTPInternalServerError.code,
528
+                             response.status_int)
529
+            self.assertTrue(exceptions.PortBindingError.__name__ in
530
+                            response.text)
531
+
532
+    def test_activate_port_binding(self):
533
+        port, new_binding = self._create_port_and_binding()
534
+        with mock.patch.object(mechanism_test.TestMechanismDriver,
535
+                '_check_port_context'):
536
+            active_binding = self._activate_port_binding(
537
+                port['id'], self.host, raw_response=False)
538
+        self._assert_bound_port_binding(active_binding)
539
+        updated_port = self._show('ports', port['id'])['port']
540
+        self.assertEqual(new_binding[pbe_ext.HOST],
541
+            updated_port[portbindings.HOST_ID])
542
+        self.assertEqual(new_binding[pbe_ext.PROFILE],
543
+                updated_port[portbindings.PROFILE])
544
+        self.assertEqual(new_binding[pbe_ext.VNIC_TYPE],
545
+                updated_port[portbindings.VNIC_TYPE])
546
+        self.assertEqual(new_binding[pbe_ext.VIF_TYPE],
547
+                updated_port[portbindings.VIF_TYPE])
548
+        self.assertEqual(new_binding[pbe_ext.VIF_DETAILS],
549
+                updated_port[portbindings.VIF_DETAILS])
550
+        retrieved_bindings = self._list_port_bindings(
551
+            port['id'], raw_response=False)['bindings']
552
+        retrieved_active_binding = utils.get_port_binding_by_status_and_host(
553
+            retrieved_bindings, const.ACTIVE)
554
+        self._assert_bound_port_binding(retrieved_active_binding)
555
+        retrieved_inactive_binding = utils.get_port_binding_by_status_and_host(
556
+            retrieved_bindings, const.INACTIVE)
557
+        self._assert_unbound_port_binding(retrieved_inactive_binding)
558
+
559
+    def test_activate_port_binding_for_non_compute_owner(self):
560
+        port, new_binding = self._create_port_and_binding()
561
+        data = {'port': {'device_owner': ''}}
562
+        self.new_update_request('ports', data, port['id'],
563
+                                self.fmt).get_response(self.api)
564
+        response = self._activate_port_binding(port['id'], self.host)
565
+        self.assertEqual(webob.exc.HTTPBadRequest.code,
566
+                         response.status_int)
567
+
568
+    def test_activate_port_binding_already_active(self):
569
+        port, new_binding = self._create_port_and_binding()
570
+        with mock.patch.object(mechanism_test.TestMechanismDriver,
571
+                '_check_port_context'):
572
+            self._activate_port_binding(port['id'], self.host)
573
+        response = self._activate_port_binding(port['id'], self.host)
574
+        self.assertEqual(webob.exc.HTTPConflict.code,
575
+                         response.status_int)
576
+
577
+    def test_activate_port_binding_failure(self):
578
+        port, new_binding = self._create_port_and_binding()
579
+        with mock.patch.object(self.plugin, '_commit_port_binding',
580
+                               return_value=(None, None, True,)) as p_mock:
581
+            response = self._activate_port_binding(port['id'], self.host)
582
+            self.assertEqual(webob.exc.HTTPInternalServerError.code,
583
+                             response.status_int)
584
+            self.assertTrue(exceptions.PortBindingError.__name__ in
585
+                            response.text)
586
+            self.assertEqual(ml2_plugin.MAX_BIND_TRIES, p_mock.call_count)
587
+
588
+    def test_activate_port_binding_non_existing_binding(self):
589
+        port, new_binding = self._create_port_and_binding()
590
+        response = self._activate_port_binding(port['id'], 'other-host')
591
+        self.assertEqual(webob.exc.HTTPNotFound.code, response.status_int)
592
+
593
+    def test_list_port_bindings(self):
594
+        port, new_binding = self._create_port_and_binding()
595
+        retrieved_bindings = self._list_port_bindings(
596
+            port['id'], raw_response=False)['bindings']
597
+        self.assertEqual(2, len(retrieved_bindings))
598
+        status = const.ACTIVE
599
+        self._assert_unbound_port_binding(
600
+            utils.get_port_binding_by_status_and_host(retrieved_bindings,
601
+                                                      status))
602
+        status = const.INACTIVE
603
+        self._assert_bound_port_binding(
604
+            utils.get_port_binding_by_status_and_host(retrieved_bindings,
605
+                                                      status, host=self.host))
606
+
607
+    def test_list_port_bindings_with_query_parameters(self):
608
+        port, new_binding = self._create_port_and_binding()
609
+        params = '%s=%s' % (pbe_ext.STATUS, const.INACTIVE)
610
+        retrieved_bindings = self._list_port_bindings(
611
+            port['id'], params=params, raw_response=False)['bindings']
612
+        self.assertEqual(1, len(retrieved_bindings))
613
+        self._assert_bound_port_binding(retrieved_bindings[0])
614
+
615
+    def test_show_port_binding(self):
616
+        port, new_binding = self._create_port_and_binding()
617
+        retrieved_binding = self._show_port_binding(
618
+            port['id'], self.host, raw_response=False)['binding']
619
+        self._assert_bound_port_binding(retrieved_binding)
620
+
621
+    def test_show_port_binding_with_fields(self):
622
+        port, new_binding = self._create_port_and_binding()
623
+        fields = 'fields=%s' % pbe_ext.HOST
624
+        retrieved_binding = self._show_port_binding(
625
+            port['id'], self.host, raw_response=False,
626
+            params=fields)['binding']
627
+        self.assertEqual(self.host, retrieved_binding[pbe_ext.HOST])
628
+        for key in (pbe_ext.STATUS, pbe_ext.PROFILE, pbe_ext.VNIC_TYPE,
629
+                    pbe_ext.VIF_TYPE, pbe_ext.VIF_DETAILS,):
630
+            self.assertNotIn(key, retrieved_binding)
631
+
632
+    def test_delete_port_binding(self):
633
+        port, new_binding = self._create_port_and_binding()
634
+        response = self._delete_port_binding(port['id'], self.host)
635
+        self.assertEqual(webob.exc.HTTPNoContent.code, response.status_int)
636
+        response = self._show_port_binding(port['id'], self.host)
637
+        self.assertEqual(webob.exc.HTTPNotFound.code, response.status_int)
638
+
639
+    def test_delete_non_existing_port_binding(self):
640
+        port, new_binding = self._create_port_and_binding()
641
+        response = self._delete_port_binding(port['id'], 'other-host')
642
+        self.assertEqual(webob.exc.HTTPNotFound.code, response.status_int)

+ 1
- 1
neutron/tests/unit/services/logapi/common/test_validators.py View File

@@ -105,7 +105,7 @@ class TestLogDriversLoggingTypeValidations(drv_mgr.TestLogDriversManagerBase):
105 105
         port_binding = ports.PortBinding(
106 106
             self.ctxt, port_id=port_id, vif_type=vif_type, vnic_type=vnic_type)
107 107
         return ports.Port(
108
-            self.ctxt, id=uuidutils.generate_uuid(), binding=port_binding)
108
+            self.ctxt, id=uuidutils.generate_uuid(), binding=[port_binding])
109 109
 
110 110
     def _test_validate_log_type_for_port(self, port, expected_result):
111 111
         driver_manager = self._create_manager_with_drivers({

+ 1
- 1
neutron/tests/unit/services/qos/drivers/test_manager.py View File

@@ -86,7 +86,7 @@ class TestQoSDriversRulesValidations(TestQosDriversManagerBase):
86 86
         port_binding = ports_object.PortBinding(
87 87
             self.ctxt, port_id=port_id, vif_type=vif_type, vnic_type=vnic_type)
88 88
         return ports_object.Port(
89
-            self.ctxt, id=uuidutils.generate_uuid(), binding=port_binding)
89
+            self.ctxt, id=uuidutils.generate_uuid(), binding=[port_binding])
90 90
 
91 91
     def _test_validate_rule_for_port(self, port, expected_result):
92 92
         driver_manager = self._create_manager_with_drivers({

Loading…
Cancel
Save