diff --git a/neutron_dynamic_routing/api/__init__.py b/neutron_dynamic_routing/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/api/rpc/__init__.py b/neutron_dynamic_routing/api/rpc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/api/rpc/agentnotifiers/__init__.py b/neutron_dynamic_routing/api/rpc/agentnotifiers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/api/rpc/agentnotifiers/bgp_dr_rpc_agent_api.py b/neutron_dynamic_routing/api/rpc/agentnotifiers/bgp_dr_rpc_agent_api.py new file mode 100644 index 00000000..89d8dc10 --- /dev/null +++ b/neutron_dynamic_routing/api/rpc/agentnotifiers/bgp_dr_rpc_agent_api.py @@ -0,0 +1,107 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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 oslo_messaging + +from neutron.common import rpc as n_rpc + +from neutron_dynamic_routing.services.bgp.common import constants as bgp_consts + + +class BgpDrAgentNotifyApi(object): + """API for plugin to notify BGP DrAgent. + + This class implements the client side of an rpc interface. The server side + is neutron_dynamic_routing.services.bgp.agent.bgp_dragent.BgpDrAgent. For + more information about rpc interfaces, please see + http://docs.openstack.org/developer/neutron/devref/rpc_api.html. + """ + + def __init__(self, topic=bgp_consts.BGP_DRAGENT): + target = oslo_messaging.Target(topic=topic, version='1.0') + self.client = n_rpc.get_client(target) + self.topic = topic + + def bgp_routes_advertisement(self, context, bgp_speaker_id, + routes, host): + """Tell BgpDrAgent to begin advertising the given route. + + Invoked on FIP association, adding router port to a tenant network, + and new DVR port-host bindings, and subnet creation(?). + """ + self._notification_host_cast(context, 'bgp_routes_advertisement_end', + {'advertise_routes': {'speaker_id': bgp_speaker_id, + 'routes': routes}}, host) + + def bgp_routes_withdrawal(self, context, bgp_speaker_id, + routes, host): + """Tell BgpDrAgent to stop advertising the given route. + + Invoked on FIP disassociation, removal of a router port on a + network, and removal of DVR port-host binding, and subnet delete(?). + """ + self._notification_host_cast(context, 'bgp_routes_withdrawal_end', + {'withdraw_routes': {'speaker_id': bgp_speaker_id, + 'routes': routes}}, host) + + def bgp_peer_disassociated(self, context, bgp_speaker_id, + bgp_peer_ip, host): + """Tell BgpDrAgent about a new BGP Peer association. + + This effectively tells the BgpDrAgent to stop a peering session. + """ + self._notification_host_cast(context, 'bgp_peer_disassociation_end', + {'bgp_peer': {'speaker_id': bgp_speaker_id, + 'peer_ip': bgp_peer_ip}}, host) + + def bgp_peer_associated(self, context, bgp_speaker_id, + bgp_peer_id, host): + """Tell BgpDrAgent about a BGP Peer disassociation. + + This effectively tells the bgp_dragent to open a peering session. + """ + self._notification_host_cast(context, 'bgp_peer_association_end', + {'bgp_peer': {'speaker_id': bgp_speaker_id, + 'peer_id': bgp_peer_id}}, host) + + def bgp_speaker_created(self, context, bgp_speaker_id, host): + """Tell BgpDrAgent about the creation of a BGP Speaker. + + Because a BGP Speaker can be created with BgpPeer binding in place, + we need to inform the BgpDrAgent of a new BGP Speaker in case a + peering session needs to opened immediately. + """ + self._notification_host_cast(context, 'bgp_speaker_create_end', + {'bgp_speaker': {'id': bgp_speaker_id}}, host) + + def bgp_speaker_removed(self, context, bgp_speaker_id, host): + """Tell BgpDrAgent about the removal of a BGP Speaker. + + Because a BGP Speaker can be removed with BGP Peer binding in + place, we need to inform the BgpDrAgent of the removal of a + BGP Speaker in case peering sessions need to be stopped. + """ + self._notification_host_cast(context, 'bgp_speaker_remove_end', + {'bgp_speaker': {'id': bgp_speaker_id}}, host) + + def _notification_host_cast(self, context, method, payload, host): + """Send payload to BgpDrAgent in the cast mode""" + cctxt = self.client.prepare(topic=self.topic, server=host) + cctxt.cast(context, method, payload=payload) + + def _notification_host_call(self, context, method, payload, host): + """Send payload to BgpDrAgent in the call mode""" + cctxt = self.client.prepare(topic=self.topic, server=host) + cctxt.call(context, method, payload=payload) diff --git a/neutron_dynamic_routing/api/rpc/handlers/__init__.py b/neutron_dynamic_routing/api/rpc/handlers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/api/rpc/handlers/bgp_speaker_rpc.py b/neutron_dynamic_routing/api/rpc/handlers/bgp_speaker_rpc.py new file mode 100644 index 00000000..6b96288d --- /dev/null +++ b/neutron_dynamic_routing/api/rpc/handlers/bgp_speaker_rpc.py @@ -0,0 +1,66 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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 oslo_messaging + +from neutron import manager + +from neutron_dynamic_routing.extensions import bgp as bgp_ext + + +class BgpSpeakerRpcCallback(object): + """BgpDrAgent RPC callback in plugin implementations. + + This class implements the server side of an RPC interface. + The client side of this interface can be found in + neutron_dynamic_routing.services.bgp.agent.bgp_dragent.BgpDrPluginApi. + For more information about changing RPC interfaces, + see http://docs.openstack.org/developer/neutron/devref/rpc_api.html. + """ + + # API version history: + # 1.0 BGPDRPluginApi BASE_RPC_API_VERSION + target = oslo_messaging.Target(version='1.0') + + @property + def plugin(self): + if not hasattr(self, '_plugin'): + self._plugin = manager.NeutronManager.get_service_plugins().get( + bgp_ext.BGP_EXT_ALIAS) + return self._plugin + + def get_bgp_speaker_info(self, context, bgp_speaker_id): + """Return BGP Speaker details such as peer list and local_as. + + Invoked by the BgpDrAgent to lookup the details of a BGP Speaker. + """ + return self.plugin.get_bgp_speaker_with_advertised_routes( + context, bgp_speaker_id) + + def get_bgp_peer_info(self, context, bgp_peer_id): + """Return BgpPeer details such as IP, remote_as, and credentials. + + Invoked by the BgpDrAgent to lookup the details of a BGP peer. + """ + return self.plugin.get_bgp_peer(context, bgp_peer_id, + ['peer_ip', 'remote_as', + 'auth_type', 'password']) + + def get_bgp_speakers(self, context, host=None, **kwargs): + """Returns the list of all BgpSpeakers. + + Typically invoked by the BgpDrAgent as part of its bootstrap process. + """ + return self.plugin.get_bgp_speakers_for_agent_host(context, host) diff --git a/neutron_dynamic_routing/cmd/__init__.py b/neutron_dynamic_routing/cmd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/unit/test_neutron_dynamic_routing.py b/neutron_dynamic_routing/cmd/eventlet/__init__.py similarity index 55% rename from neutron_dynamic_routing/tests/unit/test_neutron_dynamic_routing.py rename to neutron_dynamic_routing/cmd/eventlet/__init__.py index 526df599..01f9f693 100644 --- a/neutron_dynamic_routing/tests/unit/test_neutron_dynamic_routing.py +++ b/neutron_dynamic_routing/cmd/eventlet/__init__.py @@ -1,6 +1,3 @@ -# Copyright (c) 2016 Huawei Technologies India Pvt Ltd. -# 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 @@ -13,18 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -""" Tests for `neutron_dynamic_routing` module""" +from neutron.common import eventlet_utils -from oslotest import base - - -class TestNeutron_dynamic_routing(base.BaseTestCase): - """TestNeutron_dynamic_routing base class""" - - def setUp(self): - """setUp function""" - super(TestNeutron_dynamic_routing, self).setUp() - - def test_dummy(self): - """Added dummy test just for test""" - pass +eventlet_utils.monkey_patch() diff --git a/neutron_dynamic_routing/cmd/eventlet/agents/__init__.py b/neutron_dynamic_routing/cmd/eventlet/agents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/cmd/eventlet/agents/bgp_dragent.py b/neutron_dynamic_routing/cmd/eventlet/agents/bgp_dragent.py new file mode 100644 index 00000000..39fcef18 --- /dev/null +++ b/neutron_dynamic_routing/cmd/eventlet/agents/bgp_dragent.py @@ -0,0 +1,20 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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.services.bgp.agent import entry as bgp_dragent + + +def main(): + bgp_dragent.main() diff --git a/neutron_dynamic_routing/db/bgp_db.py b/neutron_dynamic_routing/db/bgp_db.py new file mode 100644 index 00000000..21976adc --- /dev/null +++ b/neutron_dynamic_routing/db/bgp_db.py @@ -0,0 +1,1011 @@ +# Copyright 2016 Hewlett Packard Enterprise Development Company LP +# +# 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 itertools + +from oslo_db import exception as oslo_db_exc +from oslo_log import log as logging +from oslo_utils import uuidutils +import sqlalchemy as sa +from sqlalchemy import and_ +from sqlalchemy import orm +from sqlalchemy.orm import aliased +from sqlalchemy.orm import exc as sa_exc + +from neutron_lib import constants as lib_consts +from neutron_lib import exceptions as n_exc + +from neutron.api.v2 import attributes as attr +from neutron.db import address_scope_db +from neutron.db import common_db_mixin as common_db +from neutron.db import l3_attrs_db +from neutron.db import l3_db +from neutron.db import model_base +from neutron.db import models_v2 +from neutron.plugins.ml2 import models as ml2_models + +from neutron_dynamic_routing._i18n import _ +from neutron_dynamic_routing.extensions import bgp as bgp_ext + + +LOG = logging.getLogger(__name__) +DEVICE_OWNER_ROUTER_GW = lib_consts.DEVICE_OWNER_ROUTER_GW +DEVICE_OWNER_ROUTER_INTF = lib_consts.DEVICE_OWNER_ROUTER_INTF + + +class BgpSpeakerPeerBinding(model_base.BASEV2): + + """Represents a mapping between BGP speaker and BGP peer""" + + __tablename__ = 'bgp_speaker_peer_bindings' + + bgp_speaker_id = sa.Column(sa.String(length=36), + sa.ForeignKey('bgp_speakers.id', + ondelete='CASCADE'), + nullable=False, + primary_key=True) + bgp_peer_id = sa.Column(sa.String(length=36), + sa.ForeignKey('bgp_peers.id', + ondelete='CASCADE'), + nullable=False, + primary_key=True) + + +class BgpSpeakerNetworkBinding(model_base.BASEV2): + + """Represents a mapping between a network and BGP speaker""" + + __tablename__ = 'bgp_speaker_network_bindings' + + bgp_speaker_id = sa.Column(sa.String(length=36), + sa.ForeignKey('bgp_speakers.id', + ondelete='CASCADE'), + nullable=False, + primary_key=True) + network_id = sa.Column(sa.String(length=36), + sa.ForeignKey('networks.id', + ondelete='CASCADE'), + nullable=False, + primary_key=True) + ip_version = sa.Column(sa.Integer, nullable=False, autoincrement=False, + primary_key=True) + + +class BgpSpeaker(model_base.BASEV2, + model_base.HasId, + model_base.HasTenant): + + """Represents a BGP speaker""" + + __tablename__ = 'bgp_speakers' + + name = sa.Column(sa.String(attr.NAME_MAX_LEN), nullable=False) + local_as = sa.Column(sa.Integer, nullable=False, autoincrement=False) + advertise_floating_ip_host_routes = sa.Column(sa.Boolean, nullable=False) + advertise_tenant_networks = sa.Column(sa.Boolean, nullable=False) + peers = orm.relationship(BgpSpeakerPeerBinding, + backref='bgp_speaker_peer_bindings', + cascade='all, delete, delete-orphan', + lazy='joined') + networks = orm.relationship(BgpSpeakerNetworkBinding, + backref='bgp_speaker_network_bindings', + cascade='all, delete, delete-orphan', + lazy='joined') + ip_version = sa.Column(sa.Integer, nullable=False, autoincrement=False) + + +class BgpPeer(model_base.BASEV2, + model_base.HasId, + model_base.HasTenant): + + """Represents a BGP routing peer.""" + + __tablename__ = 'bgp_peers' + + name = sa.Column(sa.String(attr.NAME_MAX_LEN), nullable=False) + peer_ip = sa.Column(sa.String(64), + nullable=False) + remote_as = sa.Column(sa.Integer, nullable=False, autoincrement=False) + auth_type = sa.Column(sa.String(16), nullable=False) + password = sa.Column(sa.String(255), nullable=True) + + +class BgpDbMixin(common_db.CommonDbMixin): + + def create_bgp_speaker(self, context, bgp_speaker): + uuid = uuidutils.generate_uuid() + self._save_bgp_speaker(context, bgp_speaker, uuid) + return self.get_bgp_speaker(context, uuid) + + def get_bgp_speakers(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + with context.session.begin(subtransactions=True): + return self._get_collection(context, BgpSpeaker, + self._make_bgp_speaker_dict, + filters=filters, fields=fields, + sorts=sorts, limit=limit, + page_reverse=page_reverse) + + def get_bgp_speaker(self, context, bgp_speaker_id, fields=None): + with context.session.begin(subtransactions=True): + bgp_speaker = self._get_bgp_speaker(context, bgp_speaker_id) + return self._make_bgp_speaker_dict(bgp_speaker, fields) + + def get_bgp_speaker_with_advertised_routes(self, context, + bgp_speaker_id): + bgp_speaker_attrs = ['id', 'local_as', 'tenant_id'] + bgp_peer_attrs = ['peer_ip', 'remote_as', 'password'] + with context.session.begin(subtransactions=True): + bgp_speaker = self.get_bgp_speaker(context, bgp_speaker_id, + fields=bgp_speaker_attrs) + res = dict((k, bgp_speaker[k]) for k in bgp_speaker_attrs) + res['peers'] = self.get_bgp_peers_by_bgp_speaker(context, + bgp_speaker['id'], + fields=bgp_peer_attrs) + res['advertised_routes'] = self.get_routes_by_bgp_speaker_id( + context, + bgp_speaker_id) + return res + + def update_bgp_speaker(self, context, bgp_speaker_id, bgp_speaker): + bp = bgp_speaker[bgp_ext.BGP_SPEAKER_BODY_KEY_NAME] + with context.session.begin(subtransactions=True): + bgp_speaker_db = self._get_bgp_speaker(context, bgp_speaker_id) + bgp_speaker_db.update(bp) + + bgp_speaker_dict = self._make_bgp_speaker_dict(bgp_speaker_db) + return bgp_speaker_dict + + def _save_bgp_speaker(self, context, bgp_speaker, uuid): + ri = bgp_speaker[bgp_ext.BGP_SPEAKER_BODY_KEY_NAME] + ri['tenant_id'] = context.tenant_id + with context.session.begin(subtransactions=True): + res_keys = ['local_as', 'tenant_id', 'name', 'ip_version', + 'advertise_floating_ip_host_routes', + 'advertise_tenant_networks'] + res = dict((k, ri[k]) for k in res_keys) + res['id'] = uuid + bgp_speaker_db = BgpSpeaker(**res) + context.session.add(bgp_speaker_db) + + def add_bgp_peer(self, context, bgp_speaker_id, bgp_peer_info): + bgp_peer_id = self._get_id_for(bgp_peer_info, 'bgp_peer_id') + self._save_bgp_speaker_peer_binding(context, + bgp_speaker_id, + bgp_peer_id) + return {'bgp_peer_id': bgp_peer_id} + + def remove_bgp_peer(self, context, bgp_speaker_id, bgp_peer_info): + bgp_peer_id = self._get_id_for(bgp_peer_info, 'bgp_peer_id') + self._remove_bgp_speaker_peer_binding(context, + bgp_speaker_id, + bgp_peer_id) + return {'bgp_peer_id': bgp_peer_id} + + def add_gateway_network(self, context, bgp_speaker_id, network_info): + network_id = self._get_id_for(network_info, 'network_id') + with context.session.begin(subtransactions=True): + try: + self._save_bgp_speaker_network_binding(context, + bgp_speaker_id, + network_id) + except oslo_db_exc.DBDuplicateEntry: + raise bgp_ext.BgpSpeakerNetworkBindingError( + network_id=network_id, + bgp_speaker_id=bgp_speaker_id) + return {'network_id': network_id} + + def remove_gateway_network(self, context, bgp_speaker_id, network_info): + with context.session.begin(subtransactions=True): + network_id = self._get_id_for(network_info, 'network_id') + self._remove_bgp_speaker_network_binding(context, + bgp_speaker_id, + network_id) + return {'network_id': network_id} + + def delete_bgp_speaker(self, context, bgp_speaker_id): + with context.session.begin(subtransactions=True): + bgp_speaker_db = self._get_bgp_speaker(context, bgp_speaker_id) + context.session.delete(bgp_speaker_db) + + def create_bgp_peer(self, context, bgp_peer): + ri = bgp_peer[bgp_ext.BGP_PEER_BODY_KEY_NAME] + auth_type = ri.get('auth_type') + password = ri.get('password') + if auth_type == 'md5' and not password: + raise bgp_ext.InvalidBgpPeerMd5Authentication() + + with context.session.begin(subtransactions=True): + res_keys = ['tenant_id', 'name', 'remote_as', 'peer_ip', + 'auth_type', 'password'] + res = dict((k, ri[k]) for k in res_keys) + res['id'] = uuidutils.generate_uuid() + bgp_peer_db = BgpPeer(**res) + context.session.add(bgp_peer_db) + peer = self._make_bgp_peer_dict(bgp_peer_db) + peer.pop('password') + return peer + + def get_bgp_peers(self, context, fields=None, filters=None, sorts=None, + limit=None, marker=None, page_reverse=False): + return self._get_collection(context, BgpPeer, + self._make_bgp_peer_dict, + filters=filters, fields=fields, + sorts=sorts, limit=limit, + page_reverse=page_reverse) + + def get_bgp_peers_by_bgp_speaker(self, context, + bgp_speaker_id, fields=None): + filters = [BgpSpeakerPeerBinding.bgp_speaker_id == bgp_speaker_id, + BgpSpeakerPeerBinding.bgp_peer_id == BgpPeer.id] + with context.session.begin(subtransactions=True): + query = context.session.query(BgpPeer) + query = query.filter(*filters) + return [self._make_bgp_peer_dict(x) for x in query.all()] + + def get_bgp_peer(self, context, bgp_peer_id, fields=None): + bgp_peer_db = self._get_bgp_peer(context, bgp_peer_id) + return self._make_bgp_peer_dict(bgp_peer_db, fields=fields) + + def delete_bgp_peer(self, context, bgp_peer_id): + with context.session.begin(subtransactions=True): + bgp_peer_db = self._get_bgp_peer(context, bgp_peer_id) + context.session.delete(bgp_peer_db) + + def update_bgp_peer(self, context, bgp_peer_id, bgp_peer): + bp = bgp_peer[bgp_ext.BGP_PEER_BODY_KEY_NAME] + with context.session.begin(subtransactions=True): + bgp_peer_db = self._get_bgp_peer(context, bgp_peer_id) + if ((bp['password'] is not None) and + (bgp_peer_db['auth_type'] == 'none')): + raise bgp_ext.BgpPeerNotAuthenticated(bgp_peer_id=bgp_peer_id) + bgp_peer_db.update(bp) + + bgp_peer_dict = self._make_bgp_peer_dict(bgp_peer_db) + return bgp_peer_dict + + def _get_bgp_speaker(self, context, bgp_speaker_id): + try: + return self._get_by_id(context, BgpSpeaker, + bgp_speaker_id) + except sa_exc.NoResultFound: + raise bgp_ext.BgpSpeakerNotFound(id=bgp_speaker_id) + + def _get_bgp_speaker_ids_by_router(self, context, router_id): + with context.session.begin(subtransactions=True): + network_binding = aliased(BgpSpeakerNetworkBinding) + r_port = aliased(l3_db.RouterPort) + query = context.session.query(network_binding.bgp_speaker_id) + query = query.filter( + r_port.router_id == router_id, + r_port.port_type == lib_consts.DEVICE_OWNER_ROUTER_GW, + r_port.port_id == models_v2.Port.id, + models_v2.Port.network_id == network_binding.network_id) + + return [binding.bgp_speaker_id for binding in query.all()] + + def _get_bgp_speaker_ids_by_binding_network(self, context, network_id): + with context.session.begin(subtransactions=True): + query = context.session.query( + BgpSpeakerNetworkBinding.bgp_speaker_id) + query = query.filter( + BgpSpeakerNetworkBinding.network_id == network_id) + return query.all() + + def get_advertised_routes(self, context, bgp_speaker_id): + routes = self.get_routes_by_bgp_speaker_id(context, bgp_speaker_id) + return self._make_advertised_routes_dict(routes) + + def _get_id_for(self, resource, id_name): + try: + return resource.get(id_name) + except AttributeError: + msg = _("%s must be specified") % id_name + raise n_exc.BadRequest(resource=bgp_ext.BGP_SPEAKER_RESOURCE_NAME, + msg=msg) + + def _get_bgp_peers_by_bgp_speaker_binding(self, context, bgp_speaker_id): + with context.session.begin(subtransactions=True): + query = context.session.query(BgpPeer) + query = query.filter( + BgpSpeakerPeerBinding.bgp_speaker_id == bgp_speaker_id, + BgpSpeakerPeerBinding.bgp_peer_id == BgpPeer.id) + return query.all() + + def _save_bgp_speaker_peer_binding(self, context, bgp_speaker_id, + bgp_peer_id): + with context.session.begin(subtransactions=True): + try: + bgp_speaker = self._get_by_id(context, BgpSpeaker, + bgp_speaker_id) + except sa_exc.NoResultFound: + raise bgp_ext.BgpSpeakerNotFound(id=bgp_speaker_id) + + try: + bgp_peer = self._get_by_id(context, BgpPeer, + bgp_peer_id) + except sa_exc.NoResultFound: + raise bgp_ext.BgpPeerNotFound(id=bgp_peer_id) + + peers = self._get_bgp_peers_by_bgp_speaker_binding(context, + bgp_speaker_id) + self._validate_peer_ips(bgp_speaker_id, peers, bgp_peer) + binding = BgpSpeakerPeerBinding(bgp_speaker_id=bgp_speaker.id, + bgp_peer_id=bgp_peer.id) + context.session.add(binding) + + def _validate_peer_ips(self, bgp_speaker_id, current_peers, new_peer): + for peer in current_peers: + if peer.peer_ip == new_peer.peer_ip: + raise bgp_ext.DuplicateBgpPeerIpException( + bgp_peer_id=new_peer.id, + peer_ip=new_peer.peer_ip, + bgp_speaker_id=bgp_speaker_id) + + def _remove_bgp_speaker_peer_binding(self, context, bgp_speaker_id, + bgp_peer_id): + with context.session.begin(subtransactions=True): + + try: + binding = self._get_bgp_speaker_peer_binding(context, + bgp_speaker_id, + bgp_peer_id) + except sa_exc.NoResultFound: + raise bgp_ext.BgpSpeakerPeerNotAssociated( + bgp_peer_id=bgp_peer_id, + bgp_speaker_id=bgp_speaker_id) + context.session.delete(binding) + + def _save_bgp_speaker_network_binding(self, + context, + bgp_speaker_id, + network_id): + with context.session.begin(subtransactions=True): + try: + bgp_speaker = self._get_by_id(context, BgpSpeaker, + bgp_speaker_id) + except sa_exc.NoResultFound: + raise bgp_ext.BgpSpeakerNotFound(id=bgp_speaker_id) + + try: + network = self._get_by_id(context, models_v2.Network, + network_id) + except sa_exc.NoResultFound: + raise n_exc.NetworkNotFound(net_id=network_id) + + binding = BgpSpeakerNetworkBinding( + bgp_speaker_id=bgp_speaker.id, + network_id=network.id, + ip_version=bgp_speaker.ip_version) + context.session.add(binding) + + def _remove_bgp_speaker_network_binding(self, context, + bgp_speaker_id, network_id): + with context.session.begin(subtransactions=True): + + try: + binding = self._get_bgp_speaker_network_binding( + context, + bgp_speaker_id, + network_id) + except sa_exc.NoResultFound: + raise bgp_ext.BgpSpeakerNetworkNotAssociated( + network_id=network_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'] + 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] + return self._fields(res, fields) + + def _make_advertised_routes_dict(self, routes): + return {'advertised_routes': list(routes)} + + def _get_bgp_peer(self, context, bgp_peer_id): + try: + return self._get_by_id(context, BgpPeer, bgp_peer_id) + except sa_exc.NoResultFound: + raise bgp_ext.BgpPeerNotFound(id=bgp_peer_id) + + def _get_bgp_speaker_peer_binding(self, context, + bgp_speaker_id, bgp_peer_id): + query = self._model_query(context, BgpSpeakerPeerBinding) + return query.filter( + BgpSpeakerPeerBinding.bgp_speaker_id == bgp_speaker_id, + BgpSpeakerPeerBinding.bgp_peer_id == bgp_peer_id).one() + + def _get_bgp_speaker_network_binding(self, context, + bgp_speaker_id, network_id): + query = self._model_query(context, BgpSpeakerNetworkBinding) + return query.filter(bgp_speaker_id == bgp_speaker_id, + network_id == network_id).one() + + def _make_bgp_peer_dict(self, bgp_peer, fields=None): + attrs = ['tenant_id', 'id', 'name', 'peer_ip', 'remote_as', + 'auth_type', 'password'] + res = dict((k, bgp_peer[k]) for k in attrs) + return self._fields(res, fields) + + def _get_address_scope_ids_for_bgp_speaker(self, context, bgp_speaker_id): + with context.session.begin(subtransactions=True): + binding = aliased(BgpSpeakerNetworkBinding) + address_scope = aliased(address_scope_db.AddressScope) + query = context.session.query(address_scope) + query = query.filter( + binding.bgp_speaker_id == bgp_speaker_id, + models_v2.Subnet.ip_version == binding.ip_version, + models_v2.Subnet.network_id == binding.network_id, + models_v2.Subnet.subnetpool_id == models_v2.SubnetPool.id, + models_v2.SubnetPool.address_scope_id == address_scope.id) + return [scope.id for scope in query.all()] + + def get_routes_by_bgp_speaker_id(self, context, bgp_speaker_id): + """Get all routes that should be advertised by a BgpSpeaker.""" + with context.session.begin(subtransactions=True): + net_routes = self._get_tenant_network_routes_by_bgp_speaker( + context, + bgp_speaker_id) + fip_routes = self._get_central_fip_host_routes_by_bgp_speaker( + context, + bgp_speaker_id) + dvr_fip_routes = self._get_dvr_fip_host_routes_by_bgp_speaker( + context, + bgp_speaker_id) + return itertools.chain(fip_routes, net_routes, dvr_fip_routes) + + def get_routes_by_bgp_speaker_binding(self, context, + bgp_speaker_id, network_id): + """Get all routes for the given bgp_speaker binding.""" + with context.session.begin(subtransactions=True): + fip_routes = self._get_central_fip_host_routes_by_binding( + context, + network_id, + bgp_speaker_id) + net_routes = self._get_tenant_network_routes_by_binding( + context, + network_id, + bgp_speaker_id) + dvr_fip_routes = self._get_dvr_fip_host_routes_by_binding( + context, + network_id, + bgp_speaker_id) + return itertools.chain(fip_routes, net_routes, dvr_fip_routes) + + def _get_routes_by_router(self, context, router_id): + bgp_speaker_ids = self._get_bgp_speaker_ids_by_router(context, + router_id) + route_dict = {} + for bgp_speaker_id in bgp_speaker_ids: + fip_routes = self._get_central_fip_host_routes_by_router( + context, + router_id, + bgp_speaker_id) + net_routes = self._get_tenant_network_routes_by_router( + context, + router_id, + bgp_speaker_id) + dvr_fip_routes = self._get_dvr_fip_host_routes_by_router( + context, + router_id, + bgp_speaker_id) + routes = itertools.chain(fip_routes, net_routes, dvr_fip_routes) + route_dict[bgp_speaker_id] = list(routes) + return route_dict + + def _get_central_fip_host_routes_by_router(self, context, router_id, + bgp_speaker_id): + """Get floating IP host routes with the given router as nexthop.""" + with context.session.begin(subtransactions=True): + dest_alias = aliased(l3_db.FloatingIP, + name='destination') + next_hop_alias = aliased(models_v2.IPAllocation, + name='next_hop') + binding_alias = aliased(BgpSpeakerNetworkBinding, + name='binding') + router_attrs = aliased(l3_attrs_db.RouterExtraAttributes, + name='router_attrs') + query = context.session.query(dest_alias.floating_ip_address, + next_hop_alias.ip_address) + query = query.join( + next_hop_alias, + next_hop_alias.network_id == dest_alias.floating_network_id) + query = query.join(l3_db.Router, + dest_alias.router_id == l3_db.Router.id) + query = query.filter( + l3_db.Router.id == router_id, + dest_alias.router_id == l3_db.Router.id, + l3_db.Router.id == router_attrs.router_id, + router_attrs.distributed == sa.sql.false(), + l3_db.Router.gw_port_id == next_hop_alias.port_id, + next_hop_alias.subnet_id == models_v2.Subnet.id, + models_v2.Subnet.ip_version == 4, + binding_alias.network_id == models_v2.Subnet.network_id, + binding_alias.bgp_speaker_id == bgp_speaker_id, + binding_alias.ip_version == 4, + BgpSpeaker.advertise_floating_ip_host_routes == sa.sql.true()) + query = query.outerjoin(router_attrs, + l3_db.Router.id == router_attrs.router_id) + query = query.filter(router_attrs.distributed != sa.sql.true()) + return self._host_route_list_from_tuples(query.all()) + + def _get_dvr_fip_host_routes_by_router(self, context, bgp_speaker_id, + router_id): + with context.session.begin(subtransactions=True): + gw_query = self._get_gateway_query(context, bgp_speaker_id) + + fip_query = self._get_fip_query(context, bgp_speaker_id) + fip_query.filter(l3_db.FloatingIP.router_id == router_id) + + #Create the join query + join_query = self._join_fip_by_host_binding_to_agent_gateway( + context, + fip_query.subquery(), + gw_query.subquery()) + return self._host_route_list_from_tuples(join_query.all()) + + def _get_central_fip_host_routes_by_binding(self, context, + network_id, bgp_speaker_id): + """Get all floating IP host routes for the given network binding.""" + with context.session.begin(subtransactions=True): + # Query the DB for floating IP's and the IP address of the + # gateway port + dest_alias = aliased(l3_db.FloatingIP, + name='destination') + next_hop_alias = aliased(models_v2.IPAllocation, + name='next_hop') + binding_alias = aliased(BgpSpeakerNetworkBinding, + name='binding') + router_attrs = aliased(l3_attrs_db.RouterExtraAttributes, + name='router_attrs') + query = context.session.query(dest_alias.floating_ip_address, + next_hop_alias.ip_address) + query = query.join( + next_hop_alias, + next_hop_alias.network_id == dest_alias.floating_network_id) + query = query.join( + binding_alias, + binding_alias.network_id == dest_alias.floating_network_id) + query = query.join(l3_db.Router, + dest_alias.router_id == l3_db.Router.id) + query = query.filter( + dest_alias.floating_network_id == network_id, + dest_alias.router_id == l3_db.Router.id, + l3_db.Router.gw_port_id == next_hop_alias.port_id, + next_hop_alias.subnet_id == models_v2.Subnet.id, + models_v2.Subnet.ip_version == 4, + binding_alias.network_id == models_v2.Subnet.network_id, + binding_alias.bgp_speaker_id == BgpSpeaker.id, + BgpSpeaker.id == bgp_speaker_id, + BgpSpeaker.advertise_floating_ip_host_routes == sa.sql.true()) + query = query.outerjoin(router_attrs, + l3_db.Router.id == router_attrs.router_id) + query = query.filter(router_attrs.distributed != sa.sql.true()) + return self._host_route_list_from_tuples(query.all()) + + def _get_dvr_fip_host_routes_by_binding(self, context, network_id, + bgp_speaker_id): + with context.session.begin(subtransactions=True): + BgpBinding = BgpSpeakerNetworkBinding + + gw_query = self._get_gateway_query(context, bgp_speaker_id) + gw_query.filter(BgpBinding.network_id == network_id) + + fip_query = self._get_fip_query(context, bgp_speaker_id) + fip_query.filter(BgpBinding.network_id == network_id) + + #Create the join query + join_query = self._join_fip_by_host_binding_to_agent_gateway( + context, + fip_query.subquery(), + gw_query.subquery()) + return self._host_route_list_from_tuples(join_query.all()) + + def _get_central_fip_host_routes_by_bgp_speaker(self, context, + bgp_speaker_id): + """Get all the floating IP host routes advertised by a BgpSpeaker.""" + with context.session.begin(subtransactions=True): + dest_alias = aliased(l3_db.FloatingIP, + name='destination') + next_hop_alias = aliased(models_v2.IPAllocation, + name='next_hop') + speaker_binding = aliased(BgpSpeakerNetworkBinding, + name="speaker_network_mapping") + router_attrs = aliased(l3_attrs_db.RouterExtraAttributes, + name='router_attrs') + query = context.session.query(dest_alias.floating_ip_address, + next_hop_alias.ip_address) + query = query.select_from(dest_alias, + BgpSpeaker, + l3_db.Router, + models_v2.Subnet) + query = query.join( + next_hop_alias, + next_hop_alias.network_id == dest_alias.floating_network_id) + query = query.join( + speaker_binding, + speaker_binding.network_id == dest_alias.floating_network_id) + query = query.join(l3_db.Router, + dest_alias.router_id == l3_db.Router.id) + query = query.filter( + BgpSpeaker.id == bgp_speaker_id, + BgpSpeaker.advertise_floating_ip_host_routes, + speaker_binding.bgp_speaker_id == BgpSpeaker.id, + dest_alias.floating_network_id == speaker_binding.network_id, + next_hop_alias.network_id == speaker_binding.network_id, + dest_alias.router_id == l3_db.Router.id, + l3_db.Router.gw_port_id == next_hop_alias.port_id, + next_hop_alias.subnet_id == models_v2.Subnet.id, + models_v2.Subnet.ip_version == 4) + query = query.outerjoin(router_attrs, + l3_db.Router.id == router_attrs.router_id) + query = query.filter(router_attrs.distributed != sa.sql.true()) + return self._host_route_list_from_tuples(query.all()) + + def _get_gateway_query(self, context, bgp_speaker_id): + BgpBinding = BgpSpeakerNetworkBinding + ML2PortBinding = ml2_models.PortBinding + IpAllocation = models_v2.IPAllocation + Port = models_v2.Port + gw_query = context.session.query(Port.network_id, + ML2PortBinding.host, + IpAllocation.ip_address) + + #Subquery for FIP agent gateway ports + gw_query = gw_query.filter( + ML2PortBinding.port_id == Port.id, + IpAllocation.port_id == Port.id, + IpAllocation.subnet_id == models_v2.Subnet.id, + models_v2.Subnet.ip_version == 4, + Port.device_owner == lib_consts.DEVICE_OWNER_AGENT_GW, + Port.network_id == BgpBinding.network_id, + BgpBinding.bgp_speaker_id == bgp_speaker_id, + BgpBinding.ip_version == 4) + return gw_query + + def _get_fip_query(self, context, bgp_speaker_id): + BgpBinding = BgpSpeakerNetworkBinding + ML2PortBinding = ml2_models.PortBinding + + #Subquery for floating IP's + fip_query = context.session.query( + l3_db.FloatingIP.floating_network_id, + ML2PortBinding.host, + l3_db.FloatingIP.floating_ip_address) + fip_query = fip_query.filter( + l3_db.FloatingIP.fixed_port_id == ML2PortBinding.port_id, + l3_db.FloatingIP.floating_network_id == BgpBinding.network_id, + BgpBinding.bgp_speaker_id == bgp_speaker_id) + return fip_query + + def _get_dvr_fip_host_routes_by_bgp_speaker(self, context, + bgp_speaker_id): + with context.session.begin(subtransactions=True): + gw_query = self._get_gateway_query(context, bgp_speaker_id) + fip_query = self._get_fip_query(context, bgp_speaker_id) + + #Create the join query + join_query = self._join_fip_by_host_binding_to_agent_gateway( + context, + fip_query.subquery(), + gw_query.subquery()) + return self._host_route_list_from_tuples(join_query.all()) + + def _join_fip_by_host_binding_to_agent_gateway(self, context, + fip_subq, gw_subq): + join_query = context.session.query(fip_subq.c.floating_ip_address, + gw_subq.c.ip_address) + and_cond = and_( + gw_subq.c.host == fip_subq.c.host, + gw_subq.c.network_id == fip_subq.c.floating_network_id) + + return join_query.join(gw_subq, and_cond) + + def _get_tenant_network_routes_by_binding(self, context, + network_id, bgp_speaker_id): + """Get all tenant network routes for the given network.""" + + with context.session.begin(subtransactions=True): + tenant_networks_query = self._tenant_networks_by_network_query( + context, + network_id, + bgp_speaker_id) + nexthops_query = self._nexthop_ip_addresses_by_binding_query( + context, + network_id, + bgp_speaker_id) + join_q = self._join_tenant_networks_to_next_hops( + context, + tenant_networks_query.subquery(), + nexthops_query.subquery()) + return self._make_advertised_routes_list(join_q.all()) + + def _get_tenant_network_routes_by_router(self, context, router_id, + bgp_speaker_id): + """Get all tenant network routes with the given router as nexthop.""" + + with context.session.begin(subtransactions=True): + scopes = self._get_address_scope_ids_for_bgp_speaker( + context, + bgp_speaker_id) + address_scope = aliased(address_scope_db.AddressScope) + inside_query = context.session.query( + models_v2.Subnet.cidr, + models_v2.IPAllocation.ip_address, + address_scope.id) + outside_query = context.session.query( + address_scope.id, + models_v2.IPAllocation.ip_address) + speaker_binding = aliased(BgpSpeakerNetworkBinding, + name="speaker_network_mapping") + port_alias = aliased(l3_db.RouterPort, name='routerport') + inside_query = inside_query.filter( + port_alias.router_id == router_id, + models_v2.IPAllocation.port_id == port_alias.port_id, + models_v2.IPAllocation.subnet_id == models_v2.Subnet.id, + models_v2.Subnet.subnetpool_id == models_v2.SubnetPool.id, + models_v2.SubnetPool.address_scope_id == address_scope.id, + address_scope.id.in_(scopes), + port_alias.port_type != lib_consts.DEVICE_OWNER_ROUTER_GW, + speaker_binding.bgp_speaker_id == bgp_speaker_id) + outside_query = outside_query.filter( + port_alias.router_id == router_id, + port_alias.port_type == lib_consts.DEVICE_OWNER_ROUTER_GW, + models_v2.IPAllocation.port_id == port_alias.port_id, + models_v2.IPAllocation.subnet_id == models_v2.Subnet.id, + models_v2.Subnet.subnetpool_id == models_v2.SubnetPool.id, + models_v2.SubnetPool.address_scope_id == address_scope.id, + address_scope.id.in_(scopes), + speaker_binding.bgp_speaker_id == bgp_speaker_id, + speaker_binding.network_id == models_v2.Port.network_id, + port_alias.port_id == models_v2.Port.id) + inside_query = inside_query.subquery() + outside_query = outside_query.subquery() + join_query = context.session.query(inside_query.c.cidr, + outside_query.c.ip_address) + and_cond = and_(inside_query.c.id == outside_query.c.id) + join_query = join_query.join(outside_query, and_cond) + return self._make_advertised_routes_list(join_query.all()) + + def _get_tenant_network_routes_by_bgp_speaker(self, context, + bgp_speaker_id): + """Get all tenant network routes to be advertised by a BgpSpeaker.""" + + with context.session.begin(subtransactions=True): + tenant_nets_q = self._tenant_networks_by_bgp_speaker_query( + context, + bgp_speaker_id) + nexthops_q = self._nexthop_ip_addresses_by_bgp_speaker_query( + context, + bgp_speaker_id) + join_q = self._join_tenant_networks_to_next_hops( + context, + tenant_nets_q.subquery(), + nexthops_q.subquery()) + + return self._make_advertised_routes_list(join_q.all()) + + def _join_tenant_networks_to_next_hops(self, context, + tenant_networks_subquery, + nexthops_subquery): + """Join subquery for tenant networks to subquery for nexthop IP's""" + left_subq = tenant_networks_subquery + right_subq = nexthops_subquery + join_query = context.session.query(left_subq.c.cidr, + right_subq.c.ip_address) + and_cond = and_(left_subq.c.router_id == right_subq.c.router_id, + left_subq.c.ip_version == right_subq.c.ip_version) + join_query = join_query.join(right_subq, and_cond) + return join_query + + def _tenant_networks_by_network_query(self, context, + network_id, bgp_speaker_id): + """Return subquery for tenant networks by binding network ID""" + address_scope = aliased(address_scope_db.AddressScope, + name='address_scope') + router_attrs = aliased(l3_attrs_db.RouterExtraAttributes, + name='router_attrs') + tenant_networks_query = context.session.query( + l3_db.RouterPort.router_id, + models_v2.Subnet.cidr, + models_v2.Subnet.ip_version, + address_scope.id) + tenant_networks_query = tenant_networks_query.filter( + l3_db.RouterPort.port_type != lib_consts.DEVICE_OWNER_ROUTER_GW, + l3_db.RouterPort.port_type != lib_consts.DEVICE_OWNER_ROUTER_SNAT, + l3_db.RouterPort.router_id == router_attrs.router_id, + models_v2.IPAllocation.port_id == l3_db.RouterPort.port_id, + models_v2.IPAllocation.subnet_id == models_v2.Subnet.id, + models_v2.Subnet.network_id != network_id, + models_v2.Subnet.subnetpool_id == models_v2.SubnetPool.id, + models_v2.SubnetPool.address_scope_id == address_scope.id, + BgpSpeaker.id == bgp_speaker_id, + BgpSpeaker.ip_version == address_scope.ip_version, + models_v2.Subnet.ip_version == address_scope.ip_version) + return tenant_networks_query + + def _tenant_networks_by_bgp_speaker_query(self, context, bgp_speaker_id): + """Return subquery for tenant networks by binding bgp_speaker_id""" + router_id = l3_db.RouterPort.router_id.distinct().label('router_id') + tenant_nets_subq = context.session.query(router_id, + models_v2.Subnet.cidr, + models_v2.Subnet.ip_version) + scopes = self._get_address_scope_ids_for_bgp_speaker(context, + bgp_speaker_id) + filters = self._tenant_networks_by_bgp_speaker_filters(scopes) + tenant_nets_subq = tenant_nets_subq.filter(*filters) + return tenant_nets_subq + + def _tenant_networks_by_bgp_speaker_filters(self, address_scope_ids): + """Return the filters for querying tenant networks by BGP speaker""" + router_attrs = aliased(l3_attrs_db.RouterExtraAttributes, + name='router_attrs') + return [models_v2.IPAllocation.port_id == l3_db.RouterPort.port_id, + l3_db.RouterPort.router_id == router_attrs.router_id, + l3_db.RouterPort.port_type != lib_consts.DEVICE_OWNER_ROUTER_GW, + l3_db.RouterPort.port_type != lib_consts.DEVICE_OWNER_ROUTER_SNAT, + models_v2.IPAllocation.subnet_id == models_v2.Subnet.id, + models_v2.Subnet.network_id != BgpSpeakerNetworkBinding.network_id, + models_v2.Subnet.subnetpool_id == models_v2.SubnetPool.id, + models_v2.SubnetPool.address_scope_id.in_(address_scope_ids), + models_v2.Subnet.ip_version == BgpSpeakerNetworkBinding.ip_version, + BgpSpeakerNetworkBinding.bgp_speaker_id == BgpSpeaker.id, + BgpSpeaker.advertise_tenant_networks == sa.sql.true()] + + def _nexthop_ip_addresses_by_binding_query(self, context, + network_id, bgp_speaker_id): + """Return the subquery for locating nexthops by binding network""" + nexthops_query = context.session.query( + l3_db.RouterPort.router_id, + models_v2.IPAllocation.ip_address, + models_v2.Subnet.ip_version) + filters = self._next_hop_ip_addresses_by_binding_filters( + network_id, + bgp_speaker_id) + nexthops_query = nexthops_query.filter(*filters) + return nexthops_query + + def _next_hop_ip_addresses_by_binding_filters(self, + network_id, + bgp_speaker_id): + """Return the filters for querying nexthops by binding network""" + address_scope = aliased(address_scope_db.AddressScope, + name='address_scope') + return [models_v2.IPAllocation.port_id == l3_db.RouterPort.port_id, + models_v2.IPAllocation.subnet_id == models_v2.Subnet.id, + BgpSpeaker.id == bgp_speaker_id, + BgpSpeakerNetworkBinding.bgp_speaker_id == BgpSpeaker.id, + BgpSpeakerNetworkBinding.network_id == network_id, + models_v2.Subnet.network_id == BgpSpeakerNetworkBinding.network_id, + models_v2.Subnet.subnetpool_id == models_v2.SubnetPool.id, + models_v2.SubnetPool.address_scope_id == address_scope.id, + models_v2.Subnet.ip_version == address_scope.ip_version, + l3_db.RouterPort.port_type == DEVICE_OWNER_ROUTER_GW] + + def _nexthop_ip_addresses_by_bgp_speaker_query(self, context, + bgp_speaker_id): + """Return the subquery for locating nexthops by BGP speaker""" + nexthops_query = context.session.query( + l3_db.RouterPort.router_id, + models_v2.IPAllocation.ip_address, + models_v2.Subnet.ip_version) + filters = self._next_hop_ip_addresses_by_bgp_speaker_filters( + bgp_speaker_id) + nexthops_query = nexthops_query.filter(*filters) + return nexthops_query + + def _next_hop_ip_addresses_by_bgp_speaker_filters(self, bgp_speaker_id): + """Return the filters for querying nexthops by BGP speaker""" + router_attrs = aliased(l3_attrs_db.RouterExtraAttributes, + name='router_attrs') + + return [l3_db.RouterPort.port_type == DEVICE_OWNER_ROUTER_GW, + l3_db.RouterPort.router_id == router_attrs.router_id, + BgpSpeakerNetworkBinding.network_id == models_v2.Subnet.network_id, + BgpSpeakerNetworkBinding.ip_version == models_v2.Subnet.ip_version, + BgpSpeakerNetworkBinding.bgp_speaker_id == bgp_speaker_id, + models_v2.IPAllocation.port_id == l3_db.RouterPort.port_id, + models_v2.IPAllocation.subnet_id == models_v2.Subnet.id] + + def _tenant_prefixes_by_router(self, context, router_id, bgp_speaker_id): + with context.session.begin(subtransactions=True): + query = context.session.query(models_v2.Subnet.cidr.distinct()) + filters = self._tenant_prefixes_by_router_filters(router_id, + bgp_speaker_id) + query = query.filter(*filters) + return [x[0] for x in query.all()] + + def _tenant_prefixes_by_router_filters(self, router_id, bgp_speaker_id): + binding = aliased(BgpSpeakerNetworkBinding, name='network_binding') + subnetpool = aliased(models_v2.SubnetPool, + name='subnetpool') + router_attrs = aliased(l3_attrs_db.RouterExtraAttributes, + name='router_attrs') + return [models_v2.Subnet.id == models_v2.IPAllocation.subnet_id, + models_v2.Subnet.subnetpool_id == subnetpool.id, + l3_db.RouterPort.router_id == router_id, + l3_db.Router.id == l3_db.RouterPort.router_id, + l3_db.Router.id == router_attrs.router_id, + l3_db.Router.gw_port_id == models_v2.Port.id, + models_v2.Port.network_id == binding.network_id, + binding.bgp_speaker_id == BgpSpeaker.id, + l3_db.RouterPort.port_type == DEVICE_OWNER_ROUTER_INTF, + models_v2.IPAllocation.port_id == l3_db.RouterPort.port_id] + + def _tenant_prefixes_by_router_interface(self, + context, + router_port_id, + bgp_speaker_id): + with context.session.begin(subtransactions=True): + query = context.session.query(models_v2.Subnet.cidr.distinct()) + filters = self._tenant_prefixes_by_router_filters(router_port_id, + bgp_speaker_id) + query = query.filter(*filters) + return [x[0] for x in query.all()] + + def _tenant_prefixes_by_router_port_filters(self, + router_port_id, + bgp_speaker_id): + binding = aliased(BgpSpeakerNetworkBinding, name='network_binding') + return [models_v2.Subnet.id == models_v2.IPAllocation.subnet_id, + l3_db.RouterPort.port_id == router_port_id, + l3_db.Router.id == l3_db.RouterPort.router_id, + l3_db.Router.gw_port_id == models_v2.Port.id, + models_v2.Port.network_id == binding.network_id, + binding.bgp_speaker_id == BgpSpeaker.id, + models_v2.Subnet.ip_version == binding.ip_version, + l3_db.RouterPort.port_type == DEVICE_OWNER_ROUTER_INTF, + models_v2.IPAllocation.port_id == l3_db.RouterPort.port_id] + + def _bgp_speakers_for_gateway_network(self, context, network_id): + """Return all BgpSpeakers for the given gateway network""" + with context.session.begin(subtransactions=True): + query = context.session.query(BgpSpeaker) + query = query.filter( + BgpSpeakerNetworkBinding.network_id == network_id, + BgpSpeakerNetworkBinding.bgp_speaker_id == BgpSpeaker.id) + return query.all() + + def _bgp_speakers_for_gw_network_by_family(self, context, + network_id, ip_version): + """Return the BgpSpeaker by given gateway network and ip_version""" + with context.session.begin(subtransactions=True): + query = context.session.query(BgpSpeaker) + query = query.filter( + BgpSpeakerNetworkBinding.network_id == network_id, + BgpSpeakerNetworkBinding.bgp_speaker_id == BgpSpeaker.id, + BgpSpeakerNetworkBinding.ip_version == ip_version) + return query.all() + + def _make_advertised_routes_list(self, routes): + route_list = ({'destination': x, + 'next_hop': y} for x, y in routes) + return route_list + + def _route_list_from_prefixes_and_next_hop(self, routes, next_hop): + route_list = [{'destination': x, + 'next_hop': next_hop} for x in routes] + return route_list + + def _host_route_list_from_tuples(self, ip_next_hop_tuples): + """Return the list of host routes given a list of (IP, nexthop)""" + return ({'destination': x + '/32', + 'next_hop': y} for x, y in ip_next_hop_tuples) diff --git a/neutron_dynamic_routing/db/bgp_dragentscheduler_db.py b/neutron_dynamic_routing/db/bgp_dragentscheduler_db.py new file mode 100644 index 00000000..af31064c --- /dev/null +++ b/neutron_dynamic_routing/db/bgp_dragentscheduler_db.py @@ -0,0 +1,216 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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 oslo_config import cfg +from oslo_db import exception as db_exc +from oslo_log import log as logging +import sqlalchemy as sa +from sqlalchemy import orm +from sqlalchemy.orm import exc + +from neutron.db import agents_db +from neutron.db import agentschedulers_db as as_db +from neutron.db import model_base + +from neutron_dynamic_routing._i18n import _ +from neutron_dynamic_routing._i18n import _LW +from neutron_dynamic_routing.extensions import bgp_dragentscheduler as bgp_dras_ext # noqa +from neutron_dynamic_routing.services.bgp.common import constants as bgp_consts + + +LOG = logging.getLogger(__name__) + + +BGP_DRAGENT_SCHEDULER_OPTS = [ + cfg.StrOpt( + 'bgp_drscheduler_driver', + default='neutron_dynamic_routing.services.bgp.scheduler' + '.bgp_dragent_scheduler.ChanceScheduler', + help=_('Driver used for scheduling BGP speakers to BGP DrAgent')) +] + +cfg.CONF.register_opts(BGP_DRAGENT_SCHEDULER_OPTS) + + +class BgpSpeakerDrAgentBinding(model_base.BASEV2): + """Represents a mapping between BGP speaker and BGP DRAgent""" + + __tablename__ = 'bgp_speaker_dragent_bindings' + + bgp_speaker_id = sa.Column(sa.String(length=36), + sa.ForeignKey("bgp_speakers.id", + ondelete='CASCADE'), + nullable=False) + dragent = orm.relation(agents_db.Agent) + agent_id = sa.Column(sa.String(length=36), + sa.ForeignKey("agents.id", + ondelete='CASCADE'), + primary_key=True) + + +class BgpDrAgentSchedulerDbMixin(bgp_dras_ext.BgpDrSchedulerPluginBase, + as_db.AgentSchedulerDbMixin): + + bgp_drscheduler = None + + def schedule_unscheduled_bgp_speakers(self, context, host): + if self.bgp_drscheduler: + return self.bgp_drscheduler.schedule_unscheduled_bgp_speakers( + context, host) + else: + LOG.warning(_LW("Cannot schedule BgpSpeaker to DrAgent. " + "Reason: No scheduler registered.")) + + def schedule_bgp_speaker(self, context, created_bgp_speaker): + if self.bgp_drscheduler: + agents = self.bgp_drscheduler.schedule(context, + created_bgp_speaker) + for agent in agents: + self._bgp_rpc.bgp_speaker_created(context, + created_bgp_speaker['id'], + agent.host) + else: + LOG.warning(_LW("Cannot schedule BgpSpeaker to DrAgent. " + "Reason: No scheduler registered.")) + + def add_bgp_speaker_to_dragent(self, context, agent_id, speaker_id): + """Associate a BgpDrAgent with a BgpSpeaker.""" + try: + self._save_bgp_speaker_dragent_binding(context, + agent_id, + speaker_id) + except db_exc.DBDuplicateEntry: + raise bgp_dras_ext.DrAgentAssociationError( + agent_id=agent_id) + + LOG.debug('BgpSpeaker %(bgp_speaker_id)s added to ' + 'BgpDrAgent %(agent_id)s', + {'bgp_speaker_id': speaker_id, 'agent_id': agent_id}) + + def _save_bgp_speaker_dragent_binding(self, context, + agent_id, speaker_id): + with context.session.begin(subtransactions=True): + agent_db = self._get_agent(context, agent_id) + agent_up = agent_db['admin_state_up'] + is_agent_bgp = (agent_db['agent_type'] == + bgp_consts.AGENT_TYPE_BGP_ROUTING) + if not is_agent_bgp or not agent_up: + raise bgp_dras_ext.DrAgentInvalid(id=agent_id) + + binding = BgpSpeakerDrAgentBinding() + binding.bgp_speaker_id = speaker_id + binding.agent_id = agent_id + context.session.add(binding) + + self._bgp_rpc.bgp_speaker_created(context, speaker_id, agent_db.host) + + def remove_bgp_speaker_from_dragent(self, context, agent_id, speaker_id): + with context.session.begin(subtransactions=True): + agent_db = self._get_agent(context, agent_id) + is_agent_bgp = (agent_db['agent_type'] == + bgp_consts.AGENT_TYPE_BGP_ROUTING) + if not is_agent_bgp: + raise bgp_dras_ext.DrAgentInvalid(id=agent_id) + + query = context.session.query(BgpSpeakerDrAgentBinding) + query = query.filter_by(bgp_speaker_id=speaker_id, + agent_id=agent_id) + + num_deleted = query.delete() + if not num_deleted: + raise bgp_dras_ext.DrAgentNotHostingBgpSpeaker( + bgp_speaker_id=speaker_id, + agent_id=agent_id) + LOG.debug('BgpSpeaker %(bgp_speaker_id)s removed from ' + 'BgpDrAgent %(agent_id)s', + {'bgp_speaker_id': speaker_id, + 'agent_id': agent_id}) + + self._bgp_rpc.bgp_speaker_removed(context, speaker_id, agent_db.host) + + def get_dragents_hosting_bgp_speakers(self, context, bgp_speaker_ids, + active=None, admin_state_up=None): + query = context.session.query(BgpSpeakerDrAgentBinding) + query = query.options(orm.contains_eager( + BgpSpeakerDrAgentBinding.dragent)) + query = query.join(BgpSpeakerDrAgentBinding.dragent) + + if len(bgp_speaker_ids) == 1: + query = query.filter( + BgpSpeakerDrAgentBinding.bgp_speaker_id == ( + bgp_speaker_ids[0])) + elif bgp_speaker_ids: + query = query.filter( + BgpSpeakerDrAgentBinding.bgp_speaker_id in bgp_speaker_ids) + if admin_state_up is not None: + query = query.filter(agents_db.Agent.admin_state_up == + admin_state_up) + + return [binding.dragent + for binding in query + if as_db.AgentSchedulerDbMixin.is_eligible_agent( + active, binding.dragent)] + + def get_dragent_bgp_speaker_bindings(self, context): + return context.session.query(BgpSpeakerDrAgentBinding).all() + + def list_dragent_hosting_bgp_speaker(self, context, speaker_id): + dragents = self.get_dragents_hosting_bgp_speakers(context, + [speaker_id]) + agent_ids = [dragent.id for dragent in dragents] + if not agent_ids: + return {'agents': []} + return {'agents': self.get_agents(context, filters={'id': agent_ids})} + + def list_bgp_speaker_on_dragent(self, context, agent_id): + query = context.session.query(BgpSpeakerDrAgentBinding.bgp_speaker_id) + query = query.filter_by(agent_id=agent_id) + + bgp_speaker_ids = [item[0] for item in query] + if not bgp_speaker_ids: + # Exception will be thrown if the requested agent does not exist. + self._get_agent(context, agent_id) + return {'bgp_speakers': []} + return {'bgp_speakers': + self.get_bgp_speakers(context, + filters={'id': bgp_speaker_ids})} + + def get_bgp_speakers_for_agent_host(self, context, host): + agent = self._get_agent_by_type_and_host( + context, bgp_consts.AGENT_TYPE_BGP_ROUTING, host) + if not agent.admin_state_up: + return {} + + query = context.session.query(BgpSpeakerDrAgentBinding) + query = query.filter(BgpSpeakerDrAgentBinding.agent_id == agent.id) + try: + binding = query.one() + except exc.NoResultFound: + return [] + bgp_speaker = self.get_bgp_speaker_with_advertised_routes( + context, binding['bgp_speaker_id']) + return [bgp_speaker] + + def get_bgp_speaker_by_speaker_id(self, context, bgp_speaker_id): + try: + return self.get_bgp_speaker(context, bgp_speaker_id) + except exc.NoResultFound: + return {} + + def get_bgp_peer_by_peer_id(self, context, bgp_peer_id): + try: + return self.get_bgp_peer(context, bgp_peer_id) + except exc.NoResultFound: + return {} diff --git a/neutron_dynamic_routing/db/migration/models/head.py b/neutron_dynamic_routing/db/migration/models/head.py index 25ad4968..b042f6a0 100644 --- a/neutron_dynamic_routing/db/migration/models/head.py +++ b/neutron_dynamic_routing/db/migration/models/head.py @@ -14,6 +14,9 @@ from neutron.db import model_base +from neutron_dynamic_routing.db import bgp_db # noqa +from neutron_dynamic_routing.db import bgp_dragentscheduler_db # noqa + def get_metadata(): return model_base.BASEV2.metadata diff --git a/neutron_dynamic_routing/extensions/__init__.py b/neutron_dynamic_routing/extensions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/extensions/bgp.py b/neutron_dynamic_routing/extensions/bgp.py new file mode 100644 index 00000000..ecce2e87 --- /dev/null +++ b/neutron_dynamic_routing/extensions/bgp.py @@ -0,0 +1,209 @@ +# Copyright 2016 Hewlett Packard Development Coompany LP +# +# 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 exceptions as n_exc + +from neutron.api import extensions +from neutron.api.v2 import attributes as attr +from neutron.api.v2 import resource_helper as rh + +from neutron_dynamic_routing._i18n import _ +from neutron_dynamic_routing.services.bgp.common import constants as bgp_consts + +BGP_EXT_ALIAS = 'bgp' +BGP_SPEAKER_RESOURCE_NAME = 'bgp-speaker' +BGP_SPEAKER_BODY_KEY_NAME = 'bgp_speaker' +BGP_PEER_BODY_KEY_NAME = 'bgp_peer' + + +RESOURCE_ATTRIBUTE_MAP = { + BGP_SPEAKER_RESOURCE_NAME + 's': { + 'id': {'allow_post': False, 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, 'primary_key': True}, + 'name': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': attr.NAME_MAX_LEN}, + 'is_visible': True, 'default': ''}, + 'local_as': {'allow_post': True, 'allow_put': False, + 'validate': {'type:range': (bgp_consts.MIN_ASNUM, + bgp_consts.MAX_ASNUM)}, + 'is_visible': True, 'default': None, + 'required_by_policy': False, + 'enforce_policy': False}, + 'ip_version': {'allow_post': True, 'allow_put': False, + 'validate': {'type:values': [4, 6]}, + 'is_visible': True, 'default': None, + 'required_by_policy': False, + 'enforce_policy': False}, + 'tenant_id': {'allow_post': True, 'allow_put': False, + 'required_by_policy': False, + 'validate': {'type:string': attr.TENANT_ID_MAX_LEN}, + 'is_visible': True}, + 'peers': {'allow_post': False, 'allow_put': False, + 'validate': {'type:uuid_list': None}, + 'is_visible': True, 'default': [], + 'required_by_policy': False, + 'enforce_policy': True}, + 'networks': {'allow_post': False, 'allow_put': False, + 'validate': {'type:uuid_list': None}, + 'is_visible': True, 'default': [], + 'required_by_policy': False, + 'enforce_policy': True}, + 'advertise_floating_ip_host_routes': { + 'allow_post': True, + 'allow_put': True, + 'convert_to': attr.convert_to_boolean, + 'validate': {'type:boolean': None}, + 'is_visible': True, 'default': True, + 'required_by_policy': False, + 'enforce_policy': True}, + 'advertise_tenant_networks': { + 'allow_post': True, + 'allow_put': True, + 'convert_to': attr.convert_to_boolean, + 'validate': {'type:boolean': None}, + 'is_visible': True, 'default': True, + 'required_by_policy': False, + 'enforce_policy': True}, + }, + 'bgp-peers': { + 'id': {'allow_post': False, 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, 'primary_key': True}, + 'name': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': attr.NAME_MAX_LEN}, + 'is_visible': True, 'default': ''}, + 'peer_ip': {'allow_post': True, 'allow_put': False, + 'required_by_policy': True, + 'validate': {'type:ip_address': None}, + 'is_visible': True}, + 'remote_as': {'allow_post': True, 'allow_put': False, + 'validate': {'type:range': (bgp_consts.MIN_ASNUM, + bgp_consts.MAX_ASNUM)}, + 'is_visible': True, 'default': None, + 'required_by_policy': False, + 'enforce_policy': False}, + 'auth_type': {'allow_post': True, 'allow_put': False, + 'required_by_policy': True, + 'validate': {'type:values': + bgp_consts.SUPPORTED_AUTH_TYPES}, + 'is_visible': True}, + 'password': {'allow_post': True, 'allow_put': True, + 'required_by_policy': True, + 'validate': {'type:string_or_none': None}, + 'is_visible': False, + 'default': None}, + 'tenant_id': {'allow_post': True, 'allow_put': False, + 'required_by_policy': False, + 'validate': {'type:string': attr.TENANT_ID_MAX_LEN}, + 'is_visible': True} + } +} + + +# Dynamic Routing Exceptions +class BgpSpeakerNotFound(n_exc.NotFound): + message = _("BGP speaker %(id)s could not be found.") + + +class BgpPeerNotFound(n_exc.NotFound): + message = _("BGP peer %(id)s could not be found.") + + +class BgpPeerNotAuthenticated(n_exc.NotFound): + message = _("BGP peer %(bgp_peer_id)s not authenticated.") + + +class BgpSpeakerPeerNotAssociated(n_exc.NotFound): + message = _("BGP peer %(bgp_peer_id)s is not associated with " + "BGP speaker %(bgp_speaker_id)s.") + + +class BgpSpeakerNetworkNotAssociated(n_exc.NotFound): + message = _("Network %(network_id)s is not associated with " + "BGP speaker %(bgp_speaker_id)s.") + + +class BgpSpeakerNetworkBindingError(n_exc.Conflict): + message = _("Network %(network_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.") + + +class DuplicateBgpPeerIpException(n_exc.Conflict): + _message = _("BGP Speaker %(bgp_speaker_id)s is already configured to " + "peer with a BGP Peer at %(peer_ip)s, it cannot peer with " + "BGP Peer %(bgp_peer_id)s.") + + +class InvalidBgpPeerMd5Authentication(n_exc.BadRequest): + message = _("A password must be supplied when using auth_type md5.") + + +class NetworkNotBoundForIpVersion(NetworkNotBound): + message = _("Network %(network_id)s is not bound to a IPv%(ip_version)s " + "BgpSpeaker.") + + +class Bgp(extensions.ExtensionDescriptor): + + @classmethod + def get_name(cls): + return "Neutron BGP Dynamic Routing Extension" + + @classmethod + def get_alias(cls): + return BGP_EXT_ALIAS + + @classmethod + def get_description(cls): + return("Discover and advertise routes for Neutron prefixes " + "dynamically via BGP") + + @classmethod + def get_updated(cls): + return "2016-05-10T15:37:00-00:00" + + @classmethod + def get_resources(cls): + plural_mappings = rh.build_plural_mappings( + {}, RESOURCE_ATTRIBUTE_MAP) + attr.PLURALS.update(plural_mappings) + action_map = {BGP_SPEAKER_RESOURCE_NAME: + {'add_bgp_peer': 'PUT', + 'remove_bgp_peer': 'PUT', + 'add_gateway_network': 'PUT', + 'remove_gateway_network': 'PUT', + 'get_advertised_routes': 'GET'}} + exts = rh.build_resource_info(plural_mappings, + RESOURCE_ATTRIBUTE_MAP, + BGP_EXT_ALIAS, + action_map=action_map) + + return exts + + def get_extended_resources(self, version): + if version == "2.0": + return RESOURCE_ATTRIBUTE_MAP + else: + return {} + + def update_attributes_map(self, attributes): + super(Bgp, self).update_attributes_map( + attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP) diff --git a/neutron_dynamic_routing/extensions/bgp_dragentscheduler.py b/neutron_dynamic_routing/extensions/bgp_dragentscheduler.py new file mode 100644 index 00000000..1628cf8f --- /dev/null +++ b/neutron_dynamic_routing/extensions/bgp_dragentscheduler.py @@ -0,0 +1,184 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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. + +import abc +import six +import webob + +from neutron_lib import exceptions as n_exc +from oslo_log import log as logging + +from neutron.api import extensions +from neutron.api.v2 import base +from neutron.api.v2 import resource +from neutron.extensions import agent +from neutron import manager +from neutron import wsgi + +from neutron_dynamic_routing._i18n import _, _LE +from neutron_dynamic_routing.extensions import bgp as bgp_ext + + +LOG = logging.getLogger(__name__) + +BGP_DRAGENT_SCHEDULER_EXT_ALIAS = 'bgp_dragent_scheduler' +BGP_DRINSTANCE = 'bgp-drinstance' +BGP_DRINSTANCES = BGP_DRINSTANCE + 's' +BGP_DRAGENT = 'bgp-dragent' +BGP_DRAGENTS = BGP_DRAGENT + 's' + + +class DrAgentInvalid(agent.AgentNotFound): + message = _("BgpDrAgent %(id)s is invalid or has been disabled.") + + +class DrAgentNotHostingBgpSpeaker(n_exc.NotFound): + message = _("BGP speaker %(bgp_speaker_id)s is not hosted " + "by the BgpDrAgent %(agent_id)s.") + + +class DrAgentAssociationError(n_exc.Conflict): + message = _("BgpDrAgent %(agent_id)s is already associated " + "to a BGP speaker.") + + +class BgpDrSchedulerController(wsgi.Controller): + """Schedule BgpSpeaker for a BgpDrAgent""" + def get_plugin(self): + plugin = manager.NeutronManager.get_service_plugins().get( + bgp_ext.BGP_EXT_ALIAS) + if not plugin: + LOG.error(_LE('No plugin for BGP routing registered')) + msg = _('The resource could not be found.') + raise webob.exc.HTTPNotFound(msg) + return plugin + + def index(self, request, **kwargs): + plugin = self.get_plugin() + return plugin.list_bgp_speaker_on_dragent( + request.context, kwargs['agent_id']) + + def create(self, request, body, **kwargs): + plugin = self.get_plugin() + return plugin.add_bgp_speaker_to_dragent( + request.context, + kwargs['agent_id'], + body['bgp_speaker_id']) + + def delete(self, request, id, **kwargs): + plugin = self.get_plugin() + return plugin.remove_bgp_speaker_from_dragent( + request.context, kwargs['agent_id'], id) + + +class BgpDrAgentController(wsgi.Controller): + def get_plugin(self): + plugin = manager.NeutronManager.get_service_plugins().get( + bgp_ext.BGP_EXT_ALIAS) + if not plugin: + LOG.error(_LE('No plugin for BGP routing registered')) + msg = _LE('The resource could not be found.') + raise webob.exc.HTTPNotFound(msg) + return plugin + + def index(self, request, **kwargs): + plugin = manager.NeutronManager.get_service_plugins().get( + bgp_ext.BGP_EXT_ALIAS) + return plugin.list_dragent_hosting_bgp_speaker( + request.context, kwargs['bgp_speaker_id']) + + +class Bgp_dragentscheduler(extensions.ExtensionDescriptor): + """Extension class supporting Dynamic Routing scheduler. + """ + @classmethod + def get_name(cls): + return "BGP Dynamic Routing Agent Scheduler" + + @classmethod + def get_alias(cls): + return BGP_DRAGENT_SCHEDULER_EXT_ALIAS + + @classmethod + def get_description(cls): + return "Schedules BgpSpeakers on BgpDrAgent" + + @classmethod + def get_updated(cls): + return "2015-07-30T10:00:00-00:00" + + @classmethod + def get_resources(cls): + """Returns Ext Resources.""" + exts = [] + parent = dict(member_name="agent", + collection_name="agents") + + controller = resource.Resource(BgpDrSchedulerController(), + base.FAULT_MAP) + exts.append(extensions.ResourceExtension(BGP_DRINSTANCES, + controller, parent)) + + parent = dict(member_name="bgp_speaker", + collection_name="bgp-speakers") + controller = resource.Resource(BgpDrAgentController(), + base.FAULT_MAP) + exts.append(extensions.ResourceExtension(BGP_DRAGENTS, + controller, parent)) + return exts + + def get_extended_resources(self, version): + return {} + + +@six.add_metaclass(abc.ABCMeta) +class BgpDrSchedulerPluginBase(object): + """REST API to operate BGP dynamic routing agent scheduler. + + All the methods must be executed in admin context. + """ + def get_plugin_description(self): + return "Neutron BGP dynamic routing scheduler Plugin" + + def get_plugin_type(self): + return bgp_ext.BGP_EXT_ALIAS + + @abc.abstractmethod + def add_bgp_speaker_to_dragent(self, context, agent_id, speaker_id): + pass + + @abc.abstractmethod + def remove_bgp_speaker_from_dragent(self, context, agent_id, speaker_id): + pass + + @abc.abstractmethod + def list_dragent_hosting_bgp_speaker(self, context, speaker_id): + pass + + @abc.abstractmethod + def list_bgp_speaker_on_dragent(self, context, agent_id): + pass + + @abc.abstractmethod + def get_bgp_speakers_for_agent_host(self, context, host): + pass + + @abc.abstractmethod + def get_bgp_speaker_by_speaker_id(self, context, speaker_id): + pass + + @abc.abstractmethod + def get_bgp_peer_by_peer_id(self, context, bgp_peer_id): + pass diff --git a/neutron_dynamic_routing/services/__init__.py b/neutron_dynamic_routing/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/services/bgp/__init__.py b/neutron_dynamic_routing/services/bgp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/services/bgp/agent/__init__.py b/neutron_dynamic_routing/services/bgp/agent/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/services/bgp/agent/bgp_dragent.py b/neutron_dynamic_routing/services/bgp/agent/bgp_dragent.py new file mode 100644 index 00000000..c1d690e9 --- /dev/null +++ b/neutron_dynamic_routing/services/bgp/agent/bgp_dragent.py @@ -0,0 +1,708 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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 collections + +from neutron_lib import constants as n_const +from oslo_config import cfg +from oslo_log import log as logging +import oslo_messaging +from oslo_service import loopingcall +from oslo_service import periodic_task +from oslo_utils import importutils + +from neutron.agent import rpc as agent_rpc +from neutron.common import rpc as n_rpc +from neutron.common import topics +from neutron.common import utils +from neutron import context +from neutron import manager + +from neutron_dynamic_routing.extensions import bgp as bgp_ext +from neutron_dynamic_routing._i18n import _, _LE, _LI, _LW +from neutron_dynamic_routing.services.bgp.agent.driver import exceptions as driver_exc # noqa +from neutron_dynamic_routing.services.bgp.common import constants as bgp_consts # noqa + +LOG = logging.getLogger(__name__) + + +class BgpDrAgent(manager.Manager): + """BGP Dynamic Routing agent service manager. + + Note that the public methods of this class are exposed as the server side + of an rpc interface. The neutron server uses + neutron.api.rpc.agentnotifiers.bgp_dr_rpc_agent_api. + BgpDrAgentNotifyApi as the client side to execute the methods + here. For more information about changing rpc interfaces, see + http://docs.openstack.org/developer/neutron/devref/rpc_api.html. + + API version history: + 1.0 initial Version + """ + target = oslo_messaging.Target(version='1.0') + + def __init__(self, host, conf=None): + super(BgpDrAgent, self).__init__() + self.initialize_driver(conf) + self.needs_resync_reasons = collections.defaultdict(list) + self.needs_full_sync_reason = None + + self.cache = BgpSpeakerCache() + self.context = context.get_admin_context_without_session() + self.plugin_rpc = BgpDrPluginApi(bgp_consts.BGP_PLUGIN, + self.context, host) + + def initialize_driver(self, conf): + self.conf = conf or cfg.CONF.BGP + try: + self.dr_driver_cls = ( + importutils.import_object(self.conf.bgp_speaker_driver, + self.conf)) + except ImportError: + LOG.exception(_LE("Error while importing BGP speaker driver %s"), + self.conf.bgp_speaker_driver) + raise SystemExit(1) + + def _handle_driver_failure(self, bgp_speaker_id, method, driver_exec): + self.schedule_resync(reason=driver_exec, + speaker_id=bgp_speaker_id) + LOG.error(_LE('Call to driver for BGP Speaker %(bgp_speaker)s ' + '%(method)s has failed with exception ' + '%(driver_exec)s.'), + {'bgp_speaker': bgp_speaker_id, + 'method': method, + 'driver_exec': driver_exec}) + + def after_start(self): + self.run() + LOG.info(_LI("BGP Dynamic Routing agent started")) + + def run(self): + """Activate BGP Dynamic Routing agent.""" + self.sync_state(self.context) + self.periodic_resync(self.context) + + @utils.synchronized('bgp-dragent') + def sync_state(self, context, full_sync=None, bgp_speakers=None): + try: + hosted_bgp_speakers = self.plugin_rpc.get_bgp_speakers(context) + hosted_bgp_speaker_ids = [bgp_speaker['id'] + for bgp_speaker in hosted_bgp_speakers] + cached_bgp_speakers = self.cache.get_bgp_speaker_ids() + for bgp_speaker_id in cached_bgp_speakers: + if bgp_speaker_id not in hosted_bgp_speaker_ids: + self.remove_bgp_speaker_from_dragent(bgp_speaker_id) + + resync_all = not bgp_speakers or full_sync + only_bs = set() if resync_all else set(bgp_speakers) + for hosted_bgp_speaker in hosted_bgp_speakers: + hosted_bs_id = hosted_bgp_speaker['id'] + if resync_all or hosted_bs_id in only_bs: + if not self.cache.is_bgp_speaker_added(hosted_bs_id): + self.safe_configure_dragent_for_bgp_speaker( + hosted_bgp_speaker) + continue + self.sync_bgp_speaker(hosted_bgp_speaker) + resync_reason = "Periodic route cache refresh" + self.schedule_resync(speaker_id=hosted_bs_id, + reason=resync_reason) + except Exception as e: + self.schedule_full_resync(reason=e) + LOG.error(_LE('Unable to sync BGP speaker state.')) + + def sync_bgp_speaker(self, bgp_speaker): + # sync BGP Speakers + bgp_peer_ips = set( + [bgp_peer['peer_ip'] for bgp_peer in bgp_speaker['peers']]) + cached_bgp_peer_ips = set( + self.cache.get_bgp_peer_ips(bgp_speaker['id'])) + removed_bgp_peer_ips = cached_bgp_peer_ips - bgp_peer_ips + + for bgp_peer_ip in removed_bgp_peer_ips: + self.remove_bgp_peer_from_bgp_speaker(bgp_speaker['id'], + bgp_peer_ip) + if bgp_peer_ips: + self.add_bgp_peers_to_bgp_speaker(bgp_speaker) + + # sync advertise routes + cached_adv_routes = self.cache.get_adv_routes(bgp_speaker['id']) + adv_routes = bgp_speaker['advertised_routes'] + if cached_adv_routes == adv_routes: + return + + for cached_route in cached_adv_routes: + if cached_route not in adv_routes: + self.withdraw_route_via_bgp_speaker(bgp_speaker['id'], + bgp_speaker['local_as'], + cached_route) + + self.advertise_routes_via_bgp_speaker(bgp_speaker) + + @utils.exception_logger() + def _periodic_resync_helper(self, context): + """Resync the BgpDrAgent state at the configured interval.""" + if self.needs_resync_reasons or self.needs_full_sync_reason: + full_sync = self.needs_full_sync_reason + reasons = self.needs_resync_reasons + # Reset old reasons + self.needs_full_sync_reason = None + self.needs_resync_reasons = collections.defaultdict(list) + if full_sync: + LOG.debug("resync all: %(reason)s", {"reason": full_sync}) + for bgp_speaker, reason in reasons.items(): + LOG.debug("resync (%(bgp_speaker)s): %(reason)s", + {"reason": reason, "bgp_speaker": bgp_speaker}) + self.sync_state( + context, full_sync=full_sync, bgp_speakers=reasons.keys()) + + # NOTE: spacing is set 1 sec. The actual interval is controlled + # by neutron/service.py which defaults to CONF.periodic_interval + @periodic_task.periodic_task(spacing=1) + def periodic_resync(self, context): + LOG.debug("Started periodic resync.") + self._periodic_resync_helper(context) + + @utils.synchronized('bgp-dr-agent') + def bgp_speaker_create_end(self, context, payload): + """Handle bgp_speaker_create_end notification event.""" + bgp_speaker_id = payload['bgp_speaker']['id'] + LOG.debug('Received BGP speaker create notification for ' + 'speaker_id=%(speaker_id)s from the neutron server.', + {'speaker_id': bgp_speaker_id}) + self.add_bgp_speaker_helper(bgp_speaker_id) + + @utils.synchronized('bgp-dr-agent') + def bgp_speaker_remove_end(self, context, payload): + """Handle bgp_speaker_create_end notification event.""" + + bgp_speaker_id = payload['bgp_speaker']['id'] + LOG.debug('Received BGP speaker remove notification for ' + 'speaker_id=%(speaker_id)s from the neutron server.', + {'speaker_id': bgp_speaker_id}) + self.remove_bgp_speaker_from_dragent(bgp_speaker_id) + + @utils.synchronized('bgp-dr-agent') + def bgp_peer_association_end(self, context, payload): + """Handle bgp_peer_association_end notification event.""" + + bgp_peer_id = payload['bgp_peer']['peer_id'] + bgp_speaker_id = payload['bgp_peer']['speaker_id'] + LOG.debug('Received BGP peer associate notification for ' + 'speaker_id=%(speaker_id)s peer_id=%(peer_id)s ' + 'from the neutron server.', + {'speaker_id': bgp_speaker_id, + 'peer_id': bgp_peer_id}) + self.add_bgp_peer_helper(bgp_speaker_id, bgp_peer_id) + + @utils.synchronized('bgp-dr-agent') + def bgp_peer_disassociation_end(self, context, payload): + """Handle bgp_peer_disassociation_end notification event.""" + + bgp_peer_ip = payload['bgp_peer']['peer_ip'] + bgp_speaker_id = payload['bgp_peer']['speaker_id'] + LOG.debug('Received BGP peer disassociate notification for ' + 'speaker_id=%(speaker_id)s peer_ip=%(peer_ip)s ' + 'from the neutron server.', + {'speaker_id': bgp_speaker_id, + 'peer_ip': bgp_peer_ip}) + self.remove_bgp_peer_from_bgp_speaker(bgp_speaker_id, bgp_peer_ip) + + @utils.synchronized('bgp-dr-agent') + def bgp_routes_advertisement_end(self, context, payload): + """Handle bgp_routes_advertisement_end notification event.""" + + bgp_speaker_id = payload['advertise_routes']['speaker_id'] + LOG.debug('Received routes advertisement end notification ' + 'for speaker_id=%(speaker_id)s from the neutron server.', + {'speaker_id': bgp_speaker_id}) + routes = payload['advertise_routes']['routes'] + self.add_routes_helper(bgp_speaker_id, routes) + + @utils.synchronized('bgp-dr-agent') + def bgp_routes_withdrawal_end(self, context, payload): + """Handle bgp_routes_withdrawal_end notification event.""" + + bgp_speaker_id = payload['withdraw_routes']['speaker_id'] + LOG.debug('Received route withdrawal notification for ' + 'speaker_id=%(speaker_id)s from the neutron server.', + {'speaker_id': bgp_speaker_id}) + routes = payload['withdraw_routes']['routes'] + self.withdraw_routes_helper(bgp_speaker_id, routes) + + def add_bgp_speaker_helper(self, bgp_speaker_id): + """Add BGP speaker.""" + bgp_speaker = self.safe_get_bgp_speaker_info(bgp_speaker_id) + if bgp_speaker: + self.add_bgp_speaker_on_dragent(bgp_speaker) + + def add_bgp_peer_helper(self, bgp_speaker_id, bgp_peer_id): + """Add BGP peer.""" + # Ideally BGP Speaker must be added by now, If not then let's + # re-sync. + if not self.cache.is_bgp_speaker_added(bgp_speaker_id): + self.schedule_resync(speaker_id=bgp_speaker_id, + reason="BGP Speaker Out-of-sync") + return + + bgp_peer = self.safe_get_bgp_peer_info(bgp_speaker_id, + bgp_peer_id) + if bgp_peer: + bgp_speaker_as = self.cache.get_bgp_speaker_local_as( + bgp_speaker_id) + self.add_bgp_peer_to_bgp_speaker(bgp_speaker_id, + bgp_speaker_as, + bgp_peer) + + def add_routes_helper(self, bgp_speaker_id, routes): + """Advertise routes to BGP speaker.""" + # Ideally BGP Speaker must be added by now, If not then let's + # re-sync. + if not self.cache.is_bgp_speaker_added(bgp_speaker_id): + self.schedule_resync(speaker_id=bgp_speaker_id, + reason="BGP Speaker Out-of-sync") + return + + bgp_speaker_as = self.cache.get_bgp_speaker_local_as(bgp_speaker_id) + for route in routes: + self.advertise_route_via_bgp_speaker(bgp_speaker_id, + bgp_speaker_as, + route) + if self.is_resync_scheduled(bgp_speaker_id): + break + + def withdraw_routes_helper(self, bgp_speaker_id, routes): + """Withdraw routes advertised by BGP speaker.""" + # Ideally BGP Speaker must be added by now, If not then let's + # re-sync. + if not self.cache.is_bgp_speaker_added(bgp_speaker_id): + self.schedule_resync(speaker_id=bgp_speaker_id, + reason="BGP Speaker Out-of-sync") + return + + bgp_speaker_as = self.cache.get_bgp_speaker_local_as(bgp_speaker_id) + for route in routes: + self.withdraw_route_via_bgp_speaker(bgp_speaker_id, + bgp_speaker_as, + route) + if self.is_resync_scheduled(bgp_speaker_id): + break + + def safe_get_bgp_speaker_info(self, bgp_speaker_id): + try: + bgp_speaker = self.plugin_rpc.get_bgp_speaker_info(self.context, + bgp_speaker_id) + if not bgp_speaker: + LOG.warning(_LW('BGP Speaker %s has been deleted.'), + bgp_speaker_id) + return bgp_speaker + except Exception as e: + self.schedule_resync(speaker_id=bgp_speaker_id, + reason=e) + LOG.error(_LE('BGP Speaker %(bgp_speaker)s info call ' + 'failed with reason=%(e)s.'), + {'bgp_speaker': bgp_speaker_id, 'e': e}) + + def safe_get_bgp_peer_info(self, bgp_speaker_id, bgp_peer_id): + try: + bgp_peer = self.plugin_rpc.get_bgp_peer_info(self.context, + bgp_peer_id) + if not bgp_peer: + LOG.warning(_LW('BGP Peer %s has been deleted.'), bgp_peer) + return bgp_peer + except Exception as e: + self.schedule_resync(speaker_id=bgp_speaker_id, + reason=e) + LOG.error(_LE('BGP peer %(bgp_peer)s info call ' + 'failed with reason=%(e)s.'), + {'bgp_peer': bgp_peer_id, 'e': e}) + + @utils.exception_logger() + def safe_configure_dragent_for_bgp_speaker(self, bgp_speaker): + try: + self.add_bgp_speaker_on_dragent(bgp_speaker) + except (bgp_ext.BgpSpeakerNotFound, RuntimeError): + LOG.warning(_LW('BGP speaker %s may have been deleted and its ' + 'resources may have already been disposed.'), + bgp_speaker['id']) + + def add_bgp_speaker_on_dragent(self, bgp_speaker): + # Caching BGP speaker details in BGPSpeakerCache. Will be used + # during smooth. + self.cache.put_bgp_speaker(bgp_speaker) + + LOG.debug('Calling driver for adding BGP speaker %(speaker_id)s,' + ' speaking for local_as %(local_as)s', + {'speaker_id': bgp_speaker['id'], + 'local_as': bgp_speaker['local_as']}) + try: + self.dr_driver_cls.add_bgp_speaker(bgp_speaker['local_as']) + except driver_exc.BgpSpeakerAlreadyScheduled: + return + except Exception as e: + self._handle_driver_failure(bgp_speaker['id'], + 'add_bgp_speaker', e) + + # Add peer and route information to the driver. + self.add_bgp_peers_to_bgp_speaker(bgp_speaker) + self.advertise_routes_via_bgp_speaker(bgp_speaker) + self.schedule_resync(speaker_id=bgp_speaker['id'], + reason="Periodic route cache refresh") + + def remove_bgp_speaker_from_dragent(self, bgp_speaker_id): + if self.cache.is_bgp_speaker_added(bgp_speaker_id): + bgp_speaker_as = self.cache.get_bgp_speaker_local_as( + bgp_speaker_id) + self.cache.remove_bgp_speaker_by_id(bgp_speaker_id) + + LOG.debug('Calling driver for removing BGP speaker %(speaker_as)s', + {'speaker_as': bgp_speaker_as}) + try: + self.dr_driver_cls.delete_bgp_speaker(bgp_speaker_as) + except Exception as e: + self._handle_driver_failure(bgp_speaker_id, + 'remove_bgp_speaker', e) + return + + # Ideally, only the added speakers can be removed by the neutron + # server. Looks like there might be some synchronization + # issue between the server and the agent. Let's initiate a re-sync + # to resolve the issue. + self.schedule_resync(speaker_id=bgp_speaker_id, + reason="BGP Speaker Out-of-sync") + + def add_bgp_peers_to_bgp_speaker(self, bgp_speaker): + for bgp_peer in bgp_speaker['peers']: + self.add_bgp_peer_to_bgp_speaker(bgp_speaker['id'], + bgp_speaker['local_as'], + bgp_peer) + if self.is_resync_scheduled(bgp_speaker['id']): + break + + def add_bgp_peer_to_bgp_speaker(self, bgp_speaker_id, + bgp_speaker_as, bgp_peer): + if self.cache.get_bgp_peer_by_ip(bgp_speaker_id, bgp_peer['peer_ip']): + return + + self.cache.put_bgp_peer(bgp_speaker_id, bgp_peer) + + LOG.debug('Calling driver interface for adding BGP peer %(peer_ip)s ' + 'remote_as=%(remote_as)s to BGP Speaker running for ' + 'local_as=%(local_as)d', + {'peer_ip': bgp_peer['peer_ip'], + 'remote_as': bgp_peer['remote_as'], + 'local_as': bgp_speaker_as}) + try: + self.dr_driver_cls.add_bgp_peer(bgp_speaker_as, + bgp_peer['peer_ip'], + bgp_peer['remote_as'], + bgp_peer['auth_type'], + bgp_peer['password']) + except Exception as e: + self._handle_driver_failure(bgp_speaker_id, + 'add_bgp_peer', e) + + def remove_bgp_peer_from_bgp_speaker(self, bgp_speaker_id, bgp_peer_ip): + # Ideally BGP Speaker must be added by now, If not then let's + # re-sync. + if not self.cache.is_bgp_speaker_added(bgp_speaker_id): + self.schedule_resync(speaker_id=bgp_speaker_id, + reason="BGP Speaker Out-of-sync") + return + + if self.cache.is_bgp_peer_added(bgp_speaker_id, bgp_peer_ip): + self.cache.remove_bgp_peer_by_ip(bgp_speaker_id, bgp_peer_ip) + + bgp_speaker_as = self.cache.get_bgp_speaker_local_as( + bgp_speaker_id) + + LOG.debug('Calling driver interface to remove BGP peer ' + '%(peer_ip)s from BGP Speaker running for ' + 'local_as=%(local_as)d', + {'peer_ip': bgp_peer_ip, 'local_as': bgp_speaker_as}) + try: + self.dr_driver_cls.delete_bgp_peer(bgp_speaker_as, + bgp_peer_ip) + except Exception as e: + self._handle_driver_failure(bgp_speaker_id, + 'remove_bgp_peer', e) + return + + # Ideally, only the added peers can be removed by the neutron + # server. Looks like there might be some synchronization + # issue between the server and the agent. Let's initiate a re-sync + # to resolve the issue. + self.schedule_resync(speaker_id=bgp_speaker_id, + reason="BGP Peer Out-of-sync") + + def advertise_routes_via_bgp_speaker(self, bgp_speaker): + for route in bgp_speaker['advertised_routes']: + self.advertise_route_via_bgp_speaker(bgp_speaker['id'], + bgp_speaker['local_as'], + route) + if self.is_resync_scheduled(bgp_speaker['id']): + break + + def advertise_route_via_bgp_speaker(self, bgp_speaker_id, + bgp_speaker_as, route): + if self.cache.is_route_advertised(bgp_speaker_id, route): + # Requested route already advertised. Hence, Nothing to be done. + return + self.cache.put_adv_route(bgp_speaker_id, route) + + LOG.debug('Calling driver for advertising prefix: %(cidr)s, ' + 'next_hop: %(nexthop)s', + {'cidr': route['destination'], + 'nexthop': route['next_hop']}) + try: + self.dr_driver_cls.advertise_route(bgp_speaker_as, + route['destination'], + route['next_hop']) + except Exception as e: + self._handle_driver_failure(bgp_speaker_id, + 'advertise_route', e) + + def withdraw_route_via_bgp_speaker(self, bgp_speaker_id, + bgp_speaker_as, route): + if self.cache.is_route_advertised(bgp_speaker_id, route): + self.cache.remove_adv_route(bgp_speaker_id, route) + LOG.debug('Calling driver for withdrawing prefix: %(cidr)s, ' + 'next_hop: %(nexthop)s', + {'cidr': route['destination'], + 'nexthop': route['next_hop']}) + try: + self.dr_driver_cls.withdraw_route(bgp_speaker_as, + route['destination'], + route['next_hop']) + except Exception as e: + self._handle_driver_failure(bgp_speaker_id, + 'withdraw_route', e) + return + + # Ideally, only the advertised routes can be withdrawn by the + # neutron server. Looks like there might be some synchronization + # issue between the server and the agent. Let's initiate a re-sync + # to resolve the issue. + self.schedule_resync(speaker_id=bgp_speaker_id, + reason="Advertised routes Out-of-sync") + + def schedule_full_resync(self, reason): + LOG.debug('Recording full resync request for all BGP Speakers ' + 'with reason=%s', reason) + self.needs_full_sync_reason = reason + + def schedule_resync(self, reason, speaker_id): + """Schedule a full resync for a given BGP Speaker. + If no BGP Speaker is specified, resync all BGP Speakers. + """ + LOG.debug('Recording resync request for BGP Speaker %s ' + 'with reason=%s', speaker_id, reason) + self.needs_resync_reasons[speaker_id].append(reason) + + def is_resync_scheduled(self, bgp_speaker_id): + if bgp_speaker_id not in self.needs_resync_reasons: + return False + + reason = self.needs_resync_reasons[bgp_speaker_id] + # Re-sync scheduled for the queried BGP speaker. No point + # continuing further. Let's stop processing and wait for + # re-sync to happen. + LOG.debug('Re-sync already scheduled for BGP Speaker %s ' + 'with reason=%s', bgp_speaker_id, reason) + return True + + +class BgpDrPluginApi(object): + """Agent side of BgpDrAgent RPC API. + + This class implements the client side of an rpc interface. + The server side of this interface can be found in + neutron.api.rpc.handlers.bgp_speaker_rpc.BgpSpeakerRpcCallback. + For more information about changing rpc interfaces, see + doc/source/devref/rpc_api.rst. + + API version history: + 1.0 - Initial version. + """ + def __init__(self, topic, context, host): + self.context = context + self.host = host + target = oslo_messaging.Target(topic=topic, version='1.0') + self.client = n_rpc.get_client(target) + + def get_bgp_speakers(self, context): + """Make a remote process call to retrieve all BGP speakers info.""" + cctxt = self.client.prepare() + return cctxt.call(context, 'get_bgp_speakers', host=self.host) + + 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) + + +class BgpSpeakerCache(object): + """Agent cache of the current BGP speaker state. + + This class is designed to support the advertisement for + multiple BGP speaker via a single driver interface. + + Version history: + 1.0 - Initial version for caching the state of BGP speaker. + """ + def __init__(self): + self.cache = {} + + def get_bgp_speaker_ids(self): + return self.cache.keys() + + def put_bgp_speaker(self, bgp_speaker): + if bgp_speaker['id'] in self.cache: + self.remove_bgp_speaker_by_id(self.cache[bgp_speaker['id']]) + self.cache[bgp_speaker['id']] = {'bgp_speaker': bgp_speaker, + 'peers': {}, + 'advertised_routes': []} + + def get_bgp_speaker_by_id(self, bgp_speaker_id): + if bgp_speaker_id in self.cache: + return self.cache[bgp_speaker_id]['bgp_speaker'] + + def get_bgp_speaker_local_as(self, bgp_speaker_id): + bgp_speaker = self.get_bgp_speaker_by_id(bgp_speaker_id) + if bgp_speaker: + return bgp_speaker['local_as'] + + def is_bgp_speaker_added(self, bgp_speaker_id): + return self.get_bgp_speaker_by_id(bgp_speaker_id) + + def remove_bgp_speaker_by_id(self, bgp_speaker_id): + if bgp_speaker_id in self.cache: + del self.cache[bgp_speaker_id] + + def put_bgp_peer(self, bgp_speaker_id, bgp_peer): + if bgp_peer['peer_ip'] in self.get_bgp_peer_ips(bgp_speaker_id): + del self.cache[bgp_speaker_id]['peers'][bgp_peer['peer_ip']] + + self.cache[bgp_speaker_id]['peers'][bgp_peer['peer_ip']] = bgp_peer + + def is_bgp_peer_added(self, bgp_speaker_id, bgp_peer_ip): + return self.get_bgp_peer_by_ip(bgp_speaker_id, bgp_peer_ip) + + def get_bgp_peer_ips(self, bgp_speaker_id): + bgp_speaker = self.get_bgp_speaker_by_id(bgp_speaker_id) + if bgp_speaker: + return self.cache[bgp_speaker_id]['peers'].keys() + + def get_bgp_peer_by_ip(self, bgp_speaker_id, bgp_peer_ip): + bgp_speaker = self.get_bgp_speaker_by_id(bgp_speaker_id) + if bgp_speaker: + return self.cache[bgp_speaker_id]['peers'].get(bgp_peer_ip) + + def remove_bgp_peer_by_ip(self, bgp_speaker_id, bgp_peer_ip): + if bgp_peer_ip in self.get_bgp_peer_ips(bgp_speaker_id): + del self.cache[bgp_speaker_id]['peers'][bgp_peer_ip] + + def put_adv_route(self, bgp_speaker_id, route): + self.cache[bgp_speaker_id]['advertised_routes'].append(route) + + def is_route_advertised(self, bgp_speaker_id, route): + routes = self.cache[bgp_speaker_id]['advertised_routes'] + for r in routes: + if r['destination'] == route['destination'] and ( + r['next_hop'] == route['next_hop']): + return True + return False + + def remove_adv_route(self, bgp_speaker_id, route): + routes = self.cache[bgp_speaker_id]['advertised_routes'] + updated_routes = [r for r in routes if ( + r['destination'] != route['destination'])] + self.cache[bgp_speaker_id]['advertised_routes'] = updated_routes + + def get_adv_routes(self, bgp_speaker_id): + return self.cache[bgp_speaker_id]['advertised_routes'] + + def get_state(self): + bgp_speaker_ids = self.get_bgp_speaker_ids() + num_bgp_speakers = len(bgp_speaker_ids) + num_bgp_peers = 0 + num_advertised_routes = 0 + for bgp_speaker_id in bgp_speaker_ids: + bgp_speaker = self.get_bgp_speaker_by_id(bgp_speaker_id) + num_bgp_peers += len(bgp_speaker['peers']) + num_advertised_routes += len(bgp_speaker['advertised_routes']) + return {'bgp_speakers': num_bgp_speakers, + 'bgp_peers': num_bgp_peers, + 'advertise_routes': num_advertised_routes} + + +class BgpDrAgentWithStateReport(BgpDrAgent): + def __init__(self, host, conf=None): + super(BgpDrAgentWithStateReport, + self).__init__(host, conf) + self.state_rpc = agent_rpc.PluginReportStateAPI(topics.REPORTS) + self.agent_state = { + 'agent_type': bgp_consts.AGENT_TYPE_BGP_ROUTING, + 'binary': 'neutron-bgp-dragent', + 'configurations': {}, + 'host': host, + 'topic': bgp_consts.BGP_DRAGENT, + 'start_flag': True} + report_interval = cfg.CONF.AGENT.report_interval + if report_interval: + self.heartbeat = loopingcall.FixedIntervalLoopingCall( + self._report_state) + self.heartbeat.start(interval=report_interval) + + def _report_state(self): + LOG.debug("Report state task started") + try: + self.agent_state.get('configurations').update( + self.cache.get_state()) + ctx = context.get_admin_context_without_session() + agent_status = self.state_rpc.report_state(ctx, self.agent_state, + True) + if agent_status == n_const.AGENT_REVIVED: + LOG.info(_LI("Agent has just been revived. " + "Scheduling full sync")) + self.schedule_full_resync( + reason=_("Agent has just been revived")) + except AttributeError: + # This means the server does not support report_state + LOG.warning(_LW("Neutron server does not support state report. " + "State report for this agent will be disabled.")) + self.heartbeat.stop() + self.run() + return + except Exception: + LOG.exception(_LE("Failed reporting state!")) + return + if self.agent_state.pop('start_flag', None): + self.run() + + def agent_updated(self, context, payload): + """Handle the agent_updated notification event.""" + self.schedule_full_resync( + reason=_("BgpDrAgent updated: %s") % payload) + LOG.info(_LI("agent_updated by server side %s!"), payload) + + def after_start(self): + LOG.info(_LI("BGP dynamic routing agent started")) diff --git a/neutron_dynamic_routing/services/bgp/agent/config.py b/neutron_dynamic_routing/services/bgp/agent/config.py new file mode 100644 index 00000000..55ae407a --- /dev/null +++ b/neutron_dynamic_routing/services/bgp/agent/config.py @@ -0,0 +1,29 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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_config import cfg + +from neutron_dynamic_routing._i18n import _ + +BGP_DRIVER_OPTS = [ + cfg.StrOpt('bgp_speaker_driver', + help=_("BGP speaker driver class to be instantiated.")) +] + +BGP_PROTO_CONFIG_OPTS = [ + cfg.StrOpt('bgp_router_id', + help=_("32-bit BGP identifier, typically an IPv4 address " + "owned by the system running the BGP DrAgent.")) +] diff --git a/neutron_dynamic_routing/services/bgp/agent/driver/__init__.py b/neutron_dynamic_routing/services/bgp/agent/driver/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/services/bgp/agent/driver/base.py b/neutron_dynamic_routing/services/bgp/agent/driver/base.py new file mode 100644 index 00000000..128651b7 --- /dev/null +++ b/neutron_dynamic_routing/services/bgp/agent/driver/base.py @@ -0,0 +1,142 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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 abc +import six + + +@six.add_metaclass(abc.ABCMeta) +class BgpDriverBase(object): + """Base class for BGP Speaking drivers. + + Any class which provides BGP functionality should extend this + defined base class. + """ + + @abc.abstractmethod + def add_bgp_speaker(self, speaker_as): + """Add a BGP speaker. + + :param speaker_as: Specifies BGP Speaker autonomous system number. + Must be an integer between MIN_ASNUM and MAX_ASNUM. + :type speaker_as: integer + :raises: BgpSpeakerAlreadyScheduled, BgpSpeakerMaxScheduled, + InvalidParamType, InvalidParamRange + """ + + @abc.abstractmethod + def delete_bgp_speaker(self, speaker_as): + """Deletes BGP speaker. + + :param speaker_as: Specifies BGP Speaker autonomous system number. + Must be an integer between MIN_ASNUM and MAX_ASNUM. + :type speaker_as: integer + :raises: BgpSpeakerNotAdded + """ + + @abc.abstractmethod + def add_bgp_peer(self, speaker_as, peer_ip, peer_as, + auth_type='none', password=None): + """Add a new BGP peer. + + :param speaker_as: Specifies BGP Speaker autonomous system number. + Must be an integer between MIN_ASNUM and MAX_ASNUM. + :type speaker_as: integer + :param peer_ip: Specifies the IP address of the peer. + :type peer_ip: string + :param peer_as: Specifies Autonomous Number of the peer. + Must be an integer between MIN_ASNUM and MAX_ASNUM. + :type peer_as: integer + :param auth_type: Specifies authentication type. + By default, authentication will be disabled. + :type auth_type: value in SUPPORTED_AUTH_TYPES + :param password: Authentication password.By default, authentication + will be disabled. + :type password: string + :raises: BgpSpeakerNotAdded, InvalidParamType, InvalidParamRange, + InvaildAuthType, PasswordNotSpecified + """ + + @abc.abstractmethod + def delete_bgp_peer(self, speaker_as, peer_ip): + """Delete a BGP peer associated with the given peer IP + + :param speaker_as: Specifies BGP Speaker autonomous system number. + Must be an integer between MIN_ASNUM and MAX_ASNUM. + :type speaker_as: integer + :param peer_ip: Specifies the IP address of the peer. Must be the + string representation of an IP address. + :type peer_ip: string + :raises: BgpSpeakerNotAdded, BgpPeerNotAdded + """ + + @abc.abstractmethod + def advertise_route(self, speaker_as, cidr, nexthop): + """Add a new prefix to advertise. + + :param speaker_as: Specifies BGP Speaker autonomous system number. + Must be an integer between MIN_ASNUM and MAX_ASNUM. + :type speaker_as: integer + :param cidr: CIDR of the network to advertise. Must be the string + representation of an IP network (e.g., 10.1.1.0/24) + :type cidr: string + :param nexthop: Specifies the next hop address for the above + prefix. + :type nexthop: string + :raises: BgpSpeakerNotAdded, InvalidParamType + """ + + @abc.abstractmethod + def withdraw_route(self, speaker_as, cidr, nexthop=None): + """Withdraw an advertised prefix. + + :param speaker_as: Specifies BGP Speaker autonomous system number. + Must be an integer between MIN_ASNUM and MAX_ASNUM. + :type speaker_as: integer + :param cidr: CIDR of the network to withdraw. Must be the string + representation of an IP network (e.g., 10.1.1.0/24) + :type cidr: string + :param nexthop: Specifies the next hop address for the above + prefix. + :type nexthop: string + :raises: BgpSpeakerNotAdded, RouteNotAdvertised, InvalidParamType + """ + + @abc.abstractmethod + def get_bgp_speaker_statistics(self, speaker_as): + """Collect BGP Speaker statistics. + + :param speaker_as: Specifies BGP Speaker autonomous system number. + Must be an integer between MIN_ASNUM and MAX_ASNUM. + :type speaker_as: integer + :raises: BgpSpeakerNotAdded + :returns: bgp_speaker_stats: string + """ + + @abc.abstractmethod + def get_bgp_peer_statistics(self, speaker_as, peer_ip, peer_as): + """Collect BGP Peer statistics. + + :param speaker_as: Specifies BGP Speaker autonomous system number. + Must be an integer between MIN_ASNUM and MAX_ASNUM. + :type speaker_as: integer + :param peer_ip: Specifies the IP address of the peer. + :type peer_ip: string + :param peer_as: Specifies the AS number of the peer. Must be an + integer between MIN_ASNUM and MAX_ASNUM. + :type peer_as: integer . + :raises: BgpSpeakerNotAdded, BgpPeerNotAdded + :returns: bgp_peer_stats: string + """ diff --git a/neutron_dynamic_routing/services/bgp/agent/driver/exceptions.py b/neutron_dynamic_routing/services/bgp/agent/driver/exceptions.py new file mode 100644 index 00000000..3b0b3f55 --- /dev/null +++ b/neutron_dynamic_routing/services/bgp/agent/driver/exceptions.py @@ -0,0 +1,62 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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 exceptions as n_exc + +from neutron_dynamic_routing._i18n import _ + + +# BGP Driver Exceptions +class BgpSpeakerNotAdded(n_exc.BadRequest): + message = _("BGP Speaker for local_as=%(local_as)s with " + "router_id=%(rtid)s not added yet.") + + +class BgpSpeakerMaxScheduled(n_exc.BadRequest): + message = _("Already hosting maximum number of BGP Speakers. " + "Allowed scheduled count=%(count)d") + + +class BgpSpeakerAlreadyScheduled(n_exc.Conflict): + message = _("Already hosting BGP Speaker for local_as=%(current_as)d with " + "router_id=%(rtid)s.") + + +class BgpPeerNotAdded(n_exc.BadRequest): + message = _("BGP Peer %(peer_ip)s for remote_as=%(remote_as)s, running " + "for BGP Speaker %(speaker_as)d not added yet.") + + +class RouteNotAdvertised(n_exc.BadRequest): + message = _("Route %(cidr)s not advertised for BGP Speaker " + "%(speaker_as)d.") + + +class InvalidParamType(n_exc.NeutronException): + message = _("Parameter %(param)s must be of %(param_type)s type.") + + +class InvalidParamRange(n_exc.NeutronException): + message = _("%(param)s must be in %(range)s range.") + + +class InvaildAuthType(n_exc.BadRequest): + message = _("Authentication type not supported. Requested " + "type=%(auth_type)s.") + + +class PasswordNotSpecified(n_exc.BadRequest): + message = _("Password not specified for authentication " + "type=%(auth_type)s.") diff --git a/neutron_dynamic_routing/services/bgp/agent/driver/ryu/__init__.py b/neutron_dynamic_routing/services/bgp/agent/driver/ryu/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/services/bgp/agent/driver/ryu/driver.py b/neutron_dynamic_routing/services/bgp/agent/driver/ryu/driver.py new file mode 100644 index 00000000..47da7a3f --- /dev/null +++ b/neutron_dynamic_routing/services/bgp/agent/driver/ryu/driver.py @@ -0,0 +1,202 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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 oslo_log import log as logging +from ryu.services.protocols.bgp import bgpspeaker +from ryu.services.protocols.bgp.rtconf.neighbors import CONNECT_MODE_ACTIVE + +from neutron_dynamic_routing._i18n import _LE, _LI +from neutron_dynamic_routing.services.bgp.agent.driver import base +from neutron_dynamic_routing.services.bgp.agent.driver import exceptions as bgp_driver_exc # noqa +from neutron_dynamic_routing.services.bgp.agent.driver import utils + +LOG = logging.getLogger(__name__) + + +# Function for logging BGP peer and path changes. +def bgp_peer_down_cb(remote_ip, remote_as): + LOG.info(_LI('BGP Peer %(peer_ip)s for remote_as=%(peer_as)d went DOWN.'), + {'peer_ip': remote_ip, 'peer_as': remote_as}) + + +def bgp_peer_up_cb(remote_ip, remote_as): + LOG.info(_LI('BGP Peer %(peer_ip)s for remote_as=%(peer_as)d is UP.'), + {'peer_ip': remote_ip, 'peer_as': remote_as}) + + +def best_path_change_cb(event): + LOG.info(_LI("Best path change observed. cidr=%(prefix)s, " + "nexthop=%(nexthop)s, remote_as=%(remote_as)d, " + "is_withdraw=%(is_withdraw)s"), + {'prefix': event.prefix, 'nexthop': event.nexthop, + 'remote_as': event.remote_as, + 'is_withdraw': event.is_withdraw}) + + +class RyuBgpDriver(base.BgpDriverBase): + """BGP speaker implementation via Ryu.""" + + def __init__(self, cfg): + LOG.info(_LI('Initializing Ryu driver for BGP Speaker functionality.')) + self._read_config(cfg) + + # Note: Even though Ryu can only support one BGP speaker as of now, + # we have tried making the framework generic for the future purposes. + self.cache = utils.BgpMultiSpeakerCache() + + def _read_config(self, cfg): + if cfg is None or cfg.bgp_router_id is None: + # If either cfg or router_id is not specified, raise voice + LOG.error(_LE('BGP router-id MUST be specified for the correct ' + 'functional working.')) + else: + self.routerid = cfg.bgp_router_id + LOG.info(_LI('Initialized Ryu BGP Speaker driver interface with ' + 'bgp_router_id=%s'), self.routerid) + + def add_bgp_speaker(self, speaker_as): + curr_speaker = self.cache.get_bgp_speaker(speaker_as) + if curr_speaker is not None: + raise bgp_driver_exc.BgpSpeakerAlreadyScheduled( + current_as=speaker_as, + rtid=self.routerid) + + # Ryu can only support One speaker + if self.cache.get_hosted_bgp_speakers_count() == 1: + raise bgp_driver_exc.BgpSpeakerMaxScheduled(count=1) + + # Validate input parameters. + # speaker_as must be an integer in the allowed range. + utils.validate_as_num('local_as', speaker_as) + + # Notify Ryu about BGP Speaker addition. + # Please note: Since, only the route-advertisement support is + # implemented we are explicitly setting the bgp_server_port + # attribute to 0 which disables listening on port 179. + curr_speaker = bgpspeaker.BGPSpeaker(as_number=speaker_as, + router_id=self.routerid, bgp_server_port=0, + best_path_change_handler=best_path_change_cb, + peer_down_handler=bgp_peer_down_cb, + peer_up_handler=bgp_peer_up_cb) + LOG.info(_LI('Added BGP Speaker for local_as=%(as)d with ' + 'router_id= %(rtid)s.'), + {'as': speaker_as, 'rtid': self.routerid}) + + self.cache.put_bgp_speaker(speaker_as, curr_speaker) + + def delete_bgp_speaker(self, speaker_as): + curr_speaker = self.cache.get_bgp_speaker(speaker_as) + if not curr_speaker: + raise bgp_driver_exc.BgpSpeakerNotAdded(local_as=speaker_as, + rtid=self.routerid) + # Notify Ryu about BGP Speaker deletion + curr_speaker.shutdown() + LOG.info(_LI('Removed BGP Speaker for local_as=%(as)d with ' + 'router_id=%(rtid)s.'), + {'as': speaker_as, 'rtid': self.routerid}) + self.cache.remove_bgp_speaker(speaker_as) + + def add_bgp_peer(self, speaker_as, peer_ip, peer_as, + auth_type='none', password=None): + curr_speaker = self.cache.get_bgp_speaker(speaker_as) + if not curr_speaker: + raise bgp_driver_exc.BgpSpeakerNotAdded(local_as=speaker_as, + rtid=self.routerid) + + # Validate peer_ip and peer_as. + utils.validate_as_num('remote_as', peer_as) + utils.validate_string(peer_ip) + utils.validate_auth(auth_type, password) + + # Notify Ryu about BGP Peer addition + curr_speaker.neighbor_add(address=peer_ip, + remote_as=peer_as, + password=password, + connect_mode=CONNECT_MODE_ACTIVE) + LOG.info(_LI('Added BGP Peer %(peer)s for remote_as=%(as)d to ' + 'BGP Speaker running for local_as=%(local_as)d.'), + {'peer': peer_ip, 'as': peer_as, 'local_as': speaker_as}) + + def delete_bgp_peer(self, speaker_as, peer_ip): + curr_speaker = self.cache.get_bgp_speaker(speaker_as) + if not curr_speaker: + raise bgp_driver_exc.BgpSpeakerNotAdded(local_as=speaker_as, + rtid=self.routerid) + # Validate peer_ip. It must be a string. + utils.validate_string(peer_ip) + + # Notify Ryu about BGP Peer removal + curr_speaker.neighbor_del(address=peer_ip) + LOG.info(_LI('Removed BGP Peer %(peer)s from BGP Speaker ' + 'running for local_as=%(local_as)d.'), + {'peer': peer_ip, 'local_as': speaker_as}) + + def advertise_route(self, speaker_as, cidr, nexthop): + curr_speaker = self.cache.get_bgp_speaker(speaker_as) + if not curr_speaker: + raise bgp_driver_exc.BgpSpeakerNotAdded(local_as=speaker_as, + rtid=self.routerid) + + # Validate cidr and nexthop. Both must be strings. + utils.validate_string(cidr) + utils.validate_string(nexthop) + + # Notify Ryu about route advertisement + curr_speaker.prefix_add(prefix=cidr, next_hop=nexthop) + LOG.info(_LI('Route cidr=%(prefix)s, nexthop=%(nexthop)s is ' + 'advertised for BGP Speaker running for ' + 'local_as=%(local_as)d.'), + {'prefix': cidr, 'nexthop': nexthop, 'local_as': speaker_as}) + + def withdraw_route(self, speaker_as, cidr, nexthop=None): + curr_speaker = self.cache.get_bgp_speaker(speaker_as) + if not curr_speaker: + raise bgp_driver_exc.BgpSpeakerNotAdded(local_as=speaker_as, + rtid=self.routerid) + # Validate cidr. It must be a string. + utils.validate_string(cidr) + + # Notify Ryu about route withdrawal + curr_speaker.prefix_del(prefix=cidr) + LOG.info(_LI('Route cidr=%(prefix)s is withdrawn from BGP Speaker ' + 'running for local_as=%(local_as)d.'), + {'prefix': cidr, 'local_as': speaker_as}) + + def get_bgp_speaker_statistics(self, speaker_as): + LOG.info(_LI('Collecting BGP Speaker statistics for local_as=%d.'), + speaker_as) + curr_speaker = self.cache.get_bgp_speaker(speaker_as) + if not curr_speaker: + raise bgp_driver_exc.BgpSpeakerNotAdded(local_as=speaker_as, + rtid=self.routerid) + + # TODO(vikram): Filter and return the necessary information. + # Will be done as part of new RFE requirement + # https://bugs.launchpad.net/neutron/+bug/1527993 + return curr_speaker.neighbor_state_get() + + def get_bgp_peer_statistics(self, speaker_as, peer_ip): + LOG.info(_LI('Collecting BGP Peer statistics for peer_ip=%(peer)s, ' + 'running in speaker_as=%(speaker_as)d '), + {'peer': peer_ip, 'speaker_as': speaker_as}) + curr_speaker = self.cache.get_bgp_speaker(speaker_as) + if not curr_speaker: + raise bgp_driver_exc.BgpSpeakerNotAdded(local_as=speaker_as, + rtid=self.routerid) + + # TODO(vikram): Filter and return the necessary information. + # Will be done as part of new RFE requirement + # https://bugs.launchpad.net/neutron/+bug/1527993 + return curr_speaker.neighbor_state_get(address=peer_ip) diff --git a/neutron_dynamic_routing/services/bgp/agent/driver/utils.py b/neutron_dynamic_routing/services/bgp/agent/driver/utils.py new file mode 100644 index 00000000..95bdfd69 --- /dev/null +++ b/neutron_dynamic_routing/services/bgp/agent/driver/utils.py @@ -0,0 +1,75 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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 six + +from neutron_dynamic_routing.services.bgp.agent.driver import exceptions as bgp_driver_exc # noqa +from neutron_dynamic_routing.services.bgp.common import constants as bgp_consts # noqa + + +# Parameter validation functions provided are provided by the base. +def validate_as_num(param, as_num): + if not isinstance(as_num, six.integer_types): + raise bgp_driver_exc.InvalidParamType(param=param, + param_type='integer') + + if not (bgp_consts.MIN_ASNUM <= as_num <= bgp_consts.MAX_ASNUM): + # Must be in [AS_NUM_MIN, AS_NUM_MAX] range. + allowed_range = ('[' + + str(bgp_consts.MIN_ASNUM) + '-' + + str(bgp_consts.MAX_ASNUM) + + ']') + raise bgp_driver_exc.InvalidParamRange(param=param, + range=allowed_range) + + +def validate_auth(auth_type, password): + validate_string(password) + if auth_type in bgp_consts.SUPPORTED_AUTH_TYPES: + if auth_type != 'none' and password is None: + raise bgp_driver_exc.PasswordNotSpecified(auth_type=auth_type) + if auth_type == 'none' and password is not None: + raise bgp_driver_exc.InvaildAuthType(auth_type=auth_type) + else: + raise bgp_driver_exc.InvaildAuthType(auth_type=auth_type) + + +def validate_string(param): + if param is not None: + if not isinstance(param, six.string_types): + raise bgp_driver_exc.InvalidParamType(param=param, + param_type='string') + + +class BgpMultiSpeakerCache(object): + """Class for saving multiple BGP speakers information. + + Version history: + 1.0 - Initial version for caching multiple BGP speaker information. + """ + def __init__(self): + self.cache = {} + + def get_hosted_bgp_speakers_count(self): + return len(self.cache) + + def put_bgp_speaker(self, local_as, speaker): + self.cache[local_as] = speaker + + def get_bgp_speaker(self, local_as): + return self.cache.get(local_as) + + def remove_bgp_speaker(self, local_as): + self.cache.pop(local_as, None) diff --git a/neutron_dynamic_routing/services/bgp/agent/entry.py b/neutron_dynamic_routing/services/bgp/agent/entry.py new file mode 100644 index 00000000..a59c1cb4 --- /dev/null +++ b/neutron_dynamic_routing/services/bgp/agent/entry.py @@ -0,0 +1,48 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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 sys + +from oslo_config import cfg +from oslo_service import service + +from neutron.agent.common import config +from neutron.agent.linux import external_process +from neutron.common import config as common_config +from neutron import service as neutron_service + +from neutron_dynamic_routing.services.bgp.agent import config as bgp_dragent_config # noqa +from neutron_dynamic_routing.services.bgp.common import constants as bgp_consts # noqa + + +def register_options(): + config.register_agent_state_opts_helper(cfg.CONF) + config.register_root_helper(cfg.CONF) + cfg.CONF.register_opts(bgp_dragent_config.BGP_DRIVER_OPTS, 'BGP') + cfg.CONF.register_opts(bgp_dragent_config.BGP_PROTO_CONFIG_OPTS, 'BGP') + cfg.CONF.register_opts(external_process.OPTS) + + +def main(): + register_options() + common_config.init(sys.argv[1:]) + config.setup_logging() + server = neutron_service.Service.create( + binary='neutron-bgp-dragent', + topic=bgp_consts.BGP_DRAGENT, + report_interval=cfg.CONF.AGENT.report_interval, + manager='neutron_dynamic_routing.services.bgp.agent.bgp_dragent.' + 'BgpDrAgentWithStateReport') + service.launch(cfg.CONF, server).wait() diff --git a/neutron_dynamic_routing/services/bgp/bgp_plugin.py b/neutron_dynamic_routing/services/bgp/bgp_plugin.py new file mode 100644 index 00000000..e740a582 --- /dev/null +++ b/neutron_dynamic_routing/services/bgp/bgp_plugin.py @@ -0,0 +1,390 @@ +# Copyright 2016 Hewlett Packard Enterprise Development Company LP +# +# 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 netaddr import IPAddress + +from neutron_lib import constants as n_const +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import importutils + +from neutron.callbacks import events +from neutron.callbacks import registry +from neutron.callbacks import resources +from neutron.common import rpc as n_rpc +from neutron import context +from neutron import policy +from neutron.services import service_base + +from neutron_dynamic_routing.api.rpc.agentnotifiers import bgp_dr_rpc_agent_api # noqa +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 as bgp_ext +from neutron_dynamic_routing.extensions import bgp_dragentscheduler as dras_ext +from neutron_dynamic_routing.services.bgp.common import constants as bgp_consts + +PLUGIN_NAME = bgp_ext.BGP_EXT_ALIAS + '_svc_plugin' +LOG = logging.getLogger(__name__) + + +class BgpPlugin(service_base.ServicePluginBase, + bgp_db.BgpDbMixin, + bgp_dragentscheduler_db.BgpDrAgentSchedulerDbMixin): + + supported_extension_aliases = [bgp_ext.BGP_EXT_ALIAS, + dras_ext.BGP_DRAGENT_SCHEDULER_EXT_ALIAS] + + def __init__(self): + super(BgpPlugin, self).__init__() + self.bgp_drscheduler = importutils.import_object( + cfg.CONF.bgp_drscheduler_driver) + self._setup_rpc() + self._register_callbacks() + + def get_plugin_name(self): + return PLUGIN_NAME + + 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 tenant networks, floating IP's, and DVR host routes.") + + def _setup_rpc(self): + self.topic = bgp_consts.BGP_PLUGIN + self.conn = n_rpc.create_connection() + self.agent_notifiers[bgp_consts.AGENT_TYPE_BGP_ROUTING] = ( + bgp_dr_rpc_agent_api.BgpDrAgentNotifyApi() + ) + self._bgp_rpc = self.agent_notifiers[bgp_consts.AGENT_TYPE_BGP_ROUTING] + self.endpoints = [bs_rpc.BgpSpeakerRpcCallback()] + self.conn.create_consumer(self.topic, self.endpoints, + fanout=False) + self.conn.consume_in_threads() + + def _register_callbacks(self): + registry.subscribe(self.floatingip_update_callback, + resources.FLOATING_IP, + events.AFTER_UPDATE) + registry.subscribe(self.router_interface_callback, + resources.ROUTER_INTERFACE, + events.AFTER_CREATE) + registry.subscribe(self.router_interface_callback, + resources.ROUTER_INTERFACE, + events.BEFORE_CREATE) + registry.subscribe(self.router_interface_callback, + resources.ROUTER_INTERFACE, + events.AFTER_DELETE) + registry.subscribe(self.router_gateway_callback, + resources.ROUTER_GATEWAY, + events.AFTER_CREATE) + registry.subscribe(self.router_gateway_callback, + resources.ROUTER_GATEWAY, + events.AFTER_DELETE) + + def get_bgp_speakers(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + policy.check_is_admin(context) + return super(BgpPlugin, self).get_bgp_speakers( + context, + filters=filters, + fields=fields, + sorts=sorts, + limit=limit, + marker=marker, + page_reverse=page_reverse) + + def get_bgp_speaker(self, context, bgp_speaker_id, fields=None): + policy.check_is_admin(context) + return super(BgpPlugin, self).get_bgp_speaker(context, + bgp_speaker_id, + fields=fields) + + def create_bgp_speaker(self, context, bgp_speaker): + policy.check_is_admin(context) + bgp_speaker = super(BgpPlugin, self).create_bgp_speaker(context, + bgp_speaker) + return bgp_speaker + + def update_bgp_speaker(self, context, bgp_speaker_id, bgp_speaker): + policy.check_is_admin(context) + return super(BgpPlugin, self).update_bgp_speaker(context, + bgp_speaker_id, + bgp_speaker) + + def delete_bgp_speaker(self, context, bgp_speaker_id): + policy.check_is_admin(context) + hosted_bgp_dragents = self.get_dragents_hosting_bgp_speakers( + context, + [bgp_speaker_id]) + super(BgpPlugin, self).delete_bgp_speaker(context, bgp_speaker_id) + for agent in hosted_bgp_dragents: + self._bgp_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): + policy.check_is_admin(context) + return super(BgpPlugin, self).get_bgp_peers( + context, fields=fields, + filters=filters, sorts=sorts, + limit=limit, marker=marker, + page_reverse=page_reverse) + + def get_bgp_peer(self, context, bgp_peer_id, fields=None): + policy.check_is_admin(context) + return super(BgpPlugin, self).get_bgp_peer(context, + bgp_peer_id, + fields=fields) + + def create_bgp_peer(self, context, bgp_peer): + policy.check_is_admin(context) + return super(BgpPlugin, self).create_bgp_peer(context, bgp_peer) + + def update_bgp_peer(self, context, bgp_peer_id, bgp_peer): + policy.check_is_admin(context) + return super(BgpPlugin, self).update_bgp_peer(context, + bgp_peer_id, + bgp_peer) + + def delete_bgp_peer(self, context, bgp_peer_id): + policy.check_is_admin(context) + super(BgpPlugin, self).delete_bgp_peer(context, bgp_peer_id) + + def add_bgp_peer(self, context, bgp_speaker_id, bgp_peer_info): + policy.check_is_admin(context) + ret_value = super(BgpPlugin, self).add_bgp_peer(context, + bgp_speaker_id, + bgp_peer_info) + hosted_bgp_dragents = self.get_dragents_hosting_bgp_speakers( + 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) + return ret_value + + def remove_bgp_peer(self, context, bgp_speaker_id, bgp_peer_info): + policy.check_is_admin(context) + hosted_bgp_dragents = self.get_dragents_hosting_bgp_speakers( + context, [bgp_speaker_id]) + + ret_value = super(BgpPlugin, self).remove_bgp_peer(context, + bgp_speaker_id, + bgp_peer_info) + + for agent in hosted_bgp_dragents: + self._bgp_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): + policy.check_is_admin(context) + super(BgpPlugin, self).add_bgp_speaker_to_dragent(context, + agent_id, + speaker_id) + + def remove_bgp_speaker_from_dragent(self, context, agent_id, speaker_id): + policy.check_is_admin(context) + super(BgpPlugin, self).remove_bgp_speaker_from_dragent(context, + agent_id, + speaker_id) + + def list_bgp_speaker_on_dragent(self, context, agent_id): + policy.check_is_admin(context) + return super(BgpPlugin, self).list_bgp_speaker_on_dragent(context, + agent_id) + + def list_dragent_hosting_bgp_speaker(self, context, speaker_id): + policy.check_is_admin(context) + return super(BgpPlugin, self).list_dragent_hosting_bgp_speaker( + context, + speaker_id) + + def add_gateway_network(self, context, bgp_speaker_id, network_info): + policy.check_is_admin(context) + return super(BgpPlugin, self).add_gateway_network(context, + bgp_speaker_id, + network_info) + + def remove_gateway_network(self, context, bgp_speaker_id, network_info): + policy.check_is_admin(context) + return super(BgpPlugin, self).remove_gateway_network(context, + bgp_speaker_id, + network_info) + + def get_advertised_routes(self, context, bgp_speaker_id): + policy.check_is_admin(context) + return super(BgpPlugin, self).get_advertised_routes(context, + bgp_speaker_id) + + def floatingip_update_callback(self, resource, event, trigger, **kwargs): + if event != events.AFTER_UPDATE: + return + + ctx = context.get_admin_context() + new_router_id = kwargs['router_id'] + last_router_id = kwargs['last_known_router_id'] + next_hop = kwargs['next_hop'] + dest = kwargs['floating_ip_address'] + '/32' + bgp_speakers = self._bgp_speakers_for_gw_network_by_family( + ctx, + kwargs['floating_network_id'], + n_const.IP_VERSION_4) + + if last_router_id and new_router_id != last_router_id: + for bgp_speaker in bgp_speakers: + self.stop_route_advertisements(ctx, self._bgp_rpc, + bgp_speaker.id, [dest]) + + if next_hop and new_router_id != last_router_id: + new_host_route = {'destination': dest, 'next_hop': next_hop} + for bgp_speaker in bgp_speakers: + self.start_route_advertisements(ctx, self._bgp_rpc, + bgp_speaker.id, + [new_host_route]) + + def router_interface_callback(self, resource, event, trigger, **kwargs): + if event == events.AFTER_CREATE: + self._handle_router_interface_after_create(**kwargs) + if event == events.AFTER_DELETE: + gw_network = kwargs['network_id'] + next_hops = self._next_hops_from_gateway_ips( + kwargs['gateway_ips']) + ctx = context.get_admin_context() + speakers = self._bgp_speakers_for_gateway_network(ctx, gw_network) + for speaker in speakers: + routes = self._route_list_from_prefixes_and_next_hop( + kwargs['cidrs'], + next_hops[speaker.ip_version]) + self._handle_router_interface_after_delete(gw_network, routes) + + def _handle_router_interface_after_create(self, **kwargs): + gw_network = kwargs['network_id'] + if not gw_network: + return + + ctx = context.get_admin_context() + with ctx.session.begin(subtransactions=True): + speakers = self._bgp_speakers_for_gateway_network(ctx, + gw_network) + next_hops = self._next_hops_from_gateway_ips( + kwargs['gateway_ips']) + + for speaker in speakers: + prefixes = self._tenant_prefixes_by_router( + ctx, + kwargs['router_id'], + speaker.id) + next_hop = next_hops.get(speaker.ip_version) + if next_hop: + rl = self._route_list_from_prefixes_and_next_hop(prefixes, + next_hop) + self.start_route_advertisements(ctx, + self._bgp_rpc, + speaker.id, + rl) + + def router_gateway_callback(self, resource, event, trigger, **kwargs): + if event == events.AFTER_CREATE: + self._handle_router_gateway_after_create(**kwargs) + if event == events.AFTER_DELETE: + gw_network = kwargs['network_id'] + router_id = kwargs['router_id'] + next_hops = self._next_hops_from_gateway_ips( + kwargs['gateway_ips']) + ctx = context.get_admin_context() + speakers = self._bgp_speakers_for_gateway_network(ctx, gw_network) + for speaker in speakers: + if speaker.ip_version in next_hops: + next_hop = next_hops[speaker.ip_version] + prefixes = self._tenant_prefixes_by_router(ctx, + router_id, + speaker.id) + routes = self._route_list_from_prefixes_and_next_hop( + prefixes, + next_hop) + self._handle_router_interface_after_delete(gw_network, routes) + + def _handle_router_gateway_after_create(self, **kwargs): + ctx = context.get_admin_context() + gw_network = kwargs['network_id'] + router_id = kwargs['router_id'] + with ctx.session.begin(subtransactions=True): + speakers = self._bgp_speakers_for_gateway_network(ctx, + gw_network) + next_hops = self._next_hops_from_gateway_ips(kwargs['gw_ips']) + + for speaker in speakers: + if speaker.ip_version in next_hops: + next_hop = next_hops[speaker.ip_version] + prefixes = self._tenant_prefixes_by_router(ctx, + router_id, + speaker.id) + routes = self._route_list_from_prefixes_and_next_hop( + prefixes, + next_hop) + self.start_route_advertisements(ctx, self._bgp_rpc, + speaker.id, routes) + + def _handle_router_interface_after_delete(self, gw_network, routes): + if gw_network and routes: + ctx = context.get_admin_context() + speakers = self._bgp_speakers_for_gateway_network(ctx, gw_network) + for speaker in speakers: + self.stop_route_advertisements(ctx, self._bgp_rpc, + speaker.id, routes) + + def _next_hops_from_gateway_ips(self, gw_ips): + if gw_ips: + return {IPAddress(ip).version: ip for ip in gw_ips} + return {} + + def start_route_advertisements(self, ctx, bgp_rpc, + bgp_speaker_id, routes): + agents = self.list_dragent_hosting_bgp_speaker(ctx, bgp_speaker_id) + for agent in agents['agents']: + bgp_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) + + def stop_route_advertisements(self, ctx, bgp_rpc, + bgp_speaker_id, routes): + agents = self.list_dragent_hosting_bgp_speaker(ctx, bgp_speaker_id) + for agent in agents['agents']: + bgp_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) + + def _debug_log_for_routes(self, msg, routes, bgp_speaker_id): + + # Could have a large number of routes passed, check log level first + if LOG.isEnabledFor(logging.DEBUG): + for route in routes: + LOG.debug(msg, route, bgp_speaker_id) diff --git a/neutron_dynamic_routing/services/bgp/common/__init__.py b/neutron_dynamic_routing/services/bgp/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/services/bgp/common/constants.py b/neutron_dynamic_routing/services/bgp/common/constants.py new file mode 100644 index 00000000..42c47bd0 --- /dev/null +++ b/neutron_dynamic_routing/services/bgp/common/constants.py @@ -0,0 +1,27 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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. + +AGENT_TYPE_BGP_ROUTING = 'BGP dynamic routing agent' + +BGP_DRAGENT = 'bgp_dragent' + +BGP_PLUGIN = 'q-bgp-plugin' + +# List of supported authentication types. +SUPPORTED_AUTH_TYPES = ['none', 'md5'] + +# Supported AS number range +MIN_ASNUM = 1 +MAX_ASNUM = 65535 diff --git a/neutron_dynamic_routing/services/bgp/common/opts.py b/neutron_dynamic_routing/services/bgp/common/opts.py new file mode 100644 index 00000000..c0ee666d --- /dev/null +++ b/neutron_dynamic_routing/services/bgp/common/opts.py @@ -0,0 +1,30 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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. + +import itertools + +import neutron_dynamic_routing.services.bgp.agent.config + + +def list_bgp_agent_opts(): + return [ + ('BGP', + itertools.chain( + neutron_dynamic_routing.services.bgp.agent. + config.BGP_DRIVER_OPTS, + neutron_dynamic_routing.services.bgp.agent. + config.BGP_PROTO_CONFIG_OPTS) + ) + ] diff --git a/neutron_dynamic_routing/services/bgp/scheduler/__init__.py b/neutron_dynamic_routing/services/bgp/scheduler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/services/bgp/scheduler/bgp_dragent_scheduler.py b/neutron_dynamic_routing/services/bgp/scheduler/bgp_dragent_scheduler.py new file mode 100644 index 00000000..212ee9c9 --- /dev/null +++ b/neutron_dynamic_routing/services/bgp/scheduler/bgp_dragent_scheduler.py @@ -0,0 +1,192 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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 oslo_db import exception as db_exc +from oslo_log import log as logging +from sqlalchemy.orm import exc +from sqlalchemy import sql + +from neutron.db import agents_db +from neutron.scheduler import base_resource_filter +from neutron.scheduler import base_scheduler + +from neutron_dynamic_routing._i18n import _LI, _LW +from neutron_dynamic_routing.db import bgp_db +from neutron_dynamic_routing.db import bgp_dragentscheduler_db as bgp_dras_db +from neutron_dynamic_routing.services.bgp.common import constants as bgp_consts + +LOG = logging.getLogger(__name__) +BGP_SPEAKER_PER_DRAGENT = 1 + + +class BgpDrAgentFilter(base_resource_filter.BaseResourceFilter): + + def bind(self, context, agents, bgp_speaker_id): + """Bind the BgpSpeaker to a BgpDrAgent.""" + bound_agents = agents[:] + for agent in agents: + # saving agent_id to use it after rollback to avoid + # DetachedInstanceError + agent_id = agent.id + binding = bgp_dras_db.BgpSpeakerDrAgentBinding() + binding.agent_id = agent_id + binding.bgp_speaker_id = bgp_speaker_id + try: + with context.session.begin(subtransactions=True): + context.session.add(binding) + except db_exc.DBDuplicateEntry: + # it's totally ok, someone just did our job! + bound_agents.remove(agent) + LOG.info(_LI('BgpDrAgent %s already present'), agent_id) + LOG.debug('BgpSpeaker %(bgp_speaker_id)s is scheduled to be ' + 'hosted by BgpDrAgent %(agent_id)s', + {'bgp_speaker_id': bgp_speaker_id, + 'agent_id': agent_id}) + super(BgpDrAgentFilter, self).bind(context, bound_agents, + bgp_speaker_id) + + def filter_agents(self, plugin, context, bgp_speaker): + """Return the agents that can host the BgpSpeaker.""" + agents_dict = self._get_bgp_speaker_hostable_dragents( + plugin, context, bgp_speaker) + if not agents_dict['hostable_agents'] or agents_dict['n_agents'] <= 0: + return {'n_agents': 0, + 'hostable_agents': [], + 'hosted_agents': []} + return agents_dict + + def _get_active_dragents(self, plugin, context): + """Return a list of active BgpDrAgents.""" + with context.session.begin(subtransactions=True): + active_dragents = plugin.get_agents_db( + context, filters={ + 'agent_type': [bgp_consts.AGENT_TYPE_BGP_ROUTING], + 'admin_state_up': [True]}) + if not active_dragents: + return [] + return active_dragents + + def _get_num_dragents_hosting_bgp_speaker(self, bgp_speaker_id, + dragent_bindings): + return sum(1 if dragent_binding.bgp_speaker_id == bgp_speaker_id else 0 + for dragent_binding in dragent_bindings) + + def _get_bgp_speaker_hostable_dragents(self, plugin, context, bgp_speaker): + """Return number of additional BgpDrAgents which will actually host + the given BgpSpeaker and a list of BgpDrAgents which can host the + given BgpSpeaker + """ + # only one BgpSpeaker can be hosted by a BgpDrAgent for now. + dragents_per_bgp_speaker = BGP_SPEAKER_PER_DRAGENT + dragent_bindings = plugin.get_dragent_bgp_speaker_bindings(context) + agents_hosting = [dragent_binding.agent_id + for dragent_binding in dragent_bindings] + + num_dragents_hosting_bgp_speaker = ( + self._get_num_dragents_hosting_bgp_speaker(bgp_speaker['id'], + dragent_bindings)) + n_agents = dragents_per_bgp_speaker - num_dragents_hosting_bgp_speaker + if n_agents <= 0: + return {'n_agents': 0, + 'hostable_agents': [], + 'hosted_agents': []} + + active_dragents = self._get_active_dragents(plugin, context) + hostable_dragents = [ + agent for agent in set(active_dragents) + if agent.id not in agents_hosting and plugin.is_eligible_agent( + active=True, agent=agent) + ] + if not hostable_dragents: + return {'n_agents': 0, + 'hostable_agents': [], + 'hosted_agents': []} + + n_agents = min(len(hostable_dragents), n_agents) + return {'n_agents': n_agents, + 'hostable_agents': hostable_dragents, + 'hosted_agents': num_dragents_hosting_bgp_speaker} + + +class BgpDrAgentSchedulerBase(BgpDrAgentFilter): + + def schedule_unscheduled_bgp_speakers(self, context, host): + """Schedule unscheduled BgpSpeaker to a BgpDrAgent. + """ + + LOG.debug('Started auto-scheduling on host %s', host) + with context.session.begin(subtransactions=True): + query = context.session.query(agents_db.Agent) + query = query.filter_by( + agent_type=bgp_consts.AGENT_TYPE_BGP_ROUTING, + host=host, + admin_state_up=sql.true()) + try: + bgp_dragent = query.one() + except (exc.NoResultFound): + LOG.debug('No enabled BgpDrAgent on host %s', host) + return False + + if agents_db.AgentDbMixin.is_agent_down( + bgp_dragent.heartbeat_timestamp): + LOG.warning(_LW('BgpDrAgent %s is down'), bgp_dragent.id) + return False + + if self._is_bgp_speaker_hosted(context, bgp_dragent['id']): + # One BgpDrAgent can only host one BGP speaker + LOG.debug('BgpDrAgent already hosting a speaker on host %s. ' + 'Cannot schedule an another one', host) + return False + + unscheduled_speakers = self._get_unscheduled_bgp_speakers(context) + if not unscheduled_speakers: + LOG.debug('Nothing to auto-schedule on host %s', host) + return False + + self.bind(context, [bgp_dragent], unscheduled_speakers[0]) + return True + + def _is_bgp_speaker_hosted(self, context, agent_id): + speaker_binding_model = bgp_dras_db.BgpSpeakerDrAgentBinding + + query = context.session.query(speaker_binding_model) + query = query.filter(speaker_binding_model.agent_id == agent_id) + + return query.count() > 0 + + def _get_unscheduled_bgp_speakers(self, context): + """BGP speakers that needs to be scheduled. + """ + + no_agent_binding = ~sql.exists().where( + bgp_db.BgpSpeaker.id == + bgp_dras_db.BgpSpeakerDrAgentBinding.bgp_speaker_id) + query = context.session.query(bgp_db.BgpSpeaker.id).filter( + no_agent_binding) + return [bgp_speaker_id_[0] for bgp_speaker_id_ in query] + + +class ChanceScheduler(base_scheduler.BaseChanceScheduler, + BgpDrAgentSchedulerBase): + + def __init__(self): + super(ChanceScheduler, self).__init__(self) + + +class WeightScheduler(base_scheduler.BaseWeightScheduler, + BgpDrAgentSchedulerBase): + + def __init__(self): + super(WeightScheduler, self).__init__(self) diff --git a/neutron_dynamic_routing/tests/api/test_bgp_speaker_extensions.py b/neutron_dynamic_routing/tests/api/test_bgp_speaker_extensions.py new file mode 100644 index 00000000..bb57783f --- /dev/null +++ b/neutron_dynamic_routing/tests/api/test_bgp_speaker_extensions.py @@ -0,0 +1,288 @@ +# Copyright 2016 Hewlett Packard Enterprise Development Company LP +# +# 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 netaddr +from tempest import config +from tempest.lib import exceptions as lib_exc +from tempest import test +import testtools + +from neutron.tests.api import base +from neutron.tests.tempest.common import tempest_fixtures as fixtures + +CONF = config.CONF + + +class BgpSpeakerTestJSONBase(base.BaseAdminNetworkTest): + + default_bgp_speaker_args = {'local_as': '1234', + 'ip_version': 4, + 'name': 'my-bgp-speaker', + 'advertise_floating_ip_host_routes': True, + 'advertise_tenant_networks': True} + default_bgp_peer_args = {'remote_as': '4321', + 'name': 'my-bgp-peer', + 'peer_ip': '192.168.1.1', + 'auth_type': 'md5', 'password': 'my-secret'} + + @classmethod + def resource_setup(cls): + super(BgpSpeakerTestJSONBase, cls).resource_setup() + if not test.is_extension_enabled('bgp_speaker', 'network'): + msg = "BGP Speaker extension is not enabled." + raise cls.skipException(msg) + + cls.admin_routerports = [] + cls.admin_floatingips = [] + cls.admin_routers = [] + cls.ext_net_id = CONF.network.public_network_id + + @classmethod + def resource_cleanup(cls): + for floatingip in cls.admin_floatingips: + cls._try_delete_resource(cls.admin_client.delete_floatingip, + floatingip['id']) + for routerport in cls.admin_routerports: + cls._try_delete_resource( + cls.admin_client.remove_router_interface_with_subnet_id, + routerport['router_id'], routerport['subnet_id']) + for router in cls.admin_routers: + cls._try_delete_resource(cls.admin_client.delete_router, + router['id']) + super(BgpSpeakerTestJSONBase, cls).resource_cleanup() + + def create_bgp_speaker(self, auto_delete=True, **args): + data = {'bgp_speaker': args} + bgp_speaker = self.admin_client.create_bgp_speaker(data) + bgp_speaker_id = bgp_speaker['bgp-speaker']['id'] + if auto_delete: + self.addCleanup(self.delete_bgp_speaker, bgp_speaker_id) + return bgp_speaker + + def create_bgp_peer(self, **args): + bgp_peer = self.admin_client.create_bgp_peer({'bgp_peer': args}) + bgp_peer_id = bgp_peer['bgp-peer']['id'] + self.addCleanup(self.delete_bgp_peer, bgp_peer_id) + return bgp_peer + + def update_bgp_speaker(self, id, **args): + data = {'bgp_speaker': args} + return self.admin_client.update_bgp_speaker(id, data) + + def delete_bgp_speaker(self, id): + return self.admin_client.delete_bgp_speaker(id) + + def get_bgp_speaker(self, id): + return self.admin_client.get_bgp_speaker(id) + + def create_bgp_speaker_and_peer(self): + bgp_speaker = self.create_bgp_speaker(**self.default_bgp_speaker_args) + bgp_peer = self.create_bgp_peer(**self.default_bgp_peer_args) + return (bgp_speaker, bgp_peer) + + def delete_bgp_peer(self, id): + return self.admin_client.delete_bgp_peer(id) + + def add_bgp_peer(self, bgp_speaker_id, bgp_peer_id): + return self.admin_client.add_bgp_peer_with_id(bgp_speaker_id, + bgp_peer_id) + + def remove_bgp_peer(self, bgp_speaker_id, bgp_peer_id): + return self.admin_client.remove_bgp_peer_with_id(bgp_speaker_id, + bgp_peer_id) + + def delete_address_scope(self, id): + return self.admin_client.delete_address_scope(id) + + +class BgpSpeakerTestJSON(BgpSpeakerTestJSONBase): + + """ + Tests the following operations in the Neutron API using the REST client for + Neutron: + + Create bgp-speaker + Delete bgp-speaker + Create bgp-peer + Update bgp-peer + Delete bgp-peer + """ + + @test.idempotent_id('df259771-7104-4ffa-b77f-bd183600d7f9') + def test_delete_bgp_speaker(self): + bgp_speaker = self.create_bgp_speaker(auto_delete=False, + **self.default_bgp_speaker_args) + bgp_speaker_id = bgp_speaker['bgp-speaker']['id'] + self.delete_bgp_speaker(bgp_speaker_id) + self.assertRaises(lib_exc.NotFound, + self.get_bgp_speaker, + bgp_speaker_id) + + @test.idempotent_id('81d9dc45-19f8-4c6e-88b8-401d965cd1b0') + def test_create_bgp_peer(self): + self.create_bgp_peer(**self.default_bgp_peer_args) + + @test.idempotent_id('6ade0319-1ee2-493c-ac4b-5eb230ff3a77') + def test_add_bgp_peer(self): + bgp_speaker, bgp_peer = self.create_bgp_speaker_and_peer() + bgp_speaker_id = bgp_speaker['bgp-speaker']['id'] + bgp_peer_id = bgp_peer['bgp-peer']['id'] + + self.add_bgp_peer(bgp_speaker_id, bgp_peer_id) + bgp_speaker = self.admin_client.get_bgp_speaker(bgp_speaker_id) + bgp_peers_list = bgp_speaker['bgp-speaker']['peers'] + self.assertEqual(1, len(bgp_peers_list)) + self.assertTrue(bgp_peer_id in bgp_peers_list) + + @test.idempotent_id('f9737708-1d79-440b-8350-779f97d882ee') + def test_remove_bgp_peer(self): + bgp_peer = self.create_bgp_peer(**self.default_bgp_peer_args) + bgp_peer_id = bgp_peer['bgp-peer']['id'] + bgp_speaker = self.create_bgp_speaker(**self.default_bgp_speaker_args) + bgp_speaker_id = bgp_speaker['bgp-speaker']['id'] + self.add_bgp_peer(bgp_speaker_id, bgp_peer_id) + bgp_speaker = self.admin_client.get_bgp_speaker(bgp_speaker_id) + bgp_peers_list = bgp_speaker['bgp-speaker']['peers'] + self.assertTrue(bgp_peer_id in bgp_peers_list) + + bgp_speaker = self.remove_bgp_peer(bgp_speaker_id, bgp_peer_id) + bgp_speaker = self.admin_client.get_bgp_speaker(bgp_speaker_id) + bgp_peers_list = bgp_speaker['bgp-speaker']['peers'] + self.assertTrue(not bgp_peers_list) + + @testtools.skip('bug/1553374') + @test.idempotent_id('23c8eb37-d10d-4f43-b2e7-6542cb6a4405') + def test_add_gateway_network(self): + self.useFixture(fixtures.LockFixture('gateway_network_binding')) + bgp_speaker = self.create_bgp_speaker(**self.default_bgp_speaker_args) + bgp_speaker_id = bgp_speaker['bgp-speaker']['id'] + + self.admin_client.add_bgp_gateway_network(bgp_speaker_id, + self.ext_net_id) + bgp_speaker = self.admin_client.get_bgp_speaker(bgp_speaker_id) + network_list = bgp_speaker['bgp-speaker']['networks'] + self.assertEqual(1, len(network_list)) + self.assertTrue(self.ext_net_id in network_list) + + @testtools.skip('bug/1553374') + @test.idempotent_id('6cfc7137-0d99-4a3d-826c-9d1a3a1767b0') + def test_remove_gateway_network(self): + self.useFixture(fixtures.LockFixture('gateway_network_binding')) + bgp_speaker = self.create_bgp_speaker(**self.default_bgp_speaker_args) + bgp_speaker_id = bgp_speaker['bgp-speaker']['id'] + self.admin_client.add_bgp_gateway_network(bgp_speaker_id, + self.ext_net_id) + bgp_speaker = self.admin_client.get_bgp_speaker(bgp_speaker_id) + networks = bgp_speaker['bgp-speaker']['networks'] + + self.assertTrue(self.ext_net_id in networks) + self.admin_client.remove_bgp_gateway_network(bgp_speaker_id, + self.ext_net_id) + bgp_speaker = self.admin_client.get_bgp_speaker(bgp_speaker_id) + network_list = bgp_speaker['bgp-speaker']['networks'] + self.assertTrue(not network_list) + + @testtools.skip('bug/1553374') + @test.idempotent_id('5bef22ad-5e70-4f7b-937a-dc1944642996') + def test_get_advertised_routes_null_address_scope(self): + self.useFixture(fixtures.LockFixture('gateway_network_binding')) + bgp_speaker = self.create_bgp_speaker(**self.default_bgp_speaker_args) + bgp_speaker_id = bgp_speaker['bgp-speaker']['id'] + self.admin_client.add_bgp_gateway_network(bgp_speaker_id, + self.ext_net_id) + routes = self.admin_client.get_bgp_advertised_routes(bgp_speaker_id) + self.assertEqual(0, len(routes['advertised_routes'])) + + @testtools.skip('bug/1553374') + @test.idempotent_id('cae9cdb1-ad65-423c-9604-d4cd0073616e') + def test_get_advertised_routes_floating_ips(self): + self.useFixture(fixtures.LockFixture('gateway_network_binding')) + bgp_speaker = self.create_bgp_speaker(**self.default_bgp_speaker_args) + bgp_speaker_id = bgp_speaker['bgp-speaker']['id'] + self.admin_client.add_bgp_gateway_network(bgp_speaker_id, + self.ext_net_id) + tenant_net = self.create_network() + tenant_subnet = self.create_subnet(tenant_net) + ext_gw_info = {'network_id': self.ext_net_id} + router = self.admin_client.create_router( + 'my-router', + external_gateway_info=ext_gw_info, + admin_state_up=True, + distributed=False) + self.admin_routers.append(router['router']) + self.admin_client.add_router_interface_with_subnet_id( + router['router']['id'], + tenant_subnet['id']) + self.admin_routerports.append({'router_id': router['router']['id'], + 'subnet_id': tenant_subnet['id']}) + tenant_port = self.create_port(tenant_net) + floatingip = self.create_floatingip(self.ext_net_id) + self.admin_floatingips.append(floatingip) + self.client.update_floatingip(floatingip['id'], + port_id=tenant_port['id']) + routes = self.admin_client.get_bgp_advertised_routes(bgp_speaker_id) + self.assertEqual(1, len(routes['advertised_routes'])) + self.assertEqual(floatingip['floating_ip_address'] + '/32', + routes['advertised_routes'][0]['destination']) + + @testtools.skip('bug/1553374') + @test.idempotent_id('c9ad566e-fe8f-4559-8303-bbad9062a30c') + def test_get_advertised_routes_tenant_networks(self): + self.useFixture(fixtures.LockFixture('gateway_network_binding')) + addr_scope = self.create_address_scope('my-scope', ip_version=4) + ext_net = self.create_shared_network(**{'router:external': True}) + tenant_net = self.create_network() + ext_subnetpool = self.create_subnetpool( + 'test-pool-ext', + is_admin=True, + default_prefixlen=24, + address_scope_id=addr_scope['id'], + prefixes=['8.0.0.0/8']) + tenant_subnetpool = self.create_subnetpool( + 'tenant-test-pool', + default_prefixlen=25, + address_scope_id=addr_scope['id'], + prefixes=['10.10.0.0/16']) + self.create_subnet({'id': ext_net['id']}, + cidr=netaddr.IPNetwork('8.0.0.0/24'), + ip_version=4, + client=self.admin_client, + subnetpool_id=ext_subnetpool['id']) + tenant_subnet = self.create_subnet( + {'id': tenant_net['id']}, + cidr=netaddr.IPNetwork('10.10.0.0/24'), + ip_version=4, + subnetpool_id=tenant_subnetpool['id']) + ext_gw_info = {'network_id': ext_net['id']} + router = self.admin_client.create_router( + 'my-router', + external_gateway_info=ext_gw_info, + distributed=False)['router'] + self.admin_routers.append(router) + self.admin_client.add_router_interface_with_subnet_id( + router['id'], + tenant_subnet['id']) + self.admin_routerports.append({'router_id': router['id'], + 'subnet_id': tenant_subnet['id']}) + bgp_speaker = self.create_bgp_speaker(**self.default_bgp_speaker_args) + bgp_speaker_id = bgp_speaker['bgp-speaker']['id'] + self.admin_client.add_bgp_gateway_network(bgp_speaker_id, + ext_net['id']) + routes = self.admin_client.get_bgp_advertised_routes(bgp_speaker_id) + self.assertEqual(1, len(routes['advertised_routes'])) + self.assertEqual(tenant_subnet['cidr'], + routes['advertised_routes'][0]['destination']) + fixed_ip = router['external_gateway_info']['external_fixed_ips'][0] + self.assertEqual(fixed_ip['ip_address'], + routes['advertised_routes'][0]['next_hop']) diff --git a/neutron_dynamic_routing/tests/api/test_bgp_speaker_extensions_negative.py b/neutron_dynamic_routing/tests/api/test_bgp_speaker_extensions_negative.py new file mode 100644 index 00000000..d7c78d07 --- /dev/null +++ b/neutron_dynamic_routing/tests/api/test_bgp_speaker_extensions_negative.py @@ -0,0 +1,121 @@ +# Copyright 2016 Hewlett Packard Enterprise Development Company LP +# +# 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 netaddr + +from tempest.lib import exceptions as lib_exc +from tempest import test + +from neutron_dynamic_routing.tests.api import test_bgp_speaker_extensions as test_base # noqa + + +class BgpSpeakerTestJSONNegative(test_base.BgpSpeakerTestJSONBase): + + """Negative test cases asserting proper behavior of BGP API extension""" + + @test.attr(type=['negative', 'smoke']) + @test.idempotent_id('75e9ee2f-6efd-4320-bff7-ae24741c8b06') + def test_create_bgp_speaker_illegal_local_asn(self): + self.assertRaises(lib_exc.BadRequest, + self.create_bgp_speaker, + local_as='65537') + + @test.attr(type=['negative', 'smoke']) + @test.idempotent_id('6742ec2e-382a-4453-8791-13a19b47cd13') + def test_create_bgp_speaker_non_admin(self): + self.assertRaises(lib_exc.Forbidden, + self.client.create_bgp_speaker, + {'bgp_speaker': self.default_bgp_speaker_args}) + + @test.attr(type=['negative', 'smoke']) + @test.idempotent_id('33f7aaf0-9786-478b-b2d1-a51086a50eb4') + def test_create_bgp_peer_non_admin(self): + self.assertRaises(lib_exc.Forbidden, + self.client.create_bgp_peer, + {'bgp_peer': self.default_bgp_peer_args}) + + @test.attr(type=['negative', 'smoke']) + @test.idempotent_id('39435932-0266-4358-899b-0e9b1e53c3e9') + def test_update_bgp_speaker_local_asn(self): + bgp_speaker = self.create_bgp_speaker(**self.default_bgp_speaker_args) + bgp_speaker_id = bgp_speaker['bgp-speaker']['id'] + + self.assertRaises(lib_exc.BadRequest, self.update_bgp_speaker, + bgp_speaker_id, local_as='4321') + + @test.idempotent_id('9cc33701-51e5-421f-a5d5-fd7b330e550f') + def test_get_advertised_routes_tenant_networks(self): + addr_scope1 = self.create_address_scope('my-scope1', ip_version=4) + addr_scope2 = self.create_address_scope('my-scope2', ip_version=4) + ext_net = self.create_shared_network(**{'router:external': True}) + tenant_net1 = self.create_network() + tenant_net2 = self.create_network() + ext_subnetpool = self.create_subnetpool( + 'test-pool-ext', + is_admin=True, + default_prefixlen=24, + address_scope_id=addr_scope1['id'], + prefixes=['8.0.0.0/8']) + tenant_subnetpool1 = self.create_subnetpool( + 'tenant-test-pool', + default_prefixlen=25, + address_scope_id=addr_scope1['id'], + prefixes=['10.10.0.0/16']) + tenant_subnetpool2 = self.create_subnetpool( + 'tenant-test-pool', + default_prefixlen=25, + address_scope_id=addr_scope2['id'], + prefixes=['11.10.0.0/16']) + self.create_subnet({'id': ext_net['id']}, + cidr=netaddr.IPNetwork('8.0.0.0/24'), + ip_version=4, + client=self.admin_client, + subnetpool_id=ext_subnetpool['id']) + tenant_subnet1 = self.create_subnet( + {'id': tenant_net1['id']}, + cidr=netaddr.IPNetwork('10.10.0.0/24'), + ip_version=4, + subnetpool_id=tenant_subnetpool1['id']) + tenant_subnet2 = self.create_subnet( + {'id': tenant_net2['id']}, + cidr=netaddr.IPNetwork('11.10.0.0/24'), + ip_version=4, + subnetpool_id=tenant_subnetpool2['id']) + ext_gw_info = {'network_id': ext_net['id']} + router = self.admin_client.create_router( + 'my-router', + distributed=False, + external_gateway_info=ext_gw_info)['router'] + self.admin_routers.append(router) + self.admin_client.add_router_interface_with_subnet_id( + router['id'], + tenant_subnet1['id']) + self.admin_routerports.append({'router_id': router['id'], + 'subnet_id': tenant_subnet1['id']}) + self.admin_client.add_router_interface_with_subnet_id( + router['id'], + tenant_subnet2['id']) + self.admin_routerports.append({'router_id': router['id'], + 'subnet_id': tenant_subnet2['id']}) + bgp_speaker = self.create_bgp_speaker(**self.default_bgp_speaker_args) + bgp_speaker_id = bgp_speaker['bgp-speaker']['id'] + self.admin_client.add_bgp_gateway_network(bgp_speaker_id, + ext_net['id']) + routes = self.admin_client.get_bgp_advertised_routes(bgp_speaker_id) + self.assertEqual(1, len(routes['advertised_routes'])) + self.assertEqual(tenant_subnet1['cidr'], + routes['advertised_routes'][0]['destination']) + fixed_ip = router['external_gateway_info']['external_fixed_ips'][0] + self.assertEqual(fixed_ip['ip_address'], + routes['advertised_routes'][0]['next_hop']) diff --git a/neutron_dynamic_routing/tests/common/__init__.py b/neutron_dynamic_routing/tests/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/common/helpers.py b/neutron_dynamic_routing/tests/common/helpers.py new file mode 100644 index 00000000..1a8c9908 --- /dev/null +++ b/neutron_dynamic_routing/tests/common/helpers.py @@ -0,0 +1,42 @@ +# Copyright 2016 Hewlett Packard Development Co +# +# 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 import context +from neutron.tests.common import helpers + +from neutron_dynamic_routing.services.bgp.common import constants as bgp_const + + +def _get_bgp_dragent_dict(host): + agent = { + 'binary': 'neutron-bgp-dragent', + 'host': host, + 'topic': 'q-bgp_dragent', + 'agent_type': bgp_const.AGENT_TYPE_BGP_ROUTING, + 'configurations': {'bgp_speakers': 1}} + return agent + + +def register_bgp_dragent(host=helpers.HOST, admin_state_up=True, + alive=True): + agent = helpers._register_agent( + _get_bgp_dragent_dict(host)) + + if not admin_state_up: + helpers.set_agent_admin_state(agent['id']) + if not alive: + helpers.kill_agent(agent['id']) + + return helpers.FakePlugin()._get_agent_by_type_and_host( + context.get_admin_context(), agent['agent_type'], agent['host']) diff --git a/neutron_dynamic_routing/tests/functional/services/__init__.py b/neutron_dynamic_routing/tests/functional/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/functional/services/bgp/__init__.py b/neutron_dynamic_routing/tests/functional/services/bgp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/functional/services/bgp/scheduler/__init__.py b/neutron_dynamic_routing/tests/functional/services/bgp/scheduler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/functional/services/bgp/scheduler/test_bgp_dragent_scheduler.py b/neutron_dynamic_routing/tests/functional/services/bgp/scheduler/test_bgp_dragent_scheduler.py new file mode 100644 index 00000000..81f84e9e --- /dev/null +++ b/neutron_dynamic_routing/tests/functional/services/bgp/scheduler/test_bgp_dragent_scheduler.py @@ -0,0 +1,209 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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. + +import testscenarios + +from neutron import context +from neutron.db import agents_db +from neutron.db import common_db_mixin +from neutron.tests.unit import testlib_api + +from neutron_dynamic_routing.db import bgp_db +from neutron_dynamic_routing.db import bgp_dragentscheduler_db as bgp_dras_db +from neutron_dynamic_routing.services.bgp.scheduler import bgp_dragent_scheduler as bgp_dras # noqa +from neutron_dynamic_routing.tests.common import helpers + +# Required to generate tests from scenarios. Not compatible with nose. +load_tests = testscenarios.load_tests_apply_scenarios + + +class TestAutoSchedule(testlib_api.SqlTestCase, + bgp_dras_db.BgpDrAgentSchedulerDbMixin, + agents_db.AgentDbMixin, + common_db_mixin.CommonDbMixin): + """Test various scenarios for schedule_unscheduled_bgp_speakers. + + Below is the brief description of the scenario variables + -------------------------------------------------------- + host_count + number of hosts. + + agent_count + number of BGP dynamic routing agents. + + down_agent_count + number of DRAgents which are inactive. + + bgp_speaker_count + Number of bgp_speakers. + + hosted_bgp_speakers + A mapping of agent id to the ids of the bgp_speakers that they + should be initially hosting. + + expected_schedule_return_value + Expected return value of 'schedule_unscheduled_bgp_speakers'. + + expected_hosted_bgp_speakers + This stores the expected bgp_speakers that should have been + scheduled (or that could have already been scheduled) for each + agent after the 'schedule_unscheduled_bgp_speakers' function is + called. + """ + + scenarios = [ + ('No BgpDrAgent scheduled, if no DRAgent is present', + dict(host_count=1, + agent_count=0, + down_agent_count=0, + bgp_speaker_count=1, + hosted_bgp_speakers={}, + expected_schedule_return_value=False)), + + ('No BgpDrAgent scheduled, if no BGP speaker are present', + dict(host_count=1, + agent_count=1, + down_agent_count=0, + bgp_speaker_count=0, + hosted_bgp_speakers={}, + expected_schedule_return_value=False, + expected_hosted_bgp_speakers={'agent-0': []})), + + ('No BgpDrAgent scheduled, if BGP speaker already hosted', + dict(host_count=1, + agent_count=1, + down_agent_count=0, + bgp_speaker_count=1, + hosted_bgp_speakers={'agent-0': ['bgp-speaker-0']}, + expected_schedule_return_value=False, + expected_hosted_bgp_speakers={'agent-0': ['bgp-speaker-0']})), + + ('BgpDrAgent scheduled to the speaker, if the speaker is not hosted', + dict(host_count=1, + agent_count=1, + down_agent_count=0, + bgp_speaker_count=1, + hosted_bgp_speakers={}, + expected_schedule_return_value=True, + expected_hosted_bgp_speakers={'agent-0': ['bgp-speaker-0']})), + + ('No BgpDrAgent scheduled, if all the agents are down', + dict(host_count=2, + agent_count=2, + down_agent_count=2, + bgp_speaker_count=1, + hosted_bgp_speakers={}, + expected_schedule_return_value=False, + expected_hosted_bgp_speakers={'agent-0': [], + 'agent-1': [], })), + ] + + def _strip_host_index(self, name): + """Strips the host index. + + Eg. if name = '2-agent-3', then 'agent-3' is returned. + """ + return name[name.find('-') + 1:] + + def _extract_index(self, name): + """Extracts the index number and returns. + + Eg. if name = '2-agent-3', then 3 is returned + """ + return int(name.split('-')[-1]) + + def _get_hosted_bgp_speakers_on_dragent(self, agent_id): + query = self.ctx.session.query( + bgp_dras_db.BgpSpeakerDrAgentBinding.bgp_speaker_id) + query = query.filter( + bgp_dras_db.BgpSpeakerDrAgentBinding.agent_id == + agent_id) + + return [item[0] for item in query] + + def _create_and_set_agents_down(self, hosts, agent_count=0, + down_agent_count=0, admin_state_up=True): + agents = [] + if agent_count: + for i, host in enumerate(hosts): + is_alive = i >= down_agent_count + agents.append(helpers.register_bgp_dragent( + host, + admin_state_up=admin_state_up, + alive=is_alive)) + return agents + + def _save_bgp_speakers(self, bgp_speakers): + cls = bgp_db.BgpDbMixin() + bgp_speaker_body = { + 'bgp_speaker': {'name': 'fake_bgp_speaker', + 'ip_version': '4', + 'local_as': '123', + 'advertise_floating_ip_host_routes': '0', + 'advertise_tenant_networks': '0', + 'peers': [], + 'networks': []}} + i = 1 + for bgp_speaker_id in bgp_speakers: + bgp_speaker_body['bgp_speaker']['local_as'] = i + cls._save_bgp_speaker(self.ctx, bgp_speaker_body, + uuid=bgp_speaker_id) + i = i + 1 + + def _test_auto_schedule(self, host_index): + scheduler = bgp_dras.ChanceScheduler() + self.ctx = context.get_admin_context() + msg = 'host_index = %s' % host_index + + # create hosts + hosts = ['%s-agent-%s' % (host_index, i) + for i in range(self.host_count)] + bgp_dragents = self._create_and_set_agents_down(hosts, + self.agent_count, + self.down_agent_count) + + # create bgp_speakers + self._bgp_speakers = ['%s-bgp-speaker-%s' % (host_index, i) + for i in range(self.bgp_speaker_count)] + self._save_bgp_speakers(self._bgp_speakers) + + # pre schedule the bgp_speakers to the agents defined in + # self.hosted_bgp_speakers before calling auto_schedule_bgp_speaker + for agent, bgp_speakers in self.hosted_bgp_speakers.items(): + agent_index = self._extract_index(agent) + for bgp_speaker in bgp_speakers: + bs_index = self._extract_index(bgp_speaker) + scheduler.bind(self.ctx, [bgp_dragents[agent_index]], + self._bgp_speakers[bs_index]) + + retval = scheduler.schedule_unscheduled_bgp_speakers(self.ctx, + hosts[host_index]) + self.assertEqual(self.expected_schedule_return_value, retval, + message=msg) + + if self.agent_count: + agent_id = bgp_dragents[host_index].id + hosted_bgp_speakers = self._get_hosted_bgp_speakers_on_dragent( + agent_id) + hosted_bs_ids = [self._strip_host_index(net) + for net in hosted_bgp_speakers] + expected_hosted_bgp_speakers = self.expected_hosted_bgp_speakers[ + 'agent-%s' % host_index] + self.assertItemsEqual(hosted_bs_ids, expected_hosted_bgp_speakers, + msg) + + def test_auto_schedule(self): + for i in range(self.host_count): + self._test_auto_schedule(i) diff --git a/neutron_dynamic_routing/tests/unit/api/__init__.py b/neutron_dynamic_routing/tests/unit/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/unit/api/rpc/__init__.py b/neutron_dynamic_routing/tests/unit/api/rpc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/unit/api/rpc/agentnotifiers/__init__.py b/neutron_dynamic_routing/tests/unit/api/rpc/agentnotifiers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/unit/api/rpc/agentnotifiers/test_bgp_dr_rpc_agent_api.py b/neutron_dynamic_routing/tests/unit/api/rpc/agentnotifiers/test_bgp_dr_rpc_agent_api.py new file mode 100644 index 00000000..af93d964 --- /dev/null +++ b/neutron_dynamic_routing/tests/unit/api/rpc/agentnotifiers/test_bgp_dr_rpc_agent_api.py @@ -0,0 +1,84 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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 mock + +from neutron import context +from neutron.tests import base + +from neutron_dynamic_routing.api.rpc.agentnotifiers import bgp_dr_rpc_agent_api + + +class TestBgpDrAgentNotifyApi(base.BaseTestCase): + + def setUp(self): + super(TestBgpDrAgentNotifyApi, self).setUp() + self.notifier = ( + bgp_dr_rpc_agent_api.BgpDrAgentNotifyApi()) + + mock_cast_p = mock.patch.object(self.notifier, + '_notification_host_cast') + self.mock_cast = mock_cast_p.start() + mock_call_p = mock.patch.object(self.notifier, + '_notification_host_call') + self.mock_call = mock_call_p.start() + self.context = context.get_admin_context() + self.host = 'host-1' + + def test_notify_dragent_bgp_routes_advertisement(self): + bgp_speaker_id = 'bgp-speaker-1' + routes = [{'destination': '1.1.1.1', 'next_hop': '2.2.2.2'}] + self.notifier.bgp_routes_advertisement(self.context, bgp_speaker_id, + routes, self.host) + self.assertEqual(1, self.mock_cast.call_count) + self.assertEqual(0, self.mock_call.call_count) + + def test_notify_dragent_bgp_routes_withdrawal(self): + bgp_speaker_id = 'bgp-speaker-1' + routes = [{'destination': '1.1.1.1'}] + self.notifier.bgp_routes_withdrawal(self.context, bgp_speaker_id, + routes, self.host) + self.assertEqual(1, self.mock_cast.call_count) + self.assertEqual(0, self.mock_call.call_count) + + def test_notify_bgp_peer_disassociated(self): + bgp_speaker_id = 'bgp-speaker-1' + bgp_peer_ip = '1.1.1.1' + self.notifier.bgp_peer_disassociated(self.context, bgp_speaker_id, + bgp_peer_ip, self.host) + self.assertEqual(1, self.mock_cast.call_count) + self.assertEqual(0, self.mock_call.call_count) + + def test_notify_bgp_peer_associated(self): + bgp_speaker_id = 'bgp-speaker-1' + bgp_peer_id = 'bgp-peer-1' + self.notifier.bgp_peer_associated(self.context, bgp_speaker_id, + bgp_peer_id, self.host) + self.assertEqual(1, self.mock_cast.call_count) + self.assertEqual(0, self.mock_call.call_count) + + def test_notify_bgp_speaker_created(self): + bgp_speaker_id = 'bgp-speaker-1' + self.notifier.bgp_speaker_created(self.context, bgp_speaker_id, + self.host) + self.assertEqual(1, self.mock_cast.call_count) + self.assertEqual(0, self.mock_call.call_count) + + def test_notify_bgp_speaker_removed(self): + bgp_speaker_id = 'bgp-speaker-1' + self.notifier.bgp_speaker_removed(self.context, bgp_speaker_id, + self.host) + self.assertEqual(1, self.mock_cast.call_count) + self.assertEqual(0, self.mock_call.call_count) diff --git a/neutron_dynamic_routing/tests/unit/api/rpc/handlers/__init__.py b/neutron_dynamic_routing/tests/unit/api/rpc/handlers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/unit/api/rpc/handlers/test_bgp_speaker_rpc.py b/neutron_dynamic_routing/tests/unit/api/rpc/handlers/test_bgp_speaker_rpc.py new file mode 100644 index 00000000..d72e89f8 --- /dev/null +++ b/neutron_dynamic_routing/tests/unit/api/rpc/handlers/test_bgp_speaker_rpc.py @@ -0,0 +1,45 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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 mock + +from neutron.tests import base + +from neutron_dynamic_routing.api.rpc.handlers import bgp_speaker_rpc + + +class TestBgpSpeakerRpcCallback(base.BaseTestCase): + + def setUp(self): + self.plugin_p = mock.patch('neutron.manager.NeutronManager.' + 'get_service_plugins') + self.plugin = self.plugin_p.start() + self.callback = bgp_speaker_rpc.BgpSpeakerRpcCallback() + super(TestBgpSpeakerRpcCallback, self).setUp() + + def test_get_bgp_speaker_info(self): + self.callback.get_bgp_speaker_info(mock.Mock(), + bgp_speaker_id='id1') + self.assertIsNotNone(len(self.plugin.mock_calls)) + + def test_get_bgp_peer_info(self): + self.callback.get_bgp_peer_info(mock.Mock(), + bgp_peer_id='id1') + self.assertIsNotNone(len(self.plugin.mock_calls)) + + def test_get_bgp_speakers(self): + self.callback.get_bgp_speakers(mock.Mock(), + host='host') + self.assertIsNotNone(len(self.plugin.mock_calls)) diff --git a/neutron_dynamic_routing/tests/unit/db/__init__.py b/neutron_dynamic_routing/tests/unit/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/unit/db/test_bgp_db.py b/neutron_dynamic_routing/tests/unit/db/test_bgp_db.py new file mode 100644 index 00000000..910ecad8 --- /dev/null +++ b/neutron_dynamic_routing/tests/unit/db/test_bgp_db.py @@ -0,0 +1,1047 @@ +# Copyright 2016 Hewlett Packard Enterprise Development Company LP +# +# 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 contextlib +import netaddr + +from neutron_lib import constants as n_const +from neutron_lib import exceptions as n_exc +from oslo_utils import uuidutils + +from neutron.extensions import external_net +from neutron.extensions import portbindings +from neutron import manager +from neutron.plugins.common import constants as p_const +from neutron.tests.unit.plugins.ml2 import test_plugin + +from neutron_dynamic_routing.extensions import bgp +from neutron_dynamic_routing.services.bgp import bgp_plugin + +_uuid = uuidutils.generate_uuid + +ADVERTISE_FIPS_KEY = 'advertise_floating_ip_host_routes' + + +class BgpEntityCreationMixin(object): + + @contextlib.contextmanager + def bgp_speaker(self, ip_version, local_as, name='my-speaker', + advertise_fip_host_routes=True, + advertise_tenant_networks=True, + networks=None, peers=None): + data = {'ip_version': ip_version, + ADVERTISE_FIPS_KEY: advertise_fip_host_routes, + 'advertise_tenant_networks': advertise_tenant_networks, + 'local_as': local_as, 'name': name} + bgp_speaker = self.bgp_plugin.create_bgp_speaker(self.context, + {'bgp_speaker': data}) + bgp_speaker_id = bgp_speaker['id'] + + if networks: + for network_id in networks: + self.bgp_plugin.add_gateway_network( + self.context, + bgp_speaker_id, + {'network_id': network_id}) + if peers: + for peer_id in peers: + self.bgp_plugin.add_bgp_peer(self.context, bgp_speaker_id, + {'bgp_peer_id': peer_id}) + + yield self.bgp_plugin.get_bgp_speaker(self.context, bgp_speaker_id) + + @contextlib.contextmanager + def bgp_peer(self, tenant_id=_uuid(), remote_as='4321', + peer_ip="192.168.1.1", auth_type="md5", + password="my-secret", name="my-peer"): + data = {'peer_ip': peer_ip, 'tenant_id': tenant_id, + 'remote_as': remote_as, 'auth_type': auth_type, + 'password': password, 'name': name} + bgp_peer = self.bgp_plugin.create_bgp_peer(self.context, + {'bgp_peer': data}) + yield bgp_peer + self.bgp_plugin.delete_bgp_peer(self.context, bgp_peer['id']) + + @contextlib.contextmanager + def bgp_speaker_with_gateway_network(self, address_scope_id, local_as, + advertise_fip_host_routes=True, + advertise_tenant_networks=True, + network_external=True, + fmt=None, set_context=False): + pass + + @contextlib.contextmanager + def bgp_speaker_with_router(self, address_scope_id, local_as, + gw_network_id=None, gw_subnet_ids=None, + tenant_subnet_ids=None, + advertise_fip_host_routes=True, + advertise_tenant_networks=True, + fmt=None, set_context=False, + router_distributed=False): + pass + + @contextlib.contextmanager + def router(self, name='bgp-test-router', tenant_id=_uuid(), + admin_state_up=True, **kwargs): + request = {'router': {'tenant_id': tenant_id, + 'name': name, + 'admin_state_up': admin_state_up}} + for arg in kwargs: + request['router'][arg] = kwargs[arg] + router = self.l3plugin.create_router(self.context, request) + yield router + + @contextlib.contextmanager + def router_with_external_and_tenant_networks( + self, + tenant_id=_uuid(), + gw_prefix='8.8.8.0/24', + tenant_prefix='192.168.0.0/16', + address_scope=None, + distributed=False): + prefixes = [gw_prefix, tenant_prefix] + gw_ip_net = netaddr.IPNetwork(gw_prefix) + tenant_ip_net = netaddr.IPNetwork(tenant_prefix) + subnetpool_args = {'tenant_id': tenant_id, + 'name': 'bgp-pool'} + if address_scope: + subnetpool_args['address_scope_id'] = address_scope['id'] + + with self.network() as ext_net, self.network() as int_net,\ + self.subnetpool(prefixes, **subnetpool_args) as pool: + subnetpool_id = pool['subnetpool']['id'] + gw_net_id = ext_net['network']['id'] + with self.subnet(ext_net, + cidr=gw_prefix, + subnetpool_id=subnetpool_id, + ip_version=gw_ip_net.version),\ + self.subnet(int_net, + cidr=tenant_prefix, + subnetpool_id=subnetpool_id, + ip_version=tenant_ip_net.version) as int_subnet: + self._update('networks', gw_net_id, + {'network': {external_net.EXTERNAL: True}}) + ext_gw_info = {'network_id': gw_net_id} + with self.router(external_gateway_info=ext_gw_info, + distributed=distributed) as router: + router_id = router['id'] + router_interface_info = {'subnet_id': + int_subnet['subnet']['id']} + self.l3plugin.add_router_interface(self.context, + router_id, + router_interface_info) + yield router, ext_net, int_net + + +class BgpTests(test_plugin.Ml2PluginV2TestCase, + BgpEntityCreationMixin): + fmt = 'json' + + def setup_parent(self): + self.l3_plugin = ('neutron.tests.unit.extensions.test_l3.' + 'TestL3NatAgentSchedulingServicePlugin') + super(BgpTests, self).setup_parent() + + def setUp(self): + super(BgpTests, self).setUp() + self.l3plugin = manager.NeutronManager.get_service_plugins().get( + p_const.L3_ROUTER_NAT) + self.bgp_plugin = bgp_plugin.BgpPlugin() + self.plugin = manager.NeutronManager.get_plugin() + self.l3plugin = manager.NeutronManager.get_service_plugins().get( + p_const.L3_ROUTER_NAT) + + @contextlib.contextmanager + def subnetpool_with_address_scope(self, ip_version, prefixes=None, + shared=False, admin=True, + name='test-pool', is_default_pool=False, + tenant_id=None, **kwargs): + if not tenant_id: + tenant_id = _uuid() + + scope_data = {'tenant_id': tenant_id, 'ip_version': ip_version, + 'shared': shared, 'name': name + '-scope'} + address_scope = self.plugin.create_address_scope( + self.context, + {'address_scope': scope_data}) + address_scope_id = address_scope['id'] + pool_data = {'tenant_id': tenant_id, 'shared': shared, 'name': name, + 'address_scope_id': address_scope_id, + 'prefixes': prefixes, 'is_default': is_default_pool} + for key in kwargs: + pool_data[key] = kwargs[key] + + yield self.plugin.create_subnetpool(self.context, + {'subnetpool': pool_data}) + + @contextlib.contextmanager + def floatingip_from_address_scope_assoc(self, prefixes, + address_scope_id, + ext_prefixlen=24, + int_prefixlen=24): + pass + + def test_add_duplicate_bgp_peer_ip(self): + peer_ip = '192.168.1.10' + with self.bgp_peer(peer_ip=peer_ip) as peer1,\ + self.bgp_peer(peer_ip=peer_ip) as peer2,\ + self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + + with self.bgp_speaker(sp['ip_version'], 1234, + peers=[peer1['id']]) as speaker: + self.assertRaises(bgp.DuplicateBgpPeerIpException, + self.bgp_plugin.add_bgp_peer, + self.context, speaker['id'], + {'bgp_peer_id': peer2['id']}) + + def test_bgpspeaker_create(self): + with self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + speaker_name = 'test-speaker' + expected_values = [('ip_version', sp['ip_version']), + ('name', speaker_name)] + with self.bgp_speaker(sp['ip_version'], 1234, + name=speaker_name) as bgp_speaker: + for k, v in expected_values: + self.assertEqual(v, bgp_speaker[k]) + + def test_bgp_speaker_list(self): + with self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp1,\ + self.subnetpool_with_address_scope(4, + prefixes=['9.0.0.0/8']) as sp2: + with self.bgp_speaker(sp1['ip_version'], 1234, + name='speaker1'),\ + self.bgp_speaker(sp2['ip_version'], 4321, + name='speaker2'): + speakers = self.bgp_plugin.get_bgp_speakers(self.context) + self.assertEqual(2, len(speakers)) + + def test_bgp_speaker_update_local_as(self): + local_as_1 = 1234 + local_as_2 = 4321 + with self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + with self.bgp_speaker(sp['ip_version'], local_as_1) as speaker: + self.assertEqual(local_as_1, speaker['local_as']) + new_speaker = self.bgp_plugin.update_bgp_speaker( + self.context, + speaker['id'], + {'bgp_speaker': + {'local_as': local_as_2}}) + self.assertEqual(local_as_2, new_speaker['local_as']) + + def test_bgp_speaker_show_non_existent(self): + self.assertRaises(bgp.BgpSpeakerNotFound, + self.bgp_plugin.get_bgp_speaker, + self.context, _uuid()) + + def test_create_bgp_peer(self): + args = {'tenant_id': _uuid(), + 'remote_as': '1111', + 'peer_ip': '10.10.10.10', + 'auth_type': 'md5'} + with self.bgp_peer(tenant_id=args['tenant_id'], + remote_as=args['remote_as'], + peer_ip=args['peer_ip'], + auth_type='md5', + password='my-secret') as peer: + self.assertIsNone(peer.get('password')) + for key in args: + self.assertEqual(args[key], peer[key]) + + def test_bgp_peer_show_non_existent(self): + self.assertRaises(bgp.BgpPeerNotFound, + self.bgp_plugin.get_bgp_peer, + self.context, + 'unreal-bgp-peer-id') + + def test_associate_bgp_peer(self): + with self.bgp_peer() as peer,\ + self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + with self.bgp_speaker(sp['ip_version'], 1234) as speaker: + self.bgp_plugin.add_bgp_peer(self.context, speaker['id'], + {'bgp_peer_id': peer['id']}) + new_speaker = self.bgp_plugin.get_bgp_speaker(self.context, + speaker['id']) + self.assertIn('peers', new_speaker) + self.assertIn(peer['id'], new_speaker['peers']) + self.assertEqual(1, len(new_speaker['peers'])) + + def test_remove_bgp_peer(self): + with self.bgp_peer() as peer,\ + self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + with self.bgp_speaker(sp['ip_version'], 1234, + peers=[peer['id']]) as speaker: + self.bgp_plugin.remove_bgp_peer(self.context, speaker['id'], + {'bgp_peer_id': peer['id']}) + new_speaker = self.bgp_plugin.get_bgp_speaker(self.context, + speaker['id']) + self.assertIn('peers', new_speaker) + self.assertTrue(not new_speaker['peers']) + + def test_remove_unassociated_bgp_peer(self): + with self.bgp_peer() as peer,\ + self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + with self.bgp_speaker(sp['ip_version'], 1234) as speaker: + self.assertRaises(bgp.BgpSpeakerPeerNotAssociated, + self.bgp_plugin.remove_bgp_peer, + self.context, + speaker['id'], + {'bgp_peer_id': peer['id']}) + + def test_remove_non_existent_bgp_peer(self): + bgp_peer_id = "imaginary" + with self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + with self.bgp_speaker(sp['ip_version'], 1234) as speaker: + self.assertRaises(bgp.BgpSpeakerPeerNotAssociated, + self.bgp_plugin.remove_bgp_peer, + self.context, + speaker['id'], + {'bgp_peer_id': bgp_peer_id}) + + def test_add_non_existent_bgp_peer(self): + bgp_peer_id = "imaginary" + with self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + with self.bgp_speaker(sp['ip_version'], 1234) as speaker: + self.assertRaises(bgp.BgpPeerNotFound, + self.bgp_plugin.add_bgp_peer, + self.context, + speaker['id'], + {'bgp_peer_id': bgp_peer_id}) + + def test_add_gateway_network(self): + with self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + with self.bgp_speaker(sp['ip_version'], 1234) as speaker,\ + self.network() as network: + network_id = network['network']['id'] + self.bgp_plugin.add_gateway_network(self.context, + speaker['id'], + {'network_id': network_id}) + new_speaker = self.bgp_plugin.get_bgp_speaker(self.context, + speaker['id']) + self.assertEqual(1, len(new_speaker['networks'])) + self.assertTrue(network_id in new_speaker['networks']) + + def test_create_bgp_speaker_with_network(self): + with self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + network = self.plugin.create_network(self.context, + {'network': + {'name': 'test-net', + 'tenant_id': _uuid(), + 'admin_state_up': True, + 'shared': True}}) + with self.bgp_speaker(sp['ip_version'], 1234, + networks=[network['id']]) as speaker: + self.assertEqual(1, len(speaker['networks'])) + self.assertTrue(network['id'] in speaker['networks']) + + def test_remove_gateway_network(self): + with self.network() as network,\ + self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + network_id = network['network']['id'] + with self.bgp_speaker(sp['ip_version'], 1234, + networks=[network_id]) as speaker: + self.bgp_plugin.remove_gateway_network( + self.context, + speaker['id'], + {'network_id': network_id}) + new_speaker = self.bgp_plugin.get_bgp_speaker(self.context, + speaker['id']) + self.assertEqual(0, len(new_speaker['networks'])) + + def test_add_non_existent_gateway_network(self): + network_id = "imaginary" + with self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + with self.bgp_speaker(sp['ip_version'], 1234) as speaker: + self.assertRaises(n_exc.NetworkNotFound, + self.bgp_plugin.add_gateway_network, + self.context, speaker['id'], + {'network_id': network_id}) + + def test_remove_non_existent_gateway_network(self): + network_id = "imaginary" + with self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + with self.bgp_speaker(sp['ip_version'], 1234) as speaker: + self.assertRaises(bgp.BgpSpeakerNetworkNotAssociated, + self.bgp_plugin.remove_gateway_network, + self.context, speaker['id'], + {'network_id': network_id}) + + def test_add_gateway_network_two_bgp_speakers_same_scope(self): + with self.subnetpool_with_address_scope(4, + prefixes=['8.0.0.0/8']) as sp: + with self.bgp_speaker(sp['ip_version'], 1234) as speaker1,\ + self.bgp_speaker(sp['ip_version'], 4321) as speaker2,\ + self.network() as network: + network_id = network['network']['id'] + self.bgp_plugin.add_gateway_network(self.context, + speaker1['id'], + {'network_id': network_id}) + self.bgp_plugin.add_gateway_network(self.context, + speaker2['id'], + {'network_id': network_id}) + speaker1 = self.bgp_plugin.get_bgp_speaker(self.context, + speaker1['id']) + speaker2 = self.bgp_plugin.get_bgp_speaker(self.context, + speaker2['id']) + for speaker in [speaker1, speaker2]: + self.assertEqual(1, len(speaker['networks'])) + self.assertEqual(network_id, + speaker['networks'][0]) + + def test_create_bgp_peer_md5_auth_no_password(self): + bgp_peer = {'bgp_peer': {'auth_type': 'md5', 'password': None}} + self.assertRaises(bgp.InvalidBgpPeerMd5Authentication, + self.bgp_plugin.create_bgp_peer, + self.context, bgp_peer) + + def test__get_address_scope_ids_for_bgp_speaker(self): + prefixes1 = ['8.0.0.0/8'] + prefixes2 = ['9.0.0.0/8'] + prefixes3 = ['10.0.0.0/8'] + tenant_id = _uuid() + with self.bgp_speaker(4, 1234) as speaker,\ + self.subnetpool_with_address_scope(4, + prefixes=prefixes1, + tenant_id=tenant_id) as sp1,\ + self.subnetpool_with_address_scope(4, + prefixes=prefixes2, + tenant_id=tenant_id) as sp2,\ + self.subnetpool_with_address_scope(4, + prefixes=prefixes3, + tenant_id=tenant_id) as sp3,\ + self.network() as network1, self.network() as network2,\ + self.network() as network3: + network1_id = network1['network']['id'] + network2_id = network2['network']['id'] + network3_id = network3['network']['id'] + base_subnet_data = { + 'allocation_pools': n_const.ATTR_NOT_SPECIFIED, + 'cidr': n_const.ATTR_NOT_SPECIFIED, + 'prefixlen': n_const.ATTR_NOT_SPECIFIED, + 'ip_version': 4, + 'enable_dhcp': True, + 'dns_nameservers': n_const.ATTR_NOT_SPECIFIED, + 'host_routes': n_const.ATTR_NOT_SPECIFIED} + subnet1_data = {'network_id': network1_id, + 'subnetpool_id': sp1['id'], + 'name': 'subnet1', + 'tenant_id': tenant_id} + subnet2_data = {'network_id': network2_id, + 'subnetpool_id': sp2['id'], + 'name': 'subnet2', + 'tenant_id': tenant_id} + subnet3_data = {'network_id': network3_id, + 'subnetpool_id': sp3['id'], + 'name': 'subnet2', + 'tenant_id': tenant_id} + for k in base_subnet_data: + subnet1_data[k] = base_subnet_data[k] + subnet2_data[k] = base_subnet_data[k] + subnet3_data[k] = base_subnet_data[k] + + self.plugin.create_subnet(self.context, {'subnet': subnet1_data}) + self.plugin.create_subnet(self.context, {'subnet': subnet2_data}) + self.plugin.create_subnet(self.context, {'subnet': subnet3_data}) + self.bgp_plugin.add_gateway_network(self.context, speaker['id'], + {'network_id': network1_id}) + self.bgp_plugin.add_gateway_network(self.context, speaker['id'], + {'network_id': network2_id}) + scopes = self.bgp_plugin._get_address_scope_ids_for_bgp_speaker( + self.context, + speaker['id']) + self.assertEqual(2, len(scopes)) + self.assertTrue(sp1['address_scope_id'] in scopes) + self.assertTrue(sp2['address_scope_id'] in scopes) + + def test_get_routes_by_bgp_speaker_binding(self): + gw_prefix = '172.16.10.0/24' + tenant_prefix = '10.10.10.0/24' + 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, + gw_prefix=gw_prefix, + tenant_prefix=tenant_prefix, + address_scope=scope) as res: + router, ext_net, int_net = res + ext_gw_info = router['external_gateway_info'] + gw_net_id = ext_net['network']['id'] + with self.bgp_speaker(4, 1234, + networks=[gw_net_id]) as speaker: + bgp_speaker_id = speaker['id'] + routes = self.bgp_plugin.get_routes_by_bgp_speaker_binding( + self.context, + bgp_speaker_id, + gw_net_id) + routes = list(routes) + next_hop = ext_gw_info['external_fixed_ips'][0]['ip_address'] + self.assertEqual(1, len(routes)) + self.assertEqual(tenant_prefix, routes[0]['destination']) + self.assertEqual(next_hop, routes[0]['next_hop']) + + def test_get_routes_by_binding_network(self): + gw_prefix = '172.16.10.0/24' + tenant_prefix = '10.10.10.0/24' + 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, + gw_prefix=gw_prefix, + tenant_prefix=tenant_prefix, + address_scope=scope) as res: + router, ext_net, int_net = res + ext_gw_info = router['external_gateway_info'] + gw_net_id = ext_net['network']['id'] + with self.bgp_speaker(4, 1234, networks=[gw_net_id]) as speaker: + bgp_speaker_id = speaker['id'] + routes = self.bgp_plugin.get_routes_by_bgp_speaker_binding( + self.context, + bgp_speaker_id, + gw_net_id) + routes = list(routes) + next_hop = ext_gw_info['external_fixed_ips'][0]['ip_address'] + self.assertEqual(1, len(routes)) + self.assertEqual(tenant_prefix, routes[0]['destination']) + self.assertEqual(next_hop, routes[0]['next_hop']) + + def _advertised_routes_by_bgp_speaker(self, + bgp_speaker_ip_version, + local_as, + tenant_cidr, + gateway_cidr, + fip_routes=True, + router_distributed=False): + tenant_id = _uuid() + scope_data = {'tenant_id': tenant_id, + 'ip_version': bgp_speaker_ip_version, + '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, + gw_prefix=gateway_cidr, + tenant_prefix=tenant_cidr, + address_scope=scope, + distributed=router_distributed) as res: + router, ext_net, int_net = res + gw_net_id = ext_net['network']['id'] + with self.bgp_speaker( + bgp_speaker_ip_version, + local_as, + networks=[gw_net_id], + advertise_fip_host_routes=fip_routes) as speaker: + routes = self.bgp_plugin.get_advertised_routes( + self.context, + speaker['id']) + return routes['advertised_routes'] + + def test__tenant_prefixes_by_router_no_gateway_port(self): + with self.network() as net1, self.network() as net2,\ + self.subnetpool_with_address_scope(6, tenant_id='test-tenant', + prefixes=['2001:db8::/63']) as pool: + subnetpool_id = pool['id'] + with self.subnet(network=net1, + cidr=None, + subnetpool_id=subnetpool_id, + ip_version=6) as ext_subnet,\ + self.subnet(network=net2, + cidr=None, + subnetpool_id=subnetpool_id, + ip_version=6) as int_subnet,\ + self.router() as router: + + router_id = router['id'] + int_subnet_id = int_subnet['subnet']['id'] + ext_subnet_id = ext_subnet['subnet']['id'] + self.l3plugin.add_router_interface(self.context, + router_id, + {'subnet_id': + int_subnet_id}) + self.l3plugin.add_router_interface(self.context, + router_id, + {'subnet_id': + ext_subnet_id}) + with self.bgp_speaker(6, 1234) as speaker: + bgp_speaker_id = speaker['id'] + cidrs = list(self.bgp_plugin._tenant_prefixes_by_router( + self.context, + router_id, + bgp_speaker_id)) + self.assertFalse(cidrs) + + def test_get_ipv6_tenant_subnet_routes_by_bgp_speaker_ipv6(self): + tenant_cidr = '2001:db8::/64' + binding_cidr = '2001:ab8::/64' + routes = self._advertised_routes_by_bgp_speaker(6, 1234, tenant_cidr, + binding_cidr) + self.assertEqual(1, len(routes)) + dest_prefix = routes[0]['destination'] + next_hop = routes[0]['next_hop'] + self.assertEqual(tenant_cidr, dest_prefix) + self.assertTrue(netaddr.IPSet([binding_cidr]).__contains__(next_hop)) + + def test_get_ipv4_tenant_subnet_routes_by_bgp_speaker_ipv4(self): + tenant_cidr = '172.16.10.0/24' + binding_cidr = '20.10.1.0/24' + routes = self._advertised_routes_by_bgp_speaker(4, 1234, tenant_cidr, + binding_cidr) + routes = list(routes) + self.assertEqual(1, len(routes)) + dest_prefix = routes[0]['destination'] + next_hop = routes[0]['next_hop'] + self.assertEqual(tenant_cidr, dest_prefix) + self.assertTrue(netaddr.IPSet([binding_cidr]).__contains__(next_hop)) + + def test_get_ipv4_tenant_subnet_routes_by_bgp_speaker_dvr_router(self): + tenant_cidr = '172.16.10.0/24' + binding_cidr = '20.10.1.0/24' + routes = self._advertised_routes_by_bgp_speaker( + 4, + 1234, + tenant_cidr, + binding_cidr, + router_distributed=True) + routes = list(routes) + self.assertEqual(1, len(routes)) + + def test_all_routes_by_bgp_speaker_different_tenant_address_scope(self): + binding_cidr = '2001:db8::/64' + tenant_cidr = '2002:ab8::/64' + with self.subnetpool_with_address_scope(6, tenant_id='test-tenant', + prefixes=[binding_cidr]) as ext_pool,\ + self.subnetpool_with_address_scope(6, tenant_id='test-tenant', + prefixes=[tenant_cidr]) as int_pool,\ + self.network() as ext_net, self.network() as int_net: + gw_net_id = ext_net['network']['id'] + ext_pool_id = ext_pool['id'] + int_pool_id = int_pool['id'] + self._update('networks', gw_net_id, + {'network': {external_net.EXTERNAL: True}}) + with self.subnet(cidr=None, + subnetpool_id=ext_pool_id, + network=ext_net, + ip_version=6) as ext_subnet,\ + self.subnet(cidr=None, + subnetpool_id=int_pool_id, + network=int_net, + ip_version=6) as int_subnet,\ + self.router() as router: + router_id = router['id'] + int_subnet_id = int_subnet['subnet']['id'] + ext_subnet_id = ext_subnet['subnet']['id'] + self.l3plugin.add_router_interface(self.context, + router_id, + {'subnet_id': + int_subnet_id}) + self.l3plugin.add_router_interface(self.context, + router_id, + {'subnet_id': + ext_subnet_id}) + with self.bgp_speaker(6, 1234, + networks=[gw_net_id]) as speaker: + bgp_speaker_id = speaker['id'] + cidrs = self.bgp_plugin.get_routes_by_bgp_speaker_id( + self.context, + bgp_speaker_id) + self.assertEqual(0, len(list(cidrs))) + + def test__get_routes_by_router_with_fip(self): + gw_prefix = '172.16.10.0/24' + tenant_prefix = '10.10.10.0/24' + 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, + gw_prefix=gw_prefix, + tenant_prefix=tenant_prefix, + address_scope=scope) as res: + router, ext_net, int_net = res + ext_gw_info = router['external_gateway_info'] + gw_net_id = ext_net['network']['id'] + tenant_net_id = int_net['network']['id'] + fixed_port_data = {'port': + {'name': 'test', + 'network_id': tenant_net_id, + 'tenant_id': tenant_id, + 'admin_state_up': True, + 'device_id': _uuid(), + 'device_owner': 'compute:nova', + 'mac_address': n_const.ATTR_NOT_SPECIFIED, + 'fixed_ips': n_const.ATTR_NOT_SPECIFIED}} + fixed_port = self.plugin.create_port(self.context, + fixed_port_data) + fip_data = {'floatingip': {'floating_network_id': gw_net_id, + 'tenant_id': tenant_id, + 'port_id': fixed_port['id']}} + fip = self.l3plugin.create_floatingip(self.context, fip_data) + fip_prefix = fip['floating_ip_address'] + '/32' + with self.bgp_speaker(4, 1234, networks=[gw_net_id]) as speaker: + bgp_speaker_id = speaker['id'] + routes = self.bgp_plugin._get_routes_by_router(self.context, + router['id']) + routes = routes[bgp_speaker_id] + next_hop = ext_gw_info['external_fixed_ips'][0]['ip_address'] + self.assertEqual(2, len(routes)) + tenant_prefix_found = False + fip_prefix_found = False + for route in routes: + self.assertEqual(next_hop, route['next_hop']) + if route['destination'] == tenant_prefix: + tenant_prefix_found = True + if route['destination'] == fip_prefix: + fip_prefix_found = True + self.assertTrue(tenant_prefix_found) + self.assertTrue(fip_prefix_found) + + def test_get_routes_by_bgp_speaker_id_with_fip(self): + gw_prefix = '172.16.10.0/24' + tenant_prefix = '10.10.10.0/24' + 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, + gw_prefix=gw_prefix, + tenant_prefix=tenant_prefix, + address_scope=scope) as res: + router, ext_net, int_net = res + ext_gw_info = router['external_gateway_info'] + gw_net_id = ext_net['network']['id'] + tenant_net_id = int_net['network']['id'] + fixed_port_data = {'port': + {'name': 'test', + 'network_id': tenant_net_id, + 'tenant_id': tenant_id, + 'admin_state_up': True, + 'device_id': _uuid(), + 'device_owner': 'compute:nova', + 'mac_address': n_const.ATTR_NOT_SPECIFIED, + 'fixed_ips': n_const.ATTR_NOT_SPECIFIED}} + fixed_port = self.plugin.create_port(self.context, + fixed_port_data) + fip_data = {'floatingip': {'floating_network_id': gw_net_id, + 'tenant_id': tenant_id, + 'port_id': fixed_port['id']}} + fip = self.l3plugin.create_floatingip(self.context, fip_data) + fip_prefix = fip['floating_ip_address'] + '/32' + with self.bgp_speaker(4, 1234, networks=[gw_net_id]) as speaker: + bgp_speaker_id = speaker['id'] + routes = self.bgp_plugin.get_routes_by_bgp_speaker_id( + self.context, + bgp_speaker_id) + routes = list(routes) + next_hop = ext_gw_info['external_fixed_ips'][0]['ip_address'] + self.assertEqual(2, len(routes)) + tenant_prefix_found = False + fip_prefix_found = False + for route in routes: + self.assertEqual(next_hop, route['next_hop']) + if route['destination'] == tenant_prefix: + tenant_prefix_found = True + if route['destination'] == fip_prefix: + fip_prefix_found = True + self.assertTrue(tenant_prefix_found) + self.assertTrue(fip_prefix_found) + + def test_get_routes_by_bgp_speaker_id_with_fip_dvr(self): + gw_prefix = '172.16.10.0/24' + tenant_prefix = '10.10.10.0/24' + 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, + gw_prefix=gw_prefix, + tenant_prefix=tenant_prefix, + address_scope=scope, + distributed=True) as res: + router, ext_net, int_net = res + ext_gw_info = router['external_gateway_info'] + gw_net_id = ext_net['network']['id'] + tenant_net_id = int_net['network']['id'] + fixed_port_data = {'port': + {'name': 'test', + 'network_id': tenant_net_id, + 'tenant_id': tenant_id, + 'admin_state_up': True, + 'device_id': _uuid(), + 'device_owner': 'compute:nova', + 'mac_address': n_const.ATTR_NOT_SPECIFIED, + 'fixed_ips': n_const.ATTR_NOT_SPECIFIED, + portbindings.HOST_ID: 'test-host'}} + fixed_port = self.plugin.create_port(self.context, + fixed_port_data) + self.plugin._create_or_update_agent(self.context, + {'agent_type': 'L3 agent', + 'host': 'test-host', + 'binary': 'neutron-l3-agent', + 'topic': 'test'}) + fip_gw = self.l3plugin.create_fip_agent_gw_port_if_not_exists( + self.context, + gw_net_id, + 'test-host') + fip_data = {'floatingip': {'floating_network_id': gw_net_id, + 'tenant_id': tenant_id, + 'port_id': fixed_port['id']}} + fip = self.l3plugin.create_floatingip(self.context, fip_data) + fip_prefix = fip['floating_ip_address'] + '/32' + with self.bgp_speaker(4, 1234, networks=[gw_net_id]) as speaker: + bgp_speaker_id = speaker['id'] + routes = self.bgp_plugin.get_routes_by_bgp_speaker_id( + self.context, + bgp_speaker_id) + routes = list(routes) + cvr_gw_ip = ext_gw_info['external_fixed_ips'][0]['ip_address'] + dvr_gw_ip = fip_gw['fixed_ips'][0]['ip_address'] + self.assertEqual(2, len(routes)) + tenant_route_verified = False + fip_route_verified = False + for route in routes: + if route['destination'] == tenant_prefix: + self.assertEqual(cvr_gw_ip, route['next_hop']) + tenant_route_verified = True + if route['destination'] == fip_prefix: + self.assertEqual(dvr_gw_ip, route['next_hop']) + fip_route_verified = True + self.assertTrue(tenant_route_verified) + self.assertTrue(fip_route_verified) + + def test__get_dvr_fip_host_routes_by_binding(self): + gw_prefix = '172.16.10.0/24' + tenant_prefix = '10.10.10.0/24' + 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, + gw_prefix=gw_prefix, + tenant_prefix=tenant_prefix, + address_scope=scope, + distributed=True) as res: + router, ext_net, int_net = res + gw_net_id = ext_net['network']['id'] + tenant_net_id = int_net['network']['id'] + fixed_port_data = {'port': + {'name': 'test', + 'network_id': tenant_net_id, + 'tenant_id': tenant_id, + 'admin_state_up': True, + 'device_id': _uuid(), + 'device_owner': 'compute:nova', + 'mac_address': n_const.ATTR_NOT_SPECIFIED, + 'fixed_ips': n_const.ATTR_NOT_SPECIFIED, + portbindings.HOST_ID: 'test-host'}} + fixed_port = self.plugin.create_port(self.context, + fixed_port_data) + self.plugin._create_or_update_agent(self.context, + {'agent_type': 'L3 agent', + 'host': 'test-host', + 'binary': 'neutron-l3-agent', + 'topic': 'test'}) + fip_gw = self.l3plugin.create_fip_agent_gw_port_if_not_exists( + self.context, + gw_net_id, + 'test-host') + fip_data = {'floatingip': {'floating_network_id': gw_net_id, + 'tenant_id': tenant_id, + 'port_id': fixed_port['id']}} + fip = self.l3plugin.create_floatingip(self.context, fip_data) + fip_prefix = fip['floating_ip_address'] + '/32' + with self.bgp_speaker(4, 1234, networks=[gw_net_id]) as speaker: + bgp_speaker_id = speaker['id'] + routes = self.bgp_plugin._get_dvr_fip_host_routes_by_binding( + self.context, + gw_net_id, + bgp_speaker_id) + routes = list(routes) + dvr_gw_ip = fip_gw['fixed_ips'][0]['ip_address'] + self.assertEqual(1, len(routes)) + self.assertEqual(dvr_gw_ip, routes[0]['next_hop']) + self.assertEqual(fip_prefix, routes[0]['destination']) + + def test__get_dvr_fip_host_routes_by_router(self): + gw_prefix = '172.16.10.0/24' + tenant_prefix = '10.10.10.0/24' + 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, + gw_prefix=gw_prefix, + tenant_prefix=tenant_prefix, + address_scope=scope, + distributed=True) as res: + router, ext_net, int_net = res + gw_net_id = ext_net['network']['id'] + tenant_net_id = int_net['network']['id'] + fixed_port_data = {'port': + {'name': 'test', + 'network_id': tenant_net_id, + 'tenant_id': tenant_id, + 'admin_state_up': True, + 'device_id': _uuid(), + 'device_owner': 'compute:nova', + 'mac_address': n_const.ATTR_NOT_SPECIFIED, + 'fixed_ips': n_const.ATTR_NOT_SPECIFIED, + portbindings.HOST_ID: 'test-host'}} + fixed_port = self.plugin.create_port(self.context, + fixed_port_data) + self.plugin._create_or_update_agent(self.context, + {'agent_type': 'L3 agent', + 'host': 'test-host', + 'binary': 'neutron-l3-agent', + 'topic': 'test'}) + fip_gw = self.l3plugin.create_fip_agent_gw_port_if_not_exists( + self.context, + gw_net_id, + 'test-host') + fip_data = {'floatingip': {'floating_network_id': gw_net_id, + 'tenant_id': tenant_id, + 'port_id': fixed_port['id']}} + fip = self.l3plugin.create_floatingip(self.context, fip_data) + fip_prefix = fip['floating_ip_address'] + '/32' + with self.bgp_speaker(4, 1234, networks=[gw_net_id]) as speaker: + bgp_speaker_id = speaker['id'] + routes = self.bgp_plugin._get_dvr_fip_host_routes_by_router( + self.context, + bgp_speaker_id, + router['id']) + routes = list(routes) + dvr_gw_ip = fip_gw['fixed_ips'][0]['ip_address'] + self.assertEqual(1, len(routes)) + self.assertEqual(dvr_gw_ip, routes[0]['next_hop']) + self.assertEqual(fip_prefix, routes[0]['destination']) + + def test_get_routes_by_bgp_speaker_binding_with_fip(self): + gw_prefix = '172.16.10.0/24' + tenant_prefix = '10.10.10.0/24' + 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, + gw_prefix=gw_prefix, + tenant_prefix=tenant_prefix, + address_scope=scope) as res: + router, ext_net, int_net = res + ext_gw_info = router['external_gateway_info'] + gw_net_id = ext_net['network']['id'] + tenant_net_id = int_net['network']['id'] + fixed_port_data = {'port': + {'name': 'test', + 'network_id': tenant_net_id, + 'tenant_id': tenant_id, + 'admin_state_up': True, + 'device_id': _uuid(), + 'device_owner': 'compute:nova', + 'mac_address': n_const.ATTR_NOT_SPECIFIED, + 'fixed_ips': n_const.ATTR_NOT_SPECIFIED}} + fixed_port = self.plugin.create_port(self.context, + fixed_port_data) + fip_data = {'floatingip': {'floating_network_id': gw_net_id, + 'tenant_id': tenant_id, + 'port_id': fixed_port['id']}} + fip = self.l3plugin.create_floatingip(self.context, fip_data) + fip_prefix = fip['floating_ip_address'] + '/32' + with self.bgp_speaker(4, 1234, networks=[gw_net_id]) as speaker: + bgp_speaker_id = speaker['id'] + routes = self.bgp_plugin.get_routes_by_bgp_speaker_binding( + self.context, + bgp_speaker_id, + gw_net_id) + routes = list(routes) + next_hop = ext_gw_info['external_fixed_ips'][0]['ip_address'] + self.assertEqual(2, len(routes)) + tenant_prefix_found = False + fip_prefix_found = False + for route in routes: + self.assertEqual(next_hop, route['next_hop']) + if route['destination'] == tenant_prefix: + tenant_prefix_found = True + if route['destination'] == fip_prefix: + fip_prefix_found = True + self.assertTrue(tenant_prefix_found) + self.assertTrue(fip_prefix_found) + + def test__bgp_speakers_for_gateway_network_by_ip_version(self): + with self.network() as ext_net, self.bgp_speaker(6, 1234) as s1,\ + self.bgp_speaker(6, 4321) as s2: + gw_net_id = ext_net['network']['id'] + self._update('networks', gw_net_id, + {'network': {external_net.EXTERNAL: True}}) + self.bgp_plugin.add_gateway_network(self.context, + s1['id'], + {'network_id': gw_net_id}) + self.bgp_plugin.add_gateway_network(self.context, + s2['id'], + {'network_id': gw_net_id}) + speakers = self.bgp_plugin._bgp_speakers_for_gw_network_by_family( + self.context, + gw_net_id, + 6) + self.assertEqual(2, len(speakers)) + + def test__bgp_speakers_for_gateway_network_by_ip_version_no_binding(self): + with self.network() as ext_net, self.bgp_speaker(6, 1234),\ + self.bgp_speaker(6, 4321): + gw_net_id = ext_net['network']['id'] + self._update('networks', gw_net_id, + {'network': {external_net.EXTERNAL: True}}) + speakers = self.bgp_plugin._bgp_speakers_for_gw_network_by_family( + self.context, + gw_net_id, + 6) + self.assertTrue(not speakers) diff --git a/neutron_dynamic_routing/tests/unit/db/test_bgp_dragentscheduler_db.py b/neutron_dynamic_routing/tests/unit/db/test_bgp_dragentscheduler_db.py new file mode 100644 index 00000000..c1856709 --- /dev/null +++ b/neutron_dynamic_routing/tests/unit/db/test_bgp_dragentscheduler_db.py @@ -0,0 +1,206 @@ +# Copyright (c) 2016 Hewlett Packard Enterprise Development Company, L.P. +# +# 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_config import cfg +from oslo_utils import importutils + +from neutron.api.v2 import attributes +from neutron import context +from neutron.extensions import agent +from neutron import manager +from neutron.tests.unit.db import test_db_base_plugin_v2 as test_db_base_plugin +from neutron.tests.unit.extensions import test_agent + +from neutron_dynamic_routing.db import bgp_db +from neutron_dynamic_routing.db import bgp_dragentscheduler_db as bgp_dras_db +from neutron_dynamic_routing.extensions import bgp +from neutron_dynamic_routing.extensions import bgp_dragentscheduler as bgp_dras_ext # noqa +from neutron_dynamic_routing.tests.common import helpers +from neutron_dynamic_routing.tests.unit.db import test_bgp_db + +from webob import exc + + +class BgpDrSchedulerTestExtensionManager(object): + + def get_resources(self): + attributes.RESOURCE_ATTRIBUTE_MAP.update( + agent.RESOURCE_ATTRIBUTE_MAP) + resources = agent.Agent.get_resources() + resources.extend(bgp_dras_ext.Bgp_dragentscheduler.get_resources()) + return resources + + def get_actions(self): + return [] + + def get_request_extensions(self): + return [] + + +class TestBgpDrSchedulerPlugin(bgp_db.BgpDbMixin, + bgp_dras_db.BgpDrAgentSchedulerDbMixin): + + bgp_drscheduler = importutils.import_object( + cfg.CONF.bgp_drscheduler_driver) + + supported_extension_aliases = ["bgp_dragent_scheduler"] + + def get_plugin_description(self): + return ("BGP dynamic routing service Plugin test class that test " + "BGP speaker functionality, with scheduler.") + + +class BgpDrSchedulingTestCase(test_agent.AgentDBTestMixIn, + test_bgp_db.BgpEntityCreationMixin): + + def test_schedule_bgp_speaker(self): + """Test happy path over full scheduling cycle.""" + with self.bgp_speaker(4, 1234) as ri: + bgp_speaker_id = ri['id'] + helpers.register_bgp_dragent(host='host1') + agent = self._list('agents')['agents'][0] + agent_id = agent['id'] + + data = {'bgp_speaker_id': bgp_speaker_id} + req = self.new_create_request('agents', data, self.fmt, + agent_id, 'bgp-drinstances') + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + + req_show = self.new_show_request('agents', agent_id, self.fmt, + 'bgp-drinstances') + res = req_show.get_response(self.ext_api) + self.assertEqual(exc.HTTPOk.code, res.status_int) + res = self.deserialize(self.fmt, res) + self.assertIn('bgp_speakers', res) + self.assertTrue(bgp_speaker_id, + res['bgp_speakers'][0]['id']) + + req = self.new_delete_request('agents', + agent_id, + self.fmt, + 'bgp-drinstances', + bgp_speaker_id) + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPNoContent.code, res.status_int) + + res = req_show.get_response(self.ext_api) + self.assertEqual(exc.HTTPOk.code, res.status_int) + res = self.deserialize(self.fmt, res) + self.assertIn('bgp_speakers', res) + self.assertEqual([], res['bgp_speakers']) + + def test_schedule_bgp_speaker_on_invalid_agent(self): + """Test error while scheduling BGP speaker on an invalid agent.""" + with self.bgp_speaker(4, 1234) as ri: + bgp_speaker_id = ri['id'] + self._register_l3_agent(host='host1') # Register wrong agent + agent = self._list('agents')['agents'][0] + data = {'bgp_speaker_id': bgp_speaker_id} + req = self.new_create_request( + 'agents', data, self.fmt, + agent['id'], 'bgp-drinstances') + res = req.get_response(self.ext_api) + + # Raises an AgentNotFound exception if the agent is invalid + self.assertEqual(exc.HTTPNotFound.code, res.status_int) + + def test_schedule_bgp_speaker_twice_on_same_agent(self): + """Test error if a BGP speaker is scheduled twice on same agent""" + with self.bgp_speaker(4, 1234) as ri: + bgp_speaker_id = ri['id'] + helpers.register_bgp_dragent(host='host1') + agent = self._list('agents')['agents'][0] + data = {'bgp_speaker_id': bgp_speaker_id} + req = self.new_create_request( + 'agents', data, self.fmt, + agent['id'], 'bgp-drinstances') + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + + # Try second time, should raise conflict + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPConflict.code, res.status_int) + + def test_schedule_bgp_speaker_on_two_different_agents(self): + """Test that a BGP speaker can be associated to two agents.""" + with self.bgp_speaker(4, 1234) as ri: + bgp_speaker_id = ri['id'] + helpers.register_bgp_dragent(host='host1') + helpers.register_bgp_dragent(host='host2') + data = {'bgp_speaker_id': bgp_speaker_id} + + agent1 = self._list('agents')['agents'][0] + req = self.new_create_request( + 'agents', data, self.fmt, + agent1['id'], 'bgp-drinstances') + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + + agent2 = self._list('agents')['agents'][1] + req = self.new_create_request( + 'agents', data, self.fmt, + agent2['id'], 'bgp-drinstances') + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + + def test_schedule_multi_bgp_speaker_on_one_dragent(self): + """Test only one BGP speaker can be associated to one dragent.""" + with self.bgp_speaker(4, 1) as ri1, self.bgp_speaker(4, 2) as ri2: + helpers.register_bgp_dragent(host='host1') + + agent = self._list('agents')['agents'][0] + data = {'bgp_speaker_id': ri1['id']} + req = self.new_create_request( + 'agents', data, self.fmt, + agent['id'], 'bgp-drinstances') + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + + data = {'bgp_speaker_id': ri2['id']} + req = self.new_create_request( + 'agents', data, self.fmt, + agent['id'], 'bgp-drinstances') + res = req.get_response(self.ext_api) + self.assertEqual(exc.HTTPConflict.code, res.status_int) + + def test_non_scheduled_bgp_speaker_binding_removal(self): + """Test exception while removing an invalid binding.""" + with self.bgp_speaker(4, 1234) as ri1: + helpers.register_bgp_dragent(host='host1') + agent = self._list('agents')['agents'][0] + agent_id = agent['id'] + self.assertRaises(bgp_dras_ext.DrAgentNotHostingBgpSpeaker, + self.bgp_plugin.remove_bgp_speaker_from_dragent, + self.context, agent_id, ri1['id']) + + +class BgpDrPluginSchedulerTests(test_db_base_plugin.NeutronDbPluginV2TestCase, + BgpDrSchedulingTestCase): + + def setUp(self, plugin=None, ext_mgr=None, service_plugins=None): + if not plugin: + plugin = ('neutron_dynamic_routing.tests.unit.db.' + 'test_bgp_dragentscheduler_db.TestBgpDrSchedulerPlugin') + if not service_plugins: + service_plugins = {bgp.BGP_EXT_ALIAS: + 'neutron_dynamic_routing.services.bgp.' + 'bgp_plugin.BgpPlugin'} + + ext_mgr = ext_mgr or BgpDrSchedulerTestExtensionManager() + super(BgpDrPluginSchedulerTests, self).setUp( + plugin=plugin, ext_mgr=ext_mgr, service_plugins=service_plugins) + self.bgp_plugin = manager.NeutronManager.get_service_plugins().get( + bgp.BGP_EXT_ALIAS) + self.context = context.get_admin_context() diff --git a/neutron_dynamic_routing/tests/unit/services/__init__.py b/neutron_dynamic_routing/tests/unit/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/unit/services/bgp/__init__.py b/neutron_dynamic_routing/tests/unit/services/bgp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/unit/services/bgp/agent/__init__.py b/neutron_dynamic_routing/tests/unit/services/bgp/agent/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/unit/services/bgp/agent/test_bgp_dragent.py b/neutron_dynamic_routing/tests/unit/services/bgp/agent/test_bgp_dragent.py new file mode 100644 index 00000000..9908ce5b --- /dev/null +++ b/neutron_dynamic_routing/tests/unit/services/bgp/agent/test_bgp_dragent.py @@ -0,0 +1,748 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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 copy +import sys +import uuid + +import eventlet +import mock +from oslo_config import cfg +import testtools + +from neutron.agent.common import config +from neutron.common import config as n_config +from neutron import context +from neutron.tests import base + +from neutron_dynamic_routing.services.bgp.agent import bgp_dragent +from neutron_dynamic_routing.services.bgp.agent import config as bgp_config +from neutron_dynamic_routing.services.bgp.agent import entry + +HOSTNAME = 'hostname' +rpc_api = bgp_dragent.BgpDrPluginApi +BGP_PLUGIN = '%s.%s' % (rpc_api.__module__, rpc_api.__name__) + +FAKE_BGPSPEAKER_UUID = str(uuid.uuid4()) +FAKE_BGPPEER_UUID = str(uuid.uuid4()) + +FAKE_BGP_SPEAKER = {'id': FAKE_BGPSPEAKER_UUID, + 'local_as': 12345, + 'peers': [{'remote_as': '2345', + 'peer_ip': '1.1.1.1', + 'auth_type': 'none', + 'password': ''}], + 'advertised_routes': []} + +FAKE_BGP_PEER = {'id': FAKE_BGPPEER_UUID, + 'remote_as': '2345', + 'peer_ip': '1.1.1.1', + 'auth_type': 'none', + 'password': ''} + +FAKE_ROUTE = {'id': FAKE_BGPSPEAKER_UUID, + 'destination': '2.2.2.2/32', + 'next_hop': '3.3.3.3'} + +FAKE_ROUTES = {'routes': {'id': FAKE_BGPSPEAKER_UUID, + 'destination': '2.2.2.2/32', + 'next_hop': '3.3.3.3'} + } + + +class TestBgpDrAgent(base.BaseTestCase): + def setUp(self): + super(TestBgpDrAgent, self).setUp() + cfg.CONF.register_opts(bgp_config.BGP_DRIVER_OPTS, 'BGP') + cfg.CONF.register_opts(bgp_config.BGP_PROTO_CONFIG_OPTS, 'BGP') + mock_log_p = mock.patch.object(bgp_dragent, 'LOG') + self.mock_log = mock_log_p.start() + self.driver_cls_p = mock.patch( + 'neutron_dynamic_routing.services.bgp.agent.bgp_dragent.' + 'importutils.import_class') + self.driver_cls = self.driver_cls_p.start() + self.context = context.get_admin_context() + + def test_bgp_dragent_manager(self): + state_rpc_str = 'neutron.agent.rpc.PluginReportStateAPI' + # sync_state is needed for this test + with mock.patch.object(bgp_dragent.BgpDrAgentWithStateReport, + 'sync_state', + autospec=True) as mock_sync_state: + with mock.patch(state_rpc_str) as state_rpc: + with mock.patch.object(sys, 'argv') as sys_argv: + sys_argv.return_value = [ + 'bgp_dragent', '--config-file', + base.etcdir('neutron.conf')] + config.register_agent_state_opts_helper(cfg.CONF) + n_config.init(sys.argv[1:]) + agent_mgr = bgp_dragent.BgpDrAgentWithStateReport( + 'testhost') + eventlet.greenthread.sleep(1) + agent_mgr.after_start() + self.assertIsNotNone(len(mock_sync_state.mock_calls)) + state_rpc.assert_has_calls( + [mock.call(mock.ANY), + mock.call().report_state(mock.ANY, mock.ANY, + mock.ANY)]) + + def test_bgp_dragent_main_agent_manager(self): + logging_str = 'neutron.agent.common.config.setup_logging' + launcher_str = 'oslo_service.service.ServiceLauncher' + with mock.patch(logging_str): + with mock.patch.object(sys, 'argv') as sys_argv: + with mock.patch(launcher_str) as launcher: + sys_argv.return_value = ['bgp_dragent', '--config-file', + base.etcdir('neutron.conf')] + entry.main() + if launcher.mock_calls[0][2]: + launcher.assert_has_calls( + [mock.call(cfg.CONF, restart_method='reload'), + mock.call().launch_service(mock.ANY, workers=1), + mock.call().wait()]) + else: + launcher.assert_has_calls( + [mock.call(cfg.CONF), + mock.call().launch_service(mock.ANY), + mock.call().wait()]) + + def test_run_completes_single_pass(self): + bgp_dr = bgp_dragent.BgpDrAgent(HOSTNAME) + with mock.patch.object(bgp_dr, 'sync_state') as sync_state: + bgp_dr.run() + self.assertIsNotNone(len(sync_state.mock_calls)) + + def test_after_start(self): + bgp_dr = bgp_dragent.BgpDrAgent(HOSTNAME) + with mock.patch.object(bgp_dr, 'sync_state') as sync_state: + bgp_dr.after_start() + self.assertIsNotNone(len(sync_state.mock_calls)) + + def _test_sync_state_helper(self, bgp_speaker_list=None, + cached_info=None, + safe_configure_call_count=0, + sync_bgp_speaker_call_count=0, + remove_bgp_speaker_call_count=0, + remove_bgp_speaker_ids=None, + added_bgp_speakers=None, + synced_bgp_speakers=None): + bgp_dr = bgp_dragent.BgpDrAgent(HOSTNAME) + + attrs_to_mock = dict( + [(a, mock.MagicMock()) + for a in ['plugin_rpc', 'sync_bgp_speaker', + 'safe_configure_dragent_for_bgp_speaker', + 'remove_bgp_speaker_from_dragent']]) + + with mock.patch.multiple(bgp_dr, **attrs_to_mock): + if not cached_info: + cached_info = {} + if not added_bgp_speakers: + added_bgp_speakers = [] + if not remove_bgp_speaker_ids: + remove_bgp_speaker_ids = [] + if not synced_bgp_speakers: + synced_bgp_speakers = [] + + bgp_dr.plugin_rpc.get_bgp_speakers.return_value = bgp_speaker_list + bgp_dr.cache.cache = cached_info + bgp_dr.cache.clear_cache = mock.Mock() + bgp_dr.sync_state(mock.ANY) + + self.assertEqual( + remove_bgp_speaker_call_count, + bgp_dr.remove_bgp_speaker_from_dragent.call_count) + + if remove_bgp_speaker_call_count: + expected_calls = [mock.call(bgp_speaker_id) + for bgp_speaker_id in remove_bgp_speaker_ids] + bgp_dr.remove_bgp_speaker_from_dragent.assert_has_calls( + expected_calls) + + self.assertEqual( + safe_configure_call_count, + bgp_dr.safe_configure_dragent_for_bgp_speaker.call_count) + + if safe_configure_call_count: + expected_calls = [mock.call(bgp_speaker) + for bgp_speaker in added_bgp_speakers] + bgp_dr.safe_configure_dragent_for_bgp_speaker.assert_has_calls( + expected_calls) + + self.assertEqual(sync_bgp_speaker_call_count, + bgp_dr.sync_bgp_speaker.call_count) + + if sync_bgp_speaker_call_count: + expected_calls = [mock.call(bgp_speaker) + for bgp_speaker in synced_bgp_speakers] + bgp_dr.sync_bgp_speaker.assert_has_calls(expected_calls) + + def test_sync_state_bgp_speaker_added(self): + bgp_speaker_list = [{'id': 'foo-id', + 'local_as': 12345, + 'peers': [], + 'advertised_routes': []}] + self._test_sync_state_helper(bgp_speaker_list=bgp_speaker_list, + safe_configure_call_count=1, + added_bgp_speakers=bgp_speaker_list) + + def test_sync_state_bgp_speaker_deleted(self): + bgp_speaker_list = [] + cached_bgp_speaker = {'id': 'foo-id', + 'local_as': 12345, + 'peers': ['peer-1'], + 'advertised_routes': []} + cached_info = {'foo-id': cached_bgp_speaker} + self._test_sync_state_helper(bgp_speaker_list=bgp_speaker_list, + cached_info=cached_info, + remove_bgp_speaker_call_count=1, + remove_bgp_speaker_ids=['foo-id']) + + def test_sync_state_added_and_deleted(self): + bgp_speaker_list = [{'id': 'foo-id', + 'local_as': 12345, + 'peers': [], + 'advertised_routes': []}] + cached_bgp_speaker = {'bgp_speaker': {'local_as': 12345}, + 'peers': ['peer-1'], + 'advertised_routes': []} + cached_info = {'bar-id': cached_bgp_speaker} + + self._test_sync_state_helper(bgp_speaker_list=bgp_speaker_list, + cached_info=cached_info, + remove_bgp_speaker_call_count=1, + remove_bgp_speaker_ids=['bar-id'], + safe_configure_call_count=1, + added_bgp_speakers=bgp_speaker_list) + + def test_sync_state_added_and_synced(self): + bgp_speaker_list = [{'id': 'foo-id', + 'local_as': 12345, + 'peers': [], + 'advertised_routes': []}, + {'id': 'bar-id', 'peers': ['peer-2'], + 'advertised_routes': []}, + {'id': 'temp-id', 'peers': ['temp-1'], + 'advertised_routes': []}] + + cached_bgp_speaker = {'id': 'bar-id', 'bgp_speaker': {'id': 'bar-id'}, + 'peers': ['peer-1'], + 'advertised_routes': []} + cached_bgp_speaker_2 = {'id': 'temp-id', + 'bgp_speaker': {'id': 'temp-id'}, + 'peers': ['temp-1'], + 'advertised_routes': []} + cached_info = {'bar-id': cached_bgp_speaker, + 'temp-id': cached_bgp_speaker_2} + + self._test_sync_state_helper(bgp_speaker_list=bgp_speaker_list, + cached_info=cached_info, + safe_configure_call_count=1, + added_bgp_speakers=[bgp_speaker_list[0]], + sync_bgp_speaker_call_count=2, + synced_bgp_speakers=[bgp_speaker_list[1], + bgp_speaker_list[2]] + ) + + def test_sync_state_added_synced_and_removed(self): + bgp_speaker_list = [{'id': 'foo-id', + 'local_as': 12345, + 'peers': [], + 'advertised_routes': []}, + {'id': 'bar-id', 'peers': ['peer-2'], + 'advertised_routes': []}] + cached_bgp_speaker = {'id': 'bar-id', + 'bgp_speaker': {'id': 'bar-id'}, + 'peers': ['peer-1'], + 'advertised_routes': []} + cached_bgp_speaker_2 = {'id': 'temp-id', + 'bgp_speaker': {'id': 'temp-id'}, + 'peers': ['temp-1'], + 'advertised_routes': []} + cached_info = {'bar-id': cached_bgp_speaker, + 'temp-id': cached_bgp_speaker_2} + + self._test_sync_state_helper(bgp_speaker_list=bgp_speaker_list, + cached_info=cached_info, + remove_bgp_speaker_call_count=1, + remove_bgp_speaker_ids=['temp-id'], + safe_configure_call_count=1, + added_bgp_speakers=[bgp_speaker_list[0]], + sync_bgp_speaker_call_count=1, + synced_bgp_speakers=[bgp_speaker_list[1]]) + + def _test_sync_bgp_speaker_helper(self, bgp_speaker, cached_info=None, + remove_bgp_peer_call_count=0, + removed_bgp_peer_ip_list=None, + withdraw_route_call_count=0, + withdraw_routes_list=None, + add_bgp_peers_called=False, + advertise_routes_called=False): + if not cached_info: + cached_info = {} + if not removed_bgp_peer_ip_list: + removed_bgp_peer_ip_list = [] + if not withdraw_routes_list: + withdraw_routes_list = [] + + bgp_dr = bgp_dragent.BgpDrAgent(HOSTNAME) + + attrs_to_mock = dict( + [(a, mock.MagicMock()) + for a in ['remove_bgp_peer_from_bgp_speaker', + 'add_bgp_peers_to_bgp_speaker', + 'advertise_routes_via_bgp_speaker', + 'withdraw_route_via_bgp_speaker']]) + + with mock.patch.multiple(bgp_dr, **attrs_to_mock): + bgp_dr.cache.cache = cached_info + bgp_dr.sync_bgp_speaker(bgp_speaker) + + self.assertEqual( + remove_bgp_peer_call_count, + bgp_dr.remove_bgp_peer_from_bgp_speaker.call_count) + + if remove_bgp_peer_call_count: + expected_calls = [mock.call(bgp_speaker['id'], peer_ip) + for peer_ip in removed_bgp_peer_ip_list] + bgp_dr.remove_bgp_peer_from_bgp_speaker.assert_has_calls( + expected_calls) + + self.assertEqual(add_bgp_peers_called, + bgp_dr.add_bgp_peers_to_bgp_speaker.called) + + if add_bgp_peers_called: + bgp_dr.add_bgp_peers_to_bgp_speaker.assert_called_with( + bgp_speaker) + + self.assertEqual( + withdraw_route_call_count, + bgp_dr.withdraw_route_via_bgp_speaker.call_count) + + if withdraw_route_call_count: + expected_calls = [mock.call(bgp_speaker['id'], 12345, route) + for route in withdraw_routes_list] + bgp_dr.withdraw_route_via_bgp_speaker.assert_has_calls( + expected_calls) + + self.assertEqual(advertise_routes_called, + bgp_dr.advertise_routes_via_bgp_speaker.called) + + if advertise_routes_called: + bgp_dr.advertise_routes_via_bgp_speaker.assert_called_with( + bgp_speaker) + + def test_sync_bgp_speaker_bgp_peers_updated(self): + peers = [{'id': 'peer-1', 'peer_ip': '1.1.1.1'}, + {'id': 'peer-2', 'peer_ip': '2.2.2.2'}] + bgp_speaker = {'id': 'foo-id', + 'local_as': 12345, + 'peers': peers, + 'advertised_routes': []} + + cached_peers = {'1.1.1.1': {'id': 'peer-2', 'peer_ip': '1.1.1.1'}, + '3.3.3.3': {'id': 'peer-3', 'peer_ip': '3.3.3.3'}} + + cached_bgp_speaker = {'foo-id': {'bgp_speaker': {'local_as': 12345}, + 'peers': cached_peers, + 'advertised_routes': []}} + self._test_sync_bgp_speaker_helper( + bgp_speaker, cached_info=cached_bgp_speaker, + remove_bgp_peer_call_count=1, + removed_bgp_peer_ip_list=['3.3.3.3'], + add_bgp_peers_called=True, + advertise_routes_called=False) + + def test_sync_bgp_speaker_routes_updated(self): + adv_routes = [{'destination': '10.0.0.0/24', 'next_hop': '1.1.1.1'}, + {'destination': '20.0.0.0/24', 'next_hop': '2.2.2.2'}] + bgp_speaker = {'id': 'foo-id', + 'local_as': 12345, + 'peers': {}, + 'advertised_routes': adv_routes} + + cached_adv_routes = [{'destination': '20.0.0.0/24', + 'next_hop': '2.2.2.2'}, + {'destination': '30.0.0.0/24', + 'next_hop': '3.3.3.3'}] + + cached_bgp_speaker = { + 'foo-id': {'bgp_speaker': {'local_as': 12345}, + 'peers': {}, + 'advertised_routes': cached_adv_routes}} + + self._test_sync_bgp_speaker_helper( + bgp_speaker, cached_info=cached_bgp_speaker, + withdraw_route_call_count=1, + withdraw_routes_list=[cached_adv_routes[1]], + add_bgp_peers_called=False, + advertise_routes_called=True) + + def test_sync_bgp_speaker_peers_routes_added(self): + peers = [{'id': 'peer-1', 'peer_ip': '1.1.1.1'}, + {'id': 'peer-2', 'peer_ip': '2.2.2.2'}] + adv_routes = [{'destination': '10.0.0.0/24', + 'next_hop': '1.1.1.1'}, + {'destination': '20.0.0.0/24', + 'next_hop': '2.2.2.2'}] + bgp_speaker = {'id': 'foo-id', + 'local_as': 12345, + 'peers': peers, + 'advertised_routes': adv_routes} + + cached_bgp_speaker = { + 'foo-id': {'bgp_speaker': {'local_as': 12345}, + 'peers': {}, + 'advertised_routes': []}} + + self._test_sync_bgp_speaker_helper( + bgp_speaker, cached_info=cached_bgp_speaker, + add_bgp_peers_called=True, + advertise_routes_called=True) + + def test_sync_state_plugin_error(self): + with mock.patch(BGP_PLUGIN) as plug: + mock_plugin = mock.Mock() + mock_plugin.get_bgp_speakers.side_effect = Exception + plug.return_value = mock_plugin + + with mock.patch.object(bgp_dragent.LOG, 'error') as log: + bgp_dr = bgp_dragent.BgpDrAgent(HOSTNAME) + with mock.patch.object(bgp_dr, + 'schedule_full_resync') as schedule_full_resync: + bgp_dr.sync_state(mock.ANY) + + self.assertTrue(log.called) + self.assertTrue(schedule_full_resync.called) + + def test_periodic_resync(self): + bgp_dr = bgp_dragent.BgpDrAgent(HOSTNAME) + with mock.patch.object(bgp_dr, + '_periodic_resync_helper') as resync_helper: + bgp_dr.periodic_resync(self.context) + self.assertTrue(resync_helper.called) + + def test_periodic_resync_helper(self): + bgp_dr = bgp_dragent.BgpDrAgent(HOSTNAME) + bgp_dr.schedule_resync('foo reason', 'foo-id') + with mock.patch.object(bgp_dr, 'sync_state') as sync_state: + sync_state.side_effect = RuntimeError + with testtools.ExpectedException(RuntimeError): + bgp_dr._periodic_resync_helper(self.context) + self.assertTrue(sync_state.called) + self.assertEqual(len(bgp_dr.needs_resync_reasons), 0) + + def _test_add_bgp_peer_helper(self, bgp_speaker_id, + bgp_peer, cached_bgp_speaker, + put_bgp_peer_called=True): + bgp_dr = bgp_dragent.BgpDrAgent(HOSTNAME) + + bgp_dr.cache.cache = cached_bgp_speaker + with mock.patch.object( + bgp_dr.cache, 'put_bgp_peer') as mock_put_bgp_peer: + bgp_dr.add_bgp_peer_to_bgp_speaker('foo-id', 12345, bgp_peer) + if put_bgp_peer_called: + mock_put_bgp_peer.assert_called_once_with( + bgp_speaker_id, bgp_peer) + else: + self.assertFalse(mock_put_bgp_peer.called) + + def test_add_bgp_peer_not_cached(self): + bgp_peer = {'peer_ip': '1.1.1.1', 'remote_as': 34567, + 'auth_type': 'md5', 'password': 'abc'} + cached_bgp_speaker = {'foo-id': {'bgp_speaker': {'local_as': 12345}, + 'peers': {}, + 'advertised_routes': []}} + + self._test_add_bgp_peer_helper('foo-id', bgp_peer, cached_bgp_speaker) + + def test_add_bgp_peer_already_cached(self): + bgp_peer = {'peer_ip': '1.1.1.1', 'remote_as': 34567, + 'auth_type': 'md5', 'password': 'abc'} + cached_peers = {'1.1.1.1': {'peer_ip': '1.1.1.1', 'remote_as': 34567}} + cached_bgp_speaker = {'foo-id': {'bgp_speaker': {'local_as': 12345}, + 'peers': cached_peers, + 'advertised_routes': []}} + + self._test_add_bgp_peer_helper('foo-id', bgp_peer, cached_bgp_speaker, + put_bgp_peer_called=False) + + def _test_advertise_route_helper(self, bgp_speaker_id, + route, cached_bgp_speaker, + put_adv_route_called=True): + bgp_dr = bgp_dragent.BgpDrAgent(HOSTNAME) + + bgp_dr.cache.cache = cached_bgp_speaker + with mock.patch.object( + bgp_dr.cache, 'put_adv_route') as mock_put_adv_route: + bgp_dr.advertise_route_via_bgp_speaker(bgp_speaker_id, 12345, + route) + if put_adv_route_called: + mock_put_adv_route.assert_called_once_with( + bgp_speaker_id, route) + else: + self.assertFalse(mock_put_adv_route.called) + + def test_advertise_route_helper_not_cached(self): + route = {'destination': '10.0.0.0/24', 'next_hop': '1.1.1.1'} + cached_bgp_speaker = {'foo-id': {'bgp_speaker': {'local_as': 12345}, + 'peers': {}, + 'advertised_routes': []}} + + self._test_advertise_route_helper('foo-id', route, cached_bgp_speaker, + put_adv_route_called=True) + + def test_advertise_route_helper_already_cached(self): + route = {'destination': '10.0.0.0/24', 'next_hop': '1.1.1.1'} + cached_bgp_speaker = {'foo-id': {'bgp_speaker': {'local_as': 12345}, + 'peers': {}, + 'advertised_routes': [route]}} + + self._test_advertise_route_helper('foo-id', route, cached_bgp_speaker, + put_adv_route_called=False) + + +class TestBgpDrAgentEventHandler(base.BaseTestCase): + + cache_cls = 'neutron_dynamic_routing.services.bgp.'\ + 'agent.bgp_dragent.BgpSpeakerCache' + + def setUp(self): + super(TestBgpDrAgentEventHandler, self).setUp() + cfg.CONF.register_opts(bgp_config.BGP_DRIVER_OPTS, 'BGP') + cfg.CONF.register_opts(bgp_config.BGP_PROTO_CONFIG_OPTS, 'BGP') + + mock_log_p = mock.patch.object(bgp_dragent, 'LOG') + self.mock_log = mock_log_p.start() + + self.plugin_p = mock.patch(BGP_PLUGIN) + plugin_cls = self.plugin_p.start() + self.plugin = mock.Mock() + plugin_cls.return_value = self.plugin + + self.cache_p = mock.patch(self.cache_cls) + cache_cls = self.cache_p.start() + self.cache = mock.Mock() + cache_cls.return_value = self.cache + + self.driver_cls_p = mock.patch( + 'neutron_dynamic_routing.services.bgp.agent.bgp_dragent.' + 'importutils.import_class') + self.driver_cls = self.driver_cls_p.start() + + self.bgp_dr = bgp_dragent.BgpDrAgent(HOSTNAME) + self.schedule_full_resync_p = mock.patch.object( + self.bgp_dr, 'schedule_full_resync') + self.schedule_full_resync = self.schedule_full_resync_p.start() + self.context = mock.Mock() + + def test_bgp_speaker_create_end(self): + payload = {'bgp_speaker': {'id': FAKE_BGPSPEAKER_UUID}} + + with mock.patch.object(self.bgp_dr, + 'add_bgp_speaker_helper') as enable: + self.bgp_dr.bgp_speaker_create_end(None, payload) + enable.assert_called_once_with(FAKE_BGP_SPEAKER['id']) + + def test_bgp_peer_association_end(self): + payload = {'bgp_peer': {'speaker_id': FAKE_BGPSPEAKER_UUID, + 'peer_id': FAKE_BGPPEER_UUID}} + + with mock.patch.object(self.bgp_dr, + 'add_bgp_peer_helper') as enable: + self.bgp_dr.bgp_peer_association_end(None, payload) + enable.assert_called_once_with(FAKE_BGP_SPEAKER['id'], + FAKE_BGP_PEER['id']) + + def test_route_advertisement_end(self): + routes = [{'destination': '2.2.2.2/32', 'next_hop': '3.3.3.3'}, + {'destination': '4.4.4.4/32', 'next_hop': '5.5.5.5'}] + payload = {'advertise_routes': {'speaker_id': FAKE_BGPSPEAKER_UUID, + 'routes': routes}} + + expected_calls = [mock.call(FAKE_BGP_SPEAKER['id'], routes)] + + with mock.patch.object(self.bgp_dr, + 'add_routes_helper') as enable: + self.bgp_dr.bgp_routes_advertisement_end(None, payload) + enable.assert_has_calls(expected_calls) + + def test_add_bgp_speaker_helper(self): + self.plugin.get_bgp_speaker_info.return_value = FAKE_BGP_SPEAKER + add_bs_p = mock.patch.object(self.bgp_dr, + 'add_bgp_speaker_on_dragent') + add_bs = add_bs_p.start() + self.bgp_dr.add_bgp_speaker_helper(FAKE_BGP_SPEAKER['id']) + self.plugin.assert_has_calls([ + mock.call.get_bgp_speaker_info(mock.ANY, + FAKE_BGP_SPEAKER['id'])]) + add_bs.assert_called_once_with(FAKE_BGP_SPEAKER) + + def test_add_bgp_peer_helper(self): + self.plugin.get_bgp_peer_info.return_value = FAKE_BGP_PEER + add_bp_p = mock.patch.object(self.bgp_dr, + 'add_bgp_peer_to_bgp_speaker') + add_bp = add_bp_p.start() + self.bgp_dr.add_bgp_peer_helper(FAKE_BGP_SPEAKER['id'], + FAKE_BGP_PEER['id']) + self.plugin.assert_has_calls([ + mock.call.get_bgp_peer_info(mock.ANY, + FAKE_BGP_PEER['id'])]) + self.assertEqual(1, add_bp.call_count) + + def test_add_routes_helper(self): + add_rt_p = mock.patch.object(self.bgp_dr, + 'advertise_route_via_bgp_speaker') + add_bp = add_rt_p.start() + self.bgp_dr.add_routes_helper(FAKE_BGP_SPEAKER['id'], FAKE_ROUTES) + self.assertEqual(1, add_bp.call_count) + + def test_bgp_speaker_remove_end(self): + payload = {'bgp_speaker': {'id': FAKE_BGPSPEAKER_UUID}} + + with mock.patch.object(self.bgp_dr, + 'remove_bgp_speaker_from_dragent') as disable: + self.bgp_dr.bgp_speaker_remove_end(None, payload) + disable.assert_called_once_with(FAKE_BGP_SPEAKER['id']) + + def test_bgp_peer_disassociation_end(self): + payload = {'bgp_peer': {'speaker_id': FAKE_BGPSPEAKER_UUID, + 'peer_ip': '1.1.1.1'}} + + with mock.patch.object(self.bgp_dr, + 'remove_bgp_peer_from_bgp_speaker') as disable: + self.bgp_dr.bgp_peer_disassociation_end(None, payload) + disable.assert_called_once_with(FAKE_BGPSPEAKER_UUID, + FAKE_BGP_PEER['peer_ip']) + + def test_bgp_routes_withdrawal_end(self): + withdraw_routes = [{'destination': '2.2.2.2/32'}, + {'destination': '3.3.3.3/32'}] + payload = {'withdraw_routes': {'speaker_id': FAKE_BGPSPEAKER_UUID, + 'routes': withdraw_routes}} + + expected_calls = [mock.call(FAKE_BGP_SPEAKER['id'], withdraw_routes)] + + with mock.patch.object(self.bgp_dr, + 'withdraw_routes_helper') as disable: + self.bgp_dr.bgp_routes_withdrawal_end(None, payload) + disable.assert_has_calls(expected_calls) + + +class TestBGPSpeakerCache(base.BaseTestCase): + + def setUp(self): + super(TestBGPSpeakerCache, self).setUp() + self.expected_cache = {FAKE_BGP_SPEAKER['id']: + {'bgp_speaker': FAKE_BGP_SPEAKER, + 'peers': {}, + 'advertised_routes': []}} + self.bs_cache = bgp_dragent.BgpSpeakerCache() + + def test_put_bgp_speaker(self): + self.bs_cache.put_bgp_speaker(FAKE_BGP_SPEAKER) + self.assertEqual(self.expected_cache, self.bs_cache.cache) + + def test_put_bgp_speaker_existing(self): + prev_bs_info = {'id': 'foo-id'} + with mock.patch.object(self.bs_cache, + 'remove_bgp_speaker_by_id') as remove: + self.bs_cache.cache[FAKE_BGP_SPEAKER['id']] = prev_bs_info + self.bs_cache.put_bgp_speaker(FAKE_BGP_SPEAKER) + remove.assert_called_once_with(prev_bs_info) + self.assertEqual(self.expected_cache, self.bs_cache.cache) + + def remove_bgp_speaker_by_id(self): + self.bs_cache.put_bgp_speaker(FAKE_BGP_SPEAKER) + self.assertEqual(1, len(self.bs_cache.cache)) + self.bs_cache.remove_bgp_speaker_by_id(FAKE_BGP_SPEAKER['id']) + self.assertEqual(0, len(self.bs_cache.cache)) + + def test_get_bgp_speaker_by_id(self): + self.bs_cache.put_bgp_speaker(FAKE_BGP_SPEAKER) + + self.assertEqual( + FAKE_BGP_SPEAKER, + self.bs_cache.get_bgp_speaker_by_id(FAKE_BGP_SPEAKER['id'])) + + def test_get_bgp_speaker_ids(self): + self.bs_cache.put_bgp_speaker(FAKE_BGP_SPEAKER) + + self.assertEqual([FAKE_BGP_SPEAKER['id']], + list(self.bs_cache.get_bgp_speaker_ids())) + + def _test_bgp_peer_helper(self, remove=False): + self.bs_cache.put_bgp_speaker(FAKE_BGP_SPEAKER) + self.bs_cache.put_bgp_peer(FAKE_BGP_SPEAKER['id'], FAKE_BGP_PEER) + expected_cache = copy.deepcopy(self.expected_cache) + expected_cache[FAKE_BGP_SPEAKER['id']]['peers'] = { + FAKE_BGP_PEER['peer_ip']: FAKE_BGP_PEER} + self.assertEqual(expected_cache, self.bs_cache.cache) + + if remove: + self.bs_cache.remove_bgp_peer_by_ip(FAKE_BGP_SPEAKER['id'], + 'foo-ip') + self.assertEqual(expected_cache, self.bs_cache.cache) + + self.bs_cache.remove_bgp_peer_by_ip(FAKE_BGP_SPEAKER['id'], + FAKE_BGP_PEER['peer_ip']) + self.assertEqual(self.expected_cache, self.bs_cache.cache) + + def test_put_bgp_peer(self): + self._test_bgp_peer_helper() + + def test_remove_bgp_peer(self): + self._test_bgp_peer_helper(remove=True) + + def _test_bgp_speaker_adv_route_helper(self, remove=False): + self.bs_cache.put_bgp_speaker(FAKE_BGP_SPEAKER) + self.bs_cache.put_adv_route(FAKE_BGP_SPEAKER['id'], FAKE_ROUTE) + expected_cache = copy.deepcopy(self.expected_cache) + expected_cache[FAKE_BGP_SPEAKER['id']]['advertised_routes'].append( + FAKE_ROUTE) + self.assertEqual(expected_cache, self.bs_cache.cache) + + fake_route_2 = copy.deepcopy(FAKE_ROUTE) + fake_route_2['destination'] = '4.4.4.4/32' + self.bs_cache.put_adv_route(FAKE_BGP_SPEAKER['id'], fake_route_2) + + expected_cache[FAKE_BGP_SPEAKER['id']]['advertised_routes'].append( + fake_route_2) + self.assertEqual(expected_cache, self.bs_cache.cache) + + if remove: + self.bs_cache.remove_adv_route(FAKE_BGP_SPEAKER['id'], + fake_route_2) + expected_cache[FAKE_BGP_SPEAKER['id']]['advertised_routes'] = ( + [FAKE_ROUTE]) + self.assertEqual(expected_cache, self.bs_cache.cache) + + self.bs_cache.remove_adv_route(FAKE_BGP_SPEAKER['id'], + FAKE_ROUTE) + self.assertEqual(self.expected_cache, self.bs_cache.cache) + + def test_put_bgp_speaker_adv_route(self): + self._test_bgp_speaker_adv_route_helper() + + def test_remove_bgp_speaker_adv_route(self): + self._test_bgp_speaker_adv_route_helper(remove=True) + + def test_is_bgp_speaker_adv_route_present(self): + self._test_bgp_speaker_adv_route_helper() + self.assertTrue(self.bs_cache.is_route_advertised( + FAKE_BGP_SPEAKER['id'], FAKE_ROUTE)) + self.assertFalse(self.bs_cache.is_route_advertised( + FAKE_BGP_SPEAKER['id'], {'destination': 'foo-destination', + 'next_hop': 'foo-next-hop'})) diff --git a/neutron_dynamic_routing/tests/unit/services/bgp/driver/__init__.py b/neutron_dynamic_routing/tests/unit/services/bgp/driver/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/unit/services/bgp/driver/ryu/__init__.py b/neutron_dynamic_routing/tests/unit/services/bgp/driver/ryu/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/unit/services/bgp/driver/ryu/test_driver.py b/neutron_dynamic_routing/tests/unit/services/bgp/driver/ryu/test_driver.py new file mode 100644 index 00000000..971a4d5f --- /dev/null +++ b/neutron_dynamic_routing/tests/unit/services/bgp/driver/ryu/test_driver.py @@ -0,0 +1,251 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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 mock +from oslo_config import cfg +from ryu.services.protocols.bgp import bgpspeaker +from ryu.services.protocols.bgp.rtconf.neighbors import CONNECT_MODE_ACTIVE + +from neutron.tests import base + +from neutron_dynamic_routing.services.bgp.agent import config as bgp_config +from neutron_dynamic_routing.services.bgp.agent.driver import exceptions as bgp_driver_exc # noqa +from neutron_dynamic_routing.services.bgp.agent.driver.ryu import driver as ryu_driver # noqa + +# Test variables for BGP Speaker +FAKE_LOCAL_AS1 = 12345 +FAKE_LOCAL_AS2 = 23456 +FAKE_ROUTER_ID = '1.1.1.1' + +# Test variables for BGP Peer +FAKE_PEER_AS = 45678 +FAKE_PEER_IP = '2.2.2.5' +FAKE_AUTH_TYPE = 'md5' +FAKE_PEER_PASSWORD = 'awesome' + +# Test variables for Route +FAKE_ROUTE = '2.2.2.0/24' +FAKE_NEXTHOP = '5.5.5.5' + + +class TestRyuBgpDriver(base.BaseTestCase): + + def setUp(self): + super(TestRyuBgpDriver, self).setUp() + cfg.CONF.register_opts(bgp_config.BGP_PROTO_CONFIG_OPTS, 'BGP') + cfg.CONF.set_override('bgp_router_id', FAKE_ROUTER_ID, 'BGP') + self.ryu_bgp_driver = ryu_driver.RyuBgpDriver(cfg.CONF.BGP) + mock_ryu_speaker_p = mock.patch.object(bgpspeaker, 'BGPSpeaker') + self.mock_ryu_speaker = mock_ryu_speaker_p.start() + + def test_add_new_bgp_speaker(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertEqual(1, + self.ryu_bgp_driver.cache.get_hosted_bgp_speakers_count()) + self.mock_ryu_speaker.assert_called_once_with( + as_number=FAKE_LOCAL_AS1, router_id=FAKE_ROUTER_ID, + bgp_server_port=0, + best_path_change_handler=ryu_driver.best_path_change_cb, + peer_down_handler=ryu_driver.bgp_peer_down_cb, + peer_up_handler=ryu_driver.bgp_peer_up_cb) + + def test_remove_bgp_speaker(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertEqual(1, + self.ryu_bgp_driver.cache.get_hosted_bgp_speakers_count()) + speaker = self.ryu_bgp_driver.cache.get_bgp_speaker(FAKE_LOCAL_AS1) + self.ryu_bgp_driver.delete_bgp_speaker(FAKE_LOCAL_AS1) + self.assertEqual(0, + self.ryu_bgp_driver.cache.get_hosted_bgp_speakers_count()) + self.assertEqual(1, speaker.shutdown.call_count) + + def test_add_bgp_peer_without_password(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertEqual(1, + self.ryu_bgp_driver.cache.get_hosted_bgp_speakers_count()) + self.ryu_bgp_driver.add_bgp_peer(FAKE_LOCAL_AS1, + FAKE_PEER_IP, + FAKE_PEER_AS) + speaker = self.ryu_bgp_driver.cache.get_bgp_speaker(FAKE_LOCAL_AS1) + speaker.neighbor_add.assert_called_once_with( + address=FAKE_PEER_IP, + remote_as=FAKE_PEER_AS, + password=None, + connect_mode=CONNECT_MODE_ACTIVE) + + def test_add_bgp_peer_with_password(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertEqual(1, + self.ryu_bgp_driver.cache.get_hosted_bgp_speakers_count()) + self.ryu_bgp_driver.add_bgp_peer(FAKE_LOCAL_AS1, + FAKE_PEER_IP, + FAKE_PEER_AS, + FAKE_AUTH_TYPE, + FAKE_PEER_PASSWORD) + speaker = self.ryu_bgp_driver.cache.get_bgp_speaker(FAKE_LOCAL_AS1) + speaker.neighbor_add.assert_called_once_with( + address=FAKE_PEER_IP, + remote_as=FAKE_PEER_AS, + password=FAKE_PEER_PASSWORD, + connect_mode=CONNECT_MODE_ACTIVE) + + def test_remove_bgp_peer(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertEqual(1, + self.ryu_bgp_driver.cache.get_hosted_bgp_speakers_count()) + self.ryu_bgp_driver.delete_bgp_peer(FAKE_LOCAL_AS1, FAKE_PEER_IP) + speaker = self.ryu_bgp_driver.cache.get_bgp_speaker(FAKE_LOCAL_AS1) + speaker.neighbor_del.assert_called_once_with(address=FAKE_PEER_IP) + + def test_advertise_route(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertEqual(1, + self.ryu_bgp_driver.cache.get_hosted_bgp_speakers_count()) + self.ryu_bgp_driver.advertise_route(FAKE_LOCAL_AS1, + FAKE_ROUTE, + FAKE_NEXTHOP) + speaker = self.ryu_bgp_driver.cache.get_bgp_speaker(FAKE_LOCAL_AS1) + speaker.prefix_add.assert_called_once_with(prefix=FAKE_ROUTE, + next_hop=FAKE_NEXTHOP) + + def test_withdraw_route(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertEqual(1, + self.ryu_bgp_driver.cache.get_hosted_bgp_speakers_count()) + self.ryu_bgp_driver.withdraw_route(FAKE_LOCAL_AS1, FAKE_ROUTE) + speaker = self.ryu_bgp_driver.cache.get_bgp_speaker(FAKE_LOCAL_AS1) + speaker.prefix_del.assert_called_once_with(prefix=FAKE_ROUTE) + + def test_add_same_bgp_speakers_twice(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertRaises(bgp_driver_exc.BgpSpeakerAlreadyScheduled, + self.ryu_bgp_driver.add_bgp_speaker, FAKE_LOCAL_AS1) + + def test_add_different_bgp_speakers_when_one_already_added(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertRaises(bgp_driver_exc.BgpSpeakerMaxScheduled, + self.ryu_bgp_driver.add_bgp_speaker, + FAKE_LOCAL_AS2) + + def test_add_bgp_speaker_with_invalid_asnum_paramtype(self): + self.assertRaises(bgp_driver_exc.InvalidParamType, + self.ryu_bgp_driver.add_bgp_speaker, '12345') + + def test_add_bgp_speaker_with_invalid_asnum_range(self): + self.assertRaises(bgp_driver_exc.InvalidParamRange, + self.ryu_bgp_driver.add_bgp_speaker, -1) + self.assertRaises(bgp_driver_exc.InvalidParamRange, + self.ryu_bgp_driver.add_bgp_speaker, 65536) + + def test_add_bgp_peer_with_invalid_paramtype(self): + # Test with an invalid asnum data-type + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertRaises(bgp_driver_exc.InvalidParamType, + self.ryu_bgp_driver.add_bgp_peer, + FAKE_LOCAL_AS1, FAKE_PEER_IP, '12345') + # Test with an invalid auth-type and an invalid password + self.assertRaises(bgp_driver_exc.InvalidParamType, + self.ryu_bgp_driver.add_bgp_peer, + FAKE_LOCAL_AS1, FAKE_PEER_IP, FAKE_PEER_AS, + 'sha-1', 1234) + # Test with an invalid auth-type and a valid password + self.assertRaises(bgp_driver_exc.InvaildAuthType, + self.ryu_bgp_driver.add_bgp_peer, + FAKE_LOCAL_AS1, FAKE_PEER_IP, FAKE_PEER_AS, + 'hmac-md5', FAKE_PEER_PASSWORD) + # Test with none auth-type and a valid password + self.assertRaises(bgp_driver_exc.InvaildAuthType, + self.ryu_bgp_driver.add_bgp_peer, + FAKE_LOCAL_AS1, FAKE_PEER_IP, FAKE_PEER_AS, + 'none', FAKE_PEER_PASSWORD) + # Test with none auth-type and an invalid password + self.assertRaises(bgp_driver_exc.InvalidParamType, + self.ryu_bgp_driver.add_bgp_peer, + FAKE_LOCAL_AS1, FAKE_PEER_IP, FAKE_PEER_AS, + 'none', 1234) + # Test with a valid auth-type and no password + self.assertRaises(bgp_driver_exc.PasswordNotSpecified, + self.ryu_bgp_driver.add_bgp_peer, + FAKE_LOCAL_AS1, FAKE_PEER_IP, FAKE_PEER_AS, + FAKE_AUTH_TYPE, None) + + def test_add_bgp_peer_with_invalid_asnum_range(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertRaises(bgp_driver_exc.InvalidParamRange, + self.ryu_bgp_driver.add_bgp_peer, + FAKE_LOCAL_AS1, FAKE_PEER_IP, -1) + self.assertRaises(bgp_driver_exc.InvalidParamRange, + self.ryu_bgp_driver.add_bgp_peer, + FAKE_LOCAL_AS1, FAKE_PEER_IP, 65536) + + def test_add_bgp_peer_without_adding_speaker(self): + self.assertRaises(bgp_driver_exc.BgpSpeakerNotAdded, + self.ryu_bgp_driver.add_bgp_peer, + FAKE_LOCAL_AS1, FAKE_PEER_IP, FAKE_PEER_AS) + + def test_remove_bgp_peer_with_invalid_paramtype(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertRaises(bgp_driver_exc.InvalidParamType, + self.ryu_bgp_driver.delete_bgp_peer, + FAKE_LOCAL_AS1, 12345) + + def test_remove_bgp_peer_without_adding_speaker(self): + self.assertRaises(bgp_driver_exc.BgpSpeakerNotAdded, + self.ryu_bgp_driver.delete_bgp_peer, + FAKE_LOCAL_AS1, FAKE_PEER_IP) + + def test_advertise_route_with_invalid_paramtype(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertRaises(bgp_driver_exc.InvalidParamType, + self.ryu_bgp_driver.advertise_route, + FAKE_LOCAL_AS1, 12345, FAKE_NEXTHOP) + self.assertRaises(bgp_driver_exc.InvalidParamType, + self.ryu_bgp_driver.advertise_route, + FAKE_LOCAL_AS1, FAKE_ROUTE, 12345) + + def test_advertise_route_without_adding_speaker(self): + self.assertRaises(bgp_driver_exc.BgpSpeakerNotAdded, + self.ryu_bgp_driver.advertise_route, + FAKE_LOCAL_AS1, FAKE_ROUTE, FAKE_NEXTHOP) + + def test_withdraw_route_with_invalid_paramtype(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertRaises(bgp_driver_exc.InvalidParamType, + self.ryu_bgp_driver.withdraw_route, + FAKE_LOCAL_AS1, 12345) + self.assertRaises(bgp_driver_exc.InvalidParamType, + self.ryu_bgp_driver.withdraw_route, + FAKE_LOCAL_AS1, 12345) + + def test_withdraw_route_without_adding_speaker(self): + self.assertRaises(bgp_driver_exc.BgpSpeakerNotAdded, + self.ryu_bgp_driver.withdraw_route, + FAKE_LOCAL_AS1, FAKE_ROUTE) + + def test_add_multiple_bgp_speakers(self): + self.ryu_bgp_driver.add_bgp_speaker(FAKE_LOCAL_AS1) + self.assertEqual(1, + self.ryu_bgp_driver.cache.get_hosted_bgp_speakers_count()) + self.assertRaises(bgp_driver_exc.BgpSpeakerMaxScheduled, + self.ryu_bgp_driver.add_bgp_speaker, + FAKE_LOCAL_AS2) + self.assertRaises(bgp_driver_exc.BgpSpeakerNotAdded, + self.ryu_bgp_driver.delete_bgp_speaker, + FAKE_LOCAL_AS2) + self.assertEqual(1, + self.ryu_bgp_driver.cache.get_hosted_bgp_speakers_count()) + self.ryu_bgp_driver.delete_bgp_speaker(FAKE_LOCAL_AS1) + self.assertEqual(0, + self.ryu_bgp_driver.cache.get_hosted_bgp_speakers_count()) diff --git a/neutron_dynamic_routing/tests/unit/services/bgp/driver/test_utils.py b/neutron_dynamic_routing/tests/unit/services/bgp/driver/test_utils.py new file mode 100644 index 00000000..1f2abfe8 --- /dev/null +++ b/neutron_dynamic_routing/tests/unit/services/bgp/driver/test_utils.py @@ -0,0 +1,49 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# +# 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.tests import base + +from neutron_dynamic_routing.services.bgp.agent.driver import utils as bgp_driver_utils # noqa + +FAKE_LOCAL_AS = 12345 +FAKE_RYU_SPEAKER = {} + + +class TestBgpMultiSpeakerCache(base.BaseTestCase): + + def setUp(self): + super(TestBgpMultiSpeakerCache, self).setUp() + self.expected_cache = {FAKE_LOCAL_AS: FAKE_RYU_SPEAKER} + self.bs_cache = bgp_driver_utils.BgpMultiSpeakerCache() + + def test_put_bgp_speaker(self): + self.bs_cache.put_bgp_speaker(FAKE_LOCAL_AS, FAKE_RYU_SPEAKER) + self.assertEqual(self.expected_cache, self.bs_cache.cache) + + def test_remove_bgp_speaker(self): + self.bs_cache.put_bgp_speaker(FAKE_LOCAL_AS, FAKE_RYU_SPEAKER) + self.assertEqual(1, len(self.bs_cache.cache)) + self.bs_cache.remove_bgp_speaker(FAKE_LOCAL_AS) + self.assertEqual(0, len(self.bs_cache.cache)) + + def test_get_bgp_speaker(self): + self.bs_cache.put_bgp_speaker(FAKE_LOCAL_AS, FAKE_RYU_SPEAKER) + self.assertEqual( + FAKE_RYU_SPEAKER, + self.bs_cache.get_bgp_speaker(FAKE_LOCAL_AS)) + + def test_get_hosted_bgp_speakers_count(self): + self.bs_cache.put_bgp_speaker(FAKE_LOCAL_AS, FAKE_RYU_SPEAKER) + self.assertEqual(1, self.bs_cache.get_hosted_bgp_speakers_count()) diff --git a/neutron_dynamic_routing/tests/unit/services/bgp/scheduler/__init__.py b/neutron_dynamic_routing/tests/unit/services/bgp/scheduler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_dynamic_routing/tests/unit/services/bgp/scheduler/test_bgp_dragent_scheduler.py b/neutron_dynamic_routing/tests/unit/services/bgp/scheduler/test_bgp_dragent_scheduler.py new file mode 100644 index 00000000..a8a6a10b --- /dev/null +++ b/neutron_dynamic_routing/tests/unit/services/bgp/scheduler/test_bgp_dragent_scheduler.py @@ -0,0 +1,225 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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. + +import testscenarios + +from oslo_utils import importutils + +from neutron import context +from neutron.tests.unit import testlib_api + +from neutron_dynamic_routing.db import bgp_db +from neutron_dynamic_routing.db import bgp_dragentscheduler_db as bgp_dras_db +from neutron_dynamic_routing.services.bgp.scheduler import bgp_dragent_scheduler as bgp_dras # noqa +from neutron_dynamic_routing.tests.common import helpers + +# Required to generate tests from scenarios. Not compatible with nose. +load_tests = testscenarios.load_tests_apply_scenarios + + +class TestBgpDrAgentSchedulerBaseTestCase(testlib_api.SqlTestCase): + + def setUp(self): + super(TestBgpDrAgentSchedulerBaseTestCase, self).setUp() + self.ctx = context.get_admin_context() + self.bgp_speaker = {'id': 'foo_bgp_speaker_id'} + self.bgp_speaker_id = 'foo_bgp_speaker_id' + self._save_bgp_speaker(self.bgp_speaker_id) + + def _create_and_set_agents_down(self, hosts, down_agent_count=0, + admin_state_up=True): + agents = [] + for i, host in enumerate(hosts): + is_alive = i >= down_agent_count + agents.append(helpers.register_bgp_dragent( + host, + admin_state_up=admin_state_up, + alive=is_alive)) + return agents + + def _save_bgp_speaker(self, bgp_speaker_id): + cls = bgp_db.BgpDbMixin() + bgp_speaker_body = {'bgp_speaker': { + 'name': 'fake_bgp_speaker', + 'ip_version': '4', + 'local_as': '123', + 'advertise_floating_ip_host_routes': '0', + 'advertise_tenant_networks': '0', + 'peers': [], + 'networks': []}} + cls._save_bgp_speaker(self.ctx, bgp_speaker_body, uuid=bgp_speaker_id) + + def _test_schedule_bind_bgp_speaker(self, agents, bgp_speaker_id): + scheduler = bgp_dras.ChanceScheduler() + scheduler.resource_filter.bind(self.ctx, agents, bgp_speaker_id) + results = self.ctx.session.query( + bgp_dras_db.BgpSpeakerDrAgentBinding).filter_by( + bgp_speaker_id=bgp_speaker_id).all() + + for result in results: + self.assertEqual(bgp_speaker_id, result.bgp_speaker_id) + + +class TestBgpDrAgentScheduler(TestBgpDrAgentSchedulerBaseTestCase, + bgp_db.BgpDbMixin): + + def test_schedule_bind_bgp_speaker_single_agent(self): + agents = self._create_and_set_agents_down(['host-a']) + self._test_schedule_bind_bgp_speaker(agents, self.bgp_speaker_id) + + def test_schedule_bind_bgp_speaker_multi_agents(self): + agents = self._create_and_set_agents_down(['host-a', 'host-b']) + self._test_schedule_bind_bgp_speaker(agents, self.bgp_speaker_id) + + +class TestBgpAgentFilter(TestBgpDrAgentSchedulerBaseTestCase, + bgp_db.BgpDbMixin, + bgp_dras_db.BgpDrAgentSchedulerDbMixin): + + def setUp(self): + super(TestBgpAgentFilter, self).setUp() + self.bgp_drscheduler = importutils.import_object( + 'neutron_dynamic_routing.services.bgp.scheduler.' + 'bgp_dragent_scheduler.ChanceScheduler' + ) + self.plugin = self + + def _test_filter_agents_helper(self, bgp_speaker, + expected_filtered_dragent_ids=None, + expected_num_agents=1): + filtered_agents = ( + self.plugin.bgp_drscheduler.resource_filter.filter_agents( + self.plugin, self.ctx, bgp_speaker)) + self.assertEqual(expected_num_agents, + filtered_agents['n_agents']) + actual_filtered_dragent_ids = [ + agent.id for agent in filtered_agents['hostable_agents']] + if expected_filtered_dragent_ids is None: + expected_filtered_dragent_ids = [] + self.assertEqual(len(expected_filtered_dragent_ids), + len(actual_filtered_dragent_ids)) + for filtered_agent_id in actual_filtered_dragent_ids: + self.assertIn(filtered_agent_id, expected_filtered_dragent_ids) + + def test_filter_agents_single_agent(self): + agents = self._create_and_set_agents_down(['host-a']) + expected_filtered_dragent_ids = [agents[0].id] + self._test_filter_agents_helper( + self.bgp_speaker, + expected_filtered_dragent_ids=expected_filtered_dragent_ids) + + def test_filter_agents_no_agents(self): + expected_filtered_dragent_ids = [] + self._test_filter_agents_helper( + self.bgp_speaker, + expected_filtered_dragent_ids=expected_filtered_dragent_ids, + expected_num_agents=0) + + def test_filter_agents_two_agents(self): + agents = self._create_and_set_agents_down(['host-a', 'host-b']) + expected_filtered_dragent_ids = [agent.id for agent in agents] + self._test_filter_agents_helper( + self.bgp_speaker, + expected_filtered_dragent_ids=expected_filtered_dragent_ids) + + def test_filter_agents_agent_already_scheduled(self): + agents = self._create_and_set_agents_down(['host-a', 'host-b']) + self._test_schedule_bind_bgp_speaker([agents[0]], self.bgp_speaker_id) + self._test_filter_agents_helper(self.bgp_speaker, + expected_num_agents=0) + + def test_filter_agents_multiple_agents_bgp_speakers(self): + agents = self._create_and_set_agents_down(['host-a', 'host-b']) + self._test_schedule_bind_bgp_speaker([agents[0]], self.bgp_speaker_id) + bgp_speaker = {'id': 'bar-speaker-id'} + self._save_bgp_speaker(bgp_speaker['id']) + expected_filtered_dragent_ids = [agents[1].id] + self._test_filter_agents_helper( + bgp_speaker, + expected_filtered_dragent_ids=expected_filtered_dragent_ids) + + +class TestAutoScheduleBgpSpeakers(TestBgpDrAgentSchedulerBaseTestCase): + """Unit test scenarios for schedule_unscheduled_bgp_speakers. + + bgp_speaker_present + BGP speaker is present or not + + scheduled_already + BGP speaker is already scheduled to the agent or not + + agent_down + BGP DRAgent is down or alive + + valid_host + If true, then an valid host is passed to schedule BGP speaker, + else an invalid host is passed. + """ + scenarios = [ + ('BGP speaker present', + dict(bgp_speaker_present=True, + scheduled_already=False, + agent_down=False, + valid_host=True, + expected_result=True)), + + ('No BGP speaker', + dict(bgp_speaker_present=False, + scheduled_already=False, + agent_down=False, + valid_host=True, + expected_result=False)), + + ('BGP speaker already scheduled', + dict(bgp_speaker_present=True, + scheduled_already=True, + agent_down=False, + valid_host=True, + expected_result=False)), + + ('BGP DR agent down', + dict(bgp_speaker_present=True, + scheduled_already=False, + agent_down=True, + valid_host=False, + expected_result=False)), + + ('Invalid host', + dict(bgp_speaker_present=True, + scheduled_already=False, + agent_down=False, + valid_host=False, + expected_result=False)), + ] + + def test_auto_schedule_bgp_speaker(self): + scheduler = bgp_dras.ChanceScheduler() + if self.bgp_speaker_present: + down_agent_count = 1 if self.agent_down else 0 + agents = self._create_and_set_agents_down( + ['host-a'], down_agent_count=down_agent_count) + if self.scheduled_already: + self._test_schedule_bind_bgp_speaker(agents, + self.bgp_speaker_id) + + expected_hosted_agents = (1 if self.bgp_speaker_present and + self.valid_host else 0) + host = "host-a" if self.valid_host else "host-b" + observed_ret_value = scheduler.schedule_unscheduled_bgp_speakers( + self.ctx, host) + self.assertEqual(self.expected_result, observed_ret_value) + hosted_agents = self.ctx.session.query( + bgp_dras_db.BgpSpeakerDrAgentBinding).all() + self.assertEqual(expected_hosted_agents, len(hosted_agents)) diff --git a/setup.cfg b/setup.cfg index 37dbef8e..08eac267 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,8 @@ setup-hooks = [entry_points] neutron.db.alembic_migrations = neutron-dynamic-routing = neutron_dynamic_routing.db.migration:alembic_migrations +oslo.config.opts = + bgp.agent = neutron_dynamic_routing.services.bgp.common.opts:list_bgp_agent_opts [build_sphinx] all_files = 1 diff --git a/tox.ini b/tox.ini index 26fd93f2..c3688590 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,34 @@ commands = # there is also secret magic in pretty_tox.sh which lets you run in a fail only # mode. To do this define the TRACE_FAILONLY environmental variable. +[testenv:functional] +setenv = OS_TEST_PATH=./neutron_dynamic_routing/tests/functional +commands = + python setup.py testr --slowest --testr-args='{posargs}' + +[testenv:api] +sitepackages=True +setenv = + OS_TEST_PATH=./neutron_dynamic_routing/tests/api/ + OS_TESTR_CONCURRENCY=1 + TEMPEST_CONFIG_DIR={env:TEMPEST_CONFIG_DIR:/opt/stack/tempest/etc} +commands = + python setup.py testr --slowest --testr-args='{posargs}' + +[testenv:dsvm-functional] +setenv = + OS_TEST_PATH=./neutron_dynamic_routing/tests/functional + OS_SUDO_TESTING=1 + OS_ROOTWRAP_CMD=sudo {envdir}/bin/neutron-rootwrap {envdir}/etc/neutron/rootwrap.conf + OS_ROOTWRAP_DAEMON_CMD=sudo {envdir}/bin/neutron-rootwrap-daemon {envdir}/etc/neutron/rootwrap.conf + OS_FAIL_ON_MISSING_DEPS=1 +whitelist_externals = + sh + cp + sudo +commands = + python setup.py testr --slowest --testr-args='{posargs}' + [testenv:releasenotes] # TODO(ihrachys): remove once infra supports constraints for this target install_command = {toxinidir}/tools/tox_install.sh unconstrained {opts} {packages}