Use neutronclient's port binding APIs

Take advantage of the neutronclient bindings for the port binding APIs
added in neutronclient 7.1.0 to avoid having to vendor this stuff
ourselves.

Change-Id: Icc284203fb53658abe304f24a62705217f90b22b
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane 2020-03-06 10:43:09 +00:00
parent 9cdecc81fb
commit 982e2ee02d
7 changed files with 277 additions and 449 deletions

View File

@ -122,7 +122,7 @@ python-glanceclient==2.8.0
python-ironicclient==3.0.0 python-ironicclient==3.0.0
python-keystoneclient==3.15.0 python-keystoneclient==3.15.0
python-mimeparse==1.6.0 python-mimeparse==1.6.0
python-neutronclient==6.7.0 python-neutronclient==7.1.0
python-subunit==1.4.0 python-subunit==1.4.0
pytz==2018.3 pytz==2018.3
PyYAML==5.1 PyYAML==5.1

View File

@ -816,13 +816,13 @@ class PortBindingFailed(Invalid):
class PortBindingDeletionFailed(NovaException): class PortBindingDeletionFailed(NovaException):
msg_fmt = _("Failed to delete binding for port %(port_id)s and host " msg_fmt = _("Failed to delete binding for port(s) %(port_id)s on host "
"%(host)s.") "%(host)s; please check neutron logs for more information")
class PortBindingActivationFailed(NovaException): class PortBindingActivationFailed(NovaException):
msg_fmt = _("Failed to activate binding for port %(port_id)s and host " msg_fmt = _("Failed to activate binding for port %(port_id)s on host "
"%(host)s.") "%(host)s; please check neutron logs for more information")
class PortUpdateFailed(Invalid): class PortUpdateFailed(Invalid):

View File

