VPNaaS Service Driver for Cisco CSR

This has the service driver part of the vendor specific VPNaaS plugin.
This version DOES NOT rely on the Service Type Framework code, which is
presently under review (client 53602, server 41827) and on hold due to
discussion over flavors. As a result, this changeset has modifications
so that the service driver is not hard-coded in the VPN plugin.

The device driver will be under a separate review and has the REST
client that talks to the Cisco CSR (running out-of-band).

Note: See review 74156 for more details on device driver portion of
      this blueprint.

Change-Id: I39b1475c992b594256f5a28be0caa1ee9398050e
Partially-implements: blueprint vpnaas-cisco-driver
This commit is contained in:
Paul Michali 2014-03-04 01:08:50 +00:00 committed by Mark McClain
parent bc8a2dcf76
commit fd8e37e221
12 changed files with 1027 additions and 57 deletions

View File

@ -418,9 +418,12 @@ signing_dir = $state_path/keystone-signing
# service_provider=FIREWALL:name2:firewall_driver_path
# --- Reference implementations ---
service_provider=LOADBALANCER:Haproxy:neutron.services.loadbalancer.drivers.haproxy.plugin_driver.HaproxyOnHostPluginDriver:default
service_provider=VPN:openswan:neutron.services.vpn.service_drivers.ipsec.IPsecVPNDriver:default
# In order to activate Radware's lbaas driver you need to uncomment the next line.
# If you want to keep the HA Proxy as the default lbaas driver, remove the attribute default from the line below.
# Otherwise comment the HA Proxy line
#service_provider = LOADBALANCER:Radware:neutron.services.loadbalancer.drivers.radware.driver.LoadBalancerDriver:default
#uncomment the following line to make the 'netscaler' LBaaS provider available.
#service_provider=LOADBALANCER:NetScaler:neutron.services.loadbalancer.drivers.netscaler.netscaler_driver.NetScalerPluginDriver
# service_provider = LOADBALANCER:Radware:neutron.services.loadbalancer.drivers.radware.driver.LoadBalancerDriver:default
# uncomment the following line to make the 'netscaler' LBaaS provider available.
# service_provider=LOADBALANCER:NetScaler:neutron.services.loadbalancer.drivers.netscaler.netscaler_driver.NetScalerPluginDriver
# Uncomment the following line (and comment out the OpenSwan VPN line) to enable Cisco's VPN driver.
# service_provider=VPN:cisco:neutron.services.vpn.service_drivers.cisco_ipsec.CiscoCsrIPsecVPNDriver:default

View File

@ -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

View File

@ -0,0 +1,60 @@
# 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.
"""Cisco CSR VPNaaS
Revision ID: 24c7ea5160d7
Revises: 492a106273f8
Create Date: 2014-02-03 13:06:50.407601
"""
# revision identifiers, used by Alembic.
revision = '24c7ea5160d7'
down_revision = '492a106273f8'
# Change to ['*'] if this migration applies to all plugins
migration_for_plugins = [
'neutron.services.vpn.plugin.VPNDriverPlugin',
]
from alembic import op
import sqlalchemy as sa
from neutron.db import migration
def upgrade(active_plugins=None, options=None):
if not migration.should_run(active_plugins, migration_for_plugins):
return
op.create_table(
'cisco_csr_identifier_map',
sa.Column('tenant_id', sa.String(length=255), nullable=True),
sa.Column('ipsec_site_conn_id', sa.String(length=64),
primary_key=True),
sa.Column('csr_tunnel_id', sa.Integer(), nullable=False),
sa.Column('csr_ike_policy_id', sa.Integer(), nullable=False),
sa.Column('csr_ipsec_policy_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['ipsec_site_conn_id'],
['ipsec_site_connections.id'],
ondelete='CASCADE')
)
def downgrade(active_plugins=None, options=None):
if not migration.should_run(active_plugins, migration_for_plugins):
return
op.drop_table('cisco_csr_identifier_map')

View File

@ -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'])

View File

@ -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'

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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')

View File

@ -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])