diff --git a/etc/vpn_agent.ini b/etc/vpn_agent.ini index 3f8f61b18..c3089df95 100644 --- a/etc/vpn_agent.ini +++ b/etc/vpn_agent.ini @@ -3,11 +3,12 @@ # Note vpn-agent inherits l3-agent, so you can use configs on l3-agent also [vpnagent] -#vpn device drivers which vpn agent will use -#If we want to use multiple drivers, we need to define this option multiple times. -#vpn_device_driver=neutron.services.vpn.device_drivers.ipsec.OpenSwanDriver -#vpn_device_driver=another_driver +# vpn device drivers which vpn agent will use +# If we want to use multiple drivers, we need to define this option multiple times. +# vpn_device_driver=neutron.services.vpn.device_drivers.ipsec.OpenSwanDriver +# vpn_device_driver=neutron.services.vpn.device_drivers.cisco_ipsec.CiscoCsrIPsecDriver +# vpn_device_driver=another_driver [ipsec] -#Status check interval -#ipsec_status_check_interval=60 +# Status check interval +# ipsec_status_check_interval=60 diff --git a/neutron/db/vpn/vpn_db.py b/neutron/db/vpn/vpn_db.py index cbcf6efe9..c750d8c4b 100644 --- a/neutron/db/vpn/vpn_db.py +++ b/neutron/db/vpn/vpn_db.py @@ -367,6 +367,25 @@ class VPNPluginDb(VPNPluginBase, base_db.CommonDbMixin): self._make_ipsec_site_connection_dict, filters=filters, fields=fields) + def update_ipsec_site_conn_status(self, context, conn_id, new_status): + with context.session.begin(): + self._update_connection_status(context, conn_id, new_status, True) + + def _update_connection_status(self, context, conn_id, new_status, + updated_pending): + """Update the connection status, if changed. + + If the connection is not in a pending state, unconditionally update + the status. Likewise, if in a pending state, and have an indication + that the status has changed, then update the database. + """ + try: + conn_db = self._get_ipsec_site_connection(context, conn_id) + except vpnaas.IPsecSiteConnectionNotFound: + return + if not utils.in_pending_status(conn_db.status) or updated_pending: + conn_db.status = new_status + def _make_ikepolicy_dict(self, ikepolicy, fields=None): res = {'id': ikepolicy['id'], 'tenant_id': ikepolicy['tenant_id'], @@ -667,11 +686,6 @@ class VPNPluginRpcDbMixin(): vpnservice_db.status = vpnservice['status'] for conn_id, conn in vpnservice[ 'ipsec_site_connections'].items(): - try: - conn_db = self._get_ipsec_site_connection( - context, conn_id) - except vpnaas.IPsecSiteConnectionNotFound: - continue - if (not utils.in_pending_status(conn_db.status) - or conn['updated_pending_status']): - conn_db.status = conn['status'] + self._update_connection_status( + context, conn_id, conn['status'], + conn['updated_pending_status']) diff --git a/neutron/services/vpn/common/topics.py b/neutron/services/vpn/common/topics.py index 87df69ce8..2639fbe6d 100644 --- a/neutron/services/vpn/common/topics.py +++ b/neutron/services/vpn/common/topics.py @@ -18,3 +18,5 @@ IPSEC_DRIVER_TOPIC = 'ipsec_driver' IPSEC_AGENT_TOPIC = 'ipsec_agent' +CISCO_IPSEC_DRIVER_TOPIC = 'cisco_csr_ipsec_driver' +CISCO_IPSEC_AGENT_TOPIC = 'cisco_csr_ipsec_agent' diff --git a/neutron/services/vpn/plugin.py b/neutron/services/vpn/plugin.py index 3d5835467..1a128dafc 100644 --- a/neutron/services/vpn/plugin.py +++ b/neutron/services/vpn/plugin.py @@ -19,7 +19,11 @@ # @author: Swaminathan Vasudevan, Hewlett-Packard from neutron.db.vpn import vpn_db -from neutron.services.vpn.service_drivers import ipsec as ipsec_driver +from neutron.openstack.common import log as logging +from neutron.plugins.common import constants +from neutron.services import service_base + +LOG = logging.getLogger(__name__) class VPNPlugin(vpn_db.VPNPluginDb): @@ -30,7 +34,7 @@ class VPNPlugin(vpn_db.VPNPluginDb): Most DB related works are implemented in class vpn_db.VPNPluginDb. """ - supported_extension_aliases = ["vpnaas"] + supported_extension_aliases = ["vpnaas", "service-type"] class VPNDriverPlugin(VPNPlugin, vpn_db.VPNPluginRpcDbMixin): @@ -38,7 +42,11 @@ class VPNDriverPlugin(VPNPlugin, vpn_db.VPNPluginRpcDbMixin): #TODO(nati) handle ikepolicy and ipsecpolicy update usecase def __init__(self): super(VPNDriverPlugin, self).__init__() - self.ipsec_driver = ipsec_driver.IPsecVPNDriver(self) + # Load the service driver from neutron.conf. + drivers, default_provider = service_base.load_drivers( + constants.VPN, self) + LOG.info(_("VPN plugin using service driver: %s"), default_provider) + self.ipsec_driver = drivers[default_provider] def _get_driver_for_vpnservice(self, vpnservice): return self.ipsec_driver diff --git a/neutron/services/vpn/service_drivers/__init__.py b/neutron/services/vpn/service_drivers/__init__.py index edb27abec..c932de716 100644 --- a/neutron/services/vpn/service_drivers/__init__.py +++ b/neutron/services/vpn/service_drivers/__init__.py @@ -19,10 +19,20 @@ import abc import six +from neutron import manager +from neutron.openstack.common import log as logging +from neutron.openstack.common.rpc import proxy +from neutron.plugins.common import constants + +LOG = logging.getLogger(__name__) + @six.add_metaclass(abc.ABCMeta) class VpnDriver(object): + def __init__(self, service_plugin): + self.service_plugin = service_plugin + @property def service_type(self): pass @@ -39,3 +49,43 @@ class VpnDriver(object): @abc.abstractmethod def delete_vpnservice(self, context, vpnservice): pass + + +class BaseIPsecVpnAgentApi(proxy.RpcProxy): + """Base class for IPSec API to agent.""" + + def __init__(self, to_agent_topic, topic, default_version): + self.to_agent_topic = to_agent_topic + super(BaseIPsecVpnAgentApi, self).__init__(topic, default_version) + + def _agent_notification(self, context, method, router_id, + version=None, **kwargs): + """Notify update for the agent. + + This method will find where is the router, and + dispatch notification for the agent. + """ + admin_context = context.is_admin and context or context.elevated() + plugin = manager.NeutronManager.get_service_plugins().get( + constants.L3_ROUTER_NAT) + if not version: + version = self.RPC_API_VERSION + l3_agents = plugin.get_l3_agents_hosting_routers( + admin_context, [router_id], + admin_state_up=True, + active=True) + for l3_agent in l3_agents: + LOG.debug(_('Notify agent at %(topic)s.%(host)s the message ' + '%(method)s'), + {'topic': self.to_agent_topic, + 'host': l3_agent.host, + 'method': method, + 'args': kwargs}) + self.cast( + context, self.make_msg(method, **kwargs), + version=version, + topic='%s.%s' % (self.to_agent_topic, l3_agent.host)) + + def vpnservice_updated(self, context, router_id): + """Send update event of vpnservices.""" + self._agent_notification(context, 'vpnservice_updated', router_id) diff --git a/neutron/services/vpn/service_drivers/cisco_csr_db.py b/neutron/services/vpn/service_drivers/cisco_csr_db.py new file mode 100644 index 000000000..3df2fb036 --- /dev/null +++ b/neutron/services/vpn/service_drivers/cisco_csr_db.py @@ -0,0 +1,235 @@ +# Copyright 2014 Cisco Systems, 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 sqlalchemy as sa +from sqlalchemy.orm import exc as sql_exc + +from neutron.common import exceptions +from neutron.db import model_base +from neutron.db import models_v2 +from neutron.db.vpn import vpn_db +from neutron.openstack.common.db import exception as db_exc +from neutron.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + +# Note: Artificially limit these to reduce mapping table size and performance +# Tunnel can be 0..7FFFFFFF, IKE policy can be 1..10000, IPSec policy can be +# 1..31 characters long. +MAX_CSR_TUNNELS = 10000 +MAX_CSR_IKE_POLICIES = 2000 +MAX_CSR_IPSEC_POLICIES = 2000 + +TUNNEL = 'Tunnel' +IKE_POLICY = 'IKE Policy' +IPSEC_POLICY = 'IPSec Policy' + +MAPPING_LIMITS = {TUNNEL: (0, MAX_CSR_TUNNELS), + IKE_POLICY: (1, MAX_CSR_IKE_POLICIES), + IPSEC_POLICY: (1, MAX_CSR_IPSEC_POLICIES)} + + +class CsrInternalError(exceptions.NeutronException): + message = _("Fatal - %(reason)s") + + +class IdentifierMap(model_base.BASEV2, models_v2.HasTenant): + + """Maps OpenStack IDs to compatible numbers for Cisco CSR.""" + + __tablename__ = 'cisco_csr_identifier_map' + + ipsec_site_conn_id = sa.Column(sa.String(64), + sa.ForeignKey('ipsec_site_connections.id', + ondelete="CASCADE"), + primary_key=True) + csr_tunnel_id = sa.Column(sa.Integer, nullable=False) + csr_ike_policy_id = sa.Column(sa.Integer, nullable=False) + csr_ipsec_policy_id = sa.Column(sa.Integer, nullable=False) + + +def get_next_available_id(session, table_field, id_type): + """Find first unused id for the specified field in IdentifierMap table. + + As entries are removed, find the first "hole" and return that as the + next available ID. To improve performance, artificially limit + the number of entries to a smaller range. Currently, these IDs are + globally unique. Could enhance in the future to be unique per router + (CSR). + """ + min_value = MAPPING_LIMITS[id_type][0] + max_value = MAPPING_LIMITS[id_type][1] + rows = session.query(table_field).order_by(table_field) + used_ids = set([row[0] for row in rows]) + all_ids = set(range(min_value, max_value + min_value)) + available_ids = all_ids - used_ids + if not available_ids: + msg = _("No available Cisco CSR %(type)s IDs from " + "%(min)d..%(max)d") % {'type': id_type, + 'min': min_value, + 'max': max_value} + LOG.error(msg) + raise IndexError(msg) + return available_ids.pop() + + +def get_next_available_tunnel_id(session): + """Find first available tunnel ID from 0..MAX_CSR_TUNNELS-1.""" + return get_next_available_id(session, IdentifierMap.csr_tunnel_id, + TUNNEL) + + +def get_next_available_ike_policy_id(session): + """Find first available IKE Policy ID from 1..MAX_CSR_IKE_POLICIES.""" + return get_next_available_id(session, IdentifierMap.csr_ike_policy_id, + IKE_POLICY) + + +def get_next_available_ipsec_policy_id(session): + """Find first available IPSec Policy ID from 1..MAX_CSR_IKE_POLICIES.""" + return get_next_available_id(session, IdentifierMap.csr_ipsec_policy_id, + IPSEC_POLICY) + + +def find_conn_with_policy(policy_field, policy_id, conn_id, session): + """Return ID of another conneciton (if any) that uses same policy ID.""" + qry = session.query(vpn_db.IPsecSiteConnection.id) + match = qry.filter(policy_field == policy_id, + vpn_db.IPsecSiteConnection.id != conn_id).first() + if match: + return match[0] + + +def find_connection_using_ike_policy(ike_policy_id, conn_id, session): + """Return ID of another connection that uses same IKE policy ID.""" + return find_conn_with_policy(vpn_db.IPsecSiteConnection.ikepolicy_id, + ike_policy_id, conn_id, session) + + +def find_connection_using_ipsec_policy(ipsec_policy_id, conn_id, session): + """Return ID of another connection that uses same IPSec policy ID.""" + return find_conn_with_policy(vpn_db.IPsecSiteConnection.ipsecpolicy_id, + ipsec_policy_id, conn_id, session) + + +def lookup_policy(policy_type, policy_field, conn_id, session): + """Obtain specified policy's mapping from other connection.""" + try: + return session.query(policy_field).filter_by( + ipsec_site_conn_id=conn_id).one()[0] + except sql_exc.NoResultFound: + msg = _("Database inconsistency between IPSec connection and " + "Cisco CSR mapping table (%s)") % policy_type + raise CsrInternalError(reason=msg) + + +def lookup_ike_policy_id_for(conn_id, session): + """Obtain existing Cisco CSR IKE policy ID from another connection.""" + return lookup_policy(IKE_POLICY, IdentifierMap.csr_ike_policy_id, + conn_id, session) + + +def lookup_ipsec_policy_id_for(conn_id, session): + """Obtain existing Cisco CSR IPSec policy ID from another connection.""" + return lookup_policy(IPSEC_POLICY, IdentifierMap.csr_ipsec_policy_id, + conn_id, session) + + +def determine_csr_policy_id(policy_type, conn_policy_field, map_policy_field, + policy_id, conn_id, session): + """Use existing or reserve a new policy ID for Cisco CSR use. + + TODO(pcm) FUTURE: Once device driver adds support for IKE/IPSec policy + ID sharing, add call to find_conn_with_policy() to find used ID and + then call lookup_policy() to find the current mapping for that ID. + """ + csr_id = get_next_available_id(session, map_policy_field, policy_type) + LOG.debug(_("Reserved new CSR ID %(csr_id)d for %(policy)s " + "ID %(policy_id)s"), {'csr_id': csr_id, + 'policy': policy_type, + 'policy_id': policy_id}) + return csr_id + + +def determine_csr_ike_policy_id(ike_policy_id, conn_id, session): + """Use existing, or reserve a new IKE policy ID for Cisco CSR.""" + return determine_csr_policy_id(IKE_POLICY, + vpn_db.IPsecSiteConnection.ikepolicy_id, + IdentifierMap.csr_ike_policy_id, + ike_policy_id, conn_id, session) + + +def determine_csr_ipsec_policy_id(ipsec_policy_id, conn_id, session): + """Use existing, or reserve a new IPSec policy ID for Cisco CSR.""" + return determine_csr_policy_id(IPSEC_POLICY, + vpn_db.IPsecSiteConnection.ipsecpolicy_id, + IdentifierMap.csr_ipsec_policy_id, + ipsec_policy_id, conn_id, session) + + +def get_tunnel_mapping_for(conn_id, session): + try: + entry = session.query(IdentifierMap).filter_by( + ipsec_site_conn_id=conn_id).one() + LOG.debug(_("Mappings for IPSec connection %(conn)s - " + "tunnel=%(tunnel)s ike_policy=%(csr_ike)d " + "ipsec_policy=%(csr_ipsec)d"), + {'conn': conn_id, 'tunnel': entry.csr_tunnel_id, + 'csr_ike': entry.csr_ike_policy_id, + 'csr_ipsec': entry.csr_ipsec_policy_id}) + return (entry.csr_tunnel_id, entry.csr_ike_policy_id, + entry.csr_ipsec_policy_id) + except sql_exc.NoResultFound: + msg = _("Existing entry for IPSec connection %s not found in Cisco " + "CSR mapping table") % conn_id + raise CsrInternalError(reason=msg) + + +def create_tunnel_mapping(context, conn_info): + """Create Cisco CSR IDs, using mapping table and OpenStack UUIDs.""" + conn_id = conn_info['id'] + ike_policy_id = conn_info['ikepolicy_id'] + ipsec_policy_id = conn_info['ipsecpolicy_id'] + tenant_id = conn_info['tenant_id'] + with context.session.begin(): + csr_tunnel_id = get_next_available_tunnel_id(context.session) + csr_ike_id = determine_csr_ike_policy_id(ike_policy_id, conn_id, + context.session) + csr_ipsec_id = determine_csr_ipsec_policy_id(ipsec_policy_id, conn_id, + context.session) + map_entry = IdentifierMap(tenant_id=tenant_id, + ipsec_site_conn_id=conn_id, + csr_tunnel_id=csr_tunnel_id, + csr_ike_policy_id=csr_ike_id, + csr_ipsec_policy_id=csr_ipsec_id) + try: + context.session.add(map_entry) + context.session.flush() + except db_exc.DBDuplicateEntry: + msg = _("Attempt to create duplicate entry in Cisco CSR " + "mapping table for connection %s") % conn_id + raise CsrInternalError(reason=msg) + LOG.info(_("Mapped connection %(conn_id)s to Tunnel%(tunnel_id)d " + "using IKE policy ID %(ike_id)d and IPSec policy " + "ID %(ipsec_id)d"), + {'conn_id': conn_id, 'tunnel_id': csr_tunnel_id, + 'ike_id': csr_ike_id, 'ipsec_id': csr_ipsec_id}) + + +def delete_tunnel_mapping(context, conn_info): + conn_id = conn_info['id'] + with context.session.begin(): + sess_qry = context.session.query(IdentifierMap) + sess_qry.filter_by(ipsec_site_conn_id=conn_id).delete() + LOG.info(_("Removed mapping for connection %s"), conn_id) diff --git a/neutron/services/vpn/service_drivers/cisco_ipsec.py b/neutron/services/vpn/service_drivers/cisco_ipsec.py new file mode 100644 index 000000000..625fea936 --- /dev/null +++ b/neutron/services/vpn/service_drivers/cisco_ipsec.py @@ -0,0 +1,247 @@ +# Copyright 2014 Cisco Systems, 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 netaddr import core as net_exc + +from neutron.common import exceptions +from neutron.common import rpc as n_rpc +from neutron.openstack.common import excutils +from neutron.openstack.common import log as logging +from neutron.openstack.common import rpc +from neutron.plugins.common import constants +from neutron.services.vpn.common import topics +from neutron.services.vpn import service_drivers +from neutron.services.vpn.service_drivers import cisco_csr_db as csr_id_map + + +LOG = logging.getLogger(__name__) + +IPSEC = 'ipsec' +BASE_IPSEC_VERSION = '1.0' +LIFETIME_LIMITS = {'IKE Policy': {'min': 60, 'max': 86400}, + 'IPSec Policy': {'min': 120, 'max': 2592000}} +MIN_CSR_MTU = 1500 +MAX_CSR_MTU = 9192 + + +class CsrValidationFailure(exceptions.BadRequest): + message = _("Cisco CSR does not support %(resource)s attribute %(key)s " + "with value '%(value)s'") + + +class CsrUnsupportedError(exceptions.NeutronException): + message = _("Cisco CSR does not currently support %(capability)s") + + +class CiscoCsrIPsecVpnDriverCallBack(object): + + """Handler for agent to plugin RPC messaging.""" + + # history + # 1.0 Initial version + + RPC_API_VERSION = BASE_IPSEC_VERSION + + def __init__(self, driver): + self.driver = driver + + def create_rpc_dispatcher(self): + return n_rpc.PluginRpcDispatcher([self]) + + def get_vpn_services_on_host(self, context, host=None): + """Retuns info on the vpnservices on the host.""" + plugin = self.driver.service_plugin + vpnservices = plugin._get_agent_hosting_vpn_services( + context, host) + return [self.driver._make_vpnservice_dict(vpnservice, context) + for vpnservice in vpnservices] + + def update_status(self, context, status): + """Update status of all vpnservices.""" + plugin = self.driver.service_plugin + plugin.update_status_by_agent(context, status) + + +class CiscoCsrIPsecVpnAgentApi(service_drivers.BaseIPsecVpnAgentApi): + + """API and handler for Cisco IPSec plugin to agent RPC messaging.""" + + RPC_API_VERSION = BASE_IPSEC_VERSION + + def __init__(self, topic, default_version): + super(CiscoCsrIPsecVpnAgentApi, self).__init__( + topics.CISCO_IPSEC_AGENT_TOPIC, topic, default_version) + + +class CiscoCsrIPsecVPNDriver(service_drivers.VpnDriver): + + """Cisco CSR VPN Service Driver class for IPsec.""" + + def __init__(self, service_plugin): + super(CiscoCsrIPsecVPNDriver, self).__init__(service_plugin) + self.callbacks = CiscoCsrIPsecVpnDriverCallBack(self) + self.conn = rpc.create_connection(new=True) + self.conn.create_consumer( + topics.CISCO_IPSEC_DRIVER_TOPIC, + self.callbacks.create_rpc_dispatcher(), + fanout=False) + self.conn.consume_in_thread() + self.agent_rpc = CiscoCsrIPsecVpnAgentApi( + topics.CISCO_IPSEC_AGENT_TOPIC, BASE_IPSEC_VERSION) + + @property + def service_type(self): + return IPSEC + + def validate_lifetime(self, for_policy, policy_info): + """Ensure lifetime in secs and value is supported, based on policy.""" + units = policy_info['lifetime']['units'] + if units != 'seconds': + raise CsrValidationFailure(resource=for_policy, + key='lifetime:units', + value=units) + value = policy_info['lifetime']['value'] + if (value < LIFETIME_LIMITS[for_policy]['min'] or + value > LIFETIME_LIMITS[for_policy]['max']): + raise CsrValidationFailure(resource=for_policy, + key='lifetime:value', + value=value) + + def validate_ike_version(self, policy_info): + """Ensure IKE policy is v1 for current REST API.""" + version = policy_info['ike_version'] + if version != 'v1': + raise CsrValidationFailure(resource='IKE Policy', + key='ike_version', + value=version) + + def validate_mtu(self, conn_info): + """Ensure the MTU value is supported.""" + mtu = conn_info['mtu'] + if mtu < MIN_CSR_MTU or mtu > MAX_CSR_MTU: + raise CsrValidationFailure(resource='IPSec Connection', + key='mtu', + value=mtu) + + def validate_public_ip_present(self, vpn_service): + """Ensure there is one gateway IP specified for the router used.""" + gw_port = vpn_service.router.gw_port + if not gw_port or len(gw_port.fixed_ips) != 1: + raise CsrValidationFailure(resource='IPSec Connection', + key='router:gw_port:ip_address', + value='missing') + + def validate_peer_id(self, ipsec_conn): + """Ensure that an IP address is specified for peer ID.""" + # TODO(pcm) Should we check peer_address too? + peer_id = ipsec_conn['peer_id'] + try: + netaddr.IPAddress(peer_id) + except net_exc.AddrFormatError: + raise CsrValidationFailure(resource='IPSec Connection', + key='peer_id', value=peer_id) + + def validate_ipsec_connection(self, context, ipsec_conn, vpn_service): + """Validate attributes w.r.t. Cisco CSR capabilities.""" + ike_policy = self.service_plugin.get_ikepolicy( + context, ipsec_conn['ikepolicy_id']) + ipsec_policy = self.service_plugin.get_ipsecpolicy( + context, ipsec_conn['ipsecpolicy_id']) + self.validate_lifetime('IKE Policy', ike_policy) + self.validate_lifetime('IPSec Policy', ipsec_policy) + self.validate_ike_version(ike_policy) + self.validate_mtu(ipsec_conn) + self.validate_public_ip_present(vpn_service) + self.validate_peer_id(ipsec_conn) + LOG.debug(_("IPSec connection %s validated for Cisco CSR"), + ipsec_conn['id']) + + def create_ipsec_site_connection(self, context, ipsec_site_connection): + vpnservice = self.service_plugin._get_vpnservice( + context, ipsec_site_connection['vpnservice_id']) + try: + self.validate_ipsec_connection(context, ipsec_site_connection, + vpnservice) + except CsrValidationFailure: + with excutils.save_and_reraise_exception(): + self.service_plugin.update_ipsec_site_conn_status( + context, ipsec_site_connection['id'], constants.ERROR) + csr_id_map.create_tunnel_mapping(context, ipsec_site_connection) + self.agent_rpc.vpnservice_updated(context, vpnservice['router_id']) + + def update_ipsec_site_connection( + self, context, old_ipsec_site_connection, ipsec_site_connection): + capability = _("update of IPSec connections. You can delete and " + "re-add, as a workaround.") + raise CsrUnsupportedError(capability=capability) + + def delete_ipsec_site_connection(self, context, ipsec_site_connection): + vpnservice = self.service_plugin._get_vpnservice( + context, ipsec_site_connection['vpnservice_id']) + self.agent_rpc.vpnservice_updated(context, vpnservice['router_id']) + + def create_ikepolicy(self, context, ikepolicy): + pass + + def delete_ikepolicy(self, context, ikepolicy): + pass + + def update_ikepolicy(self, context, old_ikepolicy, ikepolicy): + pass + + def create_ipsecpolicy(self, context, ipsecpolicy): + pass + + def delete_ipsecpolicy(self, context, ipsecpolicy): + pass + + def update_ipsecpolicy(self, context, old_ipsec_policy, ipsecpolicy): + pass + + def create_vpnservice(self, context, vpnservice): + pass + + def update_vpnservice(self, context, old_vpnservice, vpnservice): + self.agent_rpc.vpnservice_updated(context, vpnservice['router_id']) + + def delete_vpnservice(self, context, vpnservice): + self.agent_rpc.vpnservice_updated(context, vpnservice['router_id']) + + def get_cisco_connection_mappings(self, conn_id, context): + """Obtain persisted mappings for IDs related to connection.""" + tunnel_id, ike_id, ipsec_id = csr_id_map.get_tunnel_mapping_for( + conn_id, context.session) + return {'site_conn_id': u'Tunnel%d' % tunnel_id, + 'ike_policy_id': u'%d' % ike_id, + 'ipsec_policy_id': u'%s' % ipsec_id} + + def _make_vpnservice_dict(self, vpnservice, context): + """Collect all info on service, including Cisco info per IPSec conn.""" + vpnservice_dict = dict(vpnservice) + vpnservice_dict['ipsec_conns'] = [] + vpnservice_dict['subnet'] = dict( + vpnservice.subnet) + vpnservice_dict['external_ip'] = vpnservice.router.gw_port[ + 'fixed_ips'][0]['ip_address'] + for ipsec_conn in vpnservice.ipsec_site_connections: + ipsec_conn_dict = dict(ipsec_conn) + ipsec_conn_dict['ike_policy'] = dict(ipsec_conn.ikepolicy) + ipsec_conn_dict['ipsec_policy'] = dict(ipsec_conn.ipsecpolicy) + ipsec_conn_dict['peer_cidrs'] = [ + peer_cidr.cidr for peer_cidr in ipsec_conn.peer_cidrs] + ipsec_conn_dict['cisco'] = self.get_cisco_connection_mappings( + ipsec_conn['id'], context) + vpnservice_dict['ipsec_conns'].append(ipsec_conn_dict) + return vpnservice_dict diff --git a/neutron/services/vpn/service_drivers/ipsec.py b/neutron/services/vpn/service_drivers/ipsec.py index 1dc1657b2..552bda46f 100644 --- a/neutron/services/vpn/service_drivers/ipsec.py +++ b/neutron/services/vpn/service_drivers/ipsec.py @@ -17,11 +17,8 @@ import netaddr from neutron.common import rpc as n_rpc -from neutron import manager from neutron.openstack.common import log as logging from neutron.openstack.common import rpc -from neutron.openstack.common.rpc import proxy -from neutron.plugins.common import constants from neutron.services.vpn.common import topics from neutron.services.vpn import service_drivers @@ -60,50 +57,22 @@ class IPsecVpnDriverCallBack(object): plugin.update_status_by_agent(context, status) -class IPsecVpnAgentApi(proxy.RpcProxy): +class IPsecVpnAgentApi(service_drivers.BaseIPsecVpnAgentApi): """Agent RPC API for IPsecVPNAgent.""" RPC_API_VERSION = BASE_IPSEC_VERSION - def _agent_notification(self, context, method, router_id, - version=None): - """Notify update for the agent. - - This method will find where is the router, and - dispatch notification for the agent. - """ - adminContext = context.is_admin and context or context.elevated() - plugin = manager.NeutronManager.get_service_plugins().get( - constants.L3_ROUTER_NAT) - if not version: - version = self.RPC_API_VERSION - l3_agents = plugin.get_l3_agents_hosting_routers( - adminContext, [router_id], - admin_state_up=True, - active=True) - for l3_agent in l3_agents: - LOG.debug(_('Notify agent at %(topic)s.%(host)s the message ' - '%(method)s'), - {'topic': topics.IPSEC_AGENT_TOPIC, - 'host': l3_agent.host, - 'method': method}) - self.cast( - context, self.make_msg(method), - version=version, - topic='%s.%s' % (topics.IPSEC_AGENT_TOPIC, l3_agent.host)) - - def vpnservice_updated(self, context, router_id): - """Send update event of vpnservices.""" - method = 'vpnservice_updated' - self._agent_notification(context, method, router_id) + def __init__(self, topic, default_version): + super(IPsecVpnAgentApi, self).__init__( + topics.IPSEC_AGENT_TOPIC, topic, default_version) class IPsecVPNDriver(service_drivers.VpnDriver): """VPN Service Driver class for IPsec.""" def __init__(self, service_plugin): + super(IPsecVPNDriver, self).__init__(service_plugin) self.callbacks = IPsecVpnDriverCallBack(self) - self.service_plugin = service_plugin self.conn = rpc.create_connection(new=True) self.conn.create_consumer( topics.IPSEC_DRIVER_TOPIC, diff --git a/neutron/tests/unit/db/vpn/test_db_vpnaas.py b/neutron/tests/unit/db/vpn/test_db_vpnaas.py index 0c7493445..a32830683 100644 --- a/neutron/tests/unit/db/vpn/test_db_vpnaas.py +++ b/neutron/tests/unit/db/vpn/test_db_vpnaas.py @@ -20,6 +20,7 @@ import contextlib import os +from oslo.config import cfg import webob.exc from neutron.api.extensions import ExtensionMiddleware @@ -28,6 +29,7 @@ from neutron.common import config from neutron import context from neutron.db import agentschedulers_db from neutron.db import l3_agentschedulers_db +from neutron.db import servicetype_db as sdb from neutron.db.vpn import vpn_db from neutron import extensions from neutron.extensions import vpnaas @@ -417,7 +419,20 @@ class VPNTestMixin(object): class VPNPluginDbTestCase(VPNTestMixin, test_l3_plugin.L3NatTestCaseMixin, test_db_plugin.NeutronDbPluginV2TestCase): - def setUp(self, core_plugin=None, vpnaas_plugin=DB_VPN_PLUGIN_KLASS): + def setUp(self, core_plugin=None, vpnaas_plugin=DB_VPN_PLUGIN_KLASS, + vpnaas_provider=None): + if not vpnaas_provider: + vpnaas_provider = ( + constants.VPN + + ':vpnaas:neutron.services.vpn.' + 'service_drivers.ipsec.IPsecVPNDriver:default') + + cfg.CONF.set_override('service_provider', + [vpnaas_provider], + 'service_providers') + # force service type manager to reload configuration: + sdb.ServiceTypeManager._instance = None + service_plugins = {'vpnaas_plugin': vpnaas_plugin} plugin_str = ('neutron.tests.unit.db.vpn.' 'test_db_vpnaas.TestVpnCorePlugin') diff --git a/neutron/tests/unit/services/vpn/service_drivers/test_cisco_ipsec.py b/neutron/tests/unit/services/vpn/service_drivers/test_cisco_ipsec.py new file mode 100644 index 000000000..de0dfb7b7 --- /dev/null +++ b/neutron/tests/unit/services/vpn/service_drivers/test_cisco_ipsec.py @@ -0,0 +1,366 @@ +# Copyright 2014 Cisco Systems, 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 import context as n_ctx +from neutron.db import api as dbapi +from neutron.openstack.common import uuidutils +from neutron.plugins.common import constants +from neutron.services.vpn.service_drivers import cisco_csr_db as csr_db +from neutron.services.vpn.service_drivers import cisco_ipsec as ipsec_driver +from neutron.tests import base + +_uuid = uuidutils.generate_uuid + +FAKE_VPN_CONN_ID = _uuid() + +FAKE_VPN_CONNECTION = { + 'vpnservice_id': _uuid(), + 'id': FAKE_VPN_CONN_ID, + 'ikepolicy_id': _uuid(), + 'ipsecpolicy_id': _uuid(), + 'tenant_id': _uuid() +} +FAKE_VPN_SERVICE = { + 'router_id': _uuid() +} +FAKE_HOST = 'fake_host' + + +class TestCiscoIPsecDriverValidation(base.BaseTestCase): + + def setUp(self): + super(TestCiscoIPsecDriverValidation, self).setUp() + self.addCleanup(mock.patch.stopall) + mock.patch('neutron.openstack.common.rpc.create_connection').start() + self.service_plugin = mock.Mock() + self.driver = ipsec_driver.CiscoCsrIPsecVPNDriver(self.service_plugin) + self.context = n_ctx.Context('some_user', 'some_tenant') + self.vpn_service = mock.Mock() + + def test_ike_version_unsupported(self): + """Failure test that Cisco CSR REST API does not support IKE v2.""" + policy_info = {'ike_version': 'v2', + 'lifetime': {'units': 'seconds', 'value': 60}} + self.assertRaises(ipsec_driver.CsrValidationFailure, + self.driver.validate_ike_version, policy_info) + + def test_ike_lifetime_not_in_seconds(self): + """Failure test of unsupported lifetime units for IKE policy.""" + policy_info = {'lifetime': {'units': 'kilobytes', 'value': 1000}} + self.assertRaises(ipsec_driver.CsrValidationFailure, + self.driver.validate_lifetime, + "IKE Policy", policy_info) + + def test_ipsec_lifetime_not_in_seconds(self): + """Failure test of unsupported lifetime units for IPSec policy.""" + policy_info = {'lifetime': {'units': 'kilobytes', 'value': 1000}} + self.assertRaises(ipsec_driver.CsrValidationFailure, + self.driver.validate_lifetime, + "IPSec Policy", policy_info) + + def test_ike_lifetime_seconds_values_at_limits(self): + """Test valid lifetime values for IKE policy.""" + policy_info = {'lifetime': {'units': 'seconds', 'value': 60}} + self.driver.validate_lifetime('IKE Policy', policy_info) + policy_info = {'lifetime': {'units': 'seconds', 'value': 86400}} + self.driver.validate_lifetime('IKE Policy', policy_info) + + def test_ipsec_lifetime_seconds_values_at_limits(self): + """Test valid lifetime values for IPSec policy.""" + policy_info = {'lifetime': {'units': 'seconds', 'value': 120}} + self.driver.validate_lifetime('IPSec Policy', policy_info) + policy_info = {'lifetime': {'units': 'seconds', 'value': 2592000}} + self.driver.validate_lifetime('IPSec Policy', policy_info) + + def test_ike_lifetime_values_invalid(self): + """Failure test of unsupported lifetime values for IKE policy.""" + which = "IKE Policy" + policy_info = {'lifetime': {'units': 'seconds', 'value': 59}} + self.assertRaises(ipsec_driver.CsrValidationFailure, + self.driver.validate_lifetime, + which, policy_info) + policy_info = {'lifetime': {'units': 'seconds', 'value': 86401}} + self.assertRaises(ipsec_driver.CsrValidationFailure, + self.driver.validate_lifetime, + which, policy_info) + + def test_ipsec_lifetime_values_invalid(self): + """Failure test of unsupported lifetime values for IPSec policy.""" + which = "IPSec Policy" + policy_info = {'lifetime': {'units': 'seconds', 'value': 119}} + self.assertRaises(ipsec_driver.CsrValidationFailure, + self.driver.validate_lifetime, + which, policy_info) + policy_info = {'lifetime': {'units': 'seconds', 'value': 2592001}} + self.assertRaises(ipsec_driver.CsrValidationFailure, + self.driver.validate_lifetime, + which, policy_info) + + def test_ipsec_connection_with_mtu_at_limits(self): + """Test IPSec site-to-site connection with MTU at limits.""" + conn_info = {'mtu': 1500} + self.driver.validate_mtu(conn_info) + conn_info = {'mtu': 9192} + self.driver.validate_mtu(conn_info) + + def test_ipsec_connection_with_invalid_mtu(self): + """Failure test of IPSec site connection with unsupported MTUs.""" + conn_info = {'mtu': 1499} + self.assertRaises(ipsec_driver.CsrValidationFailure, + self.driver.validate_mtu, conn_info) + conn_info = {'mtu': 9193} + self.assertRaises(ipsec_driver.CsrValidationFailure, + self.driver.validate_mtu, conn_info) + + def simulate_gw_ip_available(self): + """Helper function indicating that tunnel has a gateway IP.""" + def have_one(): + return 1 + self.vpn_service.router.gw_port.fixed_ips.__len__ = have_one + ip_addr_mock = mock.Mock() + self.vpn_service.router.gw_port.fixed_ips = [ip_addr_mock] + return ip_addr_mock + + def test_have_public_ip_for_router(self): + """Ensure that router for IPSec connection has gateway IP.""" + self.simulate_gw_ip_available() + self.driver.validate_public_ip_present(self.vpn_service) + + def test_router_with_missing_gateway_ip(self): + """Failure test of IPSec connection with missing gateway IP.""" + self.simulate_gw_ip_available() + self.vpn_service.router.gw_port = None + self.assertRaises(ipsec_driver.CsrValidationFailure, + self.driver.validate_public_ip_present, + self.vpn_service) + + def test_peer_id_is_an_ip_address(self): + """Ensure peer ID is an IP address for IPsec connection create.""" + ipsec_conn = {'peer_id': '10.10.10.10'} + self.driver.validate_peer_id(ipsec_conn) + + def test_peer_id_is_not_ip_address(self): + """Failure test of peer_id that is not an IP address.""" + ipsec_conn = {'peer_id': 'some-site.com'} + self.assertRaises(ipsec_driver.CsrValidationFailure, + self.driver.validate_peer_id, ipsec_conn) + + def test_validation_for_create_ipsec_connection(self): + """Ensure all validation passes for IPSec site connection create.""" + self.simulate_gw_ip_available() + # Provide the minimum needed items to validate + ipsec_conn = {'id': '1', + 'ikepolicy_id': '123', + 'ipsecpolicy_id': '2', + 'mtu': 1500, + 'peer_id': '10.10.10.10'} + self.service_plugin.get_ikepolicy = mock.Mock( + return_value={'ike_version': 'v1', + 'lifetime': {'units': 'seconds', 'value': 60}}) + self.service_plugin.get_ipsecpolicy = mock.Mock( + return_value={'lifetime': {'units': 'seconds', 'value': 120}}) + self.driver.validate_ipsec_connection(self.context, ipsec_conn, + self.vpn_service) + + +class TestCiscoIPsecDriverMapping(base.BaseTestCase): + + def setUp(self): + super(TestCiscoIPsecDriverMapping, self).setUp() + self.addCleanup(mock.patch.stopall) + self.context = mock.patch.object(n_ctx, 'Context').start() + self.session = self.context.session + self.query_mock = self.session.query.return_value.order_by + + def test_identifying_first_mapping_id(self): + """Make sure first available ID is obtained for each ID type.""" + # Simulate mapping table is empty - get first one + self.query_mock.return_value = [] + next_id = csr_db.get_next_available_tunnel_id(self.session) + self.assertEqual(0, next_id) + + next_id = csr_db.get_next_available_ike_policy_id(self.session) + self.assertEqual(1, next_id) + + next_id = csr_db.get_next_available_ipsec_policy_id(self.session) + self.assertEqual(1, next_id) + + def test_last_mapping_id_available(self): + """Make sure can get the last ID for each of the table types.""" + # Simulate query indicates table is full + self.query_mock.return_value = [ + (x, ) for x in xrange(csr_db.MAX_CSR_TUNNELS - 1)] + next_id = csr_db.get_next_available_tunnel_id(self.session) + self.assertEqual(csr_db.MAX_CSR_TUNNELS - 1, next_id) + + self.query_mock.return_value = [ + (x, ) for x in xrange(1, csr_db.MAX_CSR_IKE_POLICIES)] + next_id = csr_db.get_next_available_ike_policy_id(self.session) + self.assertEqual(csr_db.MAX_CSR_IKE_POLICIES, next_id) + + self.query_mock.return_value = [ + (x, ) for x in xrange(1, csr_db.MAX_CSR_IPSEC_POLICIES)] + next_id = csr_db.get_next_available_ipsec_policy_id(self.session) + self.assertEqual(csr_db.MAX_CSR_IPSEC_POLICIES, next_id) + + def test_reusing_first_available_mapping_id(self): + """Ensure that we reuse the first available ID. + + Make sure that the next lowest ID is obtained from the mapping + table when there are "holes" from deletions. Database query sorts + the entries, so will return them in order. Using tunnel ID, as the + logic is the same for each ID type. + """ + self.query_mock.return_value = [(0, ), (1, ), (2, ), (5, ), (6, )] + next_id = csr_db.get_next_available_tunnel_id(self.session) + self.assertEqual(3, next_id) + + def test_no_more_mapping_ids_available(self): + """Failure test of trying to reserve ID, when none available.""" + self.query_mock.return_value = [ + (x, ) for x in xrange(csr_db.MAX_CSR_TUNNELS)] + self.assertRaises(IndexError, csr_db.get_next_available_tunnel_id, + self.session) + + self.query_mock.return_value = [ + (x, ) for x in xrange(1, csr_db.MAX_CSR_IKE_POLICIES + 1)] + self.assertRaises(IndexError, csr_db.get_next_available_ike_policy_id, + self.session) + + self.query_mock.return_value = [ + (x, ) for x in xrange(1, csr_db.MAX_CSR_IPSEC_POLICIES + 1)] + self.assertRaises(IndexError, + csr_db.get_next_available_ipsec_policy_id, + self.session) + + def test_create_tunnel_mappings(self): + """Ensure successfully create new tunnel mappings.""" + # Simulate that first IDs are obtained + self.query_mock.return_value = [] + map_db_mock = mock.patch.object(csr_db, 'IdentifierMap').start() + conn_info = {'ikepolicy_id': '10', + 'ipsecpolicy_id': '50', + 'id': '100', + 'tenant_id': '1000'} + csr_db.create_tunnel_mapping(self.context, conn_info) + map_db_mock.assert_called_once_with(csr_tunnel_id=0, + csr_ike_policy_id=1, + csr_ipsec_policy_id=1, + ipsec_site_conn_id='100', + tenant_id='1000') + # Create another, with next ID of 2 for all IDs (not mocking each + # ID separately, so will not have different IDs). + self.query_mock.return_value = [(0, ), (1, )] + map_db_mock.reset_mock() + conn_info = {'ikepolicy_id': '20', + 'ipsecpolicy_id': '60', + 'id': '101', + 'tenant_id': '1000'} + csr_db.create_tunnel_mapping(self.context, conn_info) + map_db_mock.assert_called_once_with(csr_tunnel_id=2, + csr_ike_policy_id=2, + csr_ipsec_policy_id=2, + ipsec_site_conn_id='101', + tenant_id='1000') + + +class TestCiscoIPsecDriver(base.BaseTestCase): + + """Test that various incoming requests are sent to device driver.""" + + def setUp(self): + super(TestCiscoIPsecDriver, self).setUp() + self.addCleanup(mock.patch.stopall) + dbapi.configure_db() + self.addCleanup(dbapi.clear_db) + mock.patch('neutron.openstack.common.rpc.create_connection').start() + + l3_agent = mock.Mock() + l3_agent.host = FAKE_HOST + plugin = mock.Mock() + plugin.get_l3_agents_hosting_routers.return_value = [l3_agent] + plugin_p = mock.patch('neutron.manager.NeutronManager.get_plugin') + get_plugin = plugin_p.start() + get_plugin.return_value = plugin + service_plugin_p = mock.patch( + 'neutron.manager.NeutronManager.get_service_plugins') + get_service_plugin = service_plugin_p.start() + get_service_plugin.return_value = {constants.L3_ROUTER_NAT: plugin} + + service_plugin = mock.Mock() + service_plugin.get_l3_agents_hosting_routers.return_value = [l3_agent] + service_plugin._get_vpnservice.return_value = { + 'router_id': _uuid() + } + self.db_update_mock = service_plugin.update_ipsec_site_conn_status + self.driver = ipsec_driver.CiscoCsrIPsecVPNDriver(service_plugin) + self.driver.validate_ipsec_connection = mock.Mock() + mock.patch.object(csr_db, 'create_tunnel_mapping').start() + self.context = n_ctx.Context('some_user', 'some_tenant') + + def _test_update(self, func, args): + with mock.patch.object(self.driver.agent_rpc, 'cast') as cast: + func(self.context, *args) + cast.assert_called_once_with( + self.context, + {'args': {}, + 'namespace': None, + 'method': 'vpnservice_updated'}, + version='1.0', + topic='cisco_csr_ipsec_agent.fake_host') + + def test_create_ipsec_site_connection(self): + self._test_update(self.driver.create_ipsec_site_connection, + [FAKE_VPN_CONNECTION]) + + def test_failure_validation_ipsec_connection(self): + """Failure test of validation during IPSec site connection create. + + Simulate a validation failure, and ensure that database is + updated to indicate connection is in error state. + + TODO(pcm): FUTURE - remove test case, once vendor plugin + validation is done before database commit. + """ + self.driver.validate_ipsec_connection.side_effect = ( + ipsec_driver.CsrValidationFailure(resource='IPSec Connection', + key='mtu', value=1000)) + self.assertRaises(ipsec_driver.CsrValidationFailure, + self.driver.create_ipsec_site_connection, + self.context, FAKE_VPN_CONNECTION) + self.db_update_mock.assert_called_with(self.context, + FAKE_VPN_CONN_ID, + constants.ERROR) + + def test_update_ipsec_site_connection(self): + # TODO(pcm) FUTURE - Update test, when supported + self.assertRaises(ipsec_driver.CsrUnsupportedError, + self._test_update, + self.driver.update_ipsec_site_connection, + [FAKE_VPN_CONNECTION, FAKE_VPN_CONNECTION]) + + def test_delete_ipsec_site_connection(self): + self._test_update(self.driver.delete_ipsec_site_connection, + [FAKE_VPN_CONNECTION]) + + def test_update_vpnservice(self): + self._test_update(self.driver.update_vpnservice, + [FAKE_VPN_SERVICE, FAKE_VPN_SERVICE]) + + def test_delete_vpnservice(self): + self._test_update(self.driver.delete_vpnservice, + [FAKE_VPN_SERVICE])