--- /dev/null
+[cisco_csr_ipsec]
+# Status check interval in seconds, for VPNaaS IPSec connections used on CSR
+# status_check_interval = 60
+
+# Cisco CSR management port information for REST access used by VPNaaS
+# TODO(pcm): Remove once CSR is integrated in as a Neutron router.
+#
+# Format is:
+# [cisco_csr_rest:<public IP>]
+# rest_mgmt = <mgmt port IP>
+# tunnel_ip = <tunnel IP>
+# username = <user>
+# password = <password>
+# timeout = <timeout>
+#
+# where:
+# public IP ----- Public IP address of router used with a VPN service (1:1 with CSR)
+# tunnel IP ----- Public IP address of the CSR used for the IPSec tunnel
+# mgmt port IP -- IP address of CSR for REST API access (not console port)
+# user ---------- Username for REST management port access to Cisco CSR
+# password ------ Password for REST management port access to Cisco CSR
+# timeout ------- REST request timeout to Cisco CSR (optional)
--- /dev/null
+# 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.
+#
+# @author: Paul Michali, Cisco Systems, Inc.
+
+import time
+
+import netaddr
+import requests
+from requests import exceptions as r_exc
+
+from neutron.openstack.common import jsonutils
+from neutron.openstack.common import log as logging
+
+
+TIMEOUT = 20.0
+
+LOG = logging.getLogger(__name__)
+HEADER_CONTENT_TYPE_JSON = {'content-type': 'application/json'}
+URL_BASE = 'https://%(host)s/api/v1/%(resource)s'
+
+
+def make_route_id(cidr, interface):
+ """Build ID that will be used to identify route for later deletion."""
+ net = netaddr.IPNetwork(cidr)
+ return '%(network)s_%(prefix)s_%(interface)s' % {
+ 'network': net.network,
+ 'prefix': net.prefixlen,
+ 'interface': interface}
+
+
+class CsrRestClient(object):
+
+ """REST CsrRestClient for accessing the Cisco Cloud Services Router."""
+
+ def __init__(self, host, tunnel_ip, username, password, timeout=None):
+ self.host = host
+ self.tunnel_ip = tunnel_ip
+ self.auth = (username, password)
+ self.token = None
+ self.status = requests.codes.OK
+ self.timeout = timeout
+ self.max_tries = 5
+ self.session = requests.Session()
+
+ def _response_info_for(self, response, method):
+ """Return contents or location from response.
+
+ For a POST or GET with a 200 response, the response content
+ is returned.
+
+ For a POST with a 201 response, return the header's location,
+ which contains the identifier for the created resource.
+
+ If there is an error, return the response content, so that
+ it can be used in error processing ('error-code', 'error-message',
+ and 'detail' fields).
+ """
+ if method in ('POST', 'GET') and self.status == requests.codes.OK:
+ LOG.debug(_('RESPONSE: %s'), response.json())
+ return response.json()
+ if method == 'POST' and self.status == requests.codes.CREATED:
+ return response.headers.get('location', '')
+ if self.status >= requests.codes.BAD_REQUEST and response.content:
+ if 'error-code' in response.content:
+ content = jsonutils.loads(response.content)
+ LOG.debug("Error response content %s", content)
+ return content
+
+ def _request(self, method, url, **kwargs):
+ """Perform REST request and save response info."""
+ try:
+ LOG.debug(_("%(method)s: Request for %(resource)s payload: "
+ "%(payload)s"),
+ {'method': method.upper(), 'resource': url,
+ 'payload': kwargs.get('data')})
+ start_time = time.time()
+ response = self.session.request(method, url, verify=False,
+ timeout=self.timeout, **kwargs)
+ LOG.debug(_("%(method)s Took %(time).2f seconds to process"),
+ {'method': method.upper(),
+ 'time': time.time() - start_time})
+ except (r_exc.Timeout, r_exc.SSLError) as te:
+ # Should never see SSLError, unless requests package is old (<2.0)
+ timeout_val = 0.0 if self.timeout is None else self.timeout
+ LOG.warning(_("%(method)s: Request timeout%(ssl)s "
+ "(%(timeout).3f sec) for CSR(%(host)s)"),
+ {'method': method,
+ 'timeout': timeout_val,
+ 'ssl': '(SSLError)'
+ if isinstance(te, r_exc.SSLError) else '',
+ 'host': self.host})
+ self.status = requests.codes.REQUEST_TIMEOUT
+ except r_exc.ConnectionError as ce:
+ LOG.error(_("%(method)s: Unable to connect to CSR(%(host)s): "
+ "%(error)s"),
+ {'method': method, 'host': self.host, 'error': ce})
+ self.status = requests.codes.NOT_FOUND
+ except Exception as e:
+ LOG.error(_("%(method)s: Unexpected error for CSR (%(host)s): "
+ "%(error)s"),
+ {'method': method, 'host': self.host, 'error': e})
+ self.status = requests.codes.INTERNAL_SERVER_ERROR
+ else:
+ self.status = response.status_code
+ LOG.debug(_("%(method)s: Completed [%(status)s]"),
+ {'method': method, 'status': self.status})
+ return self._response_info_for(response, method)
+
+ def authenticate(self):
+ """Obtain a token to use for subsequent CSR REST requests.
+
+ This is called when there is no token yet, or if the token has expired
+ and attempts to use it resulted in an UNAUTHORIZED REST response.
+ """
+
+ url = URL_BASE % {'host': self.host, 'resource': 'auth/token-services'}
+ headers = {'Content-Length': '0',
+ 'Accept': 'application/json'}
+ headers.update(HEADER_CONTENT_TYPE_JSON)
+ LOG.debug(_("%(auth)s with CSR %(host)s"),
+ {'auth': 'Authenticating' if self.token is None
+ else 'Reauthenticating', 'host': self.host})
+ self.token = None
+ response = self._request("POST", url, headers=headers, auth=self.auth)
+ if response:
+ self.token = response['token-id']
+ LOG.debug(_("Successfully authenticated with CSR %s"), self.host)
+ return True
+ LOG.error(_("Failed authentication with CSR %(host)s [%(status)s]"),
+ {'host': self.host, 'status': self.status})
+
+ def _do_request(self, method, resource, payload=None, more_headers=None,
+ full_url=False):
+ """Perform a REST request to a CSR resource.
+
+ If this is the first time interacting with the CSR, a token will
+ be obtained. If the request fails, due to an expired token, the
+ token will be obtained and the request will be retried once more.
+ """
+
+ if self.token is None:
+ if not self.authenticate():
+ return
+
+ if full_url:
+ url = resource
+ else:
+ url = ('https://%(host)s/api/v1/%(resource)s' %
+ {'host': self.host, 'resource': resource})
+ headers = {'Accept': 'application/json', 'X-auth-token': self.token}
+ if more_headers:
+ headers.update(more_headers)
+ if payload:
+ payload = jsonutils.dumps(payload)
+ response = self._request(method, url, data=payload, headers=headers)
+ if self.status == requests.codes.UNAUTHORIZED:
+ if not self.authenticate():
+ return
+ headers['X-auth-token'] = self.token
+ response = self._request(method, url, data=payload,
+ headers=headers)
+ if self.status != requests.codes.REQUEST_TIMEOUT:
+ return response
+ LOG.error(_("%(method)s: Request timeout for CSR(%(host)s)"),
+ {'method': method, 'host': self.host})
+
+ def get_request(self, resource, full_url=False):
+ """Perform a REST GET requests for a CSR resource."""
+ return self._do_request('GET', resource, full_url=full_url)
+
+ def post_request(self, resource, payload=None):
+ """Perform a POST request to a CSR resource."""
+ return self._do_request('POST', resource, payload=payload,
+ more_headers=HEADER_CONTENT_TYPE_JSON)
+
+ def put_request(self, resource, payload=None):
+ """Perform a PUT request to a CSR resource."""
+ return self._do_request('PUT', resource, payload=payload,
+ more_headers=HEADER_CONTENT_TYPE_JSON)
+
+ def delete_request(self, resource):
+ """Perform a DELETE request on a CSR resource."""
+ return self._do_request('DELETE', resource,
+ more_headers=HEADER_CONTENT_TYPE_JSON)
+
+ def create_ike_policy(self, policy_info):
+ base_ike_policy_info = {u'version': u'v1',
+ u'local-auth-method': u'pre-share'}
+ base_ike_policy_info.update(policy_info)
+ return self.post_request('vpn-svc/ike/policies',
+ payload=base_ike_policy_info)
+
+ def create_ipsec_policy(self, policy_info):
+ base_ipsec_policy_info = {u'mode': u'tunnel'}
+ base_ipsec_policy_info.update(policy_info)
+ return self.post_request('vpn-svc/ipsec/policies',
+ payload=base_ipsec_policy_info)
+
+ def create_pre_shared_key(self, psk_info):
+ return self.post_request('vpn-svc/ike/keyrings', payload=psk_info)
+
+ def create_ipsec_connection(self, connection_info):
+ base_conn_info = {u'vpn-type': u'site-to-site',
+ u'ip-version': u'ipv4'}
+ connection_info.update(base_conn_info)
+ # TODO(pcm) pass in value, when CSR is embedded as Neutron router.
+ # Currently, get this from .INI file.
+ connection_info[u'local-device'][u'tunnel-ip-address'] = self.tunnel_ip
+ return self.post_request('vpn-svc/site-to-site',
+ payload=connection_info)
+
+ def configure_ike_keepalive(self, keepalive_info):
+ base_keepalive_info = {u'periodic': True}
+ keepalive_info.update(base_keepalive_info)
+ return self.put_request('vpn-svc/ike/keepalive', keepalive_info)
+
+ def create_static_route(self, route_info):
+ return self.post_request('routing-svc/static-routes',
+ payload=route_info)
+
+ def delete_static_route(self, route_id):
+ return self.delete_request('routing-svc/static-routes/%s' % route_id)
+
+ def delete_ipsec_connection(self, conn_id):
+ return self.delete_request('vpn-svc/site-to-site/%s' % conn_id)
+
+ def delete_ipsec_policy(self, policy_id):
+ return self.delete_request('vpn-svc/ipsec/policies/%s' % policy_id)
+
+ def delete_ike_policy(self, policy_id):
+ return self.delete_request('vpn-svc/ike/policies/%s' % policy_id)
+
+ def delete_pre_shared_key(self, key_id):
+ return self.delete_request('vpn-svc/ike/keyrings/%s' % key_id)
+
+ def read_tunnel_statuses(self):
+ results = self.get_request('vpn-svc/site-to-site/active/sessions')
+ if self.status != requests.codes.OK or not results:
+ return []
+ tunnels = [(t[u'vpn-interface-name'], t[u'status'])
+ for t in results['items']]
+ return tunnels
--- /dev/null
+# 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.
+#
+# @author: Paul Michali, Cisco Systems, Inc.
+
+import abc
+from collections import namedtuple
+import httplib
+
+import netaddr
+from oslo.config import cfg
+
+from neutron.common import exceptions
+from neutron.common import rpc as n_rpc
+from neutron import context as ctx
+from neutron.openstack.common import lockutils
+from neutron.openstack.common import log as logging
+from neutron.openstack.common import loopingcall
+from neutron.openstack.common import rpc
+from neutron.openstack.common.rpc import proxy
+from neutron.plugins.common import constants
+from neutron.plugins.common import utils as plugin_utils
+from neutron.services.vpn.common import topics
+from neutron.services.vpn import device_drivers
+from neutron.services.vpn.device_drivers import (
+ cisco_csr_rest_client as csr_client)
+
+
+ipsec_opts = [
+ cfg.IntOpt('status_check_interval',
+ default=60,
+ help=_("Status check interval for Cisco CSR IPSec connections"))
+]
+cfg.CONF.register_opts(ipsec_opts, 'cisco_csr_ipsec')
+
+LOG = logging.getLogger(__name__)
+
+RollbackStep = namedtuple('RollbackStep', ['action', 'resource_id', 'title'])
+
+
+class CsrResourceCreateFailure(exceptions.NeutronException):
+ message = _("Cisco CSR failed to create %(resource)s (%(which)s)")
+
+
+class CsrDriverMismatchError(exceptions.NeutronException):
+ message = _("Required %(resource)s attribute %(attr)s mapping for Cisco "
+ "CSR is missing in device driver")
+
+
+class CsrUnknownMappingError(exceptions.NeutronException):
+ message = _("Device driver does not have a mapping of '%(value)s for "
+ "attribute %(attr)s of %(resource)s")
+
+
+def find_available_csrs_from_config(config_files):
+ """Read INI for available Cisco CSRs that driver can use.
+
+ Loads management port, tunnel IP, user, and password information for
+ available CSRs from configuration file. Driver will use this info to
+ configure VPN connections. The CSR is associated 1:1 with a Neutron
+ router. To identify which CSR to use for a VPN service, the public
+ (GW) IP of the Neutron router will be used as an index into the CSR
+ config info.
+ """
+ multi_parser = cfg.MultiConfigParser()
+ LOG.info(_("Scanning config files %s for Cisco CSR configurations"),
+ config_files)
+ try:
+ read_ok = multi_parser.read(config_files)
+ except cfg.ParseError as pe:
+ LOG.error(_("Config file parse error: %s"), pe)
+ return {}
+
+ if len(read_ok) != len(config_files):
+ raise cfg.Error(_("Unable to parse config files %s for Cisco CSR "
+ "info") % config_files)
+ csrs_found = {}
+ for parsed_file in multi_parser.parsed:
+ for parsed_item in parsed_file.keys():
+ device_type, sep, for_router = parsed_item.partition(':')
+ if device_type.lower() == 'cisco_csr_rest':
+ try:
+ netaddr.IPNetwork(for_router)
+ except netaddr.core.AddrFormatError:
+ LOG.error(_("Ignoring Cisco CSR configuration entry - "
+ "router IP %s is not valid"), for_router)
+ continue
+ entry = parsed_file[parsed_item]
+ # Check for missing fields
+ try:
+ rest_mgmt_ip = entry['rest_mgmt'][0]
+ tunnel_ip = entry['tunnel_ip'][0]
+ username = entry['username'][0]
+ password = entry['password'][0]
+ except KeyError as ke:
+ LOG.error(_("Ignoring Cisco CSR for router %(router)s "
+ "- missing %(field)s setting"),
+ {'router': for_router, 'field': str(ke)})
+ continue
+ # Validate fields
+ try:
+ timeout = float(entry['timeout'][0])
+ except ValueError:
+ LOG.error(_("Ignoring Cisco CSR for router %s - "
+ "timeout is not a floating point number"),
+ for_router)
+ continue
+ except KeyError:
+ timeout = csr_client.TIMEOUT
+ try:
+ netaddr.IPAddress(rest_mgmt_ip)
+ except netaddr.core.AddrFormatError:
+ LOG.error(_("Ignoring Cisco CSR for subnet %s - "
+ "REST management is not an IP address"),
+ for_router)
+ continue
+ try:
+ netaddr.IPAddress(tunnel_ip)
+ except netaddr.core.AddrFormatError:
+ LOG.error(_("Ignoring Cisco CSR for router %s - "
+ "local tunnel is not an IP address"),
+ for_router)
+ continue
+ csrs_found[for_router] = {'rest_mgmt': rest_mgmt_ip,
+ 'tunnel_ip': tunnel_ip,
+ 'username': username,
+ 'password': password,
+ 'timeout': timeout}
+
+ LOG.debug(_("Found CSR for router %(router)s: %(info)s"),
+ {'router': for_router,
+ 'info': csrs_found[for_router]})
+ return csrs_found
+
+
+class CiscoCsrIPsecVpnDriverApi(proxy.RpcProxy):
+ """RPC API for agent to plugin messaging."""
+
+ def get_vpn_services_on_host(self, context, host):
+ """Get list of vpnservices on this host.
+
+ The vpnservices including related ipsec_site_connection,
+ ikepolicy, ipsecpolicy, and Cisco info on this host.
+ """
+ return self.call(context,
+ self.make_msg('get_vpn_services_on_host',
+ host=host),
+ topic=self.topic)
+
+ def update_status(self, context, status):
+ """Update status for all VPN services and connections."""
+ return self.cast(context,
+ self.make_msg('update_status',
+ status=status),
+ topic=self.topic)
+
+
+class CiscoCsrIPsecDriver(device_drivers.DeviceDriver):
+ """Cisco CSR VPN Device Driver for IPSec.
+
+ This class is designed for use with L3-agent now.
+ However this driver will be used with another agent in future.
+ so the use of "Router" is kept minimul now.
+ Insted of router_id, we are using process_id in this code.
+ """
+
+ # history
+ # 1.0 Initial version
+
+ RPC_API_VERSION = '1.0'
+ __metaclass__ = abc.ABCMeta
+
+ def __init__(self, agent, host):
+ self.host = host
+ self.conn = rpc.create_connection(new=True)
+ context = ctx.get_admin_context_without_session()
+ node_topic = '%s.%s' % (topics.CISCO_IPSEC_AGENT_TOPIC, self.host)
+
+ self.service_state = {}
+
+ self.conn.create_consumer(
+ node_topic,
+ self.create_rpc_dispatcher(),
+ fanout=False)
+ self.conn.consume_in_thread()
+ self.agent_rpc = (
+ CiscoCsrIPsecVpnDriverApi(topics.CISCO_IPSEC_DRIVER_TOPIC, '1.0'))
+ self.periodic_report = loopingcall.FixedIntervalLoopingCall(
+ self.report_status, context)
+ self.periodic_report.start(
+ interval=agent.conf.cisco_csr_ipsec.status_check_interval)
+
+ csrs_found = find_available_csrs_from_config(cfg.CONF.config_file)
+ if csrs_found:
+ LOG.info(_("Loaded %(num)d Cisco CSR configuration%(plural)s"),
+ {'num': len(csrs_found),
+ 'plural': 's'[len(csrs_found) == 1:]})
+ else:
+ raise SystemExit(_('No Cisco CSR configurations found in: %s') %
+ cfg.CONF.config_file)
+ self.csrs = dict([(k, csr_client.CsrRestClient(v['rest_mgmt'],
+ v['tunnel_ip'],
+ v['username'],
+ v['password'],
+ v['timeout']))
+ for k, v in csrs_found.items()])
+
+ def create_rpc_dispatcher(self):
+ return n_rpc.PluginRpcDispatcher([self])
+
+ def vpnservice_updated(self, context, **kwargs):
+ """Handle VPNaaS service driver change notifications."""
+ LOG.debug(_("Handling VPN service update notification"))
+ self.sync(context, [])
+
+ def create_vpn_service(self, service_data):
+ """Create new entry to track VPN service and its connections."""
+ vpn_service_id = service_data['id']
+ vpn_service_router = service_data['external_ip']
+ self.service_state[vpn_service_id] = CiscoCsrVpnService(
+ service_data, self.csrs.get(vpn_service_router))
+ return self.service_state[vpn_service_id]
+
+ def update_connection(self, context, vpn_service_id, conn_data):
+ """Handle notification for a single IPSec connection."""
+ vpn_service = self.service_state[vpn_service_id]
+ conn_id = conn_data['id']
+ is_admin_up = conn_data[u'admin_state_up']
+ if conn_id in vpn_service.conn_state:
+ ipsec_conn = vpn_service.conn_state[conn_id]
+ ipsec_conn.last_status = conn_data['status']
+ if is_admin_up:
+ ipsec_conn.is_dirty = False
+ LOG.debug(_("Update: IPSec connection %s unchanged - "
+ "marking clean"), conn_id)
+ # TODO(pcm) FUTURE - Handle update requests (delete/create?)
+ # will need to detect what has changed. For now assume no
+ # change (it is blocked in service driver).
+ else:
+ LOG.debug(_("Update: IPSec connection %s is admin down - "
+ "will be removed in sweep phase"), conn_id)
+ else:
+ if not is_admin_up:
+ LOG.debug(_("Update: Unknown IPSec connection %s is admin "
+ "down - ignoring"), conn_id)
+ return
+ LOG.debug(_("Update: New IPSec connection %s - marking clean"),
+ conn_id)
+ ipsec_conn = vpn_service.create_connection(conn_data)
+ ipsec_conn.create_ipsec_site_connection(context, conn_data)
+ return ipsec_conn
+
+ def update_service(self, context, service_data):
+ """Handle notification for a single VPN Service and its connections."""
+ vpn_service_id = service_data['id']
+ csr_id = service_data['external_ip']
+ if csr_id not in self.csrs:
+ LOG.error(_("Update: Skipping VPN service %(service)s as it's "
+ "router (%(csr_id)s is not associated with a Cisco "
+ "CSR"), {'service': vpn_service_id, 'csr_id': csr_id})
+ return
+ is_admin_up = service_data[u'admin_state_up']
+ if vpn_service_id in self.service_state:
+ vpn_service = self.service_state[vpn_service_id]
+ vpn_service.last_status = service_data['status']
+ if is_admin_up:
+ vpn_service.is_dirty = False
+ else:
+ LOG.debug(_("Update: VPN service %s is admin down - will "
+ "be removed in sweep phase"), vpn_service_id)
+ return vpn_service
+ else:
+ if not is_admin_up:
+ LOG.debug(_("Update: Unknown VPN service %s is admin down - "
+ "ignoring"), vpn_service_id)
+ return
+ vpn_service = self.create_vpn_service(service_data)
+ # Handle all the IPSec connection notifications in the data
+ LOG.debug(_("Update: Processing IPSec connections for VPN service %s"),
+ vpn_service_id)
+ for conn_data in service_data['ipsec_conns']:
+ self.update_connection(context, service_data['id'], conn_data)
+ LOG.debug(_("Update: Completed update processing"))
+ return vpn_service
+
+ def update_all_services_and_connections(self, context):
+ """Update services and connections based on plugin info.
+
+ Perform any create and update operations and then update status.
+ Mark every visited connection as no longer "dirty" so they will
+ not be deleted at end of sync processing.
+ """
+ services_data = self.agent_rpc.get_vpn_services_on_host(context,
+ self.host)
+ LOG.debug("Sync updating for %d VPN services", len(services_data))
+ vpn_services = []
+ for service_data in services_data:
+ vpn_service = self.update_service(context, service_data)
+ if vpn_service:
+ vpn_services.append(vpn_service)
+ return vpn_services
+
+ def mark_existing_connections_as_dirty(self):
+ """Mark all existing connections as "dirty" for sync."""
+ service_count = 0
+ connection_count = 0
+ for service_state in self.service_state.values():
+ service_state.is_dirty = True
+ service_count += 1
+ for conn_id in service_state.conn_state:
+ service_state.conn_state[conn_id].is_dirty = True
+ connection_count += 1
+ LOG.debug(_("Mark: %(service)d VPN services and %(conn)d IPSec "
+ "connections marked dirty"), {'service': service_count,
+ 'conn': connection_count})
+
+ def remove_unknown_connections(self, context):
+ """Remove connections that are not known by service driver."""
+ service_count = 0
+ connection_count = 0
+ for vpn_service_id, vpn_service in self.service_state.items():
+ dirty = [c_id for c_id, c in vpn_service.conn_state.items()
+ if c.is_dirty]
+ for conn_id in dirty:
+ conn_state = vpn_service.conn_state[conn_id]
+ conn_state.delete_ipsec_site_connection(context, conn_id)
+ connection_count += 1
+ del vpn_service.conn_state[conn_id]
+ if vpn_service.is_dirty:
+ service_count += 1
+ del self.service_state[vpn_service_id]
+ LOG.debug(_("Sweep: Removed %(service)d dirty VPN service%(splural)s "
+ "and %(conn)d dirty IPSec connection%(cplural)s"),
+ {'service': service_count, 'conn': connection_count,
+ 'splural': 's'[service_count == 1:],
+ 'cplural': 's'[connection_count == 1:]})
+
+ def build_report_for_connections_on(self, vpn_service):
+ """Create the report fragment for IPSec connections on a service.
+
+ Collect the current status from the Cisco CSR and use that to update
+ the status and generate report fragment for each connection on the
+ service. If there is no status information, or no change, then no
+ report info will be created for the connection. The combined report
+ data is returned.
+ """
+ LOG.debug(_("Report: Collecting status for IPSec connections on VPN "
+ "service %s"), vpn_service.service_id)
+ tunnels = vpn_service.get_ipsec_connections_status()
+ report = {}
+ for connection in vpn_service.conn_state.values():
+ current_status = connection.find_current_status_in(tunnels)
+ frag = connection.build_report_based_on_status(current_status)
+ if frag:
+ LOG.debug(_("Report: Adding info for IPSec connection %s"),
+ connection.conn_id)
+ report.update(frag)
+ return report
+
+ def build_report_for_service(self, vpn_service):
+ """Create the report info for a VPN service and its IPSec connections.
+
+ Get the report info for the connections on the service, and include
+ it into the report info for the VPN service. If there is no report
+ info for the connection, then no change has occurred and no report
+ will be generated. If there is only one connection for the service,
+ we'll set the service state to match the connection (with ERROR seen
+ as DOWN).
+ """
+ conn_report = self.build_report_for_connections_on(vpn_service)
+ if conn_report:
+ pending_handled = plugin_utils.in_pending_status(
+ vpn_service.last_status)
+ if (len(conn_report) == 1 and
+ conn_report.values()[0]['status'] != constants.ACTIVE):
+ vpn_service.last_status = constants.DOWN
+ else:
+ vpn_service.last_status = constants.ACTIVE
+ LOG.debug(_("Report: Adding info for VPN service %s"),
+ vpn_service.service_id)
+ return {u'id': vpn_service.service_id,
+ u'status': vpn_service.last_status,
+ u'updated_pending_status': pending_handled,
+ u'ipsec_site_connections': conn_report}
+ else:
+ return {}
+
+ def report_status(self, context):
+ """Report status of all VPN services and IPSec connections to plugin.
+
+ This is called periodically by the agent, to push up status changes,
+ and at the end of any sync operation to reflect the changes due to a
+ sync or change notification.
+ """
+ service_report = []
+ LOG.debug(_("Report: Starting status report"))
+ for vpn_service_id, vpn_service in self.service_state.items():
+ LOG.debug(_("Report: Collecting status for VPN service %s"),
+ vpn_service_id)
+ report = self.build_report_for_service(vpn_service)
+ if report:
+ service_report.append(report)
+ if service_report:
+ LOG.info(_("Sending status report update to plugin"))
+ self.agent_rpc.update_status(context, service_report)
+ LOG.debug(_("Report: Completed status report processing"))
+ return service_report
+
+ @lockutils.synchronized('vpn-agent', 'neutron-')
+ def sync(self, context, routers):
+ """Synchronize with plugin and report current status.
+
+ Mark all "known" services/connections as dirty, update them based on
+ information from the plugin, remove (sweep) any connections that are
+ not updated (dirty), and report updates, if any, back to plugin.
+ Called when update/delete a service or create/update/delete a
+ connection (vpnservice_updated message), or router change
+ (_process_routers).
+ """
+ self.mark_existing_connections_as_dirty()
+ self.update_all_services_and_connections(context)
+ self.remove_unknown_connections(context)
+ self.report_status(context)
+
+ def create_router(self, process_id):
+ """Actions taken when router created."""
+ # Note: Since Cisco CSR is running out-of-band, nothing to do here
+ pass
+
+ def destroy_router(self, process_id):
+ """Actions taken when router deleted."""
+ # Note: Since Cisco CSR is running out-of-band, nothing to do here
+ pass
+
+
+class CiscoCsrVpnService(object):
+
+ """Maintains state/status information for a service and its connections."""
+
+ def __init__(self, service_data, csr):
+ self.service_id = service_data['id']
+ self.last_status = service_data['status']
+ self.conn_state = {}
+ self.is_dirty = False
+ self.csr = csr
+ # TODO(pcm) FUTURE - handle sharing of policies
+
+ def create_connection(self, conn_data):
+ conn_id = conn_data['id']
+ self.conn_state[conn_id] = CiscoCsrIPSecConnection(conn_data, self.csr)
+ return self.conn_state[conn_id]
+
+ def get_connection(self, conn_id):
+ return self.conn_state.get(conn_id)
+
+ def conn_status(self, conn_id):
+ conn_state = self.get_connection(conn_id)
+ if conn_state:
+ return conn_state.last_status
+
+ def snapshot_conn_state(self, ipsec_conn):
+ """Create/obtain connection state and save current status."""
+ conn_state = self.conn_state.setdefault(
+ ipsec_conn['id'], CiscoCsrIPSecConnection(ipsec_conn, self.csr))
+ conn_state.last_status = ipsec_conn['status']
+ conn_state.is_dirty = False
+ return conn_state
+
+ STATUS_MAP = {'ERROR': constants.ERROR,
+ 'UP-ACTIVE': constants.ACTIVE,
+ 'UP-IDLE': constants.ACTIVE,
+ 'UP-NO-IKE': constants.ACTIVE,
+ 'DOWN': constants.DOWN,
+ 'DOWN-NEGOTIATING': constants.DOWN}
+
+ def get_ipsec_connections_status(self):
+ """Obtain current status of all tunnels on a Cisco CSR.
+
+ Convert them to OpenStack status values.
+ """
+ tunnels = self.csr.read_tunnel_statuses()
+ for tunnel in tunnels:
+ LOG.debug("CSR Reports %(tunnel)s status '%(status)s'",
+ {'tunnel': tunnel[0], 'status': tunnel[1]})
+ return dict(map(lambda x: (x[0], self.STATUS_MAP[x[1]]), tunnels))
+
+ def find_matching_connection(self, tunnel_id):
+ """Find IPSec connection using Cisco CSR tunnel specified, if any."""
+ for connection in self.conn_state.values():
+ if connection.tunnel == tunnel_id:
+ return connection.conn_id
+
+
+class CiscoCsrIPSecConnection(object):
+
+ """State and actions for IPSec site-to-site connections."""
+
+ def __init__(self, conn_info, csr):
+ self.conn_id = conn_info['id']
+ self.csr = csr
+ self.steps = []
+ self.is_dirty = False
+ self.last_status = conn_info['status']
+ self.tunnel = conn_info['cisco']['site_conn_id']
+
+ def find_current_status_in(self, statuses):
+ if self.tunnel in statuses:
+ return statuses[self.tunnel]
+ else:
+ return constants.ERROR
+
+ def build_report_based_on_status(self, current_status):
+ if current_status != self.last_status:
+ pending_handled = plugin_utils.in_pending_status(self.last_status)
+ self.last_status = current_status
+ return {self.conn_id: {'status': current_status,
+ 'updated_pending_status': pending_handled}}
+ else:
+ return {}
+
+ DIALECT_MAP = {'ike_policy': {'name': 'IKE Policy',
+ 'v1': u'v1',
+ # auth_algorithm -> hash
+ 'sha1': u'sha',
+ # encryption_algorithm -> encryption
+ '3des': u'3des',
+ 'aes-128': u'aes',
+ # TODO(pcm) update these 2 once CSR updated
+ 'aes-192': u'aes',
+ 'aes-256': u'aes',
+ # pfs -> dhGroup
+ 'group2': 2,
+ 'group5': 5,
+ 'group14': 14},
+ 'ipsec_policy': {'name': 'IPSec Policy',
+ # auth_algorithm -> esp-authentication
+ 'sha1': u'esp-sha-hmac',
+ # transform_protocol -> ah
+ 'esp': None,
+ 'ah': u'ah-sha-hmac',
+ 'ah-esp': u'ah-sha-hmac',
+ # encryption_algorithm -> esp-encryption
+ '3des': u'esp-3des',
+ 'aes-128': u'esp-aes',
+ # TODO(pcm) update these 2 once CSR updated
+ 'aes-192': u'esp-aes',
+ 'aes-256': u'esp-aes',
+ # pfs -> pfs
+ 'group2': u'group2',
+ 'group5': u'group5',
+ 'group14': u'group14'}}
+
+ def translate_dialect(self, resource, attribute, info):
+ """Map VPNaaS attributes values to CSR values for a resource."""
+ name = self.DIALECT_MAP[resource]['name']
+ if attribute not in info:
+ raise CsrDriverMismatchError(resource=name, attr=attribute)
+ value = info[attribute].lower()
+ if value in self.DIALECT_MAP[resource]:
+ return self.DIALECT_MAP[resource][value]
+ raise CsrUnknownMappingError(resource=name, attr=attribute,
+ value=value)
+
+ def create_psk_info(self, psk_id, conn_info):
+ """Collect/create attributes needed for pre-shared key."""
+ return {u'keyring-name': psk_id,
+ u'pre-shared-key-list': [
+ {u'key': conn_info['psk'],
+ u'encrypted': False,
+ u'peer-address': conn_info['peer_address']}]}
+
+ def create_ike_policy_info(self, ike_policy_id, conn_info):
+ """Collect/create/map attributes needed for IKE policy."""
+ for_ike = 'ike_policy'
+ policy_info = conn_info[for_ike]
+ version = self.translate_dialect(for_ike,
+ 'ike_version',
+ policy_info)
+ encrypt_algorithm = self.translate_dialect(for_ike,
+ 'encryption_algorithm',
+ policy_info)
+ auth_algorithm = self.translate_dialect(for_ike,
+ 'auth_algorithm',
+ policy_info)
+ group = self.translate_dialect(for_ike,
+ 'pfs',
+ policy_info)
+ lifetime = policy_info['lifetime_value']
+ return {u'version': version,
+ u'priority-id': ike_policy_id,
+ u'encryption': encrypt_algorithm,
+ u'hash': auth_algorithm,
+ u'dhGroup': group,
+ u'lifetime': lifetime}
+
+ def create_ipsec_policy_info(self, ipsec_policy_id, info):
+ """Collect/create attributes needed for IPSec policy.
+
+ Note: OpenStack will provide a default encryption algorithm, if one is
+ not provided, so a authentication only configuration of (ah, sha1),
+ which maps to ah-sha-hmac transform protocol, cannot be selected.
+ As a result, we'll always configure the encryption algorithm, and
+ will select ah-sha-hmac for transform protocol.
+ """
+
+ for_ipsec = 'ipsec_policy'
+ policy_info = info[for_ipsec]
+ transform_protocol = self.translate_dialect(for_ipsec,
+ 'transform_protocol',
+ policy_info)
+ auth_algorithm = self.translate_dialect(for_ipsec,
+ 'auth_algorithm',
+ policy_info)
+ encrypt_algorithm = self.translate_dialect(for_ipsec,
+ 'encryption_algorithm',
+ policy_info)
+ group = self.translate_dialect(for_ipsec, 'pfs', policy_info)
+ lifetime = policy_info['lifetime_value']
+ settings = {u'policy-id': ipsec_policy_id,
+ u'protection-suite': {
+ u'esp-encryption': encrypt_algorithm,
+ u'esp-authentication': auth_algorithm},
+ u'lifetime-sec': lifetime,
+ u'pfs': group,
+ # TODO(pcm): Remove when CSR fixes 'Disable'
+ u'anti-replay-window-size': u'64'}
+ if transform_protocol:
+ settings[u'protection-suite'][u'ah'] = transform_protocol
+ return settings
+
+ def create_site_connection_info(self, site_conn_id, ipsec_policy_id,
+ conn_info):
+ """Collect/create attributes needed for the IPSec connection."""
+ # TODO(pcm) Enable, once CSR is embedded as a Neutron router
+ # gw_ip = vpnservice['external_ip'] (need to pass in)
+ mtu = conn_info['mtu']
+ return {
+ u'vpn-interface-name': site_conn_id,
+ u'ipsec-policy-id': ipsec_policy_id,
+ u'local-device': {
+ # TODO(pcm): FUTURE - Get CSR port of interface with
+ # local subnet
+ u'ip-address': u'GigabitEthernet3',
+ # TODO(pcm): FUTURE - Get IP address of router's public
+ # I/F, once CSR is used as embedded router.
+ u'tunnel-ip-address': u'172.24.4.23'
+ # u'tunnel-ip-address': u'%s' % gw_ip
+ },
+ u'remote-device': {
+ u'tunnel-ip-address': conn_info['peer_address']
+ },
+ u'mtu': mtu
+ }
+
+ def create_routes_info(self, site_conn_id, conn_info):
+ """Collect/create attributes for static routes."""
+ routes_info = []
+ for peer_cidr in conn_info.get('peer_cidrs', []):
+ route = {u'destination-network': peer_cidr,
+ u'outgoing-interface': site_conn_id}
+ route_id = csr_client.make_route_id(peer_cidr, site_conn_id)
+ routes_info.append((route_id, route))
+ return routes_info
+
+ def _check_create(self, resource, which):
+ """Determine if REST create request was successful."""
+ if self.csr.status == httplib.CREATED:
+ LOG.debug("%(resource)s %(which)s is configured",
+ {'resource': resource, 'which': which})
+ return
+ LOG.error(_("Unable to create %(resource)s %(which)s: "
+ "%(status)d"),
+ {'resource': resource, 'which': which,
+ 'status': self.csr.status})
+ # ToDO(pcm): Set state to error
+ raise CsrResourceCreateFailure(resource=resource, which=which)
+
+ def do_create_action(self, action_suffix, info, resource_id, title):
+ """Perform a single REST step for IPSec site connection create."""
+ create_action = 'create_%s' % action_suffix
+ try:
+ getattr(self.csr, create_action)(info)
+ except AttributeError:
+ LOG.exception(_("Internal error - '%s' is not defined"),
+ create_action)
+ raise CsrResourceCreateFailure(resource=title,
+ which=resource_id)
+ self._check_create(title, resource_id)
+ self.steps.append(RollbackStep(action_suffix, resource_id, title))
+
+ def _verify_deleted(self, status, resource, which):
+ """Determine if REST delete request was successful."""
+ if status in (httplib.NO_CONTENT, httplib.NOT_FOUND):
+ LOG.debug("%(resource)s configuration %(which)s was removed",
+ {'resource': resource, 'which': which})
+ else:
+ LOG.warning(_("Unable to delete %(resource)s %(which)s: "
+ "%(status)d"), {'resource': resource,
+ 'which': which,
+ 'status': status})
+
+ def do_rollback(self):
+ """Undo create steps that were completed successfully."""
+ for step in reversed(self.steps):
+ delete_action = 'delete_%s' % step.action
+ LOG.debug(_("Performing rollback action %(action)s for "
+ "resource %(resource)s"), {'action': delete_action,
+ 'resource': step.title})
+ try:
+ getattr(self.csr, delete_action)(step.resource_id)
+ except AttributeError:
+ LOG.exception(_("Internal error - '%s' is not defined"),
+ delete_action)
+ raise CsrResourceCreateFailure(resource=step.title,
+ which=step.resource_id)
+ self._verify_deleted(self.csr.status, step.title, step.resource_id)
+ self.steps = []
+
+ def create_ipsec_site_connection(self, context, conn_info):
+ """Creates an IPSec site-to-site connection on CSR.
+
+ Create the PSK, IKE policy, IPSec policy, connection, static route,
+ and (future) DPD.
+ """
+ # Get all the IDs
+ conn_id = conn_info['id']
+ psk_id = conn_id
+ site_conn_id = conn_info['cisco']['site_conn_id']
+ ike_policy_id = conn_info['cisco']['ike_policy_id']
+ ipsec_policy_id = conn_info['cisco']['ipsec_policy_id']
+
+ LOG.debug(_('Creating IPSec connection %s'), conn_id)
+ # Get all the attributes needed to create
+ try:
+ psk_info = self.create_psk_info(psk_id, conn_info)
+ ike_policy_info = self.create_ike_policy_info(ike_policy_id,
+ conn_info)
+ ipsec_policy_info = self.create_ipsec_policy_info(ipsec_policy_id,
+ conn_info)
+ connection_info = self.create_site_connection_info(site_conn_id,
+ ipsec_policy_id,
+ conn_info)
+ routes_info = self.create_routes_info(site_conn_id, conn_info)
+ except (CsrUnknownMappingError, CsrDriverMismatchError) as e:
+ LOG.exception(e)
+ return
+
+ try:
+ self.do_create_action('pre_shared_key', psk_info,
+ conn_id, 'Pre-Shared Key')
+ self.do_create_action('ike_policy', ike_policy_info,
+ ike_policy_id, 'IKE Policy')
+ self.do_create_action('ipsec_policy', ipsec_policy_info,
+ ipsec_policy_id, 'IPSec Policy')
+ self.do_create_action('ipsec_connection', connection_info,
+ site_conn_id, 'IPSec Connection')
+
+ # TODO(pcm): FUTURE - Do DPD for v1 and handle if >1 connection
+ # and different DPD settings
+ for route_id, route_info in routes_info:
+ self.do_create_action('static_route', route_info,
+ route_id, 'Static Route')
+ except CsrResourceCreateFailure:
+ self.do_rollback()
+ LOG.info(_("FAILED: Create of IPSec site-to-site connection %s"),
+ conn_id)
+ else:
+ LOG.info(_("SUCCESS: Created IPSec site-to-site connection %s"),
+ conn_id)
+
+ def delete_ipsec_site_connection(self, context, conn_id):
+ """Delete the site-to-site IPSec connection.
+
+ This will be best effort and will continue, if there are any
+ failures.
+ """
+ LOG.debug(_('Deleting IPSec connection %s'), conn_id)
+ if not self.steps:
+ LOG.warning(_('Unable to find connection %s'), conn_id)
+ else:
+ self.do_rollback()
+
+ LOG.info(_("SUCCESS: Deleted IPSec site-to-site connection %s"),
+ conn_id)
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
+#
+# @author: Paul Michali, Cisco Systems, Inc.
import sqlalchemy as sa
from sqlalchemy.orm import exc as sql_exc
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()
+ match = qry.filter_request(
+ policy_field == policy_id,
+ vpn_db.IPsecSiteConnection.id != conn_id).first()
if match:
return match[0]
csr_ipsec_policy_id=csr_ipsec_id)
try:
context.session.add(map_entry)
+ # Force committing to database
context.session.flush()
except db_exc.DBDuplicateEntry:
msg = _("Attempt to create duplicate entry in Cisco CSR "
--- /dev/null
+# 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.
+#
+# @author: Paul Michali, Cisco Systems, Inc.
+
+"""Mock REST requests to Cisco Cloud Services Router."""
+
+import re
+
+from functools import wraps
+# import httmock
+import requests
+from requests import exceptions as r_exc
+
+from neutron.openstack.common import log as logging
+# TODO(pcm) Remove once httmock package is added to test-requirements. For
+# now, uncomment and include httmock source to UT
+from neutron.tests.unit.services.vpn.device_drivers import httmock
+
+# TODO(pcm) Remove, once verified these have been fixed
+FIXED_CSCum50512 = False
+FIXED_CSCum35484 = False
+FIXED_CSCul82396 = False
+FIXED_CSCum10324 = False
+
+LOG = logging.getLogger(__name__)
+
+
+def repeat(n):
+ """Decorator to limit the number of times a handler is called.
+
+ Will allow the wrapped function (handler) to be called 'n' times.
+ After that, this will return None for any additional calls,
+ allowing other handlers, if any, to be invoked.
+ """
+
+ class static:
+ retries = n
+
+ def decorator(func):
+ @wraps(func)
+ def wrapped(*args, **kwargs):
+ if static.retries == 0:
+ return None
+ static.retries -= 1
+ return func(*args, **kwargs)
+ return wrapped
+ return decorator
+
+
+def filter_request(methods, resource):
+ """Decorator to invoke handler once for a specific resource.
+
+ This will call the handler only for a specific resource using
+ a specific method(s). Any other resource request or method will
+ return None, allowing other handlers, if any, to be invoked.
+ """
+
+ class static:
+ target_methods = [m.upper() for m in methods]
+ target_resource = resource
+
+ def decorator(func):
+ @wraps(func)
+ def wrapped(*args, **kwargs):
+ if (args[1].method in static.target_methods and
+ static.target_resource in args[0].path):
+ return func(*args, **kwargs)
+ else:
+ return None # Not for this resource
+ return wrapped
+ return decorator
+
+
+@httmock.urlmatch(netloc=r'localhost')
+def token(url, request):
+ if 'auth/token-services' in url.path:
+ return {'status_code': requests.codes.OK,
+ 'content': {'token-id': 'dummy-token'}}
+
+
+@httmock.urlmatch(netloc=r'localhost')
+def token_unauthorized(url, request):
+ if 'auth/token-services' in url.path:
+ return {'status_code': requests.codes.UNAUTHORIZED}
+
+
+@httmock.urlmatch(netloc=r'wrong-host')
+def token_wrong_host(url, request):
+ raise r_exc.ConnectionError()
+
+
+@httmock.all_requests
+def token_timeout(url, request):
+ raise r_exc.Timeout()
+
+
+@filter_request(['get'], 'global/host-name')
+@httmock.all_requests
+def timeout(url, request):
+ """Simulated timeout of a normal request."""
+
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ raise r_exc.Timeout()
+
+
+@httmock.urlmatch(netloc=r'localhost')
+def no_such_resource(url, request):
+ """Indicate not found error, when invalid resource requested."""
+ return {'status_code': requests.codes.NOT_FOUND}
+
+
+@filter_request(['get'], 'global/host-name')
+@repeat(1)
+@httmock.urlmatch(netloc=r'localhost')
+def expired_request(url, request):
+ """Simulate access denied failure on first request for this resource.
+
+ Intent here is to simulate that the token has expired, by failing
+ the first request to the resource. Because of the repeat=1, this
+ will only be called once, and subsequent calls will not be handled
+ by this function, but instead will access the normal handler and
+ will pass. Currently configured for a GET request, but will work
+ with POST and PUT as well. For DELETE, would need to filter_request on a
+ different resource (e.g. 'global/local-users')
+ """
+
+ return {'status_code': requests.codes.UNAUTHORIZED}
+
+
+@httmock.urlmatch(netloc=r'localhost')
+def normal_get(url, request):
+ if request.method != 'GET':
+ return
+ LOG.debug("DEBUG: GET mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ if 'global/host-name' in url.path:
+ content = {u'kind': u'object#host-name',
+ u'host-name': u'Router'}
+ return httmock.response(requests.codes.OK, content=content)
+ if 'global/local-users' in url.path:
+ content = {u'kind': u'collection#local-user',
+ u'users': ['peter', 'paul', 'mary']}
+ return httmock.response(requests.codes.OK, content=content)
+ if 'interfaces/GigabitEthernet' in url.path:
+ actual_interface = url.path.split('/')[-1]
+ ip = actual_interface[-1]
+ content = {u'kind': u'object#interface',
+ u'description': u'Changed description',
+ u'if-name': actual_interface,
+ u'proxy-arp': True,
+ u'subnet-mask': u'255.255.255.0',
+ u'icmp-unreachable': True,
+ u'nat-direction': u'',
+ u'icmp-redirects': True,
+ u'ip-address': u'192.168.200.%s' % ip,
+ u'verify-unicast-source': False,
+ u'type': u'ethernet'}
+ return httmock.response(requests.codes.OK, content=content)
+ if 'vpn-svc/ike/policies/2' in url.path:
+ content = {u'kind': u'object#ike-policy',
+ u'priority-id': u'2',
+ u'version': u'v1',
+ u'local-auth-method': u'pre-share',
+ u'encryption': u'aes',
+ u'hash': u'sha',
+ u'dhGroup': 5,
+ u'lifetime': 3600}
+ return httmock.response(requests.codes.OK, content=content)
+ if 'vpn-svc/ike/keyrings' in url.path:
+ content = {u'kind': u'object#ike-keyring',
+ u'keyring-name': u'5',
+ u'pre-shared-key-list': [
+ {u'key': u'super-secret',
+ u'encrypted': False,
+ u'peer-address': u'10.10.10.20 255.255.255.0'}
+ ]}
+ return httmock.response(requests.codes.OK, content=content)
+ if 'vpn-svc/ipsec/policies/' in url.path:
+ ipsec_policy_id = url.path.split('/')[-1]
+ content = {u'kind': u'object#ipsec-policy',
+ u'mode': u'tunnel',
+ u'policy-id': u'%s' % ipsec_policy_id,
+ u'protection-suite': {
+ u'esp-encryption': u'esp-aes',
+ u'esp-authentication': u'esp-sha-hmac',
+ u'ah': u'ah-sha-hmac',
+ },
+ u'anti-replay-window-size': u'128',
+ u'lifetime-sec': 120,
+ u'pfs': u'group5',
+ u'lifetime-kb': 4608000,
+ u'idle-time': None}
+ return httmock.response(requests.codes.OK, content=content)
+ if 'vpn-svc/site-to-site/Tunnel' in url.path:
+ tunnel = url.path.split('/')[-1]
+ # Use same number, to allow mock to generate IPSec policy ID
+ ipsec_policy_id = tunnel[6:]
+ content = {u'kind': u'object#vpn-site-to-site',
+ u'vpn-interface-name': u'%s' % tunnel,
+ u'ip-version': u'ipv4',
+ u'vpn-type': u'site-to-site',
+ u'ipsec-policy-id': u'%s' % ipsec_policy_id,
+ u'ike-profile-id': None,
+ u'mtu': 1500,
+ u'local-device': {
+ u'ip-address': '10.3.0.1/24',
+ u'tunnel-ip-address': '10.10.10.10'
+ },
+ u'remote-device': {
+ u'tunnel-ip-address': '10.10.10.20'
+ }}
+ return httmock.response(requests.codes.OK, content=content)
+ if 'vpn-svc/ike/keepalive' in url.path:
+ content = {u'interval': 60,
+ u'retry': 4,
+ u'periodic': True}
+ return httmock.response(requests.codes.OK, content=content)
+ if 'routing-svc/static-routes' in url.path:
+ content = {u'destination-network': u'10.1.0.0/24',
+ u'kind': u'object#static-route',
+ u'next-hop-router': None,
+ u'outgoing-interface': u'GigabitEthernet1',
+ u'admin-distance': 1}
+ return httmock.response(requests.codes.OK, content=content)
+ if 'vpn-svc/site-to-site/active/sessions':
+ # Only including needed fields for mock
+ content = {u'kind': u'collection#vpn-active-sessions',
+ u'items': [{u'status': u'DOWN-NEGOTIATING',
+ u'vpn-interface-name': u'Tunnel123'}, ]}
+ return httmock.response(requests.codes.OK, content=content)
+
+
+@filter_request(['get'], 'vpn-svc/ike/keyrings')
+@httmock.urlmatch(netloc=r'localhost')
+def get_fqdn(url, request):
+ LOG.debug("DEBUG: GET FQDN mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ content = {u'kind': u'object#ike-keyring',
+ u'keyring-name': u'5',
+ u'pre-shared-key-list': [
+ {u'key': u'super-secret',
+ u'encrypted': False,
+ u'peer-address': u'cisco.com'}
+ ]}
+ return httmock.response(requests.codes.OK, content=content)
+
+
+@filter_request(['get'], 'vpn-svc/ipsec/policies/')
+@httmock.urlmatch(netloc=r'localhost')
+def get_no_ah(url, request):
+ LOG.debug("DEBUG: GET No AH mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ ipsec_policy_id = url.path.split('/')[-1]
+ content = {u'kind': u'object#ipsec-policy',
+ u'mode': u'tunnel',
+ u'anti-replay-window-size': u'128',
+ u'policy-id': u'%s' % ipsec_policy_id,
+ u'protection-suite': {
+ u'esp-encryption': u'esp-aes',
+ u'esp-authentication': u'esp-sha-hmac',
+ },
+ u'lifetime-sec': 120,
+ u'pfs': u'group5',
+ u'lifetime-kb': 4608000,
+ u'idle-time': None}
+ return httmock.response(requests.codes.OK, content=content)
+
+
+@httmock.urlmatch(netloc=r'localhost')
+def get_defaults(url, request):
+ if request.method != 'GET':
+ return
+ LOG.debug("DEBUG: GET mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ if 'vpn-svc/ike/policies/2' in url.path:
+ content = {u'kind': u'object#ike-policy',
+ u'priority-id': u'2',
+ u'version': u'v1',
+ u'local-auth-method': u'pre-share',
+ u'encryption': u'des',
+ u'hash': u'sha',
+ u'dhGroup': 1,
+ u'lifetime': 86400}
+ return httmock.response(requests.codes.OK, content=content)
+ if 'vpn-svc/ipsec/policies/' in url.path:
+ ipsec_policy_id = url.path.split('/')[-1]
+ content = {u'kind': u'object#ipsec-policy',
+ u'mode': u'tunnel',
+ u'policy-id': u'%s' % ipsec_policy_id,
+ u'protection-suite': {},
+ u'lifetime-sec': 3600,
+ u'pfs': u'Disable',
+ u'anti-replay-window-size': u'None',
+ u'lifetime-kb': 4608000,
+ u'idle-time': None}
+ return httmock.response(requests.codes.OK, content=content)
+
+
+@filter_request(['get'], 'vpn-svc/site-to-site')
+@httmock.urlmatch(netloc=r'localhost')
+def get_unnumbered(url, request):
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ if FIXED_CSCum50512:
+ tunnel = url.path.split('/')[-1]
+ ipsec_policy_id = tunnel[6:]
+ content = {u'kind': u'object#vpn-site-to-site',
+ u'vpn-interface-name': u'%s' % tunnel,
+ u'ip-version': u'ipv4',
+ u'vpn-type': u'site-to-site',
+ u'ipsec-policy-id': u'%s' % ipsec_policy_id,
+ u'ike-profile-id': None,
+ u'mtu': 1500,
+ u'local-device': {
+ u'ip-address': u'unnumbered GigabitEthernet3',
+ u'tunnel-ip-address': u'10.10.10.10'
+ },
+ u'remote-device': {
+ u'tunnel-ip-address': u'10.10.10.20'
+ }}
+ return httmock.response(requests.codes.OK, content=content)
+ else:
+ return httmock.response(requests.codes.INTERNAL_SERVER_ERROR)
+
+
+@filter_request(['get'], 'vpn-svc/site-to-site')
+@httmock.urlmatch(netloc=r'localhost')
+def get_mtu(url, request):
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ tunnel = url.path.split('/')[-1]
+ ipsec_policy_id = tunnel[6:]
+ content = {u'kind': u'object#vpn-site-to-site',
+ u'vpn-interface-name': u'%s' % tunnel,
+ u'ip-version': u'ipv4',
+ u'vpn-type': u'site-to-site',
+ u'ipsec-policy-id': u'%s' % ipsec_policy_id,
+ u'ike-profile-id': None,
+ u'mtu': 9192,
+ u'local-device': {
+ u'ip-address': u'10.3.0.1/24',
+ u'tunnel-ip-address': u'10.10.10.10'
+ },
+ u'remote-device': {
+ u'tunnel-ip-address': u'10.10.10.20'
+ }}
+ return httmock.response(requests.codes.OK, content=content)
+
+
+@filter_request(['get'], 'vpn-svc/ike/keepalive')
+@httmock.urlmatch(netloc=r'localhost')
+def get_not_configured(url, request):
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ return {'status_code': requests.codes.NOT_FOUND}
+
+
+@filter_request(['get'], 'vpn-svc/site-to-site/active/sessions')
+@httmock.urlmatch(netloc=r'localhost')
+def get_none(url, request):
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ content = {u'kind': u'collection#vpn-active-sessions',
+ u'items': []}
+ return httmock.response(requests.codes.OK, content=content)
+
+
+@httmock.urlmatch(netloc=r'localhost')
+def post(url, request):
+ if request.method != 'POST':
+ return
+ LOG.debug("DEBUG: POST mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ if 'interfaces/GigabitEthernet' in url.path:
+ return {'status_code': requests.codes.NO_CONTENT}
+ if 'global/local-users' in url.path:
+ if 'username' not in request.body:
+ return {'status_code': requests.codes.BAD_REQUEST}
+ if '"privilege": 20' in request.body:
+ return {'status_code': requests.codes.BAD_REQUEST}
+ headers = {'location': '%s/test-user' % url.geturl()}
+ return httmock.response(requests.codes.CREATED, headers=headers)
+ if 'vpn-svc/ike/policies' in url.path:
+ headers = {'location': "%s/2" % url.geturl()}
+ return httmock.response(requests.codes.CREATED, headers=headers)
+ if 'vpn-svc/ipsec/policies' in url.path:
+ m = re.search(r'"policy-id": "(\S+)"', request.body)
+ if m:
+ headers = {'location': "%s/%s" % (url.geturl(), m.group(1))}
+ return httmock.response(requests.codes.CREATED, headers=headers)
+ return {'status_code': requests.codes.BAD_REQUEST}
+ if 'vpn-svc/ike/keyrings' in url.path:
+ headers = {'location': "%s/5" % url.geturl()}
+ return httmock.response(requests.codes.CREATED, headers=headers)
+ if 'vpn-svc/site-to-site' in url.path:
+ m = re.search(r'"vpn-interface-name": "(\S+)"', request.body)
+ if m:
+ headers = {'location': "%s/%s" % (url.geturl(), m.group(1))}
+ return httmock.response(requests.codes.CREATED, headers=headers)
+ return {'status_code': requests.codes.BAD_REQUEST}
+ if 'routing-svc/static-routes' in url.path:
+ headers = {'location':
+ "%s/10.1.0.0_24_GigabitEthernet1" % url.geturl()}
+ return httmock.response(requests.codes.CREATED, headers=headers)
+
+
+@filter_request(['post'], 'global/local-users')
+@httmock.urlmatch(netloc=r'localhost')
+def post_change_attempt(url, request):
+ LOG.debug("DEBUG: POST change value mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ return {'status_code': requests.codes.NOT_FOUND,
+ 'content': {
+ u'error-code': -1,
+ u'error-message': u'user test-user already exists'}}
+
+
+@httmock.urlmatch(netloc=r'localhost')
+def post_duplicate(url, request):
+ LOG.debug("DEBUG: POST duplicate mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ return {'status_code': requests.codes.BAD_REQUEST,
+ 'content': {
+ u'error-code': -1,
+ u'error-message': u'policy 2 exist, not allow to '
+ u'update policy using POST method'}}
+
+
+@filter_request(['post'], 'vpn-svc/site-to-site')
+@httmock.urlmatch(netloc=r'localhost')
+def post_missing_ipsec_policy(url, request):
+ LOG.debug("DEBUG: POST missing ipsec policy mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ return {'status_code': requests.codes.BAD_REQUEST}
+
+
+@filter_request(['post'], 'vpn-svc/site-to-site')
+@httmock.urlmatch(netloc=r'localhost')
+def post_missing_ike_policy(url, request):
+ LOG.debug("DEBUG: POST missing ike policy mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ return {'status_code': requests.codes.BAD_REQUEST}
+
+
+@filter_request(['post'], 'vpn-svc/site-to-site')
+@httmock.urlmatch(netloc=r'localhost')
+def post_bad_ip(url, request):
+ LOG.debug("DEBUG: POST bad IP mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ return {'status_code': requests.codes.BAD_REQUEST}
+
+
+@filter_request(['post'], 'vpn-svc/site-to-site')
+@httmock.urlmatch(netloc=r'localhost')
+def post_bad_mtu(url, request):
+ LOG.debug("DEBUG: POST bad mtu mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ return {'status_code': requests.codes.BAD_REQUEST}
+
+
+@filter_request(['post'], 'vpn-svc/ipsec/policies')
+@httmock.urlmatch(netloc=r'localhost')
+def post_bad_lifetime(url, request):
+ LOG.debug("DEBUG: POST bad lifetime mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ return {'status_code': requests.codes.BAD_REQUEST}
+
+
+@httmock.urlmatch(netloc=r'localhost')
+def put(url, request):
+ if request.method != 'PUT':
+ return
+ LOG.debug("DEBUG: PUT mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ # Any resource
+ return {'status_code': requests.codes.NO_CONTENT}
+
+
+@httmock.urlmatch(netloc=r'localhost')
+def delete(url, request):
+ if request.method != 'DELETE':
+ return
+ LOG.debug("DEBUG: DELETE mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ # Any resource
+ return {'status_code': requests.codes.NO_CONTENT}
+
+
+@httmock.urlmatch(netloc=r'localhost')
+def delete_unknown(url, request):
+ if request.method != 'DELETE':
+ return
+ LOG.debug("DEBUG: DELETE unknown mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ # Any resource
+ return {'status_code': requests.codes.NOT_FOUND,
+ 'content': {
+ u'error-code': -1,
+ u'error-message': 'user unknown not found'}}
+
+
+@httmock.urlmatch(netloc=r'localhost')
+def delete_not_allowed(url, request):
+ if request.method != 'DELETE':
+ return
+ LOG.debug("DEBUG: DELETE not allowed mock for %s", url)
+ if not request.headers.get('X-auth-token', None):
+ return {'status_code': requests.codes.UNAUTHORIZED}
+ # Any resource
+ return {'status_code': requests.codes.METHOD_NOT_ALLOWED}
--- /dev/null
+# 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.
+#
+# @author: Paul Michali, Cisco Systems, Inc.
+
+#TODO(pcm): Rename this file to remove the "no" prefix, once httmock is
+# approved and added to requirements.txt
+
+import random
+
+import httmock
+import requests
+
+from neutron.openstack.common import log as logging
+from neutron.services.vpn.device_drivers import (
+ cisco_csr_rest_client as csr_client)
+from neutron.tests import base
+from neutron.tests.unit.services.vpn.device_drivers import (
+ cisco_csr_mock as csr_request)
+# TODO(pcm) Remove once httmock is available. In the meantime, use temp
+# copy of hhtmock source to run UT
+# from neutron.tests.unit.services.vpn.device_drivers import httmock
+
+
+LOG = logging.getLogger(__name__)
+# Enables debug logging to console
+if True:
+ logging.CONF.set_override('debug', True)
+ logging.setup('neutron')
+
+if csr_request.FIXED_CSCum35484:
+ dummy_uuid = '1eb4ee6b-0870-45a0-b554-7b69096'
+else:
+ dummy_uuid = '1eb4ee6b-0870-45a0-b554-7b'
+
+
+# Note: Helper functions to test reuse of IDs.
+def generate_pre_shared_key_id():
+ return random.randint(100, 200)
+
+
+def generate_ike_policy_id():
+ return random.randint(200, 300)
+
+
+def generate_ipsec_policy_id():
+ return random.randint(300, 400)
+
+
+class TestCsrLoginRestApi(base.BaseTestCase):
+
+ """Test logging into CSR to obtain token-id."""
+
+ def setUp(self):
+ super(TestCsrLoginRestApi, self).setUp()
+ self.csr = csr_client.CsrRestClient('localhost', '10.10.10.10',
+ 'stack', 'cisco')
+
+ def test_get_token(self):
+ """Obtain the token and its expiration time."""
+ with httmock.HTTMock(csr_request.token):
+ self.assertTrue(self.csr.authenticate())
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ self.assertIsNotNone(self.csr.token)
+
+ def test_unauthorized_token_request(self):
+ """Negative test of invalid user/password."""
+ self.csr.auth = ('stack', 'bogus')
+ with httmock.HTTMock(csr_request.token_unauthorized):
+ self.assertIsNone(self.csr.authenticate())
+ self.assertEqual(requests.codes.UNAUTHORIZED, self.csr.status)
+
+ def test_non_existent_host(self):
+ """Negative test of request to non-existent host."""
+ self.csr.host = 'wrong-host'
+ self.csr.token = 'Set by some previously successful access'
+ with httmock.HTTMock(csr_request.token_wrong_host):
+ self.assertIsNone(self.csr.authenticate())
+ self.assertEqual(requests.codes.NOT_FOUND, self.csr.status)
+ self.assertIsNone(self.csr.token)
+
+ def test_timeout_on_token_access(self):
+ """Negative test of a timeout on a request."""
+ with httmock.HTTMock(csr_request.token_timeout):
+ self.assertIsNone(self.csr.authenticate())
+ self.assertEqual(requests.codes.REQUEST_TIMEOUT, self.csr.status)
+ self.assertIsNone(self.csr.token)
+
+
+class TestCsrGetRestApi(base.BaseTestCase):
+
+ """Test CSR GET REST API."""
+
+ def setUp(self):
+ super(TestCsrGetRestApi, self).setUp()
+ self.csr = csr_client.CsrRestClient('localhost', '10.10.10.10',
+ 'stack', 'cisco')
+
+ def test_valid_rest_gets(self):
+ """Simple GET requests.
+
+ First request will do a post to get token (login). Assumes
+ that there are two interfaces on the CSR.
+ """
+
+ with httmock.HTTMock(csr_request.token, csr_request.normal_get):
+ content = self.csr.get_request('global/host-name')
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ self.assertIn('host-name', content)
+ self.assertNotEqual(None, content['host-name'])
+
+ content = self.csr.get_request('global/local-users')
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ self.assertIn('users', content)
+
+
+class TestCsrPostRestApi(base.BaseTestCase):
+
+ """Test CSR POST REST API."""
+
+ def setUp(self):
+ super(TestCsrPostRestApi, self).setUp()
+ self.csr = csr_client.CsrRestClient('localhost', '10.10.10.10',
+ 'stack', 'cisco')
+
+ def test_post_requests(self):
+ """Simple POST requests (repeatable).
+
+ First request will do a post to get token (login). Assumes
+ that there are two interfaces (Ge1 and Ge2) on the CSR.
+ """
+
+ with httmock.HTTMock(csr_request.token, csr_request.post):
+ content = self.csr.post_request(
+ 'interfaces/GigabitEthernet1/statistics',
+ payload={'action': 'clear'})
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ self.assertIsNone(content)
+ content = self.csr.post_request(
+ 'interfaces/GigabitEthernet2/statistics',
+ payload={'action': 'clear'})
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ self.assertIsNone(content)
+
+ def test_post_with_location(self):
+ """Create a user and verify that location returned."""
+ with httmock.HTTMock(csr_request.token, csr_request.post):
+ location = self.csr.post_request(
+ 'global/local-users',
+ payload={'username': 'test-user',
+ 'password': 'pass12345',
+ 'privilege': 15})
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('global/local-users/test-user', location)
+
+ def test_post_missing_required_attribute(self):
+ """Negative test of POST with missing mandatory info."""
+ with httmock.HTTMock(csr_request.token, csr_request.post):
+ self.csr.post_request('global/local-users',
+ payload={'password': 'pass12345',
+ 'privilege': 15})
+ self.assertEqual(requests.codes.BAD_REQUEST, self.csr.status)
+
+ def test_post_invalid_attribute(self):
+ """Negative test of POST with invalid info."""
+ with httmock.HTTMock(csr_request.token, csr_request.post):
+ self.csr.post_request('global/local-users',
+ payload={'username': 'test-user',
+ 'password': 'pass12345',
+ 'privilege': 20})
+ self.assertEqual(requests.codes.BAD_REQUEST, self.csr.status)
+
+ def test_post_already_exists(self):
+ """Negative test of a duplicate POST.
+
+ Uses the lower level _do_request() API to just perform the POST and
+ obtain the response, without any error processing.
+ """
+ with httmock.HTTMock(csr_request.token, csr_request.post):
+ location = self.csr._do_request(
+ 'POST',
+ 'global/local-users',
+ payload={'username': 'test-user',
+ 'password': 'pass12345',
+ 'privilege': 15},
+ more_headers=csr_client.HEADER_CONTENT_TYPE_JSON)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('global/local-users/test-user', location)
+ with httmock.HTTMock(csr_request.token,
+ csr_request.post_change_attempt):
+ self.csr._do_request(
+ 'POST',
+ 'global/local-users',
+ payload={'username': 'test-user',
+ 'password': 'pass12345',
+ 'privilege': 15},
+ more_headers=csr_client.HEADER_CONTENT_TYPE_JSON)
+ # Note: For local-user, a 404 error is returned. For
+ # site-to-site connection a 400 is returned.
+ self.assertEqual(requests.codes.NOT_FOUND, self.csr.status)
+
+ def test_post_changing_value(self):
+ """Negative test of a POST trying to change a value."""
+ with httmock.HTTMock(csr_request.token, csr_request.post):
+ location = self.csr.post_request(
+ 'global/local-users',
+ payload={'username': 'test-user',
+ 'password': 'pass12345',
+ 'privilege': 15})
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('global/local-users/test-user', location)
+ with httmock.HTTMock(csr_request.token,
+ csr_request.post_change_attempt):
+ content = self.csr.post_request('global/local-users',
+ payload={'username': 'test-user',
+ 'password': 'changed',
+ 'privilege': 15})
+ self.assertEqual(requests.codes.NOT_FOUND, self.csr.status)
+ expected = {u'error-code': -1,
+ u'error-message': u'user test-user already exists'}
+ self.assertDictContainsSubset(expected, content)
+
+
+class TestCsrPutRestApi(base.BaseTestCase):
+
+ """Test CSR PUT REST API."""
+
+ def _save_resources(self):
+ with httmock.HTTMock(csr_request.token, csr_request.normal_get):
+ details = self.csr.get_request('global/host-name')
+ if self.csr.status != requests.codes.OK:
+ self.fail("Unable to save original host name")
+ self.original_host = details['host-name']
+ details = self.csr.get_request('interfaces/GigabitEthernet1')
+ if self.csr.status != requests.codes.OK:
+ self.fail("Unable to save interface Ge1 description")
+ self.original_if = details
+ if details.get('description', ''):
+ self.original_if['description'] = ''
+ self.csr.token = None
+
+ def _restore_resources(self, user, password):
+ """Restore the host name and itnerface description.
+
+ Must restore the user and password, so that authentication
+ token can be obtained (as some tests corrupt auth info).
+ Will also clear token, so that it gets a fresh token.
+ """
+
+ self.csr.auth = (user, password)
+ self.csr.token = None
+ with httmock.HTTMock(csr_request.token, csr_request.put):
+ payload = {'host-name': self.original_host}
+ self.csr.put_request('global/host-name', payload=payload)
+ if self.csr.status != requests.codes.NO_CONTENT:
+ self.fail("Unable to restore host name after test")
+ payload = {'description': self.original_if['description'],
+ 'if-name': self.original_if['if-name'],
+ 'ip-address': self.original_if['ip-address'],
+ 'subnet-mask': self.original_if['subnet-mask'],
+ 'type': self.original_if['type']}
+ self.csr.put_request('interfaces/GigabitEthernet1',
+ payload=payload)
+ if self.csr.status != requests.codes.NO_CONTENT:
+ self.fail("Unable to restore I/F Ge1 description after test")
+
+ def setUp(self):
+ """Prepare for PUT API tests."""
+ super(TestCsrPutRestApi, self).setUp()
+ self.csr = csr_client.CsrRestClient('localhost', '10.10.10.10',
+ 'stack', 'cisco')
+ self._save_resources()
+ self.addCleanup(self._restore_resources, 'stack', 'cisco')
+
+ def test_put_requests(self):
+ """Simple PUT requests (repeatable).
+
+ First request will do a post to get token (login). Assumes
+ that there are two interfaces on the CSR (Ge1 and Ge2).
+ """
+
+ with httmock.HTTMock(csr_request.token, csr_request.put,
+ csr_request.normal_get):
+ payload = {'host-name': 'TestHost'}
+ content = self.csr.put_request('global/host-name',
+ payload=payload)
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ self.assertIsNone(content)
+
+ payload = {'host-name': 'TestHost2'}
+ content = self.csr.put_request('global/host-name',
+ payload=payload)
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ self.assertIsNone(content)
+
+ def test_change_interface_description(self):
+ """Test that interface description can be changed.
+
+ This was a problem with an earlier version of the CSR image and is
+ here to prevent regression.
+ """
+ with httmock.HTTMock(csr_request.token, csr_request.put,
+ csr_request.normal_get):
+ payload = {'description': u'Changed description',
+ 'if-name': self.original_if['if-name'],
+ 'ip-address': self.original_if['ip-address'],
+ 'subnet-mask': self.original_if['subnet-mask'],
+ 'type': self.original_if['type']}
+ content = self.csr.put_request(
+ 'interfaces/GigabitEthernet1', payload=payload)
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ self.assertIsNone(content)
+ content = self.csr.get_request('interfaces/GigabitEthernet1')
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ self.assertIn('description', content)
+ self.assertEqual(u'Changed description',
+ content['description'])
+
+ def ignore_test_change_to_empty_interface_description(self):
+ """Test that interface description can be changed to empty string.
+
+ This is a problem in the current version of the CSR image, which
+ rejects the change with a 400 error. This test is here to prevent
+ a regression (once it is fixed) Note that there is code in the
+ test setup to change the description to a non-empty string to
+ avoid failures in other tests.
+ """
+ with httmock.HTTMock(csr_request.token, csr_request.put,
+ csr_request.normal_get):
+ payload = {'description': '',
+ 'if-name': self.original_if['if-name'],
+ 'ip-address': self.original_if['ip-address'],
+ 'subnet-mask': self.original_if['subnet-mask'],
+ 'type': self.original_if['type']}
+ content = self.csr.put_request(
+ 'interfaces/GigabitEthernet1', payload=payload)
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ self.assertIsNone(content)
+ content = self.csr.get_request('interfaces/GigabitEthernet1')
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ self.assertIn('description', content)
+ self.assertEqual('', content['description'])
+
+
+class TestCsrDeleteRestApi(base.BaseTestCase):
+
+ """Test CSR DELETE REST API."""
+
+ def setUp(self):
+ super(TestCsrDeleteRestApi, self).setUp()
+ self.csr = csr_client.CsrRestClient('localhost', '10.10.10.10',
+ 'stack', 'cisco')
+
+ def _make_dummy_user(self):
+ """Create a user that will be later deleted."""
+ self.csr.post_request('global/local-users',
+ payload={'username': 'dummy',
+ 'password': 'dummy',
+ 'privilege': 15})
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+
+ def test_delete_requests(self):
+ """Simple DELETE requests (creating entry first)."""
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.delete):
+ self._make_dummy_user()
+ self.csr.token = None # Force login
+ self.csr.delete_request('global/local-users/dummy')
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ # Delete again, but without logging in this time
+ self._make_dummy_user()
+ self.csr.delete_request('global/local-users/dummy')
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+
+ def test_delete_non_existent_entry(self):
+ """Negative test of trying to delete a non-existent user."""
+ with httmock.HTTMock(csr_request.token, csr_request.delete_unknown):
+ content = self.csr.delete_request('global/local-users/unknown')
+ self.assertEqual(requests.codes.NOT_FOUND, self.csr.status)
+ expected = {u'error-code': -1,
+ u'error-message': u'user unknown not found'}
+ self.assertDictContainsSubset(expected, content)
+
+ def test_delete_not_allowed(self):
+ """Negative test of trying to delete the host-name."""
+ with httmock.HTTMock(csr_request.token,
+ csr_request.delete_not_allowed):
+ self.csr.delete_request('global/host-name')
+ self.assertEqual(requests.codes.METHOD_NOT_ALLOWED,
+ self.csr.status)
+
+
+class TestCsrRestApiFailures(base.BaseTestCase):
+
+ """Test failure cases common for all REST APIs.
+
+ Uses the lower level _do_request() to just perform the operation and get
+ the result, without any error handling.
+ """
+
+ def setUp(self):
+ super(TestCsrRestApiFailures, self).setUp()
+ self.csr = csr_client.CsrRestClient('localhost', '10.10.10.10',
+ 'stack', 'cisco', timeout=0.1)
+
+ def test_request_for_non_existent_resource(self):
+ """Negative test of non-existent resource on REST request."""
+ with httmock.HTTMock(csr_request.token, csr_request.no_such_resource):
+ self.csr.post_request('no/such/request')
+ self.assertEqual(requests.codes.NOT_FOUND, self.csr.status)
+ # The result is HTTP 404 message, so no error content to check
+
+ def test_timeout_during_request(self):
+ """Negative test of timeout during REST request."""
+ with httmock.HTTMock(csr_request.token, csr_request.timeout):
+ self.csr._do_request('GET', 'global/host-name')
+ self.assertEqual(requests.codes.REQUEST_TIMEOUT, self.csr.status)
+
+ def test_token_expired_on_request(self):
+ """Token expired before trying a REST request.
+
+ The mock is configured to return a 401 error on the first
+ attempt to reference the host name. Simulate expiration of
+ token by changing it.
+ """
+
+ with httmock.HTTMock(csr_request.token, csr_request.expired_request,
+ csr_request.normal_get):
+ self.csr.token = '123' # These are 44 characters, so won't match
+ content = self.csr._do_request('GET', 'global/host-name')
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ self.assertIn('host-name', content)
+ self.assertNotEqual(None, content['host-name'])
+
+ def test_failed_to_obtain_token_for_request(self):
+ """Negative test of unauthorized user for REST request."""
+ self.csr.auth = ('stack', 'bogus')
+ with httmock.HTTMock(csr_request.token_unauthorized):
+ self.csr._do_request('GET', 'global/host-name')
+ self.assertEqual(requests.codes.UNAUTHORIZED, self.csr.status)
+
+
+class TestCsrRestIkePolicyCreate(base.BaseTestCase):
+
+ """Test IKE policy create REST requests."""
+
+ def setUp(self):
+ super(TestCsrRestIkePolicyCreate, self).setUp()
+ self.csr = csr_client.CsrRestClient('localhost', '10.10.10.10',
+ 'stack', 'cisco')
+
+ def test_create_delete_ike_policy(self):
+ """Create and then delete IKE policy."""
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.normal_get):
+ policy_id = '2'
+ policy_info = {u'priority-id': u'%s' % policy_id,
+ u'encryption': u'aes',
+ u'hash': u'sha',
+ u'dhGroup': 5,
+ u'lifetime': 3600}
+ location = self.csr.create_ike_policy(policy_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('vpn-svc/ike/policies/%s' % policy_id, location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_policy = {u'kind': u'object#ike-policy',
+ u'version': u'v1',
+ u'local-auth-method': u'pre-share'}
+ expected_policy.update(policy_info)
+ self.assertEqual(expected_policy, content)
+ # Now delete and verify the IKE policy is gone
+ with httmock.HTTMock(csr_request.token, csr_request.delete,
+ csr_request.no_such_resource):
+ self.csr.delete_ike_policy(policy_id)
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.NOT_FOUND, self.csr.status)
+
+ def test_create_ike_policy_with_defaults(self):
+ """Create IKE policy using defaults for all optional values."""
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.get_defaults):
+ policy_id = '2'
+ policy_info = {u'priority-id': u'%s' % policy_id}
+ location = self.csr.create_ike_policy(policy_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('vpn-svc/ike/policies/%s' % policy_id, location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_policy = {u'kind': u'object#ike-policy',
+ u'version': u'v1',
+ u'encryption': u'des',
+ u'hash': u'sha',
+ u'dhGroup': 1,
+ u'lifetime': 86400,
+ # Lower level sets this, but it is the default
+ u'local-auth-method': u'pre-share'}
+ expected_policy.update(policy_info)
+ self.assertEqual(expected_policy, content)
+
+ def test_create_duplicate_ike_policy(self):
+ """Negative test of trying to create a duplicate IKE policy."""
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.normal_get):
+ policy_id = '2'
+ policy_info = {u'priority-id': u'%s' % policy_id,
+ u'encryption': u'aes',
+ u'hash': u'sha',
+ u'dhGroup': 5,
+ u'lifetime': 3600}
+ location = self.csr.create_ike_policy(policy_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('vpn-svc/ike/policies/%s' % policy_id, location)
+ with httmock.HTTMock(csr_request.token, csr_request.post_duplicate):
+ location = self.csr.create_ike_policy(policy_info)
+ self.assertEqual(requests.codes.BAD_REQUEST, self.csr.status)
+ expected = {u'error-code': -1,
+ u'error-message': u'policy 2 exist, not allow to '
+ u'update policy using POST method'}
+ self.assertDictContainsSubset(expected, location)
+
+
+class TestCsrRestIPSecPolicyCreate(base.BaseTestCase):
+
+ """Test IPSec policy create REST requests."""
+
+ def setUp(self):
+ super(TestCsrRestIPSecPolicyCreate, self).setUp()
+ self.csr = csr_client.CsrRestClient('localhost', '10.10.10.10',
+ 'stack', 'cisco')
+
+ def test_create_delete_ipsec_policy(self):
+ """Create and then delete IPSec policy."""
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.normal_get):
+ policy_id = '123'
+ policy_info = {
+ u'policy-id': u'%s' % policy_id,
+ u'protection-suite': {
+ u'esp-encryption': u'esp-aes',
+ u'esp-authentication': u'esp-sha-hmac',
+ u'ah': u'ah-sha-hmac',
+ },
+ u'lifetime-sec': 120,
+ u'pfs': u'group5',
+ u'anti-replay-window-size': u'128'
+ }
+ location = self.csr.create_ipsec_policy(policy_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('vpn-svc/ipsec/policies/%s' % policy_id, location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_policy = {u'kind': u'object#ipsec-policy',
+ u'mode': u'tunnel',
+ u'lifetime-kb': 4608000,
+ u'idle-time': None}
+ expected_policy.update(policy_info)
+ self.assertEqual(expected_policy, content)
+ # Now delete and verify the IPSec policy is gone
+ with httmock.HTTMock(csr_request.token, csr_request.delete,
+ csr_request.no_such_resource):
+ self.csr.delete_ipsec_policy(policy_id)
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.NOT_FOUND, self.csr.status)
+
+ def test_create_ipsec_policy_with_defaults(self):
+ """Create IPSec policy with default for all optional values."""
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.get_defaults):
+ policy_id = '123'
+ policy_info = {
+ u'policy-id': u'%s' % policy_id,
+ }
+ location = self.csr.create_ipsec_policy(policy_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('vpn-svc/ipsec/policies/%s' % policy_id, location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_policy = {u'kind': u'object#ipsec-policy',
+ u'mode': u'tunnel',
+ u'protection-suite': {},
+ u'lifetime-sec': 3600,
+ u'pfs': u'Disable',
+ u'anti-replay-window-size': u'None',
+ u'lifetime-kb': 4608000,
+ u'idle-time': None}
+ expected_policy.update(policy_info)
+ self.assertEqual(expected_policy, content)
+
+ def test_create_ipsec_policy_with_uuid(self):
+ """Create IPSec policy using UUID for id."""
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.normal_get):
+ policy_info = {
+ u'policy-id': u'%s' % dummy_uuid,
+ u'protection-suite': {
+ u'esp-encryption': u'esp-aes',
+ u'esp-authentication': u'esp-sha-hmac',
+ u'ah': u'ah-sha-hmac',
+ },
+ u'lifetime-sec': 120,
+ u'pfs': u'group5',
+ u'anti-replay-window-size': u'128'
+ }
+ location = self.csr.create_ipsec_policy(policy_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('vpn-svc/ipsec/policies/%s' % dummy_uuid,
+ location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_policy = {u'kind': u'object#ipsec-policy',
+ u'mode': u'tunnel',
+ u'lifetime-kb': 4608000,
+ u'idle-time': None}
+ expected_policy.update(policy_info)
+ self.assertEqual(expected_policy, content)
+
+ def test_create_ipsec_policy_without_ah(self):
+ """Create IPSec policy."""
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.get_no_ah):
+ policy_id = '10'
+ policy_info = {
+ u'policy-id': u'%s' % policy_id,
+ u'protection-suite': {
+ u'esp-encryption': u'esp-aes',
+ u'esp-authentication': u'esp-sha-hmac',
+ },
+ u'lifetime-sec': 120,
+ u'pfs': u'group5',
+ u'anti-replay-window-size': u'128'
+ }
+ location = self.csr.create_ipsec_policy(policy_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('vpn-svc/ipsec/policies/%s' % policy_id, location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_policy = {u'kind': u'object#ipsec-policy',
+ u'mode': u'tunnel',
+ u'lifetime-kb': 4608000,
+ u'idle-time': None}
+ expected_policy.update(policy_info)
+ self.assertEqual(expected_policy, content)
+
+ def test_invalid_ipsec_policy_lifetime(self):
+ """Failure test of IPSec policy with unsupported lifetime."""
+ with httmock.HTTMock(csr_request.token,
+ csr_request.post_bad_lifetime):
+ policy_id = '123'
+ policy_info = {
+ u'policy-id': u'%s' % policy_id,
+ u'protection-suite': {
+ u'esp-encryption': u'esp-aes',
+ u'esp-authentication': u'esp-sha-hmac',
+ u'ah': u'ah-sha-hmac',
+ },
+ u'lifetime-sec': 119,
+ u'pfs': u'group5',
+ u'anti-replay-window-size': u'128'
+ }
+ self.csr.create_ipsec_policy(policy_info)
+ self.assertEqual(requests.codes.BAD_REQUEST, self.csr.status)
+
+
+class TestCsrRestPreSharedKeyCreate(base.BaseTestCase):
+
+ """Test Pre-shared key (PSK) create REST requests."""
+
+ def setUp(self):
+ super(TestCsrRestPreSharedKeyCreate, self).setUp()
+ self.csr = csr_client.CsrRestClient('localhost', '10.10.10.10',
+ 'stack', 'cisco')
+
+ def test_create_delete_pre_shared_key(self):
+ """Create and then delete a keyring entry for pre-shared key."""
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.normal_get):
+ psk_id = '5'
+ psk_info = {u'keyring-name': u'%s' % psk_id,
+ u'pre-shared-key-list': [
+ {u'key': u'super-secret',
+ u'encrypted': False,
+ u'peer-address': u'10.10.10.20/24'}
+ ]}
+ location = self.csr.create_pre_shared_key(psk_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('vpn-svc/ike/keyrings/%s' % psk_id, location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_policy = {u'kind': u'object#ike-keyring'}
+ expected_policy.update(psk_info)
+ # Note: the peer CIDR is returned as an IP and mask
+ expected_policy[u'pre-shared-key-list'][0][u'peer-address'] = (
+ u'10.10.10.20 255.255.255.0')
+ self.assertEqual(expected_policy, content)
+ # Now delete and verify pre-shared key is gone
+ with httmock.HTTMock(csr_request.token, csr_request.delete,
+ csr_request.no_such_resource):
+ self.csr.delete_pre_shared_key(psk_id)
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.NOT_FOUND, self.csr.status)
+
+ def test_create_pre_shared_key_with_fqdn_peer(self):
+ """Create pre-shared key using FQDN for peer address."""
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.get_fqdn):
+ psk_id = '5'
+ psk_info = {u'keyring-name': u'%s' % psk_id,
+ u'pre-shared-key-list': [
+ {u'key': u'super-secret',
+ u'encrypted': False,
+ u'peer-address': u'cisco.com'}
+ ]}
+ location = self.csr.create_pre_shared_key(psk_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('vpn-svc/ike/keyrings/%s' % psk_id, location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_policy = {u'kind': u'object#ike-keyring'}
+ expected_policy.update(psk_info)
+ self.assertEqual(expected_policy, content)
+
+ def test_create_pre_shared_key_with_duplicate_peer_address(self):
+ """Negative test of creating a second pre-shared key with same peer."""
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.normal_get):
+ psk_id = '5'
+ psk_info = {u'keyring-name': u'%s' % psk_id,
+ u'pre-shared-key-list': [
+ {u'key': u'super-secret',
+ u'encrypted': False,
+ u'peer-address': u'10.10.10.20/24'}
+ ]}
+ location = self.csr.create_pre_shared_key(psk_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('vpn-svc/ike/keyrings/%s' % psk_id, location)
+ with httmock.HTTMock(csr_request.token, csr_request.post_duplicate):
+ psk_id = u'6'
+ another_psk_info = {u'keyring-name': psk_id,
+ u'pre-shared-key-list': [
+ {u'key': u'abc123def',
+ u'encrypted': False,
+ u'peer-address': u'10.10.10.20/24'}
+ ]}
+ self.csr.create_ike_policy(another_psk_info)
+ self.assertEqual(requests.codes.BAD_REQUEST, self.csr.status)
+
+
+class TestCsrRestIPSecConnectionCreate(base.BaseTestCase):
+
+ """Test IPSec site-to-site connection REST requests.
+
+ This requires us to have first created an IKE policy, IPSec policy,
+ and pre-shared key, so it's more of an itegration test, when used
+ with a real CSR (as we can't mock out these pre-conditions.
+ """
+
+ def setUp(self):
+ super(TestCsrRestIPSecConnectionCreate, self).setUp()
+ self.csr = csr_client.CsrRestClient('localhost', '10.10.10.10',
+ 'stack', 'cisco')
+
+ def _make_psk_for_test(self):
+ psk_id = generate_pre_shared_key_id()
+ self._remove_resource_for_test(self.csr.delete_pre_shared_key,
+ psk_id)
+ with httmock.HTTMock(csr_request.token, csr_request.post):
+ psk_info = {u'keyring-name': u'%d' % psk_id,
+ u'pre-shared-key-list': [
+ {u'key': u'super-secret',
+ u'encrypted': False,
+ u'peer-address': u'10.10.10.20/24'}
+ ]}
+ self.csr.create_pre_shared_key(psk_info)
+ if self.csr.status != requests.codes.CREATED:
+ self.fail("Unable to create PSK for test case")
+ self.addCleanup(self._remove_resource_for_test,
+ self.csr.delete_pre_shared_key, psk_id)
+ return psk_id
+
+ def _make_ike_policy_for_test(self):
+ policy_id = generate_ike_policy_id()
+ self._remove_resource_for_test(self.csr.delete_ike_policy,
+ policy_id)
+ with httmock.HTTMock(csr_request.token, csr_request.post):
+ policy_info = {u'priority-id': u'%d' % policy_id,
+ u'encryption': u'aes',
+ u'hash': u'sha',
+ u'dhGroup': 5,
+ u'lifetime': 3600}
+ self.csr.create_ike_policy(policy_info)
+ if self.csr.status != requests.codes.CREATED:
+ self.fail("Unable to create IKE policy for test case")
+ self.addCleanup(self._remove_resource_for_test,
+ self.csr.delete_ike_policy, policy_id)
+ return policy_id
+
+ def _make_ipsec_policy_for_test(self):
+ policy_id = generate_ipsec_policy_id()
+ self._remove_resource_for_test(self.csr.delete_ipsec_policy,
+ policy_id)
+ with httmock.HTTMock(csr_request.token, csr_request.post):
+ policy_info = {
+ u'policy-id': u'%d' % policy_id,
+ u'protection-suite': {
+ u'esp-encryption': u'esp-aes',
+ u'esp-authentication': u'esp-sha-hmac',
+ u'ah': u'ah-sha-hmac',
+ },
+ u'lifetime-sec': 120,
+ u'pfs': u'group5',
+ u'anti-replay-window-size': u'64'
+ }
+ self.csr.create_ipsec_policy(policy_info)
+ if self.csr.status != requests.codes.CREATED:
+ self.fail("Unable to create IPSec policy for test case")
+ self.addCleanup(self._remove_resource_for_test,
+ self.csr.delete_ipsec_policy, policy_id)
+ return policy_id
+
+ def _remove_resource_for_test(self, delete_resource, resource_id):
+ with httmock.HTTMock(csr_request.token, csr_request.delete):
+ delete_resource(resource_id)
+
+ def _prepare_for_site_conn_create(self, skip_psk=False, skip_ike=False,
+ skip_ipsec=False):
+ """Create the policies and PSK so can then create site conn."""
+ if not skip_psk:
+ self._make_psk_for_test()
+ if not skip_ike:
+ self._make_ike_policy_for_test()
+ if not skip_ipsec:
+ ipsec_policy_id = self._make_ipsec_policy_for_test()
+ else:
+ ipsec_policy_id = generate_ipsec_policy_id()
+ # Note: Use same ID number for tunnel and IPSec policy, so that when
+ # GET tunnel info, the mocks can infer the IPSec policy ID from the
+ # tunnel number.
+ return (ipsec_policy_id, ipsec_policy_id)
+
+ def test_create_delete_ipsec_connection(self):
+ """Create and then delete an IPSec connection."""
+ tunnel_id, ipsec_policy_id = self._prepare_for_site_conn_create()
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.normal_get):
+ connection_info = {
+ u'vpn-interface-name': u'Tunnel%d' % tunnel_id,
+ u'ipsec-policy-id': u'%d' % ipsec_policy_id,
+ u'mtu': 1500,
+ u'local-device': {u'ip-address': u'10.3.0.1/24',
+ u'tunnel-ip-address': u'10.10.10.10'},
+ u'remote-device': {u'tunnel-ip-address': u'10.10.10.20'}
+ }
+ location = self.csr.create_ipsec_connection(connection_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.addCleanup(self._remove_resource_for_test,
+ self.csr.delete_ipsec_connection,
+ 'Tunnel%d' % tunnel_id)
+ self.assertIn('vpn-svc/site-to-site/Tunnel%d' % tunnel_id,
+ location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_connection = {u'kind': u'object#vpn-site-to-site',
+ u'ike-profile-id': None,
+ u'mtu': 1500,
+ u'ip-version': u'ipv4'}
+ expected_connection.update(connection_info)
+ self.assertEqual(expected_connection, content)
+ # Now delete and verify that site-to-site connection is gone
+ with httmock.HTTMock(csr_request.token, csr_request.delete,
+ csr_request.no_such_resource):
+ # Only delete connection. Cleanup will take care of prerequisites
+ self.csr.delete_ipsec_connection('Tunnel%d' % tunnel_id)
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.NOT_FOUND, self.csr.status)
+
+ def test_create_ipsec_connection_with_no_tunnel_subnet(self):
+ """Create an IPSec connection without an IP address on tunnel."""
+ tunnel_id, ipsec_policy_id = self._prepare_for_site_conn_create()
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.get_unnumbered):
+ connection_info = {
+ u'vpn-interface-name': u'Tunnel%d' % tunnel_id,
+ u'ipsec-policy-id': u'%d' % ipsec_policy_id,
+ u'local-device': {u'ip-address': u'GigabitEthernet3',
+ u'tunnel-ip-address': u'10.10.10.10'},
+ u'remote-device': {u'tunnel-ip-address': u'10.10.10.20'}
+ }
+ location = self.csr.create_ipsec_connection(connection_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.addCleanup(self._remove_resource_for_test,
+ self.csr.delete_ipsec_connection,
+ 'Tunnel%d' % tunnel_id)
+ self.assertIn('vpn-svc/site-to-site/Tunnel%d' % tunnel_id,
+ location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ if csr_request.FIXED_CSCum50512:
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_connection = {u'kind': u'object#vpn-site-to-site',
+ u'ip-version': u'ipv4'}
+ expected_connection.update(connection_info)
+ expected_connection[u'local-device'][u'ip-address'] = (
+ u'unnumbered GigabitEthernet3')
+ self.assertEqual(expected_connection, content)
+ else:
+ self.assertEqual(requests.codes.INTERNAL_SERVER_ERROR,
+ self.csr.status)
+
+ def test_create_ipsec_connection_no_pre_shared_key(self):
+ """Test of connection create without associated pre-shared key.
+
+ The CSR will create the connection, but will not be able to pass
+ traffic without the pre-shared key.
+ """
+
+ tunnel_id, ipsec_policy_id = self._prepare_for_site_conn_create(
+ skip_psk=True)
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.normal_get):
+ connection_info = {
+ u'vpn-interface-name': u'Tunnel%d' % tunnel_id,
+ u'ipsec-policy-id': u'%d' % ipsec_policy_id,
+ u'mtu': 1500,
+ u'local-device': {u'ip-address': u'10.3.0.1/24',
+ u'tunnel-ip-address': u'10.10.10.10'},
+ u'remote-device': {u'tunnel-ip-address': u'10.10.10.20'}
+ }
+ location = self.csr.create_ipsec_connection(connection_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.addCleanup(self._remove_resource_for_test,
+ self.csr.delete_ipsec_connection,
+ 'Tunnel%d' % tunnel_id)
+ self.assertIn('vpn-svc/site-to-site/Tunnel%d' % tunnel_id,
+ location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_connection = {u'kind': u'object#vpn-site-to-site',
+ u'ike-profile-id': None,
+ u'mtu': 1500,
+ u'ip-version': u'ipv4'}
+ expected_connection.update(connection_info)
+ self.assertEqual(expected_connection, content)
+
+ def test_create_ipsec_connection_with_default_ike_policy(self):
+ """Test of connection create without IKE policy (uses default).
+
+ Without an IKE policy, the CSR will use a built-in default IKE
+ policy setting for the connection.
+ """
+
+ tunnel_id, ipsec_policy_id = self._prepare_for_site_conn_create(
+ skip_ike=True)
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.normal_get):
+ connection_info = {
+ u'vpn-interface-name': u'Tunnel%d' % tunnel_id,
+ u'ipsec-policy-id': u'%d' % ipsec_policy_id,
+ u'mtu': 1500,
+ u'local-device': {u'ip-address': u'10.3.0.1/24',
+ u'tunnel-ip-address': u'10.10.10.10'},
+ u'remote-device': {u'tunnel-ip-address': u'10.10.10.20'}
+ }
+ location = self.csr.create_ipsec_connection(connection_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.addCleanup(self._remove_resource_for_test,
+ self.csr.delete_ipsec_connection,
+ 'Tunnel%d' % tunnel_id)
+ self.assertIn('vpn-svc/site-to-site/Tunnel%d' % tunnel_id,
+ location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_connection = {u'kind': u'object#vpn-site-to-site',
+ u'ike-profile-id': None,
+ u'mtu': 1500,
+ u'ip-version': u'ipv4'}
+ expected_connection.update(connection_info)
+ self.assertEqual(expected_connection, content)
+
+ def test_create_ipsec_connection_missing_ipsec_policy(self):
+ """Negative test of connection create without IPSec policy."""
+ tunnel_id, ipsec_policy_id = self._prepare_for_site_conn_create(
+ skip_ipsec=True)
+ with httmock.HTTMock(csr_request.token,
+ csr_request.post_missing_ipsec_policy):
+ connection_info = {
+ u'vpn-interface-name': u'Tunnel%d' % tunnel_id,
+ u'ipsec-policy-id': u'%d' % ipsec_policy_id,
+ u'local-device': {u'ip-address': u'10.3.0.1/24',
+ u'tunnel-ip-address': u'10.10.10.10'},
+ u'remote-device': {u'tunnel-ip-address': u'10.10.10.20'}
+ }
+ self.csr.create_ipsec_connection(connection_info)
+ self.assertEqual(requests.codes.BAD_REQUEST, self.csr.status)
+
+ def test_create_ipsec_connection_conficting_tunnel_ip(self):
+ """Negative test of connection create with conflicting tunnel IP.
+
+ The GigabitEthernet3 interface has an IP of 10.2.0.6. This will
+ try a connection create with an IP that is on the same subnet.
+ """
+
+ tunnel_id, ipsec_policy_id = self._prepare_for_site_conn_create()
+ with httmock.HTTMock(csr_request.token, csr_request.post_bad_ip):
+ connection_info = {
+ u'vpn-interface-name': u'Tunnel%d' % tunnel_id,
+ u'ipsec-policy-id': u'%d' % ipsec_policy_id,
+ u'local-device': {u'ip-address': u'10.2.0.10/24',
+ u'tunnel-ip-address': u'10.10.10.10'},
+ u'remote-device': {u'tunnel-ip-address': u'10.10.10.20'}
+ }
+ self.csr.create_ipsec_connection(connection_info)
+ self.assertEqual(requests.codes.BAD_REQUEST, self.csr.status)
+
+ def test_create_ipsec_connection_with_max_mtu(self):
+ """Create an IPSec connection with max MTU value."""
+ tunnel_id, ipsec_policy_id = self._prepare_for_site_conn_create()
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.get_mtu):
+ connection_info = {
+ u'vpn-interface-name': u'Tunnel%d' % tunnel_id,
+ u'ipsec-policy-id': u'%d' % ipsec_policy_id,
+ u'mtu': 9192,
+ u'local-device': {u'ip-address': u'10.3.0.1/24',
+ u'tunnel-ip-address': u'10.10.10.10'},
+ u'remote-device': {u'tunnel-ip-address': u'10.10.10.20'}
+ }
+ location = self.csr.create_ipsec_connection(connection_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.addCleanup(self._remove_resource_for_test,
+ self.csr.delete_ipsec_connection,
+ 'Tunnel%d' % tunnel_id)
+ self.assertIn('vpn-svc/site-to-site/Tunnel%d' % tunnel_id,
+ location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_connection = {u'kind': u'object#vpn-site-to-site',
+ u'ike-profile-id': None,
+ u'ip-version': u'ipv4'}
+ expected_connection.update(connection_info)
+ self.assertEqual(expected_connection, content)
+
+ def test_create_ipsec_connection_with_bad_mtu(self):
+ """Negative test of connection create with unsupported MTU value."""
+ tunnel_id, ipsec_policy_id = self._prepare_for_site_conn_create()
+ with httmock.HTTMock(csr_request.token, csr_request.post_bad_mtu):
+ connection_info = {
+ u'vpn-interface-name': u'Tunnel%d' % tunnel_id,
+ u'ipsec-policy-id': u'%d' % ipsec_policy_id,
+ u'mtu': 9193,
+ u'local-device': {u'ip-address': u'10.3.0.1/24',
+ u'tunnel-ip-address': u'10.10.10.10'},
+ u'remote-device': {u'tunnel-ip-address': u'10.10.10.20'}
+ }
+ self.csr.create_ipsec_connection(connection_info)
+ self.assertEqual(requests.codes.BAD_REQUEST, self.csr.status)
+
+ def test_status_when_no_tunnels_exist(self):
+ """Get status, when there are no tunnels."""
+ with httmock.HTTMock(csr_request.token, csr_request.get_none):
+ tunnels = self.csr.read_tunnel_statuses()
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ self.assertEqual([], tunnels)
+
+ def test_status_for_one_tunnel(self):
+ """Get status of one tunnel."""
+ # Create the IPsec site-to-site connection first
+ tunnel_id, ipsec_policy_id = self._prepare_for_site_conn_create()
+ tunnel_id = 123 # Must hard code to work with mock
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.normal_get):
+ connection_info = {
+ u'vpn-interface-name': u'Tunnel123',
+ u'ipsec-policy-id': u'%d' % ipsec_policy_id,
+ u'local-device': {u'ip-address': u'10.3.0.1/24',
+ u'tunnel-ip-address': u'10.10.10.10'},
+ u'remote-device': {u'tunnel-ip-address': u'10.10.10.20'}
+ }
+ location = self.csr.create_ipsec_connection(connection_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.addCleanup(self._remove_resource_for_test,
+ self.csr.delete_ipsec_connection,
+ u'Tunnel123')
+ self.assertIn('vpn-svc/site-to-site/Tunnel%d' % tunnel_id,
+ location)
+ with httmock.HTTMock(csr_request.token, csr_request.normal_get):
+ tunnels = self.csr.read_tunnel_statuses()
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ self.assertEqual([(u'Tunnel123', u'DOWN-NEGOTIATING'), ], tunnels)
+
+
+class TestCsrRestIkeKeepaliveCreate(base.BaseTestCase):
+
+ """Test IKE keepalive REST requests.
+
+ This is a global configuration that will apply to all VPN tunnels and
+ is used to specify Dead Peer Detection information. Currently, the API
+ supports DELETE API, but a bug has been created to remove the API and
+ add an indicator of when the capability is disabled.
+ """
+
+ def setUp(self):
+ super(TestCsrRestIkeKeepaliveCreate, self).setUp()
+ self.csr = csr_client.CsrRestClient('localhost', '10.10.10.10',
+ 'stack', 'cisco')
+
+ def test_configure_ike_keepalive(self):
+ """Set IKE keep-alive (aka Dead Peer Detection) for the CSR."""
+ with httmock.HTTMock(csr_request.token, csr_request.put,
+ csr_request.normal_get):
+ keepalive_info = {'interval': 60, 'retry': 4}
+ self.csr.configure_ike_keepalive(keepalive_info)
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ content = self.csr.get_request('vpn-svc/ike/keepalive')
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected = {'periodic': False}
+ expected.update(keepalive_info)
+ self.assertDictContainsSubset(expected, content)
+
+ def test_disable_ike_keepalive(self):
+ """Disable IKE keep-alive (aka Dead Peer Detection) for the CSR."""
+ with httmock.HTTMock(csr_request.token, csr_request.delete,
+ csr_request.put, csr_request.get_not_configured):
+ if csr_request.FIXED_CSCum10324:
+ # TODO(pcm) Is this how to disable?
+ keepalive_info = {'interval': 0, 'retry': 4}
+ self.csr.configure_ike_keepalive(keepalive_info)
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ else:
+ self.csr.delete_request('vnc-svc/ike/keepalive')
+ self.assertIn(self.csr.status,
+ (requests.codes.NO_CONTENT,
+ requests.codes.NOT_FOUND))
+ self.csr.get_request('vpn-svc/ike/keepalive')
+ self.assertEqual(requests.codes.NOT_FOUND, self.csr.status)
+
+
+class TestCsrRestStaticRoute(base.BaseTestCase):
+
+ """Test static route REST requests.
+
+ A static route is added for the peer's private network. Would create
+ a route for each of the peer CIDRs specified for the VPN connection.
+ """
+
+ def setUp(self):
+ super(TestCsrRestStaticRoute, self).setUp()
+ self.csr = csr_client.CsrRestClient('localhost', '10.10.10.10',
+ 'stack', 'cisco')
+
+ def test_create_delete_static_route(self):
+ """Create and then delete a static route for the tunnel."""
+ cidr = u'10.1.0.0/24'
+ interface = u'GigabitEthernet1'
+ expected_id = '10.1.0.0_24_GigabitEthernet1'
+ with httmock.HTTMock(csr_request.token, csr_request.post,
+ csr_request.normal_get):
+ route_info = {u'destination-network': cidr,
+ u'outgoing-interface': interface}
+ location = self.csr.create_static_route(route_info)
+ self.assertEqual(requests.codes.CREATED, self.csr.status)
+ self.assertIn('routing-svc/static-routes/%s' % expected_id,
+ location)
+ # Check the hard-coded items that get set as well...
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.OK, self.csr.status)
+ expected_route = {u'kind': u'object#static-route',
+ u'next-hop-router': None,
+ u'admin-distance': 1}
+ expected_route.update(route_info)
+ self.assertEqual(expected_route, content)
+ # Now delete and verify that static route is gone
+ with httmock.HTTMock(csr_request.token, csr_request.delete,
+ csr_request.no_such_resource):
+ route_id = csr_client.make_route_id(cidr, interface)
+ self.csr.delete_static_route(route_id)
+ self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
+ content = self.csr.get_request(location, full_url=True)
+ self.assertEqual(requests.codes.NOT_FOUND, self.csr.status)
--- /dev/null
+# 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.
+#
+# @author: Paul Michali, Cisco Systems, Inc.
+
+import httplib
+import os
+import tempfile
+
+import mock
+
+from neutron import context
+from neutron.openstack.common import uuidutils
+from neutron.plugins.common import constants
+from neutron.services.vpn.device_drivers import (
+ cisco_csr_rest_client as csr_client)
+from neutron.services.vpn.device_drivers import cisco_ipsec as ipsec_driver
+from neutron.tests import base
+
+_uuid = uuidutils.generate_uuid
+FAKE_HOST = 'fake_host'
+FAKE_ROUTER_ID = _uuid()
+FAKE_VPN_SERVICE = {
+ 'id': _uuid(),
+ 'router_id': FAKE_ROUTER_ID,
+ 'admin_state_up': True,
+ 'status': constants.PENDING_CREATE,
+ 'subnet': {'cidr': '10.0.0.0/24'},
+ 'ipsec_site_connections': [
+ {'peer_cidrs': ['20.0.0.0/24',
+ '30.0.0.0/24']},
+ {'peer_cidrs': ['40.0.0.0/24',
+ '50.0.0.0/24']}]
+}
+FIND_CFG_FOR_CSRS = ('neutron.services.vpn.device_drivers.cisco_ipsec.'
+ 'find_available_csrs_from_config')
+
+
+class TestCiscoCsrIPSecConnection(base.BaseTestCase):
+ def setUp(self):
+ super(TestCiscoCsrIPSecConnection, self).setUp()
+ self.addCleanup(mock.patch.stopall)
+ self.conn_info = {
+ u'id': '123',
+ u'status': constants.PENDING_CREATE,
+ u'admin_state_up': True,
+ 'psk': 'secret',
+ 'peer_address': '192.168.1.2',
+ 'peer_cidrs': ['10.1.0.0/24', '10.2.0.0/24'],
+ 'mtu': 1500,
+ 'ike_policy': {'auth_algorithm': 'sha1',
+ 'encryption_algorithm': 'aes-128',
+ 'pfs': 'Group5',
+ 'ike_version': 'v1',
+ 'lifetime_units': 'seconds',
+ 'lifetime_value': 3600},
+ 'ipsec_policy': {'transform_protocol': 'ah',
+ 'encryption_algorithm': 'aes-128',
+ 'auth_algorithm': 'sha1',
+ 'pfs': 'group5',
+ 'lifetime_units': 'seconds',
+ 'lifetime_value': 3600},
+ 'cisco': {'site_conn_id': 'Tunnel0',
+ 'ike_policy_id': 222,
+ 'ipsec_policy_id': 333,
+ # TODO(pcm) FUTURE use vpnservice['external_ip']
+ 'router_public_ip': '172.24.4.23'}
+ }
+ self.csr = mock.Mock(spec=csr_client.CsrRestClient)
+ self.csr.status = 201 # All calls to CSR REST API succeed
+ self.ipsec_conn = ipsec_driver.CiscoCsrIPSecConnection(self.conn_info,
+ self.csr)
+
+ def test_create_ipsec_site_connection(self):
+ """Ensure all steps are done to create an IPSec site connection.
+
+ Verify that each of the driver calls occur (in order), and
+ the right information is stored for later deletion.
+ """
+ expected = ['create_pre_shared_key',
+ 'create_ike_policy',
+ 'create_ipsec_policy',
+ 'create_ipsec_connection',
+ 'create_static_route',
+ 'create_static_route']
+ expected_rollback_steps = [
+ ipsec_driver.RollbackStep(action='pre_shared_key',
+ resource_id='123',
+ title='Pre-Shared Key'),
+ ipsec_driver.RollbackStep(action='ike_policy',
+ resource_id=222,
+ title='IKE Policy'),
+ ipsec_driver.RollbackStep(action='ipsec_policy',
+ resource_id=333,
+ title='IPSec Policy'),
+ ipsec_driver.RollbackStep(action='ipsec_connection',
+ resource_id='Tunnel0',
+ title='IPSec Connection'),
+ ipsec_driver.RollbackStep(action='static_route',
+ resource_id='10.1.0.0_24_Tunnel0',
+ title='Static Route'),
+ ipsec_driver.RollbackStep(action='static_route',
+ resource_id='10.2.0.0_24_Tunnel0',
+ title='Static Route')]
+ self.ipsec_conn.create_ipsec_site_connection(mock.Mock(),
+ self.conn_info)
+ client_calls = [c[0] for c in self.csr.method_calls]
+ self.assertEqual(expected, client_calls)
+ self.assertEqual(expected_rollback_steps, self.ipsec_conn.steps)
+
+ def test_create_ipsec_site_connection_with_rollback(self):
+ """Failure test of IPSec site conn creation that fails and rolls back.
+
+ Simulate a failure in the last create step (making routes for the
+ peer networks), and ensure that the create steps are called in
+ order (except for create_static_route), and that the delete
+ steps are called in reverse order. At the end, there should be no
+ rollback infromation for the connection.
+ """
+ def fake_route_check_fails(*args, **kwargs):
+ if args[0] == 'Static Route':
+ # So that subsequent calls to CSR rest client (for rollback)
+ # will fake as passing.
+ self.csr.status = httplib.NO_CONTENT
+ raise ipsec_driver.CsrResourceCreateFailure(resource=args[0],
+ which=args[1])
+
+ with mock.patch.object(ipsec_driver.CiscoCsrIPSecConnection,
+ '_check_create',
+ side_effect=fake_route_check_fails):
+
+ expected = ['create_pre_shared_key',
+ 'create_ike_policy',
+ 'create_ipsec_policy',
+ 'create_ipsec_connection',
+ 'create_static_route',
+ 'delete_ipsec_connection',
+ 'delete_ipsec_policy',
+ 'delete_ike_policy',
+ 'delete_pre_shared_key']
+ self.ipsec_conn.create_ipsec_site_connection(mock.Mock(),
+ self.conn_info)
+ client_calls = [c[0] for c in self.csr.method_calls]
+ self.assertEqual(expected, client_calls)
+ self.assertEqual([], self.ipsec_conn.steps)
+
+ def test_create_verification_with_error(self):
+ """Negative test of create check step had failed."""
+ self.csr.status = httplib.NOT_FOUND
+ self.assertRaises(ipsec_driver.CsrResourceCreateFailure,
+ self.ipsec_conn._check_create, 'name', 'id')
+
+ def test_failure_with_invalid_create_step(self):
+ """Negative test of invalid create step (programming error)."""
+ self.ipsec_conn.steps = []
+ try:
+ self.ipsec_conn.do_create_action('bogus', None, '123', 'Bad Step')
+ except ipsec_driver.CsrResourceCreateFailure:
+ pass
+ else:
+ self.fail('Expected exception with invalid create step')
+
+ def test_failure_with_invalid_delete_step(self):
+ """Negative test of invalid delete step (programming error)."""
+ self.ipsec_conn.steps = [ipsec_driver.RollbackStep(action='bogus',
+ resource_id='123',
+ title='Bogus Step')]
+ try:
+ self.ipsec_conn.do_rollback()
+ except ipsec_driver.CsrResourceCreateFailure:
+ pass
+ else:
+ self.fail('Expected exception with invalid delete step')
+
+ def test_delete_ipsec_connection(self):
+ # TODO(pcm) implement
+ pass
+
+
+class TestCiscoCsrIPsecConnectionCreateTransforms(base.BaseTestCase):
+
+ """Verifies that config info is prepared/transformed correctly."""
+
+ def setUp(self):
+ super(TestCiscoCsrIPsecConnectionCreateTransforms, self).setUp()
+ self.addCleanup(mock.patch.stopall)
+ self.conn_info = {
+ u'id': '123',
+ u'status': constants.PENDING_CREATE,
+ u'admin_state_up': True,
+ 'psk': 'secret',
+ 'peer_address': '192.168.1.2',
+ 'peer_cidrs': ['10.1.0.0/24', '10.2.0.0/24'],
+ 'mtu': 1500,
+ 'ike_policy': {'auth_algorithm': 'sha1',
+ 'encryption_algorithm': 'aes-128',
+ 'pfs': 'Group5',
+ 'ike_version': 'v1',
+ 'lifetime_units': 'seconds',
+ 'lifetime_value': 3600},
+ 'ipsec_policy': {'transform_protocol': 'ah',
+ 'encryption_algorithm': 'aes-128',
+ 'auth_algorithm': 'sha1',
+ 'pfs': 'group5',
+ 'lifetime_units': 'seconds',
+ 'lifetime_value': 3600},
+ 'cisco': {'site_conn_id': 'Tunnel0',
+ 'ike_policy_id': 222,
+ 'ipsec_policy_id': 333,
+ # TODO(pcm) get from vpnservice['external_ip']
+ 'router_public_ip': '172.24.4.23'}
+ }
+ self.ipsec_conn = ipsec_driver.CiscoCsrIPSecConnection(self.conn_info,
+ mock.Mock())
+
+ def test_invalid_attribute(self):
+ """Negative test of unknown attribute - programming error."""
+ self.assertRaises(ipsec_driver.CsrDriverMismatchError,
+ self.ipsec_conn.translate_dialect,
+ 'ike_policy', 'unknown_attr', self.conn_info)
+
+ def test_driver_unknown_mapping(self):
+ """Negative test of service driver providing unknown value to map."""
+ self.conn_info['ike_policy']['pfs'] = "unknown_value"
+ self.assertRaises(ipsec_driver.CsrUnknownMappingError,
+ self.ipsec_conn.translate_dialect,
+ 'ike_policy', 'pfs', self.conn_info['ike_policy'])
+
+ def test_psk_create_info(self):
+ """Ensure that pre-shared key info is created correctly."""
+ expected = {u'keyring-name': '123',
+ u'pre-shared-key-list': [
+ {u'key': 'secret',
+ u'encrypted': False,
+ u'peer-address': '192.168.1.2'}]}
+ psk_id = self.conn_info['id']
+ psk_info = self.ipsec_conn.create_psk_info(psk_id, self.conn_info)
+ self.assertEqual(expected, psk_info)
+
+ def test_create_ike_policy_info(self):
+ """Ensure that IKE policy info is mapped/created correctly."""
+ expected = {u'priority-id': 222,
+ u'encryption': u'aes',
+ u'hash': u'sha',
+ u'dhGroup': 5,
+ u'version': u'v1',
+ u'lifetime': 3600}
+ policy_id = self.conn_info['cisco']['ike_policy_id']
+ policy_info = self.ipsec_conn.create_ike_policy_info(policy_id,
+ self.conn_info)
+ self.assertEqual(expected, policy_info)
+
+ def test_create_ike_policy_info_non_defaults(self):
+ """Ensure that IKE policy info with different values."""
+ self.conn_info['ike_policy'] = {
+ 'auth_algorithm': 'sha1',
+ 'encryption_algorithm': 'aes-256',
+ 'pfs': 'Group14',
+ 'ike_version': 'v1',
+ 'lifetime_units': 'seconds',
+ 'lifetime_value': 60
+ }
+ expected = {u'priority-id': 222,
+ u'encryption': u'aes', # TODO(pcm): fix
+ u'hash': u'sha',
+ u'dhGroup': 14,
+ u'version': u'v1',
+ u'lifetime': 60}
+ policy_id = self.conn_info['cisco']['ike_policy_id']
+ policy_info = self.ipsec_conn.create_ike_policy_info(policy_id,
+ self.conn_info)
+ self.assertEqual(expected, policy_info)
+
+ def test_ipsec_policy_info(self):
+ """Ensure that IPSec policy info is mapped/created correctly."""
+ expected = {u'policy-id': 333,
+ u'protection-suite': {
+ u'esp-encryption': u'esp-aes',
+ u'esp-authentication': u'esp-sha-hmac',
+ u'ah': u'ah-sha-hmac'
+ },
+ u'lifetime-sec': 3600,
+ u'pfs': u'group5',
+ u'anti-replay-window-size': u'64'}
+ ipsec_policy_id = self.conn_info['cisco']['ipsec_policy_id']
+ policy_info = self.ipsec_conn.create_ipsec_policy_info(ipsec_policy_id,
+ self.conn_info)
+ self.assertEqual(expected, policy_info)
+
+ def test_ipsec_policy_info_non_defaults(self):
+ """Create/map IPSec policy info with different values."""
+ self.conn_info['ipsec_policy'] = {'transform_protocol': 'esp',
+ 'encryption_algorithm': '3des',
+ 'auth_algorithm': 'sha1',
+ 'pfs': 'group14',
+ 'lifetime_units': 'seconds',
+ 'lifetime_value': 120}
+ expected = {u'policy-id': 333,
+ u'protection-suite': {
+ u'esp-encryption': u'esp-3des',
+ u'esp-authentication': u'esp-sha-hmac'
+ },
+ u'lifetime-sec': 120,
+ u'pfs': u'group14',
+ u'anti-replay-window-size': u'64'}
+ ipsec_policy_id = self.conn_info['cisco']['ipsec_policy_id']
+ policy_info = self.ipsec_conn.create_ipsec_policy_info(ipsec_policy_id,
+ self.conn_info)
+ self.assertEqual(expected, policy_info)
+
+ def test_site_connection_info(self):
+ """Ensure site-to-site connection info is created/mapped correctly."""
+ expected = {u'vpn-interface-name': 'Tunnel0',
+ u'ipsec-policy-id': 333,
+ u'local-device': {
+ u'ip-address': u'GigabitEthernet3',
+ u'tunnel-ip-address': u'172.24.4.23'
+ },
+ u'remote-device': {
+ u'tunnel-ip-address': '192.168.1.2'
+ },
+ u'mtu': 1500}
+ ipsec_policy_id = self.conn_info['cisco']['ipsec_policy_id']
+ site_conn_id = self.conn_info['cisco']['site_conn_id']
+ conn_info = self.ipsec_conn.create_site_connection_info(
+ site_conn_id, ipsec_policy_id, self.conn_info)
+ self.assertEqual(expected, conn_info)
+
+ def test_static_route_info(self):
+ """Create static route info for peer CIDRs."""
+ expected = [('10.1.0.0_24_Tunnel0',
+ {u'destination-network': '10.1.0.0/24',
+ u'outgoing-interface': 'Tunnel0'}),
+ ('10.2.0.0_24_Tunnel0',
+ {u'destination-network': '10.2.0.0/24',
+ u'outgoing-interface': 'Tunnel0'})]
+# self.driver.csr.make_route_id.side_effect = ['10.1.0.0_24_Tunnel0',
+# '10.2.0.0_24_Tunnel0']
+ site_conn_id = self.conn_info['cisco']['site_conn_id']
+ routes_info = self.ipsec_conn.create_routes_info(site_conn_id,
+ self.conn_info)
+ self.assertEqual(2, len(routes_info))
+ self.assertEqual(expected, routes_info)
+
+
+class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
+
+ """Test status/state of services and connections, after sync."""
+
+ def setUp(self):
+ super(TestCiscoCsrIPsecDeviceDriverSyncStatuses, self).setUp()
+ self.addCleanup(mock.patch.stopall)
+ for klass in ['neutron.openstack.common.rpc.create_connection',
+ 'neutron.context.get_admin_context_without_session',
+ 'neutron.openstack.common.'
+ 'loopingcall.FixedIntervalLoopingCall']:
+ mock.patch(klass).start()
+ self.context = context.Context('some_user', 'some_tenant')
+ self.agent = mock.Mock()
+ conf_patch = mock.patch('oslo.config.cfg.CONF').start()
+ conf_patch.config_file = ['dummy']
+ self.config_load = mock.patch(FIND_CFG_FOR_CSRS).start()
+ self.config_load.return_value = {'1.1.1.1': {'rest_mgmt': '2.2.2.2',
+ 'tunnel_ip': '1.1.1.3',
+ 'username': 'pe',
+ 'password': 'password',
+ 'timeout': 120}}
+ self.driver = ipsec_driver.CiscoCsrIPsecDriver(self.agent, FAKE_HOST)
+ self.driver.agent_rpc = mock.Mock()
+ self.conn_create = mock.patch.object(
+ ipsec_driver.CiscoCsrIPSecConnection,
+ 'create_ipsec_site_connection').start()
+ self.conn_delete = mock.patch.object(
+ ipsec_driver.CiscoCsrIPSecConnection,
+ 'delete_ipsec_site_connection').start()
+ self.csr = mock.Mock()
+ self.driver.csrs['1.1.1.1'] = self.csr
+ self.service123_data = {u'id': u'123',
+ u'status': constants.DOWN,
+ u'admin_state_up': False,
+ u'external_ip': u'1.1.1.1'}
+ self.conn1_data = {u'id': u'1', u'status': constants.ACTIVE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+
+ # NOTE: For sync, there is mark (trivial), update (tested),
+ # sweep (tested), and report(tested) phases.
+
+ def test_update_ipsec_connection_create_notify(self):
+ """Notified of connection create request - create."""
+ # Make the (existing) service
+ self.driver.create_vpn_service(self.service123_data)
+ conn_data = {u'id': u'1', u'status': constants.PENDING_CREATE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+
+ connection = self.driver.update_connection(self.context,
+ u'123', conn_data)
+ self.assertFalse(connection.is_dirty)
+ self.assertEqual(u'Tunnel0', connection.tunnel)
+ self.assertEqual(constants.PENDING_CREATE, connection.last_status)
+ self.assertEqual(1, self.conn_create.call_count)
+
+ def test_update_ipsec_connection_changed_settings(self):
+ """Notified of connection changing config - update."""
+ # TODO(pcm) Place holder for this condition
+ # Make the (existing) service and connection
+ vpn_service = self.driver.create_vpn_service(self.service123_data)
+ # TODO(pcm) add info that indicates that the connection has changed
+ conn_data = {u'id': u'1', u'status': constants.ACTIVE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+ vpn_service.create_connection(conn_data)
+ self.driver.mark_existing_connections_as_dirty()
+
+ connection = self.driver.update_connection(self.context,
+ '123', conn_data)
+ self.assertFalse(connection.is_dirty)
+ self.assertEqual(u'Tunnel0', connection.tunnel)
+ self.assertEqual(constants.ACTIVE, connection.last_status)
+ self.assertEqual(0, self.conn_create.call_count)
+ # TODO(pcm) FUTURE - handling for update (delete/create?)
+
+ def test_update_of_unknown_ipsec_connection(self):
+ """Notified of update of unknown connection - create.
+
+ Occurs if agent restarts and receives a notification of change
+ to connection, but has no previous record of the connection.
+ Result will be to rebuild the connection.
+
+ This can also happen, if a connection is changed from admin
+ down to admin up (so don't need a separate test for admin up.
+ """
+ # Will have previously created service, but don't know of connection
+ self.driver.create_vpn_service(self.service123_data)
+ conn_data = {u'id': u'1', u'status': constants.DOWN,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+
+ connection = self.driver.update_connection(self.context,
+ u'123', conn_data)
+ self.assertFalse(connection.is_dirty)
+ self.assertEqual(u'Tunnel0', connection.tunnel)
+ self.assertEqual(constants.DOWN, connection.last_status)
+ self.assertEqual(1, self.conn_create.call_count)
+
+ def test_update_unchanged_ipsec_connection(self):
+ """Unchanged state for connection during sync - nop."""
+ # Make the (existing) service and connection
+ vpn_service = self.driver.create_vpn_service(self.service123_data)
+ conn_data = {u'id': u'1', u'status': constants.ACTIVE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+ vpn_service.create_connection(conn_data)
+ self.driver.mark_existing_connections_as_dirty()
+ # The notification (state) hasn't changed for the connection
+
+ connection = self.driver.update_connection(self.context,
+ '123', conn_data)
+ self.assertFalse(connection.is_dirty)
+ self.assertEqual(u'Tunnel0', connection.tunnel)
+ self.assertEqual(constants.ACTIVE, connection.last_status)
+ self.assertEqual(0, self.conn_create.call_count)
+
+ def test_update_connection_admin_down(self):
+ """Connection updated to admin down state - dirty."""
+ # Make existing service, and conenction that was active
+ vpn_service = self.driver.create_vpn_service(self.service123_data)
+ conn_data = {u'id': '1', u'status': constants.ACTIVE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+ vpn_service.create_connection(conn_data)
+ self.driver.mark_existing_connections_as_dirty()
+ # Now simulate that the notification shows the connection admin down
+ conn_data[u'admin_state_up'] = False
+ conn_data[u'status'] = constants.DOWN
+
+ connection = self.driver.update_connection(self.context,
+ u'123', conn_data)
+ self.assertTrue(connection.is_dirty)
+ self.assertEqual(u'Tunnel0', connection.tunnel)
+ self.assertEqual(constants.DOWN, connection.last_status)
+ self.assertEqual(0, self.conn_create.call_count)
+
+ def test_update_missing_connection_admin_down(self):
+ """Connection not present is in admin down state - nop.
+
+ If the agent has restarted, and a sync notification occurs with
+ a connection that is in admin down state, ignore the connection
+ versus creating and marking dirty and then deleting.
+ """
+ # Make existing service, but no connection
+ self.driver.create_vpn_service(self.service123_data)
+ conn_data = {u'id': '1', u'status': constants.DOWN,
+ u'admin_state_up': False,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+
+ connection = self.driver.update_connection(self.context,
+ u'123', conn_data)
+ self.assertIsNone(connection)
+ self.assertEqual(0, self.conn_create.call_count)
+
+ def test_update_for_vpn_service_create(self):
+ """Creation of new IPSec connection on new VPN service - create.
+
+ Service will be created and marked as 'clean', and update
+ processing for connection will occur (create).
+ """
+ conn_data = {u'id': u'1', u'status': constants.PENDING_CREATE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+ service_data = {u'id': u'123',
+ u'status': constants.PENDING_CREATE,
+ u'external_ip': u'1.1.1.1',
+ u'admin_state_up': True,
+ u'ipsec_conns': [conn_data]}
+ vpn_service = self.driver.update_service(self.context, service_data)
+ self.assertFalse(vpn_service.is_dirty)
+ self.assertEqual(constants.PENDING_CREATE, vpn_service.last_status)
+ connection = vpn_service.get_connection(u'1')
+ self.assertIsNotNone(connection)
+ self.assertFalse(connection.is_dirty)
+ self.assertEqual(u'Tunnel0', connection.tunnel)
+ self.assertEqual(constants.PENDING_CREATE, connection.last_status)
+ self.assertEqual(1, self.conn_create.call_count)
+
+ def test_update_for_new_connection_on_existing_service(self):
+ """Creating a new IPSec connection on an existing service."""
+ # Create the service before testing, and mark it dirty
+ prev_vpn_service = self.driver.create_vpn_service(self.service123_data)
+ self.driver.mark_existing_connections_as_dirty()
+ conn_data = {u'id': u'1', u'status': constants.PENDING_CREATE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+ service_data = {u'id': u'123',
+ u'status': constants.ACTIVE,
+ u'external_ip': u'1.1.1.1',
+ u'admin_state_up': True,
+ u'ipsec_conns': [conn_data]}
+ vpn_service = self.driver.update_service(self.context, service_data)
+ # Should reuse the entry and update the status
+ self.assertEqual(prev_vpn_service, vpn_service)
+ self.assertFalse(vpn_service.is_dirty)
+ self.assertEqual(constants.ACTIVE, vpn_service.last_status)
+ connection = vpn_service.get_connection(u'1')
+ self.assertIsNotNone(connection)
+ self.assertFalse(connection.is_dirty)
+ self.assertEqual(u'Tunnel0', connection.tunnel)
+ self.assertEqual(constants.PENDING_CREATE, connection.last_status)
+ self.assertEqual(1, self.conn_create.call_count)
+
+ def test_update_for_vpn_service_with_one_unchanged_connection(self):
+ """Existing VPN service and IPSec connection without any changes - nop.
+
+ Service and connection will be marked clean. No processing for
+ either, as there are no changes.
+ """
+ # Create a service and add in a connection that is active
+ prev_vpn_service = self.driver.create_vpn_service(self.service123_data)
+ conn_data = {u'id': u'1', u'status': constants.ACTIVE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+ prev_vpn_service.create_connection(conn_data)
+ self.driver.mark_existing_connections_as_dirty()
+ # Create notification with conn unchanged and service already created
+ service_data = {u'id': u'123',
+ u'status': constants.ACTIVE,
+ u'external_ip': u'1.1.1.1',
+ u'admin_state_up': True,
+ u'ipsec_conns': [conn_data]}
+ vpn_service = self.driver.update_service(self.context, service_data)
+ # Should reuse the entry and update the status
+ self.assertEqual(prev_vpn_service, vpn_service)
+ self.assertFalse(vpn_service.is_dirty)
+ self.assertEqual(constants.ACTIVE, vpn_service.last_status)
+ connection = vpn_service.get_connection(u'1')
+ self.assertIsNotNone(connection)
+ self.assertFalse(connection.is_dirty)
+ self.assertEqual(u'Tunnel0', connection.tunnel)
+ self.assertEqual(constants.ACTIVE, connection.last_status)
+ self.assertEqual(0, self.conn_create.call_count)
+
+ def test_update_service_admin_down(self):
+ """VPN service updated to admin down state - dirty.
+
+ Mark service dirty and do not process any notfications for
+ connections using the service.
+ """
+ # Create an "existing" service, prior to notification
+ prev_vpn_service = self.driver.create_vpn_service(self.service123_data)
+ self.driver.mark_existing_connections_as_dirty()
+ conn_data = {u'id': u'1', u'status': constants.ACTIVE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+ service_data = {u'id': u'123',
+ u'status': constants.DOWN,
+ u'external_ip': u'1.1.1.1',
+ u'admin_state_up': False,
+ u'ipsec_conns': [conn_data]}
+ vpn_service = self.driver.update_service(self.context, service_data)
+ self.assertEqual(prev_vpn_service, vpn_service)
+ self.assertTrue(vpn_service.is_dirty)
+ self.assertEqual(constants.DOWN, vpn_service.last_status)
+ self.assertIsNone(vpn_service.get_connection(u'1'))
+
+ def test_update_unknown_service_admin_down(self):
+ """Unknown VPN service uodated to admin down state - nop.
+
+ Can happen if agent restarts and then gets its first notificaiton
+ of a service that is in the admin down state. Will not do anything,
+ versus creating, marking dirty, and then deleting the VPN service.
+ """
+ service_data = {u'id': u'123',
+ u'status': constants.DOWN,
+ u'external_ip': u'1.1.1.1',
+ u'admin_state_up': False,
+ u'ipsec_conns': []}
+ vpn_service = self.driver.update_service(self.context, service_data)
+ self.assertIsNone(vpn_service)
+
+ def test_update_of_unknown_service_create(self):
+ """Create of VPN service that is currently unknown - record.
+
+ If agent is restarted or user changes VPN service to admin up, the
+ notification may contain a VPN service with an IPSec connection
+ that is not in PENDING_CREATE state.
+ """
+ conn_data = {u'id': u'1', u'status': constants.DOWN,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+ service_data = {u'id': u'123',
+ u'status': constants.ACTIVE,
+ u'external_ip': u'1.1.1.1',
+ u'admin_state_up': True,
+ u'ipsec_conns': [conn_data]}
+ vpn_service = self.driver.update_service(self.context, service_data)
+ self.assertFalse(vpn_service.is_dirty)
+ self.assertEqual(constants.ACTIVE, vpn_service.last_status)
+ connection = vpn_service.get_connection(u'1')
+ self.assertIsNotNone(connection)
+ self.assertFalse(connection.is_dirty)
+ self.assertEqual(u'Tunnel0', connection.tunnel)
+ self.assertEqual(constants.DOWN, connection.last_status)
+ self.assertEqual(1, self.conn_create.call_count)
+
+ def test_update_service_create_no_csr(self):
+ """Failure test of sync of service that is not on CSR - ignore.
+
+ Ignore the VPN service and its IPSec connection(s) notifications for
+ which there is no corresponding Cisco CSR.
+ """
+ conn_data = {u'id': u'1', u'status': constants.PENDING_CREATE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+ service_data = {u'id': u'123',
+ u'status': constants.PENDING_CREATE,
+ u'external_ip': u'2.2.2.2',
+ u'admin_state_up': True,
+ u'ipsec_conns': [conn_data]}
+ vpn_service = self.driver.update_service(self.context, service_data)
+ self.assertIsNone(vpn_service)
+
+ def _check_connection_for_service(self, count, vpn_service):
+ """Helper to check the connection information for a service."""
+ connection = vpn_service.get_connection(u'%d' % count)
+ self.assertIsNotNone(connection, "for connection %d" % count)
+ self.assertFalse(connection.is_dirty, "for connection %d" % count)
+ self.assertEqual(u'Tunnel%d' % count, connection.tunnel,
+ "for connection %d" % count)
+ self.assertEqual(constants.PENDING_CREATE, connection.last_status,
+ "for connection %d" % count)
+ return count + 1
+
+ def notification_for_two_services_with_two_conns(self):
+ """Helper used by tests to create two services, each with two conns."""
+ conn1_data = {u'id': u'1', u'status': constants.PENDING_CREATE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel1'}}
+ conn2_data = {u'id': u'2', u'status': constants.PENDING_CREATE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel2'}}
+ service1_data = {u'id': u'123',
+ u'status': constants.PENDING_CREATE,
+ u'external_ip': u'1.1.1.1',
+ u'admin_state_up': True,
+ u'ipsec_conns': [conn1_data, conn2_data]}
+ conn3_data = {u'id': u'3', u'status': constants.PENDING_CREATE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel3'}}
+ conn4_data = {u'id': u'4', u'status': constants.PENDING_CREATE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel4'}}
+ service2_data = {u'id': u'456',
+ u'status': constants.PENDING_CREATE,
+ u'external_ip': u'1.1.1.1',
+ u'admin_state_up': True,
+ u'ipsec_conns': [conn3_data, conn4_data]}
+ return service1_data, service2_data
+
+ def test_create_two_connections_on_two_services(self):
+ """High level test of multiple VPN services with connections."""
+ # Build notification message
+ (service1_data,
+ service2_data) = self.notification_for_two_services_with_two_conns()
+ # Simulate plugin returning notifcation, when requested
+ self.driver.agent_rpc.get_vpn_services_on_host.return_value = [
+ service1_data, service2_data]
+ vpn_services = self.driver.update_all_services_and_connections(
+ self.context)
+ self.assertEqual(2, len(vpn_services))
+ count = 1
+ for vpn_service in vpn_services:
+ self.assertFalse(vpn_service.is_dirty,
+ "for service %s" % vpn_service)
+ self.assertEqual(constants.PENDING_CREATE, vpn_service.last_status,
+ "for service %s" % vpn_service)
+ count = self._check_connection_for_service(count, vpn_service)
+ count = self._check_connection_for_service(count, vpn_service)
+ self.assertEqual(4, self.conn_create.call_count)
+
+ def test_sweep_connection_marked_as_clean(self):
+ """Sync updated connection - no action."""
+ # Create a service and connection
+ vpn_service = self.driver.create_vpn_service(self.service123_data)
+ connection = vpn_service.create_connection(self.conn1_data)
+ self.driver.mark_existing_connections_as_dirty()
+ # Simulate that the update phase visted both of them
+ vpn_service.is_dirty = False
+ connection.is_dirty = False
+ self.driver.remove_unknown_connections(self.context)
+ vpn_service = self.driver.service_state.get(u'123')
+ self.assertIsNotNone(vpn_service)
+ self.assertFalse(vpn_service.is_dirty)
+ connection = vpn_service.get_connection(u'1')
+ self.assertIsNotNone(connection)
+ self.assertFalse(connection.is_dirty)
+
+ def test_sweep_connection_dirty(self):
+ """Sync did not update connection - delete."""
+ # Create a service and connection
+ vpn_service = self.driver.create_vpn_service(self.service123_data)
+ vpn_service.create_connection(self.conn1_data)
+ self.driver.mark_existing_connections_as_dirty()
+ # Simulate that the update phase only visited the service
+ vpn_service.is_dirty = False
+ self.driver.remove_unknown_connections(self.context)
+ vpn_service = self.driver.service_state.get(u'123')
+ self.assertIsNotNone(vpn_service)
+ self.assertFalse(vpn_service.is_dirty)
+ connection = vpn_service.get_connection(u'1')
+ self.assertIsNone(connection)
+ self.assertEqual(1, self.conn_delete.call_count)
+
+ def test_sweep_service_dirty(self):
+ """Sync did not update service - delete it and all conns."""
+ # Create a service and connection
+ vpn_service = self.driver.create_vpn_service(self.service123_data)
+ vpn_service.create_connection(self.conn1_data)
+ self.driver.mark_existing_connections_as_dirty()
+ # Both the service and the connection are still 'dirty'
+ self.driver.remove_unknown_connections(self.context)
+ self.assertIsNone(self.driver.service_state.get(u'123'))
+ self.assertEqual(1, self.conn_delete.call_count)
+
+ def test_sweep_multiple_services(self):
+ """One service and conn udpated, one service and conn not."""
+ # Create two services, each with a connection
+ vpn_service1 = self.driver.create_vpn_service(self.service123_data)
+ vpn_service1.create_connection(self.conn1_data)
+ service456_data = {u'id': u'456',
+ u'status': constants.ACTIVE,
+ u'admin_state_up': False,
+ u'external_ip': u'1.1.1.1'}
+ conn2_data = {u'id': u'2', u'status': constants.ACTIVE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+ prev_vpn_service2 = self.driver.create_vpn_service(service456_data)
+ prev_connection2 = prev_vpn_service2.create_connection(conn2_data)
+ self.driver.mark_existing_connections_as_dirty()
+ # Simulate that the update phase visited the first service and conn
+ prev_vpn_service2.is_dirty = False
+ prev_connection2.is_dirty = False
+ self.driver.remove_unknown_connections(self.context)
+ self.assertIsNone(self.driver.service_state.get(u'123'))
+ vpn_service2 = self.driver.service_state.get(u'456')
+ self.assertEqual(prev_vpn_service2, vpn_service2)
+ self.assertFalse(vpn_service2.is_dirty)
+ connection2 = vpn_service2.get_connection(u'2')
+ self.assertEqual(prev_connection2, connection2)
+ self.assertFalse(connection2.is_dirty)
+ self.assertEqual(1, self.conn_delete.call_count)
+
+ def simulate_mark_update_sweep_for_service_with_conn(self, service_state,
+ connection_state):
+ """Create internal structures for single service with connection."""
+ # Simulate that we have done mark, update, and sweep.
+ conn_data = {u'id': u'1', u'status': connection_state,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel0'}}
+ service_data = {u'id': u'123',
+ u'status': service_state,
+ u'external_ip': u'1.1.1.1',
+ u'admin_state_up': True,
+ u'ipsec_conns': [conn_data]}
+ return self.driver.update_service(self.context, service_data)
+
+ def test_report_fragment_connection_created(self):
+ """Generate report section for a created connection."""
+ # Prepare service and connection in PENDING_CREATE state
+ vpn_service = self.simulate_mark_update_sweep_for_service_with_conn(
+ constants.PENDING_CREATE, constants.PENDING_CREATE)
+ # Simulate that CSR has reported the connection is still up
+ self.csr.read_tunnel_statuses.return_value = [
+ (u'Tunnel0', u'UP-ACTIVE'), ]
+
+ # Get the statuses for connections existing on CSR
+ tunnels = vpn_service.get_ipsec_connections_status()
+ self.assertEqual({u'Tunnel0': constants.ACTIVE}, tunnels)
+
+ # Check that there is a status for this connection
+ connection = vpn_service.get_connection(u'1')
+ self.assertIsNotNone(connection)
+ current_status = connection.find_current_status_in(tunnels)
+ self.assertEqual(constants.ACTIVE, current_status)
+
+ # Create report fragment due to change
+ self.assertNotEqual(connection.last_status, current_status)
+ report_frag = connection.build_report_based_on_status(current_status)
+ self.assertEqual(current_status, connection.last_status)
+ expected = {'1': {'status': constants.ACTIVE,
+ 'updated_pending_status': True}}
+ self.assertEqual(expected, report_frag)
+
+ def test_report_fragment_connection_unchanged_status(self):
+ """No report section generated for a created connection."""
+ # Prepare service and connection in ACTIVE state
+ vpn_service = self.simulate_mark_update_sweep_for_service_with_conn(
+ constants.ACTIVE, constants.ACTIVE)
+ # Simulate that CSR has reported the connection is up
+ self.csr.read_tunnel_statuses.return_value = [
+ (u'Tunnel0', u'UP-IDLE'), ]
+
+ # Get the statuses for connections existing on CSR
+ tunnels = vpn_service.get_ipsec_connections_status()
+ self.assertEqual({u'Tunnel0': constants.ACTIVE}, tunnels)
+
+ # Check that there is a status for this connection
+ connection = vpn_service.get_connection(u'1')
+ self.assertIsNotNone(connection)
+ current_status = connection.find_current_status_in(tunnels)
+ self.assertEqual(constants.ACTIVE, current_status)
+
+ # Should be no report, as no change
+ self.assertEqual(connection.last_status, current_status)
+ report_frag = connection.build_report_based_on_status(current_status)
+ self.assertEqual(current_status, connection.last_status)
+ self.assertEqual({}, report_frag)
+
+ def test_report_fragment_connection_changed_status(self):
+ """Generate report section for connection with changed state."""
+ # Prepare service in ACTIVE state and connection in DOWN state
+ vpn_service = self.simulate_mark_update_sweep_for_service_with_conn(
+ constants.ACTIVE, constants.DOWN)
+ # Simulate that CSR has reported the connection is still up
+ self.csr.read_tunnel_statuses.return_value = [
+ (u'Tunnel0', u'UP-NO-IKE'), ]
+
+ # Get the statuses for connections existing on CSR
+ tunnels = vpn_service.get_ipsec_connections_status()
+ self.assertEqual({u'Tunnel0': constants.ACTIVE}, tunnels)
+
+ # Check that there is a status for this connection
+ connection = vpn_service.get_connection(u'1')
+ self.assertIsNotNone(connection)
+ current_status = connection.find_current_status_in(tunnels)
+ self.assertEqual(constants.ACTIVE, current_status)
+
+ # Create report fragment due to change
+ self.assertNotEqual(connection.last_status, current_status)
+ report_frag = connection.build_report_based_on_status(current_status)
+ self.assertEqual(current_status, connection.last_status)
+ expected = {'1': {'status': constants.ACTIVE,
+ 'updated_pending_status': False}}
+ self.assertEqual(expected, report_frag)
+
+ def test_report_fragment_connection_failed_create(self):
+ """Failure test of report fragment for conn that failed creation.
+
+ Normally, without any status from the CSR, the connection report would
+ be skipped, but we need to report back failures.
+ """
+ # Prepare service and connection in PENDING_CREATE state
+ vpn_service = self.simulate_mark_update_sweep_for_service_with_conn(
+ constants.PENDING_CREATE, constants.PENDING_CREATE)
+ # Simulate that CSR does NOT report the status (no tunnel)
+ self.csr.read_tunnel_statuses.return_value = []
+
+ # Get the statuses for connections existing on CSR
+ tunnels = vpn_service.get_ipsec_connections_status()
+ self.assertEqual({}, tunnels)
+
+ # Check that there is a status for this connection
+ connection = vpn_service.get_connection(u'1')
+ self.assertIsNotNone(connection)
+ current_status = connection.find_current_status_in(tunnels)
+ self.assertEqual(constants.ERROR, current_status)
+
+ # Create report fragment due to change
+ self.assertNotEqual(connection.last_status, current_status)
+ report_frag = connection.build_report_based_on_status(current_status)
+ self.assertEqual(current_status, connection.last_status)
+ expected = {'1': {'status': constants.ERROR,
+ 'updated_pending_status': True}}
+ self.assertEqual(expected, report_frag)
+
+ def test_report_fragment_two_connections(self):
+ """Generate report fragment for two connections on a service."""
+ # Prepare service with two connections, one ACTIVE, one DOWN
+ conn1_data = {u'id': u'1', u'status': constants.DOWN,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel1'}}
+ conn2_data = {u'id': u'2', u'status': constants.ACTIVE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel2'}}
+ service_data = {u'id': u'123',
+ u'status': constants.ACTIVE,
+ u'external_ip': u'1.1.1.1',
+ u'admin_state_up': True,
+ u'ipsec_conns': [conn1_data, conn2_data]}
+ vpn_service = self.driver.update_service(self.context, service_data)
+ # Simulate that CSR has reported the connections with diff status
+ self.csr.read_tunnel_statuses.return_value = [
+ (u'Tunnel1', u'UP-IDLE'), (u'Tunnel2', u'DOWN-NEGOTIATING')]
+
+ # Get the report fragments for the connections
+ report_frag = self.driver.build_report_for_connections_on(vpn_service)
+ expected = {u'1': {u'status': constants.ACTIVE,
+ u'updated_pending_status': False},
+ u'2': {u'status': constants.DOWN,
+ u'updated_pending_status': False}}
+ self.assertEqual(expected, report_frag)
+
+ def test_report_service_create(self):
+ """VPN service and IPSec connection created - report."""
+ # Simulate creation of the service and connection
+ vpn_service = self.simulate_mark_update_sweep_for_service_with_conn(
+ constants.PENDING_CREATE, constants.PENDING_CREATE)
+ # Simulate that the CSR has created the connection
+ self.csr.read_tunnel_statuses.return_value = [
+ (u'Tunnel0', u'UP-ACTIVE'), ]
+
+ report = self.driver.build_report_for_service(vpn_service)
+ expected_report = {
+ u'id': u'123',
+ u'updated_pending_status': True,
+ u'status': constants.ACTIVE,
+ u'ipsec_site_connections': {
+ u'1': {u'status': constants.ACTIVE,
+ u'updated_pending_status': True}
+ }
+ }
+ self.assertEqual(expected_report, report)
+ # Check that service and connection statuses are updated
+ self.assertEqual(constants.ACTIVE, vpn_service.last_status)
+ self.assertEqual(constants.ACTIVE,
+ vpn_service.get_connection(u'1').last_status)
+
+ def test_report_service_create_of_first_conn_fails(self):
+ """VPN service and IPSec conn created, but conn failed - report.
+
+ Since this is the sole IPSec connection on the service, and the
+ create failed (connection in ERROR state), the VPN service's
+ status will be set to DOWN.
+ """
+ # Simulate creation of the service and connection
+ vpn_service = self.simulate_mark_update_sweep_for_service_with_conn(
+ constants.PENDING_CREATE, constants.PENDING_CREATE)
+ # Simulate that the CSR has no info due to failed create
+ self.csr.read_tunnel_statuses.return_value = []
+
+ report = self.driver.build_report_for_service(vpn_service)
+ expected_report = {
+ u'id': u'123',
+ u'updated_pending_status': True,
+ u'status': constants.DOWN,
+ u'ipsec_site_connections': {
+ u'1': {u'status': constants.ERROR,
+ u'updated_pending_status': True}
+ }
+ }
+ self.assertEqual(expected_report, report)
+ # Check that service and connection statuses are updated
+ self.assertEqual(constants.DOWN, vpn_service.last_status)
+ self.assertEqual(constants.ERROR,
+ vpn_service.get_connection(u'1').last_status)
+
+ def test_report_connection_created_on_existing_service(self):
+ """Creating connection on existing service - report."""
+ # Simulate existing service and connection create
+ vpn_service = self.simulate_mark_update_sweep_for_service_with_conn(
+ constants.ACTIVE, constants.PENDING_CREATE)
+ # Simulate that the CSR has created the connection
+ self.csr.read_tunnel_statuses.return_value = [
+ (u'Tunnel0', u'UP-IDLE'), ]
+
+ report = self.driver.build_report_for_service(vpn_service)
+ expected_report = {
+ u'id': u'123',
+ u'updated_pending_status': False,
+ u'status': constants.ACTIVE,
+ u'ipsec_site_connections': {
+ u'1': {u'status': constants.ACTIVE,
+ u'updated_pending_status': True}
+ }
+ }
+ self.assertEqual(expected_report, report)
+ # Check that service and connection statuses are updated
+ self.assertEqual(constants.ACTIVE, vpn_service.last_status)
+ self.assertEqual(constants.ACTIVE,
+ vpn_service.get_connection(u'1').last_status)
+
+ def test_no_report_no_changes(self):
+ """VPN service with unchanged IPSec connection - no report.
+
+ Note: No report will be generated if the last connection on the
+ service is deleted. The service (and connection) objects will
+ have been reoved by the sweep operation and thus not reported.
+ On the plugin, the service should be changed to DOWN. Likewise,
+ if the service goes to admin down state.
+ """
+ # Simulate an existing service and connection that are ACTIVE
+ vpn_service = self.simulate_mark_update_sweep_for_service_with_conn(
+ constants.ACTIVE, constants.ACTIVE)
+ # Simulate that the CSR reports the connection still active
+ self.csr.read_tunnel_statuses.return_value = [
+ (u'Tunnel0', u'UP-ACTIVE'), ]
+
+ report = self.driver.build_report_for_service(vpn_service)
+ self.assertEqual({}, report)
+ # Check that service and connection statuses are still same
+ self.assertEqual(constants.ACTIVE, vpn_service.last_status)
+ self.assertEqual(constants.ACTIVE,
+ vpn_service.get_connection(u'1').last_status)
+
+ def test_report_sole_connection_goes_down(self):
+ """Only connection on VPN service goes down - report.
+
+ In addition to reporting the status change and recording the new
+ state for the IPSec connection, the VPN service status will be
+ ACTIVE.
+ """
+ # Simulate an existing service and connection that are ACTIVE
+ vpn_service = self.simulate_mark_update_sweep_for_service_with_conn(
+ constants.ACTIVE, constants.ACTIVE)
+ # Simulate that the CSR reports the connection went down
+ self.csr.read_tunnel_statuses.return_value = [
+ (u'Tunnel0', u'DOWN-NEGOTIATING'), ]
+
+ report = self.driver.build_report_for_service(vpn_service)
+ expected_report = {
+ u'id': u'123',
+ u'updated_pending_status': False,
+ u'status': constants.DOWN,
+ u'ipsec_site_connections': {
+ u'1': {u'status': constants.DOWN,
+ u'updated_pending_status': False}
+ }
+ }
+ self.assertEqual(expected_report, report)
+ # Check that service and connection statuses are updated
+ self.assertEqual(constants.DOWN, vpn_service.last_status)
+ self.assertEqual(constants.DOWN,
+ vpn_service.get_connection(u'1').last_status)
+
+ def test_report_sole_connection_comes_up(self):
+ """Only connection on VPN service comes up - report.
+
+ In addition to reporting the status change and recording the new
+ state for the IPSec connection, the VPN service status will be
+ DOWN.
+ """
+ # Simulate an existing service and connection that are DOWN
+ vpn_service = self.simulate_mark_update_sweep_for_service_with_conn(
+ constants.DOWN, constants.DOWN)
+ # Simulate that the CSR reports the connection came up
+ self.csr.read_tunnel_statuses.return_value = [
+ (u'Tunnel0', u'UP-NO-IKE'), ]
+
+ report = self.driver.build_report_for_service(vpn_service)
+ expected_report = {
+ u'id': u'123',
+ u'updated_pending_status': False,
+ u'status': constants.ACTIVE,
+ u'ipsec_site_connections': {
+ u'1': {u'status': constants.ACTIVE,
+ u'updated_pending_status': False}
+ }
+ }
+ self.assertEqual(expected_report, report)
+ # Check that service and connection statuses are updated
+ self.assertEqual(constants.ACTIVE, vpn_service.last_status)
+ self.assertEqual(constants.ACTIVE,
+ vpn_service.get_connection(u'1').last_status)
+
+ def test_report_service_with_two_connections_gone_down(self):
+ """One service with two connections that went down - report.
+
+ If there is more than one IPSec connection on a VPN service, the
+ service will always report as being ACTIVE (whereas, if there is
+ only one connection, the service will reflect the connection status.
+ """
+ # Simulated one service with two ACTIVE connections
+ conn1_data = {u'id': u'1', u'status': constants.ACTIVE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel1'}}
+ conn2_data = {u'id': u'2', u'status': constants.ACTIVE,
+ u'admin_state_up': True,
+ u'cisco': {u'site_conn_id': u'Tunnel2'}}
+ service_data = {u'id': u'123',
+ u'status': constants.ACTIVE,
+ u'external_ip': u'1.1.1.1',
+ u'admin_state_up': True,
+ u'ipsec_conns': [conn1_data, conn2_data]}
+ vpn_service = self.driver.update_service(self.context, service_data)
+ # Simulate that the CSR has reported that the connections are DOWN
+ self.csr.read_tunnel_statuses.return_value = [
+ (u'Tunnel1', u'DOWN-NEGOTIATING'), (u'Tunnel2', u'DOWN')]
+
+ report = self.driver.build_report_for_service(vpn_service)
+ expected_report = {
+ u'id': u'123',
+ u'updated_pending_status': False,
+ u'status': constants.ACTIVE,
+ u'ipsec_site_connections': {
+ u'1': {u'status': constants.DOWN,
+ u'updated_pending_status': False},
+ u'2': {u'status': constants.DOWN,
+ u'updated_pending_status': False}}
+ }
+ self.assertEqual(expected_report, report)
+ # Check that service and connection statuses are updated
+ self.assertEqual(constants.ACTIVE, vpn_service.last_status)
+ self.assertEqual(constants.DOWN,
+ vpn_service.get_connection(u'1').last_status)
+ self.assertEqual(constants.DOWN,
+ vpn_service.get_connection(u'2').last_status)
+
+ def test_report_multiple_services(self):
+ """Status changes for several services - report."""
+ # Simulate creation of the service and connection
+ (service1_data,
+ service2_data) = self.notification_for_two_services_with_two_conns()
+ vpn_service1 = self.driver.update_service(self.context, service1_data)
+ vpn_service2 = self.driver.update_service(self.context, service2_data)
+ # Simulate that the CSR has created the connections
+ self.csr.read_tunnel_statuses.return_value = [
+ (u'Tunnel1', u'UP-ACTIVE'), (u'Tunnel2', u'DOWN'),
+ (u'Tunnel3', u'DOWN-NEGOTIATING'), (u'Tunnel4', u'UP-IDLE')]
+
+ report = self.driver.report_status(self.context)
+ expected_report = [{u'id': u'123',
+ u'updated_pending_status': True,
+ u'status': constants.ACTIVE,
+ u'ipsec_site_connections': {
+ u'1': {u'status': constants.ACTIVE,
+ u'updated_pending_status': True},
+ u'2': {u'status': constants.DOWN,
+ u'updated_pending_status': True}}
+ },
+ {u'id': u'456',
+ u'updated_pending_status': True,
+ u'status': constants.ACTIVE,
+ u'ipsec_site_connections': {
+ u'3': {u'status': constants.DOWN,
+ u'updated_pending_status': True},
+ u'4': {u'status': constants.ACTIVE,
+ u'updated_pending_status': True}}
+ }]
+ self.assertEqual(expected_report, report)
+ # Check that service and connection statuses are updated
+ self.assertEqual(constants.ACTIVE, vpn_service1.last_status)
+ self.assertEqual(constants.ACTIVE,
+ vpn_service1.get_connection(u'1').last_status)
+ self.assertEqual(constants.DOWN,
+ vpn_service1.get_connection(u'2').last_status)
+ self.assertEqual(constants.ACTIVE, vpn_service2.last_status)
+ self.assertEqual(constants.DOWN,
+ vpn_service2.get_connection(u'3').last_status)
+ self.assertEqual(constants.ACTIVE,
+ vpn_service2.get_connection(u'4').last_status)
+
+ # TODO(pcm) FUTURE - UTs for update action, when supported.
+
+ def test_vpnservice_updated(self):
+ with mock.patch.object(self.driver, 'sync') as sync:
+ context = mock.Mock()
+ self.driver.vpnservice_updated(context)
+ sync.assert_called_once_with(context, [])
+
+
+class TestCiscoCsrIPsecDeviceDriverConfigLoading(base.BaseTestCase):
+
+ def setUp(self):
+ super(TestCiscoCsrIPsecDeviceDriverConfigLoading, self).setUp()
+ self.addCleanup(mock.patch.stopall)
+
+ def create_tempfile(self, contents):
+ (fd, path) = tempfile.mkstemp(prefix='test', suffix='.conf')
+ try:
+ os.write(fd, contents.encode('utf-8'))
+ finally:
+ os.close(fd)
+ return path
+
+ def test_loading_csr_configuration(self):
+ """Ensure that Cisco CSR configs can be loaded from config files."""
+ cfg_file = self.create_tempfile('[CISCO_CSR_REST:3.2.1.1]\n'
+ 'rest_mgmt = 10.20.30.1\n'
+ 'tunnel_ip = 3.2.1.3\n'
+ 'username = me\n'
+ 'password = secret\n'
+ 'timeout = 5.0\n')
+ expected = {'3.2.1.1': {'rest_mgmt': '10.20.30.1',
+ 'tunnel_ip': '3.2.1.3',
+ 'username': 'me',
+ 'password': 'secret',
+ 'timeout': 5.0}}
+ csrs_found = ipsec_driver.find_available_csrs_from_config([cfg_file])
+ self.assertEqual(expected, csrs_found)
+
+ def test_loading_config_without_timeout(self):
+ """Cisco CSR config without timeout will use default timeout."""
+ cfg_file = self.create_tempfile('[CISCO_CSR_REST:3.2.1.1]\n'
+ 'rest_mgmt = 10.20.30.1\n'
+ 'tunnel_ip = 3.2.1.3\n'
+ 'username = me\n'
+ 'password = secret\n')
+ expected = {'3.2.1.1': {'rest_mgmt': '10.20.30.1',
+ 'tunnel_ip': '3.2.1.3',
+ 'username': 'me',
+ 'password': 'secret',
+ 'timeout': csr_client.TIMEOUT}}
+ csrs_found = ipsec_driver.find_available_csrs_from_config([cfg_file])
+ self.assertEqual(expected, csrs_found)
+
+ def test_skip_loading_duplicate_csr_configuration(self):
+ """Failure test that duplicate configurations are ignored."""
+ cfg_file = self.create_tempfile('[CISCO_CSR_REST:3.2.1.1]\n'
+ 'rest_mgmt = 10.20.30.1\n'
+ 'tunnel_ip = 3.2.1.3\n'
+ 'username = me\n'
+ 'password = secret\n'
+ 'timeout = 5.0\n'
+ '[CISCO_CSR_REST:3.2.1.1]\n'
+ 'rest_mgmt = 5.5.5.3\n'
+ 'tunnel_ip = 3.2.1.6\n'
+ 'username = me\n'
+ 'password = secret\n')
+ expected = {'3.2.1.1': {'rest_mgmt': '10.20.30.1',
+ 'tunnel_ip': '3.2.1.3',
+ 'username': 'me',
+ 'password': 'secret',
+ 'timeout': 5.0}}
+ csrs_found = ipsec_driver.find_available_csrs_from_config([cfg_file])
+ self.assertEqual(expected, csrs_found)
+
+ def test_fail_loading_config_with_invalid_timeout(self):
+ """Failure test of invalid timeout in config info."""
+ cfg_file = self.create_tempfile('[CISCO_CSR_REST:3.2.1.1]\n'
+ 'rest_mgmt = 10.20.30.1\n'
+ 'tunnel_ip = 3.2.1.3\n'
+ 'username = me\n'
+ 'password = secret\n'
+ 'timeout = yes\n')
+ csrs_found = ipsec_driver.find_available_csrs_from_config([cfg_file])
+ self.assertEqual({}, csrs_found)
+
+ def test_fail_loading_config_missing_required_info(self):
+ """Failure test of config missing required info."""
+ cfg_file = self.create_tempfile('[CISCO_CSR_REST:1.1.1.0]\n'
+ 'tunnel_ip = 1.1.1.3\n'
+ 'username = me\n'
+ 'password = secret\n'
+ 'timeout = 5.0\n'
+ '[CISCO_CSR_REST:2.2.2.0]\n'
+ 'rest_mgmt = 10.20.30.1\n'
+ 'username = me\n'
+ 'password = secret\n'
+ 'timeout = 5.0\n'
+ '[CISCO_CSR_REST:3.3.3.0]\n'
+ 'rest_mgmt = 10.20.30.1\n'
+ 'tunnel_ip = 3.3.3.3\n'
+ 'password = secret\n'
+ 'timeout = 5.0\n'
+ '[CISCO_CSR_REST:4.4.4.0]\n'
+ 'rest_mgmt = 10.20.30.1\n'
+ 'tunnel_ip = 4.4.4.4\n'
+ 'username = me\n'
+ 'timeout = 5.0\n')
+ csrs_found = ipsec_driver.find_available_csrs_from_config([cfg_file])
+ self.assertEqual({}, csrs_found)
+
+ def test_fail_loading_config_with_invalid_router_id(self):
+ """Failure test of config with invalid rotuer ID."""
+ cfg_file = self.create_tempfile('[CISCO_CSR_REST:4.3.2.1.9]\n'
+ 'rest_mgmt = 10.20.30.1\n'
+ 'tunnel_ip = 4.3.2.3\n'
+ 'username = me\n'
+ 'password = secret\n'
+ 'timeout = 5.0\n')
+ csrs_found = ipsec_driver.find_available_csrs_from_config([cfg_file])
+ self.assertEqual({}, csrs_found)
+
+ def test_fail_loading_config_with_invalid_mgmt_ip(self):
+ """Failure test of configuration with invalid management IP address."""
+ cfg_file = self.create_tempfile('[CISCO_CSR_REST:3.2.1.1]\n'
+ 'rest_mgmt = 1.1.1.1.1\n'
+ 'tunnel_ip = 3.2.1.3\n'
+ 'username = me\n'
+ 'password = secret\n'
+ 'timeout = 5.0\n')
+ csrs_found = ipsec_driver.find_available_csrs_from_config([cfg_file])
+ self.assertEqual({}, csrs_found)
+
+ def test_fail_loading_config_with_invalid_tunnel_ip(self):
+ """Failure test of configuration with invalid tunnel IP address."""
+ cfg_file = self.create_tempfile('[CISCO_CSR_REST:3.2.1.1]\n'
+ 'rest_mgmt = 1.1.1.1\n'
+ 'tunnel_ip = 3.2.1.4.5\n'
+ 'username = me\n'
+ 'password = secret\n'
+ 'timeout = 5.0\n')
+ csrs_found = ipsec_driver.find_available_csrs_from_config([cfg_file])
+ self.assertEqual({}, csrs_found)
+
+ def test_failure_no_configurations_entries(self):
+ """Failure test config file without any CSR definitions."""
+ cfg_file = self.create_tempfile('NO CISCO SECTION AT ALL\n')
+ csrs_found = ipsec_driver.find_available_csrs_from_config([cfg_file])
+ self.assertEqual({}, csrs_found)
+
+ def test_failure_no_csr_configurations_entries(self):
+ """Failure test config file without any CSR definitions."""
+ cfg_file = self.create_tempfile('[SOME_CONFIG:123]\n'
+ 'username = me\n')
+ csrs_found = ipsec_driver.find_available_csrs_from_config([cfg_file])
+ self.assertEqual({}, csrs_found)
+
+ def test_missing_config_value(self):
+ """Failure test of config file missing a value for attribute."""
+ cfg_file = self.create_tempfile('[CISCO_CSR_REST:3.2.1.1]\n'
+ 'rest_mgmt = \n'
+ 'tunnel_ip = 3.2.1.3\n'
+ 'username = me\n'
+ 'password = secret\n'
+ 'timeout = 5.0\n')
+ csrs_found = ipsec_driver.find_available_csrs_from_config([cfg_file])
+ self.assertEqual({}, csrs_found)
+
+ def test_ignores_invalid_attribute_in_config(self):
+ """Test ignoring of config file with invalid attribute."""
+ cfg_file = self.create_tempfile('[CISCO_CSR_REST:3.2.1.1]\n'
+ 'rest_mgmt = 1.1.1.1\n'
+ 'bogus = abcdef\n'
+ 'tunnel_ip = 3.2.1.3\n'
+ 'username = me\n'
+ 'password = secret\n'
+ 'timeout = 15.5\n')
+ expected = {'3.2.1.1': {'rest_mgmt': '1.1.1.1',
+ 'tunnel_ip': '3.2.1.3',
+ 'username': 'me',
+ 'password': 'secret',
+ 'timeout': 15.5}}
+ csrs_found = ipsec_driver.find_available_csrs_from_config([cfg_file])
+ self.assertEqual(expected, csrs_found)