e3f103f269
The VPNaaS plugin expects the driver to update the connection status from a separate process/thread/agent. When the user requests a connection/list, the status is retrived from the VPNaaS DB, without calling the driver. To avoid adding a process to actively query and update all connections statuses, this patch creates a new VPNaaS plugin, to be used instead of hte default one. This plugin (vmware_nsx_vpnaas) will issue a get-statuses call to the driver, update the current statuses in the DB, and call the original plugin. Change-Id: Ib750bfb8f0c8ad12265fa71506182ff5d7e8030a
804 lines
35 KiB
Python
804 lines
35 KiB
Python
# Copyright 2017 VMware, Inc.
|
|
# All Rights Reserved
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import netaddr
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import excutils
|
|
|
|
from neutron_lib.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
|
|
|
|
from vmware_nsx.common import exceptions as nsx_exc
|
|
from vmware_nsx.db import db
|
|
from vmware_nsx.extensions import projectpluginmap
|
|
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 RouterWithSNAT(nexception.BadRequest):
|
|
message = _("Router %(router_id)s has a VPN service and cannot enable "
|
|
"SNAT")
|
|
|
|
|
|
class RouterWithOverlapNoSnat(nexception.BadRequest):
|
|
message = _("Router %(router_id)s has a subnet overlapping with a VPN "
|
|
"local subnet, and cannot disable SNAT")
|
|
|
|
|
|
class RouterOverlapping(nexception.BadRequest):
|
|
message = _("Router %(router_id)s interface is overlapping with a VPN "
|
|
"local subnet and cannot be added")
|
|
|
|
|
|
class NSXv3IPsecVpnDriver(service_drivers.VpnDriver):
|
|
|
|
def __init__(self, service_plugin):
|
|
self.vpn_plugin = service_plugin
|
|
self._core_plugin = directory.get_plugin()
|
|
if self._core_plugin.is_tvd_plugin():
|
|
self._core_plugin = self._core_plugin.get_plugin_by_type(
|
|
projectpluginmap.NsxPlugins.NSX_T)
|
|
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)
|
|
|
|
registry.subscribe(
|
|
self._verify_overlap_subnet, resources.ROUTER_INTERFACE,
|
|
events.BEFORE_CREATE)
|
|
|
|
@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, plugin_type, context,
|
|
router_id=None):
|
|
"""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,
|
|
'destinations': self._translate_addresses_to_target(
|
|
peer_cidrs),
|
|
'sources': 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'] or ikepolicy['id'],
|
|
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']],
|
|
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'] or ipsecpolicy['id'],
|
|
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 _get_dpd_profile_name(self, connection):
|
|
return (connection['name'] or connection['id'])[:240] + '-dpd-profile'
|
|
|
|
def _create_dpd_profile(self, context, connection):
|
|
dpd_info = connection['dpd']
|
|
try:
|
|
profile = self._nsx_vpn.dpd_profile.create(
|
|
self._get_dpd_profile_name(connection),
|
|
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,
|
|
name=self._get_dpd_profile_name(connection),
|
|
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'] or connection['id'],
|
|
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'] or connection['id'],
|
|
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, project_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, 'project_id': project_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 = 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,
|
|
vpnservice['project_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-' + router_id],
|
|
'device_owner': [ipsec_utils.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'], force_delete_vpn=True)
|
|
|
|
def _check_subnets_overlap_with_all_conns(self, context, subnets):
|
|
# find all vpn services with connections
|
|
filters = {'status': [constants.ACTIVE]}
|
|
connections = self.vpn_plugin.get_ipsec_site_connections(
|
|
context, filters=filters)
|
|
for conn in connections:
|
|
srv_id = conn.get('vpnservice_id')
|
|
srv = self.vpn_plugin._get_vpnservice(context, srv_id)
|
|
srv_subnet = self.l3_plugin.get_subnet(
|
|
context, srv['subnet_id'])
|
|
if netaddr.IPSet(subnets) & netaddr.IPSet([srv_subnet['cidr']]):
|
|
return False
|
|
|
|
return True
|
|
|
|
def _verify_overlap_subnet(self, resource, event, trigger, **kwargs):
|
|
"""Upon router interface creation validation overlapping with vpn"""
|
|
router_db = kwargs.get('router_db')
|
|
port = kwargs.get('port')
|
|
if not port or not router_db:
|
|
LOG.warning("NSX V3 VPNaaS ROUTER_INTERFACE BEFORE_CRAETE "
|
|
"callback didn't get all the relevant information")
|
|
return
|
|
|
|
if router_db.enable_snat:
|
|
# checking only no-snat routers
|
|
return
|
|
|
|
admin_con = n_context.get_admin_context()
|
|
subnet_id = port['fixed_ips'][0].get('subnet_id')
|
|
if subnet_id:
|
|
subnet = self._core_plugin.get_subnet(admin_con, subnet_id)
|
|
# find all vpn services with connections
|
|
if not self._check_subnets_overlap_with_all_conns(
|
|
admin_con, [subnet['cidr']]):
|
|
raise RouterOverlapping(router_id=kwargs.get('router_id'))
|
|
|
|
def validate_router_gw_info(self, context, router_id, gw_info):
|
|
"""Upon router gw update - verify no-snat"""
|
|
# check if this router has a vpn service
|
|
admin_con = context.elevated()
|
|
# get all relevant services, except those waiting to be deleted or in
|
|
# ERROR state
|
|
filters = {'router_id': [router_id],
|
|
'status': [constants.ACTIVE, constants.PENDING_CREATE,
|
|
constants.INACTIVE, constants.PENDING_UPDATE]}
|
|
services = self.vpn_plugin.get_vpnservices(admin_con, filters=filters)
|
|
if services:
|
|
# do not allow enable-snat
|
|
if (gw_info and
|
|
gw_info.get('enable_snat', cfg.CONF.enable_snat_by_default)):
|
|
raise RouterWithSNAT(router_id=router_id)
|
|
else:
|
|
# if this is a non-vpn router. if snat was disabled, should check
|
|
# there is no overlapping with vpn connections
|
|
if (gw_info and
|
|
not gw_info.get('enable_snat',
|
|
cfg.CONF.enable_snat_by_default)):
|
|
# get router subnets
|
|
subnets = self._core_plugin._find_router_subnets_cidrs(
|
|
context, router_id)
|
|
# find all vpn services with connections
|
|
if not self._check_subnets_overlap_with_all_conns(
|
|
admin_con, subnets):
|
|
raise RouterWithOverlapNoSnat(router_id=router_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, enabled=True):
|
|
try:
|
|
session = self._nsx_vpn.session.create(
|
|
connection['name'] or connection['id'],
|
|
local_ep_id, peer_ep_id, rules,
|
|
description=connection['description'],
|
|
tags=self._nsx_tags(context, connection),
|
|
enabled=enabled)
|
|
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=None,
|
|
enabled=True):
|
|
self._nsx_vpn.session.update(
|
|
session_id,
|
|
name=connection['name'] or connection['id'],
|
|
description=connection['description'],
|
|
policy_rules=rules,
|
|
enabled=enabled)
|
|
|
|
def get_ipsec_site_connection_status(self, context, ipsec_site_conn_id):
|
|
mapping = db.get_nsx_vpn_connection_mapping(
|
|
context.session, ipsec_site_conn_id)
|
|
if not mapping or not mapping['session_id']:
|
|
LOG.info("Couldn't find NSX session for VPN connection %s",
|
|
ipsec_site_conn_id)
|
|
return
|
|
|
|
status_result = self._nsx_vpn.session.get_status(mapping['session_id'])
|
|
if status_result and 'session_status' in status_result:
|
|
status = status_result['session_status']
|
|
# NSX statuses are UP, DOWN, DEGRADE
|
|
# VPNaaS connection status should be ACTIVE or DOWN
|
|
if status == 'UP':
|
|
return 'ACTIVE'
|
|
elif status == 'DOWN' or status == 'DEGRADED':
|
|
return 'DOWN'
|
|
|
|
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)
|
|
connection_enabled = (vpnservice['admin_state_up'] and
|
|
ipsec_site_conn['admin_state_up'])
|
|
session_id = self._create_session(
|
|
context, ipsec_site_conn, local_ep_id, peer_ep_id, rules,
|
|
enabled=connection_enabled)
|
|
|
|
# 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)
|
|
|
|
# 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
|
|
old_ipsec_conn['name'] != ipsec_site_conn['name']):
|
|
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)
|
|
connection_enabled = (vpnservice['admin_state_up'] and
|
|
ipsec_site_conn['admin_state_up'])
|
|
self._update_session(mapping['session_id'], ipsec_site_conn, rules,
|
|
enabled=connection_enabled)
|
|
|
|
if ipsec_site_conn['peer_cidrs'] != old_ipsec_conn['peer_cidrs']:
|
|
# Update firewall
|
|
self._update_firewall_rules(context, vpnservice)
|
|
|
|
# No service updates. No need to update router advertisement rules
|
|
|
|
def _create_vpn_service(self, tier0_uuid):
|
|
try:
|
|
service = self._nsx_vpn.service.create(
|
|
'Neutron VPN service for T0 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'] == 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 _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
|
|
# Note(asarfaty): using a unique device owner and device id to
|
|
# make sure tis port will be ignored in certain queries
|
|
ext_net = vpnservice['router']['gw_port']['network_id']
|
|
port_data = {
|
|
'port': {
|
|
'network_id': ext_net,
|
|
'name': 'VPN local address port',
|
|
'admin_state_up': True,
|
|
'device_id': 'router-' + router_id,
|
|
'device_owner': ipsec_utils.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.elevated(), vpnservice)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
# Rolling back change on the neutron
|
|
self.service_plugin.delete_vpnservice(context, vpnservice_id)
|
|
|
|
vpnservice['external_v4_ip'] = local_address
|
|
self.service_plugin.set_external_tunnel_ips(context,
|
|
vpnservice_id,
|
|
v4_ip=local_address)
|
|
self._create_vpn_service_if_needed(context, vpnservice)
|
|
|
|
def update_vpnservice(self, context, old_vpnservice, vpnservice):
|
|
# Only handle the case of admin-state-up changes
|
|
if old_vpnservice['admin_state_up'] != vpnservice['admin_state_up']:
|
|
# update all relevant connections
|
|
filters = {'vpnservice_id': [vpnservice['id']]}
|
|
connections = self.vpn_plugin.get_ipsec_site_connections(
|
|
context, filters=filters)
|
|
for conn in connections:
|
|
mapping = db.get_nsx_vpn_connection_mapping(
|
|
context.session, conn['id'])
|
|
if mapping:
|
|
connection_enabled = (vpnservice['admin_state_up'] and
|
|
conn['admin_state_up'])
|
|
self._update_session(mapping['session_id'], conn,
|
|
enabled=connection_enabled)
|
|
|
|
def delete_vpnservice(self, context, vpnservice):
|
|
# Do not delete the NSX service or DB entry as those will be reused.
|
|
pass
|