# Copyright 2014 Cisco Systems, Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sqlalchemy as sa from sqlalchemy.orm import exc as sql_exc from neutron.common import exceptions from neutron.db import model_base from neutron.db import models_v2 from neutron.db.vpn import vpn_db from neutron.openstack.common.db import exception as db_exc from neutron.openstack.common import log as logging LOG = logging.getLogger(__name__) # Note: Artificially limit these to reduce mapping table size and performance # Tunnel can be 0..7FFFFFFF, IKE policy can be 1..10000, IPSec policy can be # 1..31 characters long. MAX_CSR_TUNNELS = 10000 MAX_CSR_IKE_POLICIES = 2000 MAX_CSR_IPSEC_POLICIES = 2000 TUNNEL = 'Tunnel' IKE_POLICY = 'IKE Policy' IPSEC_POLICY = 'IPSec Policy' MAPPING_LIMITS = {TUNNEL: (0, MAX_CSR_TUNNELS), IKE_POLICY: (1, MAX_CSR_IKE_POLICIES), IPSEC_POLICY: (1, MAX_CSR_IPSEC_POLICIES)} class CsrInternalError(exceptions.NeutronException): message = _("Fatal - %(reason)s") class IdentifierMap(model_base.BASEV2, models_v2.HasTenant): """Maps OpenStack IDs to compatible numbers for Cisco CSR.""" __tablename__ = 'cisco_csr_identifier_map' ipsec_site_conn_id = sa.Column(sa.String(64), sa.ForeignKey('ipsec_site_connections.id', ondelete="CASCADE"), primary_key=True) csr_tunnel_id = sa.Column(sa.Integer, nullable=False) csr_ike_policy_id = sa.Column(sa.Integer, nullable=False) csr_ipsec_policy_id = sa.Column(sa.Integer, nullable=False) def get_next_available_id(session, table_field, id_type): """Find first unused id for the specified field in IdentifierMap table. As entries are removed, find the first "hole" and return that as the next available ID. To improve performance, artificially limit the number of entries to a smaller range. Currently, these IDs are globally unique. Could enhance in the future to be unique per router (CSR). """ min_value = MAPPING_LIMITS[id_type][0] max_value = MAPPING_LIMITS[id_type][1] rows = session.query(table_field).order_by(table_field) used_ids = set([row[0] for row in rows]) all_ids = set(range(min_value, max_value + min_value)) available_ids = all_ids - used_ids if not available_ids: msg = _("No available Cisco CSR %(type)s IDs from " "%(min)d..%(max)d") % {'type': id_type, 'min': min_value, 'max': max_value} LOG.error(msg) raise IndexError(msg) return available_ids.pop() def get_next_available_tunnel_id(session): """Find first available tunnel ID from 0..MAX_CSR_TUNNELS-1.""" return get_next_available_id(session, IdentifierMap.csr_tunnel_id, TUNNEL) def get_next_available_ike_policy_id(session): """Find first available IKE Policy ID from 1..MAX_CSR_IKE_POLICIES.""" return get_next_available_id(session, IdentifierMap.csr_ike_policy_id, IKE_POLICY) def get_next_available_ipsec_policy_id(session): """Find first available IPSec Policy ID from 1..MAX_CSR_IKE_POLICIES.""" return get_next_available_id(session, IdentifierMap.csr_ipsec_policy_id, IPSEC_POLICY) def find_conn_with_policy(policy_field, policy_id, conn_id, session): """Return ID of another conneciton (if any) that uses same policy ID.""" qry = session.query(vpn_db.IPsecSiteConnection.id) match = qry.filter(policy_field == policy_id, vpn_db.IPsecSiteConnection.id != conn_id).first() if match: return match[0] def find_connection_using_ike_policy(ike_policy_id, conn_id, session): """Return ID of another connection that uses same IKE policy ID.""" return find_conn_with_policy(vpn_db.IPsecSiteConnection.ikepolicy_id, ike_policy_id, conn_id, session) def find_connection_using_ipsec_policy(ipsec_policy_id, conn_id, session): """Return ID of another connection that uses same IPSec policy ID.""" return find_conn_with_policy(vpn_db.IPsecSiteConnection.ipsecpolicy_id, ipsec_policy_id, conn_id, session) def lookup_policy(policy_type, policy_field, conn_id, session): """Obtain specified policy's mapping from other connection.""" try: return session.query(policy_field).filter_by( ipsec_site_conn_id=conn_id).one()[0] except sql_exc.NoResultFound: msg = _("Database inconsistency between IPSec connection and " "Cisco CSR mapping table (%s)") % policy_type raise CsrInternalError(reason=msg) def lookup_ike_policy_id_for(conn_id, session): """Obtain existing Cisco CSR IKE policy ID from another connection.""" return lookup_policy(IKE_POLICY, IdentifierMap.csr_ike_policy_id, conn_id, session) def lookup_ipsec_policy_id_for(conn_id, session): """Obtain existing Cisco CSR IPSec policy ID from another connection.""" return lookup_policy(IPSEC_POLICY, IdentifierMap.csr_ipsec_policy_id, conn_id, session) def determine_csr_policy_id(policy_type, conn_policy_field, map_policy_field, policy_id, conn_id, session): """Use existing or reserve a new policy ID for Cisco CSR use. TODO(pcm) FUTURE: Once device driver adds support for IKE/IPSec policy ID sharing, add call to find_conn_with_policy() to find used ID and then call lookup_policy() to find the current mapping for that ID. """ csr_id = get_next_available_id(session, map_policy_field, policy_type) LOG.debug(_("Reserved new CSR ID %(csr_id)d for %(policy)s " "ID %(policy_id)s"), {'csr_id': csr_id, 'policy': policy_type, 'policy_id': policy_id}) return csr_id def determine_csr_ike_policy_id(ike_policy_id, conn_id, session): """Use existing, or reserve a new IKE policy ID for Cisco CSR.""" return determine_csr_policy_id(IKE_POLICY, vpn_db.IPsecSiteConnection.ikepolicy_id, IdentifierMap.csr_ike_policy_id, ike_policy_id, conn_id, session) def determine_csr_ipsec_policy_id(ipsec_policy_id, conn_id, session): """Use existing, or reserve a new IPSec policy ID for Cisco CSR.""" return determine_csr_policy_id(IPSEC_POLICY, vpn_db.IPsecSiteConnection.ipsecpolicy_id, IdentifierMap.csr_ipsec_policy_id, ipsec_policy_id, conn_id, session) def get_tunnel_mapping_for(conn_id, session): try: entry = session.query(IdentifierMap).filter_by( ipsec_site_conn_id=conn_id).one() LOG.debug(_("Mappings for IPSec connection %(conn)s - " "tunnel=%(tunnel)s ike_policy=%(csr_ike)d " "ipsec_policy=%(csr_ipsec)d"), {'conn': conn_id, 'tunnel': entry.csr_tunnel_id, 'csr_ike': entry.csr_ike_policy_id, 'csr_ipsec': entry.csr_ipsec_policy_id}) return (entry.csr_tunnel_id, entry.csr_ike_policy_id, entry.csr_ipsec_policy_id) except sql_exc.NoResultFound: msg = _("Existing entry for IPSec connection %s not found in Cisco " "CSR mapping table") % conn_id raise CsrInternalError(reason=msg) def create_tunnel_mapping(context, conn_info): """Create Cisco CSR IDs, using mapping table and OpenStack UUIDs.""" conn_id = conn_info['id'] ike_policy_id = conn_info['ikepolicy_id'] ipsec_policy_id = conn_info['ipsecpolicy_id'] tenant_id = conn_info['tenant_id'] with context.session.begin(): csr_tunnel_id = get_next_available_tunnel_id(context.session) csr_ike_id = determine_csr_ike_policy_id(ike_policy_id, conn_id, context.session) csr_ipsec_id = determine_csr_ipsec_policy_id(ipsec_policy_id, conn_id, context.session) map_entry = IdentifierMap(tenant_id=tenant_id, ipsec_site_conn_id=conn_id, csr_tunnel_id=csr_tunnel_id, csr_ike_policy_id=csr_ike_id, csr_ipsec_policy_id=csr_ipsec_id) try: context.session.add(map_entry) context.session.flush() except db_exc.DBDuplicateEntry: msg = _("Attempt to create duplicate entry in Cisco CSR " "mapping table for connection %s") % conn_id raise CsrInternalError(reason=msg) LOG.info(_("Mapped connection %(conn_id)s to Tunnel%(tunnel_id)d " "using IKE policy ID %(ike_id)d and IPSec policy " "ID %(ipsec_id)d"), {'conn_id': conn_id, 'tunnel_id': csr_tunnel_id, 'ike_id': csr_ike_id, 'ipsec_id': csr_ipsec_id}) def delete_tunnel_mapping(context, conn_info): conn_id = conn_info['id'] with context.session.begin(): sess_qry = context.session.query(IdentifierMap) sess_qry.filter_by(ipsec_site_conn_id=conn_id).delete() LOG.info(_("Removed mapping for connection %s"), conn_id)