@ -255,26 +255,6 @@ def get_client(context, admin=False):
admin=admin or context.is_admin) admin=admin or context.is_admin)
def _get_ksa_client(context, admin=False):
"""Returns a keystoneauth Adapter
This method should only be used if python-neutronclient does not yet
provide the necessary API bindings.
:param context: User request context
:param admin: If True, uses the configured credentials, else uses the
existing auth_token in the context (the user token).
:returns: keystoneauth1 Adapter object
"""
auth_plugin = _get_auth_plugin(context, admin=admin)
session = _get_session()
client = utils.get_ksa_adapter(
'network', ksa_auth=auth_plugin, ksa_session=session)
client.additional_headers = {'accept': 'application/json'}
client.connect_retries = CONF.neutron.http_retries
return client
def _is_not_duplicate(item, items, items_list_name, instance): def _is_not_duplicate(item, items, items_list_name, instance):
present = item in items present = item in items
@ -417,25 +397,29 @@ class API:
:param host: host from which to delete port bindings :param host: host from which to delete port bindings
:raises: PortBindingDeletionFailed if port binding deletion fails. :raises: PortBindingDeletionFailed if port binding deletion fails.
""" """
client = get_client(context, admin=True)
failed_port_ids = [] failed_port_ids = []
for port in ports: for port in ports:
# This call is safe in that 404s for non-existing # This call is safe in that 404s for non-existing
# bindings are ignored. # bindings are ignored.
try: try:
self.delete_port_binding( client.delete_port_binding(port['id'], host)
context, port['id'], host) except neutron_client_exc.NeutronClientException as exc:
except exception.PortBindingDeletionFailed: # We can safely ignore 404s since we're trying to delete
# delete_port_binding will log an error for each # the thing that wasn't found anyway, but for everything else
# failure but since we're iterating a list we want # we should log an error
# to keep track of all failures to build a generic if exc.status_code == 404:
# exception to raise continue
failed_port_ids.append(port['id']) failed_port_ids.append(port['id'])
LOG.exception(
"Failed to delete binding for port %(port_id)s on host "
"%(host)s", {'port_id': port['id'], 'host': host})
if failed_port_ids: if failed_port_ids:
msg = (_("Failed to delete binding for port(s) " raise exception.PortBindingDeletionFailed(
"%(port_ids)s and host %(host)s.") % port_id=','.join(failed_port_ids), host=host)
{'port_ids': ','.join(failed_port_ids),
'host': host})
raise exception.PortBindingDeletionFailed(msg)
def _get_available_networks(self, context, project_id, def _get_available_networks(self, context, project_id,
net_ids=None, neutron=None, net_ids=None, neutron=None,
@ -1329,9 +1313,9 @@ class API:
LOG.debug('Instance does not have any ports.', instance=instance) LOG.debug('Instance does not have any ports.', instance=instance)
return {} return {}
client = _get_ksa_client(context, admin=True) client = get_client(context, admin=True)
bindings_by_port_id = {} bindings_by_port_id: ty.Dict[str, ty.Any] = {}
for vif in network_info: for vif in network_info:
# Now bind each port to the destination host and keep track of each # Now bind each port to the destination host and keep track of each
# port that is bound to the resulting binding so we can rollback in # port that is bound to the resulting binding so we can rollback in
@ -1348,46 +1332,28 @@ class API:
else: else:
binding['profile'] = port_profiles[port_id] binding['profile'] = port_profiles[port_id]
data = dict(binding=binding) data = {'binding': binding}
resp = self._create_port_binding(context, client, port_id, data) try:
if resp: binding = client.create_port_binding(port_id, data)['binding']
bindings_by_port_id[port_id] = resp.json()['binding'] except neutron_client_exc.NeutronClientException:
else:
# Something failed, so log the error and rollback any # Something failed, so log the error and rollback any
# successful bindings. # successful bindings.
LOG.error('Binding failed for port %s and host %s. ' LOG.error('Binding failed for port %s and host %s.',
'Error: (%s %s)', port_id, host, instance=instance, exc_info=True)
port_id, host, resp.status_code, resp.text,
instance=instance)
for rollback_port_id in bindings_by_port_id: for rollback_port_id in bindings_by_port_id:
try: try:
self.delete_port_binding( client.delete_port_binding(rollback_port_id, host)
context, rollback_port_id, host) except neutron_client_exc.NeutronClientException as exc:
except exception.PortBindingDeletionFailed: if exc.status_code != 404:
LOG.warning('Failed to remove binding for port %s on ' LOG.warning('Failed to remove binding for port %s '
'host %s.', rollback_port_id, host, 'on host %s.', rollback_port_id, host,
instance=instance) instance=instance)
raise exception.PortBindingFailed(port_id=port_id) raise exception.PortBindingFailed(port_id=port_id)
bindings_by_port_id[port_id] = binding
return bindings_by_port_id return bindings_by_port_id
@staticmethod
def _create_port_binding(context, client, port_id, data):
"""Creates a port binding with the specified data.
:param context: The request context for the operation.
:param client: keystoneauth1.adapter.Adapter
:param port_id: The ID of the port on which to create the binding.
:param data: dict of port binding data (requires at least the host),
for example::
{'binding': {'host': 'dest.host.com'}}
:return: requests.Response object
"""
return client.post(
'/v2.0/ports/%s/bindings' % port_id, json=data, raise_exc=False,
global_request_id=context.global_id)
def delete_port_binding(self, context, port_id, host): def delete_port_binding(self, context, port_id, host):
"""Delete the port binding for the given port ID and host """Delete the port binding for the given port ID and host
@ -1400,104 +1366,19 @@ class API:
:raises: nova.exception.PortBindingDeletionFailed if a non-404 error :raises: nova.exception.PortBindingDeletionFailed if a non-404 error
response is received from neutron. response is received from neutron.
""" """
client = _get_ksa_client(context, admin=True) client = get_client(context, admin=True)
resp = self._delete_port_binding(context, client, port_id, host) try:
if resp: client.delete_port_binding(port_id, host)
LOG.debug('Deleted binding for port %s and host %s.', except neutron_client_exc.NeutronClientException as exc:
port_id, host)
else:
# We can safely ignore 404s since we're trying to delete # We can safely ignore 404s since we're trying to delete
# the thing that wasn't found anyway. # the thing that wasn't found anyway.
if resp.status_code != 404: if exc.status_code != 404:
# Log the details, raise an exception. LOG.error(
LOG.error('Unexpected error trying to delete binding ' 'Unexpected error trying to delete binding for port %s '
'for port %s and host %s. Code: %s. ' 'and host %s.', port_id, host, exc_info=True)
'Error: %s', port_id, host,
resp.status_code, resp.text)
raise exception.PortBindingDeletionFailed( raise exception.PortBindingDeletionFailed(
port_id=port_id, host=host) port_id=port_id, host=host)
@staticmethod
def _delete_port_binding(context, client, port_id, host):
"""Deletes the binding for the given host on the given port.
:param context: The request context for the operation.
:param client: keystoneauth1.adapter.Adapter
:param port_id: ID of the port from which to delete the binding
:param host: A string name of the host on which the port is bound
:return: requests.Response object
"""
return client.delete(
'/v2.0/ports/%s/bindings/%s' % (port_id, host), raise_exc=False,
global_request_id=context.global_id)
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 = self._activate_port_binding(context, client, port_id, host)
if resp:
LOG.debug('Activated binding for port %s and host %s.',
port_id, host)
# A 409 means the port binding is already active, which shouldn't
# happen if the caller is doing things in the correct order.
elif 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)
@staticmethod
def _activate_port_binding(context, client, port_id, host):
"""Activates an inactive port binding.
:param context: The request context for the operation.
:param client: keystoneauth1.adapter.Adapter
:param port_id: ID of the port to activate the binding on
:param host: A string name of the host identifying the binding to be
activated
:return: requests.Response object
"""
return client.put(
'/v2.0/ports/%s/bindings/%s/activate' % (port_id, host),
raise_exc=False,
global_request_id=context.global_id)
@staticmethod
def _get_port_binding(context, client, port_id, host):
"""Returns a port binding of a given port on a given host
:param context: The request context for the operation.
:param client: keystoneauth1.adapter.Adapter
:param port_id: ID of the port to get the binding
:param host: A string name of the host identifying the binding to be
returned
:return: requests.Response object
"""
return client.get(
'/v2.0/ports/%s/bindings/%s' % (port_id, host),
raise_exc=False,
global_request_id=context.global_id)
def _get_pci_device_profile(self, pci_dev): def _get_pci_device_profile(self, pci_dev):
dev_spec = self.pci_whitelist.get_devspec(pci_dev) dev_spec = self.pci_whitelist.get_devspec(pci_dev)
if dev_spec: if dev_spec:
@ -2865,43 +2746,73 @@ class API:
'updated later.', instance=instance) 'updated later.', instance=instance)
return return
client = _get_ksa_client(context, admin=True) client = get_client(context, admin=True)
dest_host = migration['dest_compute'] dest_host = migration['dest_compute']
for vif in instance.get_network_info(): for vif in instance.get_network_info():
# Not all compute migration flows use the port binding-extended # Not all compute migration flows use the port binding-extended
# API yet, so first check to see if there is a binding for the # API yet, so first check to see if there is a binding for the
# port and destination host. # port and destination host.
resp = self._get_port_binding( try:
context, client, vif['id'], dest_host) binding = client.show_port_binding(
if resp: vif['id'], dest_host
if resp.json()['binding']['status'] != 'ACTIVE': )['binding']
self.activate_port_binding(context, vif['id'], dest_host) except neutron_client_exc.NeutronClientException as exc:
# TODO(mriedem): Do we need to call if exc.status_code != 404:
# _clear_migration_port_profile? migrate_instance_finish # We don't raise an exception here because we assume that
# would normally take care of clearing the "migrating_to" # port bindings will be updated correctly when
# attribute on each port when updating the port's # migrate_instance_finish runs
# binding:host_id to point to the destination host. LOG.error(
else: 'Unexpected error trying to get binding info '
# We might be racing with another thread that's handling 'for port %s and destination host %s.',
# post-migrate operations and already activated the port vif['id'], dest_host, exc_info=True)
# binding for the destination host. continue
LOG.debug('Port %s binding to destination host %s is '
'already ACTIVE.', vif['id'], dest_host, # ...but if there is no port binding record for the destination
instance=instance) # host, we can safely assume none of the ports attached to the
elif resp.status_code == 404:
# If there is no port binding record for the destination host,
# we can safely assume none of the ports attached to the
# instance are using the binding-extended API in this flow and # instance are using the binding-extended API in this flow and
# exit early. # exit early.
return return
else:
# We don't raise an exception here because we assume that if binding['status'] == 'ACTIVE':
# port bindings will be updated correctly when # We might be racing with another thread that's handling
# migrate_instance_finish runs. # post-migrate operations and already activated the port
LOG.error('Unexpected error trying to get binding info ' # binding for the destination host.
'for port %s and destination host %s. Code: %i. ' LOG.debug(
'Error: %s', vif['id'], dest_host, resp.status_code, 'Port %s binding to destination host %s is already ACTIVE',
resp.text) vif['id'], dest_host, instance=instance)
continue
try:
# 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.
client.activate_port_binding(vif['id'], dest_host)
LOG.debug(
'Activated binding for port %s and host %s',
vif['id'], dest_host)
except neutron_client_exc.NeutronClientException as exc:
# A 409 means the port binding is already active, which
# shouldn't happen if the caller is doing things in the correct
# order.
if exc.status_code == 409:
LOG.warning(
'Binding for port %s and host %s is already active',
vif['id'], dest_host, exc_info=True)
continue
# Log the details, raise an exception.
LOG.error(
'Unexpected error trying to activate binding '
'for port %s and host %s.',
vif['id'], dest_host, exc_info=True)
raise exception.PortBindingActivationFailed(
port_id=vif['id'], host=dest_host)
# TODO(mriedem): Do we need to call
# _clear_migration_port_profile? migrate_instance_finish
# would normally take care of clearing the "migrating_to"
# attribute on each port when updating the port's
# binding:host_id to point to the destination host.
def migrate_instance_finish( def migrate_instance_finish(
self, context, instance, migration, provider_mappings): self, context, instance, migration, provider_mappings):

