VPNaaS: Multiple Local Subnets feature

Implements support for multiple local subnets for IPSec site to site
connections, using the new Endpoint Group API.

The implementation supports backwards compatibility as follows. If
a VPN service is created with a subnet, then the older API is assumed
and the user must specify the peer CIDRs for any IPSec connections
and cannot specify multiple local subnets.

If a subnet is not provided for the VPN service, then the user must
use the newer API and provide a local and peer endpoint group IDs for
the IPSec connection (and cannot specify the peer CIDRs in the IPSec
connection API).

Implication here is that the subnet will be an optional argument for
the VPN service API.

With this feature, when an endpoint group is deleted, a check is made
to ensure that there are no IPSec connections using the group.

Migration will move the subnet from VPN service API to a new endpoint
group, and specify the group in any connections using the service.
The peer CIDR(s) will be moved from each connection to an endpoint
group, with the group ID specified in the connection.

Note: As part of testing the database methods for this feature, several
more tests were created to test database access only, instead of doing
a round trip test. In a separate commit, these tests can be expanded,
and the existing round trip tests removed.

Note: Tests for building the dict used in sync requests was enhanced,
as part of supporting this feature. The previous tests didn't check
that the peer CIDR information was correct, so were enhanced as well.

Note: The service driver passes the local CIDR(s) in a new field,
called local_cidrs. This field is used for the older API, as well,
passing the subnet's CIDR, and allowing consistent consumption by the
device driver. The IP version is also passed, rather than obtaining
it from the subnet info (so both new and old API use the same fields).

Note: to support rolling upgrades, where an agent may be using the older
release, after the server has been updated, the subnet CIDR field passed
from the service driver to device driver, will be populated from the
first local endpoint from the first connection (there has to be at least
one connection, when sending data to the agent).

Note: In the device driver, I noticed that the local CIDR's IP version
can change the config file output, so I added test cases for IPv6, as
part of enhancing the tests for multiple local CIDRs.

Change-Id: I7a011e3170d7db463a6561e550b2ead3e3311125
Partial-Bug: 1459423
This commit is contained in:
Paul Michali 2015-09-28 20:00:58 +00:00
parent efde2a2dcc
commit 7ba17a3155
16 changed files with 2027 additions and 593 deletions

View File

@ -0,0 +1,167 @@
# (c) Copyright 2015 Cisco Systems Inc.
#
# 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.
#
"""Multiple local subnets
Revision ID: 2cb4ee992b41
Revises: 2c82e782d734
Create Date: 2015-09-09 20:32:54.254267
"""
# revision identifiers, used by Alembic.
revision = '2cb4ee992b41'
down_revision = '2c82e782d734'
depends_on = ('28ee739a7e4b',)
from alembic import op
from oslo_utils import uuidutils
import sqlalchemy as sa
from sqlalchemy.sql import expression as sa_expr
from neutron.api.v2 import attributes as attr
from neutron_vpnaas.services.vpn.common import constants as v_constants
vpnservices = sa.Table(
'vpnservices', sa.MetaData(),
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('tenant_id', sa.String(length=255), nullable=False),
sa.Column('name', sa.String(attr.NAME_MAX_LEN)),
sa.Column('description', sa.String(attr.DESCRIPTION_MAX_LEN)),
sa.Column('status', sa.String(16), nullable=False),
sa.Column('admin_state_up', sa.Boolean(), nullable=False),
sa.Column('external_v4_ip', sa.String(16)),
sa.Column('external_v6_ip', sa.String(64)),
sa.Column('subnet_id', sa.String(36)),
sa.Column('router_id', sa.String(36), nullable=False))
ipsec_site_conns = sa.Table(
'ipsec_site_connections', sa.MetaData(),
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('tenant_id', sa.String(length=255), nullable=False),
sa.Column('name', sa.String(attr.NAME_MAX_LEN)),
sa.Column('description', sa.String(attr.DESCRIPTION_MAX_LEN)),
sa.Column('peer_address', sa.String(255), nullable=False),
sa.Column('peer_id', sa.String(255), nullable=False),
sa.Column('route_mode', sa.String(8), nullable=False),
sa.Column('mtu', sa.Integer, nullable=False),
sa.Column('initiator', sa.Enum("bi-directional", "response-only",
name="vpn_initiators"), nullable=False),
sa.Column('auth_mode', sa.String(16), nullable=False),
sa.Column('psk', sa.String(255), nullable=False),
sa.Column('dpd_action', sa.Enum("hold", "clear", "restart", "disabled",
"restart-by-peer", name="vpn_dpd_actions"),
nullable=False),
sa.Column('dpd_interval', sa.Integer, nullable=False),
sa.Column('dpd_timeout', sa.Integer, nullable=False),
sa.Column('status', sa.String(16), nullable=False),
sa.Column('admin_state_up', sa.Boolean(), nullable=False),
sa.Column('vpnservice_id', sa.String(36), nullable=False),
sa.Column('ipsecpolicy_id', sa.String(36), nullable=False),
sa.Column('ikepolicy_id', sa.String(36), nullable=False),
sa.Column('local_ep_group_id', sa.String(36)),
sa.Column('peer_ep_group_id', sa.String(36)))
ipsecpeercidrs = sa.Table(
'ipsecpeercidrs', sa.MetaData(),
sa.Column('cidr', sa.String(32), nullable=False, primary_key=True),
sa.Column('ipsec_site_connection_id', sa.String(36), primary_key=True))
def _make_endpoint_groups(new_groups, new_endpoints):
"""Create endpoint groups and their corresponding endpoints."""
md = sa.MetaData()
engine = op.get_bind()
sa.Table('vpn_endpoint_groups', md, autoload=True, autoload_with=engine)
op.bulk_insert(md.tables['vpn_endpoint_groups'], new_groups)
sa.Table('vpn_endpoints', md, autoload=True, autoload_with=engine)
op.bulk_insert(md.tables['vpn_endpoints'], new_endpoints)
def _update_connections(connection_map):
"""Store the endpoint group IDs in the connections."""
for conn_id, mapping in connection_map.items():
stmt = ipsec_site_conns.update().where(
ipsec_site_conns.c.id == conn_id).values(
local_ep_group_id=mapping['local'],
peer_ep_group_id=mapping['peer'])
op.execute(stmt)
def upgrade():
new_groups = []
new_endpoints = []
service_map = {}
session = sa.orm.Session(bind=op.get_bind())
vpn_services = session.query(vpnservices).filter(
vpnservices.c.subnet_id is not None).all()
for vpn_service in vpn_services:
subnet_id = vpn_service.subnet_id
if subnet_id is None:
continue # Skip new service entries
# Define the subnet group
group_id = uuidutils.generate_uuid()
group = {'id': group_id,
'name': '',
'description': '',
'tenant_id': vpn_service.tenant_id,
'endpoint_type': v_constants.SUBNET_ENDPOINT}
new_groups.append(group)
# Define the (sole) endpoint
endpoint = {'endpoint_group_id': group_id,
'endpoint': subnet_id}
new_endpoints.append(endpoint)
# Save info to use for connections
service_map[vpn_service.id] = group_id
connection_map = {}
ipsec_conns = session.query(ipsec_site_conns).all()
for connection in ipsec_conns:
peer_cidrs = session.query(ipsecpeercidrs.c.cidr).filter(
ipsecpeercidrs.c.ipsec_site_connection_id == connection.id).all()
if not peer_cidrs:
continue # Skip new style connections
# Define the CIDR group
group_id = uuidutils.generate_uuid()
group = {'id': group_id,
'name': '',
'description': '',
'tenant_id': connection.tenant_id,
'endpoint_type': v_constants.CIDR_ENDPOINT}
new_groups.append(group)
# Define the endpoint(s)
for peer_cidr in peer_cidrs:
endpoint = {'endpoint_group_id': group_id,
'endpoint': peer_cidr[0]}
new_endpoints.append(endpoint)
# Save the endpoint group ID info for the connection
vpn_service = connection.vpnservice_id
connection_map[connection.id] = {'local': service_map[vpn_service],
'peer': group_id}
# Create all the defined endpoint groups and their endpoints
_make_endpoint_groups(new_groups, new_endpoints)
# Refer to new groups, in the IPSec connections
_update_connections(connection_map)
# Remove the peer_cidrs from IPSec connections
op.execute(sa_expr.table('ipsecpeercidrs').delete())
# Remove the subnets from VPN services
stmt = vpnservices.update().where(
vpnservices.c.subnet_id is not None).values(
subnet_id=None)
op.execute(stmt)
session.commit()

View File

@ -0,0 +1,52 @@
# (c) Copyright 2015 Cisco Systems Inc.
#
# 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.
#
"""Multiple local subnets
Revision ID: 28ee739a7e4b
Revises: 41b509d10b5e
Create Date: 2015-09-09 20:32:54.231765
"""
# revision identifiers, used by Alembic.
revision = '28ee739a7e4b'
down_revision = '41b509d10b5e'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('ipsec_site_connections',
sa.Column('local_ep_group_id',
sa.String(length=36),
nullable=True))
op.add_column('ipsec_site_connections',
sa.Column('peer_ep_group_id',
sa.String(length=36),
nullable=True))
op.create_foreign_key(constraint_name=None,
source_table='ipsec_site_connections',
referent_table='vpn_endpoint_groups',
local_cols=['local_ep_group_id'],
remote_cols=['id'])
op.create_foreign_key(constraint_name=None,
source_table='ipsec_site_connections',
referent_table='vpn_endpoint_groups',
local_cols=['peer_ep_group_id'],
remote_cols=['id'])
op.alter_column('vpnservices', 'subnet_id',
existing_type=sa.String(length=36), nullable=True)

View File

