Add delete operations for the ODL MechanismDriver

This commit adds delete operations (networks, subnets and ports) for the ODL MechanismDriver.
It also modifies sync_single_resource to reduce db operations.

Change-Id: I03ca04c83ac2ef9c879fbd87e74bae495daea16d
Closes-Bug: #1324450
Partial-Bug: #1325184
This commit is contained in:
Cédric Ollivier 2014-05-29 12:01:28 +02:00
parent f3873fa8c5
commit 7d55d15848
2 changed files with 117 additions and 42 deletions

View File

@ -39,10 +39,6 @@ ODL_SUBNETS = 'subnets'
ODL_PORT = 'port' ODL_PORT = 'port'
ODL_PORTS = 'ports' ODL_PORTS = 'ports'
not_found_exception_map = {ODL_NETWORKS: n_exc.NetworkNotFound,
ODL_SUBNETS: n_exc.SubnetNotFound,
ODL_PORTS: n_exc.PortNotFound}
odl_opts = [ odl_opts = [
cfg.StrOpt('url', cfg.StrOpt('url',
help=_("HTTP URL of OpenDaylight REST interface.")), help=_("HTTP URL of OpenDaylight REST interface.")),
@ -183,7 +179,7 @@ class OpenDaylightMechanismDriver(api.MechanismDriver):
if self.out_of_sync: if self.out_of_sync:
self.sync_full(context) self.sync_full(context)
else: else:
self.sync_object(operation, object_type, context) self.sync_single_resource(operation, object_type, context)
def filter_create_network_attributes(self, network, context, dbcontext): def filter_create_network_attributes(self, network, context, dbcontext):
"""Filter out network attributes not required for a create.""" """Filter out network attributes not required for a create."""
@ -216,7 +212,7 @@ class OpenDaylightMechanismDriver(api.MechanismDriver):
self.sendjson('get', urlpath, None) self.sendjson('get', urlpath, None)
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
with excutils.save_and_reraise_exception() as ctx: with excutils.save_and_reraise_exception() as ctx:
if e.response.status_code == 404: if e.response.status_code == requests.codes.not_found:
attr_filter(resource, context, dbcontext) attr_filter(resource, context, dbcontext)
to_be_synced.append(resource) to_be_synced.append(resource)
ctx.reraise = False ctx.reraise = False
@ -224,14 +220,15 @@ class OpenDaylightMechanismDriver(api.MechanismDriver):
key = resource_name if len(to_be_synced) == 1 else collection_name key = resource_name if len(to_be_synced) == 1 else collection_name
# 400 errors are returned if an object exists, which we ignore. # 400 errors are returned if an object exists, which we ignore.
self.sendjson('post', collection_name, {key: to_be_synced}, [400]) self.sendjson('post', collection_name, {key: to_be_synced},
[requests.codes.bad_request])
@utils.synchronized('odl-sync-full') @utils.synchronized('odl-sync-full')
def sync_full(self, context): def sync_full(self, context):
"""Resync the entire database to ODL. """Resync the entire database to ODL.
Transition to the in-sync state on success. Transition to the in-sync state on success.
Note: we only allow a single thead in here at a time. Note: we only allow a single thread in here at a time.
""" """
if not self.out_of_sync: if not self.out_of_sync:
return return
@ -274,50 +271,35 @@ class OpenDaylightMechanismDriver(api.MechanismDriver):
ODL_SUBNETS: filter_update_subnet_attributes, ODL_SUBNETS: filter_update_subnet_attributes,
ODL_PORTS: filter_update_port_attributes} ODL_PORTS: filter_update_port_attributes}
def sync_single_resource(self, operation, object_type, obj_id, def sync_single_resource(self, operation, object_type, context):
context, attr_filter_create, attr_filter_update):
"""Sync over a single resource from Neutron to OpenDaylight. """Sync over a single resource from Neutron to OpenDaylight.
Handle syncing a single operation over to OpenDaylight, and correctly Handle syncing a single operation over to OpenDaylight, and correctly
filter attributes out which are not required for the requisite filter attributes out which are not required for the requisite
operation (create or update) being handled. operation (create or update) being handled.
""" """
dbcontext = context._plugin_context try:
obj_id = context.current['id']
if operation == 'delete':
self.sendjson('delete', object_type + '/' + obj_id, None)
else:
if operation == 'create': if operation == 'create':
urlpath = object_type urlpath = object_type
method = 'post' method = 'post'
else: attr_filter = self.create_object_map[object_type]
elif operation == 'update':
urlpath = object_type + '/' + obj_id urlpath = object_type + '/' + obj_id
method = 'put' method = 'put'
attr_filter = self.update_object_map[object_type]
try: resource = context.current.copy()
obj_getter = getattr(context._plugin, 'get_%s' % object_type[:-1]) attr_filter(self, resource, context, context._plugin_context)
resource = obj_getter(dbcontext, obj_id)
except not_found_exception_map[object_type]:
LOG.debug(_('%(object_type)s not found (%(obj_id)s)'),
{'object_type': object_type.capitalize(),
'obj_id': obj_id})
else:
if operation == 'create':
attr_filter_create(self, resource, context, dbcontext)
elif operation == 'update':
attr_filter_update(self, resource, context, dbcontext)
try:
# 400 errors are returned if an object exists, which we ignore. # 400 errors are returned if an object exists, which we ignore.
self.sendjson(method, urlpath, {object_type[:-1]: resource}, self.sendjson(method, urlpath, {object_type[:-1]: resource},
[400]) [requests.codes.bad_request])
except Exception: except Exception:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
self.out_of_sync = True self.out_of_sync = True
def sync_object(self, operation, object_type, context):
"""Synchronize the single modified record to ODL."""
obj_id = context.current['id']
self.sync_single_resource(operation, object_type, obj_id, context,
self.create_object_map[object_type],
self.update_object_map[object_type])
def add_security_groups(self, context, dbcontext, port): def add_security_groups(self, context, dbcontext, port):
"""Populate the 'security_groups' field with entire records.""" """Populate the 'security_groups' field with entire records."""
groups = [context._plugin.get_security_group(dbcontext, sg) groups = [context._plugin.get_security_group(dbcontext, sg)

View File

@ -14,6 +14,9 @@
# under the License. # under the License.
# @author: Kyle Mestery, Cisco Systems, Inc. # @author: Kyle Mestery, Cisco Systems, Inc.
import mock
import requests
from neutron.plugins.common import constants from neutron.plugins.common import constants
from neutron.plugins.ml2 import config as config from neutron.plugins.ml2 import config as config
from neutron.plugins.ml2 import driver_api as api from neutron.plugins.ml2 import driver_api as api
@ -115,3 +118,93 @@ class OpenDaylightMechanismTestSubnetsV2(test_plugin.TestSubnetsV2,
class OpenDaylightMechanismTestPortsV2(test_plugin.TestPortsV2, class OpenDaylightMechanismTestPortsV2(test_plugin.TestPortsV2,
OpenDaylightTestCase): OpenDaylightTestCase):
pass pass
class AuthMatcher(object):
def __eq__(self, obj):
return (obj.username == config.cfg.CONF.ml2_odl.username and
obj.password == config.cfg.CONF.ml2_odl.password)
class OpenDaylightMechanismDriverTestCase(base.BaseTestCase):
def setUp(self):
super(OpenDaylightMechanismDriverTestCase, self).setUp()
config.cfg.CONF.set_override('mechanism_drivers',
['logger', 'opendaylight'], 'ml2')
config.cfg.CONF.set_override('url', 'http://127.0.0.1:9999', 'ml2_odl')
config.cfg.CONF.set_override('username', 'someuser', 'ml2_odl')
config.cfg.CONF.set_override('password', 'somepass', 'ml2_odl')
self.mech = mechanism_odl.OpenDaylightMechanismDriver()
self.mech.initialize()
@staticmethod
def _get_mock_delete_resource_context():
current = {'id': '00000000-1111-2222-3333-444444444444'}
context = mock.Mock(current=current)
return context
_status_code_msgs = {
204: '',
401: '401 Client Error: Unauthorized',
403: '403 Client Error: Forbidden',
404: '404 Client Error: Not Found',
409: '409 Client Error: Conflict',
501: '501 Server Error: Not Implemented'
}
@classmethod
def _get_mock_request_response(cls, status_code):
response = mock.Mock(status_code=status_code)
response.raise_for_status = mock.Mock() if status_code < 400 else (
mock.Mock(side_effect=requests.exceptions.HTTPError(
cls._status_code_msgs[status_code])))
return response
def _test_delete_resource_postcommit(self, object_type, status_code,
exc_class=None):
self.mech.out_of_sync = False
method = getattr(self.mech, 'delete_%s_postcommit' % object_type)
context = self._get_mock_delete_resource_context()
request_response = self._get_mock_request_response(status_code)
with mock.patch('requests.request',
return_value=request_response) as mock_method:
if exc_class is not None:
self.assertRaises(exc_class, method, context)
else:
method(context)
url = '%s/%ss/%s' % (config.cfg.CONF.ml2_odl.url, object_type,
context.current['id'])
mock_method.assert_called_once_with(
'delete', url=url, headers={'Content-Type': 'application/json'},
data=None, auth=AuthMatcher(),
timeout=config.cfg.CONF.ml2_odl.timeout)
def test_delete_network_postcommit(self):
self._test_delete_resource_postcommit('network',
requests.codes.no_content)
for status_code in (requests.codes.unauthorized,
requests.codes.not_found,
requests.codes.conflict):
self._test_delete_resource_postcommit(
'network', status_code, requests.exceptions.HTTPError)
def test_delete_subnet_postcommit(self):
self._test_delete_resource_postcommit('subnet',
requests.codes.no_content)
for status_code in (requests.codes.unauthorized,
requests.codes.not_found,
requests.codes.conflict,
requests.codes.not_implemented):
self._test_delete_resource_postcommit(
'subnet', status_code, requests.exceptions.HTTPError)
def test_delete_port_postcommit(self):
self._test_delete_resource_postcommit('port',
requests.codes.no_content)
for status_code in (requests.codes.unauthorized,
requests.codes.forbidden,
requests.codes.not_found,
requests.codes.not_implemented):
self._test_delete_resource_postcommit(
'port', status_code, requests.exceptions.HTTPError)