View File

@ -15,18 +15,14 @@ import copy
import random import random
import fixtures import fixtures
from keystoneauth1 import adapter as ksa_adap
import mock
from neutronclient.common import exceptions as neutron_client_exc from neutronclient.common import exceptions as neutron_client_exc
import os_resource_classes as orc import os_resource_classes as orc
from oslo_serialization import jsonutils
from oslo_utils import uuidutils from oslo_utils import uuidutils
from nova import exception from nova import exception
from nova.network import constants as neutron_constants from nova.network import constants as neutron_constants
from nova.network import model as network_model from nova.network import model as network_model
from nova.tests.fixtures import nova as nova_fixtures from nova.tests.fixtures import nova as nova_fixtures
from nova.tests.unit import fake_requests
class _FakeNeutronClient: class _FakeNeutronClient:
@ -665,25 +661,6 @@ class NeutronFixture(fixtures.Fixture):
lambda *args, **kwargs: network_model.NetworkInfo.hydrate( lambda *args, **kwargs: network_model.NetworkInfo.hydrate(
self.nw_info)) self.nw_info))
# Stub out port binding APIs which go through a KSA client Adapter
# rather than python-neutronclient.
self.test.stub_out(
'nova.network.neutron._get_ksa_client',
lambda *args, **kwargs: mock.Mock(
spec=ksa_adap.Adapter))
self.test.stub_out(
'nova.network.neutron.API._create_port_binding',
self.create_port_binding)
self.test.stub_out(
'nova.network.neutron.API._delete_port_binding',
self.delete_port_binding)
self.test.stub_out(
'nova.network.neutron.API._activate_port_binding',
self.activate_port_binding)
self.test.stub_out(
'nova.network.neutron.API._get_port_binding',
self.get_port_binding)
self.test.stub_out( self.test.stub_out(
'nova.network.neutron.get_client', self._get_client) 'nova.network.neutron.get_client', self._get_client)
@ -692,13 +669,12 @@ class NeutronFixture(fixtures.Fixture):
admin = admin or context.is_admin and not context.auth_token admin = admin or context.is_admin and not context.auth_token
return _FakeNeutronClient(self, admin) return _FakeNeutronClient(self, admin)
def create_port_binding(self, context, client, port_id, data): def create_port_binding(self, port_id, body):
if port_id not in self._ports: if port_id not in self._ports:
return fake_requests.FakeResponse( raise neutron_client_exc.NeutronClientException(status_code=404)
404, content='Port %s not found' % port_id)
port = self._ports[port_id] port = self._ports[port_id]
binding = copy.deepcopy(data['binding']) binding = copy.deepcopy(body['binding'])
# NOTE(stephenfin): We don't allow changing of backend # NOTE(stephenfin): We don't allow changing of backend
binding['vif_type'] = port['binding:vif_type'] binding['vif_type'] = port['binding:vif_type']
@ -713,61 +689,36 @@ class NeutronFixture(fixtures.Fixture):
self._port_bindings[port_id][binding['host']] = binding self._port_bindings[port_id][binding['host']] = binding
return fake_requests.FakeResponse( return {'binding': binding}
200, content=jsonutils.dumps({'binding': binding}),
)
def _get_failure_response_if_port_or_binding_not_exists( def _validate_port_binding(self, port_id, host_id):
self, port_id, host,
):
if port_id not in self._ports: if port_id not in self._ports:
return fake_requests.FakeResponse( raise neutron_client_exc.NeutronClientException(status_code=404)
404, content='Port %s not found' % port_id)
if host not in self._port_bindings[port_id]:
return fake_requests.FakeResponse(
404,
content='Binding for host %s for port %s not found'
% (host, port_id))
def delete_port_binding(self, context, client, port_id, host): if host_id not in self._port_bindings[port_id]:
failure = self._get_failure_response_if_port_or_binding_not_exists( raise neutron_client_exc.NeutronClientException(status_code=404)
port_id, host)
if failure is not None:
return failure
del self._port_bindings[port_id][host] def delete_port_binding(self, port_id, host_id):
self._validate_port_binding(port_id, host_id)
del self._port_bindings[port_id][host_id]
return fake_requests.FakeResponse(204) def _activate_port_binding(self, port_id, host_id):
def _activate_port_binding(self, port_id, host):
# It makes sure that only one binding is active for a port # It makes sure that only one binding is active for a port
for h, binding in self._port_bindings[port_id].items(): for host, binding in self._port_bindings[port_id].items():
if h == host: if host == host_id:
# NOTE(gibi): neutron returns 409 if this binding is already # NOTE(gibi): neutron returns 409 if this binding is already
# active but nova does not depend on this behaviour yet. # active but nova does not depend on this behaviour yet.
binding['status'] = 'ACTIVE' binding['status'] = 'ACTIVE'
else: else:
binding['status'] = 'INACTIVE' binding['status'] = 'INACTIVE'
def activate_port_binding(self, context, client, port_id, host): def activate_port_binding(self, port_id, host_id):
failure = self._get_failure_response_if_port_or_binding_not_exists( self._validate_port_binding(port_id, host_id)
port_id, host) self._activate_port_binding(port_id, host_id)
if failure is not None:
return failure
self._activate_port_binding(port_id, host) def show_port_binding(self, port_id, host_id):
self._validate_port_binding(port_id, host_id)
return fake_requests.FakeResponse(200) return {'binding': self._port_bindings[port_id][host_id]}
def get_port_binding(self, context, client, port_id, host):
failure = self._get_failure_response_if_port_or_binding_not_exists(
port_id, host)
if failure is not None:
return failure
binding = {"binding": self._port_bindings[port_id][host]}
return fake_requests.FakeResponse(
200, content=jsonutils.dumps(binding))
def _list_resource(self, resources, retrieve_all, **_params): def _list_resource(self, resources, retrieve_all, **_params):
# If 'fields' is passed we need to strip that out since it will mess # If 'fields' is passed we need to strip that out since it will mess