@ -1,4 +1,5 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
# (c) Copyright 2015 Cisco Systems Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -13,27 +14,28 @@
# License for the specific language governing permissions and limitations
# under the License.
import netaddr
from neutron.callbacks import events
from neutron.callbacks import registry
from neutron.callbacks import resources
from neutron.common import constants as n_constants
from neutron.db import common_db_mixin as base_db
from neutron.db import l3_agentschedulers_db as l3_agent_db
from neutron.db import models_v2
from neutron.extensions import l3 as l3_exception
from neutron.i18n import _LW
from neutron import manager
from neutron.plugins.common import constants
from neutron.plugins.common import constants as p_constants
from neutron.plugins.common import utils
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import uuidutils
import sqlalchemy as sa
from sqlalchemy.orm import exc
from neutron_vpnaas.db.vpn import vpn_models
from neutron_vpnaas.db.vpn import vpn_validator
from neutron_vpnaas.extensions import vpnaas
from neutron_vpnaas.services.vpn.common import constants as v_constants
LOG = logging.getLogger(__name__)
@ -108,16 +110,39 @@ class VPNPluginDb(vpnaas.VPNPluginBase, base_db.CommonDbMixin):
'ikepolicy_id': ipsec_site_conn['ikepolicy_id'],
'ipsecpolicy_id': ipsec_site_conn['ipsecpolicy_id'],
'peer_cidrs': [pcidr['cidr']
for pcidr in ipsec_site_conn['peer_cidrs']]
for pcidr in ipsec_site_conn['peer_cidrs']],
'local_ep_group_id': ipsec_site_conn['local_ep_group_id'],
'peer_ep_group_id': ipsec_site_conn['peer_ep_group_id'],
}
return self._fields(res, fields)
def _get_subnet_ip_version(self, context, vpnservice_id):
vpn_service_db = self._get_vpnservice(context, vpnservice_id)
subnet = vpn_service_db.subnet['cidr']
ip_version = netaddr.IPNetwork(subnet).version
return ip_version
def get_endpoint_info(self, context, ipsec_sitecon):
"""Obtain all endpoint info, and store in connection for validation."""
ipsec_sitecon['local_epg_subnets'] = self.get_endpoint_group(
context, ipsec_sitecon['local_ep_group_id'])
ipsec_sitecon['peer_epg_cidrs'] = self.get_endpoint_group(
context, ipsec_sitecon['peer_ep_group_id'])
def validate_connection_info(self, context, validator, ipsec_sitecon,
vpnservice):
"""Collect info and validate connection.
If endpoint groups used (default), collect the group info and
do not specify the IP version (as it will come from endpoints).
Otherwise, get the IP version from the (legacy) subnet for
validation purposes.
NOTE: Once the deprecated subnet is removed, the caller can just
call get_endpoint_info() and validate_ipsec_site_connection().
"""
if ipsec_sitecon['local_ep_group_id']:
self.get_endpoint_info(context, ipsec_sitecon)
ip_version = None
else:
ip_version = vpnservice.subnet.ip_version
validator.validate_ipsec_site_connection(context, ipsec_sitecon,
ip_version, vpnservice)
def create_ipsec_site_connection(self, context, ipsec_site_connection):
ipsec_sitecon = ipsec_site_connection['ipsec_site_connection']
@ -126,19 +151,19 @@ class VPNPluginDb(vpnaas.VPNPluginBase, base_db.CommonDbMixin):
tenant_id = self._get_tenant_id_for_create(context, ipsec_sitecon)
with context.session.begin(subtransactions=True):
#Check permissions
self._get_resource(context, vpn_models.VPNService,
ipsec_sitecon['vpnservice_id'])
vpnservice_id = ipsec_sitecon['vpnservice_id']
self._get_resource(context, vpn_models.VPNService, vpnservice_id)
self._get_resource(context, vpn_models.IKEPolicy,
ipsec_sitecon['ikepolicy_id'])
self._get_resource(context, vpn_models.IPsecPolicy,
ipsec_sitecon['ipsecpolicy_id'])
vpnservice_id = ipsec_sitecon['vpnservice_id']
ip_version = self._get_subnet_ip_version(context, vpnservice_id)
validator.validate_ipsec_site_connection(context,
ipsec_sitecon,
ip_version)
vpnservice = self._get_vpnservice(context, vpnservice_id)
validator.validate_ipsec_conn_optional_args(ipsec_sitecon,
vpnservice.subnet)
self.validate_connection_info(context, validator, ipsec_sitecon,
vpnservice)
validator.resolve_peer_address(ipsec_sitecon, vpnservice.router)
ipsec_site_conn_db = vpn_models.IPsecSiteConnection(
id=uuidutils.generate_uuid(),
tenant_id=tenant_id,
@ -155,10 +180,12 @@ class VPNPluginDb(vpnaas.VPNPluginBase, base_db.CommonDbMixin):
dpd_interval=ipsec_sitecon['dpd_interval'],
dpd_timeout=ipsec_sitecon['dpd_timeout'],
admin_state_up=ipsec_sitecon['admin_state_up'],
status=constants.PENDING_CREATE,
status=p_constants.PENDING_CREATE,
vpnservice_id=vpnservice_id,
ikepolicy_id=ipsec_sitecon['ikepolicy_id'],
ipsecpolicy_id=ipsec_sitecon['ipsecpolicy_id']
ipsecpolicy_id=ipsec_sitecon['ipsecpolicy_id'],
local_ep_group_id=ipsec_sitecon['local_ep_group_id'],
peer_ep_group_id=ipsec_sitecon['peer_ep_group_id']
)
context.session.add(ipsec_site_conn_db)
for cidr in ipsec_sitecon['peer_cidrs']:
@ -179,15 +206,15 @@ class VPNPluginDb(vpnaas.VPNPluginBase, base_db.CommonDbMixin):
ipsec_site_conn_db = self._get_resource(
context, vpn_models.IPsecSiteConnection, ipsec_site_conn_id)
vpnservice_id = ipsec_site_conn_db['vpnservice_id']
ip_version = self._get_subnet_ip_version(context, vpnservice_id)
vpnservice = self._get_vpnservice(context, vpnservice_id)
validator.assign_sensible_ipsec_sitecon_defaults(
ipsec_sitecon, ipsec_site_conn_db)
validator.validate_ipsec_site_connection(
context,
ipsec_sitecon,
ip_version)
validator.validate_ipsec_conn_optional_args(ipsec_sitecon,
vpnservice.subnet)
self.validate_connection_info(context, validator, ipsec_sitecon,
vpnservice)
if 'peer_address' in ipsec_sitecon:
vpnservice = self._get_vpnservice(context, vpnservice_id)
validator.resolve_peer_address(ipsec_sitecon,
vpnservice.router)
self.assert_update_allowed(ipsec_site_conn_db)
@ -209,7 +236,9 @@ class VPNPluginDb(vpnaas.VPNPluginBase, base_db.CommonDbMixin):
cidr=peer_cidr,
ipsec_site_connection_id=ipsec_site_conn_id)
context.session.add(pcidr)
del ipsec_sitecon["peer_cidrs"]
# Note: Unconditionally remove peer_cidrs, as they will be set to
# previous, if unchanged (to be able to validate above).
del ipsec_sitecon["peer_cidrs"]
if ipsec_sitecon:
ipsec_site_conn_db.update(ipsec_sitecon)
result = self._make_ipsec_site_connection_dict(ipsec_site_conn_db)
@ -280,7 +309,7 @@ class VPNPluginDb(vpnaas.VPNPluginBase, base_db.CommonDbMixin):
def create_ikepolicy(self, context, ikepolicy):
ike = ikepolicy['ikepolicy']
tenant_id = self._get_tenant_id_for_create(context, ike)
lifetime_info = ike.get('lifetime', [])
lifetime_info = ike['lifetime']
lifetime_units = lifetime_info.get('units', 'seconds')
lifetime_value = lifetime_info.get('value', 3600)
@ -449,7 +478,7 @@ class VPNPluginDb(vpnaas.VPNPluginBase, base_db.CommonDbMixin):
subnet_id=vpns['subnet_id'],
router_id=vpns['router_id'],
admin_state_up=vpns['admin_state_up'],
status=constants.PENDING_CREATE)
status=p_constants.PENDING_CREATE)
context.session.add(vpnservice_db)
return self._make_vpnservice_dict(vpnservice_db)
@ -518,6 +547,21 @@ class VPNPluginDb(vpnaas.VPNPluginBase, base_db.CommonDbMixin):
subnet_id=subnet_id,
vpnservice_id=vpnservices['id'])
def check_subnet_in_use_by_endpoint_group(self, context, subnet_id):
with context.session.begin(subtransactions=True):
query = context.session.query(vpn_models.VPNEndpointGroup)
query = query.filter(vpn_models.VPNEndpointGroup.endpoint_type ==
v_constants.SUBNET_ENDPOINT)
query = query.join(
vpn_models.VPNEndpoint,
sa.and_(vpn_models.VPNEndpoint.endpoint_group_id ==
vpn_models.VPNEndpointGroup.id,
vpn_models.VPNEndpoint.endpoint == subnet_id))
group = query.first()
if group:
raise vpnaas.SubnetInUseByEndpointGroup(
subnet_id=subnet_id, group_id=group['id'])
def _make_endpoint_group_dict(self, endpoint_group, fields=None):
res = {'id': endpoint_group['id'],
'tenant_id': endpoint_group['tenant_id'],
@ -562,6 +606,7 @@ class VPNPluginDb(vpnaas.VPNPluginBase, base_db.CommonDbMixin):
def delete_endpoint_group(self, context, endpoint_group_id):
with context.session.begin(subtransactions=True):
self.check_endpoint_group_not_in_use(context, endpoint_group_id)
endpoint_group_db = self._get_resource(
context, vpn_models.VPNEndpointGroup, endpoint_group_id)
context.session.delete(endpoint_group_db)
@ -576,6 +621,16 @@ class VPNPluginDb(vpnaas.VPNPluginBase, base_db.CommonDbMixin):
self._make_endpoint_group_dict,
filters=filters, fields=fields)
def check_endpoint_group_not_in_use(self, context, group_id):
query = context.session.query(vpn_models.IPsecSiteConnection)
query = query.filter(
sa.or_(
vpn_models.IPsecSiteConnection.local_ep_group_id == group_id,
vpn_models.IPsecSiteConnection.peer_ep_group_id == group_id)
)
if query.first():
raise vpnaas.EndpointGroupInUse(group_id=group_id)
class VPNPluginRpcDbMixin(object):
def _get_agent_hosting_vpn_services(self, context, host):
@ -593,9 +648,6 @@ class VPNPluginRpcDbMixin(object):
return []
query = context.session.query(vpn_models.VPNService)
query = query.join(vpn_models.IPsecSiteConnection)
query = query.join(vpn_models.IKEPolicy)
query = query.join(vpn_models.IPsecPolicy)
query = query.join(vpn_models.IPsecPeerCidr)
query = query.join(l3_agent_db.RouterL3AgentBinding,
l3_agent_db.RouterL3AgentBinding.router_id ==
vpn_models.VPNService.router_id)
@ -603,6 +655,21 @@ class VPNPluginRpcDbMixin(object):
l3_agent_db.RouterL3AgentBinding.l3_agent_id == agent.id)
return query
def _build_local_subnet_cidr_map(self, context):
"""Build a dict of all local endpoint subnets, with list of CIDRs."""
query = context.session.query(models_v2.Subnet.id,
models_v2.Subnet.cidr)
query = query.join(vpn_models.VPNEndpoint,
vpn_models.VPNEndpoint.endpoint ==
models_v2.Subnet.id)
query = query.join(vpn_models.VPNEndpointGroup,
vpn_models.VPNEndpointGroup.id ==
vpn_models.VPNEndpoint.endpoint_group_id)
query = query.join(vpn_models.IPsecSiteConnection,
vpn_models.IPsecSiteConnection.local_ep_group_id ==
vpn_models.VPNEndpointGroup.id)
return {sn.id: sn.cidr for sn in query.all()}
def update_status_by_agent(self, context, service_status_info_list):
"""Updating vpnservice and vpnconnection status.
@ -642,15 +709,15 @@ class VPNPluginRpcDbMixin(object):
def vpn_callback(resource, event, trigger, **kwargs):
vpnservice = manager.NeutronManager.get_service_plugins().get(
constants.VPN)
if vpnservice:
vpn_plugin = manager.NeutronManager.get_service_plugins().get(
p_constants.VPN)
if vpn_plugin:
context = kwargs.get('context')
if resource == resources.ROUTER_GATEWAY:
check_func = vpnservice.check_router_in_use
check_func = vpn_plugin.check_router_in_use
resource_id = kwargs.get('router_id')
elif resource == resources.ROUTER_INTERFACE:
check_func = vpnservice.check_subnet_in_use
check_func = vpn_plugin.check_subnet_in_use
resource_id = kwargs.get('subnet_id')
check_func(context, resource_id)
@ -658,13 +725,23 @@ def vpn_callback(resource, event, trigger, **kwargs):
def migration_callback(resource, event, trigger, **kwargs):
context = kwargs['context']
router = kwargs['router']
vpnservice = manager.NeutronManager.get_service_plugins().get(
constants.VPN)
if vpnservice:
vpnservice.check_router_in_use(context, router['id'])
vpn_plugin = manager.NeutronManager.get_service_plugins().get(
p_constants.VPN)
if vpn_plugin:
vpn_plugin.check_router_in_use(context, router['id'])
return True
def subnet_callback(resource, event, trigger, **kwargs):
"""Respond to subnet based notifications - see if subnet in use."""
context = kwargs['context']
subnet_id = kwargs['subnet_id']
vpn_plugin = manager.NeutronManager.get_service_plugins().get(
p_constants.VPN)
if vpn_plugin:
vpn_plugin.check_subnet_in_use_by_endpoint_group(context, subnet_id)
def subscribe():
registry.subscribe(
vpn_callback, resources.ROUTER_GATEWAY, events.BEFORE_DELETE)
@ -672,6 +749,9 @@ def subscribe():
vpn_callback, resources.ROUTER_INTERFACE, events.BEFORE_DELETE)
registry.subscribe(
migration_callback, resources.ROUTER, events.BEFORE_UPDATE)
registry.subscribe(
subnet_callback, resources.SUBNET, events.BEFORE_DELETE)
# NOTE(armax): multiple VPN service plugins (potentially out of tree) may
# inherit from vpn_db and may need the callbacks to be processed. Having an

View File

@ -124,6 +124,14 @@ class IPsecSiteConnection(model_base.BASEV2,
backref='ipsec_site_connection',
lazy='joined',
cascade='all, delete, delete-orphan')
local_ep_group_id = sa.Column(sa.String(36),
sa.ForeignKey('vpn_endpoint_groups.id'))
peer_ep_group_id = sa.Column(sa.String(36),
sa.ForeignKey('vpn_endpoint_groups.id'))
local_ep_group = orm.relationship("VPNEndpointGroup",
foreign_keys=local_ep_group_id)
peer_ep_group = orm.relationship("VPNEndpointGroup",
foreign_keys=peer_ep_group_id)
class VPNService(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant):
@ -134,8 +142,7 @@ class VPNService(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant):
admin_state_up = sa.Column(sa.Boolean(), nullable=False)
external_v4_ip = sa.Column(sa.String(16))
external_v6_ip = sa.Column(sa.String(64))
subnet_id = sa.Column(sa.String(36), sa.ForeignKey('subnets.id'),
nullable=False)
subnet_id = sa.Column(sa.String(36), sa.ForeignKey('subnets.id'))
router_id = sa.Column(sa.String(36), sa.ForeignKey('routers.id'),
nullable=False)
subnet = orm.relationship(models_v2.Subnet)

View File

@ -18,6 +18,7 @@ import socket
from neutron.api.v2 import attributes
from neutron.common import exceptions as nexception
from neutron.db import l3_db
from neutron.db import models_v2
from neutron import manager
from neutron.plugins.common import constants as nconstants
from oslo_log import log as logging
@ -62,7 +63,7 @@ class VpnReferenceValidator(object):
raise vpnaas.IPsecSiteConnectionMtuError(mtu=mtu,
version=ip_version)
def validate_peer_address(self, ip_version, router):
def _validate_peer_address(self, ip_version, router):
# NOTE: peer_address ip version should match with
# at least one external gateway address ip version.
# ipsec won't work with IPv6 LLA and neutron unaware GUA.
@ -90,17 +91,128 @@ class VpnReferenceValidator(object):
raise vpnaas.VPNPeerAddressNotResolved(peer_address=address)
ip_version = netaddr.IPAddress(ipsec_sitecon['peer_address']).version
self.validate_peer_address(ip_version, router)
self._validate_peer_address(ip_version, router)
def _get_local_subnets(self, context, endpoint_group):
if endpoint_group['type'] != constants.SUBNET_ENDPOINT:
raise vpnaas.WrongEndpointGroupType(
group_type=endpoint_group['type'], which=endpoint_group['id'],
expected=constants.SUBNET_ENDPOINT)
subnet_ids = endpoint_group['endpoints']
return context.session.query(models_v2.Subnet).filter(
models_v2.Subnet.id.in_(subnet_ids)).all()
def _get_peer_cidrs(self, endpoint_group):
if endpoint_group['type'] != constants.CIDR_ENDPOINT:
raise vpnaas.WrongEndpointGroupType(
group_type=endpoint_group['type'], which=endpoint_group['id'],
expected=constants.CIDR_ENDPOINT)
return endpoint_group['endpoints']
def _check_local_endpoint_ip_versions(self, group_id, local_subnets):
"""Ensure all subnets in endpoint group have the same IP version.
Will return the IP version, so it can be used for inter-group testing.
"""
if len(local_subnets) == 1:
return local_subnets[0]['ip_version']
ip_versions = set([subnet['ip_version'] for subnet in local_subnets])
if len(ip_versions) > 1:
raise vpnaas.MixedIPVersionsForIPSecEndpoints(group=group_id)
return ip_versions.pop()
def _check_peer_endpoint_ip_versions(self, group_id, peer_cidrs):
"""Ensure all CIDRs in endpoint group have the same IP version.
Will return the IP version, so it can be used for inter-group testing.
"""
if len(peer_cidrs) == 1:
return netaddr.IPNetwork(peer_cidrs[0]).version
ip_versions = set([netaddr.IPNetwork(pc).version for pc in peer_cidrs])
if len(ip_versions) > 1:
raise vpnaas.MixedIPVersionsForIPSecEndpoints(group=group_id)
return ip_versions.pop()
def _check_peer_cidrs_ip_versions(self, peer_cidrs):
"""Ensure all CIDRs have the same IP version."""
if len(peer_cidrs) == 1:
return netaddr.IPNetwork(peer_cidrs[0]).version
ip_versions = set([netaddr.IPNetwork(pc).version for pc in peer_cidrs])
if len(ip_versions) > 1:
raise vpnaas.MixedIPVersionsForPeerCidrs()
return ip_versions.pop()
def _check_local_subnets_on_router(self, context, router, local_subnets):
for subnet in local_subnets:
self._check_subnet_id(context, router, subnet['id'])
def _validate_compatible_ip_versions(self, local_ip_version,
peer_ip_version):
if local_ip_version != peer_ip_version:
raise vpnaas.MixedIPVersionsForIPSecConnection()
def validate_ipsec_conn_optional_args(self, ipsec_sitecon, subnet):
"""Ensure that proper combinations of optional args are used.
When VPN service has a subnet, then we must have peer_cidrs, and
cannot have any endpoint groups. If no subnet for the service, then
we must have both endpoint groups, and no peer_cidrs. Method will
form a string indicating which endpoints are incorrect, for any
exception raised.
"""
local_epg_id = ipsec_sitecon.get('local_ep_group_id')
peer_epg_id = ipsec_sitecon.get('peer_ep_group_id')
peer_cidrs = ipsec_sitecon.get('peer_cidrs')
if subnet:
if not peer_cidrs:
raise vpnaas.MissingPeerCidrs()
epgs = []
if local_epg_id:
epgs.append('local')
if peer_epg_id:
epgs.append('peer')
if epgs:
which = ' and '.join(epgs)
suffix = 's' if len(epgs) > 1 else ''
raise vpnaas.InvalidEndpointGroup(which=which, suffix=suffix)
else:
if peer_cidrs:
raise vpnaas.PeerCidrsInvalid()
epgs = []
if not local_epg_id:
epgs.append('local')
if not peer_epg_id:
epgs.append('peer')
if epgs:
which = ' and '.join(epgs)
suffix = 's' if len(epgs) > 1 else ''
raise vpnaas.MissingRequiredEndpointGroup(which=which,
suffix=suffix)
def assign_sensible_ipsec_sitecon_defaults(self, ipsec_sitecon,
prev_conn=None):
"""Provide defaults for optional items, if missing.
With endpoint groups capabilities, the peer_cidr (legacy mode)
and endpoint group IDs (new mode), are optional. For updating,
we need to provide the previous values for any missing values,
so that we can detect if the update request is attempting to
mix modes.
Flatten the nested DPD information, and set default values for
any missing information. For connection updates, the previous
values will be used as defaults for any missing items.
"""
if not prev_conn:
if prev_conn:
ipsec_sitecon.setdefault(
'peer_cidrs', [pc['cidr'] for pc in prev_conn['peer_cidrs']])
ipsec_sitecon.setdefault('local_ep_group_id',
prev_conn['local_ep_group_id'])
ipsec_sitecon.setdefault('peer_ep_group_id',
prev_conn['peer_ep_group_id'])
else:
prev_conn = {'dpd_action': 'hold',
'dpd_interval': 30,
'dpd_timeout': 120}
@ -113,12 +225,38 @@ class VpnReferenceValidator(object):
prev_conn['dpd_timeout'])
def validate_ipsec_site_connection(self, context, ipsec_sitecon,
ip_version):
"""Reference implementation of validation for IPSec connection."""
local_ip_version, vpnservice=None):
"""Reference implementation of validation for IPSec connection.
This makes sure that IP versions are the same. For endpoint groups,
we use the local subnet(s) IP versions, and peer CIDR(s) IP versions.
For legacy mode, we use the (sole) subnet IP version, and the peer
CIDR(s). All IP versions must be the same.
This method also checks MTU (based on the local IP version), and
DPD settings.
"""
if not local_ip_version:
# Using endpoint groups
local_subnets = self._get_local_subnets(
context, ipsec_sitecon['local_epg_subnets'])
self._check_local_subnets_on_router(
context, vpnservice['router_id'], local_subnets)
local_ip_version = self._check_local_endpoint_ip_versions(
ipsec_sitecon['local_ep_group_id'], local_subnets)
peer_cidrs = self._get_peer_cidrs(ipsec_sitecon['peer_epg_cidrs'])
peer_ip_version = self._check_peer_endpoint_ip_versions(
ipsec_sitecon['peer_ep_group_id'], peer_cidrs)
else:
peer_ip_version = self._check_peer_cidrs_ip_versions(
ipsec_sitecon['peer_cidrs'])
self._validate_compatible_ip_versions(local_ip_version,
peer_ip_version)
self._check_dpd(ipsec_sitecon)
mtu = ipsec_sitecon.get('mtu')
if mtu:
self._check_mtu(context, mtu, ip_version)
self._check_mtu(context, mtu, local_ip_version)
def _check_router(self, context, router_id):
router = self.l3_plugin.get_router(context, router_id)
@ -138,8 +276,9 @@ class VpnReferenceValidator(object):
def validate_vpnservice(self, context, vpnservice):
self._check_router(context, vpnservice['router_id'])
self._check_subnet_id(context, vpnservice['router_id'],
vpnservice['subnet_id'])
if vpnservice['subnet_id'] is not None:
self._check_subnet_id(context, vpnservice['router_id'],
vpnservice['subnet_id'])
def validate_ipsec_policy(self, context, ipsec_policy):
"""Reference implementation of validation for IPSec Policy.

View File

@ -66,6 +66,10 @@ class SubnetInUseByVPNService(nexception.InUse):
message = _("Subnet %(subnet_id)s is used by VPNService %(vpnservice_id)s")
class SubnetInUseByEndpointGroup(nexception.InUse):
message = _("Subnet %(subnet_id)s is used by endpoint group %(group_id)s")
class VPNStateInvalidToUpdate(nexception.BadRequest):
message = _("Invalid state %(state)s of vpnaas resource %(id)s"
" for updating")
@ -115,6 +119,55 @@ class NonExistingSubnetInEndpointGroup(nexception.InvalidInput):
message = _("Subnet %(subnet)s in endpoint group does not exist")
class MixedIPVersionsForIPSecEndpoints(nexception.BadRequest):
message = _("Endpoints in group %(group)s do not have the same IP "
"version, as required for IPSec site-to-site connection")
class MixedIPVersionsForPeerCidrs(nexception.BadRequest):
message = _("Peer CIDRs do not have the same IP version, as required "
"for IPSec site-to-site connection")
class MixedIPVersionsForIPSecConnection(nexception.BadRequest):
message = _("IP versions are not compatible between peer and local "
"endpoints")
class InvalidEndpointGroup(nexception.BadRequest):
message = _("Endpoint group%(suffix)s %(which)s cannot be specified, "
"when VPN Service has subnet specified")
class WrongEndpointGroupType(nexception.BadRequest):
message = _("Endpoint group %(which)s type is '%(group_type)s' and "
"should be '%(expected)s'")
class PeerCidrsInvalid(nexception.BadRequest):
message = _("Peer CIDRs cannot be specified, when using endpoint "
"groups")
class MissingPeerCidrs(nexception.BadRequest):
message = _("Missing peer CIDRs for IPsec site-to-site connection")
class MissingRequiredEndpointGroup(nexception.BadRequest):
message = _("Missing endpoint group%(suffix)s %(which)s for IPSec "
"site-to-site connection")
class EndpointGroupInUse(nexception.BadRequest):
message = _("Endpoint group %(group_id)s is in use and cannot be deleted")
def _validate_subnet_list_or_none(data, key_specs=None):
if data is not None:
attr._validate_subnet_list(data, key_specs)
attr.validators['type:subnet_list_or_none'] = _validate_subnet_list_or_none
vpn_supported_initiators = ['bi-directional', 'response-only']
vpn_supported_encryption_algorithms = ['3des', 'aes-128',
'aes-192', 'aes-256']
@ -152,8 +205,8 @@ RESOURCE_ATTRIBUTE_MAP = {
'validate': {'type:string': None},
'is_visible': True, 'default': ''},
'subnet_id': {'allow_post': True, 'allow_put': False,
'validate': {'type:uuid': None},
'is_visible': True},
'validate': {'type:uuid_or_none': None},
'is_visible': True, 'default': None},
'router_id': {'allow_post': True, 'allow_put': False,
'validate': {'type:uuid': None},
'is_visible': True},
@ -192,8 +245,15 @@ RESOURCE_ATTRIBUTE_MAP = {
'is_visible': True},
'peer_cidrs': {'allow_post': True, 'allow_put': True,
'convert_to': attr.convert_to_list,
'validate': {'type:subnet_list': None},
'is_visible': True},
'validate': {'type:subnet_list_or_none': None},
'is_visible': True,
'default': None},
'local_ep_group_id': {'allow_post': True, 'allow_put': True,
'validate': {'type:uuid_or_none': None},
'is_visible': True, 'default': None},
'peer_ep_group_id': {'allow_post': True, 'allow_put': True,
'validate': {'type:uuid_or_none': None},
'is_visible': True, 'default': None},
'route_mode': {'allow_post': False, 'allow_put': False,
'default': 'static',
'is_visible': True},

