From 9d1d6a08db02a8697987b40ed60d923bdf79f050 Mon Sep 17 00:00:00 2001 From: Ryan Tidwell Date: Fri, 5 Feb 2016 14:31:03 -0800 Subject: [PATCH] Add BGP Dynamic Routing DB Model and Basic CRUD This patch enables basic CRUD on BGP dynamic routing entities bgp_speaker and bgp_peer, as well as bgp_speaker-bgp_peer and bgp_speaker-network bindings. An admin user can create BgpSpeakers and configure peering entities (BgpPeers) for BgpSpeakers. BgpSpeaker to BgpPeer association is n-to-n. An admin user can also associate networks with BgpSpeakers. Relationship between BgpSpeaker and Network is 1-to-n. This patch provides BGP-related functionality only to the admin users. Partially-Implements: blueprint bgp-dynamic-routing Co-Authored-By: Ryan Tidwell Co-Authored-By: Jaume Devesa Co-Authored-By: vikram.choudhary Change-Id: I2412c1689683da9d7ec884a4cea506d4eed99453 --- devstack/lib/bgp | 7 + devstack/plugin.sh | 4 + etc/policy.json | 20 +- neutron/db/bgp_db.py | 380 ++++++++++++++++++ .../expand/15be73214821_add_bgp_model_data.py | 105 +++++ neutron/db/migration/models/head.py | 1 + neutron/extensions/bgp.py | 194 +++++++++ neutron/services/bgp/__init__.py | 0 neutron/services/bgp/bgp_plugin.py | 39 ++ .../tests/api/test_bgp_speaker_extensions.py | 173 ++++++++ .../test_bgp_speaker_extensions_negative.py | 53 +++ neutron/tests/etc/policy.json | 20 +- .../services/network/json/network_client.py | 105 +++++ neutron/tests/unit/db/test_bgp_db.py | 331 +++++++++++++++ setup.cfg | 1 + 15 files changed, 1431 insertions(+), 2 deletions(-) create mode 100644 devstack/lib/bgp create mode 100644 neutron/db/bgp_db.py create mode 100644 neutron/db/migration/alembic_migrations/versions/mitaka/expand/15be73214821_add_bgp_model_data.py create mode 100644 neutron/extensions/bgp.py create mode 100644 neutron/services/bgp/__init__.py create mode 100644 neutron/services/bgp/bgp_plugin.py create mode 100644 neutron/tests/api/test_bgp_speaker_extensions.py create mode 100644 neutron/tests/api/test_bgp_speaker_extensions_negative.py create mode 100644 neutron/tests/unit/db/test_bgp_db.py diff --git a/devstack/lib/bgp b/devstack/lib/bgp new file mode 100644 index 00000000..ee3d8325 --- /dev/null +++ b/devstack/lib/bgp @@ -0,0 +1,7 @@ +function configure_bgp_service_plugin { + _neutron_service_plugin_class_add "bgp" +} + +function configure_bgp { + configure_bgp_service_plugin +} diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 6038e7e4..8477dd18 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -1,5 +1,6 @@ LIBDIR=$DEST/neutron/devstack/lib +source $LIBDIR/bgp source $LIBDIR/flavors source $LIBDIR/l2_agent source $LIBDIR/l2_agent_sriovnicswitch @@ -15,6 +16,9 @@ if [[ "$1" == "stack" ]]; then if is_service_enabled q-qos; then configure_qos fi + if is_service_enabled q-bgp; then + configure_bgp + fi ;; post-config) if is_service_enabled q-agt; then diff --git a/etc/policy.json b/etc/policy.json index 4ad14fe8..9c3e314d 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -205,5 +205,23 @@ "create_flavor_service_profile": "rule:admin_only", "delete_flavor_service_profile": "rule:admin_only", "get_flavor_service_profile": "rule:regular_user", - "get_auto_allocated_topology": "rule:admin_or_owner" + "get_auto_allocated_topology": "rule:admin_or_owner", + + "get_bgp_speaker": "rule:admin_only", + "create_bgp_speaker": "rule:admin_only", + "update_bgp_speaker": "rule:admin_only", + "delete_bgp_speaker": "rule:admin_only", + + "get_bgp_peer": "rule:admin_only", + "create_bgp_peer": "rule:admin_only", + "update_bgp_peer": "rule:admin_only", + "delete_bgp_peer": "rule:admin_only", + + "add_bgp_peer": "rule:admin_only", + "remove_bgp_peer": "rule:admin_only", + + "add_gateway_network": "rule:admin_only", + "remove_gateway_network": "rule:admin_only", + + "get_advertised_routes":"rule:admin_only" } diff --git a/neutron/db/bgp_db.py b/neutron/db/bgp_db.py new file mode 100644 index 00000000..53e8dd7b --- /dev/null +++ b/neutron/db/bgp_db.py @@ -0,0 +1,380 @@ +# 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 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 orm +from sqlalchemy.orm import exc as sa_exc + +from neutron.api.v2 import attributes as attr +from neutron.common import exceptions as n_exc +from neutron.db import common_db_mixin as common_db +from neutron.db import model_base +from neutron.db import models_v2 +from neutron.extensions import bgp as bgp_ext + + +LOG = logging.getLogger(__name__) + + +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 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] + 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_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_advertised_routes(self, context, bgp_speaker_id): + return self._make_advertised_routes_dict([]) + + 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) diff --git a/neutron/db/migration/alembic_migrations/versions/mitaka/expand/15be73214821_add_bgp_model_data.py b/neutron/db/migration/alembic_migrations/versions/mitaka/expand/15be73214821_add_bgp_model_data.py new file mode 100644 index 00000000..3525731e --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/mitaka/expand/15be73214821_add_bgp_model_data.py @@ -0,0 +1,105 @@ +# 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. +# + +"""add dynamic routing model data + +Revision ID: 15be73214821 +Create Date: 2015-07-29 13:16:08.604175 + +""" + +# revision identifiers, used by Alembic. +revision = '15be73214821' +down_revision = '19f26505c74f' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + + op.create_table( + 'bgp_speakers', + sa.Column('id', sa.String(length=36), + nullable=False), + sa.Column('name', sa.String(length=255), + nullable=False), + sa.Column('local_as', sa.Integer, nullable=False, + autoincrement=False), + sa.Column('ip_version', sa.Integer, nullable=False, + autoincrement=False), + sa.Column('tenant_id', + sa.String(length=255), + nullable=True, + index=True), + sa.Column('advertise_floating_ip_host_routes', sa.Boolean(), + nullable=False), + sa.Column('advertise_tenant_networks', sa.Boolean(), + nullable=False), + sa.PrimaryKeyConstraint('id') + ) + + op.create_table( + 'bgp_peers', + sa.Column('id', sa.String(length=36), + nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('auth_type', sa.String(length=16), nullable=False), + sa.Column('password', sa.String(length=255), nullable=True), + sa.Column('peer_ip', + sa.String(length=64), + nullable=False), + sa.Column('remote_as', sa.Integer, nullable=False, + autoincrement=False), + sa.Column('tenant_id', + sa.String(length=255), + nullable=True, + index=True), + sa.PrimaryKeyConstraint('id') + ) + + op.create_table( + 'bgp_speaker_network_bindings', + sa.Column('bgp_speaker_id', + sa.String(length=36), + nullable=False), + sa.Column('network_id', + sa.String(length=36), + nullable=True), + sa.Column('ip_version', sa.Integer, nullable=False, + autoincrement=False), + sa.ForeignKeyConstraint(['bgp_speaker_id'], + ['bgp_speakers.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['network_id'], + ['networks.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('network_id', 'bgp_speaker_id', 'ip_version') + ) + + op.create_table( + 'bgp_speaker_peer_bindings', + sa.Column('bgp_speaker_id', + sa.String(length=36), + nullable=False), + sa.Column('bgp_peer_id', + sa.String(length=36), + nullable=False), + sa.ForeignKeyConstraint(['bgp_speaker_id'], ['bgp_speakers.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['bgp_peer_id'], ['bgp_peers.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('bgp_speaker_id', 'bgp_peer_id') + ) diff --git a/neutron/db/migration/models/head.py b/neutron/db/migration/models/head.py index 789c8972..addeb288 100644 --- a/neutron/db/migration/models/head.py +++ b/neutron/db/migration/models/head.py @@ -25,6 +25,7 @@ from neutron.db import address_scope_db # noqa from neutron.db import agents_db # noqa from neutron.db import agentschedulers_db # noqa from neutron.db import allowedaddresspairs_db # noqa +from neutron.db import bgp_db # noqa from neutron.db import dns_db # noqa from neutron.db import dvr_mac_db # noqa from neutron.db import external_net_db # noqa diff --git a/neutron/extensions/bgp.py b/neutron/extensions/bgp.py new file mode 100644 index 00000000..f601f60b --- /dev/null +++ b/neutron/extensions/bgp.py @@ -0,0 +1,194 @@ +# 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.api import extensions +from neutron.api.v2 import attributes as attr +from neutron.api.v2 import resource_helper as rh +from neutron.common import exceptions + +BGP_EXT_ALIAS = 'bgp' +BGP_SPEAKER_RESOURCE_NAME = 'bgp-speaker' +BGP_SPEAKER_BODY_KEY_NAME = 'bgp_speaker' +BGP_PEER_BODY_KEY_NAME = 'bgp_peer' + +bgp_supported_auth_types = ['none', 'md5'] + +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': (1, 65535)}, + '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': (1, 65535)}, + '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_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(exceptions.NotFound): + message = _("BGP speaker %(id)s could not be found.") + + +class BgpPeerNotFound(exceptions.NotFound): + message = _("BGP peer %(id)s could not be found.") + + +class BgpPeerNotAuthenticated(exceptions.NotFound): + message = _("BGP peer %(bgp_peer_id)s not authenticated.") + + +class BgpSpeakerPeerNotAssociated(exceptions.NotFound): + message = _("BGP peer %(bgp_peer_id)s is not associated with " + "BGP speaker %(bgp_speaker_id)s.") + + +class BgpSpeakerNetworkNotAssociated(exceptions.NotFound): + message = _("Network %(network_id)s is not associated with " + "BGP speaker %(bgp_speaker_id)s.") + + +class BgpSpeakerNetworkBindingError(exceptions.Conflict): + message = _("Network %(network_id)s is already bound to BgpSpeaker " + "%(bgp_speaker_id)s.") + + +class NetworkNotBound(exceptions.NotFound): + message = _("Network %(network_id)s is not bound to a BgpSpeaker.") + + +class DuplicateBgpPeerIpException(exceptions.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 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 "2014-07-01T15: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/services/bgp/__init__.py b/neutron/services/bgp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron/services/bgp/bgp_plugin.py b/neutron/services/bgp/bgp_plugin.py new file mode 100644 index 00000000..7d0ff20b --- /dev/null +++ b/neutron/services/bgp/bgp_plugin.py @@ -0,0 +1,39 @@ +# 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 neutron.db import bgp_db +from neutron.extensions import bgp as bgp_ext +from neutron.services import service_base + +PLUGIN_NAME = bgp_ext.BGP_EXT_ALIAS + '_svc_plugin' + + +class BgpPlugin(service_base.ServicePluginBase, + bgp_db.BgpDbMixin): + + supported_extension_aliases = [bgp_ext.BGP_EXT_ALIAS] + + def __init__(self): + super(BgpPlugin, self).__init__() + + 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.") diff --git a/neutron/tests/api/test_bgp_speaker_extensions.py b/neutron/tests/api/test_bgp_speaker_extensions.py new file mode 100644 index 00000000..8851e811 --- /dev/null +++ b/neutron/tests/api/test_bgp_speaker_extensions.py @@ -0,0 +1,173 @@ +# 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 tempest import config +from tempest import test +from tempest_lib import exceptions as lib_exc + +from neutron.tests.api import base +from 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.ext_net_id = CONF.network.public_network_id + + 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) + + @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) + + @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) diff --git a/neutron/tests/api/test_bgp_speaker_extensions_negative.py b/neutron/tests/api/test_bgp_speaker_extensions_negative.py new file mode 100644 index 00000000..1a1c64c4 --- /dev/null +++ b/neutron/tests/api/test_bgp_speaker_extensions_negative.py @@ -0,0 +1,53 @@ +# 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 tempest_lib import exceptions as lib_exc + +from neutron.tests.api import test_bgp_speaker_extensions as test_base +from tempest import test + + +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') diff --git a/neutron/tests/etc/policy.json b/neutron/tests/etc/policy.json index 4ad14fe8..9c3e314d 100644 --- a/neutron/tests/etc/policy.json +++ b/neutron/tests/etc/policy.json @@ -205,5 +205,23 @@ "create_flavor_service_profile": "rule:admin_only", "delete_flavor_service_profile": "rule:admin_only", "get_flavor_service_profile": "rule:regular_user", - "get_auto_allocated_topology": "rule:admin_or_owner" + "get_auto_allocated_topology": "rule:admin_or_owner", + + "get_bgp_speaker": "rule:admin_only", + "create_bgp_speaker": "rule:admin_only", + "update_bgp_speaker": "rule:admin_only", + "delete_bgp_speaker": "rule:admin_only", + + "get_bgp_peer": "rule:admin_only", + "create_bgp_peer": "rule:admin_only", + "update_bgp_peer": "rule:admin_only", + "delete_bgp_peer": "rule:admin_only", + + "add_bgp_peer": "rule:admin_only", + "remove_bgp_peer": "rule:admin_only", + + "add_gateway_network": "rule:admin_only", + "remove_gateway_network": "rule:admin_only", + + "get_advertised_routes":"rule:admin_only" } diff --git a/neutron/tests/tempest/services/network/json/network_client.py b/neutron/tests/tempest/services/network/json/network_client.py index d7c1656c..61bd85f9 100644 --- a/neutron/tests/tempest/services/network/json/network_client.py +++ b/neutron/tests/tempest/services/network/json/network_client.py @@ -48,6 +48,8 @@ class NetworkClientJSON(service_client.ServiceClient): # the following map is used to construct proper URI # for the given neutron resource service_resource_prefix_map = { + 'bgp-peers': '', + 'bgp-speakers': '', 'networks': '', 'subnets': '', 'subnetpools': '', @@ -220,6 +222,109 @@ class NetworkClientJSON(service_client.ServiceClient): self.expected_success(200, resp.status) return service_client.ResponseBody(resp, body) + # BGP speaker methods + def create_bgp_speaker(self, post_data): + body = self.serialize_list(post_data, "bgp-speakers", "bgp-speaker") + uri = self.get_uri("bgp-speakers") + resp, body = self.post(uri, body) + body = {'bgp-speaker': self.deserialize_list(body)} + self.expected_success(201, resp.status) + return service_client.ResponseBody(resp, body) + + def get_bgp_speaker(self, id): + uri = self.get_uri("bgp-speakers") + bgp_speaker_uri = '%s/%s' % (uri, id) + resp, body = self.get(bgp_speaker_uri) + body = {'bgp-speaker': self.deserialize_list(body)} + self.expected_success(200, resp.status) + return service_client.ResponseBody(resp, body) + + def get_bgp_speakers(self): + uri = self.get_uri("bgp-speakers") + resp, body = self.get(uri) + body = {'bgp-speakers': self.deserialize_list(body)} + self.expected_success(200, resp.status) + return service_client.ResponseBody(resp, body) + + def update_bgp_speaker(self, id, put_data): + body = self.serialize_list(put_data, "bgp-speakers", "bgp-speaker") + uri = self.get_uri("bgp-speakers") + bgp_speaker_uri = '%s/%s' % (uri, id) + resp, body = self.put(bgp_speaker_uri, body) + body = {'bgp-speaker': self.deserialize_list(body)} + self.expected_success(200, resp.status) + return service_client.ResponseBody(resp, body) + + def delete_bgp_speaker(self, id): + uri = self.get_uri("bgp-speakers") + bgp_speaker_uri = '%s/%s' % (uri, id) + resp, body = self.delete(bgp_speaker_uri) + self.expected_success(204, resp.status) + return service_client.ResponseBody(resp, body) + + def create_bgp_peer(self, post_data): + body = self.serialize_list(post_data, "bgp-peers", "bgp-peer") + uri = self.get_uri("bgp-peers") + resp, body = self.post(uri, body) + body = {'bgp-peer': self.deserialize_list(body)} + self.expected_success(201, resp.status) + return service_client.ResponseBody(resp, body) + + def get_bgp_peer(self, id): + uri = self.get_uri("bgp-peers") + bgp_speaker_uri = '%s/%s' % (uri, id) + resp, body = self.get(bgp_speaker_uri) + body = {'bgp-peer': self.deserialize_list(body)} + self.expected_success(200, resp.status) + return service_client.ResponseBody(resp, body) + + def delete_bgp_peer(self, id): + uri = self.get_uri("bgp-peers") + bgp_speaker_uri = '%s/%s' % (uri, id) + resp, body = self.delete(bgp_speaker_uri) + self.expected_success(204, resp.status) + return service_client.ResponseBody(resp, body) + + def add_bgp_peer_with_id(self, bgp_speaker_id, bgp_peer_id): + uri = '%s/bgp-speakers/%s/add_bgp_peer' % (self.uri_prefix, + bgp_speaker_id) + update_body = {"bgp_peer_id": bgp_peer_id} + update_body = json.dumps(update_body) + resp, body = self.put(uri, update_body) + self.expected_success(200, resp.status) + body = json.loads(body) + return service_client.ResponseBody(resp, body) + + def remove_bgp_peer_with_id(self, bgp_speaker_id, bgp_peer_id): + uri = '%s/bgp-speakers/%s/remove_bgp_peer' % (self.uri_prefix, + bgp_speaker_id) + update_body = {"bgp_peer_id": bgp_peer_id} + update_body = json.dumps(update_body) + resp, body = self.put(uri, update_body) + self.expected_success(200, resp.status) + body = json.loads(body) + return service_client.ResponseBody(resp, body) + + def add_bgp_gateway_network(self, bgp_speaker_id, network_id): + uri = '%s/bgp-speakers/%s/add_gateway_network' % (self.uri_prefix, + bgp_speaker_id) + update_body = {"network_id": network_id} + update_body = json.dumps(update_body) + resp, body = self.put(uri, update_body) + self.expected_success(200, resp.status) + body = json.loads(body) + return service_client.ResponseBody(resp, body) + + def remove_bgp_gateway_network(self, bgp_speaker_id, network_id): + uri = '%s/bgp-speakers/%s/remove_gateway_network' + uri = uri % (self.uri_prefix, bgp_speaker_id) + update_body = {"network_id": network_id} + update_body = json.dumps(update_body) + resp, body = self.put(uri, update_body) + self.expected_success(200, resp.status) + body = json.loads(body) + return service_client.ResponseBody(resp, body) + # Common methods that are hard to automate def create_bulk_network(self, names, shared=False): network_list = [{'name': name, 'shared': shared} for name in names] diff --git a/neutron/tests/unit/db/test_bgp_db.py b/neutron/tests/unit/db/test_bgp_db.py new file mode 100644 index 00000000..52921556 --- /dev/null +++ b/neutron/tests/unit/db/test_bgp_db.py @@ -0,0 +1,331 @@ +# 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 +from oslo_utils import uuidutils + +from neutron.common import exceptions as n_exc +from neutron.extensions import bgp +from neutron import manager +from neutron.plugins.common import constants as p_const +from neutron.services.bgp import bgp_plugin +from neutron.tests.unit.plugins.ml2 import test_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']) + + +class BgpTests(test_plugin.Ml2PluginV2TestCase, + BgpEntityCreationMixin): + #FIXME(tidwellr) Lots of duplicated setup code, try to streamline + fmt = 'json' + + 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() + + @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]) diff --git a/setup.cfg b/setup.cfg index f1873b4f..922a3e58 100644 --- a/setup.cfg +++ b/setup.cfg @@ -75,6 +75,7 @@ neutron.service_plugins = neutron.services.loadbalancer.plugin.LoadBalancerPlugin = neutron_lbaas.services.loadbalancer.plugin:LoadBalancerPlugin neutron.services.vpn.plugin.VPNDriverPlugin = neutron_vpnaas.services.vpn.plugin:VPNDriverPlugin qos = neutron.services.qos.qos_plugin:QoSPlugin + bgp = neutron.services.bgp.bgp_plugin:BgpPlugin flavors = neutron.services.flavors.flavors_plugin:FlavorsPlugin auto_allocate = neutron.services.auto_allocate.plugin:Plugin neutron.qos.notification_drivers =