diff --git a/neutron_dynamic_routing/db/bgp_db.py b/neutron_dynamic_routing/db/bgp_db.py index c136f499..26a6954a 100644 --- a/neutron_dynamic_routing/db/bgp_db.py +++ b/neutron_dynamic_routing/db/bgp_db.py @@ -69,6 +69,25 @@ class BgpSpeakerPeerBinding(model_base.BASEV2): primary_key=True) +class BgpSpeakerRouterBinding(model_base.BASEV2): + + """Represents a mapping between a router and BGP speaker""" + + __tablename__ = 'bgp_speaker_router_bindings' + + bgp_speaker_id = sa.Column(sa.String(length=36), + sa.ForeignKey('bgp_speakers.id', + ondelete='CASCADE'), + nullable=False, + primary_key=True) + router_id = sa.Column(sa.String(length=36), + sa.ForeignKey('routers.id', + ondelete='CASCADE'), + nullable=False, + primary_key=True) + redistribute_static = sa.Column(sa.Boolean, nullable=False) + + class BgpSpeakerNetworkBinding(model_base.BASEV2): """Represents a mapping between a network and BGP speaker""" @@ -109,6 +128,10 @@ class BgpSpeaker(model_base.BASEV2, backref='bgp_speaker_network_bindings', cascade='all, delete, delete-orphan', lazy='joined') + routers = orm.relationship(BgpSpeakerRouterBinding, + backref='bgp_speaker_router_bindings', + cascade='all, delete, delete-orphan', + lazy='joined') ip_version = sa.Column(sa.Integer, nullable=False, autoincrement=False) @@ -221,6 +244,26 @@ class BgpDbMixin(object): network_id) return {'network_id': network_id} + def add_gateway_router(self, context, bgp_speaker_id, router_assoc_info): + router_id = self._get_id_for(router_assoc_info, 'router_id') + with db_api.CONTEXT_WRITER.using(context): + try: + self._save_bgp_speaker_router_binding(context, + bgp_speaker_id, + router_assoc_info) + except oslo_db_exc.DBDuplicateEntry: + raise bgp_ext.BgpSpeakerRouterBindingError( + router_id=router_id, + bgp_speaker_id=bgp_speaker_id) + return {'router_id': router_id} + + def remove_gateway_router(self, context, bgp_speaker_id, router_info): + router_id = self._get_id_for(router_info, 'router_id') + with db_api.CONTEXT_WRITER.using(context): + self._remove_bgp_speaker_router_binding(context, bgp_speaker_id, + router_id) + return {'router_id': router_id} + def delete_bgp_speaker(self, context, bgp_speaker_id): with db_api.CONTEXT_WRITER.using(context): bgp_speaker_db = self._get_bgp_speaker(context, bgp_speaker_id) @@ -314,6 +357,10 @@ class BgpDbMixin(object): routes = self.get_routes_by_bgp_speaker_id(context, bgp_speaker_id) return self._make_advertised_routes_dict(routes) + def get_routes(self, context, bgp_speaker_id): + """TODO: Add implementation to list advertised and learnt routes""" + pass + def _get_id_for(self, resource, id_name): try: uuid = resource[id_name] @@ -415,15 +462,51 @@ class BgpDbMixin(object): bgp_speaker_id=bgp_speaker_id) context.session.delete(binding) + def _save_bgp_speaker_router_binding(self, context, bgp_speaker_id, + router_assoc_info): + router_id = self._get_id_for(router_assoc_info, 'router_id') + red_static = router_assoc_info.get('redistribute_static', False) + with db_api.CONTEXT_WRITER.using(context): + try: + model_query.get_by_id(context, BgpSpeaker, bgp_speaker_id) + except sa_exc.NoResultFound: + raise bgp_ext.BgpSpeakerNotFound(id=bgp_speaker_id) + + try: + model_query.get_by_id(context, l3_db.Router, router_id) + except sa_exc.NoResultFound: + raise l3_exc.RouterNotFound(router_id=router_id) + + binding = BgpSpeakerRouterBinding(bgp_speaker_id=bgp_speaker_id, + router_id=router_id, + redistribute_static=red_static) + context.session.add(binding) + + def _remove_bgp_speaker_router_binding(self, context, + bgp_speaker_id, router_id): + with db_api.CONTEXT_WRITER.using(context): + + try: + binding = self._get_bgp_speaker_router_binding(context, + bgp_speaker_id, + router_id) + except sa_exc.NoResultFound: + raise bgp_ext.BgpSpeakerRouterNotAssociated( + router_id=router_id, + bgp_speaker_id=bgp_speaker_id) + context.session.delete(binding) + def _make_bgp_speaker_dict(self, bgp_speaker, fields=None): attrs = {'id', 'local_as', 'tenant_id', 'name', 'ip_version', 'advertise_floating_ip_host_routes', 'advertise_tenant_networks'} peer_bindings = bgp_speaker['peers'] network_bindings = bgp_speaker['networks'] + router_bindings = bgp_speaker['routers'] res = dict((k, bgp_speaker[k]) for k in attrs) res['peers'] = [x.bgp_peer_id for x in peer_bindings] res['networks'] = [x.network_id for x in network_bindings] + res['routers'] = [x.router_id for x in router_bindings] return db_utils.resource_fields(res, fields) def _make_advertised_routes_dict(self, routes): @@ -449,6 +532,13 @@ class BgpDbMixin(object): BgpSpeakerNetworkBinding.bgp_speaker_id == bgp_speaker_id, BgpSpeakerNetworkBinding.network_id == network_id).one() + def _get_bgp_speaker_router_binding(self, context, + bgp_speaker_id, router_id): + query = model_query.query_with_hooks(context, BgpSpeakerRouterBinding) + return query.filter( + BgpSpeakerRouterBinding.bgp_speaker_id == bgp_speaker_id, + BgpSpeakerRouterBinding.router_id == router_id).one() + def _make_bgp_peer_dict(self, bgp_peer, fields=None): attrs = ['tenant_id', 'id', 'name', 'peer_ip', 'remote_as', 'auth_type', 'password'] diff --git a/neutron_dynamic_routing/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron_dynamic_routing/db/migration/alembic_migrations/versions/EXPAND_HEAD index 6177b331..ce4634aa 100644 --- a/neutron_dynamic_routing/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron_dynamic_routing/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -f399fa0f5f25 +738d0ae1984e diff --git a/neutron_dynamic_routing/db/migration/alembic_migrations/versions/xena/expand/738d0ae1984e_bgpaas_enh.py b/neutron_dynamic_routing/db/migration/alembic_migrations/versions/xena/expand/738d0ae1984e_bgpaas_enh.py new file mode 100644 index 00000000..60677e1c --- /dev/null +++ b/neutron_dynamic_routing/db/migration/alembic_migrations/versions/xena/expand/738d0ae1984e_bgpaas_enh.py @@ -0,0 +1,42 @@ +# Copyright 2016 Huawei Technologies India Pvt Limited. +# +# 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. +# + +"""bgpaas enh + +Revision ID: 738d0ae1984e +Revises: f399fa0f5f25 +Create Date: 2021-05-21 10:25:57.492514 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '738d0ae1984e' +down_revision = 'f399fa0f5f25' + + +def upgrade(): + op.create_table( + 'bgp_speaker_router_bindings', + sa.Column('bgp_speaker_id', sa.String(length=36), + sa.ForeignKey('bgp_speakers.id', ondelete='CASCADE'), + nullable=False, primary_key=True), + sa.Column('router_id', sa.String(length=36), + sa.ForeignKey('routers.id', ondelete='CASCADE'), + nullable=False, primary_key=True), + sa.Column('redistribute_static', sa.Boolean, nullable=False), + ) diff --git a/neutron_dynamic_routing/extensions/bgp.py b/neutron_dynamic_routing/extensions/bgp.py index 416b7b46..0beeca42 100644 --- a/neutron_dynamic_routing/extensions/bgp.py +++ b/neutron_dynamic_routing/extensions/bgp.py @@ -145,6 +145,16 @@ class BgpSpeakerNetworkBindingError(n_exc.Conflict): "%(bgp_speaker_id)s.") +class BgpSpeakerRouterNotAssociated(n_exc.NotFound): + message = _("Router %(router_id)s is not associated with " + "BGP speaker %(bgp_speaker_id)s.") + + +class BgpSpeakerRouterBindingError(n_exc.Conflict): + message = _("Router %(router_id)s is already bound to BgpSpeaker " + "%(bgp_speaker_id)s.") + + class NetworkNotBound(n_exc.NotFound): message = _("Network %(network_id)s is not bound to a BgpSpeaker.") diff --git a/neutron_dynamic_routing/policies/bgp_speaker.py b/neutron_dynamic_routing/policies/bgp_speaker.py index cc72468e..3297a341 100644 --- a/neutron_dynamic_routing/policies/bgp_speaker.py +++ b/neutron_dynamic_routing/policies/bgp_speaker.py @@ -109,6 +109,39 @@ rules = [ }, ] ), + policy.DocumentedRuleDefault( + 'add_gateway_router', + base.RULE_ADMIN_ONLY, + 'Add a gateway router to a BGP speaker', + [ + { + 'method': 'PUT', + 'path': '/bgp-speakers/{id}/add_gateway_router', + }, + ] + ), + policy.DocumentedRuleDefault( + 'remove_gateway_router', + base.RULE_ADMIN_ONLY, + 'Remove a gateway router from a BGP speaker', + [ + { + 'method': 'PUT', + 'path': '/bgp-speakers/{id}/remove_gateway_router', + }, + ] + ), + policy.DocumentedRuleDefault( + 'get_routes', + base.RULE_ADMIN_ONLY, + 'Get advertised and learned routes of a BGP speaker', + [ + { + 'method': 'GET', + 'path': '/bgp-speakers/{id}/get_routes', + }, + ] + ), policy.DocumentedRuleDefault( 'get_advertised_routes', base.RULE_ADMIN_ONLY, diff --git a/neutron_dynamic_routing/services/bgp/bgp_plugin.py b/neutron_dynamic_routing/services/bgp/bgp_plugin.py index 20e56321..ede271a9 100644 --- a/neutron_dynamic_routing/services/bgp/bgp_plugin.py +++ b/neutron_dynamic_routing/services/bgp/bgp_plugin.py @@ -220,6 +220,20 @@ class BgpPlugin(service_base.ServicePluginBase, bgp_speaker_id, network_info) + def add_gateway_router(self, context, bgp_speaker_id, router_info): + return super(BgpPlugin, self).add_gateway_router(context, + bgp_speaker_id, + router_info) + + def remove_gateway_router(self, context, bgp_speaker_id, router_info): + return super(BgpPlugin, self).remove_gateway_router(context, + bgp_speaker_id, + router_info) + + def get_routes(self, context, bgp_speaker_id): + return super(BgpPlugin, self).get_routes(context, + bgp_speaker_id) + def get_advertised_routes(self, context, bgp_speaker_id): return super(BgpPlugin, self).get_advertised_routes(context, bgp_speaker_id) diff --git a/neutron_dynamic_routing/tests/unit/db/test_bgp_db.py b/neutron_dynamic_routing/tests/unit/db/test_bgp_db.py index 71c7579c..d2e57149 100644 --- a/neutron_dynamic_routing/tests/unit/db/test_bgp_db.py +++ b/neutron_dynamic_routing/tests/unit/db/test_bgp_db.py @@ -27,6 +27,7 @@ from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory from oslo_config import cfg from oslo_utils import uuidutils +from sqlalchemy.orm import exc as sa_exc from neutron_dynamic_routing.extensions import bgp from neutron_dynamic_routing.services.bgp import bgp_plugin @@ -48,7 +49,7 @@ class BgpEntityCreationMixin(object): def bgp_speaker(self, ip_version, local_as, name='my-speaker', advertise_fip_host_routes=True, advertise_tenant_networks=True, - networks=None, peers=None): + networks=None, peers=None, router_id=None): data = {'ip_version': ip_version, ADVERTISE_FIPS_KEY: advertise_fip_host_routes, 'advertise_tenant_networks': advertise_tenant_networks, @@ -67,6 +68,9 @@ class BgpEntityCreationMixin(object): for peer_id in peers: self.bgp_plugin.add_bgp_peer(self.context, bgp_speaker_id, {'bgp_peer_id': peer_id}) + if router_id: + self.bgp_plugin.add_gateway_router(self.context, bgp_speaker_id, + {'router_id': router_id}) yield self.bgp_plugin.get_bgp_speaker(self.context, bgp_speaker_id) @@ -487,6 +491,59 @@ class BgpTests(BgpEntityCreationMixin): self.assertEqual(network_id, speaker['networks'][0]) + def test_add_gateway_router(self): + tenant_id = _uuid() + scope_data = {'tenant_id': tenant_id, 'ip_version': 4, + 'shared': True, 'name': 'bgp-scope'} + scope = self.plugin.create_address_scope( + self.context, + {'address_scope': scope_data}) + with self.router_with_external_and_tenant_networks( + tenant_id=tenant_id, + address_scope=scope) as res,\ + self.bgp_speaker(scope_data['ip_version'], 1234) as speaker: + router, *_ = res + router_id = router['id'] + self.bgp_plugin.add_gateway_router( + self.context, + speaker['id'], + {'router_id': router_id, + 'redistribute_static': False}) + new_speaker = self.bgp_plugin.get_bgp_speaker(self.context, + speaker['id']) + router_binding = self.bgp_plugin._get_bgp_speaker_router_binding( + self.context, speaker['id'], + router_id) + self.assertEqual(1, len(new_speaker['routers'])) + self.assertTrue(router_id in new_speaker['routers']) + self.assertIsNotNone(router_binding.bgp_speaker_id) + + def test_remove_gateway_router(self): + tenant_id = _uuid() + scope_data = {'tenant_id': tenant_id, 'ip_version': 4, + 'shared': True, 'name': 'bgp-scope'} + scope = self.plugin.create_address_scope( + self.context, + {'address_scope': scope_data}) + with self.router_with_external_and_tenant_networks( + tenant_id=tenant_id, + address_scope=scope) as res: + router, *_ = res + router_id = router['id'] + with self.bgp_speaker(scope_data['ip_version'], 1234, + router_id=router_id) as speaker: + self.bgp_plugin.remove_gateway_router( + self.context, + speaker['id'], + {'router_id': router_id}) + new_speaker = self.bgp_plugin.get_bgp_speaker(self.context, + speaker['id']) + self.assertEqual(0, len(new_speaker['routers'])) + self.assertRaises( + sa_exc.NoResultFound, + self.bgp_plugin._get_bgp_speaker_router_binding, + self.context, speaker['id'], router_id) + def test_create_bgp_peer_md5_auth_no_password(self): bgp_peer = {'bgp_peer': {'auth_type': 'md5', 'password': None}} self.assertRaises(bgp.InvalidBgpPeerMd5Authentication,