diff --git a/neutron_dynamic_routing/api/rpc/agentnotifiers/l3_bgp_rpc_agent_api.py b/neutron_dynamic_routing/api/rpc/agentnotifiers/l3_bgp_rpc_agent_api.py new file mode 100644 index 00000000..68f0e14a --- /dev/null +++ b/neutron_dynamic_routing/api/rpc/agentnotifiers/l3_bgp_rpc_agent_api.py @@ -0,0 +1,119 @@ +# 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.api.rpc.callbacks import events as rpc_events +from neutron.api.rpc.callbacks import resources as resources_registry +from neutron.api.rpc.handlers import resources_rpc + +from neutron_dynamic_routing.objects import bgp_associations as assoc_objects + + +class BgpL3AgentNotifyApi(object): + """API for plugin to notify BGP L3Agent. + + This class implements the client side of an rpc interface. The server side + is neutron_dynamic_routing.services.bgp.agent.l3.bgp_extension. For + more information about rpc interfaces, please see + https://docs.openstack.org/neutron/latest/contributor/internals/rpc_api.html. + """ + + def __init__(self): + self._register_resources() + self.push_api = resources_rpc.ResourcesPushRpcApi() + + def _register_resources(self): + resources_registry.register_resource_class( + assoc_objects.BgpSpeakerRouterAssociation) + resources_registry.register_resource_class( + assoc_objects.BgpSpeakerPeerAssociation) + + def bgp_peer_association_created(self, context, bgp_speaker_id, + assoc_info): + """Tell L3 agent about a BGP Peer Association. + This effectively tells the BgpL3Agent to start a peering session. + """ + peer_assoc_obj = assoc_objects.BgpSpeakerPeerAssociation( + context, + **assoc_info) + self.push_api.push(context, [peer_assoc_obj], rpc_events.CREATED) + + def bgp_peer_association_deleted(self, context, bgp_speaker_id, + assoc_info): + """Tell L3 agent about a BGP Peer disassociation. + This effectively tells the BgpL3Agent to stop a peering session. + """ + peer_assoc_obj = assoc_objects.BgpSpeakerPeerAssociation(context, + **assoc_info) + self.push_api.push(context, [peer_assoc_obj], rpc_events.DELETED) + + def bgp_router_association_created(self, context, bgp_speaker_id, + assoc_info): + """Tell L3 agent about a BGP Router Association. + This effectively tells the BgpL3Agent to start a speaker inside + router namespace. + """ + router_assoc_obj = assoc_objects.BgpSpeakerRouterAssociation( + context, + **assoc_info) + self.push_api.push(context, [router_assoc_obj], rpc_events.CREATED) + + def bgp_router_association_updated(self, context, bgp_speaker_id, + assoc_info): + """Tell L3 agent about a BGP Router Association updation.""" + router_assoc_obj = assoc_objects.BgpSpeakerRouterAssociation( + context, + **assoc_info) + self.push_api.push(context, [router_assoc_obj], rpc_events.UPDATED) + + def bgp_router_association_deleted(self, context, bgp_speaker_id, + assoc_info): + """Tell L3 agent about a BGP Router disassociation. + This effectively tells the BgpL3Agent to stop the speaker inside + router namespace. + """ + router_assoc_obj = assoc_objects.BgpSpeakerRouterAssociation( + context, + **assoc_info) + self.push_api.push(context, [router_assoc_obj], rpc_events.DELETED) + + def agent_updated(self, context, admin_state_up, host): + pass + + def bgp_routes_advertisement(self, context, bgp_speaker_id, + routes, host): + """Currently this is handled by L3 agent itself on router and peer + association. + This can be used in future to support FIP and other scenarios for + router attached speakers. + """ + + def bgp_routes_withdrawal(self, context, bgp_speaker_id, + routes, host): + pass + + def bgp_speaker_created(self, context, bgp_speaker_id, host): + """BGP speaker is currently created whenever router is associated + to BGP speaker. + """ + + def bgp_speaker_removed(self, context, bgp_speaker_id, host): + pass + + def bgp_peer_disassociated(self, context, bgp_speaker_id, + bgp_peer_ip, host=None): + """Currently router attached speakers only support peer association + objects. But this can be enhanced in future + """ + + def bgp_peer_associated(self, context, bgp_speaker_id, + bgp_peer_id, host=None): + pass diff --git a/neutron_dynamic_routing/api/rpc/handlers/bgp_speaker_rpc.py b/neutron_dynamic_routing/api/rpc/handlers/bgp_speaker_rpc.py index 3eed72e5..d764db40 100644 --- a/neutron_dynamic_routing/api/rpc/handlers/bgp_speaker_rpc.py +++ b/neutron_dynamic_routing/api/rpc/handlers/bgp_speaker_rpc.py @@ -83,3 +83,15 @@ class BgpSpeakerRpcCallback(object): """Returns the list of BGP speakers to which the router is associated """ return self.plugin.get_speaker_associated_to_router(context, router_id) + + def update_speaker_router_association_status(self, context, assoc_id, + status): + """Updates the status of router association""" + self.plugin.update_speaker_router_association_status(context, + assoc_id, status) + + def update_speaker_peer_association_status(self, context, assoc_id, + status): + """Updates the status of peer association""" + self.plugin.update_speaker_peer_association_status(context, assoc_id, + status) diff --git a/neutron_dynamic_routing/db/bgp_db.py b/neutron_dynamic_routing/db/bgp_db.py index a3bd0b15..50bf4b16 100644 --- a/neutron_dynamic_routing/db/bgp_db.py +++ b/neutron_dynamic_routing/db/bgp_db.py @@ -21,6 +21,8 @@ from neutron.db.models import address_scope as address_scope_db from neutron.db.models import l3 as l3_db from neutron.db.models import l3_attrs as l3_attrs_db from neutron.db import models_v2 +from neutron.objects import agent as ag_obj +from neutron.objects import l3agent as rb_obj from neutron.objects import ports from neutron.objects import subnet as subnet_obj from neutron.objects import subnetpool as subnetpool_obj @@ -81,6 +83,7 @@ class BgpSpeakerRouterAssociation(model_base.BASEV2, __tablename__ = 'bgp_speaker_router_associations' + name = sa.Column(sa.String(255), nullable=False) bgp_speaker_id = sa.Column(sa.String(length=36), sa.ForeignKey('bgp_speakers.id'), nullable=False) @@ -99,6 +102,7 @@ class BgpSpeakerPeerAssociation(model_base.BASEV2, __tablename__ = 'bgp_speaker_peer_associations' + name = sa.Column(sa.String(255), nullable=False) bgp_speaker_id = sa.Column(sa.String(length=36), sa.ForeignKey('bgp_speakers.id'), nullable=False) @@ -323,7 +327,7 @@ class BgpDbMixin(object): except sa_exc.NoResultFound: raise l3_exc.RouterNotFound(router_id=router_id) - res_keys = ['bgp_speaker_id', 'tenant_id', 'router_id', + res_keys = ['name', 'bgp_speaker_id', 'tenant_id', 'router_id', 'advertise_extra_routes', 'status'] res = dict((k, assoc_info[k]) for k in res_keys) self._validate_router_association(context, bgp_speaker_id, @@ -344,6 +348,13 @@ class BgpDbMixin(object): return self._make_bgp_speaker_router_association_dict( speaker_router_assoc_db) + def update_speaker_router_association_status(self, context, id, status): + assoc_info = {} + assoc_info['status'] = status + with db_api.CONTEXT_WRITER.using(context): + assoc_db = self._get_bgp_speaker_router_association(context, id) + assoc_db.update(assoc_info) + def delete_bgp_speaker_router_association(self, context, id): with db_api.CONTEXT_WRITER.using(context): speaker_router_assoc_db = \ @@ -388,7 +399,8 @@ class BgpDbMixin(object): except sa_exc.NoResultFound: raise bgp_ext.BgpPeerNotFound(id=peer_id) - res_keys = ['bgp_speaker_id', 'tenant_id', 'peer_id', 'status'] + res_keys = ['name', 'bgp_speaker_id', 'tenant_id', 'peer_id', + 'status'] res = dict((k, assoc_info[k]) for k in res_keys) self._validate_peer_association(context, bgp_speaker_id, peer) res['id'] = uuidutils.generate_uuid() @@ -397,6 +409,13 @@ class BgpDbMixin(object): return self._make_bgp_speaker_peer_association_dict( speaker_peer_assoc_db) + def update_speaker_peer_association_status(self, context, id, status): + assoc_info = {} + assoc_info['status'] = status + with db_api.CONTEXT_WRITER.using(context): + assoc_db = self._get_bgp_speaker_peer_association(context, id) + assoc_db.update(assoc_info) + def delete_bgp_speaker_peer_association(self, context, id): with db_api.CONTEXT_WRITER.using(context): assoc_db = self._get_bgp_speaker_peer_association(context, id) @@ -506,8 +525,13 @@ class BgpDbMixin(object): 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 + assocs = self._get_bgp_speaker_router_association_by_speaker_id( + context, bgp_speaker_id) + routes = [] + for assoc in assocs: + routes.extend(self._get_routes_by_router_association( + context, assoc['router_id'], bgp_speaker_id)) + return {'routes': self._prefix_list_from_route_tuples(routes)} def get_speaker_associated_to_router(self, context, router_id): return self._get_bgp_speaker_router_association_by_router_id(context, @@ -551,6 +575,7 @@ class BgpDbMixin(object): models_v2.SubnetPool.address_scope_id == address_scope.id, BgpSpeaker.id == bgp_speaker_id, BgpSpeaker.ip_version == address_scope.ip_version, + BgpSpeaker.advertise_tenant_networks == sa.sql.true(), models_v2.Subnet.ip_version == address_scope.ip_version) return tenant_router_query.all() @@ -785,6 +810,16 @@ class BgpDbMixin(object): raise bgp_asso_ext.DuplicateBgpSpeakerPeerAssociation( bgp_speaker_id=speaker_id, peer_id=new_peer.id) + # Do not allow peer association if network is already associated to + # BGP speaker. Currently association objects are only supported with + # router attached BGP speakers. + network_bindings = self._get_bgp_speaker_network_binding_by_speaker_id( + context, + speaker_id) + if len(network_bindings) == 1: + raise bgp_asso_ext.InvalidBgpSpeakerAssociation( + bgp_speaker_id=speaker_id, + network_id=network_bindings[0]['network_id']) # Do not allow peer association if network is already associated to # BGP speaker. Currently association objects are only supported with @@ -806,14 +841,15 @@ class BgpDbMixin(object): def _make_bgp_speaker_router_association_dict(self, router_association, fields=None): - attrs = ['id', 'tenant_id', 'router_id', 'bgp_speaker_id', + attrs = ['id', 'name', 'tenant_id', 'router_id', 'bgp_speaker_id', 'advertise_extra_routes', 'status'] res = dict((k, router_association[k]) for k in attrs) return db_utils.resource_fields(res, fields) def _make_bgp_speaker_peer_association_dict(self, peer_association, fields=None): - attrs = ['id', 'tenant_id', 'peer_id', 'bgp_speaker_id', 'status'] + attrs = ['id', 'name', 'tenant_id', 'peer_id', 'bgp_speaker_id', + 'status'] res = dict((k, peer_association[k]) for k in attrs) return db_utils.resource_fields(res, fields) @@ -1570,3 +1606,13 @@ class BgpDbMixin(object): ext_nets.append(net) return ext_nets + + def get_l3_agent_hosting_router(self, context, router_id): + with db_api.CONTEXT_READER.using(context): + record_objs = rb_obj.RouterL3AgentBinding.get_objects( + context, router_id=[router_id]) + l3_agents = [ + ag_obj.Agent.get_object(context, id=obj.l3_agent_id) + for obj in record_objs + ] + return l3_agents[0] diff --git a/neutron_dynamic_routing/db/bgp_dragentscheduler_db.py b/neutron_dynamic_routing/db/bgp_dragentscheduler_db.py index 37dd07fa..cfb4cb98 100644 --- a/neutron_dynamic_routing/db/bgp_dragentscheduler_db.py +++ b/neutron_dynamic_routing/db/bgp_dragentscheduler_db.py @@ -274,3 +274,69 @@ class BgpDrAgentSchedulerDbMixin(bgp_dras_ext.BgpDrSchedulerPluginBase, return self.get_bgp_peer(context, bgp_peer_id) except exc.NoResultFound: return {} + + def _create_or_update_bgp_speaker_l3agent_binding(self, context, agent_id, + speaker_id, + dragent_id=None): + with db_api.CONTEXT_WRITER.using(context): + agent_db = self._get_agent(context, agent_id) + is_agent_l3 = (agent_db['agent_type'] == + bgp_consts.L3_AGENT_BGP_ROUTING) + if not is_agent_l3: + raise bgp_dras_ext.DrAgentInvalid(id=agent_id) + + binding = BgpSpeakerDrAgentBinding() + binding.bgp_speaker_id = speaker_id + binding.agent_id = agent_id + query = context.session.query(BgpSpeakerDrAgentBinding) + if dragent_id: + try: + existing_binding = query.filter_by( + bgp_speaker_id=speaker_id, agent_id=dragent_id).one() + existing_binding.update(binding) + except exc.NoResultFound: + #Create a new binding with l3agent and speaker mapping + context.session.add(binding) + else: + context.session.add(binding) + + def reschedule_bgp_speaker_on_l3_agent(self, context, bgp_speaker_id, + l3_agent_id): + dragents = self.get_dragents_hosting_bgp_speakers( + context, [bgp_speaker_id]) + if len(dragents) > 1: + for dragent in dragents[1:]: + self.remove_bgp_speaker_from_dragent(context, dragent.id, + bgp_speaker_id) + + #Replacing dragent with l3agent so that it is not autoscheduled + if dragents: + dragent_id = dragents[0].id + self._create_or_update_bgp_speaker_l3agent_binding( + context, l3_agent_id, bgp_speaker_id, dragent_id) + agent_db = self._get_agent(context, dragent_id) + self._bgp_rpc.bgp_speaker_removed(context, bgp_speaker_id, + agent_db.host) + else: + self._create_or_update_bgp_speaker_l3agent_binding( + context, l3_agent_id, bgp_speaker_id) + + def remove_bgp_speaker_from_l3agent(self, context, bgp_speaker_id): + l3agent = self.get_dragents_hosting_bgp_speakers( + context, [bgp_speaker_id])[0] + agent_id = l3agent.id + with db_api.CONTEXT_WRITER.using(context): + agent_db = self._get_agent(context, agent_id) + is_agent_l3 = (agent_db['agent_type'] == + bgp_consts.L3_AGENT_BGP_ROUTING) + if not is_agent_l3: + raise bgp_dras_ext.DrAgentInvalid(id=agent_id) + + query = context.session.query(BgpSpeakerDrAgentBinding) + query = query.filter_by(bgp_speaker_id=bgp_speaker_id, + agent_id=agent_id) + query.delete() + LOG.debug('BgpSpeaker %(bgp_speaker_id)s removed from ' + 'L3Agent %(agent_id)s', + {'bgp_speaker_id': bgp_speaker_id, + 'agent_id': agent_id}) diff --git a/neutron_dynamic_routing/db/migration/alembic_migrations/versions/yoga/expand/738d0ae1984e_bgpaas_enh.py b/neutron_dynamic_routing/db/migration/alembic_migrations/versions/yoga/expand/738d0ae1984e_bgpaas_enh.py index 76308384..9a7939e6 100644 --- a/neutron_dynamic_routing/db/migration/alembic_migrations/versions/yoga/expand/738d0ae1984e_bgpaas_enh.py +++ b/neutron_dynamic_routing/db/migration/alembic_migrations/versions/yoga/expand/738d0ae1984e_bgpaas_enh.py @@ -33,6 +33,7 @@ def upgrade(): 'bgp_speaker_router_associations', sa.Column('id', sa.String(length=constants.UUID_FIELD_SIZE), nullable=False), + sa.Column('name', sa.String(255), nullable=False), sa.Column('project_id', sa.String(length=255)), sa.Column('bgp_speaker_id', sa.String(length=36), sa.ForeignKey('bgp_speakers.id'), nullable=False), @@ -45,6 +46,7 @@ def upgrade(): 'bgp_speaker_peer_associations', sa.Column('id', sa.String(length=constants.UUID_FIELD_SIZE), nullable=False), + sa.Column('name', sa.String(255), nullable=False), sa.Column('project_id', sa.String(length=255)), sa.Column('bgp_speaker_id', sa.String(length=36), sa.ForeignKey('bgp_speakers.id'), diff --git a/neutron_dynamic_routing/extensions/bgp_associations.py b/neutron_dynamic_routing/extensions/bgp_associations.py index 0a26e739..86559a2f 100644 --- a/neutron_dynamic_routing/extensions/bgp_associations.py +++ b/neutron_dynamic_routing/extensions/bgp_associations.py @@ -73,7 +73,7 @@ class DuplicateBgpSpeakerRouterAssociation(n_exc.Conflict): "router with id %(router_id)s.") -class InvalidBgpSpeakerRouterAssociation(n_exc.Conflict): +class InvalidBgpSpeakerAssociation(n_exc.Conflict): message = _("BGP Speaker %(bgp_speaker_id)s is already associated to " "network with id %(network_id)s.") diff --git a/neutron_dynamic_routing/privileged/driver.py b/neutron_dynamic_routing/privileged/driver.py index b5018093..e8191e48 100644 --- a/neutron_dynamic_routing/privileged/driver.py +++ b/neutron_dynamic_routing/privileged/driver.py @@ -25,6 +25,7 @@ from os_ken.services.protocols.bgp import net_ctrl from os_ken.services.protocols.bgp.rtconf import neighbors from os_ken.services.protocols.bgp.rtconf.neighbors import PASSWORD from oslo_log import log as logging +import tenacity from neutron.privileged.agent.linux import ip_lib from neutron_dynamic_routing._i18n import _LI @@ -75,6 +76,9 @@ def del_bgp_speaker(bgp_speaker_id, ns): @privileged.bgp_speaker_cmd.entrypoint +@tenacity.retry(retry=tenacity.retry_if_exception_type( + (EOFError, ConnectionRefusedError)), + wait=tenacity.wait_random(min=1, max=2)) def add_bgp_neighbor(bgp_speaker_id, peer_ip, remote_as, ns, password=None, auth_type='none'): with open(get_netns_path(ns)) as fd: diff --git a/neutron_dynamic_routing/services/bgp/agent/l3/bgp_extension.py b/neutron_dynamic_routing/services/bgp/agent/l3/bgp_extension.py index 52c1a66e..a2e74aaf 100644 --- a/neutron_dynamic_routing/services/bgp/agent/l3/bgp_extension.py +++ b/neutron_dynamic_routing/services/bgp/agent/l3/bgp_extension.py @@ -129,6 +129,8 @@ class BgpAgentExtension(l3_extension.L3AgentExtension): context, assoc.bgp_speaker_id, assoc.router_id) self.routes_cache.create_speaker_routes(assoc.bgp_speaker_id, routes) + self.rpc_plugin.update_speaker_router_association_status( + context, assoc.id, bgp_consts.STATUS_ACTIVE) elif event_type == rpc_events.UPDATED: if assoc.advertise_extra_routes: routes = self.rpc_plugin.get_routes_to_advertise( @@ -168,6 +170,8 @@ class BgpAgentExtension(l3_extension.L3AgentExtension): priv_driver.advertise_routes( tuple(self.routes_cache.get_speaker_routes( peer_assoc.bgp_speaker_id))) + self.rpc_plugin.update_speaker_peer_association_status( + context, peer_assoc.id, bgp_consts.STATUS_ACTIVE) elif event_type == rpc_events.DELETED: priv_driver.del_bgp_neighbor(peer_assoc.bgp_speaker_id, peer['peer_ip'], @@ -181,7 +185,33 @@ class BgpAgentExtension(l3_extension.L3AgentExtension): :param context: RPC context. :param data: Router data. """ - pass + LOG.debug("ADD ROUTER %s", data) + router_info = self.agent_api.get_router_info(data['id']) + if router_info: + router_assocs = self.rpc_plugin.get_speaker_associated_to_router( + context, data['id']) + for router_assoc in router_assocs: + bgp_speaker_id = router_assoc['bgp_speaker_id'] + sp = self.rpc_plugin.get_bgp_speaker_info(context, + bgp_speaker_id) + if priv_driver.PROCESS_CACHE.get_bgp_speaker_process( + bgp_speaker_id): + LOG.debug("BGP process already running for speaker %s", + bgp_speaker_id) + continue + priv_driver.add_bgp_speaker(bgp_speaker_id, + sp['local_as'], '1.1.1.1', + router_info.ns_name, + sp['ip_version']) + LOG.debug("ADD ROUTER PEER ASSOC %s", sp['peer_associations']) + for peer_assoc in sp['peer_associations']: + peer = self.rpc_plugin.get_bgp_peer_info( + context, peer_assoc['peer_id']) + LOG.debug("ADD ROUTER PEER %s", peer) + priv_driver.add_bgp_neighbor(peer_assoc['bgp_speaker_id'], + peer['peer_ip'], + peer['remote_as'], + router_info.ns_name) def update_router(self, context, data): """Handle a router update event. diff --git a/neutron_dynamic_routing/services/bgp/agent/l3/bgp_rpc_api.py b/neutron_dynamic_routing/services/bgp/agent/l3/bgp_rpc_api.py index 2ac45823..59f32e61 100644 --- a/neutron_dynamic_routing/services/bgp/agent/l3/bgp_rpc_api.py +++ b/neutron_dynamic_routing/services/bgp/agent/l3/bgp_rpc_api.py @@ -65,3 +65,17 @@ class BgpL3ExtPluginApi(object): cctxt = self.client.prepare() return cctxt.call(context, 'get_speaker_associated_to_router', router_id=router_id) + + def update_speaker_router_association_status(self, context, assoc_id, + status): + """Make a remote process call to update router association status""" + cctxt = self.client.prepare() + return cctxt.call(context, 'update_speaker_router_association_status', + assoc_id=assoc_id, status=status) + + def update_speaker_peer_association_status(self, context, assoc_id, + status): + """Make a remote process call to update peer association status""" + cctxt = self.client.prepare() + return cctxt.call(context, 'update_speaker_peer_association_status', + assoc_id=assoc_id, status=status) diff --git a/neutron_dynamic_routing/services/bgp/bgp_plugin.py b/neutron_dynamic_routing/services/bgp/bgp_plugin.py index 559c0d95..739cf1bb 100644 --- a/neutron_dynamic_routing/services/bgp/bgp_plugin.py +++ b/neutron_dynamic_routing/services/bgp/bgp_plugin.py @@ -14,7 +14,6 @@ from netaddr import IPAddress -from neutron.api.rpc.callbacks import events as rpc_events from neutron.api.rpc.callbacks import resources as resources_registry from neutron.api.rpc.handlers import resources_rpc from neutron_lib.api.definitions import bgp as bgp_ext @@ -34,10 +33,12 @@ from oslo_log import log as logging from oslo_utils import importutils from neutron_dynamic_routing.api.rpc.agentnotifiers import bgp_dr_rpc_agent_api # noqa +from neutron_dynamic_routing.api.rpc.agentnotifiers import l3_bgp_rpc_agent_api from neutron_dynamic_routing.api.rpc.callbacks import resources as dr_resources from neutron_dynamic_routing.api.rpc.handlers import bgp_speaker_rpc as bs_rpc from neutron_dynamic_routing.db import bgp_db from neutron_dynamic_routing.db import bgp_dragentscheduler_db +from neutron_dynamic_routing.extensions import bgp_dragentscheduler as bgp_dras_ext # noqa from neutron_dynamic_routing.objects import bgp_associations as assoc_objects from neutron_dynamic_routing.services.bgp.common import constants as bgp_consts @@ -153,10 +154,11 @@ class BgpPlugin(service_base.ServicePluginBase, context, [bgp_speaker_id]) super(BgpPlugin, self).delete_bgp_speaker(context, bgp_speaker_id) + agent_rpc = self.get_rpc(context, bgp_speaker_id) for agent in hosted_bgp_dragents: - self._bgp_rpc.bgp_speaker_removed(context, - bgp_speaker_id, - agent.host) + agent_rpc.bgp_speaker_removed(context, + bgp_speaker_id, + agent.host) def get_bgp_peers(self, context, fields=None, filters=None, sorts=None, limit=None, marker=None, page_reverse=False): @@ -189,10 +191,11 @@ class BgpPlugin(service_base.ServicePluginBase, hosted_bgp_dragents = self.get_dragents_hosting_bgp_speakers( context, [bgp_speaker_id]) + agent_rpc = self.get_rpc(context, bgp_speaker_id) for agent in hosted_bgp_dragents: - self._bgp_rpc.bgp_peer_associated(context, bgp_speaker_id, - ret_value['bgp_peer_id'], - agent.host) + agent_rpc.bgp_peer_associated(context, bgp_speaker_id, + ret_value['bgp_peer_id'], + agent.host) return ret_value def remove_bgp_peer(self, context, bgp_speaker_id, bgp_peer_info): @@ -202,12 +205,12 @@ class BgpPlugin(service_base.ServicePluginBase, ret_value = super(BgpPlugin, self).remove_bgp_peer(context, bgp_speaker_id, bgp_peer_info) - + agent_rpc = self.get_rpc(context, bgp_speaker_id) for agent in hosted_bgp_dragents: - self._bgp_rpc.bgp_peer_disassociated(context, - bgp_speaker_id, - ret_value['bgp_peer_id'], - agent.host) + agent_rpc.bgp_peer_disassociated(context, + bgp_speaker_id, + ret_value['bgp_peer_id'], + agent.host) def add_bgp_speaker_to_dragent(self, context, agent_id, speaker_id): super(BgpPlugin, self).add_bgp_speaker_to_dragent(context, @@ -262,10 +265,16 @@ class BgpPlugin(service_base.ServicePluginBase, bgp_speaker_id, router_association) - router_assoc_obj = assoc_objects.BgpSpeakerRouterAssociation( - context, - **assoc_info) - self.push_api.push(context, [router_assoc_obj], rpc_events.CREATED) + #By default, BGP speaker is bound to dr agent. On router association + #it has to be rescheduled to l3 agent + router_id = assoc_info['router_id'] + l3_agent = super(BgpPlugin, self).get_l3_agent_hosting_router( + context, router_id) + super(BgpPlugin, self).reschedule_bgp_speaker_on_l3_agent( + context, bgp_speaker_id, l3_agent.id) + agent_rpc = self.get_rpc(context, bgp_speaker_id) + agent_rpc.bgp_router_association_created(context, bgp_speaker_id, + assoc_info) return assoc_info def update_bgp_speaker_router_association(self, context, assoc_id, @@ -274,21 +283,31 @@ class BgpPlugin(service_base.ServicePluginBase, .update_bgp_speaker_router_association(context, assoc_id, router_assoc) - router_assoc_obj = assoc_objects.BgpSpeakerRouterAssociation( - context, - **assoc_info) - self.push_api.push(context, [router_assoc_obj], rpc_events.UPDATED) + agent_rpc = self.get_rpc(context, bgp_speaker_id) + agent_rpc.bgp_router_association_updated(context, bgp_speaker_id, + assoc_info) return assoc_info + def update_speaker_router_association_status(self, context, assoc_id, + status): + super(BgpPlugin, self).update_speaker_router_association_status( + context, assoc_id, status) + def delete_bgp_speaker_router_association(self, context, id, bgp_speaker_id): assoc_info = super(BgpPlugin, self)\ .delete_bgp_speaker_router_association(context, id) - router_assoc_obj = assoc_objects.BgpSpeakerRouterAssociation( - context, - **assoc_info) - self.push_api.push(context, [router_assoc_obj], rpc_events.DELETED) + agent_rpc = self.get_rpc(context, bgp_speaker_id) + agent_rpc.bgp_router_association_deleted(context, bgp_speaker_id, + assoc_info) + #Remove l3 agent to speaker mapping and reschedule bgp speaker + #to BGP DR agent again + super(BgpPlugin, self).remove_bgp_speaker_from_l3agent(context, + bgp_speaker_id) + + bgp_speaker = self.get_bgp_speaker(context, bgp_speaker_id) + super(BgpPlugin, self).schedule_bgp_speaker(context, bgp_speaker) def get_bgp_speaker_peer_associations( self, context, bgp_speaker_id, @@ -314,20 +333,24 @@ class BgpPlugin(service_base.ServicePluginBase, .create_bgp_speaker_peer_association(context, bgp_speaker_id, peer_association) - peer_assoc_obj = assoc_objects.BgpSpeakerPeerAssociation( - context, - **assoc_info) - self.push_api.push(context, [peer_assoc_obj], rpc_events.CREATED) + agent_rpc = self.get_rpc(context, bgp_speaker_id) + agent_rpc.bgp_peer_association_created(context, bgp_speaker_id, + assoc_info) return assoc_info + def update_speaker_peer_association_status(self, context, assoc_id, + status): + super(BgpPlugin, self).update_speaker_peer_association_status( + context, assoc_id, status) + def delete_bgp_speaker_peer_association(self, context, id, bgp_speaker_id): assoc_info = super(BgpPlugin, self)\ .delete_bgp_speaker_peer_association(context, id) - peer_assoc_obj = assoc_objects.BgpSpeakerPeerAssociation(context, - **assoc_info) - self.push_api.push(context, [peer_assoc_obj], rpc_events.DELETED) + agent_rpc = self.get_rpc(context, bgp_speaker_id) + agent_rpc.bgp_peer_association_deleted(context, bgp_speaker_id, + assoc_info) def get_speaker_associated_to_router(self, context, router_id): return super(BgpPlugin, self).get_speaker_associated_to_router( @@ -515,11 +538,12 @@ class BgpPlugin(service_base.ServicePluginBase, def start_route_advertisements(self, ctx, bgp_rpc, bgp_speaker_id, routes): agents = self.list_dragent_hosting_bgp_speaker(ctx, bgp_speaker_id) + agent_rpc = self.get_rpc(context, bgp_speaker_id) for agent in agents['agents']: - bgp_rpc.bgp_routes_advertisement(ctx, - bgp_speaker_id, - routes, - agent['host']) + agent_rpc.bgp_routes_advertisement(ctx, + bgp_speaker_id, + routes, + agent['host']) msg = "Starting route advertisements for %s on BgpSpeaker %s" self._debug_log_for_routes(msg, routes, bgp_speaker_id) @@ -527,11 +551,12 @@ class BgpPlugin(service_base.ServicePluginBase, def stop_route_advertisements(self, ctx, bgp_rpc, bgp_speaker_id, routes): agents = self.list_dragent_hosting_bgp_speaker(ctx, bgp_speaker_id) + agent_rpc = self.get_rpc(context, bgp_speaker_id) for agent in agents['agents']: - bgp_rpc.bgp_routes_withdrawal(ctx, - bgp_speaker_id, - routes, - agent['host']) + agent_rpc.bgp_routes_withdrawal(ctx, + bgp_speaker_id, + routes, + agent['host']) msg = "Stopping route advertisements for %s on BgpSpeaker %s" self._debug_log_for_routes(msg, routes, bgp_speaker_id) @@ -542,3 +567,19 @@ class BgpPlugin(service_base.ServicePluginBase, if LOG.isEnabledFor(logging.DEBUG): for route in routes: LOG.debug(msg, route, bgp_speaker_id) + + def get_rpc(self, context, bgp_speaker_id): + hosted_agents = self.get_dragents_hosting_bgp_speakers( + context, [bgp_speaker_id]) + rpc = self._bgp_rpc + if len(hosted_agents) >= 1: + #Checking agent type of a single binding + agent_id = hosted_agents[0].id + agent_db = self._get_agent(context, agent_id) + if agent_db['agent_type'] == bgp_consts.AGENT_TYPE_BGP_ROUTING: + return rpc + elif agent_db['agent_type'] == bgp_consts.L3_AGENT_BGP_ROUTING: + rpc = l3_bgp_rpc_agent_api.BgpL3AgentNotifyApi() + else: + raise bgp_dras_ext.DrAgentInvalid(id=agent_id) + return rpc diff --git a/neutron_dynamic_routing/services/bgp/common/constants.py b/neutron_dynamic_routing/services/bgp/common/constants.py index 4dd91215..31f10f0f 100644 --- a/neutron_dynamic_routing/services/bgp/common/constants.py +++ b/neutron_dynamic_routing/services/bgp/common/constants.py @@ -14,6 +14,7 @@ # under the License. AGENT_TYPE_BGP_ROUTING = 'BGP dynamic routing agent' +L3_AGENT_BGP_ROUTING = 'L3 agent' BGP_DRAGENT = 'bgp_dragent' diff --git a/neutron_dynamic_routing/tests/unit/services/bgp/agent/test_bgp_l3_agent.py b/neutron_dynamic_routing/tests/unit/services/bgp/agent/test_bgp_l3_agent.py new file mode 100644 index 00000000..d7b4bcb3 --- /dev/null +++ b/neutron_dynamic_routing/tests/unit/services/bgp/agent/test_bgp_l3_agent.py @@ -0,0 +1,221 @@ +# 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 unittest import mock + +from neutron.agent.l3 import agent as l3_agent +from neutron.agent.l3 import l3_agent_extension_api as l3_ext_api +from neutron.agent.l3 import router_info as l3router +from neutron.api.rpc.callbacks.consumer import registry +from neutron.api.rpc.callbacks import events +from neutron.api.rpc.handlers import resources_rpc +from neutron.tests.unit.agent.l3 import test_agent +from neutron_lib import constants +from neutron_lib import context +from oslo_utils import uuidutils + +from neutron_dynamic_routing.objects import bgp_associations as assoc_objects +from neutron_dynamic_routing.privileged import driver as priv_driver +from neutron_dynamic_routing.services.bgp.agent.l3 import bgp_extension + +BGP_ROUTER_ASSOC = assoc_objects.BgpSpeakerRouterAssociation.obj_name() +BGP_PEER_ASSOC = assoc_objects.BgpSpeakerPeerAssociation.obj_name() +HOSTNAME = 'testhost' +FAKE_BGPSPEAKER_UUID = uuidutils.generate_uuid() +FAKE_ROUTER_ASSOC_UUID = uuidutils.generate_uuid() +FAKE_PEER_ASSOC_UUID = uuidutils.generate_uuid() +FAKE_ROUTER_UUID = uuidutils.generate_uuid() +FAKE_BGPPEER_UUID = uuidutils.generate_uuid() +FAKE_BGP_SPEAKER = {'id': FAKE_BGPSPEAKER_UUID, + 'ip_version': 4, + 'local_as': 12345, + 'peers': [{'remote_as': '2345', + 'peer_ip': '1.1.1.1', + 'auth_type': 'none', + 'password': ''}], + 'router_associations': [{ + 'bgp_speaker_id': FAKE_BGPSPEAKER_UUID, + 'router_id': FAKE_ROUTER_UUID, + 'advertise_extra_routes': True}], + 'peer_associations': [{ + 'bgp_speaker_id': FAKE_BGPSPEAKER_UUID, + 'peer_id': FAKE_BGPPEER_UUID}], + 'advertised_routes': []} +FAKE_BGP_PEER = {'id': FAKE_BGPPEER_UUID, + 'remote_as': '2345', + 'peer_ip': '1.1.1.1', + 'auth_type': 'none', + 'password': ''} +FAKE_ROUTES = ['192.168.145.0/24', '145.0.0.0/24'] +FAKE_EXTRA_ROUTE = ['145.0.0.0/24'] + + +class BgpL3AgentExtensionBaseTestCase( + test_agent.BasicRouterOperationsFramework): + + def setUp(self): + super(BgpL3AgentExtensionBaseTestCase, self).setUp() + + self.bgp_ext = bgp_extension.BgpAgentExtension() + self.agent_api = l3_ext_api.L3AgentExtensionAPI(None, None) + self.bgp_ext.consume_api(self.agent_api) + self.connection = mock.Mock() + self.context = context.get_admin_context() + self.agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) + self.router_id = FAKE_ROUTER_UUID + self.router = {'id': self.router_id, + 'ha': False, + 'distributed': False} + self.router_info = l3router.RouterInfo(self.agent, self.router_id, + self.router, **self.ri_kwargs) + self.agent.router_info[self.router['id']] = self.router_info + + self.get_router_info = mock.patch( + 'neutron.agent.l3.l3_agent_extension_api.' + 'L3AgentExtensionAPI.get_router_info').start() + self.get_router_info.return_value = self.router_info + + +class BgpL3AgentExtensionInitializeTestCase(BgpL3AgentExtensionBaseTestCase): + @mock.patch.object(registry, 'register') + @mock.patch.object(resources_rpc, 'ResourcesPushRpcCallback') + def test_initialize_subscribed_to_rpc(self, rpc_mock, subscribe_mock): + call_to_patch = 'neutron_lib.rpc.Connection' + with mock.patch(call_to_patch, + return_value=self.connection) as create_connection: + self.bgp_ext.initialize( + self.connection, constants.L3_AGENT_MODE) + create_connection.assert_has_calls([mock.call()]) + self.connection.create_consumer.assert_has_calls( + [mock.call( + resources_rpc.resource_type_versioned_topic( + BGP_ROUTER_ASSOC), + [rpc_mock()], + fanout=True), + mock.call( + resources_rpc.resource_type_versioned_topic( + BGP_PEER_ASSOC), + [rpc_mock()], + fanout=True)] + ) + subscribe_mock.assert_any_call( + mock.ANY, BGP_ROUTER_ASSOC) + subscribe_mock.assert_called_with( + mock.ANY, BGP_PEER_ASSOC) + + +class BGPL3AgentExtensionRouterAssocTestCase(BgpL3AgentExtensionBaseTestCase): + + def setUp(self): + super(BGPL3AgentExtensionRouterAssocTestCase, self).setUp() + self.bgp_ext.initialize( + self.connection, constants.L3_AGENT_MODE) + router_assoc_info = {'id': FAKE_ROUTER_ASSOC_UUID, + 'router_id': self.router_id, + 'bgp_speaker_id': FAKE_BGPSPEAKER_UUID, + 'advertise_extra_routes': True} + self.router_assoc = assoc_objects.BgpSpeakerRouterAssociation( + self.context, **router_assoc_info) + peer_assoc_info = {'id': FAKE_PEER_ASSOC_UUID, + 'peer_id': FAKE_BGPPEER_UUID, + 'bgp_speaker_id': FAKE_BGPSPEAKER_UUID} + self.peer_assoc = assoc_objects.BgpSpeakerPeerAssociation( + self.context, **peer_assoc_info) + self.bgp_ext.rpc_plugin = mock.Mock() + self.plugin_rpc = self.bgp_ext.rpc_plugin + self.plugin_rpc.get_bgp_speaker_info.return_value = FAKE_BGP_SPEAKER + self.plugin_rpc.get_bgp_peer_info.return_value = FAKE_BGP_PEER + self.plugin_rpc.get_routes_to_advertise.return_value = FAKE_ROUTES + self.plugin_rpc.get_routes_to_withdraw.return_value = FAKE_EXTRA_ROUTE + self.plugin_rpc.get_speaker_associated_to_router.return_value = [ + router_assoc_info] + + @mock.patch.object(priv_driver, 'add_bgp_speaker') + def test_handle_create_router_assoc(self, driver_mock): + self.bgp_ext._handle_router_notification(self.context, mock.Mock(), + [self.router_assoc], + events.CREATED) + driver_mock.assert_called_once_with(FAKE_BGPSPEAKER_UUID, + FAKE_BGP_SPEAKER['local_as'], + mock.ANY, + mock.ANY, 4) + + @mock.patch.object(priv_driver, 'advertise_routes') + def test_handle_update_router_assoc_with_advertise_extra_routes( + self, driver_mock): + self.bgp_ext._handle_router_notification(self.context, mock.Mock(), + [self.router_assoc], + events.UPDATED) + driver_mock.assert_called_once_with(tuple(FAKE_ROUTES)) + + @mock.patch.object(priv_driver, 'withdraw_routes') + def test_handle_update_router_assoc_without_advertise_extra_routes( + self, driver_mock): + assoc_info = {'id': FAKE_ROUTER_ASSOC_UUID, + 'router_id': self.router_id, + 'bgp_speaker_id': FAKE_BGPSPEAKER_UUID, + 'advertise_extra_routes': False} + router_assoc = assoc_objects.BgpSpeakerPeerAssociation( + self.context, **assoc_info) + self.bgp_ext._handle_router_notification(self.context, mock.Mock(), + [router_assoc], + events.UPDATED) + driver_mock.assert_called_once_with(tuple(FAKE_EXTRA_ROUTE)) + + @mock.patch.object(priv_driver, 'del_bgp_speaker') + def test_handle_delete_router_assoc(self, driver_mock): + self.bgp_ext._handle_router_notification(self.context, mock.Mock(), + [self.router_assoc], + events.DELETED) + driver_mock.assert_called_once_with(FAKE_BGPSPEAKER_UUID, mock.ANY) + + @mock.patch.object(priv_driver, 'advertise_routes') + @mock.patch.object(priv_driver, 'add_bgp_neighbor') + def test_handle_create_peer_assoc(self, driver_mock, advertise_mock): + priv_driver.add_bgp_speaker = mock.Mock() + self.bgp_ext._handle_router_notification(self.context, mock.Mock(), + [self.router_assoc], + events.CREATED) + self.bgp_ext._handle_peer_notification(self.context, mock.Mock(), + [self.peer_assoc], + events.CREATED) + driver_mock.assert_called_once_with(FAKE_BGPSPEAKER_UUID, + FAKE_BGP_PEER['peer_ip'], + FAKE_BGP_PEER['remote_as'], + mock.ANY) + advertise_mock.assert_called_once_with(tuple(FAKE_ROUTES)) + + @mock.patch.object(priv_driver, 'del_bgp_neighbor') + def test_handle_delete_peer_assoc(self, driver_mock): + self.bgp_ext._handle_peer_notification(self.context, mock.Mock(), + [self.peer_assoc], + events.DELETED) + driver_mock.assert_called_once_with(FAKE_BGPSPEAKER_UUID, + FAKE_BGP_PEER['peer_ip'], + mock.ANY) + + @mock.patch.object(priv_driver, 'advertise_routes') + @mock.patch.object(priv_driver, 'withdraw_routes') + def test_update_router(self, mock_withdraw, mock_advertise): + priv_driver.add_bgp_speaker = mock.Mock() + priv_driver.add_bgp_neighbor = mock.Mock() + self.bgp_ext._handle_router_notification(self.context, mock.Mock(), + [self.router_assoc], + events.CREATED) + self.bgp_ext._handle_peer_notification(self.context, mock.Mock(), + [self.peer_assoc], + events.CREATED) + new_route = ['155.0.0.0/24'] + self.plugin_rpc.get_routes_to_advertise.return_value = new_route + self.bgp_ext.update_router(self.context, self.router) + mock_advertise.assert_called_with(tuple(new_route)) + mock_withdraw.assert_called_with(tuple(FAKE_ROUTES))