Cisco VPN device driver - support IPSec connection updates

Provides support for IPSec connection updates and state changes. To do
this, the configuration of the connection is maintained, when the
connection is created. This is checked against the current settings, at
sync time, to determine whether a configuration change (as opposed to a
state change) has occurred.

If there is a change to the configuration detected, then the simple
approach is taken of deleting and then re-creating the connection, with
the new settings.

In addition, if the admin state of the connection changes, the tunnel
will be taken admin down/up, as needed. Admin down will occur if the
IPSec connection or the associated VPN service is set to admin down.
Admin up will occur, if both the IPSec connection and the VPN service
are in admin up state.

Added REST client method to allow changing the IPSec connection tunnel
to admin up/down (effectively doing a no-shut/shut on the tunnel I/F),
based on the above mentioned state.

Modified UTs for the support of IPSec connection update requests (used to
throw an "unsupported" exception), and to check that the configuration
and state changing are processed correctly.

Updated so that tunnel_ip is set in device driver, rather than hard
coding, and then overriding in REST client. Since device driver has the
same info, this will fit into future plans to obtain the info from
router, vs reading an .ini file. Revised UTs as well.

Change-Id: I184942d7f2f282c867ba020f62cd48ec53315d3e
Closes-Bug: 1303830
This commit is contained in:
Paul Michali 2014-04-04 19:14:36 +00:00
parent 6313680cf0
commit 083481d7e0
7 changed files with 312 additions and 126 deletions

View File

@ -214,9 +214,6 @@ class CsrRestClient(object):
base_conn_info = {u'vpn-type': u'site-to-site', base_conn_info = {u'vpn-type': u'site-to-site',
u'ip-version': u'ipv4'} u'ip-version': u'ipv4'}
connection_info.update(base_conn_info) 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', return self.post_request('vpn-svc/site-to-site',
payload=connection_info) payload=connection_info)
@ -232,6 +229,14 @@ class CsrRestClient(object):
def delete_static_route(self, route_id): def delete_static_route(self, route_id):
return self.delete_request('routing-svc/static-routes/%s' % route_id) return self.delete_request('routing-svc/static-routes/%s' % route_id)
def set_ipsec_connection_state(self, tunnel, admin_up=True):
"""Set the IPSec site-to-site connection (tunnel) admin state.
Note: When a tunnel is created, it will be admin up.
"""
info = {u'vpn-interface-name': tunnel, u'enabled': admin_up}
return self.put_request('vpn-svc/site-to-site/%s/state' % tunnel, info)
def delete_ipsec_connection(self, conn_id): def delete_ipsec_connection(self, conn_id):
return self.delete_request('vpn-svc/site-to-site/%s' % conn_id) return self.delete_request('vpn-svc/site-to-site/%s' % conn_id)

View File

