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:
parent
9cdecc81fb
commit
982e2ee02d
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
89
nova/tests/fixtures/neutron.py
vendored
89
nova/tests/fixtures/neutron.py
vendored
@ -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
|
||||||
|
@ -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'])
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user