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:
parent
f3873fa8c5
commit
7d55d15848
@ -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)
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user