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):