View File

@ -12,6 +12,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import collections import collections
import copy import copy
import datetime import datetime
@ -8383,10 +8384,11 @@ class CrossCellResizeWithQoSPort(PortResourceRequestBasedSchedulingTestBase):
server = self._create_server_with_ports_and_check_allocation( server = self._create_server_with_ports_and_check_allocation(
non_qos_normal_port, qos_normal_port, qos_sriov_port) non_qos_normal_port, qos_normal_port, qos_sriov_port)
orig_create_binding = neutronapi.API._create_port_binding orig_create_binding = self.neutron.create_port_binding
hosts = { hosts = {
'host1': self.compute1_rp_uuid, 'host2': self.compute2_rp_uuid} 'host1': self.compute1_rp_uuid, 'host2': self.compute2_rp_uuid,
}
# Add an extra check to our neutron fixture. This check makes sure that # Add an extra check to our neutron fixture. This check makes sure that
# the RP sent in the binding corresponds to host of the binding. In a # the RP sent in the binding corresponds to host of the binding. In a
@ -8394,21 +8396,23 @@ class CrossCellResizeWithQoSPort(PortResourceRequestBasedSchedulingTestBase):
# 1907522 showed we fail this check for cross cell migration with qos # 1907522 showed we fail this check for cross cell migration with qos
# ports in a real deployment. So to reproduce that bug we need to have # ports in a real deployment. So to reproduce that bug we need to have
# the same check in our test env too. # the same check in our test env too.
def spy_on_create_binding(context, client, port_id, data): def spy_on_create_binding(port_id, data):
host_rp_uuid = hosts[data['binding']['host']] host_rp_uuid = hosts[data['binding']['host']]
device_rp_uuid = data['binding']['profile'].get('allocation') device_rp_uuid = data['binding']['profile'].get('allocation')
if port_id == qos_normal_port['id']: if port_id == qos_normal_port['id']:
if device_rp_uuid != self.ovs_bridge_rp_per_host[host_rp_uuid]: if device_rp_uuid != self.ovs_bridge_rp_per_host[host_rp_uuid]:
raise exception.PortBindingFailed(port_id=port_id) raise exception.PortBindingFailed(port_id=port_id)
elif port_id == qos_sriov_port['id']: elif port_id == qos_sriov_port['id']:
if (device_rp_uuid not in if (
self.sriov_dev_rp_per_host[host_rp_uuid].values()): device_rp_uuid not in
self.sriov_dev_rp_per_host[host_rp_uuid].values()
):
raise exception.PortBindingFailed(port_id=port_id) raise exception.PortBindingFailed(port_id=port_id)
return orig_create_binding(context, client, port_id, data) return orig_create_binding(port_id, data)
with mock.patch( with mock.patch(
'nova.network.neutron.API._create_port_binding', 'nova.tests.fixtures.NeutronFixture.create_port_binding',
side_effect=spy_on_create_binding, autospec=True side_effect=spy_on_create_binding, autospec=True
): ):
# We expect the migration to fail as the only available target # We expect the migration to fail as the only available target
@ -8440,8 +8444,8 @@ class CrossCellResizeWithQoSPort(PortResourceRequestBasedSchedulingTestBase):
self._create_networking_rp_tree('host3', self.compute3_rp_uuid) self._create_networking_rp_tree('host3', self.compute3_rp_uuid)
with mock.patch( with mock.patch(
'nova.network.neutron.API._create_port_binding', 'nova.tests.fixtures.NeutronFixture.create_port_binding',
side_effect=spy_on_create_binding, autospec=True side_effect=spy_on_create_binding, autospec=True
): ):
server = self._migrate_server(server) server = self._migrate_server(server)
self.assertEqual('host3', server['OS-EXT-SRV-ATTR:host']) self.assertEqual('host3', server['OS-EXT-SRV-ATTR:host'])

View File