View File

@ -494,8 +494,9 @@ class OpenSwanProcess(BaseSwanProcess):
that are allowed as subnet for the remote client.
"""
virtual_privates = []
nets = [self.vpnservice['subnet']['cidr']]
nets = []
for ipsec_site_conn in self.vpnservice['ipsec_site_connections']:
nets += ipsec_site_conn['local_cidrs']
nets += ipsec_site_conn['peer_cidrs']
for net in nets:
version = netaddr.IPNetwork(net).version
@ -748,21 +749,20 @@ class IPsecDriver(device_drivers.DeviceDriver):
:param vpnservice: vpnservices
:param func: self.add_nat_rule or self.remove_nat_rule
"""
local_cidr = vpnservice['subnet']['cidr']
# This ipsec rule is not needed for ipv6.
if netaddr.IPNetwork(local_cidr).version == 6:
return
router_id = vpnservice['router_id']
for ipsec_site_connection in vpnservice['ipsec_site_connections']:
for peer_cidr in ipsec_site_connection['peer_cidrs']:
func(
router_id,
'POSTROUTING',
'-s %s -d %s -m policy '
'--dir out --pol ipsec '
'-j ACCEPT ' % (local_cidr, peer_cidr),
top=True)
for local_cidr in ipsec_site_connection['local_cidrs']:
# This ipsec rule is not needed for ipv6.
if netaddr.IPNetwork(local_cidr).version == 6:
continue
for peer_cidr in ipsec_site_connection['peer_cidrs']:
func(router_id,
'POSTROUTING',
'-s %s -d %s -m policy '
'--dir out --pol ipsec '
'-j ACCEPT ' % (local_cidr, peer_cidr),
top=True)
self.iptables_apply(router_id)
def vpnservice_updated(self, context, **kwargs):

View File

@ -8,7 +8,7 @@ conn %default
{% for ipsec_site_connection in vpnservice.ipsec_site_connections if ipsec_site_connection.admin_state_up
-%}
conn {{ipsec_site_connection.id}}
{% if vpnservice.subnet.ip_version == 6 -%}
{% if ipsec_site_connection['local_ip_vers'] == 6 -%}
# To recognize the given IP addresses in this config
# as IPv6 addresses by pluto whack. Default is ipv4
connaddrfamily=ipv6
@ -26,8 +26,11 @@ conn {{ipsec_site_connection.id}}
auto={{ipsec_site_connection.initiator}}
# NOTE:REQUIRED
# [subnet]
leftsubnet={{vpnservice.subnet.cidr}}
# leftsubnet=networkA/netmaskA, networkB/netmaskB (IKEv2 only)
{% if ipsec_site_connection['local_cidrs']|length == 1 -%}
leftsubnet={{ipsec_site_connection['local_cidrs'][0]}}
{% else -%}
leftsubnets={ {{ipsec_site_connection['local_cidrs']|join(' ')}} }
{% endif -%}
# [updown]
# What "updown" script to run to adjust routing and/or firewalling when
# the status of the connection changes (default "ipsec _updown").

View File

@ -12,7 +12,7 @@ conn %default
conn {{ipsec_site_connection.id}}
keyexchange={{ipsec_site_connection.ikepolicy.ike_version}}
left={{ipsec_site_connection.external_ip}}
leftsubnet={{vpnservice.subnet.cidr}}
leftsubnet={{ipsec_site_connection['local_cidrs']|join(',')}}
leftid={{ipsec_site_connection.external_ip}}
leftfirewall=yes
right={{ipsec_site_connection.peer_address}}

View File

@ -45,7 +45,8 @@ class IPsecVpnDriverCallBack(object):
plugin = self.driver.service_plugin
vpnservices = plugin._get_agent_hosting_vpn_services(
context, host)
return [self.driver.make_vpnservice_dict(vpnservice)
local_cidr_map = plugin._build_local_subnet_cidr_map(context)
return [self.driver.make_vpnservice_dict(vpnservice, local_cidr_map)
for vpnservice in vpnservices]
def update_status(self, context, status):
@ -165,15 +166,22 @@ class BaseIPsecVPNDriver(service_drivers.VpnDriver):
# don't need a check here.
return ip_to_use
def make_vpnservice_dict(self, vpnservice):
def make_vpnservice_dict(self, vpnservice, local_cidr_map):
"""Convert vpnservice information for vpn agent.
also converting parameter name for vpn agent driver
"""
vpnservice_dict = dict(vpnservice)
vpnservice_dict['ipsec_site_connections'] = []
vpnservice_dict['subnet'] = dict(
vpnservice.subnet)
if vpnservice.subnet:
vpnservice_dict['subnet'] = dict(vpnservice.subnet)
else:
vpnservice_dict['subnet'] = None
# NOTE: Following is used for rolling upgrades, where agent may be
# at version N, and server at N+1. We need to populate the subnet
# with (only) the CIDR from the first connection's local endpoint
# group.
subnet_cidr = None
# Not removing external_ip from vpnservice_dict, as some providers
# may be still using it from vpnservice_dict. Will use whichever IP
# is specified.
@ -192,11 +200,28 @@ class BaseIPsecVPNDriver(service_drivers.VpnDriver):
ipsec_site_connection.ipsecpolicy)
vpnservice_dict['ipsec_site_connections'].append(
ipsec_site_connection_dict)
peer_cidrs = [
peer_cidr.cidr
for peer_cidr in ipsec_site_connection.peer_cidrs]
if vpnservice.subnet:
local_cidrs = [vpnservice.subnet.cidr]
peer_cidrs = [
peer_cidr.cidr
for peer_cidr in ipsec_site_connection.peer_cidrs]
else:
local_cidrs = [local_cidr_map[ep.endpoint]
for ep in ipsec_site_connection.local_ep_group.endpoints]
peer_cidrs = [
ep.endpoint
for ep in ipsec_site_connection.peer_ep_group.endpoints]
if not subnet_cidr:
epg = ipsec_site_connection.local_ep_group
subnet_cidr = local_cidr_map[epg.endpoints[0].endpoint]
ipsec_site_connection_dict['peer_cidrs'] = peer_cidrs
ipsec_site_connection_dict['local_cidrs'] = local_cidrs
ipsec_site_connection_dict['local_ip_vers'] = netaddr.IPNetwork(
local_cidrs[0]).version
ipsec_site_connection_dict['external_ip'] = (
self.get_external_ip_based_on_peer(vpnservice,
ipsec_site_connection_dict))
if not vpnservice.subnet:
vpnservice_dict['subnet'] = {'cidr': subnet_cidr}
return vpnservice_dict

View File

@ -320,7 +320,9 @@ class TestIPSecBase(base.BaseSudoTestCase):
'external_ip': vpn_service['external_ip'],
'peer_cidrs': [peer_vpn_service['subnet']['cidr']],
'peer_address': peer_vpn_service['external_ip'],
'peer_id': peer_vpn_service['external_ip']
'peer_id': peer_vpn_service['external_ip'],
'local_cidrs': [vpn_service['subnet']['cidr']],
'local_ip_vers': 4
})
vpn_service['ipsec_site_connections'] = [ipsec_conn]

View File

@ -1,4 +1,5 @@
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
# (c) Copyright 2015 Cisco Systems Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -14,6 +15,7 @@
# under the License.
import contextlib
import copy
import os
import mock
@ -54,6 +56,8 @@ ROOTDIR = os.path.normpath(os.path.join(
extensions_path = ':'.join(extensions.__path__ + nextensions.__path__)
_uuid = uuidutils.generate_uuid
class TestVpnCorePlugin(test_l3_plugin.TestL3NatIntPlugin,
l3_agentschedulers_db.L3AgentSchedulerDbMixin,
@ -295,6 +299,8 @@ class VPNTestMixin(object):
ikepolicy_id='fake_id',
ipsecpolicy_id='fake_id',
admin_state_up=True,
local_ep_group_id=None,
peer_ep_group_id=None,
expected_res_status=None, **kwargs):
data = {
'ipsec_site_connection': {'name': name,
@ -313,7 +319,9 @@ class VPNTestMixin(object):
'ikepolicy_id': ikepolicy_id,
'ipsecpolicy_id': ipsecpolicy_id,
'admin_state_up': admin_state_up,
'tenant_id': self._tenant_id}
'tenant_id': self._tenant_id,
'local_ep_group_id': local_ep_group_id,
'peer_ep_group_id': peer_ep_group_id}
}
if kwargs.get('description') is not None:
data['ipsec_site_connection'][
@ -347,6 +355,8 @@ class VPNTestMixin(object):
ikepolicy=None,
ipsecpolicy=None,
admin_state_up=True, do_delete=True,
local_ep_group_id=None,
peer_ep_group_id=None,
**kwargs):
if not fmt:
fmt = self.fmt
@ -359,6 +369,9 @@ class VPNTestMixin(object):
vpnservice_id = tmp_vpnservice['vpnservice']['id']
ikepolicy_id = tmp_ikepolicy['ikepolicy']['id']
ipsecpolicy_id = tmp_ipsecpolicy['ipsecpolicy']['id']
if not peer_cidrs and not local_ep_group_id:
# Must be legacy usage - pick default to use
peer_cidrs = ['10.0.0.0/24']
res = self._create_ipsec_site_connection(fmt,
name,
peer_address,
@ -374,6 +387,8 @@ class VPNTestMixin(object):
ikepolicy_id,
ipsecpolicy_id,
admin_state_up,
local_ep_group_id,
peer_ep_group_id,
**kwargs)
if res.status_int >= 400:
raise webob.exc.HTTPClientError(code=res.status_int)
@ -447,7 +462,7 @@ class VPNPluginDbTestCase(VPNTestMixin,
plugin_str,
service_plugins=service_plugins
)
self._subnet_id = uuidutils.generate_uuid()
self._subnet_id = _uuid()
self.core_plugin = TestVpnCorePlugin()
self.plugin = vpn_plugin.VPNPlugin()
ext_mgr = api_extensions.PluginAwareExtensionManager(
@ -1732,13 +1747,14 @@ class TestVpnDatabase(base.NeutronDbPluginV2TestCase, NeutronResourcesMixin):
# Create VPN database instance
self.plugin = vpn_db.VPNPluginDb()
self.tenant_id = uuidutils.generate_uuid()
self.tenant_id = _uuid()
self.context = context.get_admin_context()
def prepare_service_info(self, private_subnet, router):
subnet_id = private_subnet['id'] if private_subnet else None
return {'vpnservice': {'name': 'my-service',
'description': 'new service',
'subnet_id': private_subnet['id'],
'subnet_id': subnet_id,
'router_id': router['id'],
'admin_state_up': True}}
@ -1753,6 +1769,19 @@ class TestVpnDatabase(base.NeutronDbPluginV2TestCase, NeutronResourcesMixin):
new_service = self.plugin.create_vpnservice(self.context, info)
self.assertDictSupersetOf(expected, new_service)
def test_create_vpn_service_without_subnet(self):
"""Create service w/o subnet (will use endpoint groups for conn)."""
private_subnet, router = self.create_basic_topology()
info = self.prepare_service_info(private_subnet=None, router=router)
expected = {'admin_state_up': True,
'external_v4_ip': None,
'external_v6_ip': None,
'status': 'PENDING_CREATE'}
expected.update(info['vpnservice'])
new_service = self.plugin.create_vpnservice(self.context, info)
self.assertDictSupersetOf(expected, new_service)
def test_update_external_tunnel_ips(self):
"""Verify that external tunnel IPs can be set."""
private_subnet, router = self.create_basic_topology()
@ -1782,7 +1811,7 @@ class TestVpnDatabase(base.NeutronDbPluginV2TestCase, NeutronResourcesMixin):
'type': group_type,
'endpoints': endpoints}}
def test_endpoint_group_create_and_with_cidrs(self):
def test_endpoint_group_create_with_cidrs(self):
"""Verify create endpoint group using CIDRs."""
info = self.prepare_endpoint_info(constants.CIDR_ENDPOINT,
['10.10.10.0/24', '20.20.20.0/24'])
@ -1822,7 +1851,7 @@ class TestVpnDatabase(base.NeutronDbPluginV2TestCase, NeutronResourcesMixin):
self.assertDictSupersetOf(expected, new_endpoint_group)
def helper_create_endpoint_group(self, info):
"""Helper to create endpoint group database entry."""
"""Create endpoint group database entry and verify OK."""
try:
actual = self.plugin.create_endpoint_group(self.context, info)
except db_exc.DBError as e:
@ -1877,7 +1906,7 @@ class TestVpnDatabase(base.NeutronDbPluginV2TestCase, NeutronResourcesMixin):
self.assertDictSupersetOf(expected, actual)
def test_fail_showing_non_existent_endpoint_group(self):
"""Test failure to show non-existent endpoint gourp."""
"""Test failure to show non-existent endpoint group."""
self.assertRaises(vpnaas.VPNEndpointGroupNotFound,
self.plugin.get_endpoint_group,
self.context, uuidutils.generate_uuid())
@ -1937,4 +1966,207 @@ class TestVpnDatabase(base.NeutronDbPluginV2TestCase, NeutronResourcesMixin):
self.assertRaises(
vpnaas.VPNEndpointGroupNotFound,
self.plugin.update_endpoint_group,
self.context, uuidutils.generate_uuid(), group_updates)
self.context, _uuid(), group_updates)
def prepare_ike_policy_info(self):
return {'ikepolicy': {'name': 'ike policy',
'description': 'my ike policy',
'auth_algorithm': 'sha1',
'encryption_algorithm': 'aes-128',
'phase1_negotiation_mode': 'main',
'lifetime': {'units': 'seconds', 'value': 3600},
'ike_version': 'v1',
'pfs': 'group5'}}
def test_create_ike_policy(self):
"""Create IKE policy with all settings specified."""
info = self.prepare_ike_policy_info()
expected = info['ikepolicy']
new_ike_policy = self.plugin.create_ikepolicy(self.context, info)
self.assertDictSupersetOf(expected, new_ike_policy)
def prepare_ipsec_policy_info(self):
return {'ipsecpolicy': {'name': 'ipsec policy',
'description': 'my ipsec policy',
'auth_algorithm': 'sha1',
'encryption_algorithm': 'aes-128',
'encapsulation_mode': 'tunnel',
'transform_protocol': 'esp',
'lifetime': {'units': 'seconds',
'value': 3600},
'pfs': 'group5'}}
def test_create_ipsec_policy(self):
"""Create IPsec policy with all settings specified."""
info = self.prepare_ipsec_policy_info()
expected = info['ipsecpolicy']
new_ipsec_policy = self.plugin.create_ipsecpolicy(self.context, info)
self.assertDictSupersetOf(expected, new_ipsec_policy)
def create_vpn_service(self, with_subnet=True):
private_subnet, router = self.create_basic_topology()
if not with_subnet:
private_subnet = None
info = self.prepare_service_info(private_subnet, router)
return self.plugin.create_vpnservice(self.context, info)
def create_ike_policy(self):
info = self.prepare_ike_policy_info()
return self.plugin.create_ikepolicy(self.context, info)
def create_ipsec_policy(self):
info = self.prepare_ipsec_policy_info()
return self.plugin.create_ipsecpolicy(self.context, info)
def create_endpoint_group(self, group_type, endpoints):
info = self.prepare_endpoint_info(group_type=group_type,
endpoints=endpoints)
return self.plugin.create_endpoint_group(self.context, info)
def prepare_connection_info(self, service_id, ike_policy_id,
ipsec_policy_id):
"""Creates connection request dictionary.
The peer_cidrs, local_ep_group_id, and peer_ep_group_id are set to
defaults. Caller must then fill in either CIDRs or endpoints, before
creating a connection.
"""
return {'ipsec_site_connection': {'name': 'my connection',
'description': 'my description',
'peer_id': '192.168.1.10',
'peer_address': '192.168.1.10',
'peer_cidrs': [],
'mtu': 1500,
'psk': 'shhhh!!!',
'initiator': 'bi-directional',
'dpd_action': 'hold',
'dpd_interval': 30,
'dpd_timeout': 120,
'vpnservice_id': service_id,
'ikepolicy_id': ike_policy_id,
'ipsecpolicy_id': ipsec_policy_id,
'admin_state_up': True,
'tenant_id': self._tenant_id,
'local_ep_group_id': None,
'peer_ep_group_id': None}}
def build_expected_connection_result(self, info):
"""Create the expected IPsec connection dict from the request info.
The DPD information is stored and converted to a nested dict, instead
of individual fields.
"""
expected = copy.copy(info['ipsec_site_connection'])
expected['dpd'] = {'action': expected['dpd_action'],
'interval': expected['dpd_interval'],
'timeout': expected['dpd_timeout']}
del expected['dpd_action']
del expected['dpd_interval']
del expected['dpd_timeout']
expected['status'] = 'PENDING_CREATE'
return expected
def prepare_for_ipsec_connection_create(self, with_subnet=True):
service = self.create_vpn_service(with_subnet)
ike_policy = self.create_ike_policy()
ipsec_policy = self.create_ipsec_policy()
return self.prepare_connection_info(service['id'],
ike_policy['id'],
ipsec_policy['id'])
def test_create_ipsec_site_connection_with_peer_cidrs(self):
"""Create connection using old API with peer CIDRs specified."""
info = self.prepare_for_ipsec_connection_create()
info['ipsec_site_connection']['peer_cidrs'] = ['10.1.0.0/24',
'10.2.0.0/24']
expected = self.build_expected_connection_result(info)
new_conn = self.plugin.create_ipsec_site_connection(self.context,
info)
self.assertDictSupersetOf(expected, new_conn)
def test_create_ipsec_site_connection_with_endpoint_groups(self):
"""Create connection using new API with endpoint groups."""
# Skip validation, which is tested separately
mock.patch.object(self.plugin, '_get_validator').start()
local_net = self.create_network(overrides={'name': 'local'})
overrides = {'name': 'local-subnet',
'cidr': '30.0.0.0/24',
'gateway_ip': '30.0.0.1',
'network_id': local_net['id']}
local_subnet = self.create_subnet(overrides=overrides)
info = self.prepare_for_ipsec_connection_create(with_subnet=False)
local_ep_group = self.create_endpoint_group(
group_type='subnet', endpoints=[local_subnet['id']])
peer_ep_group = self.create_endpoint_group(
group_type='cidr', endpoints=['20.1.0.0/24', '20.2.0.0/24'])
info['ipsec_site_connection'].update(
{'local_ep_group_id': local_ep_group['id'],
'peer_ep_group_id': peer_ep_group['id']})
expected = self.build_expected_connection_result(info)
new_conn = self.plugin.create_ipsec_site_connection(self.context,
info)
self.assertDictSupersetOf(expected, new_conn)
def test_fail_endpoint_group_delete_when_in_use_by_ipsec_conn(self):
"""Ensure endpoint group is not deleted from under IPSec connection."""
# Skip validation, which is tested separately
mock.patch.object(self.plugin, '_get_validator').start()
local_net = self.create_network(overrides={'name': 'local'})
overrides = {'name': 'local-subnet',
'cidr': '30.0.0.0/24',
'gateway_ip': '30.0.0.1',
'network_id': local_net['id']}
local_subnet = self.create_subnet(overrides=overrides)
info = self.prepare_for_ipsec_connection_create(with_subnet=False)
local_ep_group = self.create_endpoint_group(
group_type='subnet', endpoints=[local_subnet['id']])
peer_ep_group = self.create_endpoint_group(
group_type='cidr', endpoints=['20.1.0.0/24', '20.2.0.0/24'])
info['ipsec_site_connection'].update(
{'local_ep_group_id': local_ep_group['id'],
'peer_ep_group_id': peer_ep_group['id']})
self.plugin.create_ipsec_site_connection(self.context, info)
self.assertRaises(vpnaas.EndpointGroupInUse,
self.plugin.delete_endpoint_group,
self.context,
local_ep_group['id'])
self.assertRaises(vpnaas.EndpointGroupInUse,
self.plugin.delete_endpoint_group,
self.context,
peer_ep_group['id'])
unused_ep_group = self.create_endpoint_group(
group_type=constants.CIDR_ENDPOINT, endpoints=['30.0.0.0/24'])
self.plugin.delete_endpoint_group(self.context, unused_ep_group['id'])
def test_fail_subnet_delete_when_in_use_by_endpoint_group(self):
"""Ensure don't delete subnet from under endpoint group."""
# mock.patch.object(self.plugin, '_get_validator').start()
local_net = self.create_network(overrides={'name': 'local'})
overrides = {'name': 'local-subnet',
'cidr': '30.0.0.0/24',
'gateway_ip': '30.0.0.1',
'network_id': local_net['id']}
local_subnet = self.create_subnet(overrides=overrides)
self.create_endpoint_group(group_type='subnet',
endpoints=[local_subnet['id']])
self.assertRaises(vpnaas.SubnetInUseByEndpointGroup,
self.plugin.check_subnet_in_use_by_endpoint_group,
self.context, local_subnet['id'])
def test_fail_subnet_change_when_in_use_by_endpoint_group(self):
"""Prevent subnet changes, when used by endpoint group.
An IPSec connection *may* be using an endpoint group that includes
the subnet being changed.
TODO(pcm): Either get notify before subnet change, and block if in
use, or get notify after change and force a sync operation. Either
way, this requires a Neutron change first.
"""
pass

