diff --git a/neutron_dynamic_routing/db/bgp_db.py b/neutron_dynamic_routing/db/bgp_db.py index 37190fc6..f165ad2e 100644 --- a/neutron_dynamic_routing/db/bgp_db.py +++ b/neutron_dynamic_routing/db/bgp_db.py @@ -197,7 +197,7 @@ class BgpDbMixin(object): def get_bgp_speaker_with_advertised_routes(self, context, bgp_speaker_id): - bgp_speaker_attrs = ['id', 'local_as', 'tenant_id'] + bgp_speaker_attrs = ['id', 'local_as', 'tenant_id', 'ip_version'] bgp_peer_attrs = ['peer_ip', 'remote_as', 'auth_type', 'password'] with db_api.CONTEXT_READER.using(context): bgp_speaker = self.get_bgp_speaker(context, bgp_speaker_id, @@ -209,6 +209,14 @@ class BgpDbMixin(object): res['advertised_routes'] = self.get_routes_by_bgp_speaker_id( context, bgp_speaker_id) + res['router_associations'] = \ + self._get_bgp_speaker_router_association_by_speaker_id( + context, + bgp_speaker_id) + res['peer_associations'] = \ + self._get_bgp_speaker_peer_association_by_speaker_id( + context, + bgp_speaker_id) return res def update_bgp_speaker(self, context, bgp_speaker_id, bgp_speaker): @@ -250,6 +258,7 @@ class BgpDbMixin(object): network_id = self._get_id_for(network_info, 'network_id') with db_api.CONTEXT_WRITER.using(context): try: + self._validate_network_binding(context, bgp_speaker_id) self._save_bgp_speaker_network_binding(context, bgp_speaker_id, network_id) @@ -267,6 +276,16 @@ class BgpDbMixin(object): network_id) return {'network_id': network_id} + def _validate_network_binding(self, context, speaker_id): + #Do not allow network binding if router association is present. + router_assocs = self._get_bgp_speaker_router_association_by_speaker_id( + context, + speaker_id) + if len(router_assocs) > 0: + raise bgp_asso_ext.DuplicateBgpSpeakerRouterAssociation( + bgp_speaker_id=speaker_id, + router_id=router_assocs[0]['router_id']) + def get_bgp_speaker_router_associations(self, context, bgp_speaker_id, fields=None, filters=None, sorts=None, limit=None, @@ -299,14 +318,16 @@ class BgpDbMixin(object): raise bgp.BgpSpeakerNotFound(id=bgp_speaker_id) try: - model_query.get_by_id(context, l3_db.Router, router_id) + router_info = model_query.get_by_id( + context, l3_db.Router, router_id) except sa_exc.NoResultFound: raise l3_exc.RouterNotFound(router_id=router_id) res_keys = ['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) + self._validate_router_association(context, bgp_speaker_id, + router_info) res['id'] = uuidutils.generate_uuid() speaker_router_assoc_db = BgpSpeakerRouterAssociation(**res) context.session.add(speaker_router_assoc_db) @@ -325,8 +346,11 @@ class BgpDbMixin(object): def delete_bgp_speaker_router_association(self, context, id): with db_api.CONTEXT_WRITER.using(context): - binding = self._get_bgp_speaker_router_association(context, id) - context.session.delete(binding) + speaker_router_assoc_db = \ + self._get_bgp_speaker_router_association(context, id) + context.session.delete(speaker_router_assoc_db) + return self._make_bgp_speaker_router_association_dict( + speaker_router_assoc_db) def get_bgp_speaker_peer_associations(self, context, bgp_speaker_id, fields=None, filters=None, @@ -375,12 +399,17 @@ class BgpDbMixin(object): def delete_bgp_speaker_peer_association(self, context, id): with db_api.CONTEXT_WRITER.using(context): - binding = self._get_bgp_speaker_peer_association(context, id) - context.session.delete(binding) + assoc_db = self._get_bgp_speaker_peer_association(context, id) + context.session.delete(assoc_db) + return self._make_bgp_speaker_peer_association_dict(assoc_db) 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) + if (bgp_speaker_db['router_associations'] or + bgp_speaker_db['peer_associations']): + raise bgp_asso_ext.DependentAssociationExists( + bgp_speaker_id=bgp_speaker_id) context.session.delete(bgp_speaker_db) def create_bgp_peer(self, context, bgp_peer): @@ -424,6 +453,11 @@ class BgpDbMixin(object): def delete_bgp_peer(self, context, bgp_peer_id): with db_api.CONTEXT_WRITER.using(context): + peer_assocs = self._get_bgp_speaker_peer_association_by_peer_id( + context, bgp_peer_id) + if peer_assocs: + raise bgp_asso_ext.DependentPeerAssociationExists( + bgp_peer_id=bgp_peer_id) bgp_peer_db = self._get_bgp_peer(context, bgp_peer_id) context.session.delete(bgp_peer_db) @@ -634,7 +668,7 @@ class BgpDbMixin(object): return query.filter( BgpSpeakerRouterAssociation.bgp_speaker_id == bgp_speaker_id).all() - def _validate_router_association(self, context, speaker_id): + def _validate_router_association(self, context, speaker_id, router_info): # Current implementation allows only one router to be associated with # BGP speaker. # Also, do not allow same router to be associated again with speaker. @@ -643,8 +677,8 @@ class BgpDbMixin(object): speaker_id) if len(router_assocs) == 1: raise bgp_asso_ext.DuplicateBgpSpeakerRouterAssociation( - bgp_speaker_id=speaker_id, - router_id=router_assocs[0]['router_id']) + bgp_speaker_id=speaker_id, + router_id=router_assocs[0]['router_id']) # Do not allow router association if network is already associated to # BGP speaker. @@ -652,9 +686,13 @@ class BgpDbMixin(object): context, speaker_id) if len(network_bindings) == 1: - raise bgp_asso_ext.InvalidBgpSpeakerRouterAssociation( - bgp_speaker_id=speaker_id, - network_id=network_bindings[0]['network_id']) + raise bgp_asso_ext.InvalidBgpSpeakerAssociation( + bgp_speaker_id=speaker_id, + network_id=network_bindings[0]['network_id']) + + if router_info['enable_snat']: + raise bgp_asso_ext.InvalidBgpSpeakerSnatRouterAssociation( + bgp_speaker_id=speaker_id, router_id=router_info['id']) def _get_bgp_speaker_peer_association_by_speaker_id(self, context, bgp_speaker_id): @@ -663,6 +701,12 @@ class BgpDbMixin(object): return query.filter( BgpSpeakerPeerAssociation.bgp_speaker_id == bgp_speaker_id).all() + def _get_bgp_speaker_peer_association_by_peer_id(self, context, peer_id): + query = model_query.query_with_hooks(context, + BgpSpeakerPeerAssociation) + return query.filter( + BgpSpeakerPeerAssociation.peer_id == peer_id).all() + def _validate_peer_association(self, context, speaker_id, new_peer): # Do not allow same peer to be associated again with BGP speaker. peer_assocs = self._get_bgp_speaker_peer_association_by_speaker_id( @@ -675,6 +719,17 @@ class BgpDbMixin(object): 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']) + def _get_bgp_speaker_peer_association(self, context, id): try: return model_query.get_by_id(context, BgpSpeakerPeerAssociation, diff --git a/neutron_dynamic_routing/extensions/bgp_associations.py b/neutron_dynamic_routing/extensions/bgp_associations.py index 4b682e44..0a26e739 100644 --- a/neutron_dynamic_routing/extensions/bgp_associations.py +++ b/neutron_dynamic_routing/extensions/bgp_associations.py @@ -85,3 +85,18 @@ class BgpSpeakerPeerAssociationNotFound(n_exc.NotFound): class DuplicateBgpSpeakerPeerAssociation(n_exc.Conflict): message = _("BGP Speaker %(bgp_speaker_id)s is already associated to " "peer with id %(peer_id)s.") + + +class InvalidBgpSpeakerSnatRouterAssociation(n_exc.Conflict): + message = _("BGP Speaker %(bgp_speaker_id)s associated to SNAT " + "enabled router with id %(router_id)s is not supported.") + + +class DependentAssociationExists(n_exc.Conflict): + message = _("One or more router or peer association is still in use for " + "bgp speaker with id %(bgp_speaker_id)s.") + + +class DependentPeerAssociationExists(n_exc.Conflict): + message = _("Peer association is still in use for bgp peer with id " + "%(bgp_peer_id)s.") diff --git a/neutron_dynamic_routing/objects/bgp_associations.py b/neutron_dynamic_routing/objects/bgp_associations.py new file mode 100644 index 00000000..b96a1150 --- /dev/null +++ b/neutron_dynamic_routing/objects/bgp_associations.py @@ -0,0 +1,53 @@ +# Copyright (c) 2019 Red Hat, Inc. +# +# 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.objects import base +from neutron_dynamic_routing.db import bgp_db +from neutron_lib.objects import common_types +from oslo_versionedobjects import fields as obj_fields + + +@base.NeutronObjectRegistry.register +class BgpSpeakerRouterAssociation(base.NeutronDbObject): + # Version 1.0: Initial version + VERSION = '1.0' + + db_model = bgp_db.BgpSpeakerRouterAssociation + + fields = { + 'id': common_types.UUIDField(), + 'bgp_speaker_id': common_types.UUIDField(), + 'router_id': common_types.UUIDField(), + 'advertise_extra_routes': obj_fields.BooleanField(), + 'status': obj_fields.StringField(), + } + + primary_keys = ['id'] + + +@base.NeutronObjectRegistry.register +class BgpSpeakerPeerAssociation(base.NeutronDbObject): + # Version 1.0: Initial version + VERSION = '1.0' + + db_model = bgp_db.BgpSpeakerPeerAssociation + + fields = { + 'id': common_types.UUIDField(), + 'bgp_speaker_id': common_types.UUIDField(), + 'peer_id': common_types.UUIDField(), + 'status': obj_fields.StringField(), + } + + primary_keys = ['id'] diff --git a/neutron_dynamic_routing/privileged/__init__.py b/neutron_dynamic_routing/privileged/__init__.py new file mode 100644 index 00000000..a0743a79 --- /dev/null +++ b/neutron_dynamic_routing/privileged/__init__.py @@ -0,0 +1,22 @@ +# 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 oslo_privsep import capabilities as caps +from oslo_privsep import priv_context + +bgp_speaker_cmd = priv_context.PrivContext( + __name__, + cfg_section='privsep_bgpspeaker', + pypath=__name__ + '.bgp_speaker_cmd', + capabilities=[caps.CAP_NET_BIND_SERVICE, + caps.CAP_SYS_ADMIN] +) diff --git a/neutron_dynamic_routing/privileged/driver.py b/neutron_dynamic_routing/privileged/driver.py new file mode 100644 index 00000000..e819febb --- /dev/null +++ b/neutron_dynamic_routing/privileged/driver.py @@ -0,0 +1,148 @@ +# 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. + +import ctypes +from ctypes import util as ctypes_util +from multiprocessing import Process +import socket + +import eventlet +from os_ken.lib import hub +from os_ken.lib import rpc +from os_ken.services.protocols.bgp import bgpspeaker +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 + +from neutron_dynamic_routing._i18n import _LI +from neutron_dynamic_routing import privileged +from neutron_dynamic_routing.privileged import utils as bgp_utils +from neutron_dynamic_routing.services.bgp.agent.driver import utils + +eventlet.monkey_patch() +libc = ctypes.PyDLL(ctypes_util.find_library('c'), use_errno=True) +_setns = libc.setns +CLONE_NEWNET = 0x40000000 +LOG = logging.getLogger(__name__) +PROCESS_CACHE = bgp_utils.BgpSpeakerProcessCache() +VERSION_IPV6 = 6 +RPC_PORT = 50002 +RPC_HOST = '127.0.0.1' + + +def setns(fd, nstype): + if hasattr(fd, 'fileno'): + fd = fd.fileno() + _setns(fd, nstype) + + +def get_netns_path(nsname): + return '/var/run/netns/%s' % nsname + + +@privileged.bgp_speaker_cmd.entrypoint +def add_bgp_speaker(bgp_speaker_id, local_as, bgp_router_id, ns, ip_version): + with open(get_netns_path(ns)) as fd: + setns(fd, CLONE_NEWNET) + bgp_process = BgpProcess(ns, local_as, bgp_router_id, ip_version) + bgp_process.start() + PROCESS_CACHE.put_bgp_speaker_process(bgp_speaker_id, bgp_process) + + +@privileged.bgp_speaker_cmd.entrypoint +def del_bgp_speaker(bgp_speaker_id, ns): + with open(get_netns_path(ns)) as fd: + setns(fd, CLONE_NEWNET) + endpoint = socket.create_connection((RPC_HOST, RPC_PORT)) + client = rpc.Client(endpoint) + client.call('core.stop', []) + process = PROCESS_CACHE.remove_bgp_speaker_process(bgp_speaker_id) + if process: + process.terminate() + + +@privileged.bgp_speaker_cmd.entrypoint +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: + setns(fd, CLONE_NEWNET) + bgp_neighbor = { + neighbors.IP_ADDRESS: peer_ip, + neighbors.REMOTE_AS: remote_as, + PASSWORD: password, + } + endpoint = socket.create_connection((RPC_HOST, RPC_PORT)) + client = rpc.Client(endpoint) + client.call('neighbor.create', [bgp_neighbor]) + + +@privileged.bgp_speaker_cmd.entrypoint +def del_bgp_neighbor(bgp_speaker_id, peer_ip, ns): + LOG.info(_LI('BGPAAS: Router namespace= %(ns)s.'), {'ns': ns}) + with open(get_netns_path(ns)) as fd: + setns(fd, CLONE_NEWNET) + bgp_neighbor = { + neighbors.IP_ADDRESS: peer_ip, + } + endpoint = socket.create_connection((RPC_HOST, RPC_PORT)) + client = rpc.Client(endpoint) + client.call('neighbor.delete', [bgp_neighbor]) + + +class BgpProcess(Process): + def __init__(self, namespace, local_as, bgp_router_id, ip_version): + Process.__init__(self) + self._namespace = namespace + self._local_as = local_as + self._bgp_router_id = bgp_router_id + self._ip_version = ip_version + + def run(self): + utils.validate_as_num('local_as', self._local_as) + server_host = ('0.0.0.0',) + if self._ip_version == VERSION_IPV6: + server_host = ('::',) + bgpspeaker.BGPSpeaker( + as_number=self._local_as, + router_id=self._bgp_router_id, + bgp_server_hosts=server_host, + best_path_change_handler=self.best_path_change_event, + peer_down_handler=self.bgp_peer_down_event, + peer_up_handler=self.bgp_peer_up_event) + + hub.spawn(net_ctrl.NET_CONTROLLER.start, + **{net_ctrl.NC_RPC_BIND_IP: '0.0.0.0', + net_ctrl.NC_RPC_BIND_PORT: RPC_PORT}).wait() + + def best_path_change_event(self, event): + LOG.info(_LI("Best path change observed. cidr=%(prefix)s, " + "nexthop=%(nexthop)s, remote_as=%(remote_as)d, " + "is_withdraw=%(is_withdraw)s, " + "namespace=%(namespace)s"), + {'prefix': event.prefix, 'nexthop': event.nexthop, + 'remote_as': event.remote_as, + 'is_withdraw': event.is_withdraw, + 'namespace': self._namespace}) + + # Function for logging BGP peer. + def bgp_peer_down_event(self, remote_ip, remote_as): + LOG.info(_LI('BGP Peer %(peer_ip)s for remote_as=%(peer_as)d went ' + 'DOWN. namespace=%(namespace)s'), + {'peer_ip': remote_ip, 'peer_as': remote_as, + 'namespace': self._namespace}) + + def bgp_peer_up_event(self, remote_ip, remote_as): + LOG.info(_LI('BGP Peer %(peer_ip)s for remote_as=%(peer_as)d is UP.' + 'namespace=%(namespace)s'), + {'peer_ip': remote_ip, 'peer_as': remote_as, + 'namespace': self._namespace}) diff --git a/neutron_dynamic_routing/privileged/utils.py b/neutron_dynamic_routing/privileged/utils.py new file mode 100644 index 00000000..087f1da7 --- /dev/null +++ b/neutron_dynamic_routing/privileged/utils.py @@ -0,0 +1,32 @@ +# 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. + + +class BgpSpeakerProcessCache(): + """Class for saving multiple BGP speaker process information. This class + is currently used only for router-associated BGP speakers. In this case, + each BGP speaker associated with a router will be spawned in a seperate + process. This class holds bgp speaker id as the key and the process object + as the value. + """ + + def __init__(self): + self.cache = {} + + def put_bgp_speaker_process(self, speaker_id, process): + self.cache[speaker_id] = process + + def get_bgp_speaker_process(self, speaker_id): + return self.cache.get(speaker_id) + + def remove_bgp_speaker_process(self, speaker_id): + return self.cache.pop(speaker_id, None) diff --git a/neutron_dynamic_routing/services/bgp/agent/l3/bgp_extension.py b/neutron_dynamic_routing/services/bgp/agent/l3/bgp_extension.py new file mode 100644 index 00000000..2eaff79c --- /dev/null +++ b/neutron_dynamic_routing/services/bgp/agent/l3/bgp_extension.py @@ -0,0 +1,146 @@ +# Copyright 2021 OpenStack Foundation +# All Rights Reserved. +# +# 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.consumer import registry +from neutron.api.rpc.callbacks import events as rpc_events +from neutron.api.rpc.callbacks import resources +from neutron.api.rpc.handlers import resources_rpc +from neutron_lib.agent import l3_extension +from neutron_lib import rpc as n_rpc +from oslo_log import log as logging + +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_rpc_api +from neutron_dynamic_routing.services.bgp.common import constants as bgp_consts + +LOG = logging.getLogger(__name__) +BGP_ROUTER_ASSOC = assoc_objects.BgpSpeakerRouterAssociation.obj_name() +BGP_PEER_ASSOC = assoc_objects.BgpSpeakerPeerAssociation.obj_name() + + +class BgpAgentExtension(l3_extension.L3AgentExtension): + SUPPORTED_RESOURCE_TYPES = [BGP_ROUTER_ASSOC, BGP_PEER_ASSOC] + + def initialize(self, connection, driver_type): + LOG.debug("Initializing BgpL3AgentExtension") + resources.register_resource_class( + assoc_objects.BgpSpeakerRouterAssociation) + resources.register_resource_class( + assoc_objects.BgpSpeakerPeerAssociation) + self._register_rpc_consumers() + self.rpc_plugin = bgp_rpc_api.BgpL3ExtPluginApi( + bgp_consts.BGP_PLUGIN) + LOG.debug("Initialized BgpaasAgentExtension") + + def _register_rpc_consumers(self): + LOG.debug("Reg RPC consumers BgpaasAgentExtension") + registry.register(self._handle_router_notification, BGP_ROUTER_ASSOC) + registry.register(self._handle_peer_notification, BGP_PEER_ASSOC) + self._register_router_consumers() + self._register_peer_consumers() + LOG.debug("Registered RPC consumers BgpaasAgentExtension") + + def _register_router_consumers(self): + self._connection = n_rpc.Connection() + endpoints = [resources_rpc.ResourcesPushRpcCallback()] + topic = resources_rpc.resource_type_versioned_topic(BGP_ROUTER_ASSOC) + self._connection.create_consumer(topic, endpoints, fanout=True) + self._connection.consume_in_threads() + + def _register_peer_consumers(self): + self._connection_peer = n_rpc.Connection() + endpoints = [resources_rpc.ResourcesPushRpcCallback()] + topic_peer = resources_rpc.resource_type_versioned_topic( + BGP_PEER_ASSOC) + self._connection_peer.create_consumer(topic_peer, endpoints, + fanout=True) + self._connection_peer.consume_in_threads() + + def consume_api(self, agent_api): + self.agent_api = agent_api + + def _handle_router_notification(self, context, resource_type, + router_assocs, event_type): + LOG.debug('Received router associated push event %s for resource %s,' + 'with router associations %s', event_type, resource_type, + router_assocs) + for assoc in router_assocs: + router_info = self.agent_api.get_router_info(assoc.router_id) + sp = self.rpc_plugin.get_bgp_speaker_info(context, + assoc.bgp_speaker_id) + if event_type == rpc_events.CREATED: + priv_driver.add_bgp_speaker(assoc.bgp_speaker_id, + sp['local_as'], '127.0.0.1', + router_info.ns_name, + sp['ip_version']) + elif event_type == rpc_events.DELETED: + priv_driver.del_bgp_speaker(assoc.bgp_speaker_id, + router_info.ns_name) + + def _handle_peer_notification(self, context, resource_type, + peer_assocs, event_type): + LOG.debug('Received peer associated push event %s for resource %s,' + 'with peer associations %s', event_type, resource_type, + peer_assocs) + for peer_assoc in peer_assocs: + sp = self.rpc_plugin.get_bgp_speaker_info( + context, + peer_assoc.bgp_speaker_id) + peer = self.rpc_plugin.get_bgp_peer_info(context, + peer_assoc.peer_id) + for router_assoc in sp['router_associations']: + router_id = router_assoc['router_id'] + router_info = self.agent_api.get_router_info(router_id) + if event_type == rpc_events.CREATED: + priv_driver.add_bgp_neighbor(peer_assoc.bgp_speaker_id, + peer['peer_ip'], + peer['remote_as'], + router_info.ns_name) + elif event_type == rpc_events.DELETED: + priv_driver.del_bgp_neighbor(peer_assoc.bgp_speaker_id, + peer['peer_ip'], + router_info.ns_name) + + def add_router(self, context, data): + """Handle a router add event. + + Called on router create. + + :param context: RPC context. + :param data: Router data. + """ + pass + + def update_router(self, context, data): + """Handle a router update event. + + Called on router update. + + :param context: RPC context. + :param data: Router data. + """ + pass + + def delete_router(self, context, data): + """Handle a router delete event. + + :param context: RPC context. + :param data: Router data. + """ + pass + + def ha_state_change(self, context, data): + pass 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 new file mode 100644 index 00000000..8126675b --- /dev/null +++ b/neutron_dynamic_routing/services/bgp/agent/l3/bgp_rpc_api.py @@ -0,0 +1,45 @@ +# Copyright 2021 OpenStack Foundation +# All Rights Reserved. +# +# 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_lib import rpc as n_rpc +import oslo_messaging + + +class BgpL3ExtPluginApi(object): + """Agent side of the bgp l3 agent ext RPC API. + + This class implements the client side of an rpc interface. + The server side of this interface can be found in + api.rpc.handlers.bgp_speaker_rpc.BgpSpeakerRpcCallback. + + API version history: + 1.0 - Initial version. + """ + + def __init__(self, topic): + target = oslo_messaging.Target(topic=topic, version='1.0') + self.client = n_rpc.get_client(target) + + def get_bgp_speaker_info(self, context, bgp_speaker_id): + """Make a remote process call to retrieve a BGP speaker info.""" + cctxt = self.client.prepare() + return cctxt.call(context, 'get_bgp_speaker_info', + bgp_speaker_id=bgp_speaker_id) + + def get_bgp_peer_info(self, context, bgp_peer_id): + """Make a remote process call to retrieve a BGP peer info.""" + cctxt = self.client.prepare() + return cctxt.call(context, 'get_bgp_peer_info', + bgp_peer_id=bgp_peer_id) diff --git a/neutron_dynamic_routing/services/bgp/bgp_plugin.py b/neutron_dynamic_routing/services/bgp/bgp_plugin.py index 4c8eff41..cdbb6b4a 100644 --- a/neutron_dynamic_routing/services/bgp/bgp_plugin.py +++ b/neutron_dynamic_routing/services/bgp/bgp_plugin.py @@ -14,6 +14,9 @@ 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 from neutron_lib.api.definitions import bgp_4byte_asn from neutron_lib.api.definitions import bgp_associations @@ -35,6 +38,7 @@ 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.objects import bgp_associations as assoc_objects from neutron_dynamic_routing.services.bgp.common import constants as bgp_consts PLUGIN_NAME = bgp_ext.ALIAS + '_svc_plugin' @@ -57,6 +61,8 @@ class BgpPlugin(service_base.ServicePluginBase, self._setup_rpc() self._register_callbacks() self.add_periodic_dragent_status_check() + self._register_resources() + self.push_api = resources_rpc.ResourcesPushRpcApi() def get_plugin_type(self): return bgp_ext.ALIAS @@ -104,6 +110,12 @@ class BgpPlugin(service_base.ServicePluginBase, resources.PORT, events.AFTER_UPDATE) + def _register_resources(self): + resources_registry.register_resource_class( + assoc_objects.BgpSpeakerRouterAssociation) + resources_registry.register_resource_class( + assoc_objects.BgpSpeakerPeerAssociation) + def get_bgp_speakers(self, context, filters=None, fields=None, sorts=None, limit=None, marker=None, page_reverse=False): @@ -245,9 +257,16 @@ class BgpPlugin(service_base.ServicePluginBase, def create_bgp_speaker_router_association(self, context, bgp_speaker_id, router_association): - return super(BgpPlugin, self).create_bgp_speaker_router_association( - context, bgp_speaker_id, - router_association) + assoc_info = super(BgpPlugin, self)\ + .create_bgp_speaker_router_association(context, + 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) + return assoc_info def update_bgp_speaker_router_association(self, context, assoc_id, bgp_speaker_id, router_assoc): @@ -257,8 +276,13 @@ class BgpPlugin(service_base.ServicePluginBase, def delete_bgp_speaker_router_association(self, context, id, bgp_speaker_id): - super(BgpPlugin, self).delete_bgp_speaker_router_association(context, - 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) def get_bgp_speaker_peer_associations( self, context, bgp_speaker_id, @@ -280,14 +304,24 @@ class BgpPlugin(service_base.ServicePluginBase, def create_bgp_speaker_peer_association(self, context, bgp_speaker_id, peer_association): - return super(BgpPlugin, self).create_bgp_speaker_peer_association( - context, bgp_speaker_id, - peer_association) + assoc_info = super(BgpPlugin, self)\ + .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) + return assoc_info def delete_bgp_speaker_peer_association(self, context, id, bgp_speaker_id): - super(BgpPlugin, self).delete_bgp_speaker_peer_association( - context, 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) def get_routes(self, context, bgp_speaker_id): return super(BgpPlugin, self).get_routes(context, diff --git a/setup.cfg b/setup.cfg index d5db0b74..af8de43b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,3 +37,5 @@ neutron.policies = neutron-dynamic-routing = neutron_dynamic_routing.policies:list_rules neutron.service_plugins = bgp = neutron_dynamic_routing.services.bgp.bgp_plugin:BgpPlugin +neutron.agent.l3.extensions = + bgp-ext = neutron_dynamic_routing.services.bgp.agent.l3.bgp_extension:BgpAgentExtension