@ -54,6 +54,11 @@ class CsrResourceCreateFailure(exceptions.NeutronException):
message = _("Cisco CSR failed to create %(resource)s (%(which)s)") 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): class CsrDriverMismatchError(exceptions.NeutronException):
message = _("Required %(resource)s attribute %(attr)s mapping for Cisco " message = _("Required %(resource)s attribute %(attr)s mapping for Cisco "
"CSR is missing in device driver") "CSR is missing in device driver")
@ -240,36 +245,37 @@ class CiscoCsrIPsecDriver(device_drivers.DeviceDriver):
conn_id = conn_data['id'] conn_id = conn_data['id']
conn_is_admin_up = conn_data[u'admin_state_up'] 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] 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 ipsec_conn.forced_down:
if vpn_service.is_admin_up and conn_is_admin_up: if vpn_service.is_admin_up and conn_is_admin_up:
LOG.debug(_("Update: Connection %s no longer admin down"), LOG.debug(_("Update: Connection %s no longer admin down"),
conn_id) 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.forced_down = False
ipsec_conn.create_ipsec_site_connection(context, conn_data)
else: else:
if not vpn_service.is_admin_up or not conn_is_admin_up: if not vpn_service.is_admin_up or not conn_is_admin_up:
LOG.debug(_("Update: Connection %s forced to admin down"), LOG.debug(_("Update: Connection %s forced to admin down"),
conn_id) 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.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... else: # New connection...
ipsec_conn = vpn_service.create_connection(conn_data) 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: 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 " LOG.debug(_("Update: Created new connection %s in admin down "
"state"), conn_id) "state"), conn_id)
ipsec_conn.set_admin_state(is_up=False)
ipsec_conn.forced_down = True ipsec_conn.forced_down = True
else: else:
LOG.debug(_("Update: Created new connection %s"), conn_id) 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.is_dirty = False
ipsec_conn.last_status = conn_data['status'] ipsec_conn.last_status = conn_data['status']
@ -539,12 +545,33 @@ class CiscoCsrIPSecConnection(object):
"""State and actions for IPSec site-to-site connections.""" """State and actions for IPSec site-to-site connections."""
def __init__(self, conn_info, csr): def __init__(self, conn_info, csr):
self.conn_id = conn_info['id'] self.conn_info = conn_info
self.csr = csr self.csr = csr
self.steps = [] self.steps = []
self.forced_down = False self.forced_down = False
self.is_admin_up = conn_info[u'admin_state_up'] self.changed = False
self.tunnel = conn_info['cisco']['site_conn_id']
@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): def find_current_status_in(self, statuses):
if self.tunnel in statuses: if self.tunnel in statuses:
@ -683,7 +710,7 @@ class CiscoCsrIPSecConnection(object):
u'ip-address': u'GigabitEthernet3', u'ip-address': u'GigabitEthernet3',
# TODO(pcm): FUTURE - Get IP address of router's public # TODO(pcm): FUTURE - Get IP address of router's public
# I/F, once CSR is used as embedded router. # 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'tunnel-ip-address': u'%s' % gw_ip
}, },
u'remote-device': { u'remote-device': {
@ -822,3 +849,12 @@ class CiscoCsrIPSecConnection(object):
LOG.info(_("SUCCESS: Deleted IPSec site-to-site connection %s"), LOG.info(_("SUCCESS: Deleted IPSec site-to-site connection %s"),
conn_id) 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)

View File

@ -41,10 +41,6 @@ class CsrValidationFailure(exceptions.BadRequest):
"with value '%(value)s'") "with value '%(value)s'")
class CsrUnsupportedError(exceptions.NeutronException):
message = _("Cisco CSR does not currently support %(capability)s")
class CiscoCsrIPsecVpnDriverCallBack(object): class CiscoCsrIPsecVpnDriverCallBack(object):
"""Handler for agent to plugin RPC messaging.""" """Handler for agent to plugin RPC messaging."""
@ -184,9 +180,11 @@ class CiscoCsrIPsecVPNDriver(service_drivers.VpnDriver):
def update_ipsec_site_connection( def update_ipsec_site_connection(
self, context, old_ipsec_site_connection, ipsec_site_connection): self, context, old_ipsec_site_connection, ipsec_site_connection):
capability = _("update of IPSec connections. You can delete and " vpnservice = self.service_plugin._get_vpnservice(
"re-add, as a workaround.") context, ipsec_site_connection['vpnservice_id'])
raise CsrUnsupportedError(capability=capability) self.agent_rpc.vpnservice_updated(
context, vpnservice['router_id'],
reason='ipsec-conn-update')
def delete_ipsec_site_connection(self, context, ipsec_site_connection): def delete_ipsec_site_connection(self, context, ipsec_site_connection):
vpnservice = self.service_plugin._get_vpnservice( vpnservice = self.service_plugin._get_vpnservice(

View File

@ -331,6 +331,34 @@ def get_unnumbered(url, request):
return httmock.response(requests.codes.OK, content=content) return httmock.response(requests.codes.OK, content=content)
@filter_request(['get'], 'vpn-svc/site-to-site/Tunnel')
@httmock.urlmatch(netloc=r'localhost')
def get_admin_down(url, request):
if not request.headers.get('X-auth-token', None):
return {'status_code': requests.codes.UNAUTHORIZED}
# URI has .../Tunnel#/state, so get number from 2nd to last element
tunnel = url.path.split('/')[-2]
content = {u'kind': u'object#vpn-site-to-site-state',
u'vpn-interface-name': u'%s' % tunnel,
u'line-protocol-state': u'down',
u'enabled': False}
return httmock.response(requests.codes.OK, content=content)
@filter_request(['get'], 'vpn-svc/site-to-site/Tunnel')
@httmock.urlmatch(netloc=r'localhost')
def get_admin_up(url, request):
if not request.headers.get('X-auth-token', None):
return {'status_code': requests.codes.UNAUTHORIZED}
# URI has .../Tunnel#/state, so get number from 2nd to last element
tunnel = url.path.split('/')[-2]
content = {u'kind': u'object#vpn-site-to-site-state',
u'vpn-interface-name': u'%s' % tunnel,
u'line-protocol-state': u'down',
u'enabled': True}
return httmock.response(requests.codes.OK, content=content)
@filter_request(['get'], 'vpn-svc/site-to-site') @filter_request(['get'], 'vpn-svc/site-to-site')
@httmock.urlmatch(netloc=r'localhost') @httmock.urlmatch(netloc=r'localhost')
def get_mtu(url, request): def get_mtu(url, request):

View File

@ -1012,6 +1012,47 @@ class TestCsrRestIPSecConnectionCreate(base.BaseTestCase):
expected_connection.update(connection_info) expected_connection.update(connection_info)
self.assertEqual(expected_connection, content) self.assertEqual(expected_connection, content)
def test_set_ipsec_connection_admin_state_changes(self):
"""Create IPSec connection in admin down state."""
tunnel_id, ipsec_policy_id = self._prepare_for_site_conn_create()
tunnel = u'Tunnel%d' % tunnel_id
with httmock.HTTMock(csr_request.token, csr_request.post):
connection_info = {
u'vpn-interface-name': tunnel,
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.addCleanup(self._remove_resource_for_test,
self.csr.delete_ipsec_connection,
tunnel)
self.assertEqual(requests.codes.CREATED, self.csr.status)
self.assertIn('vpn-svc/site-to-site/%s' % tunnel, location)
state_uri = location + "/state"
# Note: When created, the tunnel will be in admin 'up' state
# Note: Line protocol state will be down, unless have an active conn.
expected_state = {u'kind': u'object#vpn-site-to-site-state',
u'vpn-interface-name': tunnel,
u'line-protocol-state': u'down',
u'enabled': False}
with httmock.HTTMock(csr_request.put, csr_request.get_admin_down):
self.csr.set_ipsec_connection_state(tunnel, admin_up=False)
self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
content = self.csr.get_request(state_uri, full_url=True)
self.assertEqual(requests.codes.OK, self.csr.status)
self.assertEqual(expected_state, content)
with httmock.HTTMock(csr_request.put, csr_request.get_admin_up):
self.csr.set_ipsec_connection_state(tunnel, admin_up=True)
self.assertEqual(requests.codes.NO_CONTENT, self.csr.status)
content = self.csr.get_request(state_uri, full_url=True)
self.assertEqual(requests.codes.OK, self.csr.status)
expected_state[u'enabled'] = True
self.assertEqual(expected_state, content)
def test_create_ipsec_connection_missing_ipsec_policy(self): def test_create_ipsec_connection_missing_ipsec_policy(self):
"""Negative test of connection create without IPSec policy.""" """Negative test of connection create without IPSec policy."""
tunnel_id, ipsec_policy_id = self._prepare_for_site_conn_create( tunnel_id, ipsec_policy_id = self._prepare_for_site_conn_create(

View File

@ -14,6 +14,7 @@
# #
# @author: Paul Michali, Cisco Systems, Inc. # @author: Paul Michali, Cisco Systems, Inc.
import copy
import httplib import httplib
import os import os
import tempfile import tempfile
@ -78,6 +79,7 @@ class TestCiscoCsrIPSecConnection(base.BaseTestCase):
} }
self.csr = mock.Mock(spec=csr_client.CsrRestClient) self.csr = mock.Mock(spec=csr_client.CsrRestClient)
self.csr.status = 201 # All calls to CSR REST API succeed 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.ipsec_conn = ipsec_driver.CiscoCsrIPSecConnection(self.conn_info,
self.csr) self.csr)
@ -219,8 +221,10 @@ class TestCiscoCsrIPsecConnectionCreateTransforms(base.BaseTestCase):
# TODO(pcm) get from vpnservice['external_ip'] # TODO(pcm) get from vpnservice['external_ip']
'router_public_ip': '172.24.4.23'} '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, self.ipsec_conn = ipsec_driver.CiscoCsrIPSecConnection(self.conn_info,
mock.Mock()) self.csr)
def test_invalid_attribute(self): def test_invalid_attribute(self):
"""Negative test of unknown attribute - programming error.""" """Negative test of unknown attribute - programming error."""
@ -360,7 +364,7 @@ class TestCiscoCsrIPsecConnectionCreateTransforms(base.BaseTestCase):
u'ipsec-policy-id': 333, u'ipsec-policy-id': 333,
u'local-device': { u'local-device': {
u'ip-address': u'GigabitEthernet3', 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'remote-device': {
u'tunnel-ip-address': '192.168.1.2' u'tunnel-ip-address': '192.168.1.2'
@ -418,14 +422,36 @@ class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
self.conn_delete = mock.patch.object( self.conn_delete = mock.patch.object(
ipsec_driver.CiscoCsrIPSecConnection, ipsec_driver.CiscoCsrIPSecConnection,
'delete_ipsec_site_connection').start() 'delete_ipsec_site_connection').start()
self.admin_state = mock.patch.object(
ipsec_driver.CiscoCsrIPSecConnection,
'set_admin_state').start()
self.csr = mock.Mock() self.csr = mock.Mock()
self.driver.csrs['1.1.1.1'] = self.csr self.driver.csrs['1.1.1.1'] = self.csr
self.service123_data = {u'id': u'123', self.service123_data = {u'id': u'123',
u'status': constants.DOWN, u'status': constants.DOWN,
u'admin_state_up': False, u'admin_state_up': False,
u'external_ip': u'1.1.1.1'} 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'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'}} u'cisco': {u'site_conn_id': u'Tunnel0'}}
# NOTE: For sync, there is mark (trivial), update (tested), # NOTE: For sync, there is mark (trivial), update (tested),
@ -435,9 +461,8 @@ class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
"""Notified of connection create request - create.""" """Notified of connection create request - create."""
# Make the (existing) service # Make the (existing) service
self.driver.create_vpn_service(self.service123_data) self.driver.create_vpn_service(self.service123_data)
conn_data = {u'id': u'1', u'status': constants.PENDING_CREATE, conn_data = copy.deepcopy(self.conn1_data)
u'admin_state_up': True, conn_data[u'status'] = constants.PENDING_CREATE
u'cisco': {u'site_conn_id': u'Tunnel0'}}
connection = self.driver.update_connection(self.context, connection = self.driver.update_connection(self.context,
u'123', conn_data) u'123', conn_data)
@ -446,17 +471,50 @@ class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
self.assertEqual(constants.PENDING_CREATE, connection.last_status) self.assertEqual(constants.PENDING_CREATE, connection.last_status)
self.assertEqual(1, self.conn_create.call_count) self.assertEqual(1, self.conn_create.call_count)
def test_update_ipsec_connection_changed_settings(self): def test_detect_no_change_to_ipsec_connection(self):
"""Notified of connection changing config - update.""" """No change to IPSec connection - nop."""
# TODO(pcm) Place holder for this condition # Make existing service, and connection that was active
# Make the (existing) service and connection
vpn_service = self.driver.create_vpn_service(self.service123_data) vpn_service = self.driver.create_vpn_service(self.service123_data)
# TODO(pcm) add info that indicates that the connection has changed connection = vpn_service.create_connection(self.conn1_data)
conn_data = {u'id': u'1', u'status': constants.ACTIVE,
u'admin_state_up': True, self.assertFalse(connection.check_for_changes(self.conn1_data))
u'cisco': {u'site_conn_id': u'Tunnel0'}}
vpn_service.create_connection(conn_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() 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, connection = self.driver.update_connection(self.context,
'123', conn_data) '123', conn_data)
@ -464,7 +522,37 @@ class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
self.assertEqual(u'Tunnel0', connection.tunnel) self.assertEqual(u'Tunnel0', connection.tunnel)
self.assertEqual(constants.ACTIVE, connection.last_status) self.assertEqual(constants.ACTIVE, connection.last_status)
self.assertFalse(self.conn_create.called) self.assertFalse(self.conn_create.called)
# TODO(pcm) FUTURE - handling for update (delete/create?) self.assertFalse(connection.is_admin_up)
self.assertTrue(connection.forced_down)
self.assertEqual(1, self.admin_state.call_count)
def test_update_ipsec_connection_changed_config(self):
"""Notified of connection changing config - update.
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).
"""
# 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'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.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_of_unknown_ipsec_connection(self): def test_update_of_unknown_ipsec_connection(self):
"""Notified of update of unknown connection - create. """Notified of update of unknown connection - create.
@ -472,15 +560,14 @@ class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
Occurs if agent restarts and receives a notification of change Occurs if agent restarts and receives a notification of change
to connection, but has no previous record of the connection. to connection, but has no previous record of the connection.
Result will be to rebuild 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 # Will have previously created service, but don't know of connection
self.driver.create_vpn_service(self.service123_data) self.driver.create_vpn_service(self.service123_data)
conn_data = {u'id': u'1', u'status': constants.DOWN,
u'admin_state_up': True, # Simulate that notification of connection update received
u'cisco': {u'site_conn_id': u'Tunnel0'}} self.driver.mark_existing_connections_as_dirty()
conn_data = copy.deepcopy(self.conn1_data)
conn_data[u'status'] = constants.DOWN
connection = self.driver.update_connection(self.context, connection = self.driver.update_connection(self.context,
u'123', conn_data) u'123', conn_data)
@ -488,91 +575,58 @@ class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
self.assertEqual(u'Tunnel0', connection.tunnel) self.assertEqual(u'Tunnel0', connection.tunnel)
self.assertEqual(constants.DOWN, connection.last_status) self.assertEqual(constants.DOWN, connection.last_status)
self.assertEqual(1, self.conn_create.call_count) self.assertEqual(1, self.conn_create.call_count)
self.assertTrue(connection.is_admin_up)
def test_update_unchanged_ipsec_connection(self): self.assertFalse(connection.forced_down)
"""Unchanged state for connection during sync - nop.""" self.assertFalse(self.admin_state.called)
# 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.assertFalse(self.conn_create.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)
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.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)
def test_update_missing_connection_admin_down(self): def test_update_missing_connection_admin_down(self):
"""Connection not present is in admin down state - nop. """Connection not present is in admin down state - nop.
If the agent has restarted, and a sync notification occurs with 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. but indicate that the connection is down.
""" """
# Make existing service, but no connection # Make existing service, but no connection
self.driver.create_vpn_service(self.service123_data) 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, connection = self.driver.update_connection(self.context,
u'123', conn_data) u'123', conn_data)
self.assertIsNotNone(connection) self.assertIsNotNone(connection)
self.assertFalse(connection.is_dirty) self.assertFalse(connection.is_dirty)
self.assertEqual(1, self.conn_create.call_count)
self.assertFalse(connection.is_admin_up) self.assertFalse(connection.is_admin_up)
self.assertTrue(connection.forced_down) 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): def test_update_connection_admin_up(self):
"""Connection updated to admin up state - record.""" """Connection updated to admin up state - record."""
# Make existing service, and connection that was admin down # Make existing service, and connection that was admin down
conn_data = {u'id': '1', u'status': constants.DOWN, conn_data = copy.deepcopy(self.conn1_data)
u'admin_state_up': False, conn_data.update({u'status': constants.DOWN, u'admin_state_up': False})
u'cisco': {u'site_conn_id': u'Tunnel0'}}
service_data = {u'id': u'123', service_data = {u'id': u'123',
u'status': constants.DOWN, u'status': constants.DOWN,
u'external_ip': u'1.1.1.1', u'external_ip': u'1.1.1.1',
u'admin_state_up': True, u'admin_state_up': True,
u'ipsec_conns': [conn_data]} u'ipsec_conns': [conn_data]}
self.driver.update_service(self.context, service_data) self.driver.update_service(self.context, service_data)
# Simulate that notification of connection update received
self.driver.mark_existing_connections_as_dirty() self.driver.mark_existing_connections_as_dirty()
# Now simulate that the notification shows the connection admin up # Now simulate that the notification shows the connection admin up
conn_data[u'admin_state_up'] = True new_conn_data = copy.deepcopy(conn_data)
conn_data[u'status'] = constants.DOWN new_conn_data[u'admin_state_up'] = True
connection = self.driver.update_connection(self.context, connection = self.driver.update_connection(self.context,
u'123', conn_data) u'123', new_conn_data)
self.assertFalse(connection.is_dirty) self.assertFalse(connection.is_dirty)
self.assertFalse(connection.forced_down)
self.assertEqual(u'Tunnel0', connection.tunnel) self.assertEqual(u'Tunnel0', connection.tunnel)
self.assertEqual(constants.DOWN, connection.last_status) 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): def test_update_for_vpn_service_create(self):
"""Creation of new IPSec connection on new VPN service - create. """Creation of new IPSec connection on new VPN service - create.
@ -580,9 +634,8 @@ class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
Service will be created and marked as 'clean', and update Service will be created and marked as 'clean', and update
processing for connection will occur (create). processing for connection will occur (create).
""" """
conn_data = {u'id': u'1', u'status': constants.PENDING_CREATE, conn_data = copy.deepcopy(self.conn1_data)
u'admin_state_up': True, conn_data[u'status'] = constants.PENDING_CREATE
u'cisco': {u'site_conn_id': u'Tunnel0'}}
service_data = {u'id': u'123', service_data = {u'id': u'123',
u'status': constants.PENDING_CREATE, u'status': constants.PENDING_CREATE,
u'external_ip': u'1.1.1.1', u'external_ip': u'1.1.1.1',
@ -597,15 +650,17 @@ class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
self.assertEqual(u'Tunnel0', connection.tunnel) self.assertEqual(u'Tunnel0', connection.tunnel)
self.assertEqual(constants.PENDING_CREATE, connection.last_status) self.assertEqual(constants.PENDING_CREATE, connection.last_status)
self.assertEqual(1, self.conn_create.call_count) 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): def test_update_for_new_connection_on_existing_service(self):
"""Creating a new IPSec connection on an existing service.""" """Creating a new IPSec connection on an existing service."""
# Create the service before testing, and mark it dirty # Create the service before testing, and mark it dirty
prev_vpn_service = self.driver.create_vpn_service(self.service123_data) prev_vpn_service = self.driver.create_vpn_service(self.service123_data)
self.driver.mark_existing_connections_as_dirty() self.driver.mark_existing_connections_as_dirty()
conn_data = {u'id': u'1', u'status': constants.PENDING_CREATE, conn_data = copy.deepcopy(self.conn1_data)
u'admin_state_up': True, conn_data[u'status'] = constants.PENDING_CREATE
u'cisco': {u'site_conn_id': u'Tunnel0'}}
service_data = {u'id': u'123', service_data = {u'id': u'123',
u'status': constants.ACTIVE, u'status': constants.ACTIVE,
u'external_ip': u'1.1.1.1', u'external_ip': u'1.1.1.1',
@ -631,17 +686,15 @@ class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
""" """
# Create a service and add in a connection that is active # Create a service and add in a connection that is active
prev_vpn_service = self.driver.create_vpn_service(self.service123_data) prev_vpn_service = self.driver.create_vpn_service(self.service123_data)
conn_data = {u'id': u'1', u'status': constants.ACTIVE, prev_vpn_service.create_connection(self.conn1_data)
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() self.driver.mark_existing_connections_as_dirty()
# Create notification with conn unchanged and service already created # Create notification with conn unchanged and service already created
service_data = {u'id': u'123', service_data = {u'id': u'123',
u'status': constants.ACTIVE, u'status': constants.ACTIVE,
u'external_ip': u'1.1.1.1', u'external_ip': u'1.1.1.1',
u'admin_state_up': True, 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) vpn_service = self.driver.update_service(self.context, service_data)
# Should reuse the entry and update the status # Should reuse the entry and update the status
self.assertEqual(prev_vpn_service, vpn_service) self.assertEqual(prev_vpn_service, vpn_service)
@ -661,15 +714,13 @@ class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
""" """
# Create an "existing" service, prior to notification # Create an "existing" service, prior to notification
prev_vpn_service = self.driver.create_vpn_service(self.service123_data) prev_vpn_service = self.driver.create_vpn_service(self.service123_data)
self.driver.mark_existing_connections_as_dirty() 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', service_data = {u'id': u'123',
u'status': constants.DOWN, u'status': constants.DOWN,
u'external_ip': u'1.1.1.1', u'external_ip': u'1.1.1.1',
u'admin_state_up': False, 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) vpn_service = self.driver.update_service(self.context, service_data)
self.assertEqual(prev_vpn_service, vpn_service) self.assertEqual(prev_vpn_service, vpn_service)
self.assertFalse(vpn_service.is_dirty) self.assertFalse(vpn_service.is_dirty)
@ -688,14 +739,11 @@ class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
of a service that is in the admin down state. Structures will be of a service that is in the admin down state. Structures will be
created, but forced down. 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', service_data = {u'id': u'123',
u'status': constants.DOWN, u'status': constants.DOWN,
u'external_ip': u'1.1.1.1', u'external_ip': u'1.1.1.1',
u'admin_state_up': False, 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) vpn_service = self.driver.update_service(self.context, service_data)
self.assertIsNotNone(vpn_service) self.assertIsNotNone(vpn_service)
self.assertFalse(vpn_service.is_dirty) self.assertFalse(vpn_service.is_dirty)
@ -888,7 +936,7 @@ class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
self.assertEqual(1, self.conn_delete.call_count) self.assertEqual(1, self.conn_delete.call_count)
def test_sweep_multiple_services(self): 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 # Create two services, each with a connection
vpn_service1 = self.driver.create_vpn_service(self.service123_data) vpn_service1 = self.driver.create_vpn_service(self.service123_data)
vpn_service1.create_connection(self.conn1_data) vpn_service1.create_connection(self.conn1_data)
@ -1315,9 +1363,41 @@ class TestCiscoCsrIPsecDeviceDriverSyncStatuses(base.BaseTestCase):
# Simulate one service with one connection up, one down # Simulate one service with one connection up, one down
conn1_data = {u'id': u'1', u'status': constants.ACTIVE, conn1_data = {u'id': u'1', u'status': constants.ACTIVE,
u'admin_state_up': True, 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'}} u'cisco': {u'site_conn_id': u'Tunnel1'}}
conn2_data = {u'id': u'2', u'status': constants.DOWN, conn2_data = {u'id': u'2', u'status': constants.DOWN,
u'admin_state_up': True, 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'}} u'cisco': {u'site_conn_id': u'Tunnel2'}}
service_data = {u'id': u'123', service_data = {u'id': u'123',
u'status': constants.ACTIVE, u'status': constants.ACTIVE,

View File

@ -309,12 +309,12 @@ class TestCiscoIPsecDriver(base.BaseTestCase):
mock.patch.object(csr_db, 'create_tunnel_mapping').start() mock.patch.object(csr_db, 'create_tunnel_mapping').start()
self.context = n_ctx.Context('some_user', 'some_tenant') self.context = n_ctx.Context('some_user', 'some_tenant')
def _test_update(self, func, args, reason=None): def _test_update(self, func, args, additional_info=None):
with mock.patch.object(self.driver.agent_rpc, 'cast') as cast: with mock.patch.object(self.driver.agent_rpc, 'cast') as cast:
func(self.context, *args) func(self.context, *args)
cast.assert_called_once_with( cast.assert_called_once_with(
self.context, self.context,
{'args': reason, {'args': additional_info,
'namespace': None, 'namespace': None,
'method': 'vpnservice_updated'}, 'method': 'vpnservice_updated'},
version='1.0', version='1.0',
@ -345,11 +345,9 @@ class TestCiscoIPsecDriver(base.BaseTestCase):
constants.ERROR) constants.ERROR)
def test_update_ipsec_site_connection(self): def test_update_ipsec_site_connection(self):
# TODO(pcm) FUTURE - Update test, when supported self._test_update(self.driver.update_ipsec_site_connection,
self.assertRaises(ipsec_driver.CsrUnsupportedError, [FAKE_VPN_CONNECTION, FAKE_VPN_CONNECTION],
self._test_update, {'reason': 'ipsec-conn-update'})
self.driver.update_ipsec_site_connection,
[FAKE_VPN_CONNECTION, FAKE_VPN_CONNECTION])
def test_delete_ipsec_site_connection(self): def test_delete_ipsec_site_connection(self):
self._test_update(self.driver.delete_ipsec_site_connection, self._test_update(self.driver.delete_ipsec_site_connection,