View File

@ -0,0 +1,556 @@
# Copyright 2015, 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
import socket
from neutron.common import exceptions as nexception
from neutron import context as n_ctx
from neutron.db import l3_db
from neutron.db import servicetype_db as st_db
from neutron.plugins.common import constants as nconstants
from oslo_utils import uuidutils
from sqlalchemy.orm import query
from neutron_vpnaas.extensions import vpnaas
from neutron_vpnaas.services.vpn.common import constants as v_constants
from neutron_vpnaas.services.vpn import plugin as vpn_plugin
from neutron_vpnaas.services.vpn.service_drivers \
import ipsec_validator as vpn_validator
from neutron_vpnaas.tests import base
_uuid = uuidutils.generate_uuid
FAKE_ROUTER_ID = _uuid()
FAKE_ROUTER = {l3_db.EXTERNAL_GW_INFO: FAKE_ROUTER_ID}
FAKE_SUBNET_ID = _uuid()
IPV4 = 4
IPV6 = 6
IPSEC_SERVICE_DRIVER = ('neutron_vpnaas.services.vpn.service_drivers.'
'ipsec.IPsecVPNDriver')
class TestValidatorSelection(base.BaseTestCase):
def setUp(self):
super(TestValidatorSelection, self).setUp()
vpnaas_provider = [{
'service_type': nconstants.VPN,
'name': 'vpnaas',
'driver': IPSEC_SERVICE_DRIVER,
'default': True
}]
# override the default service provider
self.service_providers = (
mock.patch.object(st_db.ServiceTypeManager,
'get_service_providers').start())
self.service_providers.return_value = vpnaas_provider
mock.patch('neutron.common.rpc.create_connection').start()
stm = st_db.ServiceTypeManager()
mock.patch('neutron.db.servicetype_db.ServiceTypeManager.get_instance',
return_value=stm).start()
self.vpn_plugin = vpn_plugin.VPNDriverPlugin()
def test_reference_driver_used(self):
self.assertIsInstance(self.vpn_plugin._get_validator(),
vpn_validator.IpsecVpnValidator)
class TestIPsecDriverValidation(base.BaseTestCase):
def setUp(self):
super(TestIPsecDriverValidation, self).setUp()
self.l3_plugin = mock.Mock()
mock.patch(
'neutron.manager.NeutronManager.get_service_plugins',
return_value={nconstants.L3_ROUTER_NAT: self.l3_plugin}).start()
self.core_plugin = mock.Mock()
mock.patch('neutron.manager.NeutronManager.get_plugin',
return_value=self.core_plugin).start()
self.context = n_ctx.Context('some_user', 'some_tenant')
self.service_plugin = mock.Mock()
self.validator = vpn_validator.IpsecVpnValidator(self.service_plugin)
self.router = mock.Mock()
self.router.gw_port = {'fixed_ips': [{'ip_address': '10.0.0.99'}]}
def test_non_public_router_for_vpn_service(self):
"""Failure test of service validate, when router missing ext. I/F."""
self.l3_plugin.get_router.return_value = {} # No external gateway
vpnservice = {'router_id': 123, 'subnet_id': 456}
self.assertRaises(vpnaas.RouterIsNotExternal,
self.validator.validate_vpnservice,
self.context, vpnservice)
def test_subnet_not_connected_for_vpn_service(self):
"""Failure test of service validate, when subnet not on router."""
self.l3_plugin.get_router.return_value = FAKE_ROUTER
self.core_plugin.get_ports.return_value = None
vpnservice = {'router_id': FAKE_ROUTER_ID, 'subnet_id': FAKE_SUBNET_ID}
self.assertRaises(vpnaas.SubnetIsNotConnectedToRouter,
self.validator.validate_vpnservice,
self.context, vpnservice)
def test_defaults_for_ipsec_site_connections_on_create(self):
"""Check that defaults are applied correctly.
MTU has a default and will always be present on create.
However, the DPD settings do not have a default, so
database create method will assign default values for any
missing. In addition, the DPD dict will be flattened
for storage into the database, so we'll do it as part of
assigning defaults.
"""
ipsec_sitecon = {}
self.validator.assign_sensible_ipsec_sitecon_defaults(ipsec_sitecon)
expected = {
'dpd_action': 'hold',
'dpd_timeout': 120,
'dpd_interval': 30
}
self.assertEqual(expected, ipsec_sitecon)
ipsec_sitecon = {'dpd': {'interval': 50}}
self.validator.assign_sensible_ipsec_sitecon_defaults(ipsec_sitecon)
expected = {
'dpd': {'interval': 50},
'dpd_action': 'hold',
'dpd_timeout': 120,
'dpd_interval': 50
}
self.assertEqual(expected, ipsec_sitecon)
def test_resolve_peer_address_with_ipaddress(self):
ipsec_sitecon = {'peer_address': '10.0.0.9'}
self.validator._validate_peer_address = mock.Mock()
self.validator.resolve_peer_address(ipsec_sitecon, self.router)
self.assertEqual('10.0.0.9', ipsec_sitecon['peer_address'])
self.validator._validate_peer_address.assert_called_once_with(
IPV4, self.router)
def test_resolve_peer_address_with_fqdn(self):
with mock.patch.object(socket, 'getaddrinfo') as mock_getaddr_info:
mock_getaddr_info.return_value = [(2, 1, 6, '',
('10.0.0.9', 0))]
ipsec_sitecon = {'peer_address': 'fqdn.peer.addr'}
self.validator._validate_peer_address = mock.Mock()
self.validator.resolve_peer_address(ipsec_sitecon, self.router)
self.assertEqual('10.0.0.9', ipsec_sitecon['peer_address'])
self.validator._validate_peer_address.assert_called_once_with(
IPV4, self.router)
def test_resolve_peer_address_with_invalid_fqdn(self):
with mock.patch.object(socket, 'getaddrinfo') as mock_getaddr_info:
def getaddr_info_failer(*args, **kwargs):
raise socket.gaierror()
mock_getaddr_info.side_effect = getaddr_info_failer
ipsec_sitecon = {'peer_address': 'fqdn.invalid'}
self.assertRaises(vpnaas.VPNPeerAddressNotResolved,
self.validator.resolve_peer_address,
ipsec_sitecon, self.router)
def helper_validate_peer_address(self, fixed_ips, ip_version,
expected_exception=False):
self.router.id = FAKE_ROUTER_ID
self.router.gw_port = {'fixed_ips': fixed_ips}
try:
self.validator._validate_peer_address(ip_version, self.router)
if expected_exception:
self.fail("No exception raised for invalid peer address")
except vpnaas.ExternalNetworkHasNoSubnet:
if not expected_exception:
self.fail("exception for valid peer address raised")
def test_validate_peer_address(self):
# validate ipv4 peer_address with ipv4 gateway
fixed_ips = [{'ip_address': '10.0.0.99'}]
self.helper_validate_peer_address(fixed_ips, IPV4)
# validate ipv6 peer_address with ipv6 gateway
fixed_ips = [{'ip_address': '2001::1'}]
self.helper_validate_peer_address(fixed_ips, IPV6)
# validate ipv6 peer_address with both ipv4 and ipv6 gateways
fixed_ips = [{'ip_address': '2001::1'}, {'ip_address': '10.0.0.99'}]
self.helper_validate_peer_address(fixed_ips, IPV6)
# validate ipv4 peer_address with both ipv4 and ipv6 gateways
fixed_ips = [{'ip_address': '2001::1'}, {'ip_address': '10.0.0.99'}]
self.helper_validate_peer_address(fixed_ips, IPV4)
# validate ipv4 peer_address with ipv6 gateway
fixed_ips = [{'ip_address': '2001::1'}]
self.helper_validate_peer_address(fixed_ips, IPV4,
expected_exception=True)
# validate ipv6 peer_address with ipv4 gateway
fixed_ips = [{'ip_address': '10.0.0.99'}]
self.helper_validate_peer_address(fixed_ips, IPV6,
expected_exception=True)
def test_validate_ipsec_policy(self):
ipsec_policy = {'transform_protocol': 'ah-esp'}
self.assertRaises(vpn_validator.IpsecValidationFailure,
self.validator.validate_ipsec_policy,
self.context, ipsec_policy)
def test_defaults_for_ipsec_site_connections_on_update(self):
"""Check that defaults are used for any values not specified."""
ipsec_sitecon = {}
prev_connection = {'peer_cidrs': [{'cidr': '10.0.0.0/24'},
{'cidr': '20.0.0.0/24'}],
'local_ep_group_id': None,
'peer_ep_group_id': None,
'dpd_action': 'clear',
'dpd_timeout': 500,
'dpd_interval': 250}
self.validator.assign_sensible_ipsec_sitecon_defaults(ipsec_sitecon,
prev_connection)
expected = {
'peer_cidrs': ['10.0.0.0/24', '20.0.0.0/24'],
'local_ep_group_id': None,
'peer_ep_group_id': None,
'dpd_action': 'clear',
'dpd_timeout': 500,
'dpd_interval': 250
}
self.assertEqual(expected, ipsec_sitecon)
ipsec_sitecon = {'dpd': {'timeout': 200}}
local_epg_id = _uuid()
peer_epg_id = _uuid()
prev_connection = {'peer_cidrs': [],
'local_ep_group_id': local_epg_id,
'peer_ep_group_id': peer_epg_id,
'dpd_action': 'clear',
'dpd_timeout': 500,
'dpd_interval': 100}
self.validator.assign_sensible_ipsec_sitecon_defaults(ipsec_sitecon,
prev_connection)
expected = {
'peer_cidrs': [],
'local_ep_group_id': local_epg_id,
'peer_ep_group_id': peer_epg_id,
'dpd': {'timeout': 200},
'dpd_action': 'clear',
'dpd_timeout': 200,
'dpd_interval': 100
}
self.assertEqual(expected, ipsec_sitecon)
def test_bad_dpd_settings_on_create(self):
"""Failure tests of DPD settings for IPSec conn during create."""
ipsec_sitecon = {'dpd_action': 'hold', 'dpd_interval': 100,
'dpd_timeout': 100}
self.assertRaises(vpnaas.IPsecSiteConnectionDpdIntervalValueError,
self.validator._check_dpd, ipsec_sitecon)
ipsec_sitecon = {'dpd_action': 'hold', 'dpd_interval': 100,
'dpd_timeout': 99}
self.assertRaises(vpnaas.IPsecSiteConnectionDpdIntervalValueError,
self.validator._check_dpd, ipsec_sitecon)
def test_bad_mtu_for_ipsec_connection(self):
"""Failure test of invalid MTU values for IPSec conn create/update."""
ip_version_limits = vpn_validator.IpsecVpnValidator.IP_MIN_MTU
for version, limit in ip_version_limits.items():
ipsec_sitecon = {'mtu': limit - 1}
self.assertRaises(
vpnaas.IPsecSiteConnectionMtuError,
self.validator._check_mtu,
self.context, ipsec_sitecon['mtu'], version)
def test_endpoints_all_cidrs_in_endpoint_group(self):
"""All endpoints in the endpoint group are valid CIDRs."""
endpoint_group = {'type': v_constants.CIDR_ENDPOINT,
'endpoints': ['10.10.10.0/24', '20.20.20.0/24']}
try:
self.validator.validate_endpoint_group(self.context,
endpoint_group)
except Exception:
self.fail("All CIDRs in endpoint_group should be valid")
def test_endpoints_all_subnets_in_endpoint_group(self):
"""All endpoints in the endpoint group are valid subnets."""
endpoint_group = {'type': v_constants.SUBNET_ENDPOINT,
'endpoints': [_uuid(), _uuid()]}
try:
self.validator.validate_endpoint_group(self.context,
endpoint_group)
except Exception:
self.fail("All subnets in endpoint_group should be valid")
def test_mixed_endpoint_types_in_endpoint_group(self):
"""Fail when mixing types of endpoints in endpoint group."""
endpoint_group = {'type': v_constants.CIDR_ENDPOINT,
'endpoints': ['10.10.10.0/24', _uuid()]}
self.assertRaises(vpnaas.InvalidEndpointInEndpointGroup,
self.validator.validate_endpoint_group,
self.context, endpoint_group)
endpoint_group = {'type': v_constants.SUBNET_ENDPOINT,
'endpoints': [_uuid(), '10.10.10.0/24']}
self.assertRaises(vpnaas.InvalidEndpointInEndpointGroup,
self.validator.validate_endpoint_group,
self.context, endpoint_group)
def test_missing_endpoints_for_endpoint_group(self):
endpoint_group = {'type': v_constants.CIDR_ENDPOINT,
'endpoints': []}
self.assertRaises(vpnaas.MissingEndpointForEndpointGroup,
self.validator.validate_endpoint_group,
self.context, endpoint_group)
def test_fail_bad_cidr_in_endpoint_group(self):
"""Testing catches bad CIDR.
Just check one case, as CIDR validator used has good test coverage.
"""
endpoint_group = {'type': v_constants.CIDR_ENDPOINT,
'endpoints': ['10.10.10.10/24', '20.20.20.1']}
self.assertRaises(vpnaas.InvalidEndpointInEndpointGroup,
self.validator.validate_endpoint_group,
self.context, endpoint_group)
def test_unknown_subnet_in_endpoint_group(self):
subnet_id = _uuid()
self.core_plugin.get_subnet.side_effect = nexception.SubnetNotFound(
subnet_id=subnet_id)
endpoint_group = {'type': v_constants.SUBNET_ENDPOINT,
'endpoints': [subnet_id]}
self.assertRaises(vpnaas.NonExistingSubnetInEndpointGroup,
self.validator.validate_endpoint_group,
self.context, endpoint_group)
def test_fail_subnets_not_on_same_router_for_endpoint_group(self):
"""Detect when local endpoints not on the same router."""
subnet1 = {'id': _uuid(), 'ip_version': 4}
subnet2 = {'id': _uuid(), 'ip_version': 4}
router = _uuid()
multiple_subnets = [subnet1, subnet2]
port_mock = mock.patch.object(self.core_plugin, "get_ports").start()
port_mock.side_effect = ['dummy info', None]
self.assertRaises(vpnaas.SubnetIsNotConnectedToRouter,
self.validator._check_local_subnets_on_router,
self.context, router, multiple_subnets)
def test_ipsec_conn_local_endpoints_same_ip_version(self):
"""Check local endpoint subnets all have same IP version."""
endpoint_group_id = _uuid()
subnet1 = {'ip_version': 4}
subnet2 = {'ip_version': 4}
single_subnet = [subnet1]
version = self.validator._check_local_endpoint_ip_versions(
endpoint_group_id, single_subnet)
self.assertEqual(4, version)
multiple_subnets = [subnet1, subnet2]
version = self.validator._check_local_endpoint_ip_versions(
endpoint_group_id, multiple_subnets)
self.assertEqual(4, version)
def test_fail_ipsec_conn_local_endpoints_mixed_ip_version(self):
"""Fail when mixed IP versions in local endpoints."""
endpoint_group_id = _uuid()
subnet1 = {'ip_version': 6}
subnet2 = {'ip_version': 4}
mixed_subnets = [subnet1, subnet2]
self.assertRaises(vpnaas.MixedIPVersionsForIPSecEndpoints,
self.validator._check_local_endpoint_ip_versions,
endpoint_group_id, mixed_subnets)
def test_ipsec_conn_peer_endpoints_same_ip_version(self):
"""Check all CIDRs have the same IP version."""
endpoint_group_id = _uuid()
one_cidr = ['2002:0a00::/48']
version = self.validator._check_peer_endpoint_ip_versions(
endpoint_group_id, one_cidr)
self.assertEqual(6, version)
multiple_cidr = ['10.10.10.0/24', '20.20.20.0/24']
version = self.validator._check_peer_endpoint_ip_versions(
endpoint_group_id, multiple_cidr)
self.assertEqual(4, version)
def test_fail_ipsec_conn_peer_endpoints_mixed_ip_version(self):
"""Fail when mixed IP versions in peer endpoints."""
endpoint_group_id = _uuid()
mixed_cidrs = ['10.10.10.0/24', '2002:1400::/48']
self.assertRaises(vpnaas.MixedIPVersionsForIPSecEndpoints,
self.validator._check_peer_endpoint_ip_versions,
endpoint_group_id, mixed_cidrs)
def test_fail_ipsec_conn_locals_and_peers_different_ip_version(self):
"""Ensure catch when local and peer IP versions are not the same."""
self.assertRaises(vpnaas.MixedIPVersionsForIPSecConnection,
self.validator._validate_compatible_ip_versions,
4, 6)
def test_fail_ipsec_conn_no_subnet_requiring_endpoint_groups(self):
"""When no subnet, connection must use endpoints.
This means both endpoint groups must be present, and peer_cidrs
cannot be used.
"""
subnet = None
ipsec_sitecon = {'peer_cidrs': ['10.0.0.0/24'],
'local_ep_group_id': 'local-epg-id',
'peer_ep_group_id': 'peer-epg-id'}
self.assertRaises(vpnaas.PeerCidrsInvalid,
self.validator.validate_ipsec_conn_optional_args,
ipsec_sitecon, subnet)
ipsec_sitecon = {'peer_cidrs': [],
'local_ep_group_id': None,
'peer_ep_group_id': 'peer-epg-id'}
self.assertRaises(vpnaas.MissingRequiredEndpointGroup,
self.validator.validate_ipsec_conn_optional_args,
ipsec_sitecon, subnet)
ipsec_sitecon = {'peer_cidrs': [],
'local_ep_group_id': 'local-epg-id',
'peer_ep_group_id': None}
self.assertRaises(vpnaas.MissingRequiredEndpointGroup,
self.validator.validate_ipsec_conn_optional_args,
ipsec_sitecon, subnet)
ipsec_sitecon = {'peer_cidrs': [],
'local_ep_group_id': None,
'peer_ep_group_id': None}
self.assertRaises(vpnaas.MissingRequiredEndpointGroup,
self.validator.validate_ipsec_conn_optional_args,
ipsec_sitecon, subnet)
def test_fail_ipsec_conn_subnet_requiring_peer_cidrs(self):
"""When legacy mode, no endpoint groups.
This means neither endpoint group can be specified, and the peer_cidrs
must be present.
"""
subnet = {'id': FAKE_SUBNET_ID}
ipsec_sitecon = {'peer_cidrs': [],
'local_ep_group_id': None,
'peer_ep_group_id': None}
self.assertRaises(vpnaas.MissingPeerCidrs,
self.validator.validate_ipsec_conn_optional_args,
ipsec_sitecon, subnet)
ipsec_sitecon = {'peer_cidrs': ['10.0.0.0/24'],
'local_ep_group_id': 'local-epg-id',
'peer_ep_group_id': None}
self.assertRaises(vpnaas.InvalidEndpointGroup,
self.validator.validate_ipsec_conn_optional_args,
ipsec_sitecon, subnet)
ipsec_sitecon = {'peer_cidrs': ['10.0.0.0/24'],
'local_ep_group_id': None,
'peer_ep_group_id': 'peer-epg-id'}
self.assertRaises(vpnaas.InvalidEndpointGroup,
self.validator.validate_ipsec_conn_optional_args,
ipsec_sitecon, subnet)
ipsec_sitecon = {'peer_cidrs': ['10.0.0.0/24'],
'local_ep_group_id': 'local-epg-id',
'peer_ep_group_id': 'peer-epg-id'}
self.assertRaises(vpnaas.InvalidEndpointGroup,
self.validator.validate_ipsec_conn_optional_args,
ipsec_sitecon, subnet)
def test_ipsec_conn_get_local_subnets(self):
subnet1 = _uuid()
subnet2 = _uuid()
expected_subnets = [subnet1, subnet2]
local_epg = {'id': _uuid(),
'type': v_constants.SUBNET_ENDPOINT,
'endpoints': expected_subnets}
query_mock = mock.patch.object(query.Query, 'all').start()
query_mock.return_value = expected_subnets
subnets = self.validator._get_local_subnets(self.context, local_epg)
self.assertEqual(expected_subnets, subnets)
def test_ipsec_conn_get_peer_cidrs(self):
expected_cidrs = ['10.10.10.10/24', '20.20.20.20/24']
peer_epg = {'id': 'should-be-cidrs',
'type': v_constants.CIDR_ENDPOINT,
'endpoints': expected_cidrs}
cidrs = self.validator._get_peer_cidrs(peer_epg)
self.assertEqual(expected_cidrs, cidrs)
def test_fail_ipsec_conn_endpoint_group_types(self):
local_epg = {'id': 'should-be-subnets',
'type': v_constants.CIDR_ENDPOINT,
'endpoints': ['10.10.10.10/24', '20.20.20.20/24']}
self.assertRaises(vpnaas.WrongEndpointGroupType,
self.validator._get_local_subnets,
self.context, local_epg)
peer_epg = {'id': 'should-be-cidrs',
'type': v_constants.SUBNET_ENDPOINT,
'endpoints': [_uuid(), _uuid()]}
self.assertRaises(vpnaas.WrongEndpointGroupType,
self.validator._get_peer_cidrs,
peer_epg)
def test_validate_ipsec_conn_for_endpoints(self):
"""Check upper-level validation method for endpoint groups.
Tests the happy path for doing general validation of the IPSec
connection, calling all the sub-checks for an endpoint group case.
"""
subnet1 = {'id': _uuid(), 'ip_version': 4}
subnet2 = {'id': _uuid(), 'ip_version': 4}
local_subnets = [subnet1, subnet2]
local_epg_id = _uuid()
local_epg = {'id': local_epg_id,
'type': v_constants.SUBNET_ENDPOINT,
'endpoints': local_subnets}
# Mock getting the subnets from the IDs
query_mock = mock.patch.object(query.Query, 'all').start()
query_mock.return_value = local_subnets
# Mock that subnet is on router
port_mock = mock.patch.object(self.core_plugin, "get_ports").start()
port_mock.side_effect = ['dummy info', 'more dummy info']
peer_epg_id = _uuid()
peer_cidrs = ['10.10.10.10/24', '20.20.20.20/24']
peer_epg = {'id': peer_epg_id,
'type': v_constants.CIDR_ENDPOINT,
'endpoints': peer_cidrs}
ipsec_sitecon = {'local_ep_group_id': local_epg_id,
'local_epg_subnets': local_epg,
'peer_ep_group_id': peer_epg_id,
'peer_epg_cidrs': peer_epg,
'mtu': 2000,
'dpd_action': 'hold',
'dpd_interval': 30,
'dpd_timeout': 120}
local_version = None
vpnservice = {'router_id': _uuid()}
self.validator.validate_ipsec_site_connection(
self.context, ipsec_sitecon, local_version, vpnservice)
# NOTE: Following are tests for the older API, providing some additional
# coverage.
def test_ipsec_conn_peer_cidrs_same_ip_version(self):
"""Check legacy peer_cidrs have same IP version."""
one_cidr = ['2002:0a00::/48']
version = self.validator._check_peer_cidrs_ip_versions(one_cidr)
self.assertEqual(6, version)
multiple_cidrs = ['10.10.10.0/24', '20.20.20.0/24']
version = self.validator._check_peer_cidrs_ip_versions(multiple_cidrs)
self.assertEqual(4, version)
def test_fail_ipsec_conn_peer_cidrs_mixed_ip_version(self):
mixed_cidrs = ['2002:0a00::/48', '20.20.20.0/24']
self.assertRaises(vpnaas.MixedIPVersionsForPeerCidrs,
self.validator._check_peer_cidrs_ip_versions,
mixed_cidrs)

