diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 273d49baf..79d3bb8cf 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -155,6 +155,16 @@ function configure_qos { iniset /$Q_PLUGIN_CONF_FILE ml2 extension_drivers "$Q_ML2_PLUGIN_EXT_DRIVERS" } +function configure_bgp { + setup_develop $DEST/neutron-dynamic-routing + sudo install -d -o $STACK_USER $NEUTRON_CONF_DIR/policy.d + cp -v $DEST/neutron-dynamic-routing/etc/neutron/policy.d/dynamic_routing.conf $NEUTRON_CONF_DIR/policy.d + _neutron_service_plugin_class_add df-bgp + # Since we are using a plugin outside neutron-dynamic-routing, we need to + # specify api_extensions_path explicitly. + iniset $NEUTRON_CONF DEFAULT api_extensions_path "$DEST/neutron-dynamic-routing/neutron_dynamic_routing/extensions" +} + function init_neutron_sample_config { # NOTE: We must make sure that neutron config file exists before # going further with ovs setup @@ -182,6 +192,10 @@ function configure_df_plugin { configure_qos fi + if [[ "$DR_MODE" == "df-bgp" ]]; then + configure_bgp + fi + # NOTE(gsagie) needed for tempest export NETWORK_API_EXTENSIONS=$(python -c \ 'from dragonflow.common import extensions ;\ diff --git a/devstack/settings b/devstack/settings index e048c9673..8ba5a2f68 100644 --- a/devstack/settings +++ b/devstack/settings @@ -19,6 +19,8 @@ METADATA_PROXY_SHARED_SECRET=${METADATA_PROXY_SHARED_SECRET:-"secret"} # df-bgp DF_BGP_SERVICE=${DF_BGP_SERVICE:-"$NEUTRON_BIN_DIR/df-bgp-service"} +# This can be overridden in the localrc file +DR_MODE=${DR_MODE:-df-bgp} DF_L2_RESPONDER=${DF_L2_RESPONDER:-'True'} diff --git a/dragonflow/db/neutron/lockedobjects_db.py b/dragonflow/db/neutron/lockedobjects_db.py index b4bfbf3e5..38307c5d3 100644 --- a/dragonflow/db/neutron/lockedobjects_db.py +++ b/dragonflow/db/neutron/lockedobjects_db.py @@ -48,6 +48,8 @@ RESOURCE_FIP_UPDATE_OR_DELETE = 7 RESOURCE_ROUTER_UPDATE_OR_DELETE = 8 RESOURCE_QOS = 9 RESOURCE_NEUTRON_LISTENER = 10 +RESOURCE_BGP_SPEAKER = 11 +RESOURCE_BGP_PEER = 12 LOG = log.getLogger(__name__) @@ -116,6 +118,10 @@ def _get_lock_id_by_resource_type(resource_type, *args, **kwargs): # The db model of lock is uuid of 36 chars, but the neutron listener # uses hostname as lock-id, so we need to truncate it. lock_id = args[0][1][:35] + elif RESOURCE_BGP_SPEAKER == resource_type: + lock_id = args[0][2] + elif RESOURCE_BGP_PEER == resource_type: + lock_id = args[0][2] else: raise df_exc.UnknownResourceException(resource_type=resource_type) diff --git a/dragonflow/neutron/services/bgp/__init__.py b/dragonflow/neutron/services/bgp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dragonflow/neutron/services/bgp/bgp_plugin.py b/dragonflow/neutron/services/bgp/bgp_plugin.py new file mode 100644 index 000000000..9c39c7c0c --- /dev/null +++ b/dragonflow/neutron/services/bgp/bgp_plugin.py @@ -0,0 +1,169 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron_dynamic_routing.db import bgp_db +from neutron_dynamic_routing.extensions import bgp as bgp_ext +from neutron_lib.plugins import directory +from neutron_lib.services import base as service_base +from oslo_log import log as logging + +from dragonflow.db.models import bgp +from dragonflow.db.neutron import lockedobjects_db as lock_db + + +LOG = logging.getLogger(__name__) + + +def bgp_peer_from_neutron_bgp_peer(peer): + return bgp.BGPPeer(id=peer.get('id'), + topic=peer.get('tenant_id'), + name=peer.get('name'), + peer_ip=peer.get('peer_ip'), + remote_as=int(peer.get('remote_as')), + auth_type=peer.get('auth_type'), + password=peer.get('password')) + + +def bgp_speaker_from_neutron_bgp_speaker(speaker): + return bgp.BGPSpeaker(id=speaker.get('id'), + topic=speaker.get('tenant_id'), + name=speaker.get('name'), + local_as=int(speaker.get('local_as')), + peers=speaker.get('peers', []), + ip_version=speaker.get('ip_version')) + + +class DFBgpPlugin(service_base.ServicePluginBase, + bgp_db.BgpDbMixin): + + supported_extension_aliases = [bgp_ext.BGP_EXT_ALIAS] + + def __init__(self): + super(DFBgpPlugin, self).__init__() + self._nb_api = None + self._register_callbacks() + + @property + def nb_api(self): + if self._nb_api is None: + plugin = directory.get_plugin() + mech_driver = plugin.mechanism_manager.mech_drivers['df'].obj + self._nb_api = mech_driver.nb_api + + return self._nb_api + + def get_plugin_name(self): + return bgp_ext.BGP_EXT_ALIAS + '_svc_plugin' + + def get_plugin_type(self): + return bgp_ext.BGP_EXT_ALIAS + + def get_plugin_description(self): + """returns string description of the plugin.""" + return ("BGP dynamic routing service for announcement of next-hops " + "for private networks and floating IP's host routes.") + + def _register_callbacks(self): + # TODO(xiaohhui): Add subscribers to router and floatingip changes. + pass + + @lock_db.wrap_db_lock(lock_db.RESOURCE_DF_PLUGIN) + def create_bgp_speaker(self, context, bgp_speaker): + bgp_speaker = super(DFBgpPlugin, self).create_bgp_speaker(context, + bgp_speaker) + self.nb_api.create(bgp_speaker_from_neutron_bgp_speaker(bgp_speaker), + skip_send_event=True) + return bgp_speaker + + @lock_db.wrap_db_lock(lock_db.RESOURCE_BGP_SPEAKER) + def update_bgp_speaker(self, context, bgp_speaker_id, bgp_speaker): + bgp_speaker = super(DFBgpPlugin, self).update_bgp_speaker( + context, bgp_speaker_id, bgp_speaker) + self.nb_api.update(bgp_speaker_from_neutron_bgp_speaker(bgp_speaker), + skip_send_event=True) + return bgp_speaker + + @lock_db.wrap_db_lock(lock_db.RESOURCE_BGP_SPEAKER) + def delete_bgp_speaker(self, context, bgp_speaker_id): + super(DFBgpPlugin, self).delete_bgp_speaker(context, bgp_speaker_id) + self.nb_api.delete(bgp.BGPSpeaker(id=bgp_speaker_id), + skip_send_event=True) + + @lock_db.wrap_db_lock(lock_db.RESOURCE_DF_PLUGIN) + def create_bgp_peer(self, context, bgp_peer): + bgp_peer = super(DFBgpPlugin, self).create_bgp_peer(context, bgp_peer) + self.nb_api.create(bgp_peer_from_neutron_bgp_peer(bgp_peer), + skip_send_event=True) + return bgp_peer + + @lock_db.wrap_db_lock(lock_db.RESOURCE_BGP_PEER) + def update_bgp_peer(self, context, bgp_peer_id, bgp_peer): + bgp_peer = super(DFBgpPlugin, self).update_bgp_peer(context, + bgp_peer_id, + bgp_peer) + self.nb_api.update(bgp_peer_from_neutron_bgp_peer(bgp_peer), + skip_send_event=True) + return bgp_peer + + @lock_db.wrap_db_lock(lock_db.RESOURCE_BGP_PEER) + def delete_bgp_peer(self, context, bgp_peer_id): + super(DFBgpPlugin, self).delete_bgp_peer(context, bgp_peer_id) + self.nb_api.delete(bgp.BGPPeer(id=bgp_peer_id), + skip_send_event=True) + + @lock_db.wrap_db_lock(lock_db.RESOURCE_BGP_SPEAKER) + def add_bgp_peer(self, context, bgp_speaker_id, bgp_peer_info): + ret_value = super(DFBgpPlugin, self).add_bgp_peer(context, + bgp_speaker_id, + bgp_peer_info) + tenant_id = context.tenant_id + bgp_speaker = self.nb_api.get(bgp.BGPSpeaker(id=bgp_speaker_id, + topic=tenant_id)) + bgp_speaker.peers.append(ret_value['bgp_peer_id']) + self.nb_api.update(bgp_speaker, skip_send_event=True) + return ret_value + + @lock_db.wrap_db_lock(lock_db.RESOURCE_BGP_SPEAKER) + def remove_bgp_peer(self, context, bgp_speaker_id, bgp_peer_info): + ret_value = super(DFBgpPlugin, self).remove_bgp_peer(context, + bgp_speaker_id, + bgp_peer_info) + tenant_id = context.tenant_id + bgp_speaker = self.nb_api.get(bgp.BGPSpeaker(id=bgp_speaker_id, + topic=tenant_id)) + bgp_speaker.remove_peer(ret_value['bgp_peer_id']) + self.nb_api.update(bgp_speaker, skip_send_event=True) + return ret_value + + @lock_db.wrap_db_lock(lock_db.RESOURCE_BGP_SPEAKER) + def add_gateway_network(self, context, bgp_speaker_id, network_info): + ret_value = super(DFBgpPlugin, self).add_gateway_network( + context, bgp_speaker_id, network_info) + # TODO(xiaohhui): Calculate routes for bgp_speaker_id with network. + return ret_value + + @lock_db.wrap_db_lock(lock_db.RESOURCE_BGP_SPEAKER) + def remove_gateway_network(self, context, bgp_speaker_id, network_info): + ret_value = super(DFBgpPlugin, self).remove_gateway_network( + context, bgp_speaker_id, network_info) + + # TODO(xiaohhui): Calculate routes for bgp_speaker_id without network. + return ret_value + + def get_advertised_routes(self, context, bgp_speaker_id): + tenant_id = context.tenant_id + bgp_speaker = self.nb_api.get(bgp.BGPSpeaker(id=bgp_speaker_id, + topic=tenant_id)) + # Translate to the format that neutron will acccept. + return {'advertised_routes': [{'destination': r.destination, + 'next_hop': r.nexthop} + for r in bgp_speaker.routes]} diff --git a/dragonflow/tests/fullstack/test_neutron_api.py b/dragonflow/tests/fullstack/test_neutron_api.py index 75a36f114..6c9d31a8a 100644 --- a/dragonflow/tests/fullstack/test_neutron_api.py +++ b/dragonflow/tests/fullstack/test_neutron_api.py @@ -598,3 +598,53 @@ class TestNeutronAPIandDB(test_base.DFTestBase): self.assertIsNotNone(lport) real_pairs = lport.get_allowed_address_pairs() self.assertItemsEqual(expected_pairs, real_pairs) + + def test_create_delete_bgp_peer(self): + bgp_peer = self.store( + objects.BGPPeerTestObj(self.neutron, self.nb_api)) + bgp_peer.create() + self.assertTrue(bgp_peer.exists()) + bgp_peer.close() + self.assertFalse(bgp_peer.exists()) + + def test_create_delete_bgp_speaker(self): + bgp_speaker = self.store( + objects.BGPSpeakerTestObj(self.neutron, self.nb_api)) + bgp_speaker.create() + self.assertTrue(bgp_speaker.exists()) + bgp_speaker.close() + self.assertFalse(bgp_speaker.exists()) + + def test_add_remove_bgp_peer(self): + bgp_peer = self.store( + objects.BGPPeerTestObj(self.neutron, self.nb_api)) + bgp_speaker = self.store( + objects.BGPSpeakerTestObj(self.neutron, self.nb_api)) + bgp_peer.create() + bgp_speaker.create() + bgp_speaker.add_peer(bgp_peer.peer_id) + nb_bgp_speaker = bgp_speaker.get_nb_bgp_speaker() + peers = [peer.id for peer in nb_bgp_speaker.peers] + self.assertIn(bgp_peer.peer_id, peers) + + bgp_speaker.remove_peer(bgp_peer.peer_id) + nb_bgp_speaker = bgp_speaker.get_nb_bgp_speaker() + peers = [peer.id for peer in nb_bgp_speaker.peers] + self.assertNotIn(bgp_peer.peer_id, nb_bgp_speaker.peers) + + @lockutils.synchronized('need-external-net') + def test_add_remove_bgp_network(self): + bgp_peer = self.store( + objects.BGPPeerTestObj(self.neutron, self.nb_api)) + bgp_speaker = self.store( + objects.BGPSpeakerTestObj(self.neutron, self.nb_api)) + bgp_peer.create() + bgp_speaker.create() + with self._prepare_ext_net() as external_network_id: + bgp_speaker.add_network(external_network_id) + # TODO(xiaohhui): Verify the routes has been added to + # bgp speaker nb db data + + bgp_speaker.remove_network(external_network_id) + nb_bgp_speaker = bgp_speaker.get_nb_bgp_speaker() + self.assertFalse(nb_bgp_speaker.routes) diff --git a/dragonflow/tests/fullstack/test_objects.py b/dragonflow/tests/fullstack/test_objects.py index eda9ba394..7f2b61d50 100644 --- a/dragonflow/tests/fullstack/test_objects.py +++ b/dragonflow/tests/fullstack/test_objects.py @@ -17,6 +17,7 @@ from neutron.agent.common import utils as agent_utils from neutronclient.common import exceptions from oslo_log import log +from dragonflow.db.models import bgp from dragonflow.db.models import l2 from dragonflow.db.models import l3 from dragonflow.db.models import qos @@ -527,3 +528,74 @@ class QosPolicyTestObj(object): if qospolicy: return True return False + + +class BGPPeerTestObj(object): + def __init__(self, neutron, nb_api): + self.peer_id = None + self.neutron = neutron + self.nb_api = nb_api + self.closed = False + + def create(self, bgp_peer={'name': "peer1", + 'peer_ip': "172.24.4.100", + 'remote_as': 4321, + 'auth_type': 'none'}): + bgp_peer = self.neutron.create_bgp_peer({'bgp_peer': bgp_peer}) + self.peer_id = bgp_peer['bgp_peer']['id'] + return self.peer_id + + def close(self): + if self.closed or self.peer_id is None: + return + self.neutron.delete_bgp_peer(self.peer_id) + self.closed = True + + def exists(self): + bgp_peer = self.nb_api.get(bgp.BGPPeer(id=self.peer_id)) + return bool(bgp_peer) + + +class BGPSpeakerTestObj(object): + def __init__(self, neutron, nb_api): + self.speaker_id = None + self.neutron = neutron + self.nb_api = nb_api + self.closed = False + + def create(self, bgp_speaker={'name': 'speaker1', + 'local_as': 1234, + 'ip_version': 4}): + bgp_speaker = self.neutron.create_bgp_speaker( + {'bgp_speaker': bgp_speaker}) + self.speaker_id = bgp_speaker['bgp_speaker']['id'] + return self.speaker_id + + def add_peer(self, peer_id): + self.neutron.add_peer_to_bgp_speaker(self.speaker_id, + {'bgp_peer_id': peer_id}) + + def remove_peer(self, peer_id): + self.neutron.remove_peer_from_bgp_speaker(self.speaker_id, + {'bgp_peer_id': peer_id}) + + def add_network(self, network_id): + self.neutron.add_network_to_bgp_speaker(self.speaker_id, + {'network_id': network_id}) + + def remove_network(self, network_id): + self.neutron.remove_network_from_bgp_speaker( + self.speaker_id, {'network_id': network_id}) + + def close(self): + if self.closed or self.speaker_id is None: + return + self.neutron.delete_bgp_speaker(self.speaker_id) + self.closed = True + + def get_nb_bgp_speaker(self): + return self.nb_api.get(bgp.BGPSpeaker(id=self.speaker_id)) + + def exists(self): + bgp_speaker = self.nb_api.get(bgp.BGPSpeaker(id=self.speaker_id)) + return bool(bgp_speaker) diff --git a/setup.cfg b/setup.cfg index bc1f868ea..236cb0d39 100644 --- a/setup.cfg +++ b/setup.cfg @@ -72,5 +72,6 @@ dragonflow.port_status_driver = redis_port_status_notifier_driver = dragonflow.db.pubsub_drivers.redis_port_status_notifier:RedisPortStatusNotifier neutron.service_plugins = df-l3 = dragonflow.neutron.services.l3_router_plugin:DFL3RouterPlugin + df-bgp = dragonflow.neutron.services.bgp.bgp_plugin:DFBgpPlugin oslo.config.opts = dragonflow.conf = dragonflow.conf.opts:list_opts