From 53d1bd0e2f9327058f3cdb1d49d355d484ed25c1 Mon Sep 17 00:00:00 2001 From: Zhenmei Date: Fri, 18 Mar 2016 04:11:22 -0400 Subject: [PATCH] NSXv: Support ipsec VPNaaS on nsxv driver Change-Id: Id3fd4da7e4dd4cac4eb2e32024c0d8242b85a0bb Co-Authored-By: Roey Chen --- doc/source/devstack.rst | 9 + tools/tox_install.sh | 1 + vmware_nsx/common/exceptions.py | 8 + vmware_nsx/common/nsxv_constants.py | 16 + vmware_nsx/plugins/nsx_v/plugin.py | 9 +- vmware_nsx/services/vpnaas/__init__.py | 0 vmware_nsx/services/vpnaas/nsxv/__init__.py | 0 .../services/vpnaas/nsxv/ipsec_driver.py | 355 ++++++++++++++++++ .../services/vpnaas/nsxv/ipsec_validator.py | 93 +++++ .../tests/unit/services/vpnaas/__init__.py | 0 .../unit/services/vpnaas/test_nsxv_vpnaas.py | 255 +++++++++++++ .../tests/unit/shell/test_admin_utils.py | 12 +- 12 files changed, 753 insertions(+), 5 deletions(-) create mode 100644 vmware_nsx/services/vpnaas/__init__.py create mode 100644 vmware_nsx/services/vpnaas/nsxv/__init__.py create mode 100644 vmware_nsx/services/vpnaas/nsxv/ipsec_driver.py create mode 100644 vmware_nsx/services/vpnaas/nsxv/ipsec_validator.py create mode 100644 vmware_nsx/tests/unit/services/vpnaas/__init__.py create mode 100644 vmware_nsx/tests/unit/services/vpnaas/test_nsxv_vpnaas.py diff --git a/doc/source/devstack.rst b/doc/source/devstack.rst index 5dd435ae90..51b43b74d8 100644 --- a/doc/source/devstack.rst +++ b/doc/source/devstack.rst @@ -121,6 +121,15 @@ Add neutron-dynamic-routing repo as an external repository and configure followi [DEFAULT] api_extensions_path = $DEST/neutron-dynamic-routing/neutron_dynamic_routing/extensions +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.nsxv.ipsec_driver.NSXvIPsecVpnDriver:default + NSXv3 ----- diff --git a/tools/tox_install.sh b/tools/tox_install.sh index a95e5bee7b..9c656ceb41 100755 --- a/tools/tox_install.sh +++ b/tools/tox_install.sh @@ -10,6 +10,7 @@ ${DIR}/tox_install_project.sh neutron-lbaas neutron_lbaas $* ${DIR}/tox_install_project.sh vmware-nsxlib vmware_nsxlib $* ${DIR}/tox_install_project.sh neutron-fwaas neutron_fwaas $* ${DIR}/tox_install_project.sh neutron-dynamic-routing neutron-dynamic-routing $* +${DIR}/tox_install_project.sh neutron-vpnaas neutron-vpnaas $* CONSTRAINTS_FILE=$1 shift diff --git a/vmware_nsx/common/exceptions.py b/vmware_nsx/common/exceptions.py index 1d34ded646..f9c8e76277 100644 --- a/vmware_nsx/common/exceptions.py +++ b/vmware_nsx/common/exceptions.py @@ -198,3 +198,11 @@ class NsxRouterInterfaceDoesNotMatchAddressScope(n_exc.BadRequest): message = _("Unable to update no-NAT router %(router_id)s, " "only subnets allocated from address-scope " "%(address_scope_id)s can be connected.") + + +class NsxVpnValidationError(NsxPluginException): + message = _("Invalid VPN configuration: %(details)s") + + +class NsxIPsecVpnMappingNotFound(n_exc.NotFound): + message = _("Unable to find mapping for ipsec site connection: %(conn)s") diff --git a/vmware_nsx/common/nsxv_constants.py b/vmware_nsx/common/nsxv_constants.py index 9524a8c825..b4e3ba37e4 100644 --- a/vmware_nsx/common/nsxv_constants.py +++ b/vmware_nsx/common/nsxv_constants.py @@ -60,3 +60,19 @@ CSR_REQUEST = ("" RESERVED_IPS = ["169.254.128.0/17", "169.254.1.0/24", "169.254.64.192/26"] + +# VPNaaS constants +ENCRYPTION_ALGORITHM_MAP = { + '3des': '3des', + 'aes-128': 'aes', + 'aes-256': 'aes256' +} + +PFS_MAP = { + 'group2': 'dh2', + 'group5': 'dh5' +} + +TRANSFORM_PROTOCOL_ALLOWED = ('esp',) + +ENCAPSULATION_MODE_ALLOWED = ('tunnel',) diff --git a/vmware_nsx/plugins/nsx_v/plugin.py b/vmware_nsx/plugins/nsx_v/plugin.py index 64ed760825..2da187a83b 100644 --- a/vmware_nsx/plugins/nsx_v/plugin.py +++ b/vmware_nsx/plugins/nsx_v/plugin.py @@ -3549,6 +3549,7 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, fw_rules = [] router_with_firewall = True if fwaas_rules is not None else False neutron_id = router_db['id'] + edge_id = self._get_edge_id_by_rtr_id(context, router_id) # Add FW rule to open subnets firewall flows and static routes # relative flows @@ -3595,9 +3596,15 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, context, router_db) fw_rules.extend(nosnat_fw_rules) + vpn_plugin = directory.get_plugin(plugin_const.VPN) + if vpn_plugin: + vpn_driver = vpn_plugin.ipsec_driver + vpn_rules = ( + vpn_driver._generate_ipsecvpn_firewall_rules(edge_id)) + fw_rules.extend(vpn_rules) + # Get the load balancer rules in case they are refreshed # (relevant only for older LB that are still on the router edge) - edge_id = self._get_edge_id_by_rtr_id(context, router_id) lb_rules = nsxv_db.get_nsxv_lbaas_loadbalancer_binding_by_edge( context.session, edge_id) for rule in lb_rules: diff --git a/vmware_nsx/services/vpnaas/__init__.py b/vmware_nsx/services/vpnaas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/vpnaas/nsxv/__init__.py b/vmware_nsx/services/vpnaas/nsxv/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/vpnaas/nsxv/ipsec_driver.py b/vmware_nsx/services/vpnaas/nsxv/ipsec_driver.py new file mode 100644 index 0000000000..90f0f5f628 --- /dev/null +++ b/vmware_nsx/services/vpnaas/nsxv/ipsec_driver.py @@ -0,0 +1,355 @@ +# Copyright 2016 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 neutron_lib import exceptions as n_exc +from neutron_lib.plugins import directory +from neutron_vpnaas.services.vpn import service_drivers +from oslo_log import log as logging + +from vmware_nsx._i18n import _ +from vmware_nsx.common import exceptions as nsxv_exc +from vmware_nsx.common import locking +from vmware_nsx.common import nsxv_constants +from vmware_nsx.db import nsxv_db +from vmware_nsx.plugins.nsx_v.vshield.common import exceptions as vcns_exc +from vmware_nsx.services.vpnaas.nsxv import ipsec_validator + +LOG = logging.getLogger(__name__) +IPSEC = 'ipsec' + + +class NSXvIPsecVpnDriver(service_drivers.VpnDriver): + + def __init__(self, service_plugin): + self._core_plugin = directory.get_plugin() + self._vcns = self._core_plugin.nsx_v.vcns + validator = ipsec_validator.IPsecValidator(service_plugin) + super(NSXvIPsecVpnDriver, self).__init__(service_plugin, validator) + + @property + def l3_plugin(self): + return self._core_plugin + + @property + def service_type(self): + return IPSEC + + def _is_shared_router(self, router): + return router.get('router_type') == nsxv_constants.SHARED + + def _get_router_edge_id(self, context, vpnservice_id): + vpnservice = self.service_plugin._get_vpnservice(context, + vpnservice_id) + router_id = vpnservice['router_id'] + edge_binding = nsxv_db.get_nsxv_router_binding(context.session, + router_id) + if not edge_binding: + msg = _("Couldn't find edge binding for router %s") % router_id + raise nsxv_exc.NsxPluginException(err_msg=msg) + + if edge_binding['edge_type'] == nsxv_constants.VDR_EDGE: + edge_manager = self._core_plugin.edge_manager + router_id = edge_manager.get_plr_by_tlr_id(context, router_id) + binding = nsxv_db.get_nsxv_router_binding(context.session, + router_id) + edge_id = binding['edge_id'] + else: + # Get exclusive edge id + edge_id = edge_binding['edge_id'] + return router_id, edge_id + + def _convert_ipsec_conn(self, context, ipsec_site_connection): + ipsec_id = ipsec_site_connection['ipsecpolicy_id'] + vpnservice_id = ipsec_site_connection['vpnservice_id'] + ipsecpolicy = self.service_plugin.get_ipsecpolicy(context, ipsec_id) + vpnservice = self.service_plugin._get_vpnservice(context, + vpnservice_id) + local_cidr = vpnservice['subnet']['cidr'] + router_id = vpnservice['router_id'] + router = self._core_plugin.get_router(context, router_id) + local_addr = (router['external_gateway_info']['external_fixed_ips'] + [0]["ip_address"]) + encrypt = nsxv_constants.ENCRYPTION_ALGORITHM_MAP.get( + ipsecpolicy.get('encryption_algorithm')) + site = { + 'enabled': True, + 'enablePfs': True, + 'dhGroup': nsxv_constants.PFS_MAP.get(ipsecpolicy.get('pfs')), + 'name': ipsec_site_connection.get('name'), + 'description': ipsec_site_connection.get('description'), + 'localId': local_addr, + 'localIp': local_addr, + 'peerId': ipsec_site_connection['peer_id'], + 'peerIp': ipsec_site_connection.get('peer_address'), + 'localSubnets': { + 'subnets': [local_cidr]}, + 'peerSubnets': { + 'subnets': ipsec_site_connection.get('peer_cidrs')}, + 'authenticationMode': ipsec_site_connection.get('auth_mode'), + 'psk': ipsec_site_connection.get('psk'), + 'encryptionAlgorithm': encrypt + } + return site + + def _generate_new_sites(self, edge_id, ipsec_site_conn): + # Fetch the previous ipsec vpn configuration + ipsecvpn_configs = self._get_ipsec_config(edge_id) + vse_sites = [] + if ipsecvpn_configs[1]['enabled']: + vse_sites = ([site for site + in ipsecvpn_configs[1]['sites']['sites']]) + vse_sites.append(ipsec_site_conn) + return vse_sites + + def _generate_ipsecvpn_firewall_rules(self, edge_id): + ipsecvpn_configs = self._get_ipsec_config(edge_id) + ipsec_vpn_fw_rules = [] + if ipsecvpn_configs[1]['enabled']: + for site in ipsecvpn_configs[1]['sites']['sites']: + peer_subnets = site['peerSubnets']['subnets'] + local_subnets = site['localSubnets']['subnets'] + ipsec_vpn_fw_rules.append({ + 'name': 'VPN ' + site['name'], + 'action': 'allow', + 'enabled': True, + 'source_ip_address': peer_subnets, + 'destination_ip_address': local_subnets}) + return ipsec_vpn_fw_rules + + def _update_firewall_rules(self, context, vpnservice_id): + vpnservice = self.service_plugin._get_vpnservice(context, + vpnservice_id) + router_db = ( + self._core_plugin._get_router(context, vpnservice['router_id'])) + self._core_plugin._update_subnets_and_dnat_firewall(context, + router_db) + + def _update_status(self, context, vpn_service_id, ipsec_site_conn_id, + status, updated_pending_status=True): + status_list = [] + vpn_status = {} + ipsec_site_conn = {} + vpn_status['id'] = vpn_service_id + vpn_status['updated_pending_status'] = updated_pending_status + vpn_status['status'] = status + ipsec_site_conn['status'] = status + ipsec_site_conn['updated_pending_status'] = updated_pending_status + vpn_status['ipsec_site_connections'] = {ipsec_site_conn_id: + ipsec_site_conn} + status_list.append(vpn_status) + self.service_plugin.update_status_by_agent(context, status_list) + + def create_ipsec_site_connection(self, context, ipsec_site_connection): + LOG.debug('Creating ipsec site connection %(conn_info)s.', + {"conn_info": ipsec_site_connection}) + + self.validator.validate_ipsec_conn(context, ipsec_site_connection) + new_ipsec = self._convert_ipsec_conn(context, ipsec_site_connection) + vpnservice_id = ipsec_site_connection['vpnservice_id'] + edge_id = self._get_router_edge_id(context, vpnservice_id)[1] + with locking.LockManager.get_lock(edge_id): + vse_sites = self._generate_new_sites(edge_id, new_ipsec) + ipsec_id = ipsec_site_connection["id"] + try: + LOG.debug('Updating ipsec vpn configuration %(vse_sites)s.', + {'vse_sites': vse_sites}) + self._update_ipsec_config(edge_id, vse_sites, enabled=True) + except vcns_exc.VcnsApiException: + self._update_status(context, vpnservice_id, ipsec_id, + "ERROR") + msg = (_("Failed to create ipsec site connection " + "configuration with %(edge_id)s.") % + {'edge_id': edge_id}) + raise nsxv_exc.NsxPluginException(err_msg=msg) + + LOG.debug('Updating ipsec vpn firewall') + try: + self._update_firewall_rules(context, vpnservice_id) + except vcns_exc.VcnsApiException: + self._update_status(context, vpnservice_id, ipsec_id, "ERROR") + msg = (_("Failed to update firewall rule for ipsec vpn " + "with %(edge_id)s.") % {'edge_id': edge_id}) + raise nsxv_exc.NsxPluginException(err_msg=msg) + self._update_status(context, vpnservice_id, ipsec_id, "ACTIVE") + + def _get_ipsec_config(self, edge_id): + return self._vcns.get_ipsec_config(edge_id) + + def delete_ipsec_site_connection(self, context, ipsec_site_conn): + LOG.debug('Deleting ipsec site connection %(site)s.', + {"site": ipsec_site_conn}) + ipsec_id = ipsec_site_conn['id'] + edge_id = self._get_router_edge_id(context, + ipsec_site_conn['vpnservice_id'])[1] + with locking.LockManager.get_lock(edge_id): + del_site, vse_sites = self._find_vse_site(context, edge_id, + ipsec_site_conn) + if not del_site: + LOG.error("Failed to find ipsec_site_connection " + "%(ipsec_site_conn)s with %(edge_id)s.", + {'ipsec_site_conn': ipsec_site_conn, + 'edge_id': edge_id}) + raise nsxv_exc.NsxIPsecVpnMappingNotFound(conn=ipsec_id) + + vse_sites.remove(del_site) + enabled = True if vse_sites else False + try: + self._update_ipsec_config(edge_id, vse_sites, enabled) + except vcns_exc.VcnsApiException: + msg = (_("Failed to delete ipsec site connection " + "configuration with edge_id: %(edge_id)s.") % + {'egde_id': edge_id}) + raise nsxv_exc.NsxPluginException(err_msg=msg) + try: + self._update_firewall_rules(context, + ipsec_site_conn['vpnservice_id']) + except vcns_exc.VcnsApiException: + msg = _("Failed to update firewall rule for ipsec vpn with " + "%(edge_id)s.") % {'edge_id': edge_id} + raise nsxv_exc.NsxPluginException(err_msg=msg) + + def _find_vse_site(self, context, edge_id, site): + # Fetch the previous ipsec vpn configuration + ipsecvpn_configs = self._get_ipsec_config(edge_id)[1] + vpnservice = self.service_plugin._get_vpnservice(context, + site['vpnservice_id']) + local_cidr = vpnservice['subnet']['cidr'] + old_site = None + vse_sites = None + if ipsecvpn_configs['enabled']: + vse_sites = ipsecvpn_configs['sites'].get('sites') + for s in vse_sites: + if ((s['peerSubnets'].get('subnets') == site['peer_cidrs']) + and + (s['localSubnets'].get('subnets')[0] == local_cidr)): + old_site = s + break + return old_site, vse_sites + + def _update_site_dict(self, context, edge_id, site, + ipsec_site_connection): + # Fetch the previous ipsec vpn configuration + old_site, vse_sites = self._find_vse_site(context, edge_id, site) + if old_site: + vse_sites.remove(old_site) + if 'peer_addresses' in ipsec_site_connection: + old_site['peerIp'] = ipsec_site_connection['peer_address'] + if 'peer_cidrs' in ipsec_site_connection: + old_site['peerSubnets']['subnets'] = (ipsec_site_connection + ['peer_cidrs']) + vse_sites.append(old_site) + return vse_sites + + def update_ipsec_site_connection(self, context, old_ipsec_conn, + ipsec_site_connection): + LOG.debug('Updating ipsec site connection %(site)s.', + {"site": ipsec_site_connection}) + vpnservice_id = old_ipsec_conn['vpnservice_id'] + ipsec_id = old_ipsec_conn['id'] + edge_id = self._get_router_edge_id(context, vpnservice_id)[1] + with locking.LockManager.get_lock(edge_id): + vse_sites = self._update_site_dict(context, edge_id, + old_ipsec_conn, + ipsec_site_connection) + if not vse_sites: + self._update_status(context, vpnservice_id, ipsec_id, + "ERROR") + LOG.error("Failed to find ipsec_site_connection " + "%(ipsec_site_conn)s with %(edge_id)s.", + {'ipsec_site_conn': ipsec_site_connection, + 'edge_id': edge_id}) + raise nsxv_exc.NsxIPsecVpnMappingNotFound(conn=ipsec_id) + try: + LOG.debug('Updating ipsec vpn configuration %(vse_sites)s.', + {'vse_sites': vse_sites}) + self._update_ipsec_config(edge_id, vse_sites) + except vcns_exc.VcnsApiException: + self._update_status(context, vpnservice_id, ipsec_id, "ERROR") + msg = (_("Failed to create ipsec site connection " + "configuration with %(edge_id)s.") % + {'edge_id': edge_id}) + raise nsxv_exc.NsxPluginException(err_msg=msg) + + if 'peer_cidrs' in ipsec_site_connection: + # Update firewall + old_ipsec_conn['peer_cidrs'] = ( + ipsec_site_connection['peer_cidrs']) + try: + self._update_firewall_rules(context, vpnservice_id) + except vcns_exc.VcnsApiException: + self._update_status(context, vpnservice_id, ipsec_id, + "ERROR") + msg = (_("Failed to update firewall rule for ipsec " + "vpn with %(edge_id)s.") % {'edge_id': edge_id}) + raise nsxv_exc.NsxPluginException(err_msg=msg) + + 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_vpnservice(self, context, vpnservice): + LOG.debug('Creating VPN service %(vpn)s', {'vpn': vpnservice}) + # Only support distributed and exclusive router type + router_id = vpnservice['router_id'] + vpnservice_id = vpnservice['id'] + router = self._core_plugin.get_router(context, router_id) + if self._is_shared_router(router): + # Rolling back change on the neutron + self.service_plugin.delete_vpnservice(context, vpnservice_id) + msg = _("Router type is not supported for VPN service, only " + "support distributed and exclusive router") + raise n_exc.InvalidInput(error_message=msg) + + 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) + + def update_vpnservice(self, context, old_vpnservice, vpnservice): + pass + + def delete_vpnservice(self, context, vpnservice): + pass + + def _update_ipsec_config(self, edge_id, sites, enabled=True): + ipsec_config = {'featureType': "ipsec_4.0", + 'enabled': enabled} + + ipsec_config['sites'] = {'sites': sites} + try: + self._vcns.update_ipsec_config(edge_id, ipsec_config) + except vcns_exc.VcnsApiException: + msg = _("Failed to update ipsec vpn configuration with " + "edge_id: %s") % edge_id + raise nsxv_exc.NsxPluginException(err_msg=msg) diff --git a/vmware_nsx/services/vpnaas/nsxv/ipsec_validator.py b/vmware_nsx/services/vpnaas/nsxv/ipsec_validator.py new file mode 100644 index 0000000000..2549ffb8d1 --- /dev/null +++ b/vmware_nsx/services/vpnaas/nsxv/ipsec_validator.py @@ -0,0 +1,93 @@ +# Copyright 2016 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 neutron_vpnaas.db.vpn import vpn_validator +from oslo_log import log as logging + +from vmware_nsx._i18n import _ +from vmware_nsx.common import exceptions as nsxv_exc +from vmware_nsx.common import nsxv_constants + +LOG = logging.getLogger(__name__) + + +class IPsecValidator(vpn_validator.VpnReferenceValidator): + + """Validator methods for Vmware VPN support""" + + def __init__(self, service_plugin): + super(IPsecValidator, self).__init__() + self.vpn_plugin = service_plugin + + def validate_ikepolicy_version(self, policy_info): + """NSX Edge provides IKEv1""" + version = policy_info.get('ike_version') + if version != 'v1': + msg = _("Unsupported ike policy %s! only v1 " + "is supported right now.") % version + raise nsxv_exc.NsxIPsecVpnError(details=msg) + + def validate_ikepolicy_pfs(self, policy_info): + # Check whether pfs is allowed. + if not nsxv_constants.PFS_MAP.get(policy_info['pfs']): + msg = _("Unsupported pfs: %(pfs)s! currently only " + "the following pfs are supported on VSE: %s") % { + 'pfs': policy_info['pfs'], + 'supported': nsxv_constants.PFS_MAP} + raise nsxv_exc.NsxVpnValidationError(details=msg) + + def validate_encryption_algorithm(self, policy_info): + encryption = policy_info['encryption_algorithm'] + if encryption not in nsxv_constants.ENCRYPTION_ALGORITHM_MAP: + msg = _("Unsupported encryption_algorithm: %(algo)s! please " + "select one of the followoing supported algorithms: " + "%(supported_algos)s") % { + 'algo': encryption, + 'supported_algos': + nsxv_constants.ENCRYPTION_ALGORITHM_MAP} + raise nsxv_exc.NsxVpnValidationError(details=msg) + + def validate_ipsec_policy(self, context, policy_info): + """Ensure IPSec policy encap mode is tunnel for current REST API.""" + mode = policy_info['encapsulation_mode'] + if mode not in nsxv_constants.ENCAPSULATION_MODE_ALLOWED: + msg = _("Unsupported encapsulation mode: %s! currently only" + "'tunnel' mode is supported.") % mode + raise nsxv_exc.NsxVpnValidationError(details=msg) + + def validate_policies_matching_algorithms(self, ikepolicy, ipsecpolicy): + # In VSE, Phase 1 and Phase 2 share the same encryption_algorithm + # and authentication algorithms setting. At present, just record the + # discrepancy error in log and take ipsecpolicy to do configuration. + keys = ('auth_algorithm', 'encryption_algorithm', 'pfs') + for key in keys: + if ikepolicy[key] != ipsecpolicy[key]: + LOG.warning("IKEPolicy and IPsecPolicy should have consistent " + "auth_algorithm, encryption_algorithm and pfs for " + "VSE!") + break + + def validate_ipsec_conn(self, context, ipsec_site_conn): + ike_policy_id = ipsec_site_conn['ikepolicy_id'] + ipsec_policy_id = ipsec_site_conn['ipsecpolicy_id'] + ipsecpolicy = self.vpn_plugin.get_ipsecpolicy(context, + ipsec_policy_id) + ikepolicy = self.vpn_plugin.get_ikepolicy(context, + ike_policy_id) + self.validate_ikepolicy_version(ikepolicy) + self.validate_ikepolicy_pfs(ikepolicy) + self.validate_encryption_algorithm(ikepolicy) + self.validate_ipsec_policy(context, ipsecpolicy) + self.validate_policies_matching_algorithms(ikepolicy, ipsecpolicy) diff --git a/vmware_nsx/tests/unit/services/vpnaas/__init__.py b/vmware_nsx/tests/unit/services/vpnaas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/tests/unit/services/vpnaas/test_nsxv_vpnaas.py b/vmware_nsx/tests/unit/services/vpnaas/test_nsxv_vpnaas.py new file mode 100644 index 0000000000..0c9ae1e22c --- /dev/null +++ b/vmware_nsx/tests/unit/services/vpnaas/test_nsxv_vpnaas.py @@ -0,0 +1,255 @@ +# Copyright 2016 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 contextlib + +import mock +from neutron_lib import context +from neutron_lib import exceptions as n_exc +from neutron_lib.plugins import directory +from neutron_vpnaas.db.vpn import vpn_models # noqa +from oslo_utils import uuidutils + +from vmware_nsx.common import exceptions as nsxv_exc +from vmware_nsx.plugins.nsx_v.vshield.common import exceptions as vcns_exc +from vmware_nsx.services.vpnaas.nsxv import ipsec_driver +from vmware_nsx.tests.unit.nsx_v import test_plugin + + +_uuid = uuidutils.generate_uuid + +DRIVER_PATH = "vmware_nsx.services.vpnaas.nsxv.ipsec_driver.NSXvIPsecVpnDriver" +VALI_PATH = "vmware_nsx.services.vpnaas.nsxv.ipsec_validator.IPsecValidator" +FAKE_ROUTER_ID = "aaaaaa-bbbbb-ccc" +FAKE_VPNSERVICE_ID = _uuid() +FAKE_IPSEC_CONNECTION = {"vpnservice_id": FAKE_VPNSERVICE_ID, + "id": _uuid()} +FAKE_EDGE_ID = _uuid() +FAKE_IPSEC_VPN_SITE = {"peerIp": "192.168.1.1"} +FAKE_VCNSAPIEXC = {"status": "fail", + "head": "fake_head", + "response": "error"} +FAKE_NEW_CONNECTION = {"peer_cidrs": "192.168.1.0/24"} + + +class TestVpnaasDriver(test_plugin.NsxVPluginV2TestCase): + + def setUp(self): + super(TestVpnaasDriver, self).setUp() + self.context = context.get_admin_context() + self.service_plugin = mock.Mock() + self.validator = mock.Mock() + self.driver = ipsec_driver.NSXvIPsecVpnDriver(self.service_plugin) + self.plugin = directory.get_plugin() + self.l3plugin = self.plugin + + @contextlib.contextmanager + def router(self, name='vpn-test-router', tenant_id=_uuid(), + admin_state_up=True, **kwargs): + request = {'router': {'tenant_id': tenant_id, + 'name': name, + 'admin_state_up': admin_state_up}} + for arg in kwargs: + request['router'][arg] = kwargs[arg] + router = self.l3plugin.create_router(self.context, request) + yield router + + @mock.patch('%s.validate_ipsec_conn' % VALI_PATH) + @mock.patch('%s._convert_ipsec_conn' % DRIVER_PATH) + @mock.patch('%s._get_router_edge_id' % DRIVER_PATH) + @mock.patch('%s._generate_new_sites' % DRIVER_PATH) + @mock.patch('%s._update_ipsec_config' % DRIVER_PATH) + @mock.patch('%s._update_status' % DRIVER_PATH) + @mock.patch('%s._update_firewall_rules' % DRIVER_PATH) + def test_create_ipsec_site_connection(self, mock_update_fw, + mock_update_status, + mock_update_ipsec, mock_gen_new, + mock_get_id, + mock_conv_ipsec, + mock_val_conn): + mock_get_id.return_value = (FAKE_ROUTER_ID, FAKE_EDGE_ID) + mock_conv_ipsec.return_value = FAKE_IPSEC_VPN_SITE + mock_gen_new.return_value = FAKE_IPSEC_VPN_SITE + self.driver.create_ipsec_site_connection(self.context, + FAKE_IPSEC_CONNECTION) + mock_val_conn.assert_called_with(self.context, + FAKE_IPSEC_CONNECTION) + mock_conv_ipsec.assert_called_with(self.context, + FAKE_IPSEC_CONNECTION) + mock_get_id.assert_called_with(self.context, FAKE_VPNSERVICE_ID) + mock_gen_new.assert_called_with(FAKE_EDGE_ID, FAKE_IPSEC_VPN_SITE) + mock_update_ipsec.assert_called_with(FAKE_EDGE_ID, + FAKE_IPSEC_VPN_SITE, + enabled=True) + mock_update_fw.assert_called_with(self.context, FAKE_VPNSERVICE_ID) + mock_update_status.assert_called_with( + self.context, + FAKE_IPSEC_CONNECTION["vpnservice_id"], + FAKE_IPSEC_CONNECTION["id"], + "ACTIVE") + + @mock.patch('%s.validate_ipsec_conn' % VALI_PATH) + @mock.patch('%s._convert_ipsec_conn' % DRIVER_PATH) + @mock.patch('%s._get_router_edge_id' % DRIVER_PATH) + @mock.patch('%s._generate_new_sites' % DRIVER_PATH) + @mock.patch('%s._update_ipsec_config' % DRIVER_PATH) + @mock.patch('%s._update_status' % DRIVER_PATH) + def test_create_ipsec_site_connection_fail(self, + mock_update_status, + mock_update_ipsec, + mock_gen_new, mock_get_id, + mock_conv_ipsec, + mock_val_conn): + mock_get_id.return_value = (FAKE_ROUTER_ID, FAKE_EDGE_ID) + mock_conv_ipsec.return_value = FAKE_IPSEC_VPN_SITE + mock_gen_new.return_value = FAKE_IPSEC_VPN_SITE + mock_update_ipsec.side_effect = ( + vcns_exc.VcnsApiException(**FAKE_VCNSAPIEXC)) + self.assertRaises(nsxv_exc.NsxPluginException, + self.driver.create_ipsec_site_connection, + self.context, FAKE_IPSEC_CONNECTION) + mock_val_conn.assert_called_with(self.context, FAKE_IPSEC_CONNECTION) + mock_conv_ipsec.assert_called_with(self.context, FAKE_IPSEC_CONNECTION) + mock_get_id.assert_called_with(self.context, FAKE_VPNSERVICE_ID) + mock_gen_new.assert_called_with(FAKE_EDGE_ID, FAKE_IPSEC_VPN_SITE) + mock_update_ipsec.assert_called_with(FAKE_EDGE_ID, + FAKE_IPSEC_VPN_SITE, + enabled=True) + mock_update_status.assert_called_with( + self.context, + FAKE_IPSEC_CONNECTION["vpnservice_id"], + FAKE_IPSEC_CONNECTION["id"], + "ERROR") + + @mock.patch('%s.validate_ipsec_conn' % VALI_PATH) + @mock.patch('%s._convert_ipsec_conn' % DRIVER_PATH) + @mock.patch('%s._get_router_edge_id' % DRIVER_PATH) + @mock.patch('%s._generate_new_sites' % DRIVER_PATH) + @mock.patch('%s._update_ipsec_config' % DRIVER_PATH) + @mock.patch('%s._update_status' % DRIVER_PATH) + @mock.patch('%s._update_firewall_rules' % DRIVER_PATH) + def test_update_fw_fail(self, mock_update_fw, mock_update_status, + mock_update_ipsec, mock_gen_new, + mock_get_id, mock_conv_ipsec, mock_val_conn): + mock_get_id.return_value = (FAKE_ROUTER_ID, FAKE_EDGE_ID) + mock_conv_ipsec.return_value = FAKE_IPSEC_VPN_SITE + mock_gen_new.return_value = FAKE_IPSEC_VPN_SITE + mock_update_fw.side_effect = ( + vcns_exc.VcnsApiException(**FAKE_VCNSAPIEXC)) + self.assertRaises(nsxv_exc.NsxPluginException, + self.driver.create_ipsec_site_connection, + self.context, FAKE_IPSEC_CONNECTION) + mock_val_conn.assert_called_with(self.context, FAKE_IPSEC_CONNECTION) + mock_conv_ipsec.assert_called_with(self.context, FAKE_IPSEC_CONNECTION) + mock_get_id.assert_called_with(self.context, FAKE_VPNSERVICE_ID) + mock_gen_new.assert_called_with(FAKE_EDGE_ID, FAKE_IPSEC_VPN_SITE) + mock_update_ipsec.assert_called_with(FAKE_EDGE_ID, + FAKE_IPSEC_VPN_SITE, + enabled=True) + mock_update_fw.assert_called_with(self.context, FAKE_VPNSERVICE_ID) + mock_update_status.assert_called_with( + self.context, + FAKE_IPSEC_CONNECTION["vpnservice_id"], + FAKE_IPSEC_CONNECTION["id"], + "ERROR") + + @mock.patch('%s._get_router_edge_id' % DRIVER_PATH) + @mock.patch('%s._update_site_dict' % DRIVER_PATH) + @mock.patch('%s._update_ipsec_config' % DRIVER_PATH) + @mock.patch('%s._update_firewall_rules' % DRIVER_PATH) + def test_update_ipsec(self, mock_update_fw, mock_update_ipsec, + mock_update_sites, mock_get_id): + mock_get_id.return_value = (FAKE_ROUTER_ID, FAKE_EDGE_ID) + mock_update_sites.return_value = FAKE_IPSEC_VPN_SITE + self.driver.update_ipsec_site_connection(self.context, + FAKE_IPSEC_CONNECTION, + FAKE_NEW_CONNECTION) + mock_update_sites.assert_called_with(self.context, FAKE_EDGE_ID, + FAKE_IPSEC_CONNECTION, + FAKE_NEW_CONNECTION) + mock_update_ipsec.assert_called_with(FAKE_EDGE_ID, FAKE_IPSEC_VPN_SITE) + mock_update_fw.assert_called_with(self.context, FAKE_VPNSERVICE_ID) + + @mock.patch('%s._get_router_edge_id' % DRIVER_PATH) + @mock.patch('%s._update_site_dict' % DRIVER_PATH) + @mock.patch('%s._update_ipsec_config' % DRIVER_PATH) + @mock.patch('%s._update_firewall_rules' % DRIVER_PATH) + def test_update_ipsec_fail_with_notfound(self, mock_update_fw, + mock_update_ipsec, + mock_update_sites, mock_get_id): + mock_get_id.return_value = (FAKE_ROUTER_ID, FAKE_EDGE_ID) + mock_update_sites.return_value = {} + self.assertRaises(nsxv_exc.NsxIPsecVpnMappingNotFound, + self.driver.update_ipsec_site_connection, + self.context, FAKE_IPSEC_CONNECTION, + FAKE_NEW_CONNECTION) + mock_update_sites.assert_called_with(self.context, + FAKE_EDGE_ID, + FAKE_IPSEC_CONNECTION, + FAKE_NEW_CONNECTION) + + @mock.patch('%s._get_router_edge_id' % DRIVER_PATH) + @mock.patch('%s._update_site_dict' % DRIVER_PATH) + @mock.patch('%s._update_ipsec_config' % DRIVER_PATH) + @mock.patch('%s._update_firewall_rules' % DRIVER_PATH) + def test_update_ipsec_fail_with_fw_fail(self, mock_update_fw, + mock_update_ipsec, + mock_update_sites, mock_get_id): + mock_get_id.return_value = (FAKE_ROUTER_ID, FAKE_EDGE_ID) + mock_update_fw.side_effect = ( + vcns_exc.VcnsApiException(**FAKE_VCNSAPIEXC)) + self.assertRaises(nsxv_exc.NsxPluginException, + self.driver.update_ipsec_site_connection, + self.context, FAKE_IPSEC_CONNECTION, + FAKE_NEW_CONNECTION) + mock_update_sites.assert_called_with(self.context, FAKE_EDGE_ID, + FAKE_IPSEC_CONNECTION, + FAKE_NEW_CONNECTION) + mock_update_fw.assert_called_with(self.context, FAKE_VPNSERVICE_ID) + + @mock.patch('%s._get_router_edge_id' % DRIVER_PATH) + @mock.patch('%s._update_site_dict' % DRIVER_PATH) + @mock.patch('%s._update_ipsec_config' % DRIVER_PATH) + @mock.patch('%s._update_status' % DRIVER_PATH) + def test_update_ipsec_fail_with_site_fail(self, mock_update_status, + mock_update_ipsec, + mock_update_sites, mock_get_id): + mock_get_id.return_value = (FAKE_ROUTER_ID, FAKE_EDGE_ID) + mock_update_sites.return_value = FAKE_IPSEC_VPN_SITE + mock_update_ipsec.side_effect = ( + vcns_exc.VcnsApiException(**FAKE_VCNSAPIEXC)) + self.assertRaises(nsxv_exc.NsxPluginException, + self.driver.update_ipsec_site_connection, + self.context, + FAKE_IPSEC_CONNECTION, + FAKE_NEW_CONNECTION) + mock_update_sites.assert_called_with(self.context, FAKE_EDGE_ID, + FAKE_IPSEC_CONNECTION, + FAKE_NEW_CONNECTION) + mock_update_ipsec.assert_called_with(FAKE_EDGE_ID, + FAKE_IPSEC_VPN_SITE) + mock_update_status.assert_called_with( + self.context, + FAKE_IPSEC_CONNECTION["vpnservice_id"], + FAKE_IPSEC_CONNECTION["id"], + "ERROR") + + def test_create_vpn_service_on_shared_router(self): + with self.router(router_type='shared') as router, self.subnet(): + vpnservice = {'router_id': router['id'], + 'id': _uuid()} + self.assertRaises(n_exc.InvalidInput, + self.driver.create_vpnservice, + self.context, vpnservice) diff --git a/vmware_nsx/tests/unit/shell/test_admin_utils.py b/vmware_nsx/tests/unit/shell/test_admin_utils.py index a9b9349eca..e83bae2ffa 100644 --- a/vmware_nsx/tests/unit/shell/test_admin_utils.py +++ b/vmware_nsx/tests/unit/shell/test_admin_utils.py @@ -21,6 +21,7 @@ from neutron.db import servicetype_db # noqa from neutron.quota import resource_registry from neutron.tests import base from neutron_lib.callbacks import registry +from neutron_lib import constants from oslo_config import cfg from oslo_log import _options from oslo_log import log as logging @@ -137,10 +138,13 @@ class TestNsxvAdminUtils(AbstractTestAdminUtils, return_value=0).start() self._plugin = nsxv_utils.NsxVPluginWrapper() - mock_nm_get_plugin = mock.patch( - "neutron_lib.plugins.directory.get_plugin") - self.mock_nm_get_plugin = mock_nm_get_plugin.start() - self.mock_nm_get_plugin.return_value = self._plugin + + def get_plugin_mock(alias=constants.CORE): + if alias in (constants.CORE, constants.L3): + return self._plugin + + mock.patch("neutron_lib.plugins.directory.get_plugin", + side_effect=get_plugin_mock).start() # Create a router to make sure we have deployed an edge self.router = self.create_router()