View File

@ -272,30 +272,51 @@ class VpnaasExtensionTestCase(base.ExtensionTestCase):
"""Test case to delete an ipsecpolicy."""
self._test_entity_delete('ipsecpolicy')
def test_vpnservice_create(self):
"""Test case to create a vpnservice."""
vpnservice_id = _uuid()
def _test_vpnservice_create(self, more_args, defaulted_args):
"""Helper to test VPN service creation.
Allows additional args to be specified for different test cases.
Includes expected args, for case where an optional args are not
specified and API applies defaults.
"""
data = {'vpnservice': {'name': 'vpnservice1',
'description': 'descr_vpn1',
'subnet_id': _uuid(),
'router_id': _uuid(),
'admin_state_up': True,
'tenant_id': _uuid()}}
return_value = copy.copy(data['vpnservice'])
return_value.update({'status': "ACTIVE", 'id': vpnservice_id})
data['vpnservice'].update(more_args)
# Add in any default values for args that were not provided
actual_args = copy.copy(data)
actual_args['vpnservice'].update(defaulted_args)
return_value = copy.copy(data['vpnservice'])
return_value.update({'status': "ACTIVE", 'id': _uuid()})
return_value.update(defaulted_args)
instance = self.plugin.return_value
instance.create_vpnservice.return_value = return_value
res = self.api.post(_get_path('vpn/vpnservices', fmt=self.fmt),
self.serialize(data),
content_type='application/%s' % self.fmt)
instance.create_vpnservice.assert_called_with(mock.ANY,
vpnservice=data)
vpnservice=actual_args)
self.assertEqual(exc.HTTPCreated.code, res.status_int)
res = self.deserialize(res)
self.assertIn('vpnservice', res)
self.assertEqual(return_value, res['vpnservice'])
def test_vpnservice_create(self):
"""Create VPN service using subnet (older API)."""
subnet = {'subnet_id': _uuid()}
self._test_vpnservice_create(more_args=subnet, defaulted_args={})
def test_vpnservice_create_no_subnet(self):
"""Test case to create a vpnservice w/o subnet (newer API)."""
no_subnet = {'subnet_id': None}
self._test_vpnservice_create(more_args={}, defaulted_args=no_subnet)
def test_vpnservice_list(self):
"""Test case to list all vpnservices."""
vpnservice_id = _uuid()
@ -372,8 +393,8 @@ class VpnaasExtensionTestCase(base.ExtensionTestCase):
"""Test case to delete a vpnservice."""
self._test_entity_delete('vpnservice')
def test_ipsec_site_connection_create(self):
"""Test case to create a ipsec_site_connection."""
def _test_ipsec_site_connection_create(self, more_args, defaulted_args):
"""Helper to test creating IPSec connection."""
ipsecsite_con_id = _uuid()
ikepolicy_id = _uuid()
ipsecpolicy_id = _uuid()
@ -382,8 +403,6 @@ class VpnaasExtensionTestCase(base.ExtensionTestCase):
'description': 'Remote-connection1',
'peer_address': '192.168.1.10',
'peer_id': '192.168.1.10',
'peer_cidrs': ['192.168.2.0/24',
'192.168.3.0/24'],
'mtu': 1500,
'psk': 'abcd',
'initiator': 'bi-directional',
@ -397,23 +416,45 @@ class VpnaasExtensionTestCase(base.ExtensionTestCase):
'admin_state_up': True,
'tenant_id': _uuid()}
}
data['ipsec_site_connection'].update(more_args)
# Add in any default values for args that were not provided
actual_args = copy.copy(data)
actual_args['ipsec_site_connection'].update(defaulted_args)
return_value = copy.copy(data['ipsec_site_connection'])
return_value.update({'status': "ACTIVE", 'id': ipsecsite_con_id})
return_value.update(defaulted_args)
instance = self.plugin.return_value
instance.create_ipsec_site_connection.return_value = return_value
res = self.api.post(_get_path('vpn/ipsec-site-connections',
fmt=self.fmt),
self.serialize(data),
content_type='application/%s' % self.fmt)
instance.create_ipsec_site_connection.assert_called_with(
mock.ANY, ipsec_site_connection=data
)
mock.ANY, ipsec_site_connection=actual_args)
self.assertEqual(exc.HTTPCreated.code, res.status_int)
res = self.deserialize(res)
self.assertIn('ipsec_site_connection', res)
self.assertEqual(return_value, res['ipsec_site_connection'])
def test_ipsec_site_connection_create(self):
"""Create an IPSec connection with peer CIDRs (old API)."""
peer_cidrs = {'peer_cidrs': ['192.168.2.0/24', '192.168.3.0/24']}
no_endpoint_groups = {'local_ep_group_id': None,
'peer_ep_group_id': None}
self._test_ipsec_site_connection_create(
more_args=peer_cidrs, defaulted_args=no_endpoint_groups)
def test_ipsec_site_connection_create_with_endpoints(self):
"""Create an IPSec connection with endpoint groups (new API)."""
endpoint_groups = {'local_ep_group_id': _uuid(),
'peer_ep_group_id': _uuid()}
no_peer_cidrs = {'peer_cidrs': []}
self._test_ipsec_site_connection_create(more_args=endpoint_groups,
defaulted_args=no_peer_cidrs)
def test_ipsec_site_connection_list(self):
"""Test case to list all ipsec_site_connections."""
ipsecsite_con_id = _uuid()
@ -422,6 +463,8 @@ class VpnaasExtensionTestCase(base.ExtensionTestCase):
'peer_cidrs': ['192.168.2.0/24', '192.168.3.0/24'],
'route_mode': 'static',
'auth_mode': 'psk',
'local_ep_group_id': None,
'peer_ep_group_id': None,
'tenant_id': _uuid(),
'status': 'ACTIVE',
'id': ipsecsite_con_id}]
@ -457,6 +500,8 @@ class VpnaasExtensionTestCase(base.ExtensionTestCase):
'ipsecpolicy_id': _uuid(),
'vpnservice_id': _uuid(),
'admin_state_up': False,
'local_ep_group_id': None,
'peer_ep_group_id': None,
'tenant_id': _uuid(),
'status': 'ACTIVE',
'id': ipsecsite_con_id}
@ -499,6 +544,8 @@ class VpnaasExtensionTestCase(base.ExtensionTestCase):
'vpnservice_id': _uuid(),
'admin_state_up': True,
'tenant_id': _uuid(),
'local_ep_group_id': None,
'peer_ep_group_id': None,
'status': 'ACTIVE',
'id': ipsecsite_con_id}

View File

