From 78e10bc060170ad599ca4d289ae3fc49dbc72e02 Mon Sep 17 00:00:00 2001 From: Marc Koderer Date: Mon, 9 May 2016 12:05:07 +0200 Subject: [PATCH] Add neutron driver for binding Add new neutron plugin which enables port bind actions for network fabrics. This driver waits until all ports are in state "active" and sets the binding host flag. Default vNIC type is set to "baremetal" in order to benefit from the code already in place for Ironic. It's also possible to switch to 'normal' which assumes an neutron agent in place. The feature can be tested using the docker driver. DocImpact Co-Authored-By: Daniel Gonzalez Partially-Implements: bp manila-hpb-support Change-Id: I3156d7468d48f84f1b46885780a2426f9b99a387 --- devstack/plugin.sh | 7 +- devstack/settings | 4 + doc/source/adminref/network_plugins.rst | 7 + manila/exception.py | 4 + manila/network/__init__.py | 11 + manila/network/neutron/api.py | 5 +- manila/network/neutron/constants.py | 5 + .../network/neutron/neutron_network_plugin.py | 114 +++- manila/opts.py | 2 +- manila/share/driver.py | 11 + manila/share/manager.py | 3 + .../tests/network/neutron/test_neutron_api.py | 19 + .../network/neutron/test_neutron_plugin.py | 618 ++++++++++++++++-- ...utron-binding-driver-43f01565051b031b.yaml | 3 + 14 files changed, 745 insertions(+), 68 deletions(-) create mode 100644 releasenotes/notes/neutron-binding-driver-43f01565051b031b.yaml diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 6fca7ed592..fa25107afa 100755 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -75,6 +75,12 @@ function configure_default_backends { iniset $MANILA_CONF $group_name service_instance_user $MANILA_SERVICE_INSTANCE_USER iniset $MANILA_CONF $group_name driver_handles_share_servers True + if [ "$SHARE_DRIVER" == $MANILA_CONTAINER_DRIVER ]; then + iniset $MANILA_CONF $group_name network_api_class $MANILA_NETWORK_API_CLASS + iniset $MANILA_CONF $group_name neutron_host_id $(hostname) + iniset $MANILA_CONF $group_name neutron_vnic_type $MANILA_NEUTRON_VNIC_TYPE + fi + if [ $(trueorfalse False MANILA_USE_SERVICE_INSTANCE_PASSWORD) == True ]; then iniset $MANILA_CONF $group_name service_instance_password $MANILA_SERVICE_INSTANCE_PASSWORD fi @@ -522,7 +528,6 @@ function init_manila { elif [ "$SHARE_DRIVER" == $MANILA_CONTAINER_DRIVER ]; then if is_service_enabled m-shr; then SHARE_GROUP=$MANILA_CONTAINER_VOLUME_GROUP_NAME - iniset $MANILA_CONF DEFAULT neutron_host_id $(hostname) configure_backing_file fi diff --git a/devstack/settings b/devstack/settings index a534fc6dea..3de8f7f07d 100644 --- a/devstack/settings +++ b/devstack/settings @@ -166,6 +166,10 @@ MANILA_CONTAINER_VOLUME_GROUP_NAME=${MANILA_CONTAINER_VOLUME_GROUP_NAME:-"manila # permanent one as soon as possible. MANILA_DOCKER_IMAGE_URL=${MANILA_DOCKER_IMAGE_URL:-"https://github.com/a-ovchinnikov/manila-image-elements-lxd-images/releases/download/0.1.0/manila-docker-container.tar.gz"} +# Network Plugin +MANILA_NETWORK_API_CLASS=${MANILA_NETWORK_API_CLASS:-"manila.network.neutron.neutron_network_plugin.NeutronBindNetworkPlugin"} +MANILA_NEUTRON_VNIC_TYPE=${MANILA_NEUTRON_VNIC_TYPE:-"normal"} + # Enable manila services # ---------------------- # We have to add Manila to enabled services for screen_it to work diff --git a/doc/source/adminref/network_plugins.rst b/doc/source/adminref/network_plugins.rst index 43dad585c2..1898706c2d 100644 --- a/doc/source/adminref/network_plugins.rst +++ b/doc/source/adminref/network_plugins.rst @@ -77,6 +77,13 @@ There are three different network plugins and five python classes in Manila: `neutron_net_id` and `neutron_subnet_id` from the Manila configuration file and uses one network for all shares. + 1.3 `manila.network.neutron.neutron_network_plugin.NeutronBindNetworkPlugin`. + This driver waits for active binding and fails if a Neutron port can't be + bound or an error occurs. This plugin is useful for agent based binding + (like OVS with docker driver) and fabric binding where real HW + reconfiguration is taking place. The existing + `NeutronBindSingleNetworkPlugin` is a combination of 1.2 and 1.3. + When only a single network is needed, the NeutronSingleNetworkPlugin (1.2) is a simple solution. Otherwise NeutronNetworkPlugin (1.1) should be chosen. diff --git a/manila/exception.py b/manila/exception.py index 432d0926fe..7ca784ed36 100644 --- a/manila/exception.py +++ b/manila/exception.py @@ -114,6 +114,10 @@ class NetworkException(ManilaException): message = _("Exception due to network failure.") +class NetworkBindException(ManilaException): + message = _("Exception due to failed port status in binding.") + + class NetworkBadConfigurationException(NetworkException): message = _("Bad network configuration: %(reason)s.") diff --git a/manila/network/__init__.py b/manila/network/__init__.py index d618345cab..dee16b58c5 100644 --- a/manila/network/__init__.py +++ b/manila/network/__init__.py @@ -65,6 +65,17 @@ class NetworkBaseAPI(db_base.Base): "'%s'.") % share_server_id raise exception.NetworkBadConfigurationException(reason=msg) + def update_network_allocation(self, context, share_server): + """Update network allocation. + + Optional method to be called by the manager after share server creation + which can be overloaded in case the port state has to be updated. + + :param context: RequestContext object + :param share_server: share server object + :return: list of updated ports or None if nothing was updated + """ + @abc.abstractmethod def allocate_network(self, context, share_server, share_network=None, **kwargs): diff --git a/manila/network/neutron/api.py b/manila/network/neutron/api.py index 24a8e8758c..af15fb7566 100644 --- a/manila/network/neutron/api.py +++ b/manila/network/neutron/api.py @@ -164,7 +164,8 @@ class API(object): def create_port(self, tenant_id, network_id, host_id=None, subnet_id=None, fixed_ip=None, device_owner=None, device_id=None, - mac_address=None, security_group_ids=None, dhcp_opts=None): + mac_address=None, security_group_ids=None, dhcp_opts=None, + **kwargs): try: port_req_body = {'port': {}} port_req_body['port']['network_id'] = network_id @@ -187,6 +188,8 @@ class API(object): port_req_body['port']['device_owner'] = device_owner if device_id: port_req_body['port']['device_id'] = device_id + if kwargs: + port_req_body['port'].update(kwargs) port = self.client.create_port(port_req_body).get('port', {}) return port except neutron_client_exc.NeutronClientException as e: diff --git a/manila/network/neutron/constants.py b/manila/network/neutron/constants.py index 1aac2800d2..23ce4cd92a 100644 --- a/manila/network/neutron/constants.py +++ b/manila/network/neutron/constants.py @@ -15,3 +15,8 @@ PROVIDER_NW_EXT = 'Provider Network' PORTBINDING_EXT = 'Port Binding' + +PORT_STATUS_ERROR = 'ERROR' +PORT_STATUS_ACTIVE = 'ACTIVE' + +VIF_TYPE_BINDING_FAILED = 'binding_failed' diff --git a/manila/network/neutron/neutron_network_plugin.py b/manila/network/neutron/neutron_network_plugin.py index c31a664a7d..06306a8f29 100644 --- a/manila/network/neutron/neutron_network_plugin.py +++ b/manila/network/neutron/neutron_network_plugin.py @@ -14,15 +14,21 @@ # License for the specific language governing permissions and limitations # under the License. +import socket + from oslo_config import cfg +from oslo_log import log from manila.common import constants from manila import exception +from manila.i18n import _ from manila import network from manila.network.neutron import api as neutron_api from manila.network.neutron import constants as neutron_constants from manila import utils +LOG = log.getLogger(__name__) + neutron_single_network_plugin_opts = [ cfg.StrOpt( 'neutron_net_id', @@ -39,12 +45,18 @@ neutron_single_network_plugin_opts = [ deprecated_group='DEFAULT'), ] -neutron_network_plugin_opts = [ +neutron_bind_network_plugin_opts = [ + cfg.StrOpt( + 'neutron_vnic_type', + help="vNIC type used for binding.", + choices=['baremetal', 'normal', 'direct', + 'direct-physical', 'macvtap'], + default='baremetal'), cfg.StrOpt( "neutron_host_id", - help="Host ID to be used when creating neutron port. Hostname of " - "a controller running Neutron should be used in a general case.", - deprecated_group='DEFAULT'), + help="Host ID to be used when creating neutron port. If not set " + "host is set to manila-share host by default.", + default=socket.gethostname()), ] CONF = cfg.CONF @@ -59,9 +71,6 @@ class NeutronNetworkPlugin(network.NetworkBaseAPI): self._neutron_api_args = args self._neutron_api_kwargs = kwargs self._label = kwargs.pop('label', 'user') - CONF.register_opts( - neutron_network_plugin_opts, - group=self.neutron_api.config_group_name) @property def label(self): @@ -124,13 +133,22 @@ class NeutronNetworkPlugin(network.NetworkBaseAPI): for port in ports: self._delete_port(context, port) + def _get_port_create_args(self, share_server, share_network, + device_owner): + return { + "network_id": share_network['neutron_net_id'], + "subnet_id": share_network['neutron_subnet_id'], + "device_owner": 'manila:' + device_owner, + "device_id": share_server.get('id'), + } + def _create_port(self, context, share_server, share_network, device_owner): - host_id = self.neutron_api.configuration.neutron_host_id + create_args = self._get_port_create_args(share_server, share_network, + device_owner) + port = self.neutron_api.create_port( - share_network['project_id'], - network_id=share_network['neutron_net_id'], - subnet_id=share_network['neutron_subnet_id'], - device_owner='manila:' + device_owner, host_id=host_id) + share_network['project_id'], **create_args) + port_dict = { 'id': port['id'], 'share_server_id': share_server['id'], @@ -214,7 +232,7 @@ class NeutronSingleNetworkPlugin(NeutronNetworkPlugin): 'neutron_net_id': self.net, 'neutron_subnet_id': self.subnet, } - super(NeutronSingleNetworkPlugin, self).allocate_network( + return super(NeutronSingleNetworkPlugin, self).allocate_network( context, share_server, share_network, **kwargs) def _verify_net_and_subnet(self): @@ -261,3 +279,73 @@ class NeutronSingleNetworkPlugin(NeutronNetworkPlugin): share_network = self.db.share_network_update( context, share_network['id'], upd) return share_network + + +class NeutronBindNetworkPlugin(NeutronNetworkPlugin): + def __init__(self, *args, **kwargs): + super(NeutronBindNetworkPlugin, self).__init__(*args, **kwargs) + CONF.register_opts( + neutron_bind_network_plugin_opts, + group=self.neutron_api.config_group_name) + self.config = self.neutron_api.configuration + + def update_network_allocation(self, context, share_server): + if self.config.neutron_vnic_type == 'normal': + ports = self.db.network_allocations_get_for_share_server( + context, + share_server['id']) + self._wait_for_ports_bind(ports, share_server) + return ports + + @utils.retry(exception.NetworkBindException, retries=20) + def _wait_for_ports_bind(self, ports, share_server): + inactive_ports = [] + for port in ports: + port = self._neutron_api.show_port(port['id']) + if (port['status'] == neutron_constants.PORT_STATUS_ERROR or + ('binding:vif_type' in port and + port['binding:vif_type'] == + neutron_constants.VIF_TYPE_BINDING_FAILED)): + msg = _("Port binding %s failed.") % port['id'] + raise exception.NetworkException(msg) + elif port['status'] != neutron_constants.PORT_STATUS_ACTIVE: + LOG.debug("The port %(id)s is in state %(state)s. " + "Wait for active state.", { + "id": port['id'], + "state": port['status']}) + inactive_ports.append(port['id']) + if len(inactive_ports) == 0: + return + msg = _("Ports are not fully bound for share server " + "'%(s_id)s' (inactive ports: %(ports)s)") % { + "s_id": share_server['id'], + "ports": inactive_ports} + raise exception.NetworkBindException(msg) + + def _get_port_create_args(self, share_server, share_network, + device_owner): + arguments = super( + NeutronBindNetworkPlugin, self)._get_port_create_args( + share_network, share_network, device_owner) + arguments['host_id'] = self.config.neutron_host_id + arguments['binding:vnic_type'] = self.config.neutron_vnic_type + return arguments + + def allocate_network(self, context, share_server, share_network=None, + **kwargs): + ports = super(NeutronBindNetworkPlugin, self).allocate_network( + context, share_server, share_network, **kwargs) + # If vnic type is 'normal' we expect a neutron agent to bind the + # ports. This action requires a vnic to be spawned by the driver. + # Therefore we do not wait for the port binding here, but + # return the unbound ports and expect the share manager to call + # update_network_allocation after the share server was created, in + # order to update the ports with the correct binding. + if self.config.neutron_vnic_type != 'normal': + self._wait_for_ports_bind(ports, share_server) + return ports + + +class NeutronBindSingleNetworkPlugin(NeutronSingleNetworkPlugin, + NeutronBindNetworkPlugin): + pass diff --git a/manila/opts.py b/manila/opts.py index 7058ac2735..064bcbeb95 100644 --- a/manila/opts.py +++ b/manila/opts.py @@ -100,7 +100,7 @@ _global_opt_lists = [ manila.network.linux.interface.OPTS, manila.network.network_opts, manila.network.neutron.neutron_network_plugin. - neutron_network_plugin_opts, + neutron_bind_network_plugin_opts, manila.network.neutron.neutron_network_plugin. neutron_single_network_plugin_opts, manila.network.nova_network_plugin.nova_single_network_plugin_opts, diff --git a/manila/share/driver.py b/manila/share/driver.py index 4d1237b56d..304c4b2149 100644 --- a/manila/share/driver.py +++ b/manila/share/driver.py @@ -570,6 +570,17 @@ class ShareDriver(object): def get_admin_network_allocations_number(self): return 0 + def update_network_allocation(self, context, share_server): + """Update network allocation after share server creation.""" + self.network_api.update_network_allocation(context, share_server) + + def update_admin_network_allocation(self, context, share_server): + """Update admin network allocation after share server creation.""" + if (self.get_admin_network_allocations_number() and + self.admin_network_api): + self.admin_network_api.update_network_allocation(context, + share_server) + def allocate_network(self, context, share_server, share_network, count=None, **kwargs): """Allocate network resources using given network information.""" diff --git a/manila/share/manager.py b/manila/share/manager.py index e1ee98f0cb..1be3a3595c 100644 --- a/manila/share/manager.py +++ b/manila/share/manager.py @@ -2375,6 +2375,9 @@ class ShareManager(manager.SchedulerDependentManager): server_info = self.driver.setup_server( network_info, metadata=metadata) + self.driver.update_network_allocation(context, share_server) + self.driver.update_admin_network_allocation(context, share_server) + if server_info and isinstance(server_info, dict): self.db.share_server_backend_details_set( context, share_server['id'], server_info) diff --git a/manila/tests/network/neutron/test_neutron_api.py b/manila/tests/network/neutron/test_neutron_api.py index 6d14768b7c..ef3ace5428 100644 --- a/manila/tests/network/neutron/test_neutron_api.py +++ b/manila/tests/network/neutron/test_neutron_api.py @@ -164,6 +164,25 @@ class NeutronApiTest(test.TestCase): self.neutron_api._has_port_binding_extension.assert_called_once_with() self.assertTrue(clientv20.Client.called) + def test_create_port_with_additional_kwargs(self): + # Set up test data + self.mock_object(self.neutron_api, '_has_port_binding_extension', + mock.Mock(return_value=True)) + port_args = {'tenant_id': 'test tenant', 'network_id': 'test net', + 'binding_arg': 'foo'} + + # Execute method 'create_port' + port = self.neutron_api.create_port(**port_args) + + # Verify results + self.assertEqual(port_args['tenant_id'], port['tenant_id']) + self.assertEqual(port_args['network_id'], + port['network_id']) + self.assertEqual(port_args['binding_arg'], + port['binding_arg']) + self.neutron_api._has_port_binding_extension.assert_called_once_with() + self.assertTrue(clientv20.Client.called) + @mock.patch.object(neutron_api.LOG, 'exception', mock.Mock()) def test_create_port_exception(self): # Set up test data diff --git a/manila/tests/network/neutron/test_neutron_plugin.py b/manila/tests/network/neutron/test_neutron_plugin.py index eeb333b34d..8bfdda8a5a 100644 --- a/manila/tests/network/neutron/test_neutron_plugin.py +++ b/manila/tests/network/neutron/test_neutron_plugin.py @@ -14,8 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import ddt import mock +import time + from oslo_config import cfg from manila.common import constants @@ -31,7 +34,7 @@ from manila.tests import utils as test_utils CONF = cfg.CONF fake_neutron_port = { - "status": "test_port_status", + "status": "ACTIVE", "allowed_address_pairs": [], "admin_state_up": True, "network_id": "test_net_id", @@ -132,7 +135,8 @@ class NeutronNetworkPluginTest(test.TestCase): fake_share_network['project_id'], network_id=fake_share_network['neutron_net_id'], subnet_id=fake_share_network['neutron_subnet_id'], - device_owner='manila:share', host_id=None) + device_owner='manila:share', + device_id=fake_share_network['id']) db_api.network_allocation_create.assert_called_once_with( self.fake_context, fake_network_allocation) @@ -141,54 +145,6 @@ class NeutronNetworkPluginTest(test.TestCase): save_nw_data.stop() save_subnet_data.stop() - @mock.patch.object(db_api, 'network_allocation_create', - mock.Mock(return_values=fake_network_allocation)) - @mock.patch.object(db_api, 'share_network_get', - mock.Mock(return_value=fake_share_network)) - @mock.patch.object(db_api, 'share_server_get', - mock.Mock(return_value=fake_share_server)) - def test_allocate_network_host_id_prconfigured(self): - has_provider_nw_ext = mock.patch.object( - self.plugin, '_has_provider_network_extension').start() - has_provider_nw_ext.return_value = True - save_nw_data = mock.patch.object(self.plugin, - '_save_neutron_network_data').start() - save_subnet_data = mock.patch.object( - self.plugin, - '_save_neutron_subnet_data').start() - - config_data = { - 'DEFAULT': { - 'neutron_host_id': 'fake_host_id', - } - } - with test_utils.create_temp_config_with_opts(config_data): - with mock.patch.object(self.plugin.neutron_api, 'create_port', - mock.Mock(return_value=fake_neutron_port)): - self.plugin.allocate_network( - self.fake_context, - fake_share_server, - fake_share_network, - allocation_info={'count': 1}) - - has_provider_nw_ext.assert_any_call() - save_nw_data.assert_called_once_with(self.fake_context, - fake_share_network) - save_subnet_data.assert_called_once_with(self.fake_context, - fake_share_network) - self.plugin.neutron_api.create_port.assert_called_once_with( - fake_share_network['project_id'], - network_id=fake_share_network['neutron_net_id'], - subnet_id=fake_share_network['neutron_subnet_id'], - device_owner='manila:share', host_id='fake_host_id') - db_api.network_allocation_create.assert_called_once_with( - self.fake_context, - fake_network_allocation) - - has_provider_nw_ext.stop() - save_nw_data.stop() - save_subnet_data.stop() - @mock.patch.object(db_api, 'network_allocation_create', mock.Mock(return_values=fake_network_allocation)) @mock.patch.object(db_api, 'share_network_get', @@ -217,11 +173,13 @@ class NeutronNetworkPluginTest(test.TestCase): mock.call(fake_share_network['project_id'], network_id=fake_share_network['neutron_net_id'], subnet_id=fake_share_network['neutron_subnet_id'], - device_owner='manila:share', host_id=None), + device_owner='manila:share', + device_id=fake_share_network['id']), mock.call(fake_share_network['project_id'], network_id=fake_share_network['neutron_net_id'], subnet_id=fake_share_network['neutron_subnet_id'], - device_owner='manila:share', host_id=None), + device_owner='manila:share', + device_id=fake_share_network['id']), ] db_api_calls = [ mock.call(self.fake_context, fake_network_allocation), @@ -541,3 +499,559 @@ class NeutronSingleNetworkPluginTest(test.TestCase): plugin.NeutronNetworkPlugin.allocate_network.assert_called_once_with( self.context, share_server, share_network_upd, count=count, device_owner=device_owner) + plugin.NeutronNetworkPlugin.allocate_network.reset_mock() + + +@ddt.ddt +class NeutronBindNetworkPluginTest(test.TestCase): + def setUp(self): + super(NeutronBindNetworkPluginTest, self).setUp() + self.fake_context = context.RequestContext(user_id='fake user', + project_id='fake project', + is_admin=False) + self.has_binding_ext_mock = self.mock_object( + neutron_api.API, '_has_port_binding_extension') + self.has_binding_ext_mock.return_value = True + self.bind_plugin = plugin.NeutronBindNetworkPlugin() + self.bind_plugin.db = db_api + self.sleep_mock = self.mock_object(time, 'sleep') + + def test_wait_for_bind(self): + self.mock_object(self.bind_plugin.neutron_api, 'show_port') + self.bind_plugin.neutron_api.show_port.return_value = fake_neutron_port + + self.bind_plugin._wait_for_ports_bind([fake_neutron_port], + fake_share_server) + + self.bind_plugin.neutron_api.show_port.assert_called_once_with( + fake_neutron_port['id']) + self.sleep_mock.assert_not_called() + + def test_wait_for_bind_error(self): + fake_neut_port = copy.copy(fake_neutron_port) + fake_neut_port['status'] = 'ERROR' + self.mock_object(self.bind_plugin.neutron_api, 'show_port') + self.bind_plugin.neutron_api.show_port.return_value = fake_neut_port + + self.assertRaises(exception.ManilaException, + self.bind_plugin._wait_for_ports_bind, + [fake_neut_port, fake_neut_port], + fake_share_server) + + self.bind_plugin.neutron_api.show_port.assert_called_once_with( + fake_neutron_port['id']) + self.sleep_mock.assert_not_called() + + @ddt.data(('DOWN', 'ACTIVE'), ('DOWN', 'DOWN'), ('ACTIVE', 'DOWN')) + @mock.patch.object(time, 'time', side_effect=[1, 1, 3]) + def test_wait_for_bind_two_ports_no_bind(self, state, time_mock): + fake_neut_port1 = copy.copy(fake_neutron_port) + fake_neut_port1['status'] = state[0] + fake_neut_port2 = copy.copy(fake_neutron_port) + fake_neut_port2['status'] = state[1] + self.mock_object(self.bind_plugin.neutron_api, 'show_port') + self.bind_plugin.neutron_api.show_port.side_effect = [fake_neut_port1, + fake_neut_port2] + + self.assertRaises(exception.ManilaException, + self.bind_plugin._wait_for_ports_bind.__wrapped__, + self.bind_plugin, + [fake_neut_port1, fake_neut_port2], + fake_share_server) + + @mock.patch.object(db_api, 'network_allocation_create', + mock.Mock(return_values=fake_network_allocation)) + @mock.patch.object(db_api, 'share_network_get', + mock.Mock(return_value=fake_share_network)) + @mock.patch.object(db_api, 'share_server_get', + mock.Mock(return_value=fake_share_server)) + def test_allocate_network_one_allocation(self): + self.mock_object(self.bind_plugin, '_has_provider_network_extension') + self.bind_plugin._has_provider_network_extension.return_value = True + save_nw_data = self.mock_object(self.bind_plugin, + '_save_neutron_network_data') + save_subnet_data = self.mock_object(self.bind_plugin, + '_save_neutron_subnet_data') + self.mock_object(self.bind_plugin, '_wait_for_ports_bind') + neutron_host_id_opts = plugin.neutron_bind_network_plugin_opts[1] + self.mock_object(neutron_host_id_opts, 'default') + neutron_host_id_opts.default = 'foohost1' + self.mock_object(db_api, 'network_allocation_create') + + with mock.patch.object(self.bind_plugin.neutron_api, 'create_port', + mock.Mock(return_value=fake_neutron_port)): + self.bind_plugin.allocate_network( + self.fake_context, + fake_share_server, + fake_share_network, + allocation_info={'count': 1}) + + self.bind_plugin._has_provider_network_extension.assert_any_call() + save_nw_data.assert_called_once_with(self.fake_context, + fake_share_network) + save_subnet_data.assert_called_once_with(self.fake_context, + fake_share_network) + expected_kwargs = { + 'binding:vnic_type': 'baremetal', + 'host_id': 'foohost1', + 'network_id': fake_share_network['neutron_net_id'], + 'subnet_id': fake_share_network['neutron_subnet_id'], + 'device_owner': 'manila:share', + 'device_id': fake_share_network['id'], + } + self.bind_plugin.neutron_api.create_port.assert_called_once_with( + fake_share_network['project_id'], **expected_kwargs) + db_api.network_allocation_create.assert_called_once_with( + self.fake_context, + fake_network_allocation) + self.bind_plugin._wait_for_ports_bind.assert_called_once_with( + [db_api.network_allocation_create()], fake_share_server) + + +@ddt.ddt +class NeutronBindSingleNetworkPluginTest(test.TestCase): + def setUp(self): + super(NeutronBindSingleNetworkPluginTest, self).setUp() + self.context = 'fake_context' + self.fake_context = context.RequestContext(user_id='fake user', + project_id='fake project', + is_admin=False) + self.has_binding_ext_mock = self.mock_object( + neutron_api.API, '_has_port_binding_extension') + self.has_binding_ext_mock.return_value = True + self.bind_plugin = plugin.NeutronBindNetworkPlugin() + self.bind_plugin.db = db_api + self.sleep_mock = self.mock_object(time, 'sleep') + fake_net_id = 'fake net id' + fake_subnet_id = 'fake subnet id' + config_data = { + 'DEFAULT': { + 'neutron_net_id': fake_net_id, + 'neutron_subnet_id': fake_subnet_id, + } + } + fake_net = {'subnets': ['fake1', 'fake2', fake_subnet_id]} + self.mock_object( + neutron_api.API, 'get_network', mock.Mock(return_value=fake_net)) + + with test_utils.create_temp_config_with_opts(config_data): + self.bind_plugin = plugin.NeutronBindSingleNetworkPlugin() + self.bind_plugin.db = db_api + + def test_init_valid(self): + fake_net_id = 'fake_net_id' + fake_subnet_id = 'fake_subnet_id' + config_data = { + 'DEFAULT': { + 'neutron_net_id': fake_net_id, + 'neutron_subnet_id': fake_subnet_id, + } + } + fake_net = {'subnets': ['fake1', 'fake2', fake_subnet_id]} + self.mock_object( + neutron_api.API, 'get_network', mock.Mock(return_value=fake_net)) + + with test_utils.create_temp_config_with_opts(config_data): + instance = plugin.NeutronSingleNetworkPlugin() + + self.assertEqual(fake_net_id, instance.net) + self.assertEqual(fake_subnet_id, instance.subnet) + neutron_api.API.get_network.assert_called_once_with(fake_net_id) + + @ddt.data( + {'net': None, 'subnet': None}, + {'net': 'fake_net_id', 'subnet': None}, + {'net': None, 'subnet': 'fake_subnet_id'}) + @ddt.unpack + def test_init_invalid(self, net, subnet): + config_data = dict() + # Simulate absence of set values + if net: + config_data['neutron_net_id'] = net + if subnet: + config_data['neutron_subnet_id'] = subnet + config_data = dict(DEFAULT=config_data) + + with test_utils.create_temp_config_with_opts(config_data): + self.assertRaises( + exception.NetworkBadConfigurationException, + plugin.NeutronSingleNetworkPlugin) + + @ddt.data({}, {'subnets': []}, {'subnets': ['different_foo_subnet']}) + def test_init_subnet_does_not_belong_to_net(self, fake_net): + fake_net_id = 'fake_net_id' + config_data = { + 'DEFAULT': { + 'neutron_net_id': fake_net_id, + 'neutron_subnet_id': 'fake_subnet_id', + } + } + self.mock_object( + neutron_api.API, 'get_network', mock.Mock(return_value=fake_net)) + + with test_utils.create_temp_config_with_opts(config_data): + self.assertRaises( + exception.NetworkBadConfigurationException, + plugin.NeutronSingleNetworkPlugin) + neutron_api.API.get_network.assert_called_once_with(fake_net_id) + + def _get_neutron_single_network_plugin_instance(self): + fake_subnet_id = 'fake_subnet_id' + config_data = { + 'DEFAULT': { + 'neutron_net_id': 'fake_net_id', + 'neutron_subnet_id': fake_subnet_id, + } + } + fake_net = {'subnets': [fake_subnet_id]} + self.mock_object( + neutron_api.API, 'get_network', mock.Mock(return_value=fake_net)) + with test_utils.create_temp_config_with_opts(config_data): + instance = plugin.NeutronSingleNetworkPlugin() + return instance + + def test___update_share_network_net_data_same_values(self): + instance = self._get_neutron_single_network_plugin_instance() + share_network = { + 'neutron_net_id': instance.net, + 'neutron_subnet_id': instance.subnet, + } + + result = instance._update_share_network_net_data( + self.context, share_network) + + self.assertEqual(share_network, result) + + def test___update_share_network_net_data_different_values_empty(self): + instance = self._get_neutron_single_network_plugin_instance() + share_network_input = { + 'id': 'fake_share_network_id', + } + share_network_result = { + 'neutron_net_id': instance.net, + 'neutron_subnet_id': instance.subnet, + } + self.mock_object( + instance.db, 'share_network_update', + mock.Mock(return_value='foo')) + + instance._update_share_network_net_data( + self.context, share_network_input) + + instance.db.share_network_update.assert_called_once_with( + self.context, share_network_input['id'], share_network_result) + + @ddt.data( + {'n': 'fake_net_id', 's': 'bar'}, + {'n': 'foo', 's': 'fake_subnet_id'}) + @ddt.unpack + def test___update_share_network_net_data_different_values(self, n, s): + instance = self._get_neutron_single_network_plugin_instance() + share_network = { + 'id': 'fake_share_network_id', + 'neutron_net_id': n, + 'neutron_subnet_id': s, + } + self.mock_object( + instance.db, 'share_network_update', + mock.Mock(return_value=share_network)) + + self.assertRaises( + exception.NetworkBadConfigurationException, + instance._update_share_network_net_data, + self.context, share_network) + self.assertFalse(instance.db.share_network_update.called) + + def test___update_share_network_net_data_nova_net_id_present(self): + instance = self._get_neutron_single_network_plugin_instance() + share_network = { + 'id': 'fake_share_network_id', + 'nova_net_id': 'foo', + } + self.mock_object( + instance.db, 'share_network_update', + mock.Mock(return_value=share_network)) + + self.assertRaises( + exception.NetworkBadConfigurationException, + instance._update_share_network_net_data, + self.context, share_network) + self.assertFalse(instance.db.share_network_update.called) + + @mock.patch.object( + plugin.NeutronNetworkPlugin, "allocate_network", mock.Mock()) + def test_allocate_network(self): + instance = self._get_neutron_single_network_plugin_instance() + share_server = 'fake_share_server' + share_network = 'fake_share_network' + share_network_upd = 'updated_fake_share_network' + count = 2 + device_owner = 'fake_device_owner' + self.mock_object( + instance, '_update_share_network_net_data', + mock.Mock(return_value=share_network_upd)) + + instance.allocate_network( + self.context, share_server, share_network, count=count, + device_owner=device_owner) + + instance._update_share_network_net_data.assert_called_once_with( + self.context, share_network) + plugin.NeutronNetworkPlugin.allocate_network.assert_called_once_with( + self.context, share_server, share_network_upd, count=count, + device_owner=device_owner) + plugin.NeutronNetworkPlugin.allocate_network.reset_mock() + + def test_wait_for_bind(self): + self.mock_object(self.bind_plugin.neutron_api, 'show_port') + self.bind_plugin.neutron_api.show_port.return_value = fake_neutron_port + + self.bind_plugin._wait_for_ports_bind([fake_neutron_port], + fake_share_server) + + self.bind_plugin.neutron_api.show_port.assert_called_once_with( + fake_neutron_port['id']) + self.sleep_mock.assert_not_called() + + def test_wait_for_bind_error(self): + fake_neut_port = copy.copy(fake_neutron_port) + fake_neut_port['status'] = 'ERROR' + self.mock_object(self.bind_plugin.neutron_api, 'show_port') + self.bind_plugin.neutron_api.show_port.return_value = fake_neut_port + + self.assertRaises(exception.ManilaException, + self.bind_plugin._wait_for_ports_bind, + [fake_neut_port, fake_neut_port], + fake_share_server) + + self.bind_plugin.neutron_api.show_port.assert_called_once_with( + fake_neutron_port['id']) + self.sleep_mock.assert_not_called() + + @ddt.data(('DOWN', 'ACTIVE'), ('DOWN', 'DOWN'), ('ACTIVE', 'DOWN')) + @mock.patch.object(time, 'time', side_effect=[1, 1, 3]) + def test_wait_for_bind_two_ports_no_bind(self, state, time_mock): + fake_neut_port1 = copy.copy(fake_neutron_port) + fake_neut_port1['status'] = state[0] + fake_neut_port2 = copy.copy(fake_neutron_port) + fake_neut_port2['status'] = state[1] + self.mock_object(self.bind_plugin.neutron_api, 'show_port') + self.bind_plugin.neutron_api.show_port.side_effect = [fake_neut_port1, + fake_neut_port2] + + self.assertRaises(exception.NetworkBindException, + self.bind_plugin._wait_for_ports_bind.__wrapped__, + self.bind_plugin, + [fake_neut_port1, fake_neut_port2], + fake_share_server) + + @mock.patch.object(db_api, 'network_allocation_create', + mock.Mock(return_values=fake_network_allocation)) + @mock.patch.object(db_api, 'share_network_get', + mock.Mock(return_value=fake_share_network)) + @mock.patch.object(db_api, 'share_server_get', + mock.Mock(return_value=fake_share_server)) + def test_allocate_network_one_allocation(self): + self.mock_object(self.bind_plugin, '_has_provider_network_extension') + self.bind_plugin._has_provider_network_extension.return_value = True + save_nw_data = self.mock_object(self.bind_plugin, + '_save_neutron_network_data') + save_subnet_data = self.mock_object(self.bind_plugin, + '_save_neutron_subnet_data') + self.mock_object(self.bind_plugin, '_wait_for_ports_bind') + neutron_host_id_opts = plugin.neutron_bind_network_plugin_opts[1] + self.mock_object(neutron_host_id_opts, 'default') + neutron_host_id_opts.default = 'foohost1' + self.mock_object(db_api, 'network_allocation_create') + + with mock.patch.object(self.bind_plugin.neutron_api, 'create_port', + mock.Mock(return_value=fake_neutron_port)): + self.bind_plugin.allocate_network( + self.fake_context, + fake_share_server, + fake_share_network, + allocation_info={'count': 1}) + + self.bind_plugin._has_provider_network_extension.assert_any_call() + save_nw_data.assert_called_once_with(self.fake_context, + fake_share_network) + save_subnet_data.assert_called_once_with(self.fake_context, + fake_share_network) + expected_kwargs = { + 'binding:vnic_type': 'baremetal', + 'host_id': 'foohost1', + 'network_id': fake_share_network['neutron_net_id'], + 'subnet_id': fake_share_network['neutron_subnet_id'], + 'device_owner': 'manila:share', + 'device_id': fake_share_network['id'], + } + self.bind_plugin.neutron_api.create_port.assert_called_once_with( + fake_share_network['project_id'], **expected_kwargs) + db_api.network_allocation_create.assert_called_once_with( + self.fake_context, + fake_network_allocation) + self.bind_plugin._wait_for_ports_bind.assert_called_once_with( + [db_api.network_allocation_create()], fake_share_server) + + +class NeutronBindNetworkPluginWithNormalTypeTest(test.TestCase): + def setUp(self): + super(NeutronBindNetworkPluginWithNormalTypeTest, self).setUp() + config_data = { + 'DEFAULT': { + 'neutron_vnic_type': 'normal', + } + } + self.plugin = plugin.NeutronNetworkPlugin() + self.plugin.db = db_api + self.fake_context = context.RequestContext(user_id='fake user', + project_id='fake project', + is_admin=False) + + with test_utils.create_temp_config_with_opts(config_data): + self.bind_plugin = plugin.NeutronBindNetworkPlugin() + self.bind_plugin.db = db_api + + @mock.patch.object(db_api, 'network_allocation_create', + mock.Mock(return_values=fake_network_allocation)) + @mock.patch.object(db_api, 'share_network_get', + mock.Mock(return_value=fake_share_network)) + @mock.patch.object(db_api, 'share_server_get', + mock.Mock(return_value=fake_share_server)) + def test_allocate_network_one_allocation(self): + self.mock_object(self.bind_plugin, '_has_provider_network_extension') + self.bind_plugin._has_provider_network_extension.return_value = True + save_nw_data = self.mock_object(self.bind_plugin, + '_save_neutron_network_data') + save_subnet_data = self.mock_object(self.bind_plugin, + '_save_neutron_subnet_data') + self.mock_object(self.bind_plugin, '_wait_for_ports_bind') + neutron_host_id_opts = plugin.neutron_bind_network_plugin_opts[1] + self.mock_object(neutron_host_id_opts, 'default') + neutron_host_id_opts.default = 'foohost1' + self.mock_object(db_api, 'network_allocation_create') + + with mock.patch.object(self.bind_plugin.neutron_api, 'create_port', + mock.Mock(return_value=fake_neutron_port)): + self.bind_plugin.allocate_network( + self.fake_context, + fake_share_server, + fake_share_network, + allocation_info={'count': 1}) + + self.bind_plugin._has_provider_network_extension.assert_any_call() + save_nw_data.assert_called_once_with(self.fake_context, + fake_share_network) + save_subnet_data.assert_called_once_with(self.fake_context, + fake_share_network) + expected_kwargs = { + 'binding:vnic_type': 'normal', + 'host_id': 'foohost1', + 'network_id': fake_share_network['neutron_net_id'], + 'subnet_id': fake_share_network['neutron_subnet_id'], + 'device_owner': 'manila:share', + 'device_id': fake_share_network['id'], + } + self.bind_plugin.neutron_api.create_port.assert_called_once_with( + fake_share_network['project_id'], **expected_kwargs) + db_api.network_allocation_create.assert_called_once_with( + self.fake_context, + fake_network_allocation) + self.bind_plugin._wait_for_ports_bind.assert_not_called() + + def test_update_network_allocation(self): + self.mock_object(self.bind_plugin, '_wait_for_ports_bind') + self.mock_object(db_api, 'network_allocations_get_for_share_server') + db_api.network_allocations_get_for_share_server.return_value = [ + fake_neutron_port] + + self.bind_plugin.update_network_allocation(self.fake_context, + fake_share_server) + + self.bind_plugin._wait_for_ports_bind.assert_called_once_with( + [fake_neutron_port], fake_share_server) + + +class NeutronBindSingleNetworkPluginWithNormalTypeTest(test.TestCase): + def setUp(self): + super(NeutronBindSingleNetworkPluginWithNormalTypeTest, self).setUp() + fake_net_id = 'fake net id' + fake_subnet_id = 'fake subnet id' + config_data = { + 'DEFAULT': { + 'neutron_net_id': fake_net_id, + 'neutron_subnet_id': fake_subnet_id, + 'neutron_vnic_type': 'normal', + } + } + fake_net = {'subnets': ['fake1', 'fake2', fake_subnet_id]} + self.mock_object( + neutron_api.API, 'get_network', mock.Mock(return_value=fake_net)) + self.plugin = plugin.NeutronNetworkPlugin() + self.plugin.db = db_api + self.fake_context = context.RequestContext(user_id='fake user', + project_id='fake project', + is_admin=False) + + with test_utils.create_temp_config_with_opts(config_data): + self.bind_plugin = plugin.NeutronBindSingleNetworkPlugin() + self.bind_plugin.db = db_api + + @mock.patch.object(db_api, 'network_allocation_create', + mock.Mock(return_values=fake_network_allocation)) + @mock.patch.object(db_api, 'share_network_get', + mock.Mock(return_value=fake_share_network)) + @mock.patch.object(db_api, 'share_server_get', + mock.Mock(return_value=fake_share_server)) + def test_allocate_network_one_allocation(self): + self.mock_object(self.bind_plugin, '_has_provider_network_extension') + self.bind_plugin._has_provider_network_extension.return_value = True + save_nw_data = self.mock_object(self.bind_plugin, + '_save_neutron_network_data') + save_subnet_data = self.mock_object(self.bind_plugin, + '_save_neutron_subnet_data') + self.mock_object(self.bind_plugin, '_wait_for_ports_bind') + neutron_host_id_opts = plugin.neutron_bind_network_plugin_opts[1] + self.mock_object(neutron_host_id_opts, 'default') + neutron_host_id_opts.default = 'foohost1' + self.mock_object(db_api, 'network_allocation_create') + + with mock.patch.object(self.bind_plugin.neutron_api, 'create_port', + mock.Mock(return_value=fake_neutron_port)): + self.bind_plugin.allocate_network( + self.fake_context, + fake_share_server, + fake_share_network, + allocation_info={'count': 1}) + + self.bind_plugin._has_provider_network_extension.assert_any_call() + save_nw_data.assert_called_once_with(self.fake_context, + fake_share_network) + save_subnet_data.assert_called_once_with(self.fake_context, + fake_share_network) + expected_kwargs = { + 'binding:vnic_type': 'normal', + 'host_id': 'foohost1', + 'network_id': fake_share_network['neutron_net_id'], + 'subnet_id': fake_share_network['neutron_subnet_id'], + 'device_owner': 'manila:share', + 'device_id': fake_share_network['id'], + } + self.bind_plugin.neutron_api.create_port.assert_called_once_with( + fake_share_network['project_id'], **expected_kwargs) + db_api.network_allocation_create.assert_called_once_with( + self.fake_context, + fake_network_allocation) + self.bind_plugin._wait_for_ports_bind.assert_not_called() + + def test_update_network_allocation(self): + self.mock_object(self.bind_plugin, '_wait_for_ports_bind') + self.mock_object(db_api, 'network_allocations_get_for_share_server') + db_api.network_allocations_get_for_share_server.return_value = [ + fake_neutron_port] + + self.bind_plugin.update_network_allocation(self.fake_context, + fake_share_server) + + self.bind_plugin._wait_for_ports_bind.assert_called_once_with( + [fake_neutron_port], fake_share_server) diff --git a/releasenotes/notes/neutron-binding-driver-43f01565051b031b.yaml b/releasenotes/notes/neutron-binding-driver-43f01565051b031b.yaml new file mode 100644 index 0000000000..4c8b0633f4 --- /dev/null +++ b/releasenotes/notes/neutron-binding-driver-43f01565051b031b.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added neutron driver for port bind actions.