From d97a333d21278596f3f291fa31b8659e4c76835c Mon Sep 17 00:00:00 2001 From: Adit Sarfaty Date: Thu, 15 Feb 2018 11:13:16 +0200 Subject: [PATCH] NSX-v3 VPNaaS: Use a local address from the external network The local address of the local endpoint for the VPN should be an unused address on the external GW network of the Tier1 router (and not the GW address itself). To make sure this IP will not be used for anything else, a neutron port is created. The port will be deleted once the router (or its gw) is deleted. The ip will be used for all the vpn services & connection on this tier1 router. Change-Id: If956fd08f5c9cfde5cba9326c18d1d489c47a505 --- vmware_nsx/plugins/nsx_v3/plugin.py | 10 ++- .../services/vpnaas/nsxv3/ipsec_driver.py | 75 ++++++++++++------- .../services/vpnaas/nsxv3/ipsec_validator.py | 11 +-- .../unit/services/vpnaas/test_nsxv3_vpnaas.py | 9 +-- 4 files changed, 63 insertions(+), 42 deletions(-) diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index 62f33b4e26..dd55c94f4e 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -2498,6 +2498,12 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, self.nsxlib.ns_group.update_lport( context, lport_id, nsx_origial, nsx_updated) + def base_create_port(self, context, port): + neutron_db = super(NsxV3Plugin, self).create_port(context, port) + self._extension_manager.process_create_port( + context, port['port'], neutron_db) + return neutron_db + def create_port(self, context, port, l2gw_port_check=False): port_data = port['port'] dhcp_opts = port_data.get(ext_edo.EXTRADHCPOPTS) @@ -2518,9 +2524,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, self._assert_on_port_admin_state( port_data, port_data.get('device_owner')) - neutron_db = super(NsxV3Plugin, self).create_port(context, port) - self._extension_manager.process_create_port( - context, port_data, neutron_db) + neutron_db = self.base_create_port(context, port) port["port"].update(neutron_db) (is_psec_on, has_ip, sgids, psgids) = ( diff --git a/vmware_nsx/services/vpnaas/nsxv3/ipsec_driver.py b/vmware_nsx/services/vpnaas/nsxv3/ipsec_driver.py index 6a5f07936a..6f28a32adf 100644 --- a/vmware_nsx/services/vpnaas/nsxv3/ipsec_driver.py +++ b/vmware_nsx/services/vpnaas/nsxv3/ipsec_driver.py @@ -22,6 +22,7 @@ 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 import context as n_context from neutron_lib import exceptions as nexception from neutron_lib.plugins import directory from neutron_vpnaas.services.vpn import service_drivers @@ -37,6 +38,7 @@ from vmware_nsxlib.v3 import vpn_ipsec LOG = logging.getLogger(__name__) IPSEC = 'ipsec' +VPN_PORT_OWNER = constants.DEVICE_OWNER_NEUTRON_PREFIX + 'vpnservice' class RouterWithSNAT(nexception.BadRequest): @@ -348,18 +350,32 @@ class NSXv3IPsecVpnDriver(service_drivers.VpnDriver): return local_ep_id # create a new one - local_addr = self._get_router_ext_gw(context, router_id) + local_addr = vpnservice['external_v4_ip'] 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 _find_vpn_service_port(self, context, router_id): + """Look for the neutron port created for the vpnservice of a router""" + filters = {'device_id': [router_id], + 'device_owner': [VPN_PORT_OWNER]} + ports = self.l3_plugin.get_ports(context, filters=filters) + if ports: + return ports[0] + def _delete_local_endpoint(self, resource, event, trigger, **kwargs): """Upon router deletion / gw removal delete the matching endpoint""" router_id = kwargs.get('router_id') + # delete the local endpoint from the NSX local_ep_id = self._search_local_endpint(router_id) if local_ep_id: self._nsx_vpn.local_endpoint.delete(local_ep_id) + # delete the neutron port with this IP + ctx = n_context.get_admin_context() + port = self._find_vpn_service_port(ctx, router_id) + if port: + self.l3_plugin.delete_port(ctx, port['id']) def validate_router_gw_info(self, context, router_id, gw_info): """Upon router gw update - verify no-snat""" @@ -568,22 +584,6 @@ class NSXv3IPsecVpnDriver(service_drivers.VpnDriver): # 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( @@ -635,29 +635,52 @@ class NSXv3IPsecVpnDriver(service_drivers.VpnDriver): tier0_uuid = self._get_tier0_uuid(context, router_id) return self._find_vpn_service(tier0_uuid) + def _get_service_local_address(self, context, vpnservice): + """Find/Allocate a port on the external network + + to save the ip to be used as the local ip of this service + """ + router_id = vpnservice.router['id'] + # check if this router already have an IP + port = self._find_vpn_service_port(context, router_id) + if not port: + # create a new port, on the external network of the router + ext_net = vpnservice.router.gw_port['network_id'] + port_data = { + 'port': { + 'network_id': ext_net, + 'name': None, + 'admin_state_up': True, + 'device_id': vpnservice.router['id'], + 'device_owner': VPN_PORT_OWNER, + 'fixed_ips': constants.ATTR_NOT_SPECIFIED, + 'mac_address': constants.ATTR_NOT_SPECIFIED, + 'port_security_enabled': False, + 'tenant_id': vpnservice['tenant_id']}} + port = self.l3_plugin.base_create_port(context, port_data) + # return the port ip as the local address + return port['fixed_ips'][0]['ip_address'] + 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'] - + vpnservice = self.service_plugin._get_vpnservice(context, + vpnservice_id) try: self.validator.validate_vpnservice(context, vpnservice) + local_address = self._get_service_local_address( + 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 + vpnservice['external_v4_ip'] = local_address self.service_plugin.set_external_tunnel_ips(context, vpnservice_id, - v4_ip=v4_ip, v6_ip=v6_ip) + v4_ip=local_address) self._create_vpn_service_if_needed(context, vpnservice) def update_vpnservice(self, context, old_vpnservice, vpnservice): diff --git a/vmware_nsx/services/vpnaas/nsxv3/ipsec_validator.py b/vmware_nsx/services/vpnaas/nsxv3/ipsec_validator.py index f1d5cd319d..49fd274bf9 100644 --- a/vmware_nsx/services/vpnaas/nsxv3/ipsec_validator.py +++ b/vmware_nsx/services/vpnaas/nsxv3/ipsec_validator.py @@ -184,7 +184,7 @@ class IPsecV3Validator(vpn_validator.VpnReferenceValidator): "as connection %(id)s") % {'local': local_cidrs, 'peer': peer_cidrs, 'id': conn['id']}) - raise nsx_exc.NsxVpnValidationError(details=msg) + raise nsx_exc.NsxVpnValidationError(details=msg) def _check_unique_addresses(self, context, ipsec_site_conn): """Validate no repeating local & peer addresses (of all tenants) @@ -302,12 +302,13 @@ class IPsecV3Validator(vpn_validator.VpnReferenceValidator): #TODO(asarfaty): IPv6 is not yet supported. add validation def _get_service_local_address(self, context, vpnservice_id): + """The local address of the service is assigned upon creation + + From the attached external network pool + """ 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'] + return vpnservice['external_v4_ip'] def _validate_router(self, context, router_id): # Verify that the router gw network is connected to an active-standby diff --git a/vmware_nsx/tests/unit/services/vpnaas/test_nsxv3_vpnaas.py b/vmware_nsx/tests/unit/services/vpnaas/test_nsxv3_vpnaas.py index 2fa26dec1c..6c62ec0cb8 100644 --- a/vmware_nsx/tests/unit/services/vpnaas/test_nsxv3_vpnaas.py +++ b/vmware_nsx/tests/unit/services/vpnaas/test_nsxv3_vpnaas.py @@ -192,12 +192,6 @@ class TestDriverValidation(base.BaseTestCase): 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}}] @@ -211,6 +205,7 @@ class TestDriverValidation(base.BaseTestCase): return {'id': service_id, 'router_id': service_id, 'subnet_id': 'dummy_subnet', + 'external_v4_ip': '1.1.1.%s' % service_id, 'subnet': {'id': 'dummy_subnet', 'cidr': subnet_cidr}} @@ -223,8 +218,6 @@ class TestDriverValidation(base.BaseTestCase): 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,