@ -49,7 +49,8 @@ FAKE_IKE_POLICY = {
FAKE_IPSEC_POLICY = {
'encryption_algorithm': 'aes-128',
'auth_algorithm': 'sha1',
'pfs': 'group5'
'pfs': 'group5',
'transform_protocol': 'esp'
}
FAKE_VPN_SERVICE = {
@ -58,17 +59,18 @@ FAKE_VPN_SERVICE = {
'name': 'myvpn',
'admin_state_up': True,
'status': constants.PENDING_CREATE,
'external_ip': '50.0.0.4',
'subnet': {'cidr': '10.0.0.0/24'},
'external_ip': '60.0.0.4',
'ipsec_site_connections': [
{'peer_cidrs': ['20.0.0.0/24',
'30.0.0.0/24'],
'local_cidrs': ['10.0.0.0/24'],
'local_ip_vers': 4,
'admin_state_up': True,
'id': FAKE_IPSEC_SITE_CONNECTION1_ID,
'external_ip': '50.0.0.4',
'peer_address': '30.0.0.5',
'external_ip': '60.0.0.4',
'peer_address': '60.0.0.5',
'mtu': 1500,
'peer_id': '30.0.0.5',
'peer_id': '60.0.0.5',
'psk': 'password',
'initiator': 'bi-directional',
'ikepolicy': FAKE_IKE_POLICY,
@ -76,10 +78,12 @@ FAKE_VPN_SERVICE = {
'status': constants.PENDING_CREATE},
{'peer_cidrs': ['40.0.0.0/24',
'50.0.0.0/24'],
'local_cidrs': ['11.0.0.0/24'],
'local_ip_vers': 4,
'admin_state_up': True,
'external_ip': '50.0.0.4',
'peer_address': '50.0.0.5',
'peer_id': '50.0.0.5',
'external_ip': '60.0.0.4',
'peer_address': '60.0.0.6',
'peer_id': '60.0.0.6',
'mtu': 1500,
'psk': 'password',
'id': FAKE_IPSEC_SITE_CONNECTION2_ID,
@ -89,7 +93,6 @@ FAKE_VPN_SERVICE = {
'status': constants.PENDING_CREATE}]
}
AUTH_ESP = '''esp
# [encryption_algorithm]-[auth_algorithm]-[pfs]
phase2alg=aes128-sha1;modp1536'''
@ -133,26 +136,34 @@ OPENSWAN_CONNECTION_DETAILS = '''# rightsubnet=networkA/netmaskA, networkB/netma
# lifebytes=100000 if lifetime_units=kilobytes (IKEv2 only)
'''
IPV4_NEXT_HOP = '''# NOTE: a default route is required for %defaultroute to work...
leftnexthop=%defaultroute
rightnexthop=%defaultroute'''
IPV6_NEXT_HOP = '''# To recognize the given IP addresses in this config
# as IPv6 addresses by pluto whack. Default is ipv4
connaddrfamily=ipv6
# openswan can't process defaultroute for ipv6.
# Assign gateway address as leftnexthop
leftnexthop=%s
# rightnexthop is not mandatory for ipsec, so no need in ipv6.'''
EXPECTED_OPENSWAN_CONF = """
# Configuration for myvpn
config setup
nat_traversal=yes
conn %(default_id)s
conn %%default
ikelifetime=480m
keylife=60m
keyingtries=%%forever
conn %(conn1_id)s
# NOTE: a default route is required for %%defaultroute to work...
leftnexthop=%%defaultroute
rightnexthop=%%defaultroute
left=50.0.0.4
leftid=50.0.0.4
%(next_hop)s
left=%(left)s
leftid=%(left)s
auto=start
# NOTE:REQUIRED
# [subnet]
leftsubnet=10.0.0.0/24
# leftsubnet=networkA/netmaskA, networkB/netmaskB (IKEv2 only)
leftsubnet%(local_cidrs1)s
# [updown]
# What "updown" script to run to adjust routing and/or firewalling when
# the status of the connection changes (default "ipsec _updown").
@ -162,22 +173,19 @@ conn %(conn1_id)s
# ipsec_site_connections
######################
# [peer_address]
right=30.0.0.5
right=%(right1)s
# [peer_id]
rightid=30.0.0.5
rightid=%(right1)s
# [peer_cidrs]
rightsubnets={ 20.0.0.0/24 30.0.0.0/24 }
rightsubnets={ %(peer_cidrs1)s }
%(conn_details)sconn %(conn2_id)s
# NOTE: a default route is required for %%defaultroute to work...
leftnexthop=%%defaultroute
rightnexthop=%%defaultroute
left=50.0.0.4
leftid=50.0.0.4
%(next_hop)s
left=%(left)s
leftid=%(left)s
auto=start
# NOTE:REQUIRED
# [subnet]
leftsubnet=10.0.0.0/24
# leftsubnet=networkA/netmaskA, networkB/netmaskB (IKEv2 only)
leftsubnet%(local_cidrs2)s
# [updown]
# What "updown" script to run to adjust routing and/or firewalling when
# the status of the connection changes (default "ipsec _updown").
@ -187,24 +195,24 @@ conn %(conn1_id)s
# ipsec_site_connections
######################
# [peer_address]
right=50.0.0.5
right=%(right2)s
# [peer_id]
rightid=50.0.0.5
rightid=%(right2)s
# [peer_cidrs]
rightsubnets={ 40.0.0.0/24 50.0.0.0/24 }
rightsubnets={ %(peer_cidrs2)s }
%(conn_details)s
"""
EXPECTED_IPSEC_OPENSWAN_SECRET_CONF = '''
# Configuration for myvpn
50.0.0.4 30.0.0.5 : PSK "password"
50.0.0.4 50.0.0.5 : PSK "password"'''
60.0.0.4 60.0.0.5 : PSK "password"
60.0.0.4 60.0.0.6 : PSK "password"'''
EXPECTED_IPSEC_STRONGSWAN_CONF = '''
# Configuration for myvpn
config setup
conn %(default_id)s
conn %%default
ikelifetime=60m
keylife=20m
rekeymargin=3m
@ -214,28 +222,26 @@ conn %(default_id)s
conn %(conn1_id)s
keyexchange=ikev1
left=50.0.0.4
leftsubnet=10.0.0.0/24
leftid=50.0.0.4
left=%(left)s
leftsubnet=%(local_cidrs1)s
leftid=%(left)s
leftfirewall=yes
right=30.0.0.5
rightsubnet=20.0.0.0/24,30.0.0.0/24
rightid=30.0.0.5
right=%(right1)s
rightsubnet=%(peer_cidrs1)s
rightid=%(right1)s
auto=route
conn %(conn2_id)s
keyexchange=ikev1
left=50.0.0.4
leftsubnet=10.0.0.0/24
leftid=50.0.0.4
left=%(left)s
leftsubnet=%(local_cidrs2)s
leftid=%(left)s
leftfirewall=yes
right=50.0.0.5
rightsubnet=40.0.0.0/24,50.0.0.0/24
rightid=50.0.0.5
right=%(right2)s
rightsubnet=%(peer_cidrs2)s
rightid=%(right2)s
auto=route
''' % {'default_id': '%default',
'conn1_id': FAKE_IPSEC_SITE_CONNECTION1_ID,
'conn2_id': FAKE_IPSEC_SITE_CONNECTION2_ID}
'''
EXPECTED_STRONGSWAN_DEFAULT_CONF = '''
charon {
@ -250,9 +256,9 @@ include strongswan.d/*.conf
EXPECTED_IPSEC_STRONGSWAN_SECRET_CONF = '''
# Configuration for myvpn
50.0.0.4 30.0.0.5 : PSK "password"
60.0.0.4 60.0.0.5 : PSK "password"
50.0.0.4 50.0.0.5 : PSK "password"
60.0.0.4 60.0.0.6 : PSK "password"
'''
ACTIVE_STATUS = "%(conn_id)s{1}: INSTALLED, TUNNEL" % {'conn_id':
@ -264,7 +270,8 @@ NOT_RUNNING_STATUS = "Command: ['ipsec', 'status'] Exit code: 3 Stdout:"
class BaseIPsecDeviceDriver(base.BaseTestCase):
def setUp(self, driver=openswan_ipsec.OpenSwanDriver,
ipsec_process=openswan_ipsec.OpenSwanProcess):
ipsec_process=openswan_ipsec.OpenSwanProcess,
vpnservice=FAKE_VPN_SERVICE):
super(BaseIPsecDeviceDriver, self).setUp()
for klass in [
'neutron.common.rpc.create_connection',
@ -285,7 +292,7 @@ class BaseIPsecDeviceDriver(base.BaseTestCase):
'interface_driver': mock.sentinel.interface_driver}
self.iptables = mock.Mock()
self.apply_mock = mock.Mock()
self.vpnservice = copy.deepcopy(FAKE_VPN_SERVICE)
self.vpnservice = copy.deepcopy(vpnservice)
@staticmethod
def generate_diff(a, b):
@ -296,6 +303,65 @@ class BaseIPsecDeviceDriver(base.BaseTestCase):
tofile="actual")
return diff
def modify_config_for_test(self, overrides):
"""Revise service/connection settings to test variations.
Must update service, so that dialect mappings occur for any changes
that are made.
"""
ipsec_auth_protocol = overrides.get('ipsec_auth')
if ipsec_auth_protocol:
auth_proto = {'transform_protocol': ipsec_auth_protocol}
for conn in self.vpnservice['ipsec_site_connections']:
conn['ipsecpolicy'].update(auth_proto)
local_cidrs = overrides.get('local_cidrs')
if local_cidrs:
for i, conn in enumerate(
self.vpnservice['ipsec_site_connections']):
conn['local_cidrs'] = local_cidrs[i]
local_ip_version = overrides.get('local_ip_vers', 4)
for conn in self.vpnservice['ipsec_site_connections']:
conn['local_ip_vers'] = local_ip_version
peer_cidrs = overrides.get('peer_cidrs')
if peer_cidrs:
for i, conn in enumerate(
self.vpnservice['ipsec_site_connections']):
conn['peer_cidrs'] = peer_cidrs[i]
peers = overrides.get('peers')
if peers:
for i, conn in enumerate(
self.vpnservice['ipsec_site_connections']):
conn['peer_id'] = peers[i]
conn['peer_address'] = peers[i]
local_ip = overrides.get('local')
if local_ip:
for conn in self.vpnservice['ipsec_site_connections']:
conn['external_ip'] = local_ip
def check_config_file(self, expected, actual):
expected = expected.strip()
actual = actual.strip()
res_diff = self.generate_diff(expected, actual)
self.assertEqual(expected, actual, message=''.join(res_diff))
def _test_ipsec_connection_config(self, info):
"""Check config file string for service/connection.
Calls test specific method to create (and override as needed) the
expected config file string, generates the config using the test's
IPSec template, and then compares the results.
"""
expected = self.build_ipsec_expected_config_for_test(info)
actual = self.process._gen_config_content(self.ipsec_template,
self.vpnservice)
self.check_config_file(expected, actual)
class IPSecDeviceLegacy(BaseIPsecDeviceDriver):
@ -332,7 +398,7 @@ class IPSecDeviceLegacy(BaseIPsecDeviceDriver):
self.driver.processes = {
FAKE_ROUTER_ID: process}
self.driver.create_router(self.router)
self._test_add_nat_rule_helper()
self._test_add_nat_rule()
process.enable.assert_called_once_with()
def test_destroy_router(self):
@ -345,7 +411,7 @@ class IPSecDeviceLegacy(BaseIPsecDeviceDriver):
process.disable.assert_called_once_with()
self.assertNotIn(process_id, self.driver.processes)
def _test_add_nat_rule_helper(self):
def _test_add_nat_rule(self):
self.router.iptables_manager.ipv4['nat'].assert_has_calls([
mock.call.add_rule(
'POSTROUTING',
@ -359,12 +425,57 @@ class IPSecDeviceLegacy(BaseIPsecDeviceDriver):
top=True),
mock.call.add_rule(
'POSTROUTING',
'-s 10.0.0.0/24 -d 40.0.0.0/24 -m policy '
'-s 11.0.0.0/24 -d 40.0.0.0/24 -m policy '
'--dir out --pol ipsec -j ACCEPT ',
top=True),
mock.call.add_rule(
'POSTROUTING',
'-s 10.0.0.0/24 -d 50.0.0.0/24 -m policy '
'-s 11.0.0.0/24 -d 50.0.0.0/24 -m policy '
'--dir out --pol ipsec -j ACCEPT ',
top=True)
])
self.router.iptables_manager.apply.assert_called_once_with()
def _test_add_nat_rule_with_multiple_locals(self):
self.router.iptables_manager.ipv4['nat'].assert_has_calls([
mock.call.add_rule(
'POSTROUTING',
'-s 10.0.0.0/24 -d 20.0.0.0/24 -m policy '
'--dir out --pol ipsec -j ACCEPT ',
top=True),
mock.call.add_rule(
'POSTROUTING',
'-s 10.0.0.0/24 -d 30.0.0.0/24 -m policy '
'--dir out --pol ipsec -j ACCEPT ',
top=True),
mock.call.add_rule(
'POSTROUTING',
'-s 11.0.0.0/24 -d 20.0.0.0/24 -m policy '
'--dir out --pol ipsec -j ACCEPT ',
top=True),
mock.call.add_rule(
'POSTROUTING',
'-s 11.0.0.0/24 -d 30.0.0.0/24 -m policy '
'--dir out --pol ipsec -j ACCEPT ',
top=True),
mock.call.add_rule(
'POSTROUTING',
'-s 12.0.0.0/24 -d 40.0.0.0/24 -m policy '
'--dir out --pol ipsec -j ACCEPT ',
top=True),
mock.call.add_rule(
'POSTROUTING',
'-s 12.0.0.0/24 -d 50.0.0.0/24 -m policy '
'--dir out --pol ipsec -j ACCEPT ',
top=True),
mock.call.add_rule(
'POSTROUTING',
'-s 13.0.0.0/24 -d 40.0.0.0/24 -m policy '
'--dir out --pol ipsec -j ACCEPT ',
top=True),
mock.call.add_rule(
'POSTROUTING',
'-s 13.0.0.0/24 -d 50.0.0.0/24 -m policy '
'--dir out --pol ipsec -j ACCEPT ',
top=True)
])
@ -395,9 +506,17 @@ class IPSecDeviceLegacy(BaseIPsecDeviceDriver):
with mock.patch.object(self.driver, 'ensure_process') as ensure_p:
ensure_p.side_effect = self.fake_ensure_process
self.driver._sync_vpn_processes([new_vpnservice], router_id)
self._test_add_nat_rule_helper()
self._test_add_nat_rule()
self.driver.processes[router_id].update.assert_called_once_with()
def test_add_nat_rules_with_multiple_local_subnets(self):
"""Ensure that add nat rule combinations are correct."""
overrides = {'local_cidrs': [['10.0.0.0/24', '11.0.0.0/24'],
['12.0.0.0/24', '13.0.0.0/24']]}
self.modify_config_for_test(overrides)
self.driver._update_nat(self.vpnservice, self.driver.add_nat_rule)
self._test_add_nat_rule_with_multiple_locals()
def test__sync_vpn_processes_router_with_no_vpn(self):
"""Test _sync_vpn_processes with a router not hosting vpnservice.
@ -439,7 +558,7 @@ class IPSecDeviceLegacy(BaseIPsecDeviceDriver):
with mock.patch.object(self.driver, 'ensure_process') as ensure_p:
ensure_p.side_effect = self.fake_ensure_process
self.driver._sync_vpn_processes([self.vpnservice], [router_id])
self._test_add_nat_rule_helper()
self._test_add_nat_rule()
self.driver.processes[router_id].update.assert_called_once_with()
def test_delete_vpn_processes(self):
@ -497,9 +616,8 @@ class IPSecDeviceLegacy(BaseIPsecDeviceDriver):
ensure_process.side_effect = self.fake_ensure_process
new_vpn_service = FAKE_VPN_SERVICE
updated_vpn_service = copy.deepcopy(new_vpn_service)
updated_vpn_service['ipsec_site_connections'].append(
{'peer_cidrs': ['60.0.0.0/24',
'70.0.0.0/24']})
updated_vpn_service['ipsec_site_connections'][1].update(
{'peer_cidrs': ['60.0.0.0/24', '70.0.0.0/24']})
context = mock.Mock()
self.driver.process_status_cache = {}
self.driver.agent_rpc.get_vpn_services_on_host.return_value = [
@ -689,6 +807,166 @@ class IPSecDeviceDVR(BaseIPsecDeviceDriver):
'fake_chain', 'fake_rule', top=True)
class TestOpenSwanConfigGeneration(BaseIPsecDeviceDriver):
"""Verify that configuration files are generated correctly.
Besides the normal translation of some settings, when creating the config
file, the generated file can also vary based on the following
special conditions:
- IPv6 versus IPv4
- Multiple left subnets versus a single left subnet
- IPSec policy using AH transform
The tests will focus on these variations.
"""
def setUp(self, driver=openswan_ipsec.OpenSwanDriver,
ipsec_process=openswan_ipsec.OpenSwanProcess):
super(TestOpenSwanConfigGeneration, self).setUp(
driver, ipsec_process, vpnservice=FAKE_VPN_SERVICE)
self.conf.register_opts(openswan_ipsec.openswan_opts, 'openswan')
self.conf.set_override('state_path', '/tmp')
self.ipsec_template = self.conf.openswan.ipsec_config_template
self.process = openswan_ipsec.OpenSwanProcess(self.conf,
'foo-process-id',
self.vpnservice,
mock.ANY)
def build_ipsec_expected_config_for_test(self, info):
"""Modify OpenSwan ipsec expected config files for test variations."""
auth_mode = info.get('ipsec_auth', AUTH_ESP)
conn_details = OPENSWAN_CONNECTION_DETAILS % {'auth_mode': auth_mode}
# Convert local CIDRs into assignment strings. IF more than one,
# pluralize the attribute name and enclose in brackets.
cidrs = info.get('local_cidrs', [['10.0.0.0/24'], ['11.0.0.0/24']])
local_cidrs = []
for cidr in cidrs:
if len(cidr) == 2:
local_cidrs.append("s={ %s }" % ' '.join(cidr))
else:
local_cidrs.append("=%s" % cidr[0])
# Convert peer CIDRs into space separated strings
cidrs = info.get('peer_cidrs', [['20.0.0.0/24', '30.0.0.0/24'],
['40.0.0.0/24', '50.0.0.0/24']])
peer_cidrs = [' '.join(cidr) for cidr in cidrs]
local_ip = info.get('local', '60.0.0.4')
version = info.get('local_ip_vers', 4)
next_hop = IPV4_NEXT_HOP if version == 4 else IPV6_NEXT_HOP % local_ip
peer_ips = info.get('peers', ['60.0.0.5', '60.0.0.6'])
return EXPECTED_OPENSWAN_CONF % {
'next_hop': next_hop,
'local_cidrs1': local_cidrs[0], 'local_cidrs2': local_cidrs[1],
'local_ver': version,
'peer_cidrs1': peer_cidrs[0], 'peer_cidrs2': peer_cidrs[1],
'left': local_ip,
'right1': peer_ips[0], 'right2': peer_ips[1],
'conn1_id': FAKE_IPSEC_SITE_CONNECTION1_ID,
'conn2_id': FAKE_IPSEC_SITE_CONNECTION2_ID,
'conn_details': conn_details}
def test_connections_with_esp_transform_protocol(self):
"""Test config file with IPSec policy using ESP."""
self._test_ipsec_connection_config({})
def test_connections_with_ah_transform_protocol(self):
"""Test config file with IPSec policy using ESP."""
overrides = {'ipsec_auth': 'ah'}
self.modify_config_for_test(overrides)
self.process.update_vpnservice(self.vpnservice)
info = {'ipsec_auth': AUTH_AH}
self._test_ipsec_connection_config(info)
def test_connections_with_multiple_left_subnets(self):
"""Test multiple local subnets.
The configure uses the 'leftsubnets' attribute, instead of the
'leftsubnet' attribute.
"""
overrides = {'local_cidrs': [['10.0.0.0/24', '11.0.0.0/24'],
['12.0.0.0/24', '13.0.0.0/24']]}
self.modify_config_for_test(overrides)
self.process.update_vpnservice(self.vpnservice)
self._test_ipsec_connection_config(overrides)
def test_config_files_with_ipv6_addresses(self):
"""Test creating config files using IPv6 addressing."""
overrides = {'local_cidrs': [['2002:0a00::/48'], ['2002:0b00::/48']],
'local_ip_vers': 6,
'peer_cidrs': [['2002:1400::/48', '2002:1e00::/48'],
['2002:2800::/48', '2002:3200::/48']],
'local': '2002:3c00:0004::',
'peers': ['2002:3c00:0005::', '2002:3c00:0006::']}
self.modify_config_for_test(overrides)
self.process.update_vpnservice(self.vpnservice)
self._test_ipsec_connection_config(overrides)
def test_secrets_config_file(self):
expected = EXPECTED_IPSEC_OPENSWAN_SECRET_CONF
actual = self.process._gen_config_content(
self.conf.openswan.ipsec_secret_template, self.vpnservice)
self.check_config_file(expected, actual)
class IPsecStrongswanConfigGeneration(BaseIPsecDeviceDriver):
def setUp(self, driver=strongswan_ipsec.StrongSwanDriver,
ipsec_process=strongswan_ipsec.StrongSwanProcess):
super(IPsecStrongswanConfigGeneration, self).setUp(
driver, ipsec_process, vpnservice=FAKE_VPN_SERVICE)
self.conf.register_opts(strongswan_ipsec.strongswan_opts,
'strongswan')
self.conf.set_override('state_path', '/tmp')
self.ipsec_template = self.conf.strongswan.ipsec_config_template
self.process = strongswan_ipsec.StrongSwanProcess(self.conf,
'foo-process-id',
self.vpnservice,
mock.ANY)
def build_ipsec_expected_config_for_test(self, info):
cidrs = info.get('local_cidrs', [['10.0.0.0/24'], ['11.0.0.0/24']])
local_cidrs = [','.join(cidr) for cidr in cidrs]
cidrs = info.get('peer_cidrs', [['20.0.0.0/24', '30.0.0.0/24'],
['40.0.0.0/24', '50.0.0.0/24']])
peer_cidrs = [','.join(cidr) for cidr in cidrs]
local_ip = info.get('local', '60.0.0.4')
peer_ips = info.get('peers', ['60.0.0.5', '60.0.0.6'])
return EXPECTED_IPSEC_STRONGSWAN_CONF % {
'local_cidrs1': local_cidrs[0], 'local_cidrs2': local_cidrs[1],
'peer_cidrs1': peer_cidrs[0], 'peer_cidrs2': peer_cidrs[1],
'left': local_ip,
'right1': peer_ips[0], 'right2': peer_ips[1],
'conn1_id': FAKE_IPSEC_SITE_CONNECTION1_ID,
'conn2_id': FAKE_IPSEC_SITE_CONNECTION2_ID}
def test_ipsec_config_file(self):
self._test_ipsec_connection_config({})
def test_ipsec_config_file_for_v6(self):
overrides = {'local_cidrs': [['2002:0a00::/48'], ['2002:0b00::/48']],
'peer_cidrs': [['2002:1400::/48', '2002:1e00::/48'],
['2002:2800::/48', '2002:3200::/48']],
'local': '2002:3c00:0004::',
'peers': ['2002:3c00:0005::', '2002:3c00:0006::']}
self.modify_config_for_test(overrides)
self.process.update_vpnservice(self.vpnservice)
self._test_ipsec_connection_config(overrides)
def test_strongswan_default_config_file(self):
expected = EXPECTED_STRONGSWAN_DEFAULT_CONF
actual = self.process._gen_config_content(
self.conf.strongswan.strongswan_config_template, self.vpnservice)
self.check_config_file(expected, actual)
def test_secrets_config_file(self):
expected = EXPECTED_IPSEC_STRONGSWAN_SECRET_CONF
actual = self.process._gen_config_content(
self.conf.strongswan.ipsec_secret_template, self.vpnservice)
self.check_config_file(expected, actual)
class TestOpenSwanProcess(BaseIPsecDeviceDriver):
_test_timeout = 1
@ -698,7 +976,6 @@ class TestOpenSwanProcess(BaseIPsecDeviceDriver):
def setUp(self, driver=openswan_ipsec.OpenSwanDriver,
ipsec_process=openswan_ipsec.OpenSwanProcess):
super(TestOpenSwanProcess, self).setUp(driver, ipsec_process)
# Insulate tests against changes to configuration defaults.
self.conf.register_opts(openswan_ipsec.openswan_opts,
'openswan')
self.conf.set_override('state_path', '/tmp')
@ -719,41 +996,6 @@ class TestOpenSwanProcess(BaseIPsecDeviceDriver):
self.vpnservice,
mock.ANY)
def _test_config_files_on_create(self, proto, auth_mode):
"""Verify that the content of config files are correct on create."""
auth_proto = {'transform_protocol': proto}
for conn in self.vpnservice['ipsec_site_connections']:
conn['ipsecpolicy'].update(auth_proto)
content = self.process._gen_config_content(
self.conf.openswan.ipsec_config_template,
self.vpnservice)
conn_details = OPENSWAN_CONNECTION_DETAILS % {'auth_mode': auth_mode}
expected_openswan_conf = EXPECTED_OPENSWAN_CONF % {
'default_id': '%default',
'conn1_id': FAKE_IPSEC_SITE_CONNECTION1_ID,
'conn2_id': FAKE_IPSEC_SITE_CONNECTION2_ID,
'conn_details': conn_details}
res_diff = self.generate_diff(expected_openswan_conf.strip(),
content.strip())
self.assertEqual(expected_openswan_conf.strip(),
str(content.strip()), message=''.join(res_diff))
content = self.process._gen_config_content(
self.conf.openswan.ipsec_secret_template,
self.vpnservice)
res_diff = self.generate_diff(
EXPECTED_IPSEC_OPENSWAN_SECRET_CONF.strip(),
content.strip())
self.assertEqual(EXPECTED_IPSEC_OPENSWAN_SECRET_CONF.strip(),
str(content.strip()), message=''.join(res_diff))
def test_config_files_on_create_esp_transform_protocol(self):
self._test_config_files_on_create('esp', AUTH_ESP)
def test_config_files_on_create_ah_transform_protocol(self):
self._test_config_files_on_create('ah', AUTH_AH)
def test__resolve_fqdn(self):
with mock.patch.object(socket, 'getaddrinfo') as mock_getaddr_info:
mock_getaddr_info.return_value = [(2, 1, 6, '',
@ -1065,26 +1307,6 @@ class IPsecStrongswanDeviceDriverLegacy(IPSecDeviceLegacy):
self.driver.agent_rpc.get_vpn_services_on_host.return_value = [
self.vpnservice]
def test_config_files_on_create(self):
"""Verify that the content of config files are correct on create."""
process = self.driver.ensure_process(self.router.router_id,
self.vpnservice)
content = process._gen_config_content(
self.conf.strongswan.ipsec_config_template,
self.vpnservice)
self.assertEqual(EXPECTED_IPSEC_STRONGSWAN_CONF.strip(),
str(content.strip()))
content = process._gen_config_content(
self.conf.strongswan.strongswan_config_template,
self.vpnservice)
self.assertEqual(EXPECTED_STRONGSWAN_DEFAULT_CONF.strip(),
str(content.strip()))
content = process._gen_config_content(
self.conf.strongswan.ipsec_secret_template,
self.vpnservice)
self.assertEqual(EXPECTED_IPSEC_STRONGSWAN_SECRET_CONF.strip(),
str(content.strip()))
def test_status_handling_for_downed_connection(self):
"""Test status handling for downed connection."""
router_id = self.router.router_id

View File

@ -14,24 +14,15 @@
# under the License.
import mock
import socket
from neutron.common import exceptions as nexception
from neutron import context as n_ctx
from neutron.db import l3_db
from neutron.db import servicetype_db as st_db
from neutron.plugins.common import constants as nconstants
from oslo_config import cfg
from oslo_utils import uuidutils
from neutron_vpnaas.extensions import vpnaas
from neutron_vpnaas.services.vpn.common import constants
from neutron_vpnaas.services.vpn import plugin as vpn_plugin
from neutron_vpnaas.services.vpn.service_drivers import ipsec as ipsec_driver
from neutron_vpnaas.services.vpn.service_drivers \
import ipsec_validator as vpn_validator
from neutron_vpnaas.tests import base
_uuid = uuidutils.generate_uuid
FAKE_SERVICE_ID = _uuid()
@ -43,333 +34,12 @@ FAKE_VPN_SERVICE = {
'router_id': FAKE_ROUTER_ID
}
FAKE_HOST = 'fake_host'
FAKE_ROUTER = {l3_db.EXTERNAL_GW_INFO: FAKE_ROUTER_ID}
FAKE_SUBNET_ID = _uuid()
IPV4 = 4
IPV6 = 6
FAKE_CONN_ID = _uuid()
IPSEC_SERVICE_DRIVER = ('neutron_vpnaas.services.vpn.service_drivers.'
'ipsec.IPsecVPNDriver')
class TestValidatorSelection(base.BaseTestCase):
def setUp(self):
super(TestValidatorSelection, self).setUp()
# TODO(armax): remove this if branch as soon as the ServiceTypeManager
# API for adding provider configurations becomes available
if not hasattr(st_db.ServiceTypeManager, 'add_provider_configuration'):
vpnaas_provider = (nconstants.VPN + ':vpnaas:' +
IPSEC_SERVICE_DRIVER + ':default')
cfg.CONF.set_override(
'service_provider', [vpnaas_provider], 'service_providers')
else:
vpnaas_provider = [{
'service_type': nconstants.VPN,
'name': 'vpnaas',
'driver': IPSEC_SERVICE_DRIVER,
'default': True
}]
# override the default service provider
self.service_providers = (
mock.patch.object(st_db.ServiceTypeManager,
'get_service_providers').start())
self.service_providers.return_value = vpnaas_provider
mock.patch('neutron.common.rpc.create_connection').start()
stm = st_db.ServiceTypeManager()
mock.patch('neutron.db.servicetype_db.ServiceTypeManager.get_instance',
return_value=stm).start()
self.vpn_plugin = vpn_plugin.VPNDriverPlugin()
def test_reference_driver_used(self):
self.assertIsInstance(self.vpn_plugin._get_validator(),
vpn_validator.IpsecVpnValidator)
class TestIPsecDriverValidation(base.BaseTestCase):
def setUp(self):
super(TestIPsecDriverValidation, self).setUp()
self.l3_plugin = mock.Mock()
mock.patch(
'neutron.manager.NeutronManager.get_service_plugins',
return_value={nconstants.L3_ROUTER_NAT: self.l3_plugin}).start()
self.core_plugin = mock.Mock()
mock.patch('neutron.manager.NeutronManager.get_plugin',
return_value=self.core_plugin).start()
self.context = n_ctx.Context('some_user', 'some_tenant')
self.service_plugin = mock.Mock()
self.validator = vpn_validator.IpsecVpnValidator(self.service_plugin)
self.router = mock.Mock()
self.router.gw_port = {'fixed_ips': [{'ip_address': '10.0.0.99'}]}
def test_non_public_router_for_vpn_service(self):
"""Failure test of service validate, when router missing ext. I/F."""
self.l3_plugin.get_router.return_value = {} # No external gateway
vpnservice = {'router_id': 123, 'subnet_id': 456}
self.assertRaises(vpnaas.RouterIsNotExternal,
self.validator.validate_vpnservice,
self.context, vpnservice)
def test_subnet_not_connected_for_vpn_service(self):
"""Failure test of service validate, when subnet not on router."""
self.l3_plugin.get_router.return_value = FAKE_ROUTER
self.core_plugin.get_ports.return_value = None
vpnservice = {'router_id': FAKE_ROUTER_ID, 'subnet_id': FAKE_SUBNET_ID}
self.assertRaises(vpnaas.SubnetIsNotConnectedToRouter,
self.validator.validate_vpnservice,
self.context, vpnservice)
def test_defaults_for_ipsec_site_connections_on_create(self):
"""Check that defaults are applied correctly.
MTU has a default and will always be present on create.
However, the DPD settings do not have a default, so
database create method will assign default values for any
missing. In addition, the DPD dict will be flattened
for storage into the database, so we'll do it as part of
assigning defaults.
"""
ipsec_sitecon = {}
self.validator.assign_sensible_ipsec_sitecon_defaults(ipsec_sitecon)
expected = {
'dpd_action': 'hold',
'dpd_timeout': 120,
'dpd_interval': 30
}
self.assertEqual(expected, ipsec_sitecon)
ipsec_sitecon = {'dpd': {'interval': 50}}
self.validator.assign_sensible_ipsec_sitecon_defaults(ipsec_sitecon)
expected = {
'dpd': {'interval': 50},
'dpd_action': 'hold',
'dpd_timeout': 120,
'dpd_interval': 50
}
self.assertEqual(expected, ipsec_sitecon)
def test_resolve_peer_address_with_ipaddress(self):
ipsec_sitecon = {'peer_address': '10.0.0.9'}
self.validator.validate_peer_address = mock.Mock()
self.validator.resolve_peer_address(ipsec_sitecon, self.router)
self.assertEqual('10.0.0.9', ipsec_sitecon['peer_address'])
self.validator.validate_peer_address.assert_called_once_with(
IPV4, self.router)
def test_resolve_peer_address_with_fqdn(self):
with mock.patch.object(socket, 'getaddrinfo') as mock_getaddr_info:
mock_getaddr_info.return_value = [(2, 1, 6, '',
('10.0.0.9', 0))]
ipsec_sitecon = {'peer_address': 'fqdn.peer.addr'}
self.validator.validate_peer_address = mock.Mock()
self.validator.resolve_peer_address(ipsec_sitecon, self.router)
self.assertEqual('10.0.0.9', ipsec_sitecon['peer_address'])
self.validator.validate_peer_address.assert_called_once_with(
IPV4, self.router)
def test_resolve_peer_address_with_invalid_fqdn(self):
with mock.patch.object(socket, 'getaddrinfo') as mock_getaddr_info:
def getaddr_info_failer(*args, **kwargs):
raise socket.gaierror()
mock_getaddr_info.side_effect = getaddr_info_failer
ipsec_sitecon = {'peer_address': 'fqdn.invalid'}
self.assertRaises(vpnaas.VPNPeerAddressNotResolved,
self.validator.resolve_peer_address,
ipsec_sitecon, self.router)
def _validate_peer_address(self, fixed_ips, ip_version,
expected_exception=False):
self.router.id = FAKE_ROUTER_ID
self.router.gw_port = {'fixed_ips': fixed_ips}
try:
self.validator.validate_peer_address(ip_version, self.router)
if expected_exception:
self.fail("No exception raised for invalid peer address")
except vpnaas.ExternalNetworkHasNoSubnet:
if not expected_exception:
self.fail("exception for valid peer address raised")
def test_validate_peer_address(self):
# validate ipv4 peer_address with ipv4 gateway
fixed_ips = [{'ip_address': '10.0.0.99'}]
self._validate_peer_address(fixed_ips, IPV4)
# validate ipv6 peer_address with ipv6 gateway
fixed_ips = [{'ip_address': '2001::1'}]
self._validate_peer_address(fixed_ips, IPV6)
# validate ipv6 peer_address with both ipv4 and ipv6 gateways
fixed_ips = [{'ip_address': '2001::1'}, {'ip_address': '10.0.0.99'}]
self._validate_peer_address(fixed_ips, IPV6)
# validate ipv4 peer_address with both ipv4 and ipv6 gateways
fixed_ips = [{'ip_address': '2001::1'}, {'ip_address': '10.0.0.99'}]
self._validate_peer_address(fixed_ips, IPV4)
# validate ipv4 peer_address with ipv6 gateway
fixed_ips = [{'ip_address': '2001::1'}]
self._validate_peer_address(fixed_ips, IPV4, expected_exception=True)
# validate ipv6 peer_address with ipv4 gateway
fixed_ips = [{'ip_address': '10.0.0.99'}]
self._validate_peer_address(fixed_ips, IPV6, expected_exception=True)
def test_validate_ipsec_policy(self):
ipsec_policy = {'transform_protocol': 'ah-esp'}
self.assertRaises(vpn_validator.IpsecValidationFailure,
self.validator.validate_ipsec_policy,
self.context, ipsec_policy)
def test_defaults_for_ipsec_site_connections_on_update(self):
"""Check that defaults are used for any values not specified."""
ipsec_sitecon = {}
prev_connection = {'dpd_action': 'clear',
'dpd_timeout': 500,
'dpd_interval': 250}
self.validator.assign_sensible_ipsec_sitecon_defaults(ipsec_sitecon,
prev_connection)
expected = {
'dpd_action': 'clear',
'dpd_timeout': 500,
'dpd_interval': 250
}
self.assertEqual(expected, ipsec_sitecon)
ipsec_sitecon = {'dpd': {'timeout': 200}}
prev_connection = {'dpd_action': 'clear',
'dpd_timeout': 500,
'dpd_interval': 100}
self.validator.assign_sensible_ipsec_sitecon_defaults(ipsec_sitecon,
prev_connection)
expected = {
'dpd': {'timeout': 200},
'dpd_action': 'clear',
'dpd_timeout': 200,
'dpd_interval': 100
}
self.assertEqual(expected, ipsec_sitecon)
def test_bad_dpd_settings_on_create(self):
"""Failure tests of DPD settings for IPSec conn during create."""
ipsec_sitecon = {'mtu': 1500, 'dpd_action': 'hold',
'dpd_interval': 100, 'dpd_timeout': 100}
self.assertRaises(vpnaas.IPsecSiteConnectionDpdIntervalValueError,
self.validator.validate_ipsec_site_connection,
self.context, ipsec_sitecon, IPV4)
ipsec_sitecon = {'mtu': 1500, 'dpd_action': 'hold',
'dpd_interval': 100, 'dpd_timeout': 99}
self.assertRaises(vpnaas.IPsecSiteConnectionDpdIntervalValueError,
self.validator.validate_ipsec_site_connection,
self.context, ipsec_sitecon, IPV4)
def test_bad_dpd_settings_on_update(self):
"""Failure tests of DPD settings for IPSec conn. during update.
Note: On an update, the user may specify only some of the DPD settings.
Previous values will be assigned for any missing items, so by the
time the validation occurs, all items will be available for checking.
The MTU may not be provided, during validation and will be ignored,
if that is the case.
"""
prev_connection = {'mtu': 2000,
'dpd_action': 'hold',
'dpd_interval': 100,
'dpd_timeout': 120}
ipsec_sitecon = {'dpd': {'interval': 120}}
self.validator.assign_sensible_ipsec_sitecon_defaults(ipsec_sitecon,
prev_connection)
self.assertRaises(vpnaas.IPsecSiteConnectionDpdIntervalValueError,
self.validator.validate_ipsec_site_connection,
self.context, ipsec_sitecon, IPV4)
prev_connection = {'mtu': 2000,
'dpd_action': 'hold',
'dpd_interval': 100,
'dpd_timeout': 120}
ipsec_sitecon = {'dpd': {'timeout': 99}}
self.validator.assign_sensible_ipsec_sitecon_defaults(ipsec_sitecon,
prev_connection)
self.assertRaises(vpnaas.IPsecSiteConnectionDpdIntervalValueError,
self.validator.validate_ipsec_site_connection,
self.context, ipsec_sitecon, IPV4)
def test_bad_mtu_for_ipsec_connection(self):
"""Failure test of invalid MTU values for IPSec conn create/update."""
ip_version_limits = vpn_validator.IpsecVpnValidator.IP_MIN_MTU
for version, limit in ip_version_limits.items():
ipsec_sitecon = {'mtu': limit - 1,
'dpd_action': 'hold',
'dpd_interval': 100,
'dpd_timeout': 120}
self.assertRaises(
vpnaas.IPsecSiteConnectionMtuError,
self.validator.validate_ipsec_site_connection,
self.context, ipsec_sitecon, version)
def test_endpoints_all_cidrs_in_endpoint_group(self):
"""All endpoints in the endpoint group are valid CIDRs."""
endpoint_group = {'type': constants.CIDR_ENDPOINT,
'endpoints': ['10.10.10.0/24', '20.20.20.0/24']}
try:
self.validator.validate_endpoint_group(self.context,
endpoint_group)
except Exception:
self.fail("All CIDRs in endpoint_group should be valid")
def test_endpoints_all_subnets_in_endpoint_group(self):
"""All endpoints in the endpoint group are valid subnets."""
endpoint_group = {'type': constants.SUBNET_ENDPOINT,
'endpoints': [_uuid(), _uuid()]}
try:
self.validator.validate_endpoint_group(self.context,
endpoint_group)
except Exception:
self.fail("All subnets in endpoint_group should be valid")
def test_mixed_endpoint_types_in_endpoint_group(self):
"""Fail when mixing types of endpoints in endpoint group."""
endpoint_group = {'type': constants.CIDR_ENDPOINT,
'endpoints': ['10.10.10.0/24', _uuid()]}
self.assertRaises(vpnaas.InvalidEndpointInEndpointGroup,
self.validator.validate_endpoint_group,
self.context, endpoint_group)
endpoint_group = {'type': constants.SUBNET_ENDPOINT,
'endpoints': [_uuid(), '10.10.10.0/24']}
self.assertRaises(vpnaas.InvalidEndpointInEndpointGroup,
self.validator.validate_endpoint_group,
self.context, endpoint_group)
def test_missing_endpoints_for_endpoint_group(self):
endpoint_group = {'type': constants.CIDR_ENDPOINT,
'endpoints': []}
self.assertRaises(vpnaas.MissingEndpointForEndpointGroup,
self.validator.validate_endpoint_group,
self.context, endpoint_group)
def test_fail_bad_cidr_in_endpoint_group(self):
"""Testing catches bad CIDR.
Just check one case, as CIDR validator used has good test coverage.
"""
endpoint_group = {'type': constants.CIDR_ENDPOINT,
'endpoints': ['10.10.10.10/24', '20.20.20.1']}
self.assertRaises(vpnaas.InvalidEndpointInEndpointGroup,
self.validator.validate_endpoint_group,
self.context, endpoint_group)
def test_unknown_subnet_in_endpoint_group(self):
subnet_id = _uuid()
self.core_plugin.get_subnet.side_effect = nexception.SubnetNotFound(
subnet_id=subnet_id)
endpoint_group = {'type': constants.SUBNET_ENDPOINT,
'endpoints': [subnet_id]}
self.assertRaises(vpnaas.NonExistingSubnetInEndpointGroup,
self.validator.validate_endpoint_group,
self.context, endpoint_group)
class FakeSqlQueryObject(dict):
"""To fake SqlAlchemy query object and access keys as attributes."""
@ -442,72 +112,244 @@ class TestIPsecDriver(base.BaseTestCase):
[FAKE_VPN_SERVICE],
{'router': {'id': FAKE_VPN_SERVICE['router_id']}})
def _test_make_vpnservice_dict_helper(self, peer_id, expected_peer_id):
fake_subnet = FakeSqlQueryObject(id='foo-subnet-id',
name='foo-subnet',
network_id='foo-net-id')
def prepare_dummy_query_objects(self, info):
"""Create fake query objects to test dict creation for sync oper."""
external_ip = '10.0.0.99'
peer_address = '10.0.0.2'
peer_endpoints = info.get('peer_endpoints', [])
local_endpoints = info.get('local_endpoints', [])
peer_cidrs = info.get('peer_cidrs', ['40.4.0.0/24', '50.5.0.0/24'])
peer_id = info.get('peer_id', '30.30.0.0')
fake_ikepolicy = FakeSqlQueryObject(id='foo-ike', name='ike-name')
fake_ipsecpolicy = FakeSqlQueryObject(id='foo-ipsec')
fake_peer_address = '10.0.0.2'
fake_ipsec_conn = FakeSqlQueryObject(peer_id=peer_id,
peer_address=fake_peer_address,
fake_peer_cidrs_list = [
FakeSqlQueryObject(cidr=cidr, ipsec_site_connection_id='conn-id')
for cidr in peer_cidrs]
peer_epg_id = 'peer-epg-id' if peer_endpoints else None
local_epg_id = 'local-epg-id' if local_endpoints else None
fake_ipsec_conn = FakeSqlQueryObject(id='conn-id',
peer_id=peer_id,
peer_address=peer_address,
ikepolicy=fake_ikepolicy,
ipsecpolicy=fake_ipsecpolicy,
peer_cidrs=[])
fake_external_ip = '10.0.0.99'
fake_gw_port = {'fixed_ips': [{'ip_address': fake_external_ip}]}
peer_ep_group_id=peer_epg_id,
local_ep_group_id=local_epg_id,
peer_cidrs=fake_peer_cidrs_list)
if peer_endpoints:
fake_peer_ep_group = FakeSqlQueryObject(id=peer_epg_id)
fake_peer_ep_group.endpoints = [
FakeSqlQueryObject(endpoint=ep,
endpoint_group_id=peer_epg_id)
for ep in peer_endpoints]
fake_ipsec_conn.peer_ep_group = fake_peer_ep_group
if local_endpoints:
fake_local_ep_group = FakeSqlQueryObject(id=local_epg_id)
fake_local_ep_group.endpoints = [
FakeSqlQueryObject(endpoint=ep,
endpoint_group_id=local_epg_id)
for ep in local_endpoints]
fake_ipsec_conn.local_ep_group = fake_local_ep_group
subnet_id = None
else:
subnet_id = 'foo-subnet-id'
fake_gw_port = {'fixed_ips': [{'ip_address': external_ip}]}
fake_router = FakeSqlQueryObject(gw_port=fake_gw_port)
fake_vpnservice = FakeSqlQueryObject(id='foo-vpn-id', name='foo-vpn',
description='foo-vpn-service',
admin_state_up=True,
status='active',
external_v4_ip=fake_external_ip,
external_v4_ip=external_ip,
external_v6_ip=None,
subnet_id='foo-subnet-id',
subnet_id=subnet_id,
router_id='foo-router-id')
fake_vpnservice.subnet = fake_subnet
if local_endpoints:
fake_vpnservice.subnet = None
else:
fake_subnet = FakeSqlQueryObject(id=subnet_id,
name='foo-subnet',
cidr='9.0.0.0/16',
network_id='foo-net-id')
fake_vpnservice.subnet = fake_subnet
fake_vpnservice.router = fake_router
fake_vpnservice.ipsec_site_connections = [fake_ipsec_conn]
return fake_vpnservice
expected_vpnservice_dict = {'name': 'foo-vpn',
'id': 'foo-vpn-id',
'description': 'foo-vpn-service',
'admin_state_up': True,
'status': 'active',
'external_v4_ip': fake_external_ip,
'external_v6_ip': None,
'subnet_id': 'foo-subnet-id',
'router_id': 'foo-router-id',
'subnet': {'id': 'foo-subnet-id',
'name': 'foo-subnet',
'network_id': 'foo-net-id'},
'external_ip': fake_external_ip,
'ipsec_site_connections': [
{'peer_id': expected_peer_id,
'external_ip': fake_external_ip,
'peer_address': fake_peer_address,
'ikepolicy': {'id': 'foo-ike',
'name': 'ike-name'},
'ipsecpolicy': {'id': 'foo-ipsec'},
'peer_cidrs': []}]}
def build_expected_dict(self, info):
"""Create the expected dict used in sync operations.
actual_vpnservice_dict = self.driver.make_vpnservice_dict(
fake_vpnservice)
The default is to use non-endpoint groups, where the peer CIDRs come
from the peer_cidrs arguments, the local CIDRs come from the (sole)
subnet CIDR, and there is subnet info. Tests will customize the peer
ID and peer CIDRs.
"""
self.assertEqual(expected_vpnservice_dict, actual_vpnservice_dict)
external_ip = '10.0.0.99'
peer_id = info.get('peer_id', '30.30.0.0')
peer_cidrs = info.get('peer_cidrs', ['40.4.0.0/24', '50.5.0.0/24'])
return {'name': 'foo-vpn',
'id': 'foo-vpn-id',
'description': 'foo-vpn-service',
'admin_state_up': True,
'status': 'active',
'external_v4_ip': external_ip,
'external_v6_ip': None,
'router_id': 'foo-router-id',
'subnet': {'cidr': '9.0.0.0/16',
'id': 'foo-subnet-id',
'name': 'foo-subnet',
'network_id': 'foo-net-id'},
'subnet_id': 'foo-subnet-id',
'external_ip': external_ip,
'ipsec_site_connections': [
{'id': 'conn-id',
'peer_id': peer_id,
'external_ip': external_ip,
'peer_address': '10.0.0.2',
'ikepolicy': {'id': 'foo-ike',
'name': 'ike-name'},
'ipsecpolicy': {'id': 'foo-ipsec'},
'peer_ep_group_id': None,
'local_ep_group_id': None,
'peer_cidrs': peer_cidrs,
'local_cidrs': ['9.0.0.0/16'],
'local_ip_vers': 4}
]}
def build_expected_dict_for_endpoints(self, info):
"""Create the expected dict used in sync operations for endpoints.
The local and peer CIDRs come from the endpoint groups (with the
local CIDR translated from the corresponding subnets specified).
Tests will customize CIDRs, and the subnet, which is needed for
backward compatibility with agents, during rolling upgrades.
"""
external_ip = '10.0.0.99'
peer_id = '30.30.0.0'
return {'name': 'foo-vpn',
'id': 'foo-vpn-id',
'description': 'foo-vpn-service',
'admin_state_up': True,
'status': 'active',
'external_v4_ip': external_ip,
'external_v6_ip': None,
'router_id': 'foo-router-id',
'subnet': None,
'subnet_id': None,
'external_ip': external_ip,
'ipsec_site_connections': [
{'id': 'conn-id',
'peer_id': peer_id,
'external_ip': external_ip,
'peer_address': '10.0.0.2',
'ikepolicy': {'id': 'foo-ike',
'name': 'ike-name'},
'ipsecpolicy': {'id': 'foo-ipsec'},
'peer_ep_group_id': 'peer-epg-id',
'local_ep_group_id': 'local-epg-id',
'peer_cidrs': info['peers'],
'local_cidrs': info['locals'],
'local_ip_vers': info['vers']}
]}
def test_make_vpnservice_dict_peer_id_is_ipaddr(self):
"""Peer ID as IP should be copied as-is, when creating dict."""
subnet_cidr_map = {}
peer_id_as_ip = {'peer_id': '10.0.0.2'}
fake_service = self.prepare_dummy_query_objects(peer_id_as_ip)
expected_dict = self.build_expected_dict(peer_id_as_ip)
actual_dict = self.driver.make_vpnservice_dict(fake_service,
subnet_cidr_map)
self.assertEqual(expected_dict, actual_dict)
# make sure that ipsec_site_conn peer_id is not updated by
# _make_vpnservice_dict (bug #1423244)
self.assertEqual(peer_id,
fake_vpnservice.ipsec_site_connections[0].peer_id)
def test_make_vpnservice_dict_peer_id_is_ipaddr(self):
self._test_make_vpnservice_dict_helper('10.0.0.2', '10.0.0.2')
self.assertEqual(peer_id_as_ip['peer_id'],
fake_service.ipsec_site_connections[0].peer_id)
def test_make_vpnservice_dict_peer_id_is_string(self):
self._test_make_vpnservice_dict_helper('foo.peer.id', '@foo.peer.id')
"""Peer ID as string should have '@' prepended, when creating dict."""
subnet_cidr_map = {}
peer_id_as_name = {'peer_id': 'foo.peer.id'}
fake_service = self.prepare_dummy_query_objects(peer_id_as_name)
expected_peer_id = {'peer_id': '@foo.peer.id'}
expected_dict = self.build_expected_dict(expected_peer_id)
actual_dict = self.driver.make_vpnservice_dict(fake_service,
subnet_cidr_map)
self.assertEqual(expected_dict, actual_dict)
# make sure that ipsec_site_conn peer_id is not updated by
# _make_vpnservice_dict (bug #1423244)
self.assertEqual(peer_id_as_name['peer_id'],
fake_service.ipsec_site_connections[0].peer_id)
def test_make_vpnservice_dict_peer_cidrs_from_peer_cidr_table(self):
"""Peer CIDRs list populated from peer_cidr table.
User provides peer CIDRs as parameters to IPSec site-to-site
connection API, and they are stored in the peercidrs table.
"""
subnet_cidr_map = {}
peer_cidrs = {'peer_cidrs': ['80.0.0.0/24', '90.0.0.0/24']}
fake_service = self.prepare_dummy_query_objects(peer_cidrs)
expected_dict = self.build_expected_dict(peer_cidrs)
actual_dict = self.driver.make_vpnservice_dict(fake_service,
subnet_cidr_map)
self.assertEqual(expected_dict, actual_dict)
def test_make_vpnservice_dict_cidrs_from_endpoints(self):
"""CIDRs list populated from local and peer endpoints.
User provides peer and local endpoint group IDs in the IPSec
site-to-site connection API. The endpoint groups contains peer
CIDRs and local subnets (which will be mapped to CIDRs).
"""
# Cannot have peer CIDRs specified, when using endpoint group
subnet_cidr_map = {'local-sn1': '5.0.0.0/16',
'local-sn2': '5.1.0.0/16'}
endpoint_groups = {'peer_cidrs': [],
'peer_endpoints': ['80.0.0.0/24', '90.0.0.0/24'],
'local_endpoints': ['local-sn1', 'local-sn2']}
expected_cidrs = {'peers': ['80.0.0.0/24', '90.0.0.0/24'],
'locals': ['5.0.0.0/16', '5.1.0.0/16'],
'vers': 4}
fake_service = self.prepare_dummy_query_objects(endpoint_groups)
expected_dict = self.build_expected_dict_for_endpoints(expected_cidrs)
expected_dict['subnet'] = {'cidr': '5.0.0.0/16'}
actual_dict = self.driver.make_vpnservice_dict(fake_service,
subnet_cidr_map)
self.assertEqual(expected_dict, actual_dict)
def test_make_vpnservice_dict_v6_cidrs_from_endpoints(self):
"""IPv6 CIDRs list populated from local and peer endpoints."""
# Cannot have peer CIDRs specified, when using endpoint group
subnet_cidr_map = {'local-sn1': '2002:0a00:0000::/48',
'local-sn2': '2002:1400:0000::/48'}
endpoint_groups = {'peer_cidrs': [],
'peer_endpoints': ['2002:5000:0000::/48',
'2002:5a00:0000::/48'],
'local_endpoints': ['local-sn1', 'local-sn2']}
expected_cidrs = {'peers': ['2002:5000:0000::/48',
'2002:5a00:0000::/48'],
'locals': ['2002:0a00:0000::/48',
'2002:1400:0000::/48'],
'vers': 6}
fake_service = self.prepare_dummy_query_objects(endpoint_groups)
expected_dict = self.build_expected_dict_for_endpoints(expected_cidrs)
expected_dict['subnet'] = {'cidr': '2002:0a00:0000::/48'}
actual_dict = self.driver.make_vpnservice_dict(fake_service,
subnet_cidr_map)
self.assertEqual(expected_dict, actual_dict)
def test_get_external_ip_based_on_ipv4_peer(self):
vpnservice = mock.Mock()