@ -49,7 +49,6 @@ from nova import policy
from nova import service_auth from nova import service_auth
from nova import test from nova import test
from nova.tests.unit import fake_instance from nova.tests.unit import fake_instance
from nova.tests.unit import fake_requests as fake_req
CONF = cfg.CONF CONF = cfg.CONF
@ -251,8 +250,6 @@ class TestNeutronClient(test.NoDBTestCase):
auth_token='token') auth_token='token')
cl = neutronapi.get_client(my_context) cl = neutronapi.get_client(my_context)
self.assertEqual(retries, cl.httpclient.connect_retries) self.assertEqual(retries, cl.httpclient.connect_retries)
kcl = neutronapi._get_ksa_client(my_context)
self.assertEqual(retries, kcl.connect_retries)
class TestAPIBase(test.TestCase): class TestAPIBase(test.TestCase):
@ -4894,24 +4891,23 @@ class TestAPI(TestAPIBase):
constants.BINDING_PROFILE: migrate_profile, constants.BINDING_PROFILE: migrate_profile,
constants.BINDING_HOST_ID: instance.host}]} constants.BINDING_HOST_ID: instance.host}]}
self.api.list_ports = mock.Mock(return_value=get_ports) self.api.list_ports = mock.Mock(return_value=get_ports)
update_port_mock = mock.Mock() mocked_client = get_client_mock.return_value
get_client_mock.return_value.update_port = update_port_mock
with mock.patch.object(self.api, 'delete_port_binding') as del_binding:
with mock.patch.object(self.api, 'supports_port_binding_extension',
return_value=True):
self.api.setup_networks_on_host(self.context,
instance,
host='new-host',
teardown=True)
update_port_mock.assert_called_once_with(
port_id, {'port': {
constants.BINDING_PROFILE: migrate_profile}})
del_binding.assert_called_once_with(
self.context, port_id, 'new-host')
@mock.patch.object(neutronapi, 'get_client', return_value=mock.Mock()) with mock.patch.object(self.api, 'supports_port_binding_extension',
return_value=True):
self.api.setup_networks_on_host(self.context,
instance,
host='new-host',
teardown=True)
mocked_client.update_port.assert_called_once_with(
port_id, {'port': {constants.BINDING_PROFILE: migrate_profile}})
mocked_client.delete_port_binding.assert_called_once_with(
port_id, 'new-host')
@mock.patch.object(neutronapi, 'get_client')
def test_update_port_profile_for_migration_teardown_true_with_profile_exc( def test_update_port_profile_for_migration_teardown_true_with_profile_exc(
self, get_client_mock): self, get_client_mock):
"""Tests that delete_port_binding raises PortBindingDeletionFailed """Tests that delete_port_binding raises PortBindingDeletionFailed
which is raised through to the caller. which is raised through to the caller.
""" """
@ -4930,25 +4926,27 @@ class TestAPI(TestAPIBase):
constants.BINDING_HOST_ID: instance.host}]} constants.BINDING_HOST_ID: instance.host}]}
self.api.list_ports = mock.Mock(return_value=get_ports) self.api.list_ports = mock.Mock(return_value=get_ports)
self.api._clear_migration_port_profile = mock.Mock() self.api._clear_migration_port_profile = mock.Mock()
with mock.patch.object( NeutronError = exceptions.NeutronClientException(status_code=500)
self.api, 'delete_port_binding', mocked_client = get_client_mock.return_value
side_effect=exception.PortBindingDeletionFailed( mocked_client.delete_port_binding.side_effect = NeutronError
port_id=uuids.port1, host='new-host')) as del_binding:
with mock.patch.object(self.api, 'supports_port_binding_extension', with mock.patch.object(self.api, 'supports_port_binding_extension',
return_value=True): return_value=True):
ex = self.assertRaises( ex = self.assertRaises(
exception.PortBindingDeletionFailed, exception.PortBindingDeletionFailed,
self.api.setup_networks_on_host, self.api.setup_networks_on_host,
self.context, instance, host='new-host', teardown=True) self.context, instance, host='new-host', teardown=True)
# Make sure both ports show up in the exception message. # Make sure both ports show up in the exception message.
self.assertIn(uuids.port1, str(ex)) self.assertIn(uuids.port1, str(ex))
self.assertIn(uuids.port2, str(ex)) self.assertIn(uuids.port2, str(ex))
self.api._clear_migration_port_profile.assert_called_once_with( self.api._clear_migration_port_profile.assert_called_once_with(
self.context, instance, get_client_mock.return_value, self.context, instance, get_client_mock.return_value,
get_ports['ports']) get_ports['ports'])
del_binding.assert_has_calls([ mocked_client.delete_port_binding.assert_has_calls([
mock.call(self.context, uuids.port1, 'new-host'), mock.call(uuids.port1, 'new-host'),
mock.call(self.context, uuids.port2, 'new-host')]) mock.call(uuids.port2, 'new-host'),
])
@mock.patch.object(neutronapi, 'get_client', return_value=mock.Mock()) @mock.patch.object(neutronapi, 'get_client', return_value=mock.Mock())
def test_update_port_profile_for_migration_teardown_true_no_profile( def test_update_port_profile_for_migration_teardown_true_no_profile(
@ -6101,7 +6099,7 @@ class TestAPI(TestAPIBase):
mock_get_inst.assert_called_once_with(ctxt, instance.uuid) mock_get_inst.assert_called_once_with(ctxt, instance.uuid)
mock_get_map.assert_called_once_with(ctxt, instance.uuid) mock_get_map.assert_called_once_with(ctxt, instance.uuid)
@mock.patch('nova.network.neutron._get_ksa_client', @mock.patch('nova.network.neutron.get_client',
new_callable=mock.NonCallableMock) # asserts not called new_callable=mock.NonCallableMock) # asserts not called
def test_migrate_instance_start_no_binding_ext(self, get_client_mock): def test_migrate_instance_start_no_binding_ext(self, get_client_mock):
"""Tests that migrate_instance_start exits early if neutron doesn't """Tests that migrate_instance_start exits early if neutron doesn't
@ -6112,63 +6110,65 @@ class TestAPI(TestAPIBase):
self.api.migrate_instance_start( self.api.migrate_instance_start(
self.context, mock.sentinel.instance, {}) self.context, mock.sentinel.instance, {})
@mock.patch('nova.network.neutron._get_ksa_client') @mock.patch('nova.network.neutron.get_client')
def test_migrate_instance_start_activate(self, get_client_mock): def test_migrate_instance_start_activate(self, get_client_mock):
"""Tests the happy path for migrate_instance_start where the binding """Tests the happy path for migrate_instance_start where the binding
for the port(s) attached to the instance are activated on the for the port(s) attached to the instance are activated on the
destination host. destination host.
""" """
binding = {'binding': {'status': 'INACTIVE'}} binding = {'binding': {'status': 'INACTIVE'}}
resp = fake_req.FakeResponse(200, content=jsonutils.dumps(binding)) mocked_client = get_client_mock.return_value
get_client_mock.return_value.get.return_value = resp mocked_client.show_port_binding.return_value = binding
# Just create a simple instance with a single port. # Just create a simple instance with a single port.
instance = objects.Instance(info_cache=objects.InstanceInfoCache( instance = objects.Instance(info_cache=objects.InstanceInfoCache(
network_info=model.NetworkInfo([model.VIF(uuids.port_id)]))) network_info=model.NetworkInfo([model.VIF(uuids.port_id)])))
migration = objects.Migration( migration = objects.Migration(
source_compute='source', dest_compute='dest') source_compute='source', dest_compute='dest')
with mock.patch.object(self.api, 'activate_port_binding') as activate:
with mock.patch.object(self.api, 'supports_port_binding_extension',
return_value=True):
self.api.migrate_instance_start(
self.context, instance, migration)
activate.assert_called_once_with(self.context, uuids.port_id, 'dest')
get_client_mock.return_value.get.assert_called_once_with(
'/v2.0/ports/%s/bindings/dest' % uuids.port_id, raise_exc=False,
global_request_id=self.context.global_id)
@mock.patch('nova.network.neutron._get_ksa_client') with mock.patch.object(self.api, 'supports_port_binding_extension',
return_value=True):
self.api.migrate_instance_start(
self.context, instance, migration)
mocked_client.show_port_binding.assert_called_once_with(
uuids.port_id, 'dest')
mocked_client.activate_port_binding.assert_called_once_with(
uuids.port_id, 'dest')
@mock.patch('nova.network.neutron.get_client')
def test_migrate_instance_start_already_active(self, get_client_mock): def test_migrate_instance_start_already_active(self, get_client_mock):
"""Tests the case that the destination host port binding is already """Tests the case that the destination host port binding is already
ACTIVE when migrate_instance_start is called so we don't try to ACTIVE when migrate_instance_start is called so we don't try to
activate it again, which would result in a 409 from Neutron. activate it again, which would result in a 409 from Neutron.
""" """
binding = {'binding': {'status': 'ACTIVE'}} binding = {'binding': {'status': 'ACTIVE'}}
resp = fake_req.FakeResponse(200, content=jsonutils.dumps(binding)) mocked_client = get_client_mock.return_value
get_client_mock.return_value.get.return_value = resp mocked_client.show_port_binding.return_value = binding
# Just create a simple instance with a single port. # Just create a simple instance with a single port.
instance = objects.Instance(info_cache=objects.InstanceInfoCache( instance = objects.Instance(info_cache=objects.InstanceInfoCache(
network_info=model.NetworkInfo([model.VIF(uuids.port_id)]))) network_info=model.NetworkInfo([model.VIF(uuids.port_id)])))
migration = objects.Migration( migration = objects.Migration(
source_compute='source', dest_compute='dest') source_compute='source', dest_compute='dest')
with mock.patch.object(self.api, 'activate_port_binding',
new_callable=mock.NonCallableMock):
with mock.patch.object(self.api, 'supports_port_binding_extension',
return_value=True):
self.api.migrate_instance_start(
self.context, instance, migration)
get_client_mock.return_value.get.assert_called_once_with(
'/v2.0/ports/%s/bindings/dest' % uuids.port_id, raise_exc=False,
global_request_id=self.context.global_id)
@mock.patch('nova.network.neutron._get_ksa_client') with mock.patch.object(self.api, 'supports_port_binding_extension',
return_value=True):
self.api.migrate_instance_start(
self.context, instance, migration)
mocked_client.show_port_binding.assert_called_once_with(
uuids.port_id, 'dest')
mocked_client.activate_port_binding.assert_not_called()
@mock.patch('nova.network.neutron.get_client')
def test_migrate_instance_start_no_bindings(self, get_client_mock): def test_migrate_instance_start_no_bindings(self, get_client_mock):
"""Tests the case that migrate_instance_start is running against new """Tests the case that migrate_instance_start is running against new
enough neutron for the binding-extended API but the ports don't have enough neutron for the binding-extended API but the ports don't have
a binding resource against the destination host, so no activation a binding resource against the destination host, so no activation
happens. happens.
""" """
get_client_mock.return_value.get.return_value = ( NeutronNotFound = exceptions.NeutronClientException(status_code=404)
fake_req.FakeResponse(404)) mocked_client = get_client_mock.return_value
mocked_client.show_port_binding.side_effect = NeutronNotFound
# Create an instance with two ports so we can test the short circuit # Create an instance with two ports so we can test the short circuit
# when we find that the first port doesn't have a dest host binding. # when we find that the first port doesn't have a dest host binding.
instance = objects.Instance(info_cache=objects.InstanceInfoCache( instance = objects.Instance(info_cache=objects.InstanceInfoCache(
@ -6176,45 +6176,41 @@ class TestAPI(TestAPIBase):
model.VIF(uuids.port1), model.VIF(uuids.port2)]))) model.VIF(uuids.port1), model.VIF(uuids.port2)])))
migration = objects.Migration( migration = objects.Migration(
source_compute='source', dest_compute='dest') source_compute='source', dest_compute='dest')
with mock.patch.object(self.api, 'activate_port_binding',
new_callable=mock.NonCallableMock):
with mock.patch.object(self.api, 'supports_port_binding_extension',
return_value=True):
self.api.migrate_instance_start(
self.context, instance, migration)
get_client_mock.return_value.get.assert_called_once_with(
'/v2.0/ports/%s/bindings/dest' % uuids.port1, raise_exc=False,
global_request_id=self.context.global_id)
@mock.patch('nova.network.neutron._get_ksa_client') with mock.patch.object(self.api, 'supports_port_binding_extension',
return_value=True):
self.api.migrate_instance_start(
self.context, instance, migration)
mocked_client.show_port_binding.assert_called_once_with(
uuids.port1, 'dest')
mocked_client.activate_port_binding.assert_not_called()
@mock.patch('nova.network.neutron.get_client')
def test_migrate_instance_start_get_error(self, get_client_mock): def test_migrate_instance_start_get_error(self, get_client_mock):
"""Tests the case that migrate_instance_start is running against new """Tests the case that migrate_instance_start is running against new
enough neutron for the binding-extended API but getting the port enough neutron for the binding-extended API but getting the port
binding information results in an error response from neutron. binding information results in an error response from neutron.
""" """
get_client_mock.return_value.get.return_value = ( NeutronError = exceptions.NeutronClientException(status_code=500)
fake_req.FakeResponse(500)) mocked_client = get_client_mock.return_value
mocked_client.show_port_binding.side_effect = NeutronError
instance = objects.Instance(info_cache=objects.InstanceInfoCache( instance = objects.Instance(info_cache=objects.InstanceInfoCache(
network_info=model.NetworkInfo([ network_info=model.NetworkInfo([
model.VIF(uuids.port1), model.VIF(uuids.port2)]))) model.VIF(uuids.port1), model.VIF(uuids.port2)])))
migration = objects.Migration( migration = objects.Migration(
source_compute='source', dest_compute='dest') source_compute='source', dest_compute='dest')
with mock.patch.object(self.api, 'activate_port_binding',
new_callable=mock.NonCallableMock): with mock.patch.object(self.api, 'supports_port_binding_extension',
with mock.patch.object(self.api, 'supports_port_binding_extension', return_value=True):
return_value=True): self.api.migrate_instance_start(
self.api.migrate_instance_start( self.context, instance, migration)
self.context, instance, migration)
self.assertEqual(2, get_client_mock.return_value.get.call_count) self.assertEqual(2, mocked_client.show_port_binding.call_count)
get_client_mock.return_value.get.assert_has_calls([ mocked_client.show_port_binding.assert_has_calls([
mock.call( mock.call(uuids.port1, 'dest'),
'/v2.0/ports/%s/bindings/dest' % uuids.port1, mock.call(uuids.port2, 'dest'),
raise_exc=False, ])
global_request_id=self.context.global_id),
mock.call(
'/v2.0/ports/%s/bindings/dest' % uuids.port2,
raise_exc=False,
global_request_id=self.context.global_id)])
@mock.patch('nova.network.neutron.get_client') @mock.patch('nova.network.neutron.get_client')
def test_get_requested_resource_for_instance_no_resource_request( def test_get_requested_resource_for_instance_no_resource_request(
@ -6778,7 +6774,7 @@ class TestAPIPortbinding(TestAPIBase):
'fake_host', 'setup_instance_network_on_host', 'fake_host', 'setup_instance_network_on_host',
self.context, instance, 'fake_host') self.context, instance, 'fake_host')
@mock.patch('nova.network.neutron._get_ksa_client', @mock.patch('nova.network.neutron.get_client',
new_callable=mock.NonCallableMock) new_callable=mock.NonCallableMock)
def test_bind_ports_to_host_no_ports(self, mock_client): def test_bind_ports_to_host_no_ports(self, mock_client):
self.assertDictEqual({}, self.assertDictEqual({},
@ -6787,11 +6783,11 @@ class TestAPIPortbinding(TestAPIBase):
objects.Instance(info_cache=None), objects.Instance(info_cache=None),
'fake-host')) 'fake-host'))
@mock.patch('nova.network.neutron._get_ksa_client') @mock.patch('nova.network.neutron.get_client')
def test_bind_ports_to_host(self, mock_client): def test_bind_ports_to_host(self, mock_client):
"""Tests a single port happy path where everything is successful.""" """Tests a single port happy path where everything is successful."""
def post_side_effect(*args, **kwargs): def fake_create(port_id, data):
self.assertDictEqual(binding, kwargs['json']) self.assertDictEqual(binding, data)
return mock.DEFAULT return mock.DEFAULT
nwinfo = model.NetworkInfo([model.VIF(uuids.port)]) nwinfo = model.NetworkInfo([model.VIF(uuids.port)])
@ -6801,21 +6797,22 @@ class TestAPIPortbinding(TestAPIBase):
binding = {'binding': {'host': 'fake-host', binding = {'binding': {'host': 'fake-host',
'vnic_type': 'normal', 'vnic_type': 'normal',
'profile': {'foo': 'bar'}}} 'profile': {'foo': 'bar'}}}
mocked_client = mock_client.return_value
mocked_client.create_port_binding.return_value = binding
mocked_client.create_port_binding.side_effect = fake_create
resp = fake_req.FakeResponse(200, content=jsonutils.dumps(binding))
mock_client.return_value.post.return_value = resp
mock_client.return_value.post.side_effect = post_side_effect
result = self.api.bind_ports_to_host( result = self.api.bind_ports_to_host(
ctxt, inst, 'fake-host', {uuids.port: 'normal'}, ctxt, inst, 'fake-host', {uuids.port: 'normal'},
{uuids.port: {'foo': 'bar'}}) {uuids.port: {'foo': 'bar'}})
self.assertEqual(1, mock_client.return_value.post.call_count)
self.assertEqual(1, mocked_client.create_port_binding.call_count)
self.assertDictEqual({uuids.port: binding['binding']}, result) self.assertDictEqual({uuids.port: binding['binding']}, result)
@mock.patch('nova.network.neutron._get_ksa_client') @mock.patch('nova.network.neutron.get_client')
def test_bind_ports_to_host_with_vif_profile_and_vnic(self, mock_client): def test_bind_ports_to_host_with_vif_profile_and_vnic(self, mock_client):
"""Tests bind_ports_to_host with default/non-default parameters.""" """Tests bind_ports_to_host with default/non-default parameters."""
def post_side_effect(*args, **kwargs): def fake_create(port_id, data):
self.assertDictEqual(binding, kwargs['json']) self.assertDictEqual(binding, data)
return mock.DEFAULT return mock.DEFAULT
ctxt = context.get_context() ctxt = context.get_context()
@ -6828,11 +6825,13 @@ class TestAPIPortbinding(TestAPIBase):
binding = {'binding': {'host': 'fake-host', binding = {'binding': {'host': 'fake-host',
'vnic_type': 'direct', 'vnic_type': 'direct',
'profile': vif_profile}} 'profile': vif_profile}}
resp = fake_req.FakeResponse(200, content=jsonutils.dumps(binding)) mocked_client = mock_client.return_value
mock_client.return_value.post.return_value = resp mocked_client.create_port_binding.return_value = binding
mock_client.return_value.post.side_effect = post_side_effect mocked_client.create_port_binding.side_effect = fake_create
result = self.api.bind_ports_to_host(ctxt, inst, 'fake-host') result = self.api.bind_ports_to_host(ctxt, inst, 'fake-host')
self.assertEqual(1, mock_client.return_value.post.call_count)
self.assertEqual(1, mocked_client.create_port_binding.call_count)
self.assertDictEqual({uuids.port: binding['binding']}, result) self.assertDictEqual({uuids.port: binding['binding']}, result)
# assert that that if vnic_type and profile are set in VIF object # assert that that if vnic_type and profile are set in VIF object
@ -6848,14 +6847,17 @@ class TestAPIPortbinding(TestAPIBase):
binding = {'binding': {'host': 'fake-host', binding = {'binding': {'host': 'fake-host',
'vnic_type': 'direct-overridden', 'vnic_type': 'direct-overridden',
'profile': {'foo': 'overridden'}}} 'profile': {'foo': 'overridden'}}}
resp = fake_req.FakeResponse(200, content=jsonutils.dumps(binding)) mocked_client = mock_client.return_value
mock_client.return_value.post.return_value = resp mocked_client.create_port_binding.return_value = binding
mocked_client.create_port_binding.side_effect = fake_create
result = self.api.bind_ports_to_host( result = self.api.bind_ports_to_host(
ctxt, inst, 'fake-host', vnic_type_per_port, vif_profile_per_port) ctxt, inst, 'fake-host', vnic_type_per_port, vif_profile_per_port)
self.assertEqual(2, mock_client.return_value.post.call_count)
self.assertEqual(2, mocked_client.create_port_binding.call_count)
self.assertDictEqual({uuids.port: binding['binding']}, result) self.assertDictEqual({uuids.port: binding['binding']}, result)
@mock.patch('nova.network.neutron._get_ksa_client') @mock.patch('nova.network.neutron.get_client')
def test_bind_ports_to_host_rollback(self, mock_client): def test_bind_ports_to_host_rollback(self, mock_client):
"""Tests a scenario where an instance has two ports, and binding the """Tests a scenario where an instance has two ports, and binding the
first is successful but binding the second fails, so the code will first is successful but binding the second fails, so the code will
@ -6865,43 +6867,42 @@ class TestAPIPortbinding(TestAPIBase):
model.VIF(uuids.ok), model.VIF(uuids.fail)]) model.VIF(uuids.ok), model.VIF(uuids.fail)])
inst = objects.Instance( inst = objects.Instance(
info_cache=objects.InstanceInfoCache(network_info=nwinfo)) info_cache=objects.InstanceInfoCache(network_info=nwinfo))
NeutronError = exceptions.NeutronClientException(status_code=500)
def fake_post(url, *args, **kwargs): def fake_create(port_id, host):
if uuids.ok in url: if port_id == uuids.ok:
mock_response = fake_req.FakeResponse( return {'binding': {'host': 'fake-host'}}
200, content='{"binding": {"host": "fake-host"}}')
else:
mock_response = fake_req.FakeResponse(500, content='error')
return mock_response
mock_client.return_value.post.side_effect = fake_post raise NeutronError
with mock.patch.object(self.api, 'delete_port_binding',
# This will be logged but not re-raised.
side_effect=exception.PortBindingDeletionFailed(
port_id=uuids.ok, host='fake-host'
)) as mock_delete:
self.assertRaises(exception.PortBindingFailed,
self.api.bind_ports_to_host,
self.context, inst, 'fake-host')
# assert that post was called twice and delete once
self.assertEqual(2, mock_client.return_value.post.call_count)
mock_delete.assert_called_once_with(self.context, uuids.ok,
'fake-host')
@mock.patch('nova.network.neutron._get_ksa_client') mocked_client = mock_client.return_value
mocked_client.create_port_binding.side_effect = fake_create
mocked_client.delete_port_binding.side_effect = NeutronError
self.assertRaises(exception.PortBindingFailed,
self.api.bind_ports_to_host,
self.context, inst, 'fake-host')
# assert that create was called twice and delete once
self.assertEqual(2, mocked_client.create_port_binding.call_count)
self.assertEqual(1, mocked_client.delete_port_binding.call_count)
mocked_client.delete_port_binding.assert_called_once_with(
uuids.ok, 'fake-host')
@mock.patch('nova.network.neutron.get_client')
def test_delete_port_binding(self, mock_client): def test_delete_port_binding(self, mock_client):
# Create three ports where: # Create three ports where:
# - one is successfully unbound # - one is successfully unbound
# - one is not found # - one is not found
# - one fails to be unbound # - one fails to be unbound
def fake_delete(url, *args, **kwargs): def fake_delete(port_id, host):
if uuids.ok in url: if port_id == uuids.ok:
return fake_req.FakeResponse(204) return
else:
status_code = 404 if uuids.notfound in url else 500
return fake_req.FakeResponse(status_code)
mock_client.return_value.delete.side_effect = fake_delete status_code = 404 if port_id == uuids.notfound else 500
raise exceptions.NeutronClientException(status_code=status_code)
mock_client.return_value.delete_port_binding.side_effect = fake_delete
for port_id in (uuids.ok, uuids.notfound, uuids.fail): for port_id in (uuids.ok, uuids.notfound, uuids.fail):
if port_id == uuids.fail: if port_id == uuids.fail:
self.assertRaises(exception.PortBindingDeletionFailed, self.assertRaises(exception.PortBindingDeletionFailed,
@ -6911,45 +6912,6 @@ class TestAPIPortbinding(TestAPIBase):
self.api.delete_port_binding(self.context, port_id, self.api.delete_port_binding(self.context, port_id,
'fake-host') 'fake-host')
@mock.patch('nova.network.neutron._get_ksa_client')
def test_activate_port_binding(self, mock_client):
"""Tests the happy path of activating an inactive port binding."""
resp = fake_req.FakeResponse(200)
mock_client.return_value.put.return_value = resp
self.api.activate_port_binding(self.context, 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,
global_request_id=self.context.global_id)
@mock.patch('nova.network.neutron._get_ksa_client')
@mock.patch('nova.network.neutron.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."""
mock_client.return_value.put.return_value = fake_req.FakeResponse(409)
self.api.activate_port_binding(self.context, 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,
global_request_id=self.context.global_id)
self.assertEqual(1, mock_log_warning.call_count)
self.assertIn('is already active', mock_log_warning.call_args[0][0])
@mock.patch('nova.network.neutron._get_ksa_client')
def test_activate_port_binding_fails(self, mock_client):
"""Tests the unknown error case of binding activation."""
mock_client.return_value.put.return_value = fake_req.FakeResponse(500)
self.assertRaises(exception.PortBindingActivationFailed,
self.api.activate_port_binding,
self.context, 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,
global_request_id=self.context.global_id)
class TestAllocateForInstance(test.NoDBTestCase): class TestAllocateForInstance(test.NoDBTestCase):
def setUp(self): def setUp(self):

View File

@ -26,7 +26,7 @@ iso8601>=0.1.11 # MIT
jsonschema>=3.2.0 # MIT jsonschema>=3.2.0 # MIT
python-cinderclient!=4.0.0,>=3.3.0 # Apache-2.0 python-cinderclient!=4.0.0,>=3.3.0 # Apache-2.0
keystoneauth1>=3.16.0 # Apache-2.0 keystoneauth1>=3.16.0 # Apache-2.0
python-neutronclient>=6.7.0 # Apache-2.0 python-neutronclient>=7.1.0 # Apache-2.0
python-glanceclient>=2.8.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0
requests>=2.25.1 # Apache-2.0 requests>=2.25.1 # Apache-2.0
stevedore>=1.20.0 # Apache-2.0 stevedore>=1.20.0 # Apache-2.0