From 270b37f22b03469a2d5087e584864d06737bfc6c Mon Sep 17 00:00:00 2001 From: Ryan Tidwell Date: Tue, 26 Apr 2016 09:56:44 -0700 Subject: [PATCH] Move BGP service plugin, agent, and tests out of Neutron repo This patch moves the BGP service plugin, agent, driver, and tests out of the neutron repository and into the neutron-dynamic-routing repository. Partially-Implements: blueprint bgp-spinout Partial-Bug: #1560003 Co-Authored-By: vikram.choudhary Change-Id: I80ea28a51d7b18e67d6ed4cd2da22520f950300f --- neutron_dynamic_routing/api/__init__.py | 0 neutron_dynamic_routing/api/rpc/__init__.py | 0 .../api/rpc/agentnotifiers/__init__.py | 0 .../agentnotifiers/bgp_dr_rpc_agent_api.py | 107 ++ .../api/rpc/handlers/__init__.py | 0 .../api/rpc/handlers/bgp_speaker_rpc.py | 66 ++ neutron_dynamic_routing/cmd/__init__.py | 0 .../eventlet/__init__.py} | 19 +- .../cmd/eventlet/agents/__init__.py | 0 .../cmd/eventlet/agents/bgp_dragent.py | 20 + neutron_dynamic_routing/db/bgp_db.py | 1011 ++++++++++++++++ .../db/bgp_dragentscheduler_db.py | 216 ++++ .../db/migration/models/head.py | 3 + .../extensions/__init__.py | 0 neutron_dynamic_routing/extensions/bgp.py | 209 ++++ .../extensions/bgp_dragentscheduler.py | 184 +++ neutron_dynamic_routing/services/__init__.py | 0 .../services/bgp/__init__.py | 0 .../services/bgp/agent/__init__.py | 0 .../services/bgp/agent/bgp_dragent.py | 708 +++++++++++ .../services/bgp/agent/config.py | 29 + .../services/bgp/agent/driver/__init__.py | 0 .../services/bgp/agent/driver/base.py | 142 +++ .../services/bgp/agent/driver/exceptions.py | 62 + .../services/bgp/agent/driver/ryu/__init__.py | 0 .../services/bgp/agent/driver/ryu/driver.py | 202 ++++ .../services/bgp/agent/driver/utils.py | 75 ++ .../services/bgp/agent/entry.py | 48 + .../services/bgp/bgp_plugin.py | 390 ++++++ .../services/bgp/common/__init__.py | 0 .../services/bgp/common/constants.py | 27 + .../services/bgp/common/opts.py | 30 + .../services/bgp/scheduler/__init__.py | 0 .../bgp/scheduler/bgp_dragent_scheduler.py | 192 +++ .../tests/api/test_bgp_speaker_extensions.py | 288 +++++ .../test_bgp_speaker_extensions_negative.py | 121 ++ .../tests/common/__init__.py | 0 .../tests/common/helpers.py | 42 + .../tests/functional/services/__init__.py | 0 .../tests/functional/services/bgp/__init__.py | 0 .../services/bgp/scheduler/__init__.py | 0 .../scheduler/test_bgp_dragent_scheduler.py | 209 ++++ .../tests/unit/api/__init__.py | 0 .../tests/unit/api/rpc/__init__.py | 0 .../unit/api/rpc/agentnotifiers/__init__.py | 0 .../test_bgp_dr_rpc_agent_api.py | 84 ++ .../tests/unit/api/rpc/handlers/__init__.py | 0 .../api/rpc/handlers/test_bgp_speaker_rpc.py | 45 + .../tests/unit/db/__init__.py | 0 .../tests/unit/db/test_bgp_db.py | 1047 +++++++++++++++++ .../unit/db/test_bgp_dragentscheduler_db.py | 206 ++++ .../tests/unit/services/__init__.py | 0 .../tests/unit/services/bgp/__init__.py | 0 .../tests/unit/services/bgp/agent/__init__.py | 0 .../services/bgp/agent/test_bgp_dragent.py | 748 ++++++++++++ .../unit/services/bgp/driver/__init__.py | 0 .../unit/services/bgp/driver/ryu/__init__.py | 0 .../services/bgp/driver/ryu/test_driver.py | 251 ++++ .../unit/services/bgp/driver/test_utils.py | 49 + .../unit/services/bgp/scheduler/__init__.py | 0 .../scheduler/test_bgp_dragent_scheduler.py | 225 ++++ setup.cfg | 2 + tox.ini | 28 + 63 files changed, 7068 insertions(+), 17 deletions(-) create mode 100644 neutron_dynamic_routing/api/__init__.py create mode 100644 neutron_dynamic_routing/api/rpc/__init__.py create mode 100644 neutron_dynamic_routing/api/rpc/agentnotifiers/__init__.py create mode 100644 neutron_dynamic_routing/api/rpc/agentnotifiers/bgp_dr_rpc_agent_api.py create mode 100644 neutron_dynamic_routing/api/rpc/handlers/__init__.py create mode 100644 neutron_dynamic_routing/api/rpc/handlers/bgp_speaker_rpc.py create mode 100644 neutron_dynamic_routing/cmd/__init__.py rename neutron_dynamic_routing/{tests/unit/test_neutron_dynamic_routing.py => cmd/eventlet/__init__.py} (55%) create mode 100644 neutron_dynamic_routing/cmd/eventlet/agents/__init__.py create mode 100644 neutron_dynamic_routing/cmd/eventlet/agents/bgp_dragent.py create mode 100644 neutron_dynamic_routing/db/bgp_db.py create mode 100644 neutron_dynamic_routing/db/bgp_dragentscheduler_db.py create mode 100644 neutron_dynamic_routing/extensions/__init__.py create mode 100644 neutron_dynamic_routing/extensions/bgp.py create mode 100644 neutron_dynamic_routing/extensions/bgp_dragentscheduler.py create mode 100644 neutron_dynamic_routing/services/__init__.py create mode 100644 neutron_dynamic_routing/services/bgp/__init__.py create mode 100644 neutron_dynamic_routing/services/bgp/agent/__init__.py create mode 100644 neutron_dynamic_routing/services/bgp/agent/bgp_dragent.py create mode 100644 neutron_dynamic_routing/services/bgp/agent/config.py create mode 100644 neutron_dynamic_routing/services/bgp/agent/driver/__init__.py create mode 100644 neutron_dynamic_routing/services/bgp/agent/driver/base.py create mode 100644 neutron_dynamic_routing/services/bgp/agent/driver/exceptions.py create mode 100644 neutron_dynamic_routing/services/bgp/agent/driver/ryu/__init__.py create mode 100644 neutron_dynamic_routing/services/bgp/agent/driver/ryu/driver.py create mode 100644 neutron_dynamic_routing/services/bgp/agent/driver/utils.py create mode 100644 neutron_dynamic_routing/services/bgp/agent/entry.py create mode 100644 neutron_dynamic_routing/services/bgp/bgp_plugin.py create mode 100644 neutron_dynamic_routing/services/bgp/common/__init__.py create mode 100644 neutron_dynamic_routing/services/bgp/common/constants.py create mode 100644 neutron_dynamic_routing/services/bgp/common/opts.py create mode 100644 neutron_dynamic_routing/services/bgp/scheduler/__init__.py create mode 100644 neutron_dynamic_routing/services/bgp/scheduler/bgp_dragent_scheduler.py create mode 100644 neutron_dynamic_routing/tests/api/test_bgp_speaker_extensions.py create mode 100644 neutron_dynamic_routing/tests/api/test_bgp_speaker_extensions_negative.py create mode 100644 neutron_dynamic_routing/tests/common/__init__.py create mode 100644 neutron_dynamic_routing/tests/common/helpers.py create mode 100644 neutron_dynamic_routing/tests/functional/services/__init__.py create mode 100644 neutron_dynamic_routing/tests/functional/services/bgp/__init__.py create mode 100644 neutron_dynamic_routing/tests/functional/services/bgp/scheduler/__init__.py create mode 100644 neutron_dynamic_routing/tests/functional/services/bgp/scheduler/test_bgp_dragent_scheduler.py create mode 100644 neutron_dynamic_routing/tests/unit/api/__init__.py create mode 100644 neutron_dynamic_routing/tests/unit/api/rpc/__init__.py create mode 100644 neutron_dynamic_routing/tests/unit/api/rpc/agentnotifiers/__init__.py create mode 100644 neutron_dynamic_routing/tests/unit/api/rpc/agentnotifiers/test_bgp_dr_rpc_agent_api.py create mode 100644 neutron_dynamic_routing/tests/unit/api/rpc/handlers/__init__.py create mode 100644 neutron_dynamic_routing/tests/unit/api/rpc/handlers/test_bgp_speaker_rpc.py create mode 100644 neutron_dynamic_routing/tests/unit/db/__init__.py create mode 100644 neutron_dynamic_routing/tests/unit/db/test_bgp_db.py create mode 100644 neutron_dynamic_routing/tests/unit/db/test_bgp_dragentscheduler_db.py create mode 100644 neutron_dynamic_routing/tests/unit/services/__init__.py create mode 100644 neutron_dynamic_routing/tests/unit/services/bgp/__init__.py create mode 100644 neutron_dynamic_routing/tests/unit/services/bgp/agent/__init__.py create mode 100644 neutron_dynamic_routing/tests/unit/services/bgp/agent/test_bgp_dragent.py create mode 100644 neutron_dynamic_routing/tests/unit/services/bgp/driver/__init__.py create mode 100644 neutron_dynamic_routing/tests/unit/services/bgp/driver/ryu/__init__.py create mode 100644 neutron_dynamic_routing/tests/unit/services/bgp/driver/ryu/test_driver.py create mode 100644 neutron_dynamic_routing/tests/unit/services/bgp/driver/test_utils.py create mode 100644 neutron_dynamic_routing/tests/unit/services/bgp/scheduler/__init__.py create mode 100644 neutron_dynamic_routing/tests/unit/services/bgp/scheduler/test_bgp_dragent_scheduler.py 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 f1bb025f..d26932c9 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}