NSX|V3: VPNaaS support

New support for VPNaaS on NSX-V3 2.2
Creating a vpn service per neutron service,
and ike/ipsec/dpd policies + endpoints + connection per neutron connection

Change-Id: Iad3778c1d826ae67f1b602625f5be0fe2f4c8fe3
This commit is contained in:
Adit Sarfaty 2017-11-08 10:16:17 +02:00 committed by garyk
parent 6ddff8c110
commit b993b7f4c0
13 changed files with 1587 additions and 7 deletions

View File

@ -211,6 +211,16 @@ Configure the service provider::
[service_providers] [service_providers]
service_provider = LOADBALANCERV2:VMWareEdge:neutron_lbaas.drivers.vmware.edge_driver_v2.EdgeLoadBalancerDriverV2:default service_provider = LOADBALANCERV2:VMWareEdge:neutron_lbaas.drivers.vmware.edge_driver_v2.EdgeLoadBalancerDriverV2:default
Neutron VPNaaS
~~~~~~~~~~~~~~
Add neutron-vpnaas repo as an external repository and configure following flags in ``local.conf``::
[[local|localrc]]
enable_plugin neutron-vpnaas https://git.openstack.org/openstack/neutron-vpnaas
NEUTRON_VPNAAS_SERVICE_PROVIDER=VPN:vmware:vmware_nsx.services.vpnaas.nsxv3.ipsec_driver.NSXv3IPsecVpnDriver:default
NSX-TVD NSX-TVD
------- -------

View File

@ -0,0 +1,6 @@
---
prelude: >
Support VPN-as-a-Service for VPN IPSEC in NSXv3 plugin.
features:
- |
NSXv3 plugin now supports VPN SEC through VPNaaS plugin.

View File

@ -692,3 +692,31 @@ def get_project_plugin_mapping(session, project):
def get_project_plugin_mappings(session): def get_project_plugin_mappings(session):
return session.query(nsx_models.NsxProjectPluginMapping).all() return session.query(nsx_models.NsxProjectPluginMapping).all()
def add_nsx_vpn_connection_mapping(session, neutron_id, session_id,
dpd_profile_id, ike_profile_id,
ipsec_profile_id, peer_ep_id):
with session.begin(subtransactions=True):
mapping = nsx_models.NsxVpnConnectionMapping(
neutron_id=neutron_id,
session_id=session_id,
dpd_profile_id=dpd_profile_id,
ike_profile_id=ike_profile_id,
ipsec_profile_id=ipsec_profile_id,
peer_ep_id=peer_ep_id)
session.add(mapping)
return mapping
def get_nsx_vpn_connection_mapping(session, neutron_id):
try:
return (session.query(nsx_models.NsxVpnConnectionMapping).
filter_by(neutron_id=neutron_id).one())
except exc.NoResultFound:
return
def delete_nsx_vpn_connection_mapping(session, neutron_id):
return (session.query(nsx_models.NsxVpnConnectionMapping).
filter_by(neutron_id=neutron_id).delete())

View File

@ -1 +1 @@
9799427fc0e1 0dbeda408e41

View File

