diff --git a/doc/source/devstack.rst b/doc/source/devstack.rst index a365d1f804..d3732e0bd0 100644 --- a/doc/source/devstack.rst +++ b/doc/source/devstack.rst @@ -107,6 +107,19 @@ Add neutron-fwaas repo as an external repository and configure following flags i driver = vmware_nsx.services.fwaas.nsx_v.edge_fwaas_driver.EdgeFwaasDriver +Neutron dynamic routing plugin (bgp) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add neutron-dynamic-routing repo as an external repository and configure following flags in ``local.conf``:: + + [[local|localrc]] + enable_plugin neutron-dynamic-routing https://git.openstack.org/openstack/neutron-dynamic-routing + DR_MODE=dr_plugin + + [[post-config|$NEUTRON_CONF]] + [DEFAULT] + api_extensions_path = $DEST/neutron-dynamic-routing/neutron_dynamic_routing/extensions + + NSXv3 ----- diff --git a/tools/tox_install.sh b/tools/tox_install.sh index 71ca229fab..a95e5bee7b 100755 --- a/tools/tox_install.sh +++ b/tools/tox_install.sh @@ -9,6 +9,7 @@ ${DIR}/tox_install_project.sh networking-sfc networking_sfc $* ${DIR}/tox_install_project.sh neutron-lbaas neutron_lbaas $* ${DIR}/tox_install_project.sh vmware-nsxlib vmware_nsxlib $* ${DIR}/tox_install_project.sh neutron-fwaas neutron_fwaas $* +${DIR}/tox_install_project.sh neutron-dynamic-routing neutron-dynamic-routing $* CONSTRAINTS_FILE=$1 shift diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD index f19f975366..803e777337 100644 --- a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -7c4704ad37df +8699700cd95c diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/pike/expand/8699700cd95c_nsxv_bgp_speaker_mapping.py b/vmware_nsx/db/migration/alembic_migrations/versions/pike/expand/8699700cd95c_nsxv_bgp_speaker_mapping.py new file mode 100644 index 0000000000..fd9ac430be --- /dev/null +++ b/vmware_nsx/db/migration/alembic_migrations/versions/pike/expand/8699700cd95c_nsxv_bgp_speaker_mapping.py @@ -0,0 +1,51 @@ +# Copyright 2017 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""nsxv_bgp_speaker_mapping + +Revision ID: 8699700cd95c +Revises: 7c4704ad37df +Create Date: 2017-02-16 03:13:39.775670 + +""" + +# revision identifiers, used by Alembic. +revision = '8699700cd95c' +down_revision = '7c4704ad37df' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'nsxv_bgp_speaker_bindings', + sa.Column('edge_id', sa.String(36), nullable=False), + sa.Column('bgp_speaker_id', sa.String(36), nullable=False), + sa.Column('bgp_identifier', sa.String(64), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['bgp_speaker_id'], ['bgp_speakers.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('edge_id')) + + op.create_table( + 'nsxv_bgp_peer_edge_bindings', + sa.Column('peer_id', sa.String(36), nullable=False), + sa.Column('edge_id', sa.String(36), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['peer_id'], ['bgp_peers.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('peer_id')) diff --git a/vmware_nsx/db/nsxv_db.py b/vmware_nsx/db/nsxv_db.py index a89842041f..c1140f48d3 100644 --- a/vmware_nsx/db/nsxv_db.py +++ b/vmware_nsx/db/nsxv_db.py @@ -894,3 +894,56 @@ def update_nsxv_port_ext_attributes(session, port_id, except exc.NoResultFound: return add_nsxv_port_ext_attributes( session, port_id, vnic_type=vnic_type) + + +def add_nsxv_bgp_speaker_binding(session, edge_id, speaker_id, + bgp_identifier): + with session.begin(subtransactions=True): + binding = nsxv_models.NsxvBgpSpeakerBinding( + edge_id=edge_id, + bgp_speaker_id=speaker_id, + bgp_identifier=bgp_identifier) + session.add(binding) + return binding + + +def get_nsxv_bgp_speaker_binding(session, edge_id): + try: + binding = (session.query(nsxv_models.NsxvBgpSpeakerBinding). + filter_by(edge_id=edge_id). + one()) + return binding + except exc.NoResultFound: + LOG.debug("No dynamic routing enabled on edge %s.", edge_id) + + +def get_nsxv_bgp_speaker_bindings(session, speaker_id): + try: + return (session.query(nsxv_models.NsxvBgpSpeakerBinding). + filter_by(bgp_speaker_id=speaker_id).all()) + except exc.NoResultFound: + return [] + + +def delete_nsxv_bgp_speaker_binding(session, edge_id): + binding = session.query( + nsxv_models.NsxvBgpSpeakerBinding).filter_by(edge_id=edge_id) + if binding: + binding.delete() + + +def add_nsxv_bgp_peer_edge_binding(session, peer_id, edge_id): + with session.begin(subtransactions=True): + binding = nsxv_models.NsxvBgpPeerEdgeBinding(edge_id=edge_id, + peer_id=peer_id) + session.add(binding) + return binding + + +def get_nsxv_bgp_peer_edge_binding(session, peer_id): + try: + binding = (session.query(nsxv_models.NsxvBgpPeerEdgeBinding). + filter_by(peer_id=peer_id).one()) + return binding + except exc.NoResultFound: + pass diff --git a/vmware_nsx/db/nsxv_models.py b/vmware_nsx/db/nsxv_models.py index 965576022c..8d06e89f09 100644 --- a/vmware_nsx/db/nsxv_models.py +++ b/vmware_nsx/db/nsxv_models.py @@ -381,3 +381,28 @@ class NsxvPortExtAttributes(model_base.BASEV2, models.TimestampMixin): models_v2.Port, backref=orm.backref("nsx_port_attributes", lazy='joined', uselist=False, cascade='delete')) + + +class NsxvBgpSpeakerBinding(model_base.BASEV2, models.TimestampMixin): + # Maps bgp_speaker_id to NSXv edge id + __tablename__ = 'nsxv_bgp_speaker_bindings' + + edge_id = sa.Column(sa.String(36), primary_key=True) + bgp_speaker_id = sa.Column(sa.String(36), + sa.ForeignKey('bgp_speakers.id', + ondelete='CASCADE'), + nullable=False) + # A given BGP speaker sets the value of its BGP Identifier to an IP address + # that is assigned to that BGP speaker. + bgp_identifier = sa.Column(sa.String(64), nullable=False) + + +class NsxvBgpPeerEdgeBinding(model_base.BASEV2, models.TimestampMixin): + # Maps between bgp-peer and edges service gateway. + __tablename__ = 'nsxv_bgp_peer_edge_bindings' + peer_id = sa.Column(sa.String(36), + sa.ForeignKey('bgp_peers.id', + ondelete='CASCADE'), + primary_key=True, + nullable=False) + edge_id = sa.Column(sa.String(36), nullable=False) diff --git a/vmware_nsx/extensions/edge_service_gateway_bgp_peer.py b/vmware_nsx/extensions/edge_service_gateway_bgp_peer.py new file mode 100644 index 0000000000..9fa3a29cd7 --- /dev/null +++ b/vmware_nsx/extensions/edge_service_gateway_bgp_peer.py @@ -0,0 +1,98 @@ +# Copyright 2017 VMware, Inc. 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 re + +from neutron_lib.api import extensions +from neutron_lib.api import validators +from neutron_lib import exceptions as nexception + +from vmware_nsx._i18n import _ + +EDGE_SERVICE_GW = 'esg_id' +ESG_BGP_PEER_EXT_ALIAS = 'edge-service-gateway-bgp-peer' + + +def _validate_edge_service_gw_id(esg_id, valid_values=None): + if re.match(r'^edge-[1-9]+[0-9]*$', esg_id) is None: + msg = _("'%s' is not a valid edge service gateway id.") % esg_id + return msg + + +validators.add_validator('validate_edge_service_gw_id', + _validate_edge_service_gw_id) + + +RESOURCE_ATTRIBUTE_MAP = { + 'bgp-peers': { + EDGE_SERVICE_GW: { + 'allow_post': True, + 'allow_put': False, + 'default': None, + 'validate': {'type:validate_edge_service_gw_id': None}, + 'enforce_policy': True, + 'is_visible': True, + 'required_by_policy': False + } + } +} + + +class BgpDisabledOnEsgPeer(nexception.InvalidInput): + message = _("To add this peer to BGP speaker you must first enable BGP on " + "the associated ESG - '%(esg_id)s'.") + + +class EsgRemoteASDoNotMatch(nexception.InvalidInput): + message = _("Specified remote AS is '%(remote_as)s', but ESG '%(esg_id)s' " + "is configured on AS %(esg_as)s.") + + +class ExternalSubnetHasGW(nexception.InvalidInput): + message = _("Subnet '%(subnet_id)s' on external network '%(network_id)s' " + "is configured with gateway IP, set to None before enabling " + "BGP on the network.") + + +class Edge_service_gateway_bgp_peer(extensions.ExtensionDescriptor): + """Extension class to allow identifying of-peer with specificN SXv edge + service gateway. + """ + + @classmethod + def get_name(cls): + return "Edge service gateway bgp peer" + + @classmethod + def get_alias(cls): + return ESG_BGP_PEER_EXT_ALIAS + + @classmethod + def get_description(cls): + return ("Adding a new (optional) attribute 'esg_id' to bgp-peer " + "resource, where esg_id is a valid NSXv Edge service gateway " + "id.") + + @classmethod + def get_updated(cls): + return "2017-04-01T10:00:00-00:00" + + def get_required_extensions(self): + return ["bgp"] + + def get_extended_resources(self, version): + if version == "2.0": + return RESOURCE_ATTRIBUTE_MAP + else: + return {} diff --git a/vmware_nsx/plugins/nsx_v/vshield/edge_dynamic_routing_driver.py b/vmware_nsx/plugins/nsx_v/vshield/edge_dynamic_routing_driver.py new file mode 100644 index 0000000000..54091a202d --- /dev/null +++ b/vmware_nsx/plugins/nsx_v/vshield/edge_dynamic_routing_driver.py @@ -0,0 +1,200 @@ +# Copyright 2017 VMware, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from oslo_log import log as logging + +from vmware_nsx.common import locking + +LOG = logging.getLogger(__name__) + + +class EdgeDynamicRoutingDriver(object): + + """Edge driver API to implement the dynamic routing""" + + def __init__(self): + # it will be initialized at subclass + self.vcns = None + + def _get_routing_global_config(self, edge_id): + h, config = self.vcns.get_dynamic_routing_service(edge_id) + global_config = config if config else {} + global_config.setdefault('ipPrefixes', {'ipPrefixes': []}) + curr_prefixes = [{'ipPrefix': prx} + for prx in global_config['ipPrefixes']['ipPrefixes']] + global_config['ipPrefixes'] = curr_prefixes + return {'routingGlobalConfig': global_config} + + def _update_global_routing_config(self, edge_id, **kwargs): + global_config = self._get_routing_global_config(edge_id) + current_prefixes = global_config['routingGlobalConfig']['ipPrefixes'] + + global_config['routingGlobalConfig']['ecmp'] = True + + if 'router_id' in kwargs: + global_config['routingGlobalConfig']['routerId'] = ( + kwargs['router_id']) + + current_prefixes[:] = [p for p in current_prefixes + if p['ipPrefix']['name'] not in + kwargs.get('prefixes_to_remove', [])] + # Avoid adding duplicate rules when shared router relocation + current_prefixes.extend([p for p in kwargs.get('prefixes_to_add', []) + if p not in current_prefixes]) + + self.vcns.update_dynamic_routing_service(edge_id, global_config) + + def _reset_routing_global_config(self, edge_id): + global_config = self._get_routing_global_config(edge_id) + global_config['routingGlobalConfig']['ecmp'] = False + global_config['routingGlobalConfig'].pop('routerId', None) + global_config['routingGlobalConfig'].pop('ipPrefixes', None) + self.vcns.update_dynamic_routing_service(edge_id, global_config) + + def get_routing_bgp_config(self, edge_id): + h, config = self.vcns.get_bgp_routing_config(edge_id) + bgp_config = config if config else {} + bgp_config.setdefault('enabled', False) + bgp_config.setdefault('bgpNeighbours', {'bgpNeighbours': []}) + bgp_config.setdefault('redistribution', {'rules': {'rules': []}}) + + curr_neighbours = [{'bgpNeighbour': nbr} for nbr in + bgp_config['bgpNeighbours']['bgpNeighbours']] + bgp_config['bgpNeighbours'] = curr_neighbours + for nbr in curr_neighbours: + bgp_filters = [{'bgpFilter': bf} for bf + in nbr['bgpNeighbour']['bgpFilters']['bgpFilters']] + nbr['bgpNeighbour']['bgpFilters'] = bgp_filters + redistribution_rules = [{'rule': rule} for rule in + bgp_config['redistribution']['rules']['rules']] + bgp_config['redistribution']['rules'] = redistribution_rules + return {'bgp': bgp_config} + + def _update_bgp_routing_config(self, edge_id, **kwargs): + bgp_config = self.get_routing_bgp_config(edge_id) + curr_neighbours = bgp_config['bgp']['bgpNeighbours'] + curr_rules = bgp_config['bgp']['redistribution']['rules'] + + bgp_config['bgp']['enabled'] = True + + if 'local_as' in kwargs: + bgp_config['bgp']['localAS'] = kwargs['local_as'] + + if 'enabled' in kwargs: + bgp_config['bgp']['redistribution']['enabled'] = kwargs['enabled'] + + curr_rules[:] = [rule for rule in curr_rules + if rule['rule'].get('prefixName') not in + kwargs.get('rules_to_remove', [])] + # Avoid adding duplicate rules when shared router relocation + curr_rules_prefixes = [r['rule'].get('prefixName') for r in curr_rules] + curr_rules.extend([r for r in kwargs.get('rules_to_add', []) + if r['rule'].get('prefixName') not in + curr_rules_prefixes]) + + neighbours_to_remove = [nbr['bgpNeighbour']['ipAddress'] for nbr in + kwargs.get('neighbours_to_remove', [])] + curr_neighbours[:] = [nbr for nbr in curr_neighbours + if nbr['bgpNeighbour']['ipAddress'] + not in neighbours_to_remove] + curr_neighbours.extend(kwargs.get('neighbours_to_add', [])) + + self.vcns.update_bgp_dynamic_routing(edge_id, bgp_config) + + def _get_edge_static_routes(self, edge_id): + h, routes = self.vcns.get_routes(edge_id) + static_routes = routes if routes else {} + static_routes.setdefault('staticRoutes', {'staticRoutes': []}) + return static_routes + + def _remove_default_static_routes(self, edge_id, droutes=None): + routes = self._get_edge_static_routes(edge_id) + # if droutes is empty or None, then remove all default static routes + droutes = droutes or routes['staticRoutes']['staticRoutes'] + droutes = [r['nextHop'] for r in droutes] + routes['staticRoutes']['staticRoutes'] = [ + r for r in routes['staticRoutes']['staticRoutes'] + if r['network'] != '0.0.0.0/0' or r['nextHop'] not in droutes] + self.vcns.update_routes(edge_id, routes) + + def _add_default_static_routes(self, edge_id, default_routes): + routes = self._get_edge_static_routes(edge_id) + routes['staticRoutes']['staticRoutes'].extend(default_routes) + self.vcns.update_routes(edge_id, routes) + + def add_bgp_speaker_config(self, edge_id, prot_router_id, local_as, + enabled, default_routes, bgp_neighbours, + prefixes, redistribution_rules): + with locking.LockManager.get_lock(str(edge_id)): + self._update_global_routing_config(edge_id, + router_id=prot_router_id, + prefixes_to_add=prefixes) + self._update_bgp_routing_config(edge_id, enabled=enabled, + local_as=local_as, + neighbours_to_add=bgp_neighbours, + prefixes_to_add=prefixes, + rules_to_add=redistribution_rules) + self._add_default_static_routes(edge_id, default_routes) + + def delete_bgp_speaker_config(self, edge_id): + with locking.LockManager.get_lock(str(edge_id)): + self.vcns.delete_bgp_routing_config(edge_id) + self._remove_default_static_routes(edge_id) + self._reset_routing_global_config(edge_id) + + def add_bgp_neighbours(self, edge_id, bgp_neighbours, + default_routes=None): + # Query the bgp config first and update the bgpNeighbour + with locking.LockManager.get_lock(str(edge_id)): + self._update_bgp_routing_config(edge_id, + neighbours_to_add=bgp_neighbours) + if default_routes: + self._add_default_static_routes(edge_id, default_routes) + + def remove_bgp_neighbours(self, edge_id, bgp_neighbours, + default_routes=None): + with locking.LockManager.get_lock(str(edge_id)): + self._update_bgp_routing_config( + edge_id, neighbours_to_remove=bgp_neighbours) + if default_routes: + self._remove_default_static_routes(edge_id, default_routes) + + def update_bgp_neighbour(self, edge_id, bgp_neighbour): + with locking.LockManager.get_lock(str(edge_id)): + self._update_bgp_routing_config( + edge_id, + neighbours_to_remove=[bgp_neighbour], + neighbours_to_add=[bgp_neighbour]) + + def update_routing_redistribution(self, edge_id, enabled): + with locking.LockManager.get_lock(str(edge_id)): + self._update_bgp_routing_config(edge_id, enabled=enabled) + + def add_bgp_redistribution_rules(self, edge_id, prefixes, rules): + with locking.LockManager.get_lock(str(edge_id)): + self._update_global_routing_config(edge_id, + prefixes_to_add=prefixes) + self._update_bgp_routing_config(edge_id, rules_to_add=rules) + LOG.debug("Added redistribution rules %s on edge %s", rules, edge_id) + + def remove_bgp_redistribution_rules(self, edge_id, prefixes): + with locking.LockManager.get_lock(str(edge_id)): + self._update_bgp_routing_config(edge_id, rules_to_remove=prefixes) + self._update_global_routing_config(edge_id, + prefixes_to_remove=prefixes) + LOG.debug("Removed redistribution rules for prefixes %s on edge %s", + prefixes, edge_id) + + def update_router_id(self, edge_id, router_id): + with locking.LockManager.get_lock(str(edge_id)): + self._update_global_routing_config(edge_id, router_id=router_id) diff --git a/vmware_nsx/plugins/nsx_v/vshield/vcns.py b/vmware_nsx/plugins/nsx_v/vshield/vcns.py index 245b7d4277..82f91f634f 100644 --- a/vmware_nsx/plugins/nsx_v/vshield/vcns.py +++ b/vmware_nsx/plugins/nsx_v/vshield/vcns.py @@ -83,6 +83,10 @@ CERTIFICATE = "certificate" NETWORK_TYPES = ['Network', 'VirtualWire', 'DistributedVirtualPortgroup'] +# Dynamic routing constants +GLOBAL_ROUTING_CONFIG = "routing/config/global" +BGP_ROUTING_CONFIG = "routing/config/bgp" + def retry_upon_exception_exclude_error_codes( exc, excluded_errors, delay=0.5, max_delay=4, max_attempts=0): @@ -1065,3 +1069,27 @@ class Vcns(object): uri = '%s/scope/globalroot-0' % APPLICATION_PREFIX h, apps = self.do_request(HTTP_GET, uri, decode=True) return apps + + def update_dynamic_routing_service(self, edge_id, request_config): + uri = self._build_uri_path(edge_id, GLOBAL_ROUTING_CONFIG) + return self.do_request(HTTP_PUT, uri, + VcnsApiClient.xmldumps(request_config), + format='xml') + + def get_dynamic_routing_service(self, edge_id): + uri = self._build_uri_path(edge_id, GLOBAL_ROUTING_CONFIG) + return self.do_request(HTTP_GET, uri) + + def update_bgp_dynamic_routing(self, edge_id, bgp_request): + uri = self._build_uri_path(edge_id, BGP_ROUTING_CONFIG) + return self.do_request(HTTP_PUT, uri, + VcnsApiClient.xmldumps(bgp_request), + format='xml') + + def get_bgp_routing_config(self, edge_id): + uri = self._build_uri_path(edge_id, BGP_ROUTING_CONFIG) + return self.do_request(HTTP_GET, uri) + + def delete_bgp_routing_config(self, edge_id): + uri = self._build_uri_path(edge_id, BGP_ROUTING_CONFIG) + return self.do_request(HTTP_DELETE, uri) diff --git a/vmware_nsx/plugins/nsx_v/vshield/vcns_driver.py b/vmware_nsx/plugins/nsx_v/vshield/vcns_driver.py index 856161ae60..5c50c78daf 100644 --- a/vmware_nsx/plugins/nsx_v/vshield/vcns_driver.py +++ b/vmware_nsx/plugins/nsx_v/vshield/vcns_driver.py @@ -18,6 +18,7 @@ from oslo_config import cfg from oslo_log import log as logging from vmware_nsx.plugins.nsx_v.vshield import edge_appliance_driver +from vmware_nsx.plugins.nsx_v.vshield import edge_dynamic_routing_driver from vmware_nsx.plugins.nsx_v.vshield import edge_firewall_driver from vmware_nsx.plugins.nsx_v.vshield.tasks import tasks from vmware_nsx.plugins.nsx_v.vshield import vcns @@ -29,7 +30,8 @@ LOG = logging.getLogger(__name__) class VcnsDriver(edge_appliance_driver.EdgeApplianceDriver, lbaas_v2.EdgeLoadbalancerDriverV2, - edge_firewall_driver.EdgeFirewallDriver): + edge_firewall_driver.EdgeFirewallDriver, + edge_dynamic_routing_driver.EdgeDynamicRoutingDriver): def __init__(self, callbacks): super(VcnsDriver, self).__init__() diff --git a/vmware_nsx/services/dynamic_routing/__init__.py b/vmware_nsx/services/dynamic_routing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/dynamic_routing/bgp_plugin.py b/vmware_nsx/services/dynamic_routing/bgp_plugin.py new file mode 100644 index 0000000000..0724656f83 --- /dev/null +++ b/vmware_nsx/services/dynamic_routing/bgp_plugin.py @@ -0,0 +1,256 @@ +# Copyright 2017 VMware, Inc. +# +# 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 neutron.callbacks import events +from neutron.callbacks import registry +from neutron.callbacks import resources +from neutron_dynamic_routing.db import bgp_db +from neutron_dynamic_routing.extensions import bgp as bgp_ext +from neutron_lib import context as n_context +from neutron_lib.services import base as service_base + +from vmware_nsx.common import locking +from vmware_nsx.common import nsxv_constants +from vmware_nsx.db import nsxv_db +from vmware_nsx.extensions import edge_service_gateway_bgp_peer as ext_esg +from vmware_nsx.services.dynamic_routing.nsx_v import driver as nsxv_driver + +PLUGIN_NAME = bgp_ext.BGP_EXT_ALIAS + '_nsx_svc_plugin' +LOG = logging.getLogger(__name__) + + +class NSXvBgpPlugin(service_base.ServicePluginBase, bgp_db.BgpDbMixin): + + supported_extension_aliases = [bgp_ext.BGP_EXT_ALIAS, + ext_esg.ESG_BGP_PEER_EXT_ALIAS] + + def __init__(self): + super(NSXvBgpPlugin, self).__init__() + self.nsxv_driver = nsxv_driver.NSXvBgpDriver(self) + 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 project networks, floating IP's, and DVR host routes.") + + def _register_callbacks(self): + registry.subscribe(self.router_interface_callback, + resources.ROUTER_INTERFACE, + events.AFTER_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) + registry.subscribe(self._after_service_edge_create_callback, + nsxv_constants.SERVICE_EDGE, + events.AFTER_CREATE) + registry.subscribe(self._before_service_edge_delete_callback, + nsxv_constants.SERVICE_EDGE, + events.BEFORE_DELETE) + + def create_bgp_speaker(self, context, bgp_speaker): + self.nsxv_driver.create_bgp_speaker(context, bgp_speaker) + return super(NSXvBgpPlugin, self).create_bgp_speaker(context, + bgp_speaker) + + def update_bgp_speaker(self, context, bgp_speaker_id, bgp_speaker): + with locking.LockManager.get_lock(str(bgp_speaker_id)): + self.nsxv_driver.update_bgp_speaker(context, bgp_speaker_id, + bgp_speaker) + # TBD(roeyc): rolling back changes on edges base class call failed. + return super(NSXvBgpPlugin, self).update_bgp_speaker( + context, bgp_speaker_id, bgp_speaker) + + def delete_bgp_speaker(self, context, bgp_speaker_id): + with locking.LockManager.get_lock(str(bgp_speaker_id)): + self.nsxv_driver.delete_bgp_speaker(context, bgp_speaker_id) + super(NSXvBgpPlugin, self).delete_bgp_speaker(context, + bgp_speaker_id) + + def _get_esg_peer_info(self, context, bgp_peer_id): + binding = nsxv_db.get_nsxv_bgp_peer_edge_binding(context.session, + bgp_peer_id) + if binding: + return binding['edge_id'] + + def get_bgp_peer(self, context, bgp_peer_id, fields=None): + peer = super(NSXvBgpPlugin, self).get_bgp_peer(context, + bgp_peer_id, fields) + if fields is None or 'esg_id' in fields: + peer['esg_id'] = self._get_esg_peer_info(context, bgp_peer_id) + return peer + + def get_bgp_peers_by_bgp_speaker(self, context, + bgp_speaker_id, fields=None): + ret = super(NSXvBgpPlugin, self).get_bgp_peers_by_bgp_speaker( + context, bgp_speaker_id, fields=fields) + if fields is None or 'esg_id' in fields: + for peer in ret: + peer['esg_id'] = self._get_esg_peer_info(context, peer['id']) + return ret + + def create_bgp_peer(self, context, bgp_peer): + self.nsxv_driver.create_bgp_peer(context, bgp_peer) + peer = super(NSXvBgpPlugin, self).create_bgp_peer(context, bgp_peer) + esg_id = bgp_peer['bgp_peer'].get('esg_id') + if esg_id: + nsxv_db.add_nsxv_bgp_peer_edge_binding(context.session, peer['id'], + esg_id) + peer['esg_id'] = esg_id + return peer + + def update_bgp_peer(self, context, bgp_peer_id, bgp_peer): + super(NSXvBgpPlugin, self).update_bgp_peer(context, + bgp_peer_id, bgp_peer) + self.nsxv_driver.update_bgp_peer(context, bgp_peer_id, bgp_peer) + return self.get_bgp_peer(context, bgp_peer_id) + + def delete_bgp_peer(self, context, bgp_peer_id): + bgp_peer_info = {'bgp_peer_id': bgp_peer_id} + bgp_speaker_ids = self.nsxv_driver._get_bgp_speakers_by_bgp_peer( + context, bgp_peer_id) + for speaker_id in bgp_speaker_ids: + self.remove_bgp_peer(context, speaker_id, bgp_peer_info) + super(NSXvBgpPlugin, self).delete_bgp_peer(context, bgp_peer_id) + + def add_bgp_peer(self, context, bgp_speaker_id, bgp_peer_info): + with locking.LockManager.get_lock(str(bgp_speaker_id)): + self.nsxv_driver.add_bgp_peer(context, + bgp_speaker_id, bgp_peer_info) + return super(NSXvBgpPlugin, self).add_bgp_peer(context, + bgp_speaker_id, + bgp_peer_info) + + def remove_bgp_peer(self, context, bgp_speaker_id, bgp_peer_info): + with locking.LockManager.get_lock(str(bgp_speaker_id)): + speaker = self._get_bgp_speaker(context, bgp_speaker_id) + if bgp_peer_info['bgp_peer_id'] not in speaker['peers']: + return + self.nsxv_driver.remove_bgp_peer(context, + bgp_speaker_id, bgp_peer_info) + return super(NSXvBgpPlugin, self).remove_bgp_peer( + context, bgp_speaker_id, bgp_peer_info) + + def add_gateway_network(self, context, bgp_speaker_id, network_info): + with locking.LockManager.get_lock(str(bgp_speaker_id)): + self.nsxv_driver.add_gateway_network(context, + bgp_speaker_id, + network_info) + return super(NSXvBgpPlugin, self).add_gateway_network( + context, bgp_speaker_id, network_info) + + def remove_gateway_network(self, context, bgp_speaker_id, network_info): + with locking.LockManager.get_lock(str(bgp_speaker_id)): + super(NSXvBgpPlugin, self).remove_gateway_network( + context, bgp_speaker_id, network_info) + self.nsxv_driver.remove_gateway_network(context, + bgp_speaker_id, + network_info) + + def get_advertised_routes(self, context, bgp_speaker_id): + return super(NSXvBgpPlugin, self).get_advertised_routes( + context, bgp_speaker_id) + + def router_interface_callback(self, resource, event, trigger, **kwargs): + if not kwargs['network_id']: + # No GW network, hence no BGP speaker associated + return + + context = kwargs['context'] + router_id = kwargs['router_id'] + subnets = kwargs.get('subnets') + network_id = kwargs['network_id'] + port = kwargs['port'] + + speakers = self._bgp_speakers_for_gateway_network(context, + network_id) + for speaker in speakers: + speaker_id = speaker.id + with locking.LockManager.get_lock(str(speaker_id)): + speaker = self.get_bgp_speaker(context, speaker_id) + if network_id not in speaker['networks']: + continue + if event == events.AFTER_CREATE: + self.nsxv_driver.advertise_subnet(context, speaker_id, + router_id, subnets[0]) + if event == events.AFTER_DELETE: + subnet_id = port['fixed_ips'][0]['subnet_id'] + self.nsxv_driver.withdraw_subnet(context, speaker_id, + router_id, subnet_id) + + def router_gateway_callback(self, resource, event, trigger, **kwargs): + context = kwargs.get('context') or n_context.get_admin_context() + router_id = kwargs['router_id'] + network_id = kwargs['network_id'] + speakers = self._bgp_speakers_for_gateway_network(context, network_id) + + for speaker in speakers: + speaker_id = speaker.id + with locking.LockManager.get_lock(str(speaker_id)): + speaker = self.get_bgp_speaker(context, speaker_id) + if network_id not in speaker['networks']: + continue + if event == events.AFTER_DELETE: + gw_ips = kwargs['gateway_ips'] + self.nsxv_driver.disable_bgp_on_router(context, + speaker, + router_id, + gw_ips[0]) + + def _before_service_edge_delete_callback(self, resource, event, + trigger, **kwargs): + context = kwargs['context'] + router = kwargs['router'] + ext_net_id = router.gw_port and router.gw_port['network_id'] + gw_ip = router.gw_port and router.gw_port['fixed_ips'][0]['ip_address'] + edge_id = kwargs.get('edge_id') + speakers = self._bgp_speakers_for_gateway_network(context, ext_net_id) + for speaker in speakers: + with locking.LockManager.get_lock(speaker.id): + speaker = self.get_bgp_speaker(context, speaker.id) + if ext_net_id not in speaker['networks']: + continue + self.nsxv_driver.disable_bgp_on_router(context, speaker, + router['id'], + gw_ip, edge_id) + + def _after_service_edge_create_callback(self, resource, event, + trigger, **kwargs): + context = kwargs['context'] + router = kwargs['router'] + ext_net_id = router.gw_port and router.gw_port['network_id'] + speakers = self._bgp_speakers_for_gateway_network(context, ext_net_id) + for speaker in speakers: + with locking.LockManager.get_lock(speaker.id): + speaker = self.get_bgp_speaker(context, speaker.id) + if ext_net_id not in speaker['networks']: + continue + self.nsxv_driver.enable_bgp_on_router(context, speaker, + router['id']) diff --git a/vmware_nsx/services/dynamic_routing/nsx_v/__init__.py b/vmware_nsx/services/dynamic_routing/nsx_v/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/dynamic_routing/nsx_v/driver.py b/vmware_nsx/services/dynamic_routing/nsx_v/driver.py new file mode 100644 index 0000000000..dc1b718d06 --- /dev/null +++ b/vmware_nsx/services/dynamic_routing/nsx_v/driver.py @@ -0,0 +1,541 @@ +# Copyright 2017 VMware, Inc. +# +# 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 netaddr + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import excutils + +from neutron_lib import constants as n_const +from neutron_lib import exceptions as n_exc +from neutron_lib.plugins import directory +from vmware_nsx._i18n import _ +from vmware_nsx.common import locking +from vmware_nsx.common import nsxv_constants +from vmware_nsx.db import nsxv_db +from vmware_nsx.extensions import edge_service_gateway_bgp_peer as ext_esg_peer +from vmware_nsx.plugins.nsx_v.vshield.common import exceptions as vcns_exc + +LOG = logging.getLogger(__name__) + + +def ip_prefix(name, ip_address): + return {'ipPrefix': {'name': name, 'ipAddress': ip_address}} + + +def redistribution_rule(advertise_static_routes, prefix_name, action='permit'): + rule = { + 'prefixName': prefix_name, + 'action': action, + 'from': { + 'ospf': False, + 'bgp': False, + 'connected': not advertise_static_routes, + 'static': advertise_static_routes + } + } + return {'rule': rule} + + +def bgp_neighbour(bgp_peer): + bgp_filter = {'bgpFilter': [{'direction': 'out', 'action': 'permit'}]} + nbr = { + 'ipAddress': bgp_peer['peer_ip'], + 'remoteAS': bgp_peer['remote_as'], + 'bgpFilters': bgp_filter, + 'password': bgp_peer['password'] + } + return {'bgpNeighbour': nbr} + + +def gw_bgp_neighbour(ip_address, remote_as, password): + bgp_filter = {'bgpFilter': [{'direction': 'in', 'action': 'permit'}]} + nbr = { + 'ipAddress': ip_address, + 'remoteAS': remote_as, + 'bgpFilters': bgp_filter, + 'password': password + } + return {'bgpNeighbour': nbr} + + +def default_route(nexthop): + return {'network': '0.0.0.0/0', + 'nextHop': nexthop} + + +class NSXvBgpDriver(object): + """Class driver to address the neutron_dynamic_routing API""" + + def __init__(self, plugin): + super(NSXvBgpDriver, self).__init__() + self._edge_password = cfg.CONF.nsxv.edge_appliance_password + self._plugin = plugin + self._core_plugin = directory.get_plugin() + self._nsxv = self._core_plugin.nsx_v + self._edge_manager = self._core_plugin.edge_manager + + def prefix_name(self, subnet_id): + return 'subnet-%s' % subnet_id + + def _get_router_edge_info(self, context, router_id): + edge_binding = nsxv_db.get_nsxv_router_binding(context.session, + router_id) + if not edge_binding: + return None, None + + # Idicates which routes should be advertised - connected or static. + advertise_static_routes = False + if edge_binding['edge_type'] != nsxv_constants.SERVICE_EDGE: + # Distributed router + plr_id = self._edge_manager.get_plr_by_tlr_id(context, router_id) + edge_binding = nsxv_db.get_nsxv_router_binding(context.session, + plr_id) + if not edge_binding: + # Distributed router isn't bound to plr + return None, None + # PLR for distributed router, advertise static routes. + advertise_static_routes = True + return edge_binding['edge_id'], advertise_static_routes + + def _get_dynamic_routing_edge_list(self, context, gateway_network_id): + # Filter the routers attached this network as gateway interface + filters = {'network_id': [gateway_network_id], + 'device_owner': [n_const.DEVICE_OWNER_ROUTER_GW]} + fields = ['device_id', 'fixed_ips'] + gateway_ports = self._core_plugin.get_ports(context, filters=filters, + fields=fields) + edge_router_dict = {} + for port in gateway_ports: + router_id = port['device_id'] + router = self._core_plugin._get_router(context, router_id) + bgp_identifier = port['fixed_ips'][0]['ip_address'] + edge_id, advertise_static_routes = ( + self._get_router_edge_info(context, router_id)) + if not edge_id: + # Shared router is not attached on any edge + continue + if edge_id not in edge_router_dict: + edge_router_dict[edge_id] = {'no_snat_routers': [], + 'bgp_identifier': + bgp_identifier, + 'advertise_static_routes': + advertise_static_routes} + if not router.enable_snat: + edge_router_dict[edge_id]['no_snat_routers'].append(router_id) + return edge_router_dict + + def _query_tenant_subnets(self, context, router_ids): + # Query subnets attached to all of routers attached to same edge + subnets = [] + for router_id in router_ids: + filters = {'device_id': [router_id], + 'device_owner': [n_const.DEVICE_OWNER_ROUTER_INTF]} + int_ports = self._core_plugin.get_ports(context, + filters=filters, + fields=['fixed_ips']) + for p in int_ports: + subnet_id = p['fixed_ips'][0]['subnet_id'] + subnet = self._core_plugin.get_subnet(context, subnet_id) + subnets.append({'id': subnet_id, + 'cidr': subnet['cidr']}) + LOG.debug("Got related subnets %s", subnets) + return subnets + + def _get_bgp_speakers_by_bgp_peer(self, context, bgp_peer_id): + fields = ['id', 'peers'] + bgp_speakers = self._plugin.get_bgp_speakers(context, fields=fields) + bgp_speaker_ids = [bgp_speaker['id'] for bgp_speaker in bgp_speakers + if bgp_peer_id in bgp_speaker['peers']] + return bgp_speaker_ids + + def _get_prefixes_and_redistribution_rules(self, subnets, + advertise_static_routes): + prefixes = [] + redis_rules = [] + for subnet in subnets: + prefix_name = self.prefix_name(subnet['id']) + prefix = ip_prefix(prefix_name, subnet['cidr']) + prefixes.append(prefix) + rule = redistribution_rule(advertise_static_routes, prefix_name) + redis_rules.append(rule) + return prefixes, redis_rules + + def create_bgp_speaker(self, context, bgp_speaker): + bgp_speaker_data = bgp_speaker['bgp_speaker'] + ip_version = bgp_speaker_data.get('ip_version') + if ip_version and ip_version == 6: + err_msg = _("NSXv BGP does not support for IPv6") + raise n_exc.InvalidInput(error_message=err_msg) + + def update_bgp_speaker(self, context, bgp_speaker_id, bgp_speaker): + bgp_obj = bgp_speaker['bgp_speaker'] + old_speaker_info = self._plugin.get_bgp_speaker(context, + bgp_speaker_id) + enabled_state = old_speaker_info['advertise_tenant_networks'] + new_enabled_state = bgp_obj.get('advertise_tenant_networks', + enabled_state) + if new_enabled_state == enabled_state: + return + + bgp_bindings = nsxv_db.get_nsxv_bgp_speaker_bindings( + context.session, bgp_speaker_id) + edge_ids = [bgp_binding['edge_id'] for bgp_binding in bgp_bindings] + action = 'Enabling' if new_enabled_state else 'Disabling' + LOG.info("%s BGP route redistribution on edges: %s.", action, edge_ids) + for edge_id in edge_ids: + try: + self._nsxv.update_routing_redistribution(edge_id, + new_enabled_state) + except vcns_exc.VcnsApiException: + LOG.warning("Failed to update BGP on edge %s.", edge_id) + + def delete_bgp_speaker(self, context, bgp_speaker_id): + bgp_bindings = nsxv_db.get_nsxv_bgp_speaker_bindings( + context.session, bgp_speaker_id) + self._stop_bgp_on_edges(context, bgp_bindings, bgp_speaker_id) + + def _validate_bgp_configuration_on_peer_esg(self, bgp_peer): + if not bgp_peer.get('esg_id'): + return + # TBD(roeyc): Validate peer_ip is on subnet + + bgp_config = self._nsxv.get_routing_bgp_config(bgp_peer['esg_id']) + remote_as = bgp_peer['remote_as'] + esg_id = bgp_peer['esg_id'] + esg_as = bgp_config['bgp'].get('localAS') + if not bgp_config['bgp']['enabled']: + raise ext_esg_peer.BgpDisabledOnEsgPeer(esg_id=esg_id) + if esg_as != int(remote_as): + raise ext_esg_peer.EsgRemoteASDoNotMatch(remote_as=remote_as, + esg_id=esg_id, + esg_as=esg_as) + + def create_bgp_peer(self, context, bgp_peer): + bgp_peer = bgp_peer['bgp_peer'] + remote_ip = bgp_peer['peer_ip'] + if not netaddr.valid_ipv4(remote_ip): + err_msg = _("NSXv BGP does not support for IPv6") + raise n_exc.InvalidInput(error_message=err_msg) + self._validate_bgp_configuration_on_peer_esg(bgp_peer) + + def update_bgp_peer(self, context, bgp_peer_id, bgp_peer): + password = bgp_peer['bgp_peer'].get('password') + old_bgp_peer = self._plugin.get_bgp_peer(context, bgp_peer_id) + + # Only password update is relevant for backend. + if old_bgp_peer['password'] == password: + return + + bgp_speaker_ids = self._get_bgp_speakers_by_bgp_peer(context, + bgp_peer_id) + # Update the password for the old bgp peer and update NSX + old_bgp_peer['password'] = password + neighbour = bgp_neighbour(old_bgp_peer) + for bgp_speaker_id in bgp_speaker_ids: + with locking.LockManager.get_lock(bgp_speaker_id): + speaker = self._plugin.get_bgp_speaker(context, bgp_speaker_id) + if bgp_peer_id not in speaker['peers']: + continue + bgp_bindings = nsxv_db.get_nsxv_bgp_speaker_bindings( + context.session, bgp_speaker_id) + for binding in bgp_bindings: + try: + self._nsxv.update_bgp_neighbour(binding['edge_id'], + neighbour) + except vcns_exc.VcnsApiException: + LOG.error("Failed to update BGP neighbor '%s' on " + "edge '%s'", old_bgp_peer['peer_ip'], + binding['edge_id']) + + def add_bgp_peer(self, context, bgp_speaker_id, bgp_peer_info): + bgp_peer_obj = self._plugin.get_bgp_peer(context, + bgp_peer_info['bgp_peer_id']) + + nbr = bgp_neighbour(bgp_peer_obj) + bgp_bindings = nsxv_db.get_nsxv_bgp_speaker_bindings(context.session, + bgp_speaker_id) + speaker = self._plugin.get_bgp_speaker(context, bgp_speaker_id) + # list of tenant edge routers to be removed as bgp-neighbours to this + # peer if it's associated with specific ESG. + neighbours = [] + droute = default_route(nbr['bgpNeighbour']['ipAddress']) + for binding in bgp_bindings: + try: + self._nsxv.add_bgp_neighbours(binding['edge_id'], [nbr], + default_routes=[droute]) + except vcns_exc.VcnsApiException: + LOG.error("Failed to add BGP neighbour on '%s'", + binding['edge_id']) + else: + nbr = gw_bgp_neighbour(binding['bgp_identifier'], + speaker['local_as'], + self._edge_password) + neighbours.append(nbr) + LOG.debug("Succesfully added BGP neighbor '%s' on '%s'", + bgp_peer_obj['peer_ip'], binding['edge_id']) + + if bgp_peer_obj.get('esg_id'): + edge_gw = bgp_peer_obj['esg_id'] + try: + self._nsxv.add_bgp_neighbours(edge_gw, neighbours) + except vcns_exc.VcnsApiException: + with excutils.save_and_reraise_exception(): + LOG.error("Failed to add BGP neighbour on GW Edge '%s'", + edge_gw) + + def remove_bgp_peer(self, context, bgp_speaker_id, bgp_peer_info): + bgp_peer_id = bgp_peer_info['bgp_peer_id'] + bgp_peer_obj = self._plugin.get_bgp_peer(context, bgp_peer_id) + nbr = bgp_neighbour(bgp_peer_obj) + bgp_bindings = nsxv_db.get_nsxv_bgp_speaker_bindings( + context.session, bgp_speaker_id) + speaker = self._plugin.get_bgp_speaker(context, bgp_speaker_id) + # list of tenant edge routers to be removed as bgp-neighbours to this + # peer if it's associated with specific ESG. + neighbours = [] + droute = default_route(nbr['bgpNeighbour']['ipAddress']) + for binding in bgp_bindings: + try: + self._nsxv.remove_bgp_neighbours(binding['edge_id'], [nbr], + default_routes=[droute]) + except vcns_exc.VcnsApiException: + LOG.error("Failed to remove BGP neighbour on '%s'", + binding['edge_id']) + else: + nbr = gw_bgp_neighbour(binding['bgp_identifier'], + speaker['local_as'], + self._edge_password) + neighbours.append(nbr) + LOG.debug("Succesfully removed BGP neighbor '%s' on '%s'", + bgp_peer_obj['peer_ip'], binding['edge_id']) + + if bgp_peer_obj.get('esg_id'): + edge_gw = bgp_peer_obj['esg_id'] + try: + self._nsxv.remove_bgp_neighbours(edge_gw, neighbours) + except vcns_exc.VcnsApiException: + LOG.error("Failed to remove BGP neighbour on GW Edge '%s'", + edge_gw) + + def add_gateway_network(self, context, bgp_speaker_id, network_info): + gateway_network_id = network_info['network_id'] + ext_net = self._core_plugin.get_network(context, gateway_network_id) + + if not ext_net['subnets']: + return + + subnet_id = ext_net['subnets'][0] + ext_subnet = self._core_plugin.get_subnet(context, subnet_id) + + if ext_subnet.get('gateway_ip'): + raise ext_esg_peer.ExternalSubnetHasGW( + network_id=gateway_network_id, subnet_id=subnet_id) + + edge_router_dict = self._get_dynamic_routing_edge_list( + context, gateway_network_id) + + speaker = self._plugin.get_bgp_speaker(context, bgp_speaker_id) + bgp_peers = self._plugin.get_bgp_peers_by_bgp_speaker( + context, bgp_speaker_id) + neighbours = [] + for edge_id, edge_router_config in edge_router_dict.items(): + router_ids = edge_router_config['no_snat_routers'] + advertise_static_routes = ( + edge_router_config['advertise_static_routes']) + subnets = self._query_tenant_subnets(context, router_ids) + # router_id here is in IP address format and is required for + # the BGP configuration. + bgp_identifier = edge_router_config['bgp_identifier'] + try: + self._start_bgp_on_edge(context, edge_id, speaker, + bgp_peers, bgp_identifier, subnets, + advertise_static_routes) + except vcns_exc.VcnsApiException: + LOG.error("Failed to configure BGP speaker %s on edge '%s'.", + bgp_speaker_id) + else: + nbr = gw_bgp_neighbour(bgp_identifier, speaker['local_as'], + self._edge_password) + neighbours.append(nbr) + + for edge_gw in [peer['esg_id'] for peer in bgp_peers + if peer.get('esg_id')]: + try: + self._nsxv.add_bgp_neighbours(edge_gw, neighbours) + except vcns_exc.VcnsApiException: + LOG.error("Failed to add BGP neighbour on GW Edge '%s'", + edge_gw) + + def _start_bgp_on_edge(self, context, edge_id, speaker, bgp_peers, + bgp_identifier, subnets, advertise_static_routes): + enabled_state = speaker['advertise_tenant_networks'] + local_as = speaker['local_as'] + prefixes, redis_rules = self._get_prefixes_and_redistribution_rules( + subnets, advertise_static_routes) + + bgp_neighbours = [bgp_neighbour(bgp_peer) for bgp_peer in bgp_peers] + default_routes = [default_route(peer['peer_ip']) for peer in bgp_peers] + try: + self._nsxv.add_bgp_speaker_config(edge_id, bgp_identifier, + local_as, enabled_state, + default_routes, + bgp_neighbours, prefixes, + redis_rules) + except vcns_exc.VcnsApiException: + with excutils.save_and_reraise_exception(): + LOG.error("Failed to configure BGP speaker %s on edge '%s'.", + speaker['id'], edge_id) + else: + nsxv_db.add_nsxv_bgp_speaker_binding(context.session, edge_id, + speaker['id'], bgp_identifier) + + def _stop_bgp_on_edges(self, context, bgp_bindings, speaker_id): + neighbours_to_remove = [] + speaker = self._plugin.get_bgp_speaker(context, speaker_id) + for bgp_binding in bgp_bindings: + edge_id = bgp_binding['edge_id'] + try: + self._nsxv.delete_bgp_speaker_config(edge_id) + except vcns_exc.VcnsApiException: + LOG.error("Failed to delete BGP speaker %s config on edge " + "%s.", speaker_id, edge_id) + else: + nsxv_db.delete_nsxv_bgp_speaker_binding(context.session, + edge_id) + nbr = gw_bgp_neighbour(bgp_binding['bgp_identifier'], + speaker['local_as'], + self._edge_password) + neighbours_to_remove.append(nbr) + + # We should also remove all bgp neighbours on gw-edges which + # corresponds with tenant routers that are associated with this bgp + # speaker. + bgp_peers = self._plugin.get_bgp_peers_by_bgp_speaker(context, + speaker_id) + gw_edges = [peer['esg_id'] for peer in bgp_peers if peer.get('esg_id')] + for gw_edge in gw_edges: + try: + self._nsxv.remove_bgp_neighbours(gw_edge, neighbours_to_remove) + except vcns_exc.VcnsApiException: + LOG.error("Failed to remove BGP neighbour on GW edge '%s'.", + gw_edge) + + def remove_gateway_network(self, context, bgp_speaker_id, network_info): + bgp_bindings = nsxv_db.get_nsxv_bgp_speaker_bindings( + context.session, bgp_speaker_id) + self._stop_bgp_on_edges(context, bgp_bindings, bgp_speaker_id) + + def enable_bgp_on_router(self, context, speaker, router_id): + edge_id, advertise_static_routes = ( + self._get_router_edge_info(context, router_id)) + if not edge_id: + # shared router is not attached on any edge + return + router = self._core_plugin._get_router(context, router_id) + if router.enable_snat: + subnets = [] + else: + subnets = self._query_tenant_subnets(context, [router_id]) + + bgp_peers = self._plugin.get_bgp_peers_by_bgp_speaker( + context, speaker['id']) + bgp_binding = nsxv_db.get_nsxv_bgp_speaker_binding( + context.session, edge_id) + + if bgp_binding and subnets: + # Edge already configured with BGP (e.g - shared router edge), + # Add the router attached subnets. + prefixes, redis_rules = ( + self._get_prefixes_and_redistribution_rules( + subnets, advertise_static_routes)) + self._nsxv.add_bgp_redistribution_rules(edge_id, prefixes, + redis_rules) + elif not bgp_binding: + gw_port = router.gw_port['fixed_ips'][0] + bgp_identifier = gw_port['ip_address'] + self._start_bgp_on_edge(context, edge_id, speaker, bgp_peers, + bgp_identifier, subnets, + advertise_static_routes) + nbr = gw_bgp_neighbour(bgp_identifier, speaker['local_as'], + self._edge_password) + for gw_edge_id in [peer['esg_id'] for peer in bgp_peers + if peer['esg_id']]: + self._nsxv.add_bgp_neighbours(gw_edge_id, [nbr]) + + def disable_bgp_on_router(self, context, speaker, router_id, gw_ip, + edge_id=None): + speaker = self._plugin.get_bgp_speaker(context, speaker['id']) + current_edge_id, advertise_static_routes = ( + self._get_router_edge_info(context, router_id)) + edge_id = edge_id or current_edge_id + + if not edge_id: + return + + routers_ids = ( + self._core_plugin.edge_manager.get_routers_on_same_edge( + context, router_id)) + bgp_binding = nsxv_db.get_nsxv_bgp_speaker_binding(context.session, + edge_id) + if not bgp_binding: + return + + if len(routers_ids) > 1: + routers_ids.remove(router_id) + # Shared router, only remove prefixes and redistribution + # rules. + subnets = self._query_tenant_subnets(context, [router_id]) + prefixes = [self.prefix_name(subnet['id']) + for subnet in subnets] + self._nsxv.remove_bgp_redistribution_rules(edge_id, prefixes) + if bgp_binding['bgp_identifier'] == gw_ip: + router = self._core_plugin._get_router(context, routers_ids[0]) + new_bgp_identifier = ( + router.gw_port['fixed_ips'][0]['ip_address']) + with context.session.begin(subtransactions=True): + bgp_binding['bgp_identifier'] = new_bgp_identifier + self._nsxv.update_router_id(edge_id, new_bgp_identifier) + else: + self._stop_bgp_on_edges(context, [bgp_binding], speaker['id']) + + def advertise_subnet(self, context, speaker_id, router_id, subnet): + router = self._core_plugin._get_router(context, router_id) + if router.enable_snat: + # Do nothing, by default, only when advertisement is needed we add + # a new redistribution rule + return + + edge_id, advertise_static_routes = ( + self._get_router_edge_info(context, router_id)) + if not edge_id: + # shared router is not attached on any edge + return + prefixes, redis_rules = self._get_prefixes_and_redistribution_rules( + [subnet], advertise_static_routes) + self._nsxv.add_bgp_redistribution_rules(edge_id, prefixes, redis_rules) + + def withdraw_subnet(self, context, speaker_id, router_id, subnet_id): + router = self._core_plugin._get_router(context, router_id) + if router.enable_snat: + # Do nothing, by default, only when advertisement is needed we add + # a new redistribution rule + return + + edge_id, advertise_static_routes = ( + self._get_router_edge_info(context, router_id)) + prefix_name = self.prefix_name(subnet_id) + self._nsxv.remove_bgp_redistribution_rules(edge_id, [prefix_name]) diff --git a/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py b/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py index 5d2feb1528..10af0ab117 100644 --- a/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py +++ b/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py @@ -1474,3 +1474,128 @@ class FakeVcns(object): 'objectID': 'application-1001'}] return applications + + def update_dynamic_routing_service(self, edge_id, request_config): + header = {'status': 201} + response = { + 'routerId': '172.24.4.12', + 'ipPrefixes': { + 'ipPrefixes': [ + {'ipAddress': '10.0.0.0/24', + 'name': 'prefix-name'} + ] + } + } + return self.return_helper(header, response) + + def get_dynamic_routing_service(self, edge_id): + header = {'status': 200} + response = { + 'routerId': '172.24.4.12', + 'ipPrefixes': { + 'ipPrefixes': [ + {'ipAddress': '10.0.0.0/24', + 'name': 'prefix-name'} + ] + }, + 'logging': { + 'logLevel': 'info', + 'enable': False + }, + 'ecmp': False + } + return self.return_helper(header, response) + + def update_bgp_dynamic_routing(self, edge_id, bgp_request): + header = {"status": 201} + response = { + "localAS": 65000, + "enabled": True, + "bgpNeighbours": { + "bgpNeighbours": [ + { + "bgpFilters": { + "bgpFilters": [ + { + "action": "deny", + "direction": "in" + } + ] + }, + "password": None, + "ipAddress": "172.24.4.253", + "remoteAS": 65000 + } + ] + }, + "redistribution": { + "rules": { + "rules": [ + { + "action": "deny", + "from": { + "bgp": False, + "connected": False, + "static": False, + "ospf": False + }, + "id": 0 + }, + { + "action": "permit", + "from": { + "bgp": False, + "connected": True, + "static": True, + "ospf": False + }, + "id": 1, + "prefixName": "eee4eb79-359e-4416" + } + ] + }, + "enabled": True + } + } + return self.return_helper(header, response) + + def get_bgp_routing_config(self, edge_id): + header = {'status': 200} + response = { + "localAS": 65000, + "enabled": True, + "redistribution": { + "rules": { + "rules": [ + { + "action": "deny", + "from": { + "bgp": False, + "connected": False, + "static": False, + "ospf": False + }, + "id": 0 + }, + { + "action": "permit", + "from": { + "bgp": False, + "connected": True, + "static": True, + "ospf": False + }, + "id": 1, + "prefixName": "eee4eb79-359e-4416" + } + ] + }, + "enabled": True + } + } + return self.return_helper(header, response) + + def delete_bgp_routing_config(self, edge_id): + header = {'status': 200} + response = '' + return header, response diff --git a/vmware_nsx/tests/unit/services/dynamic_routing/__init__.py b/vmware_nsx/tests/unit/services/dynamic_routing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/tests/unit/services/dynamic_routing/test_nsxv_bgp_driver.py b/vmware_nsx/tests/unit/services/dynamic_routing/test_nsxv_bgp_driver.py new file mode 100644 index 0000000000..d8fe98735c --- /dev/null +++ b/vmware_nsx/tests/unit/services/dynamic_routing/test_nsxv_bgp_driver.py @@ -0,0 +1,59 @@ +# Copyright 2017 VMware, Inc. +# 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 mock + +from neutron.tests import base +from neutron_dynamic_routing.db import bgp_db # noqa +from neutron_lib import context +from neutron_lib import exceptions as n_exc +from neutron_lib.plugins import directory + +from vmware_nsx.services.dynamic_routing.nsx_v import driver as nsxv_driver + + +class TestNSXvBgpDriver(base.BaseTestCase): + + def setUp(self): + super(TestNSXvBgpDriver, self).setUp() + self.context = context.get_admin_context() + with mock.patch.object(directory, 'get_plugin', + new=mock.Mock()): + self.provider = nsxv_driver.NSXvBgpDriver(mock.MagicMock()) + + def test_create_v6_bgp_speaker(self): + fake_bgp_speaker = { + "bgp_speaker": { + "ip_version": 6, + "local_as": "1000", + "name": "bgp-speaker" + } + } + self.assertRaises(n_exc.InvalidInput, + self.provider.create_bgp_speaker, + self.context, fake_bgp_speaker) + + def test_create_v6_bgp_peer(self): + fake_bgp_peer = { + "bgp_peer": { + "auth_type": "none", + "remote_as": "1000", + "name": "bgp-peer", + "peer_ip": "fc00::/7" + } + } + self.assertRaises(n_exc.InvalidInput, + self.provider.create_bgp_peer, + self.context, fake_bgp_peer)