message = _("Cisco CSR failed to create %(resource)s (%(which)s)")
+class CsrAdminStateChangeFailure(exceptions.NeutronException):
+ message = _("Cisco CSR failed to change %(tunnel)s admin state to "
+ "%(state)s")
+
+
class CsrDriverMismatchError(exceptions.NeutronException):
message = _("Required %(resource)s attribute %(attr)s mapping for Cisco "
"CSR is missing in device driver")
conn_id = conn_data['id']
conn_is_admin_up = conn_data[u'admin_state_up']
- if conn_id in vpn_service.conn_state:
+ if conn_id in vpn_service.conn_state: # Existing connection...
ipsec_conn = vpn_service.conn_state[conn_id]
+ config_changed = ipsec_conn.check_for_changes(conn_data)
+ if config_changed:
+ LOG.debug(_("Update: Existing connection %s changed"), conn_id)
+ ipsec_conn.delete_ipsec_site_connection(context, conn_id)
+ ipsec_conn.create_ipsec_site_connection(context, conn_data)
+ ipsec_conn.conn_info = conn_data
+
if ipsec_conn.forced_down:
if vpn_service.is_admin_up and conn_is_admin_up:
LOG.debug(_("Update: Connection %s no longer admin down"),
conn_id)
- # TODO(pcm) Do no shut on tunnel, once CSR supports
+ ipsec_conn.set_admin_state(is_up=True)
ipsec_conn.forced_down = False
- ipsec_conn.create_ipsec_site_connection(context, conn_data)
else:
if not vpn_service.is_admin_up or not conn_is_admin_up:
LOG.debug(_("Update: Connection %s forced to admin down"),
conn_id)
- # TODO(pcm) Do shut on tunnel, once CSR supports
+ ipsec_conn.set_admin_state(is_up=False)
ipsec_conn.forced_down = True
- ipsec_conn.delete_ipsec_site_connection(context, conn_id)
- else:
- # TODO(pcm) FUTURE handle connection update
- LOG.debug(_("Update: Ignoring existing connection %s"),
- conn_id)
else: # New connection...
ipsec_conn = vpn_service.create_connection(conn_data)
+ ipsec_conn.create_ipsec_site_connection(context, conn_data)
if not vpn_service.is_admin_up or not conn_is_admin_up:
- # TODO(pcm) Create, but set tunnel down, once CSR supports
LOG.debug(_("Update: Created new connection %s in admin down "
"state"), conn_id)
+ ipsec_conn.set_admin_state(is_up=False)
ipsec_conn.forced_down = True
else:
LOG.debug(_("Update: Created new connection %s"), conn_id)
- ipsec_conn.create_ipsec_site_connection(context, conn_data)
ipsec_conn.is_dirty = False
ipsec_conn.last_status = conn_data['status']
"""State and actions for IPSec site-to-site connections."""
def __init__(self, conn_info, csr):
- self.conn_id = conn_info['id']
+ self.conn_info = conn_info
self.csr = csr
self.steps = []
self.forced_down = False
- self.is_admin_up = conn_info[u'admin_state_up']
- self.tunnel = conn_info['cisco']['site_conn_id']
+ self.changed = False
+
+ @property
+ def conn_id(self):
+ return self.conn_info['id']
+
+ @property
+ def is_admin_up(self):
+ return self.conn_info['admin_state_up']
+
+ @is_admin_up.setter
+ def is_admin_up(self, is_up):
+ self.conn_info['admin_state_up'] = is_up
+
+ @property
+ def tunnel(self):
+ return self.conn_info['cisco']['site_conn_id']
+
+ def check_for_changes(self, curr_conn):
+ return not all([self.conn_info[attr] == curr_conn[attr]
+ for attr in ('mtu', 'psk', 'peer_address',
+ 'peer_cidrs', 'ike_policy',
+ 'ipsec_policy', 'cisco')])
def find_current_status_in(self, statuses):
if self.tunnel in statuses:
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': self.csr.tunnel_ip
# u'tunnel-ip-address': u'%s' % gw_ip
},
u'remote-device': {
LOG.info(_("SUCCESS: Deleted IPSec site-to-site connection %s"),
conn_id)
+
+ def set_admin_state(self, is_up):
+ """Change the admin state for the IPSec connection."""
+ self.csr.set_ipsec_connection_state(self.tunnel, admin_up=is_up)
+ if self.csr.status != requests.codes.NO_CONTENT:
+ state = "UP" if is_up else "DOWN"
+ LOG.error(_("Unable to change %(tunnel)s admin state to "
+ "%(state)s"), {'tunnel': self.tunnel, 'state': state})
+ raise CsrAdminStateChangeFailure(tunnel=self.tunnel, state=state)
#
# @author: Paul Michali, Cisco Systems, Inc.
+import copy
import httplib
import os
import tempfile
}
self.csr = mock.Mock(spec=csr_client.CsrRestClient)
self.csr.status = 201 # All calls to CSR REST API succeed
+ self.csr.tunnel_ip = '172.24.4.23'
self.ipsec_conn = ipsec_driver.CiscoCsrIPSecConnection(self.conn_info,
self.csr)
# TODO(pcm) get from vpnservice['external_ip']
'router_public_ip': '172.24.4.23'}
}
+ self.csr = mock.Mock(spec=csr_client.CsrRestClient)
+ self.csr.tunnel_ip = '172.24.4.23'
self.ipsec_conn = ipsec_driver.CiscoCsrIPSecConnection(self.conn_info,
- mock.Mock())
+ self.csr)
def test_invalid_attribute(self):
"""Negative test of unknown attribute - programming error."""
u'ipsec-policy-id': 333,
u'local-device': {
u'ip-address': u'GigabitEthernet3',
- u'tunnel-ip-address': u'172.24.4.23'
+ u'tunnel-ip-address': '172.24.4.23'
},
u'remote-device': {
u'tunnel-ip-address': '192.168.1.2'
self.conn_delete = mock.patch.object(
ipsec_driver.CiscoCsrIPSecConnection,
'delete_ipsec_site_connection').start()
+ self.admin_state = mock.patch.object(
+ ipsec_driver.CiscoCsrIPSecConnection,
+ 'set_admin_state').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,
+ self.conn1_data = {u'id': u'1',
+ u'status': constants.ACTIVE,
u'admin_state_up': True,
+ u'mtu': 1500,
+ u'psk': u'secret',
+ u'peer_address': '192.168.1.2',
+ u'peer_cidrs': ['10.1.0.0/24', '10.2.0.0/24'],
+ u'ike_policy': {
+ u'auth_algorithm': u'sha1',
+ u'encryption_algorithm': u'aes-128',
+ u'pfs': u'Group5',
+ u'ike_version': u'v1',
+ u'lifetime_units': u'seconds',
+ u'lifetime_value': 3600},
+ u'ipsec_policy': {
+ u'transform_protocol': u'ah',
+ u'encryption_algorithm': u'aes-128',
+ u'auth_algorithm': u'sha1',
+ u'pfs': u'group5',
+ u'lifetime_units': u'seconds',
+ u'lifetime_value': 3600},
u'cisco': {u'site_conn_id': u'Tunnel0'}}
# NOTE: For sync, there is mark (trivial), update (tested),
"""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'}}
+ conn_data = copy.deepcopy(self.conn1_data)
+ conn_data[u'status'] = constants.PENDING_CREATE
connection = self.driver.update_connection(self.context,
u'123', conn_data)
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
+ def test_detect_no_change_to_ipsec_connection(self):
+ """No change to IPSec connection - nop."""
+ # Make existing service, and connection that was active
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)
+ connection = vpn_service.create_connection(self.conn1_data)
+
+ self.assertFalse(connection.check_for_changes(self.conn1_data))
+
+ def test_detect_state_only_change_to_ipsec_connection(self):
+ """Only IPSec connection state changed - update."""
+ # Make existing service, and connection that was active
+ vpn_service = self.driver.create_vpn_service(self.service123_data)
+ connection = vpn_service.create_connection(self.conn1_data)
+
+ conn_data = copy.deepcopy(self.conn1_data)
+ conn_data[u'admin_state_up'] = False
+ self.assertFalse(connection.check_for_changes(conn_data))
+
+ def test_detect_non_state_change_to_ipsec_connection(self):
+ """Connection change instead of/in addition to state - update."""
+ # Make existing service, and connection that was active
+ vpn_service = self.driver.create_vpn_service(self.service123_data)
+ connection = vpn_service.create_connection(self.conn1_data)
+
+ conn_data = copy.deepcopy(self.conn1_data)
+ conn_data[u'ipsec_policy'][u'encryption_algorithm'] = u'aes-256'
+ self.assertTrue(connection.check_for_changes(conn_data))
+
+ def test_update_ipsec_connection_changed_admin_down(self):
+ """Notified of connection state change - update.
+
+ For a connection that was previously created, expect to
+ force connection down on an admin down (only) change.
+ """
+
+ # Make existing service, and connection that was active
+ vpn_service = self.driver.create_vpn_service(self.service123_data)
+ connection = vpn_service.create_connection(self.conn1_data)
+
+ # Simulate that notification of connection update received
self.driver.mark_existing_connections_as_dirty()
+ # Modify the connection data for the 'sync'
+ conn_data = copy.deepcopy(self.conn1_data)
+ conn_data[u'admin_state_up'] = False
connection = self.driver.update_connection(self.context,
'123', conn_data)
self.assertEqual(u'Tunnel0', connection.tunnel)
self.assertEqual(constants.ACTIVE, connection.last_status)
self.assertFalse(self.conn_create.called)
- # TODO(pcm) FUTURE - handling for update (delete/create?)
-
- def test_update_of_unknown_ipsec_connection(self):
- """Notified of update of unknown connection - create.
+ self.assertFalse(connection.is_admin_up)
+ self.assertTrue(connection.forced_down)
+ self.assertEqual(1, self.admin_state.call_count)
- 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.
+ def test_update_ipsec_connection_changed_config(self):
+ """Notified of connection changing config - update.
- 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.
+ Goal here is to detect that the connection is deleted and then
+ created, but not that the specific values have changed, so picking
+ arbitrary value (MTU).
"""
- # 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
+ # Make existing service, and connection that was active
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)
+ connection = vpn_service.create_connection(self.conn1_data)
+
+ # Simulate that notification of connection update received
self.driver.mark_existing_connections_as_dirty()
- # The notification (state) hasn't changed for the connection
+ # Modify the connection data for the 'sync'
+ conn_data = copy.deepcopy(self.conn1_data)
+ conn_data[u'mtu'] = 9200
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.assertFalse(self.conn_create.called)
+ self.assertEqual(1, self.conn_create.call_count)
+ self.assertEqual(1, self.conn_delete.call_count)
+ self.assertTrue(connection.is_admin_up)
+ self.assertFalse(connection.forced_down)
+ self.assertFalse(self.admin_state.called)
- def test_update_connection_admin_down(self):
- """Connection updated to admin down state - force down."""
- # Make existing service, and connection 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)
+ 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.
+ """
+ # Will have previously created service, but don't know of connection
+ self.driver.create_vpn_service(self.service123_data)
+
+ # Simulate that notification of connection update received
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 = copy.deepcopy(self.conn1_data)
conn_data[u'status'] = constants.DOWN
connection = self.driver.update_connection(self.context,
u'123', conn_data)
self.assertFalse(connection.is_dirty)
- self.assertTrue(connection.forced_down)
self.assertEqual(u'Tunnel0', connection.tunnel)
self.assertEqual(constants.DOWN, connection.last_status)
- self.assertFalse(self.conn_create.called)
+ self.assertEqual(1, self.conn_create.call_count)
+ self.assertTrue(connection.is_admin_up)
+ self.assertFalse(connection.forced_down)
+ self.assertFalse(self.admin_state.called)
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, create the structures,
+ a connection that is in admin down state, recreate the connection,
but indicate that the connection is down.
"""
# 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'}}
+ conn_data = copy.deepcopy(self.conn1_data)
+ conn_data.update({u'status': constants.DOWN,
+ u'admin_state_up': False})
connection = self.driver.update_connection(self.context,
u'123', conn_data)
self.assertIsNotNone(connection)
self.assertFalse(connection.is_dirty)
+ self.assertEqual(1, self.conn_create.call_count)
self.assertFalse(connection.is_admin_up)
self.assertTrue(connection.forced_down)
- self.assertFalse(self.conn_create.called)
+ self.assertEqual(1, self.admin_state.call_count)
def test_update_connection_admin_up(self):
"""Connection updated to admin up state - record."""
# Make existing service, and connection that was admin down
- conn_data = {u'id': '1', u'status': constants.DOWN,
- u'admin_state_up': False,
- u'cisco': {u'site_conn_id': u'Tunnel0'}}
+ conn_data = copy.deepcopy(self.conn1_data)
+ conn_data.update({u'status': constants.DOWN, u'admin_state_up': False})
service_data = {u'id': u'123',
u'status': constants.DOWN,
u'external_ip': u'1.1.1.1',
u'admin_state_up': True,
u'ipsec_conns': [conn_data]}
self.driver.update_service(self.context, service_data)
+
+ # Simulate that notification of connection update received
self.driver.mark_existing_connections_as_dirty()
# Now simulate that the notification shows the connection admin up
- conn_data[u'admin_state_up'] = True
- conn_data[u'status'] = constants.DOWN
+ new_conn_data = copy.deepcopy(conn_data)
+ new_conn_data[u'admin_state_up'] = True
connection = self.driver.update_connection(self.context,
- u'123', conn_data)
+ u'123', new_conn_data)
self.assertFalse(connection.is_dirty)
- self.assertFalse(connection.forced_down)
self.assertEqual(u'Tunnel0', connection.tunnel)
self.assertEqual(constants.DOWN, connection.last_status)
- self.assertEqual(1, self.conn_create.call_count)
+ self.assertTrue(connection.is_admin_up)
+ self.assertFalse(connection.forced_down)
+ self.assertEqual(2, self.admin_state.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'}}
+ conn_data = copy.deepcopy(self.conn1_data)
+ conn_data[u'status'] = constants.PENDING_CREATE
service_data = {u'id': u'123',
u'status': constants.PENDING_CREATE,
u'external_ip': u'1.1.1.1',
self.assertEqual(u'Tunnel0', connection.tunnel)
self.assertEqual(constants.PENDING_CREATE, connection.last_status)
self.assertEqual(1, self.conn_create.call_count)
+ self.assertTrue(connection.is_admin_up)
+ self.assertFalse(connection.forced_down)
+ self.assertFalse(self.admin_state.called)
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'}}
+ conn_data = copy.deepcopy(self.conn1_data)
+ conn_data[u'status'] = constants.PENDING_CREATE
service_data = {u'id': u'123',
u'status': constants.ACTIVE,
u'external_ip': u'1.1.1.1',
"""
# 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)
+ prev_vpn_service.create_connection(self.conn1_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]}
+ u'ipsec_conns': [self.conn1_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)
"""
# 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]}
+ u'ipsec_conns': [self.conn1_data]}
vpn_service = self.driver.update_service(self.context, service_data)
self.assertEqual(prev_vpn_service, vpn_service)
self.assertFalse(vpn_service.is_dirty)
of a service that is in the admin down state. Structures will be
created, but forced down.
"""
- 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]}
+ u'ipsec_conns': [self.conn1_data]}
vpn_service = self.driver.update_service(self.context, service_data)
self.assertIsNotNone(vpn_service)
self.assertFalse(vpn_service.is_dirty)
self.assertEqual(1, self.conn_delete.call_count)
def test_sweep_multiple_services(self):
- """One service and conn udpated, one service and conn not."""
+ """One service and conn updated, 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)
# Simulate one service with one connection up, one down
conn1_data = {u'id': u'1', u'status': constants.ACTIVE,
u'admin_state_up': True,
+ u'mtu': 1500,
+ u'psk': u'secret',
+ u'peer_address': '192.168.1.2',
+ u'peer_cidrs': ['10.1.0.0/24', '10.2.0.0/24'],
+ u'ike_policy': {u'auth_algorithm': u'sha1',
+ u'encryption_algorithm': u'aes-128',
+ u'pfs': u'Group5',
+ u'ike_version': u'v1',
+ u'lifetime_units': u'seconds',
+ u'lifetime_value': 3600},
+ u'ipsec_policy': {u'transform_protocol': u'ah',
+ u'encryption_algorithm': u'aes-128',
+ u'auth_algorithm': u'sha1',
+ u'pfs': u'group5',
+ u'lifetime_units': u'seconds',
+ u'lifetime_value': 3600},
u'cisco': {u'site_conn_id': u'Tunnel1'}}
conn2_data = {u'id': u'2', u'status': constants.DOWN,
u'admin_state_up': True,
+ u'mtu': 1500,
+ u'psk': u'secret',
+ u'peer_address': '192.168.1.2',
+ u'peer_cidrs': ['10.1.0.0/24', '10.2.0.0/24'],
+ u'ike_policy': {u'auth_algorithm': u'sha1',
+ u'encryption_algorithm': u'aes-128',
+ u'pfs': u'Group5',
+ u'ike_version': u'v1',
+ u'lifetime_units': u'seconds',
+ u'lifetime_value': 3600},
+ u'ipsec_policy': {u'transform_protocol': u'ah',
+ u'encryption_algorithm': u'aes-128',
+ u'auth_algorithm': u'sha1',
+ u'pfs': u'group5',
+ u'lifetime_units': u'seconds',
+ u'lifetime_value': 3600},
u'cisco': {u'site_conn_id': u'Tunnel2'}}
service_data = {u'id': u'123',
u'status': constants.ACTIVE,