From c5356cb3d82e9ac4bbfb998994bf73c880bf769f Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 23 Mar 2018 17:44:59 -0400 Subject: [PATCH] Add "activate_port_binding" neutron API method This method will be needed to activate the inactive port binding for the destination host during post-live-migration. This is based on the neutron API spec: https://specs.openstack.org/openstack/neutron-specs/specs/backlog/pike/portbinding_information_for_nova.html#activating-an-inactive-binding And neutron ML2 plugin API code: https://review.openstack.org/414251/ Part of blueprint neutron-new-port-binding-api Change-Id: Ic9fe5ebd3433073b51e95b62867d5c1cd5f2cc8f --- nova/exception.py | 5 +++ nova/network/neutronv2/api.py | 38 +++++++++++++++++++++++ nova/tests/unit/network/test_neutronv2.py | 37 ++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/nova/exception.py b/nova/exception.py index 1204fd4c8c..4f072d81d0 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -880,6 +880,11 @@ class PortBindingDeletionFailed(NovaException): "%(host)s.") +class PortBindingActivationFailed(NovaException): + msg_fmt = _("Failed to activate binding for port %(port_id)s and host " + "%(host)s.") + + class PortUpdateFailed(Invalid): msg_fmt = _("Port update failed for port %(port_id)s: %(reason)s") diff --git a/nova/network/neutronv2/api.py b/nova/network/neutronv2/api.py index e5f55becbe..fa845f8aea 100644 --- a/nova/network/neutronv2/api.py +++ b/nova/network/neutronv2/api.py @@ -1284,6 +1284,44 @@ class API(base_api.NetworkAPI): raise exception.PortBindingDeletionFailed( port_id=port_id, host=host) + def activate_port_binding(self, context, port_id, host): + """Activates an inactive port binding. + + If there are two port bindings to different hosts, activating the + inactive binding atomically changes the other binding to inactive. + + :param context: The request context for the operation. + :param port_id: The ID of the port with an inactive binding on the + host. + :param host: The host on which the inactive port binding should be + activated. + :raises: nova.exception.PortBindingActivationFailed if a non-409 error + response is received from neutron. + """ + client = _get_ksa_client(context, admin=True) + # This is a bit weird in that we don't PUT and update the status + # to ACTIVE, it's more like a POST action method in the compute API. + resp = client.put( + '/v2.0/ports/%s/bindings/%s/activate' % (port_id, host), + raise_exc=False) + if resp: + LOG.debug('Activated binding for port %s and host %s.', + port_id, host) + else: + # A 409 means the port binding is already active, which shouldn't + # happen if the caller is doing things in the correct order. + if resp.status_code == 409: + LOG.warning('Binding for port %s and host %s is already ' + 'active.', port_id, host) + else: + # Log the details, raise an exception. + LOG.error('Unexpected error trying to activate binding ' + 'for port %s and host %s. Code: %s. ' + 'Error: %s', port_id, host, resp.status_code, + resp.text) + raise exception.PortBindingActivationFailed( + port_id=port_id, host=host) + def _get_pci_device_profile(self, pci_dev): dev_spec = self.pci_whitelist.get_devspec(pci_dev) if dev_spec: diff --git a/nova/tests/unit/network/test_neutronv2.py b/nova/tests/unit/network/test_neutronv2.py index 16fa258fc1..c6aca3bf10 100644 --- a/nova/tests/unit/network/test_neutronv2.py +++ b/nova/tests/unit/network/test_neutronv2.py @@ -5464,6 +5464,43 @@ class TestPortBindingWithMock(test.NoDBTestCase): else: self.api.delete_port_binding(ctxt, port_id, 'fake-host') + @mock.patch('nova.network.neutronv2.api._get_ksa_client') + def test_activate_port_binding(self, mock_client): + """Tests the happy path of activating an inactive port binding.""" + ctxt = context.get_context() + resp = fake_req.FakeResponse(200) + mock_client.return_value.put.return_value = resp + self.api.activate_port_binding(ctxt, uuids.port_id, 'fake-host') + mock_client.return_value.put.assert_called_once_with( + '/v2.0/ports/%s/bindings/fake-host/activate' % uuids.port_id, + raise_exc=False) + + @mock.patch('nova.network.neutronv2.api._get_ksa_client') + @mock.patch('nova.network.neutronv2.api.LOG.warning') + def test_activate_port_binding_already_active( + self, mock_log_warning, mock_client): + """Tests the 409 case of activating an already active port binding.""" + ctxt = context.get_context() + mock_client.return_value.put.return_value = fake_req.FakeResponse(409) + self.api.activate_port_binding(ctxt, uuids.port_id, 'fake-host') + mock_client.return_value.put.assert_called_once_with( + '/v2.0/ports/%s/bindings/fake-host/activate' % uuids.port_id, + raise_exc=False) + self.assertEqual(1, mock_log_warning.call_count) + self.assertIn('is already active', mock_log_warning.call_args[0][0]) + + @mock.patch('nova.network.neutronv2.api._get_ksa_client') + def test_activate_port_binding_fails(self, mock_client): + """Tests the unknown error case of binding activation.""" + ctxt = context.get_context() + mock_client.return_value.put.return_value = fake_req.FakeResponse(500) + self.assertRaises(exception.PortBindingActivationFailed, + self.api.activate_port_binding, + ctxt, uuids.port_id, 'fake-host') + mock_client.return_value.put.assert_called_once_with( + '/v2.0/ports/%s/bindings/fake-host/activate' % uuids.port_id, + raise_exc=False) + class TestAllocateForInstance(test.NoDBTestCase): def setUp(self):