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:
parent
6ddff8c110
commit
b993b7f4c0
@ -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
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
6
releasenotes/notes/nsxv3-vpnaas-0b02762ff4b83904.yaml
Normal file
6
releasenotes/notes/nsxv3-vpnaas-0b02762ff4b83904.yaml
Normal 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.
|
@ -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())
|
||||||
|
@ -1 +1 @@
|
|||||||
9799427fc0e1
|
0dbeda408e41
|
||||||
|
@ -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'))
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
0
vmware_nsx/services/vpnaas/nsxv3/__init__.py
Normal file
0
vmware_nsx/services/vpnaas/nsxv3/__init__.py
Normal file
645
vmware_nsx/services/vpnaas/nsxv3/ipsec_driver.py
Normal file
645
vmware_nsx/services/vpnaas/nsxv3/ipsec_driver.py
Normal 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
|
59
vmware_nsx/services/vpnaas/nsxv3/ipsec_utils.py
Normal file
59
vmware_nsx/services/vpnaas/nsxv3/ipsec_utils.py
Normal 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
|
374
vmware_nsx/services/vpnaas/nsxv3/ipsec_validator.py
Normal file
374
vmware_nsx/services/vpnaas/nsxv3/ipsec_validator.py
Normal 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)
|
@ -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)
|
||||||
|
|
||||||
|
390
vmware_nsx/tests/unit/services/vpnaas/test_nsxv3_vpnaas.py
Normal file
390
vmware_nsx/tests/unit/services/vpnaas/test_nsxv3_vpnaas.py
Normal 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
|
Loading…
Reference in New Issue
Block a user