@ -0,0 +1,43 @@
# 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.
"""nsxv3_vpn_mapping
Revision ID: 0dbeda408e41
Revises: 9799427fc0e1
Create Date: 2017-11-26 12:27:40.846088
"""
# revision identifiers, used by Alembic.
revision = '0dbeda408e41'
down_revision = '9799427fc0e1'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table(
'neutron_nsx_vpn_connection_mappings',
sa.Column('neutron_id', sa.String(36), nullable=False),
sa.Column('session_id', sa.String(36), nullable=False),
sa.Column('dpd_profile_id', sa.String(36), nullable=False),
sa.Column('ike_profile_id', sa.String(36), nullable=False),
sa.Column('ipsec_profile_id', sa.String(36), nullable=False),
sa.Column('peer_ep_id', sa.String(36), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('neutron_id'))

View File

@ -488,3 +488,14 @@ class NsxProjectPluginMapping(model_base.BASEV2, models.TimestampMixin):
__tablename__ = 'nsx_project_plugin_mappings' __tablename__ = 'nsx_project_plugin_mappings'
project = sa.Column(sa.String(36), primary_key=True) project = sa.Column(sa.String(36), primary_key=True)
plugin = sa.Column(sa.Enum('dvs', 'nsx-v', 'nsx-t'), nullable=False) plugin = sa.Column(sa.Enum('dvs', 'nsx-v', 'nsx-t'), nullable=False)
class NsxVpnConnectionMapping(model_base.BASEV2, models.TimestampMixin):
"""Stores the mapping between VPNaaS connections and NSX objects"""
__tablename__ = 'neutron_nsx_vpn_connection_mappings'
neutron_id = sa.Column(sa.String(36), primary_key=True)
session_id = sa.Column(sa.String(36), nullable=False)
dpd_profile_id = sa.Column(sa.String(36), nullable=False)
ike_profile_id = sa.Column(sa.String(36), nullable=False)
ipsec_profile_id = sa.Column(sa.String(36), nullable=False)
peer_ep_id = sa.Column(sa.String(36), nullable=False)

View File

@ -24,6 +24,8 @@ from neutron_lib.api.validators import availability_zone as az_validator
from neutron_lib.exceptions import allowedaddresspairs as addr_exc from neutron_lib.exceptions import allowedaddresspairs as addr_exc
from neutron_lib.exceptions import l3 as l3_exc from neutron_lib.exceptions import l3 as l3_exc
from neutron_lib.exceptions import port_security as psec_exc from neutron_lib.exceptions import port_security as psec_exc
from neutron_lib.plugins import constants as plugin_const
from neutron_lib.plugins import directory
from neutron_lib.services.qos import constants as qos_consts from neutron_lib.services.qos import constants as qos_consts
from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api
@ -3125,7 +3127,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
return (ipaddress, netmask, nexthop) return (ipaddress, netmask, nexthop)
def _get_tier0_uuid_by_net(self, context, network_id): def _get_tier0_uuid_by_router(self, context, router):
network_id = router.gw_port_id and router.gw_port.network_id
if not network_id: if not network_id:
return return
network = self.get_network(context, network_id) network = self.get_network(context, network_id)
@ -3136,10 +3139,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
def _update_router_gw_info(self, context, router_id, info): def _update_router_gw_info(self, context, router_id, info):
router = self._get_router(context, router_id) router = self._get_router(context, router_id)
org_ext_net_id = router.gw_port_id and router.gw_port.network_id org_tier0_uuid = self._get_tier0_uuid_by_router(context, router)
org_tier0_uuid = self._get_tier0_uuid_by_net(context, org_ext_net_id)
org_enable_snat = router.enable_snat org_enable_snat = router.enable_snat
new_ext_net_id = info and info.get('network_id')
orgaddr, orgmask, _orgnexthop = ( orgaddr, orgmask, _orgnexthop = (
self._get_external_attachment_info( self._get_external_attachment_info(
context, router)) context, router))
@ -3159,8 +3160,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
super(NsxV3Plugin, self)._update_router_gw_info( super(NsxV3Plugin, self)._update_router_gw_info(
context, router_id, info, router=router) context, router_id, info, router=router)
new_ext_net_id = router.gw_port_id and router.gw_port.network_id new_tier0_uuid = self._get_tier0_uuid_by_router(context, router)
new_tier0_uuid = self._get_tier0_uuid_by_net(context, new_ext_net_id)
new_enable_snat = router.enable_snat new_enable_snat = router.enable_snat
newaddr, newmask, _newnexthop = ( newaddr, newmask, _newnexthop = (
self._get_external_attachment_info( self._get_external_attachment_info(
@ -3600,6 +3600,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
(but not both) and include the source/dest nsx logical port. (but not both) and include the source/dest nsx logical port.
""" """
extra_rules = [] extra_rules = []
# DHCP relay rules: # DHCP relay rules:
# get the list of relevant relay servers # get the list of relevant relay servers
elv_ctx = context.elevated() elv_ctx = context.elevated()
@ -3651,6 +3652,16 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
'services': dhcp_services, 'services': dhcp_services,
'direction': 'OUT'}) 'direction': 'OUT'})
# VPN rules:
vpn_plugin = directory.get_plugin(plugin_const.VPN)
if vpn_plugin:
vpn_driver = vpn_plugin.drivers[vpn_plugin.default_provider]
vpn_rules = (
vpn_driver._generate_ipsecvpn_firewall_rules(
context, router_id))
if vpn_rules:
extra_rules.extend(vpn_rules)
return extra_rules return extra_rules
def _get_ports_and_address_groups(self, context, router_id, network_id, def _get_ports_and_address_groups(self, context, router_id, network_id,

View File

@ -0,0 +1,645 @@
# 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_log import log as logging
from oslo_utils import excutils
from neutron_lib.callbacks import events
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
from neutron_lib import constants
from neutron_lib.plugins import directory
from neutron_vpnaas.services.vpn import service_drivers
from vmware_nsx.common import exceptions as nsx_exc
from vmware_nsx.db import db
from vmware_nsx.services.vpnaas.nsxv3 import ipsec_utils
from vmware_nsx.services.vpnaas.nsxv3 import ipsec_validator
from vmware_nsxlib.v3 import exceptions as nsx_lib_exc
from vmware_nsxlib.v3 import nsx_constants as consts
from vmware_nsxlib.v3 import vpn_ipsec
LOG = logging.getLogger(__name__)
IPSEC = 'ipsec'
class NSXv3IPsecVpnDriver(service_drivers.VpnDriver):
def __init__(self, service_plugin):
self.vpn_plugin = service_plugin
self._core_plugin = directory.get_plugin()
self._nsxlib = self._core_plugin.nsxlib
self._nsx_vpn = self._nsxlib.vpn_ipsec
validator = ipsec_validator.IPsecV3Validator(service_plugin)
super(NSXv3IPsecVpnDriver, self).__init__(service_plugin, validator)
registry.subscribe(
self._delete_local_endpoint, resources.ROUTER_GATEWAY,
events.AFTER_DELETE)
@property
def l3_plugin(self):
return self._core_plugin
@property
def service_type(self):
return IPSEC
def _translate_cidr(self, cidr):
return self._nsxlib.firewall_section.get_ip_cidr_reference(
cidr,
consts.IPV6 if netaddr.valid_ipv6(cidr) else consts.IPV4)
def _translate_addresses_to_target(self, cidrs):
return [self._translate_cidr(ip) for ip in cidrs]
def _generate_ipsecvpn_firewall_rules(self, context, router_id):
"""Return the firewall rules needed to allow vpn traffic"""
fw_rules = []
# get all the active services of this router
filters = {'router_id': [router_id],
'status': [constants.ACTIVE]}
services = self.vpn_plugin.get_vpnservices(
context.elevated(), filters=filters)
if not services:
return fw_rules
for srv in services:
subnet = self.l3_plugin.get_subnet(
context.elevated(), srv['subnet_id'])
local_cidrs = [subnet['cidr']]
# get all the active connections of this service
filters = {'vpnservice_id': [srv['id']],
'status': [constants.ACTIVE]}
connections = self.vpn_plugin.get_ipsec_site_connections(
context.elevated(), filters=filters)
for conn in connections:
peer_cidrs = conn['peer_cidrs']
fw_rules.append({
'display_name': 'VPN connection ' + conn['id'],
'action': consts.FW_ACTION_ALLOW,
'sources': self._translate_addresses_to_target(
peer_cidrs),
'destinations': self._translate_addresses_to_target(
local_cidrs)})
return fw_rules
def _update_firewall_rules(self, context, vpnservice):
LOG.debug("Updating vpn firewall rules for router %s",
vpnservice['router_id'])
self._core_plugin.update_router_firewall(
context, vpnservice['router_id'])
def _update_router_advertisement(self, context, vpnservice):
LOG.debug("Updating router advertisement rules for router %s",
vpnservice['router_id'])
router_id = vpnservice['router_id']
# skip no-snat router as it is already advertised,
# and router with no gw
rtr = self.l3_plugin.get_router(context, router_id)
if (not rtr.get('external_gateway_info') or
not rtr['external_gateway_info'].get('enable_snat', True)):
return
rules = []
# get all the active services of this router
filters = {'router_id': [router_id], 'status': [constants.ACTIVE]}
services = self.vpn_plugin.get_vpnservices(
context.elevated(), filters=filters)
for srv in services:
# use only services with active connections
filters = {'vpnservice_id': [srv['id']],
'status': [constants.ACTIVE]}
connections = self.vpn_plugin.get_ipsec_site_connections(
context.elevated(), filters=filters)
if not connections:
continue
subnet = self.l3_plugin.get_subnet(
context.elevated(), srv['subnet_id'])
rules.append({
'display_name': 'VPN advertisement service ' + srv['id'],
'action': consts.FW_ACTION_ALLOW,
'networks': [subnet['cidr']]})
logical_router_id = db.get_nsx_router_id(context.session, router_id)
self._nsxlib.logical_router.update_advertisement_rules(
logical_router_id, rules)
def _update_status(self, context, vpn_service_id, ipsec_site_conn_id,
status, updated_pending_status=True):
ipsec_site_conn = {'status': status,
'updated_pending_status': updated_pending_status}
vpn_status = {'id': vpn_service_id,
'updated_pending_status': updated_pending_status,
'status': status,
'ipsec_site_connections': {ipsec_site_conn_id:
ipsec_site_conn}}
status_list = [vpn_status]
self.service_plugin.update_status_by_agent(context, status_list)
def _nsx_tags(self, context, connection):
return self._nsxlib.build_v3_tags_payload(
connection, resource_type='os-vpn-connection-id',
project_name=context.tenant_name)
def _nsx_tags_for_reused(self):
# Service & Local endpoint can be reused cross tenants,
# so we do not add the tenant/object id.
return self._nsxlib.build_v3_api_version_tag()
def _create_ike_profile(self, context, connection):
"""Create an ike profile for a connection"""
# Note(asarfaty) the NSX profile can be reused, so we can consider
# creating it only once in the future, and keeping a use-count for it.
# There is no driver callback for profiles creation so it has to be
# done on connection creation.
ike_policy_id = connection['ikepolicy_id']
ikepolicy = self.vpn_plugin.get_ikepolicy(context, ike_policy_id)
try:
profile = self._nsx_vpn.ike_profile.create(
ikepolicy['name'],
description=ikepolicy['description'],
encryption_algorithm=ipsec_utils.ENCRYPTION_ALGORITHM_MAP[
ikepolicy['encryption_algorithm']],
digest_algorithm=ipsec_utils.AUTH_ALGORITHM_MAP[
ikepolicy['auth_algorithm']],
ike_version=ipsec_utils.IKE_VERSION_MAP[
ikepolicy['ike_version']],
dh_group=ipsec_utils.PFS_MAP[ikepolicy['pfs']],
pfs=True,
sa_life_time=ikepolicy['lifetime']['value'],
tags=self._nsx_tags(context, connection))
except nsx_lib_exc.ManagerError as e:
msg = _("Failed to create an ike profile: %s") % e
raise nsx_exc.NsxPluginException(err_msg=msg)
return profile['id']
def _delete_ike_profile(self, ikeprofile_id):
self._nsx_vpn.ike_profile.delete(ikeprofile_id)
def _create_ipsec_profile(self, context, connection):
"""Create an ipsec profile for a connection"""
# Note(asarfaty) the NSX profile can be reused, so we can consider
# creating it only once in the future, and keeping a use-count for it.
# There is no driver callback for profiles creation so it has to be
# done on connection creation.
ipsec_policy_id = connection['ipsecpolicy_id']
ipsecpolicy = self.vpn_plugin.get_ipsecpolicy(
context, ipsec_policy_id)
try:
profile = self._nsx_vpn.tunnel_profile.create(
ipsecpolicy['name'],
description=ipsecpolicy['description'],
encryption_algorithm=ipsec_utils.ENCRYPTION_ALGORITHM_MAP[
ipsecpolicy['encryption_algorithm']],
digest_algorithm=ipsec_utils.AUTH_ALGORITHM_MAP[
ipsecpolicy['auth_algorithm']],
dh_group=ipsec_utils.PFS_MAP[ipsecpolicy['pfs']],
pfs=True,
sa_life_time=ipsecpolicy['lifetime']['value'],
tags=self._nsx_tags(context, connection))
except nsx_lib_exc.ManagerError as e:
msg = _("Failed to create a tunnel profile: %s") % e
raise nsx_exc.NsxPluginException(err_msg=msg)
return profile['id']
def _delete_ipsec_profile(self, ipsecprofile_id):
self._nsx_vpn.tunnel_profile.delete(ipsecprofile_id)
def _create_dpd_profile(self, context, connection):
dpd_info = connection['dpd']
try:
profile = self._nsx_vpn.dpd_profile.create(
connection['name'][:240] + '-dpd-profile',
description='neutron dpd profile',
timeout=dpd_info.get('timeout'),
enabled=True if dpd_info.get('action') == 'hold' else False,
tags=self._nsx_tags(context, connection))
except nsx_lib_exc.ManagerError as e:
msg = _("Failed to create a DPD profile: %s") % e
raise nsx_exc.NsxPluginException(err_msg=msg)
return profile['id']
def _delete_dpd_profile(self, dpdprofile_id):
self._nsx_vpn.dpd_profile.delete(dpdprofile_id)
def _update_dpd_profile(self, connection, dpdprofile_id):
dpd_info = connection['dpd']
self._nsx_vpn.dpd_profile.update(dpdprofile_id,
timeout=dpd_info.get('timeout'),
enabled=True if dpd_info.get('action') == 'hold' else False)
def _create_peer_endpoint(self, context, connection, ikeprofile_id,
ipsecprofile_id, dpdprofile_id):
default_auth = vpn_ipsec.AuthenticationModeTypes.AUTH_MODE_PSK
try:
peer_endpoint = self._nsx_vpn.peer_endpoint.create(
connection['name'],
connection['peer_address'],
connection['peer_id'],
description=connection['description'],
authentication_mode=default_auth,
dpd_profile_id=dpdprofile_id,
ike_profile_id=ikeprofile_id,
ipsec_tunnel_profile_id=ipsecprofile_id,
connection_initiation_mode=ipsec_utils.INITIATION_MODE_MAP[
connection['initiator']],
psk=connection['psk'],
tags=self._nsx_tags(context, connection))
except nsx_lib_exc.ManagerError as e:
msg = _("Failed to create a peer endpoint: %s") % e
raise nsx_exc.NsxPluginException(err_msg=msg)
return peer_endpoint['id']
def _update_peer_endpoint(self, peer_ep_id, connection):
self._nsx_vpn.peer_endpoint.update(
peer_ep_id,
name=connection['name'],
peer_address=connection['peer_address'],
peer_id=connection['peer_id'],
description=connection['description'],
connection_initiation_mode=ipsec_utils.INITIATION_MODE_MAP[
connection['initiator']],
psk=connection['psk'])
def _delete_peer_endpoint(self, peer_ep_id):
self._nsx_vpn.peer_endpoint.delete(peer_ep_id)
def _get_profiles_from_peer_endpoint(self, peer_ep_id):
peer_ep = self._nsx_vpn.peer_endpoint.get(peer_ep_id)
return (
peer_ep['ike_profile_id'],
peer_ep['ipsec_tunnel_profile_id'],
peer_ep['dpd_profile_id'])
def _create_local_endpoint(self, context, local_addr, nsx_service_id,
router_id):
"""Creating an NSX local endpoint for a logical router
This endpoint can be reused by other connections, and will be deleted
when the router is deleted or gateway is removed
"""
# Add the neutron router-id to the tags to help search later
tags = self._nsxlib.build_v3_tags_payload(
{'id': router_id}, resource_type='os-neutron-router-id',
project_name=context.tenant_name)
try:
local_endpoint = self._nsx_vpn.local_endpoint.create(
'Local endpoint for OS VPNaaS',
local_addr,
nsx_service_id,
tags=tags)
except nsx_lib_exc.ManagerError as e:
msg = _("Failed to create a local endpoint: %s") % e
raise nsx_exc.NsxPluginException(err_msg=msg)
return local_endpoint['id']
def _search_local_endpint(self, router_id):
tags = [{'scope': 'os-neutron-router-id', 'tag': router_id}]
ep_list = self._nsxlib.search_by_tags(
tags=tags,
resource_type=self._nsx_vpn.local_endpoint.resource_type)
if ep_list['results']:
return ep_list['results'][0]['id']
def _get_local_endpoint(self, context, connection, vpnservice):
"""Get the id of the local endpoint for a service
The NSX allows only one local endpoint per local address
This method will create it if there is not matching endpoint
"""
# use the router GW as the local ip
router_id = vpnservice['router']['id']
# check if we already have this endpoint on the NSX
local_ep_id = self._search_local_endpint(router_id)
if local_ep_id:
return local_ep_id
# create a new one
local_addr = self._get_router_ext_gw(context, router_id)
nsx_service_id = self._get_nsx_vpn_service(context, vpnservice)
local_ep_id = self._create_local_endpoint(
context, local_addr, nsx_service_id, router_id)
return local_ep_id
def _delete_local_endpoint(self, resource, event, trigger, **kwargs):
"""Upon router deletion / gw removal delete the matching endpoint"""
router_id = kwargs.get('router_id')
local_ep_id = self._search_local_endpint(router_id)
if local_ep_id:
self._nsx_vpn.local_endpoint.delete(local_ep_id)
def _get_session_rules(self, context, connection, vpnservice):
# TODO(asarfaty): support vpn-endpoint-groups too
peer_cidrs = connection['peer_cidrs']
local_cidrs = [vpnservice['subnet']['cidr']]
rule = self._nsx_vpn.session.get_rule_obj(local_cidrs, peer_cidrs)
return [rule]
def _create_session(self, context, connection, local_ep_id,
peer_ep_id, rules):
try:
session = self._nsx_vpn.session.create(
connection['name'], local_ep_id, peer_ep_id, rules,
description=connection['description'],
tags=self._nsx_tags(context, connection))
except nsx_lib_exc.ManagerError as e:
msg = _("Failed to create a session: %s") % e
raise nsx_exc.NsxPluginException(err_msg=msg)
return session['id']
def _update_session(self, session_id, connection, rules):
self._nsx_vpn.session.update(
session_id,
name=connection['name'],
description=connection['description'],
policy_rules=rules)
def _delete_session(self, session_id):
self._nsx_vpn.session.delete(session_id)
def create_ipsec_site_connection(self, context, ipsec_site_conn):
LOG.debug('Creating ipsec site connection %(conn_info)s.',
{"conn_info": ipsec_site_conn})
# Note(asarfaty) the plugin already calls the validator
# which also validated the policies and service
ikeprofile_id = None
ipsecprofile_id = None
dpdprofile_id = None
peer_ep_id = None
session_id = None
vpnservice_id = ipsec_site_conn['vpnservice_id']
vpnservice = self.service_plugin._get_vpnservice(
context, vpnservice_id)
ipsec_id = ipsec_site_conn["id"]
try:
# create the ike profile
ikeprofile_id = self._create_ike_profile(
context, ipsec_site_conn)
LOG.debug("Created NSX ike profile %s", ikeprofile_id)
# create the ipsec profile
ipsecprofile_id = self._create_ipsec_profile(
context, ipsec_site_conn)
LOG.debug("Created NSX ipsec profile %s", ipsecprofile_id)
# create the dpd profile
dpdprofile_id = self._create_dpd_profile(
context, ipsec_site_conn)
LOG.debug("Created NSX dpd profile %s", dpdprofile_id)
# create the peer endpoint and add to the DB
peer_ep_id = self._create_peer_endpoint(
context, ipsec_site_conn,
ikeprofile_id, ipsecprofile_id, dpdprofile_id)
LOG.debug("Created NSX peer endpoint %s", peer_ep_id)
# create or reuse a local endpoint using the vpn service
local_ep_id = self._get_local_endpoint(
context, ipsec_site_conn, vpnservice)
# Finally: create the session with policy rules
rules = self._get_session_rules(
context, ipsec_site_conn, vpnservice)
session_id = self._create_session(
context, ipsec_site_conn, local_ep_id, peer_ep_id, rules)
# update the DB with the session id
db.add_nsx_vpn_connection_mapping(
context.session, ipsec_site_conn['id'], session_id,
dpdprofile_id, ikeprofile_id, ipsecprofile_id, peer_ep_id)
self._update_status(context, vpnservice_id, ipsec_id,
constants.ACTIVE)
except nsx_exc.NsxPluginException:
with excutils.save_and_reraise_exception():
self._update_status(context, vpnservice_id, ipsec_id,
constants.ERROR)
# delete the NSX objects that were already created
# Do not delete reused objects: service, local endpoint
if session_id:
self._delete_session(session_id)
if peer_ep_id:
self._delete_peer_endpoint(peer_ep_id)
if dpdprofile_id:
self._delete_dpd_profile(dpdprofile_id)
if ipsecprofile_id:
self._delete_ipsec_profile(ipsecprofile_id)
if ikeprofile_id:
self._delete_ike_profile(ikeprofile_id)
# update router firewall rules
self._update_firewall_rules(context, vpnservice)
# update router advertisement rules
self._update_router_advertisement(context, vpnservice)
def delete_ipsec_site_connection(self, context, ipsec_site_conn):
LOG.debug('Deleting ipsec site connection %(site)s.',
{"site": ipsec_site_conn})
vpnservice_id = ipsec_site_conn['vpnservice_id']
vpnservice = self.service_plugin._get_vpnservice(
context, vpnservice_id)
# get all data from the nsx based on the connection id in the DB
mapping = db.get_nsx_vpn_connection_mapping(
context.session, ipsec_site_conn['id'])
if not mapping:
LOG.warning("Couldn't find nsx ids for VPN connection %s",
ipsec_site_conn['id'])
# Do not fail the deletion
return
if mapping['session_id']:
self._delete_session(mapping['session_id'])
if mapping['peer_ep_id']:
self._delete_peer_endpoint(mapping['peer_ep_id'])
if mapping['dpd_profile_id']:
self._delete_dpd_profile(mapping['dpd_profile_id'])
if mapping['ipsec_profile_id']:
self._delete_ipsec_profile(mapping['ipsec_profile_id'])
if mapping['ike_profile_id']:
self._delete_ike_profile(mapping['ike_profile_id'])
# Do not delete the local endpoint and service as they are reused
db.delete_nsx_vpn_connection_mapping(context.session,
ipsec_site_conn['id'])
# update router firewall rules
self._update_firewall_rules(context, vpnservice)
# update router advertisement rules
self._update_router_advertisement(context, vpnservice)
def update_ipsec_site_connection(self, context, old_ipsec_conn,
ipsec_site_conn):
LOG.debug('Updating ipsec site connection new %(site)s.',
{"site": ipsec_site_conn})
LOG.debug('Updating ipsec site connection old %(site)s.',
{"site": old_ipsec_conn})
# Note(asarfaty) the plugin already calls the validator
# which also validated the policies and service
ipsec_id = old_ipsec_conn['id']
vpnservice_id = old_ipsec_conn['vpnservice_id']
vpnservice = self.service_plugin._get_vpnservice(
context, vpnservice_id)
mapping = db.get_nsx_vpn_connection_mapping(
context.session, ipsec_site_conn['id'])
if not mapping:
LOG.error("Couldn't find nsx ids for VPN connection %s",
ipsec_site_conn['id'])
self._update_status(context, vpnservice_id, ipsec_id, "ERROR")
raise nsx_exc.NsxIPsecVpnMappingNotFound(conn=ipsec_id)
update_all = (old_ipsec_conn['name'] != ipsec_site_conn['name'] or
old_ipsec_conn['description'] !=
ipsec_site_conn['description'])
# check if the dpd configuration changed
old_dpd = old_ipsec_conn['dpd']
new_dpd = ipsec_site_conn['dpd']
if (old_dpd['action'] != new_dpd['action'] or
old_dpd['timeout'] != new_dpd['timeout'] or
update_all):
self._update_dpd_profile(ipsec_site_conn,
mapping['dpd_profile_id'])
# update peer endpoint with all the parameters that could be modified
# Note(asarfaty): local endpoints are reusable and will not be updated
self._update_peer_endpoint(mapping['peer_ep_id'], ipsec_site_conn)
rules = self._get_session_rules(
context, ipsec_site_conn, vpnservice)
self._update_session(mapping['session_id'], ipsec_site_conn, rules)
if 'peer_cidrs' in ipsec_site_conn:
# Update firewall
self._update_firewall_rules(context, vpnservice)
# No service updates. No need to update router advertisement rules
def _get_gateway_ips(self, router):
"""Obtain the IPv4 and/or IPv6 GW IP for the router.
If there are multiples, (arbitrarily) use the first one.
"""
v4_ip = v6_ip = None
for fixed_ip in router.gw_port['fixed_ips']:
addr = fixed_ip['ip_address']
vers = netaddr.IPAddress(addr).version
if vers == 4:
if v4_ip is None:
v4_ip = addr
elif v6_ip is None:
v6_ip = addr
return v4_ip, v6_ip
def _create_vpn_service(self, tier0_uuid):
try:
service = self._nsx_vpn.service.create(
'Neutron VPN service for router ' + tier0_uuid,
tier0_uuid,
enabled=True,
ike_log_level=ipsec_utils.DEFAULT_LOG_LEVEL,
tags=self._nsx_tags_for_reused())
except nsx_lib_exc.ManagerError as e:
msg = _("Failed to create vpn service: %s") % e
raise nsx_exc.NsxPluginException(err_msg=msg)
return service['id']
def _get_tier0_uuid(self, context, router_id):
router_db = self._core_plugin._get_router(context, router_id)
return self._core_plugin._get_tier0_uuid_by_router(context, router_db)
def _get_router_ext_gw(self, context, router_id):
router_db = self._core_plugin.get_router(context, router_id)
gw = router_db['external_gateway_info']
return gw['external_fixed_ips'][0]["ip_address"]
def _find_vpn_service(self, tier0_uuid):
# find the service for the tier0 router in the NSX.
# Note(asarfaty) we expect only a small number of services
services = self._nsx_vpn.service.list()['results']
for srv in services:
if srv['logical_router_id']['target_id'] == tier0_uuid:
# if it exists but disabled: issue an error
if not srv.get('enabled', True):
msg = _("NSX vpn service %s must be enabled") % srv['id']
raise nsx_exc.NsxPluginException(err_msg=msg)
return srv['id']
def _create_vpn_service_if_needed(self, context, vpnservice):
# The service is created on the TIER0 router attached to the router GW
# The NSX can keep only one service per tier0 router so we reuse it
router_id = vpnservice['router_id']
tier0_uuid = self._get_tier0_uuid(context, router_id)
if self._find_vpn_service(tier0_uuid):
return
# create a new one
self._create_vpn_service(tier0_uuid)
def _get_nsx_vpn_service(self, context, vpnservice):
router_id = vpnservice['router_id']
tier0_uuid = self._get_tier0_uuid(context, router_id)
return self._find_vpn_service(tier0_uuid)
def create_vpnservice(self, context, vpnservice):
#TODO(asarfaty) support vpn-endpoint-group-create for local & peer
# cidrs too
LOG.debug('Creating VPN service %(vpn)s', {'vpn': vpnservice})
vpnservice_id = vpnservice['id']
try:
self.validator.validate_vpnservice(context, vpnservice)
except Exception:
with excutils.save_and_reraise_exception():
# Rolling back change on the neutron
self.service_plugin.delete_vpnservice(context, vpnservice_id)
vpnservice = self.service_plugin._get_vpnservice(context,
vpnservice_id)
v4_ip, v6_ip = self._get_gateway_ips(vpnservice.router)
if v4_ip:
vpnservice['external_v4_ip'] = v4_ip
if v6_ip:
vpnservice['external_v6_ip'] = v6_ip
self.service_plugin.set_external_tunnel_ips(context,
vpnservice_id,
v4_ip=v4_ip, v6_ip=v6_ip)
self._create_vpn_service_if_needed(context, vpnservice)
def update_vpnservice(self, context, old_vpnservice, vpnservice):
# No meaningful field can change here
pass
def delete_vpnservice(self, context, vpnservice):
# Do not delete the NSX service or DB entry as those will be reused.
pass

View File

@ -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.
from vmware_nsxlib.v3 import vpn_ipsec
ENCRYPTION_ALGORITHM_MAP = {
'aes-128': vpn_ipsec.EncryptionAlgorithmTypes.ENCRYPTION_ALGORITHM_128,
'aes-256': vpn_ipsec.EncryptionAlgorithmTypes.ENCRYPTION_ALGORITHM_256,
}
AUTH_ALGORITHM_MAP = {
'sha1': vpn_ipsec.DigestAlgorithmTypes.DIGEST_ALGORITHM_SHA1,
'sha256': vpn_ipsec.DigestAlgorithmTypes.DIGEST_ALGORITHM_SHA256,
}
PFS_MAP = {
'group2': vpn_ipsec.DHGroupTypes.DH_GROUP_2,
'group5': vpn_ipsec.DHGroupTypes.DH_GROUP_5,
'group14': vpn_ipsec.DHGroupTypes.DH_GROUP_14
}
IKE_VERSION_MAP = {
'v1': vpn_ipsec.IkeVersionTypes.IKE_VERSION_V1,
'v2': vpn_ipsec.IkeVersionTypes.IKE_VERSION_V2,
}
ENCAPSULATION_MODE_MAP = {
'tunnel': vpn_ipsec.EncapsulationModeTypes.ENCAPSULATION_MODE_TUNNEL
}
TRANSFORM_PROTOCOL_MAP = {
'esp': vpn_ipsec.TransformProtocolTypes.TRANSFORM_PROTOCOL_ESP
}
DPD_ACTION_MAP = {
'hold': vpn_ipsec.DpdProfileActionTypes.DPD_PROFILE_ACTION_HOLD,
'disabled': None
}
INITIATION_MODE_MAP = {
'bi-directional': (vpn_ipsec.ConnectionInitiationModeTypes.
INITIATION_MODE_INITIATOR),
'response-only': (vpn_ipsec.ConnectionInitiationModeTypes.
INITIATION_MODE_RESPOND_ONLY)
}
DEFAULT_LOG_LEVEL = vpn_ipsec.IkeLogLevelTypes.LOG_LEVEL_ERROR

View File

@ -0,0 +1,374 @@
# 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_log import log as logging
from neutron_lib import constants
from neutron_vpnaas.db.vpn import vpn_validator
from vmware_nsx._i18n import _
from vmware_nsx.common import exceptions as nsx_exc
from vmware_nsx.services.vpnaas.nsxv3 import ipsec_utils
from vmware_nsxlib.v3 import nsx_constants as consts
from vmware_nsxlib.v3 import vpn_ipsec
LOG = logging.getLogger(__name__)
class IPsecV3Validator(vpn_validator.VpnReferenceValidator):
"""Validator methods for Vmware NSX-V3 VPN support"""
def __init__(self, service_plugin):
super(IPsecV3Validator, self).__init__()
self.vpn_plugin = service_plugin
self.nsxlib = self.core_plugin.nsxlib
self.check_backend_version()
def check_backend_version(self):
if not self.nsxlib.feature_supported(consts.FEATURE_IPSEC_VPN):
# ipsec vpn is not supported
LOG.warning("VPNaaS is not supported by the NSX backend (version "
"%s)",
self.nsxlib.get_version())
self.backend_support = False
else:
self.backend_support = True
def _validate_backend_version(self):
if not self.backend_support:
msg = (_("VPNaaS is not supported by the NSX backend "
"(version %s)") % self.nsxlib.get_version())
raise nsx_exc.NsxVpnValidationError(details=msg)
def _validate_policy_lifetime(self, policy_info, policy_type):
"""NSX supports only units=seconds"""
lifetime = policy_info.get('lifetime')
if not lifetime:
return
if lifetime.get('units') != 'seconds':
msg = _("Unsupported policy lifetime %(val)s in %(pol)s policy. "
"Only seconds lifetime is supported.") % {
'val': lifetime, 'pol': policy_type}
raise nsx_exc.NsxVpnValidationError(details=msg)
value = lifetime.get('value')
if (value and (value < vpn_ipsec.SALifetimeLimits.SA_LIFETIME_MIN or
value > vpn_ipsec.SALifetimeLimits.SA_LIFETIME_MAX)):
msg = _("Unsupported policy lifetime %(value)s in %(pol)s policy. "
"Value range is [%(min)s-%(max)s].") % {
'value': value,
'pol': policy_type,
'min': vpn_ipsec.SALifetimeLimits.SA_LIFETIME_MIN,
'max': vpn_ipsec.SALifetimeLimits.SA_LIFETIME_MAX}
raise nsx_exc.NsxVpnValidationError(details=msg)
def _validate_policy_auth_algorithm(self, policy_info, policy_type):
"""NSX supports only SHA1 and SHA256"""
auth = policy_info.get('auth_algorithm')
if auth and auth not in ipsec_utils.AUTH_ALGORITHM_MAP:
msg = _("Unsupported auth_algorithm: %(algo)s in %(pol)s policy. "
"Please select one of the following supported algorithms: "
"%(supported_algos)s") % {
'pol': policy_type,
'algo': auth,
'supported_algos':
ipsec_utils.AUTH_ALGORITHM_MAP.keys()}
raise nsx_exc.NsxVpnValidationError(details=msg)
def _validate_policy_encryption_algorithm(self, policy_info, policy_type):
encryption = policy_info.get('encryption_algorithm')
if (encryption and
encryption not in ipsec_utils.ENCRYPTION_ALGORITHM_MAP):
msg = _("Unsupported encryption_algorithm: %(algo)s in %(pol)s "
"policy. Please select one of the following supported "
"algorithms: %(supported_algos)s") % {
'algo': encryption,
'pol': policy_type,
'supported_algos':
ipsec_utils.ENCRYPTION_ALGORITHM_MAP.keys()}
raise nsx_exc.NsxVpnValidationError(details=msg)
def _validate_policy_pfs(self, policy_info, policy_type):
pfs = policy_info.get('pfs')
if pfs and pfs not in ipsec_utils.PFS_MAP:
msg = _("Unsupported pfs: %(pfs)s in %(pol)s policy. Please "
"select one of the following pfs: "
"%(supported_pfs)s") % {
'pfs': pfs,
'pol': policy_type,
'supported_pfs':
ipsec_utils.PFS_MAP.keys()}
raise nsx_exc.NsxVpnValidationError(details=msg)
def _validate_dpd(self, connection):
dpd_info = connection.get('dpd')
if not dpd_info:
return
action = dpd_info.get('action')
if action not in ipsec_utils.DPD_ACTION_MAP.keys():
msg = _("Unsupported DPD action: %(action)s! Currently only "
"%(supported)s is supported.") % {
'action': action,
'supported': ipsec_utils.DPD_ACTION_MAP.keys()}
raise nsx_exc.NsxVpnValidationError(details=msg)
timeout = dpd_info.get('timeout')
if (timeout < vpn_ipsec.DpdProfileTimeoutLimits.DPD_TIMEOUT_MIN or
timeout > vpn_ipsec.DpdProfileTimeoutLimits.DPD_TIMEOUT_MAX):
msg = _("Unsupported DPD timeout: %(timeout)s. Value range is "
"[%(min)s-%(max)s].") % {
'timeout': timeout,
'min': vpn_ipsec.DpdProfileTimeoutLimits.DPD_TIMEOUT_MIN,
'max': vpn_ipsec.DpdProfileTimeoutLimits.DPD_TIMEOUT_MAX}
raise nsx_exc.NsxVpnValidationError(details=msg)
def _validate_psk(self, connection):
if 'psk' in connection and not connection['psk']:
msg = _("'psk' cannot be empty or null when authentication "
"mode is psk")
raise nsx_exc.NsxVpnValidationError(details=msg)
def _check_policy_rules_overlap(self, context, ipsec_site_conn):
"""validate no overlapping policy rules
The nsx does not support overlapping policy rules cross
all tenants, and tier0 routers
"""
connections = self.vpn_plugin.get_ipsec_site_connections(
context.elevated())
if not connections:
return
vpnservice_id = ipsec_site_conn.get('vpnservice_id')
vpnservice = self.vpn_plugin._get_vpnservice(context, vpnservice_id)
local_cidrs = [vpnservice['subnet']['cidr']]
peer_cidrs = ipsec_site_conn['peer_cidrs']
for conn in connections:
# skip this connection and connections in non active state
if (conn['id'] == ipsec_site_conn.get('id') or
conn['status'] != constants.ACTIVE):
continue
# TODO(asarfaty): support peer groups too
# check if it overlaps with the peer cidrs
conn_peer_cidrs = conn['peer_cidrs']
if netaddr.IPSet(conn_peer_cidrs) & netaddr.IPSet(peer_cidrs):
# check if the local cidr also overlaps
con_service_id = conn.get('vpnservice_id')
con_service = self.vpn_plugin._get_vpnservice(
context.elevated(), con_service_id)
conn_local_cidr = [con_service['subnet']['cidr']]
if netaddr.IPSet(conn_local_cidr) & netaddr.IPSet(local_cidrs):
msg = (_("Cannot create a connection with overlapping "
"local and peer cidrs (%(local)s and %(peer)s) "
"as connection %(id)s") % {'local': local_cidrs,
'peer': peer_cidrs,
'id': conn['id']})
raise nsx_exc.NsxVpnValidationError(details=msg)
def _check_unique_addresses(self, context, ipsec_site_conn):
"""Validate no repeating local & peer addresses (of all tenants)
The nsx does not support it cross all tenants, and tier0 routers
"""
vpnservice_id = ipsec_site_conn.get('vpnservice_id')
local_addr = self._get_service_local_address(context, vpnservice_id)
peer_address = ipsec_site_conn.get('peer_address')
filters = {'peer_address': [peer_address]}
connections = self.vpn_plugin.get_ipsec_site_connections(
context.elevated(), filters=filters)
for conn in connections:
# skip this connection and connections in non active state
if (conn['id'] == ipsec_site_conn.get('id') or
conn['status'] != constants.ACTIVE):
continue
# this connection has the same peer addr as ours.
# check the service local address
srv_id = conn.get('vpnservice_id')
srv_local = self._get_service_local_address(
context.elevated(), srv_id)
if srv_local == local_addr:
msg = (_("Cannot create another connection with the same "
"local address %(local)s and peer address %(peer)s "
"as connection %(id)s") % {'local': local_addr,
'peer': peer_address,
'id': conn['id']})
raise nsx_exc.NsxVpnValidationError(details=msg)
def _check_advertisment_overlap(self, context, ipsec_site_conn):
"""Validate there is no overlapping advertisement of networks
The plugin advertise all no-snat routers networks + vpn local
networks.
The NSX does not allow different Tier1 router to advertise the
same subnets
"""
admin_con = context.elevated()
srv_id = ipsec_site_conn.get('vpnservice_id')
srv = self.vpn_plugin._get_vpnservice(admin_con, srv_id)
this_router = srv['router_id']
this_cidr = srv['subnet']['cidr']
# get all subnets of no-snat routers
all_routers = self.core_plugin.get_routers(admin_con)
nosnat_routers = [rtr for rtr in all_routers
if (rtr['id'] != this_router and
rtr.get('external_gateway_info') and
not rtr['external_gateway_info'].get(
'enable_snat', True))]
for rtr in nosnat_routers:
if rtr['id'] == this_router:
continue
# go over the subnets of this router
subnets = self.core_plugin._find_router_subnets_cidrs(
admin_con, rtr['id'])
if subnets and netaddr.IPSet(subnets) & netaddr.IPSet([this_cidr]):
msg = (_("Cannot create connection with overlapping local "
"cidrs %(local)s which was already advertised by "
"no-snat router %(rtr)s") % {'local': subnets,
'rtr': rtr['id']})
raise nsx_exc.NsxVpnValidationError(details=msg)
# add all vpn local subnets
connections = self.vpn_plugin.get_ipsec_site_connections(admin_con)
for conn in connections:
# skip this connection and connections in non active state
if (conn['id'] == ipsec_site_conn.get('id') or
conn['status'] != constants.ACTIVE):
continue
# check the service local address
conn_srv_id = conn.get('vpnservice_id')
conn_srv = self.vpn_plugin._get_vpnservice(admin_con, conn_srv_id)
if conn_srv['router_id'] == this_router:
continue
conn_cidr = conn_srv['subnet']['cidr']
if netaddr.IPSet([conn_cidr]) & netaddr.IPSet([this_cidr]):
msg = (_("Cannot create connection with overlapping local "
"cidr %(local)s which was already advertised by "
"router %(rtr)s and connection %(conn)s") % {
'local': conn_cidr,
'rtr': conn_srv['router_id'],
'conn': conn['id']})
raise nsx_exc.NsxVpnValidationError(details=msg)
# TODO(asarfaty): also add this validation when adding an interface
# or no-snat to a router through the nsx-v3 plugin
def validate_ipsec_site_connection(self, context, ipsec_site_conn):
"""Called upon create/update of a connection"""
self._validate_backend_version()
self._validate_dpd(ipsec_site_conn)
self._validate_psk(ipsec_site_conn)
ike_policy_id = ipsec_site_conn.get('ikepolicy_id')
if ike_policy_id:
ikepolicy = self.vpn_plugin.get_ikepolicy(context,
ike_policy_id)
self.validate_ike_policy(context, ikepolicy)
ipsec_policy_id = ipsec_site_conn.get('ipsecpolicy_id')
if ipsec_policy_id:
ipsecpolicy = self.vpn_plugin.get_ipsecpolicy(context,
ipsec_policy_id)
self.validate_ipsec_policy(context, ipsecpolicy)
self._check_advertisment_overlap(context, ipsec_site_conn)
self._check_unique_addresses(context, ipsec_site_conn)
self._check_policy_rules_overlap(context, ipsec_site_conn)
#TODO(asarfaty): IPv6 is not yet supported. add validation
def _get_service_local_address(self, context, vpnservice_id):
vpnservice = self.vpn_plugin._get_vpnservice(context,
vpnservice_id)
router_id = vpnservice['router_id']
router_db = self.core_plugin.get_router(context, router_id)
gw = router_db['external_gateway_info']
return gw['external_fixed_ips'][0]['ip_address']
def _validate_router(self, context, router_id):
# Verify that the router gw network is connected to an active-standby
# Tier0 router
router_db = self.core_plugin._get_router(context, router_id)
tier0_uuid = self.core_plugin._get_tier0_uuid_by_router(context,
router_db)
# TODO(asarfaty): cache this result
tier0_router = self.nsxlib.logical_router.get(tier0_uuid)
if (not tier0_router or
tier0_router.get('high_availability_mode') != 'ACTIVE_STANDBY'):
msg = _("The router GW should be connected to a TIER-0 router "
"with ACTIVE_STANDBY HA mode")
raise nsx_exc.NsxVpnValidationError(details=msg)
def validate_vpnservice(self, context, vpnservice):
"""Called upon create/update of a service"""
self._validate_backend_version()
# Call general validations
super(IPsecV3Validator, self).validate_vpnservice(
context, vpnservice)
# Call specific NSX validations
self._validate_router(context, vpnservice['router_id'])
if not vpnservice['subnet_id']:
# we currently do not support multiple subnets so a subnet must
# be defined
msg = _("Subnet must be defined in a service")
raise nsx_exc.NsxVpnValidationError(details=msg)
#TODO(asarfaty): IPv6 is not yet supported. add validation
def validate_ipsec_policy(self, context, ipsec_policy):
# Call general validations
super(IPsecV3Validator, self).validate_ipsec_policy(
context, ipsec_policy)
# Call specific NSX validations
self._validate_policy_lifetime(ipsec_policy, "IPSec")
self._validate_policy_auth_algorithm(ipsec_policy, "IPSec")
self._validate_policy_encryption_algorithm(ipsec_policy, "IPSec")
self._validate_policy_pfs(ipsec_policy, "IPSec")
# Ensure IPSec policy encap mode is tunnel
mode = ipsec_policy.get('encapsulation_mode')
if mode and mode not in ipsec_utils.ENCAPSULATION_MODE_MAP.keys():
msg = _("Unsupported encapsulation mode: %s. Only 'tunnel' mode "
"is supported.") % mode
raise nsx_exc.NsxVpnValidationError(details=msg)
# Ensure IPSec policy transform protocol is esp
prot = ipsec_policy.get('transform_protocol')
if prot and prot not in ipsec_utils.TRANSFORM_PROTOCOL_MAP.keys():
msg = _("Unsupported transform protocol: %s. Only 'esp' protocol "
"is supported.") % prot
raise nsx_exc.NsxVpnValidationError(details=msg)
def validate_ike_policy(self, context, ike_policy):
# Call general validations
super(IPsecV3Validator, self).validate_ike_policy(
context, ike_policy)
# Call specific NSX validations
self._validate_policy_lifetime(ike_policy, "IKE")
self._validate_policy_auth_algorithm(ike_policy, "IKE")
self._validate_policy_encryption_algorithm(ike_policy, "IKE")
self._validate_policy_pfs(ike_policy, "IKE")
# 'aggressive' phase1-negotiation-mode is not supported
if ike_policy.get('phase1-negotiation-mode', 'main') != 'main':
msg = _("Unsupported phase1-negotiation-mode: %s! Only 'main' is "
"supported.") % ike_policy['phase1-negotiation-mode']
raise nsx_exc.NsxVpnValidationError(details=msg)

View File

@ -197,6 +197,9 @@ class NsxV3PluginTestCaseMixin(test_plugin.NeutronDbPluginV2TestCase,
_mock_nsx_backend_calls() _mock_nsx_backend_calls()
self.setup_conf_overrides() self.setup_conf_overrides()
self.mock_plugin_methods() self.mock_plugin_methods()
# ignoring the given plugin and use the nsx-v3 one
if not plugin.endswith('NsxTVDPlugin'):
plugin = PLUGIN_NAME
super(NsxV3PluginTestCaseMixin, self).setUp(plugin=plugin, super(NsxV3PluginTestCaseMixin, self).setUp(plugin=plugin,
ext_mgr=ext_mgr) ext_mgr=ext_mgr)

View File

@ -0,0 +1,390 @@
# Copyright 2013, Nachi Ueno, NTT I3, 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_lib import context as n_ctx
from neutron_vpnaas.tests import base
from vmware_nsx.common import exceptions as nsx_exc
from vmware_nsx.services.vpnaas.nsxv3 import ipsec_validator
class TestDriverValidation(base.BaseTestCase):
def setUp(self):
super(TestDriverValidation, self).setUp()
self.context = n_ctx.Context('some_user', 'some_tenant')
self.service_plugin = mock.Mock()
driver = mock.Mock()
driver.service_plugin = self.service_plugin
with mock.patch("neutron_lib.plugins.directory.get_plugin"):
self.validator = ipsec_validator.IPsecV3Validator(driver)
self.validator._l3_plugin = mock.Mock()
self.validator._core_plugin = mock.Mock()
self.vpn_service = {'router_id': 'dummy_router',
'subnet_id': 'dummy_subnet'}
self.peer_address = '10.10.10.10'
self.peer_cidr = '10.10.11.0/20'
def _test_lifetime_not_in_seconds(self, validation_func):
policy_info = {'lifetime': {'units': 'kilobytes', 'value': 1000}}
self.assertRaises(nsx_exc.NsxVpnValidationError,
validation_func,
self.context, policy_info)
def test_ike_lifetime_not_in_seconds(self):
self._test_lifetime_not_in_seconds(
self.validator.validate_ike_policy)
def test_ipsec_lifetime_not_in_seconds(self):
self._test_lifetime_not_in_seconds(
self.validator.validate_ipsec_policy)
def _test_lifetime_seconds_values_at_limits(self, validation_func):
policy_info = {'lifetime': {'units': 'seconds', 'value': 90}}
validation_func(self.context, policy_info)
policy_info = {'lifetime': {'units': 'seconds', 'value': 86400}}
validation_func(self.context, policy_info)
policy_info = {'lifetime': {'units': 'seconds', 'value': 10}}
self.assertRaises(nsx_exc.NsxVpnValidationError,
validation_func,
self.context, policy_info)
def test_ike_lifetime_seconds_values_at_limits(self):
self._test_lifetime_seconds_values_at_limits(
self.validator.validate_ike_policy)
def test_ipsec_lifetime_seconds_values_at_limits(self):
self._test_lifetime_seconds_values_at_limits(
self.validator.validate_ipsec_policy)
def _test_auth_algorithm(self, validation_func):
auth_algorithm = {'auth_algorithm': 'sha384'}
self.assertRaises(nsx_exc.NsxVpnValidationError,
validation_func,
self.context, auth_algorithm)
auth_algorithm = {'auth_algorithm': 'sha512'}
self.assertRaises(nsx_exc.NsxVpnValidationError,
validation_func,
self.context, auth_algorithm)
auth_algorithm = {'auth_algorithm': 'sha1'}
validation_func(self.context, auth_algorithm)
auth_algorithm = {'auth_algorithm': 'sha256'}
validation_func(self.context, auth_algorithm)
def test_ipsec_auth_algorithm(self):
self._test_auth_algorithm(self.validator.validate_ipsec_policy)
def test_ike_auth_algorithm(self):
self._test_auth_algorithm(self.validator.validate_ike_policy)
def _test_encryption_algorithm(self, validation_func):
auth_algorithm = {'encryption_algorithm': 'aes-192'}
self.assertRaises(nsx_exc.NsxVpnValidationError,
validation_func,
self.context, auth_algorithm)
auth_algorithm = {'encryption_algorithm': 'aes-128'}
validation_func(self.context, auth_algorithm)
auth_algorithm = {'encryption_algorithm': 'aes-256'}
validation_func(self.context, auth_algorithm)
def test_ipsec_encryption_algorithm(self):
self._test_encryption_algorithm(self.validator.validate_ipsec_policy)
def test_ike_encryption_algorithm(self):
self._test_encryption_algorithm(self.validator.validate_ike_policy)
def test_ike_negotiation_mode(self):
policy_info = {'phase1-negotiation-mode': 'aggressive'}
self.assertRaises(nsx_exc.NsxVpnValidationError,
self.validator.validate_ike_policy,
self.context, policy_info)
policy_info = {'phase1-negotiation-mode': 'main'}
self.validator.validate_ike_policy(self.context, policy_info)
def _test_pfs(self, validation_func):
policy_info = {'pfs': 'group15'}
self.assertRaises(nsx_exc.NsxVpnValidationError,
validation_func,
self.context, policy_info)
policy_info = {'pfs': 'group5'}
validation_func(self.context, policy_info)
def test_ipsec_pfs(self):
self._test_pfs(self.validator.validate_ipsec_policy)
def test_ike_pfs(self):
self._test_pfs(self.validator.validate_ike_policy)
def test_ipsec_encap_mode(self):
policy_info = {'encapsulation_mode': 'transport'}
self.assertRaises(nsx_exc.NsxVpnValidationError,
self.validator.validate_ipsec_policy,
self.context, policy_info)
policy_info = {'encapsulation_mode': 'tunnel'}
self.validator.validate_ipsec_policy(self.context, policy_info)
def test_ipsec_transform_protocol(self):
policy_info = {'transform_protocol': 'ah'}
self.assertRaises(nsx_exc.NsxVpnValidationError,
self.validator.validate_ipsec_policy,
self.context, policy_info)
policy_info = {'transform_protocol': 'esp'}
self.validator.validate_ipsec_policy(self.context, policy_info)
def test_vpn_service_validation_router(self):
router = {'high_availability_mode': 'ACITVE_ACTIVE'}
with mock.patch.object(self.validator.nsxlib.logical_router, 'get',
return_value=router):
self.assertRaises(nsx_exc.NsxVpnValidationError,
self.validator.validate_vpnservice,
self.context, self.vpn_service)
router = {'high_availability_mode': 'ACTIVE_STANDBY'}
with mock.patch.object(self.validator.nsxlib.logical_router, 'get',
return_value=router):
self.validator.validate_vpnservice(self.context, self.vpn_service)
def _test_conn_validation(self, conn_params=None, success=True,
connections=None, service_subnets=None,
router_subnets=None):
if connections is None:
connections = []
if router_subnets is None:
router_subnets = []
def mock_get_router(context, router_id):
return {'id': router_id,
'external_gateway_info': {
'external_fixed_ips': [{
'ip_address': '1.1.1.%s' % router_id}]}}
def mock_get_routers(context, filters=None, fields=None):
return [{'id': 'no-snat',
'external_gateway_info': {'enable_snat': False}}]
def mock_get_service(context, service_id):
if service_subnets:
# option to give the test a different subnet per service
subnet_cidr = service_subnets[int(service_id) - 1]
else:
subnet_cidr = '5.5.5.0/2%s' % service_id
return {'id': service_id,
'router_id': service_id,
'subnet_id': 'dummy_subnet',
'subnet': {'id': 'dummy_subnet',
'cidr': subnet_cidr}}
def mock_get_connections(context, filters=None, fields=None):
if filters and 'peer_address' in filters:
return [conn for conn in connections
if conn['peer_address'] == filters['peer_address'][0]]
else:
return connections
with mock.patch.object(self.validator.vpn_plugin, '_get_vpnservice',
side_effect=mock_get_service),\
mock.patch.object(self.validator._core_plugin, 'get_router',
side_effect=mock_get_router),\
mock.patch.object(self.validator._core_plugin, 'get_routers',
side_effect=mock_get_routers),\
mock.patch.object(self.validator._core_plugin,
'_find_router_subnets_cidrs',
return_value=router_subnets),\
mock.patch.object(self.validator.vpn_plugin,
'get_ipsec_site_connections',
side_effect=mock_get_connections):
ipsec_sitecon = {'id': '1',
'vpnservice_id': '1',
'mtu': 1500,
'peer_address': self.peer_address,
'peer_cidrs': [self.peer_cidr]}
if conn_params:
ipsec_sitecon.update(conn_params)
if success:
self.validator.validate_ipsec_site_connection(
self.context, ipsec_sitecon)
else:
self.assertRaises(
nsx_exc.NsxVpnValidationError,
self.validator.validate_ipsec_site_connection,
self.context, ipsec_sitecon)
def test_dpd_validation(self):
params = {'dpd': {'action': 'hold',
'timeout': 120}}
self._test_conn_validation(conn_params=params, success=True)
params = {'dpd': {'action': 'clear',
'timeout': 120}}
self._test_conn_validation(conn_params=params, success=False)
params = {'dpd': {'action': 'hold',
'timeout': 5}}
self._test_conn_validation(conn_params=params, success=False)
def test_check_unique_addresses(self):
# this test runs with non-overlapping local subnets on
# different routers
subnets = ['5.5.5.0/20', '6.6.6.0/20']
# same service/router gw & peer address - should fail
connections = [{'id': '2',
'status': 'ACTIVE',
'vpnservice_id': '1',
'peer_address': self.peer_address,
'peer_cidrs': [self.peer_cidr]}]
self._test_conn_validation(success=False,
connections=connections,
service_subnets=subnets)
# different service/router gw - ok
connections = [{'id': '2',
'status': 'ACTIVE',
'vpnservice_id': '2',
'peer_address': self.peer_address,
'peer_cidrs': ['6.6.6.6']}]
self._test_conn_validation(success=True,
connections=connections,
service_subnets=subnets)
# different peer address - ok
connections = [{'id': '2',
'status': 'ACTIVE',
'vpnservice_id': '1',
'peer_address': '7.7.7.1',
'peer_cidrs': ['7.7.7.7']}]
self._test_conn_validation(success=True,
connections=connections,
service_subnets=subnets)
# ignoring non-active connections
connections = [{'id': '2',
'status': 'ERROR',
'vpnservice_id': '1',
'peer_address': self.peer_address,
'peer_cidrs': [self.peer_cidr]}]
self._test_conn_validation(success=True,
connections=connections,
service_subnets=subnets)
def test_overlapping_rules(self):
# peer-cidr overlapping with new one, same subnet - should fail
connections = [{'id': '2',
'status': 'ACTIVE',
'vpnservice_id': '1',
'peer_address': '9.9.9.9',
'peer_cidrs': ['10.10.11.1/19']}]
self._test_conn_validation(success=False,
connections=connections)
# same peer-cidr, overlapping subnets - should fail
connections = [{'id': '2',
'status': 'ACTIVE',
'vpnservice_id': '2',
'peer_address': '9.9.9.9',
'peer_cidrs': [self.peer_cidr]}]
self._test_conn_validation(success=False,
connections=connections)
# non overlapping peer-cidr, same subnet - ok
connections = [{'id': '2',
'status': 'ACTIVE',
'vpnservice_id': '1',
'peer_address': '7.7.7.1',
'peer_cidrs': ['7.7.7.7']}]
self._test_conn_validation(success=True,
connections=connections)
# ignoring non-active connections
connections = [{'id': '2',
'status': 'ERROR',
'vpnservice_id': '1',
'peer_address': '9.9.9.9',
'peer_cidrs': ['10.10.11.1/19']}]
self._test_conn_validation(success=True,
connections=connections)
def test_advertisment(self):
# different routers, same subnet - should fail
subnets = ['5.5.5.0/20', '5.5.5.0/20']
connections = [{'id': '2',
'status': 'ACTIVE',
'vpnservice_id': '2',
'peer_address': self.peer_address,
'peer_cidrs': ['6.6.6.6']}]
self._test_conn_validation(success=False,
connections=connections,
service_subnets=subnets)
# different routers, overlapping subnet - should fail
subnets = ['5.5.5.0/20', '5.5.5.0/21']
connections = [{'id': '2',
'status': 'ACTIVE',
'vpnservice_id': '2',
'peer_address': self.peer_address,
'peer_cidrs': ['6.6.6.6']}]
self._test_conn_validation(success=False,
connections=connections,
service_subnets=subnets)
# different routers, non overlapping subnet - ok
subnets = ['5.5.5.0/20', '50.5.5.0/21']
connections = [{'id': '2',
'status': 'ACTIVE',
'vpnservice_id': '2',
'peer_address': self.peer_address,
'peer_cidrs': ['6.6.6.6']}]
self._test_conn_validation(success=True,
connections=connections,
service_subnets=subnets)
# no-snat router with overlapping subnet to the service subnet - fail
subnets = ['5.5.5.0/21', '1.1.1.0/20']
connections = [{'id': '2',
'status': 'ACTIVE',
'vpnservice_id': '2',
'peer_address': self.peer_address,
'peer_cidrs': ['6.6.6.6']}]
self._test_conn_validation(success=False,
connections=connections,
router_subnets=subnets)
# no-snat router with non overlapping subnet to the service subnet - ok
service_subnets = ['5.5.5.0/20', '6.6.6.0/20']
router_subnets = ['50.5.5.0/21', '1.1.1.0/20']
connections = [{'id': '2',
'status': 'ACTIVE',
'vpnservice_id': '2',
'peer_address': self.peer_address,
'peer_cidrs': ['6.6.6.6']}]
self._test_conn_validation(success=True,
connections=connections,
service_subnets=service_subnets,
router_subnets=router_subnets)
# TODO(asarfaty): add tests for the driver