From: Kevin Benton Date: Wed, 13 Aug 2014 02:02:51 +0000 (-0700) Subject: Big Switch: Bind IVS ports in ML2 driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=4c4dbd4e411b394ddc5f4b6ea9bd455d7a319722;p=openstack-build%2Fneutron-build.git Big Switch: Bind IVS ports in ML2 driver Add support to bind IVS ports in the Big Switch ML2 mechanism driver. The backend controller will be checked to determine if a host is connected using the Indigo vswitch. If so, the mechanism driver will mark it as bound since it will be provisioned by the backend controller. Implements: blueprint bsn-ml2-bind-ivs Change-Id: Ic481fc31c8c123899fddf8185c32f127dff53b7a --- diff --git a/neutron/plugins/bigswitch/servermanager.py b/neutron/plugins/bigswitch/servermanager.py index bc070c2e7..ebb2f543f 100644 --- a/neutron/plugins/bigswitch/servermanager.py +++ b/neutron/plugins/bigswitch/servermanager.py @@ -64,6 +64,7 @@ ROUTERS_PATH = "/tenants/%s/routers/%s" ROUTER_INTF_PATH = "/tenants/%s/routers/%s/interfaces/%s" TOPOLOGY_PATH = "/topology" HEALTH_PATH = "/health" +SWITCHES_PATH = "/switches/%s" SUCCESS_CODES = range(200, 207) FAILURE_CODES = [0, 301, 302, 303, 400, 401, 403, 404, 500, 501, 502, 503, 504, 505] @@ -568,6 +569,11 @@ class ServerPool(object): errstr = _("Unable to delete floating IP: %s") self.rest_action('DELETE', resource, errstr=errstr) + def rest_get_switch(self, switch_id): + resource = SWITCHES_PATH % switch_id + errstr = _("Unable to retrieve switch: %s") + return self.rest_action('GET', resource, errstr=errstr) + def _consistency_watchdog(self, polling_interval=60): if 'consistency' not in self.get_capabilities(): LOG.warning(_("Backend server(s) do not support automated " diff --git a/neutron/plugins/ml2/drivers/mech_bigswitch/driver.py b/neutron/plugins/ml2/drivers/mech_bigswitch/driver.py index 259b14673..75f25cea9 100644 --- a/neutron/plugins/ml2/drivers/mech_bigswitch/driver.py +++ b/neutron/plugins/ml2/drivers/mech_bigswitch/driver.py @@ -16,6 +16,7 @@ # @author: Sumit Naiksatam, sumitnaiksatam@gmail.com, Big Switch Networks, Inc. # @author: Kevin Benton, Big Switch Networks, Inc. import copy +import datetime import httplib import eventlet @@ -25,14 +26,19 @@ from neutron import context as ctx from neutron.extensions import portbindings from neutron.openstack.common import excutils from neutron.openstack.common import log +from neutron.openstack.common import timeutils from neutron.plugins.bigswitch import config as pl_config from neutron.plugins.bigswitch import plugin from neutron.plugins.bigswitch import servermanager +from neutron.plugins.common import constants as pconst from neutron.plugins.ml2 import driver_api as api LOG = log.getLogger(__name__) +# time in seconds to maintain existence of vswitch response +CACHE_VSWITCH_TIME = 60 + class BigSwitchMechanismDriver(plugin.NeutronRestProxyV2Base, api.MechanismDriver): @@ -59,6 +65,9 @@ class BigSwitchMechanismDriver(plugin.NeutronRestProxyV2Base, 'get_floating_ips': False, 'get_routers': False} self.segmentation_types = ', '.join(cfg.CONF.ml2.type_drivers) + # Track hosts running IVS to avoid excessive calls to the backend + self.ivs_host_cache = {} + LOG.debug(_("Initialization done")) def create_network_postcommit(self, context): @@ -126,3 +135,57 @@ class BigSwitchMechanismDriver(plugin.NeutronRestProxyV2Base, # the host_id set return False return prepped_port + + def bind_port(self, context): + if not self.does_vswitch_exist(context.host): + # this is not an IVS host + return + + # currently only vlan segments are supported + for segment in context.network.network_segments: + if segment[api.NETWORK_TYPE] == pconst.TYPE_VLAN: + context.set_binding(segment[api.ID], portbindings.VIF_TYPE_IVS, + {portbindings.CAP_PORT_FILTER: True, + portbindings.OVS_HYBRID_PLUG: False}) + + def does_vswitch_exist(self, host): + """Check if Indigo vswitch exists with the given hostname. + + Returns True if switch exists on backend. + Returns False if switch does not exist. + Returns None if backend could not be reached. + Caches response from backend. + """ + try: + return self._get_cached_vswitch_existence(host) + except ValueError: + # cache was empty for that switch or expired + pass + + try: + self.servers.rest_get_switch(host) + exists = True + except servermanager.RemoteRestError as e: + if e.status == 404: + exists = False + else: + # Another error, return without caching to try again on + # next binding + return + self.ivs_host_cache[host] = { + 'timestamp': datetime.datetime.now(), + 'exists': exists + } + return exists + + def _get_cached_vswitch_existence(self, host): + """Returns cached existence. Old and non-cached raise ValueError.""" + entry = self.ivs_host_cache.get(host) + if not entry: + raise ValueError(_('No cache entry for host %s') % host) + diff = timeutils.delta_seconds(entry['timestamp'], + datetime.datetime.now()) + if diff > CACHE_VSWITCH_TIME: + self.ivs_host_cache.pop(host) + raise ValueError(_('Expired cache entry for host %s') % host) + return entry['exists'] diff --git a/neutron/tests/unit/ml2/drivers/test_bigswitch_mech.py b/neutron/tests/unit/ml2/drivers/test_bigswitch_mech.py index b3536c856..2dbe2a6d6 100644 --- a/neutron/tests/unit/ml2/drivers/test_bigswitch_mech.py +++ b/neutron/tests/unit/ml2/drivers/test_bigswitch_mech.py @@ -15,6 +15,8 @@ # limitations under the License. import contextlib +import functools + import mock import webob.exc @@ -87,6 +89,58 @@ class TestBigSwitchMechDriverPortsV2(test_db_plugin.TestPortsV2, raise webob.exc.HTTPClientError(code=res.status_int) return self.deserialize(fmt, res) + def test_bind_ivs_port(self): + host_arg = {portbindings.HOST_ID: 'hostname'} + with contextlib.nested( + mock.patch(SERVER_POOL + '.rest_get_switch', return_value=True), + self.port(arg_list=(portbindings.HOST_ID,), **host_arg) + ) as (rmock, port): + rmock.assert_called_once_with('hostname') + p = port['port'] + self.assertEqual('ACTIVE', p['status']) + self.assertEqual('hostname', p[portbindings.HOST_ID]) + self.assertEqual(portbindings.VIF_TYPE_IVS, + p[portbindings.VIF_TYPE]) + + def test_dont_bind_non_ivs_port(self): + host_arg = {portbindings.HOST_ID: 'hostname'} + with contextlib.nested( + mock.patch(SERVER_POOL + '.rest_get_switch', + side_effect=servermanager.RemoteRestError( + reason='No such switch', status=404)), + self.port(arg_list=(portbindings.HOST_ID,), **host_arg) + ) as (rmock, port): + rmock.assert_called_once_with('hostname') + p = port['port'] + self.assertNotEqual(portbindings.VIF_TYPE_IVS, + p[portbindings.VIF_TYPE]) + + def test_bind_port_cache(self): + with contextlib.nested( + self.subnet(), + mock.patch(SERVER_POOL + '.rest_get_switch', return_value=True) + ) as (sub, rmock): + makeport = functools.partial(self.port, **{ + 'subnet': sub, 'arg_list': (portbindings.HOST_ID,), + portbindings.HOST_ID: 'hostname'}) + + with contextlib.nested(makeport(), makeport(), + makeport()) as ports: + # response from first should be cached + rmock.assert_called_once_with('hostname') + for port in ports: + self.assertEqual(portbindings.VIF_TYPE_IVS, + port['port'][portbindings.VIF_TYPE]) + rmock.reset_mock() + # expired cache should result in new calls + mock.patch(DRIVER_MOD + '.CACHE_VSWITCH_TIME', new=0).start() + with contextlib.nested(makeport(), makeport(), + makeport()) as ports: + self.assertEqual(3, rmock.call_count) + for port in ports: + self.assertEqual(portbindings.VIF_TYPE_IVS, + port['port'][portbindings.VIF_TYPE]) + def test_create404_triggers_background_sync(self): # allow the async background thread to run for this test self.spawn_p.stop()