diff options
Diffstat (limited to 'networking-odl/networking_odl/tests')
37 files changed, 5204 insertions, 0 deletions
diff --git a/networking-odl/networking_odl/tests/__init__.py b/networking-odl/networking_odl/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/networking-odl/networking_odl/tests/__init__.py diff --git a/networking-odl/networking_odl/tests/base.py b/networking-odl/networking_odl/tests/base.py new file mode 100644 index 0000000..d28be71 --- /dev/null +++ b/networking-odl/networking_odl/tests/base.py @@ -0,0 +1,28 @@ +# Copyright (c) 2015-2016 OpenStack Foundation +# 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. + + +import mock + +from neutron.tests import base + + +class DietTestCase(base.DietTestCase): + + def patch(self, target, name, *args, **kwargs): + context = mock.patch.object(target, name, *args, **kwargs) + patch = context.start() + self.addCleanup(context.stop) + return patch diff --git a/networking-odl/networking_odl/tests/unit/__init__.py b/networking-odl/networking_odl/tests/unit/__init__.py new file mode 100644 index 0000000..faed26a --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2011 OpenStack Foundation. +# 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. + +from oslo_config import cfg + + +cfg.CONF.use_stderr = False diff --git a/networking-odl/networking_odl/tests/unit/common/__init__.py b/networking-odl/networking_odl/tests/unit/common/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/common/__init__.py diff --git a/networking-odl/networking_odl/tests/unit/common/test_cache.py b/networking-odl/networking_odl/tests/unit/common/test_cache.py new file mode 100644 index 0000000..b702455 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/common/test_cache.py @@ -0,0 +1,242 @@ +# Copyright (c) 2015 OpenStack Foundation +# 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. + +import mock + +from neutron.tests import base + +from networking_odl.common import cache + + +class TestCache(base.DietTestCase): + + def test_init_with_callable(self): + + def given_fetch_method(): + pass + + cache.Cache(given_fetch_method) + + def test_init_without_callable(self): + self.assertRaises(TypeError, lambda: cache.Cache(object())) + + def test_fecth_once(self): + value = 'value' + + given_fetch_method = mock.Mock(return_value=iter([('key', value)])) + given_cache = cache.Cache(given_fetch_method) + + # When value with key is fetched + result = given_cache.fetch('key', 60.0) + + # Result is returned + self.assertIs(value, result) + + # Then fetch method is called once + given_fetch_method.assert_called_once_with(('key',)) + + def test_fecth_with_no_result(self): + given_fetch_method = mock.Mock(return_value=iter([])) + given_cache = cache.Cache(given_fetch_method) + + # When value with key is fetched + try: + given_cache.fetch('key', 60.0) + except cache.CacheFetchError as error: + given_fetch_method.assert_called_once_with(('key',)) + self.assertRaises(KeyError, error.reraise_cause) + else: + self.fail('Expecting CacheFetchError to be raised.') + + @mock.patch.object(cache, 'LOG') + def test_fecth_with_failure(self, logger): + # pylint: disable=unused-argument + + given_error = RuntimeError("It doesn't work like this!") + + def failing_function(keys): + raise given_error + + given_fetch_method = mock.Mock(side_effect=failing_function) + given_cache = cache.Cache(given_fetch_method) + + # When value with key is fetched + try: + given_cache.fetch('key', 60.0) + except cache.CacheFetchError as error: + given_fetch_method.assert_called_once_with(('key',)) + self.assertRaises(RuntimeError, error.reraise_cause) + else: + self.fail('Expecting CacheFetchError to be raised.') + logger.warning.assert_called_once_with( + 'Error fetching values for keys: %r', "'key'", + exc_info=(type(given_error), given_error, mock.ANY)) + + def test_fecth_again_after_clear(self): + value1 = 'value1' + value2 = 'value2' + given_fetch_method = mock.Mock( + side_effect=[iter([('key', value1)]), + iter([('key', value2)])]) + given_cache = cache.Cache(given_fetch_method) + + # When value with key is fetched + result1 = given_cache.fetch('key', 60.0) + + # When cache is cleared + given_cache.clear() + + # When value with same key is fetched again + result2 = given_cache.fetch('key', 0.0) + + # Then first result is returned + self.assertIs(value1, result1) + + # Then fetch method is called twice + self.assertEqual( + [mock.call(('key',)), mock.call(('key',))], + given_fetch_method.mock_calls) + + # Then second result is returned + self.assertIs(value2, result2) + + def test_fecth_again_before_timeout(self): + value1 = 'value1' + value2 = 'value2' + given_fetch_method = mock.Mock( + side_effect=[iter([('key', value1)]), + iter([('key', value2)])]) + given_cache = cache.Cache(given_fetch_method) + + # When value with key is fetched + result1 = given_cache.fetch('key', 1.0) + + # When value with same key is fetched again and cached entry is not + # expired + result2 = given_cache.fetch('key', 0.0) + + # First result is returned + self.assertIs(value1, result1) + + # Then fetch method is called once + given_fetch_method.assert_called_once_with(('key',)) + + # Then first result is returned twice + self.assertIs(value1, result2) + + def test_fecth_again_after_timeout(self): + value1 = 'value1' + value2 = 'value2' + given_fetch_method = mock.Mock( + side_effect=[iter([('key', value1)]), + iter([('key', value2)])]) + given_cache = cache.Cache(given_fetch_method) + + # When value with key is fetched + result1 = given_cache.fetch('key', 0.0) + + # When value with same key is fetched again and cached entry is + # expired + result2 = given_cache.fetch('key', 0.0) + + # Then first result is returned + self.assertIs(value1, result1) + + # Then fetch method is called twice + self.assertEqual( + [mock.call(('key',)), mock.call(('key',))], + given_fetch_method.mock_calls) + + # Then second result is returned + self.assertIs(value2, result2) + + def test_fecth_two_values_yielding_both_before_timeout(self): + value1 = 'value1' + value2 = 'value2' + given_fetch_method = mock.Mock( + return_value=iter([('key1', value1), + ('key2', value2)])) + given_cache = cache.Cache(given_fetch_method) + + # When value with key is fetched + result1 = given_cache.fetch('key1', 60.0) + + # When value with another key is fetched and cached entry is not + # expired + result2 = given_cache.fetch('key2', 60.0) + + # Then first result is returned + self.assertIs(value1, result1) + + # Then fetch method is called once + given_fetch_method.assert_called_once_with(('key1',)) + + # Then second result is returned + self.assertIs(value2, result2) + + def test_fecth_two_values_yielding_both_after_timeout(self): + value1 = 'value1' + value2 = 'value2' + given_fetch_method = mock.Mock( + return_value=[('key1', value1), ('key2', value2)]) + given_cache = cache.Cache(given_fetch_method) + + # When value with key is fetched + result1 = given_cache.fetch('key1', 0.0) + + # When value with another key is fetched and cached entry is + # expired + result2 = given_cache.fetch('key2', 0.0) + + # Then first result is returned + self.assertIs(value1, result1) + + # Then fetch method is called twice + self.assertEqual( + [mock.call(('key1',)), mock.call(('key2',))], + given_fetch_method.mock_calls) + + # Then second result is returned + self.assertIs(value2, result2) + + def test_fecth_all_with_multiple_entries(self): + given_fetch_method = mock.Mock( + return_value=iter([('key', 'value1'), + ('key', 'value2')])) + given_cache = cache.Cache(given_fetch_method) + + # When value with key is fetched + results = list(given_cache.fetch_all(['key'], 0.0)) + + # Then fetch method is once + given_fetch_method.assert_called_once_with(('key',)) + + # Then both results are yield in the right order + self.assertEqual([('key', 'value1'), ('key', 'value2')], results) + + def test_fecth_all_with_repeated_entries(self): + entry = ('key', 'value') + given_fetch_method = mock.Mock( + return_value=iter([entry, entry, entry])) + given_cache = cache.Cache(given_fetch_method) + + # When value with key is fetched + results = list(given_cache.fetch_all(['key'], 0.0)) + + # Then fetch method is once + given_fetch_method.assert_called_once_with(('key',)) + + # Then results are yield in the right order + self.assertEqual([entry, entry, entry], results) diff --git a/networking-odl/networking_odl/tests/unit/common/test_callback.py b/networking-odl/networking_odl/tests/unit/common/test_callback.py new file mode 100644 index 0000000..f5e2ee6 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/common/test_callback.py @@ -0,0 +1,83 @@ +# Copyright (c) 2013-2014 OpenStack Foundation +# 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. + +from networking_odl.common import callback +from networking_odl.common import constants as odl_const +from networking_odl.ml2.mech_driver import OpenDaylightDriver + +import mock +import testtools + +from neutron.callbacks import events +from neutron.callbacks import resources + + +FAKE_ID = 'fakeid' + + +class ODLCallbackTestCase(testtools.TestCase): + odl_driver = OpenDaylightDriver() + sgh = callback.OdlSecurityGroupsHandler(odl_driver) + + def setUp(self): + super(ODLCallbackTestCase, self).setUp() + + @mock.patch.object(OpenDaylightDriver, 'sync_from_callback') + def _test_callback_for_sg(self, event, op, sg, sg_id, sfc): + self.sgh.sg_callback(resources.SECURITY_GROUP, + event, + None, + security_group=sg, + security_group_id=sg_id) + + expected_dict = ({resources.SECURITY_GROUP: sg} + if sg is not None else None) + sfc.assert_called_with( + op, callback._RESOURCE_MAPPING[resources.SECURITY_GROUP], sg_id, + expected_dict) + + def test_callback_sg_create(self): + self._test_callback_for_sg(events.AFTER_CREATE, odl_const.ODL_CREATE, + mock.Mock(), None) + + def test_callback_sg_update(self): + self._test_callback_for_sg(events.AFTER_UPDATE, odl_const.ODL_UPDATE, + mock.Mock(), FAKE_ID) + + def test_callback_sg_delete(self): + self._test_callback_for_sg(events.AFTER_DELETE, odl_const.ODL_DELETE, + None, FAKE_ID) + + @mock.patch.object(OpenDaylightDriver, 'sync_from_callback') + def _test_callback_for_sg_rules(self, event, op, sg_rule, sg_rule_id, sfc): + self.sgh.sg_callback(resources.SECURITY_GROUP_RULE, + event, + None, + security_group_rule=sg_rule, + security_group_rule_id=sg_rule_id) + + expected_dict = ({resources.SECURITY_GROUP_RULE: sg_rule} + if sg_rule is not None else None) + sfc.assert_called_with( + op, callback._RESOURCE_MAPPING[resources.SECURITY_GROUP_RULE], + sg_rule_id, expected_dict) + + def test_callback_sg_rules_create(self): + self._test_callback_for_sg_rules( + events.AFTER_CREATE, odl_const.ODL_CREATE, mock.Mock(), None) + + def test_callback_sg_rules_delete(self): + self._test_callback_for_sg_rules( + events.AFTER_DELETE, odl_const.ODL_DELETE, None, FAKE_ID) diff --git a/networking-odl/networking_odl/tests/unit/common/test_lightweight_testing.py b/networking-odl/networking_odl/tests/unit/common/test_lightweight_testing.py new file mode 100644 index 0000000..ea3b5a8 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/common/test_lightweight_testing.py @@ -0,0 +1,174 @@ +# Copyright (c) 2015 Intel 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. + +import mock + +from networking_odl.common import lightweight_testing as lwt + +from neutron.tests import base + + +class LightweightTestingTestCase(base.DietTestCase): + + def test_create_client_with_lwt_enabled(self): + """Have to do the importation here, otherwise there will be a loop""" + from networking_odl.common import client as odl_client + odl_client.cfg.CONF.set_override('enable_lightweight_testing', + True, 'ml2_odl') + # DietTestCase does not automatically cleans configuration overrides + self.addCleanup(odl_client.cfg.CONF.reset) + + client = odl_client.OpenDaylightRestClient.create_client() + self.assertIsInstance(client, lwt.OpenDaylightLwtClient) + + def test_create_client_with_lwt_disabled(self): + """Have to do the importation here, otherwise there will be a loop""" + from networking_odl.common import client as odl_client + odl_client.cfg.CONF.set_override('enable_lightweight_testing', + False, 'ml2_odl') + # DietTestCase does not automatically cleans configuration overrides + self.addCleanup(odl_client.cfg.CONF.reset) + + client = odl_client.OpenDaylightRestClient.create_client() + self.assertIsInstance(client, odl_client.OpenDaylightRestClient) + + @mock.patch.dict(lwt.OpenDaylightLwtClient.lwt_dict, + {'networks': {}}, clear=True) + def test_post_single_resource(self): + client = lwt.OpenDaylightLwtClient.create_client() + fake_network1 = {'id': 'fakeid1', 'name': 'fake_network1'} + obj = {'networks': fake_network1} + response = client.sendjson('post', 'networks', obj) + self.assertEqual(lwt.NO_CONTENT, response.status_code) + lwt_dict = lwt.OpenDaylightLwtClient.lwt_dict + self.assertEqual(lwt_dict['networks']['fakeid1'], + fake_network1) + + @mock.patch.dict(lwt.OpenDaylightLwtClient.lwt_dict, + {'networks': {}}, clear=True) + def test_post_multiple_resources(self): + client = lwt.OpenDaylightLwtClient.create_client() + fake_network1 = {'id': 'fakeid1', 'name': 'fake_network1'} + fake_network2 = {'id': 'fakeid2', 'name': 'fake_network2'} + obj = {'networks': [fake_network1, fake_network2]} + response = client.sendjson('post', 'networks', obj) + self.assertEqual(lwt.NO_CONTENT, response.status_code) + lwt_dict = lwt.OpenDaylightLwtClient.lwt_dict + self.assertEqual(lwt_dict['networks']['fakeid1'], + fake_network1) + self.assertEqual(lwt_dict['networks']['fakeid2'], + fake_network2) + + @mock.patch.dict(lwt.OpenDaylightLwtClient.lwt_dict, + {'ports': {'fakeid1': {'id': 'fakeid1', + 'name': 'fake_port1'}}}, + clear=True) + def test_get_single_resource(self): + client = lwt.OpenDaylightLwtClient.create_client() + url_path = 'ports/fakeid1' + response = client.sendjson('get', url_path, None) + self.assertEqual(lwt.OK, response.status_code) + res = response.json() + # For single resource, the return value is a dict + self.assertEqual(res['port']['name'], 'fake_port1') + + @mock.patch.dict(lwt.OpenDaylightLwtClient.lwt_dict, + {'ports': {'fakeid1': {'id': 'fakeid1', + 'name': 'fake_port1'}, + 'fakeid2': {'id': 'fakeid2', + 'name': 'fake_port2'}}}, + clear=True) + def test_get_multiple_resources(self): + client = lwt.OpenDaylightLwtClient.create_client() + url_path = 'ports/' + response = client.sendjson('get', url_path, None) + self.assertEqual(lwt.OK, response.status_code) + res = response.json() + for port in res: + self.assertIn(port['port']['name'], + ['fake_port1', 'fake_port2']) + + @mock.patch.dict(lwt.OpenDaylightLwtClient.lwt_dict, + {'subnets': {'fakeid1': {'id': 'fakeid1', + 'name': 'fake_subnet1'}}}, + clear=True) + def test_put_single_resource(self): + client = lwt.OpenDaylightLwtClient.create_client() + changed = {'id': 'fakeid1', 'name': 'fake_subnet1_changed'} + obj = {'subnets': changed} + + url_path = 'subnets/fakeid1' + response = client.sendjson('put', url_path, obj) + self.assertEqual(lwt.NO_CONTENT, response.status_code) + lwt_dict = lwt.OpenDaylightLwtClient.lwt_dict + self.assertEqual('fake_subnet1_changed', + lwt_dict['subnets']['fakeid1']['name']) + + """Check the client does not change the parameter""" + self.assertEqual('fakeid1', changed['id']) + self.assertEqual('fake_subnet1_changed', changed['name']) + + @mock.patch.dict(lwt.OpenDaylightLwtClient.lwt_dict, + {'subnets': {'fakeid1': {'id': 'fakeid1', + 'name': 'fake_subnet1'}, + 'fakeid2': {'id': 'fakeid2', + 'name': 'fake_subnet2'}}}, + clear=True) + def test_put_multiple_resources(self): + client = lwt.OpenDaylightLwtClient.create_client() + changed1 = {'id': 'fakeid1', 'name': 'fake_subnet1_changed'} + changed2 = {'id': 'fakeid2', 'name': 'fake_subnet2_changed'} + obj = {'subnets': [changed1, changed2]} + + url_path = 'subnets/' + response = client.sendjson('put', url_path, obj) + self.assertEqual(lwt.NO_CONTENT, response.status_code) + lwt_dict = lwt.OpenDaylightLwtClient.lwt_dict + self.assertEqual('fake_subnet1_changed', + lwt_dict['subnets']['fakeid1']['name']) + self.assertEqual('fake_subnet2_changed', + lwt_dict['subnets']['fakeid2']['name']) + + @mock.patch.dict(lwt.OpenDaylightLwtClient.lwt_dict, + {'networks': {'fakeid1': {'id': 'fakeid1', + 'name': 'fake_network1'}}}, + clear=True) + def test_delete_single_resource(self): + client = lwt.OpenDaylightLwtClient.create_client() + url_path = 'networks/fakeid1' + response = client.sendjson('delete', url_path, None) + self.assertEqual(lwt.NO_CONTENT, response.status_code) + lwt_dict = lwt.OpenDaylightLwtClient.lwt_dict + network = lwt_dict['networks'].get('fakeid1') + self.assertIsNone(network) + + @mock.patch.dict(lwt.OpenDaylightLwtClient.lwt_dict, + {'networks': {'fakeid1': {'id': 'fakeid1', + 'name': 'fake_network1'}, + 'fakeid2': {'id': 'fakeid2', + 'name': 'fake_network2'}}}, + clear=True) + def test_delete_multiple_resources(self): + client = lwt.OpenDaylightLwtClient.create_client() + network1 = {'id': 'fakeid1'} + network2 = {'id': 'fakeid2'} + obj = {'networks': [network1, network2]} + response = client.sendjson('delete', 'networks/', obj) + self.assertEqual(lwt.NO_CONTENT, response.status_code) + lwt_dict = lwt.OpenDaylightLwtClient.lwt_dict + network = lwt_dict['networks'].get('fakeid1') + self.assertIsNone(network) + network = lwt_dict['networks'].get('fakeid2') + self.assertIsNone(network) diff --git a/networking-odl/networking_odl/tests/unit/common/test_utils.py b/networking-odl/networking_odl/tests/unit/common/test_utils.py new file mode 100644 index 0000000..dcfb50e --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/common/test_utils.py @@ -0,0 +1,156 @@ +# Copyright (c) 2015 OpenStack Foundation +# 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. + +import mock + +from neutron.tests import base + +from networking_odl.common import cache +from networking_odl.common import utils + + +class TestGetAddressesByName(base.DietTestCase): + + # pylint: disable=protected-access, unused-argument + + def setUp(self): + super(TestGetAddressesByName, self).setUp() + self.clear_cache() + self.addCleanup(self.clear_cache) + time = self.patch( + utils.cache, 'time', clock=mock.Mock(return_value=0.0)) + self.clock = time.clock + socket = self.patch(utils, 'socket') + self.getaddrinfo = socket.getaddrinfo + + def patch(self, target, name, *args, **kwargs): + context = mock.patch.object(target, name, *args, **kwargs) + mocked = context.start() + self.addCleanup(context.stop) + return mocked + + def clear_cache(self): + utils._addresses_by_name_cache.clear() + + def test_get_addresses_by_valid_name(self): + self.getaddrinfo.return_value = [ + (2, 1, 6, '', ('127.0.0.1', 0)), + (2, 2, 17, '', ('127.0.0.1', 0)), + (2, 3, 0, '', ('127.0.0.1', 0)), + (2, 1, 6, '', ('10.237.214.247', 0)), + (2, 2, 17, '', ('10.237.214.247', 0)), + (2, 3, 0, '', ('10.237.214.247', 0))] + + # When valid host name is requested + result = utils.get_addresses_by_name('some_host_name') + + # Then correct addresses are returned + self.assertEqual(('127.0.0.1', '10.237.214.247'), result) + + # Then fetched addresses are cached + self.assertEqual(result, utils.get_addresses_by_name('some_host_name')) + + # Then addresses are fetched only once + self.getaddrinfo.assert_called_once_with('some_host_name', None) + + def test_get_addresses_by_valid_name_when_cache_expires(self): + self.getaddrinfo.return_value = [ + (2, 1, 6, '', ('127.0.0.1', 0)), + (2, 2, 17, '', ('127.0.0.1', 0)), + (2, 3, 0, '', ('127.0.0.1', 0)), + (2, 1, 6, '', ('10.237.214.247', 0)), + (2, 2, 17, '', ('10.237.214.247', 0)), + (2, 3, 0, '', ('10.237.214.247', 0))] + + # When valid host name is requested + result1 = utils.get_addresses_by_name('some_host_name') + + # and after a long time + self.clock.return_value = 1.0e6 + + # When valid host name is requested + result2 = utils.get_addresses_by_name('some_host_name') + + # Then correct addresses are returned + self.assertEqual(('127.0.0.1', '10.237.214.247'), result1) + self.assertEqual(('127.0.0.1', '10.237.214.247'), result2) + + # Then addresses are fetched twice + self.getaddrinfo.assert_has_calls( + [mock.call('some_host_name', None), + mock.call('some_host_name', None)]) + + @mock.patch.object(cache, 'LOG') + def test_get_addresses_by_invalid_name(self, cache_logger): + + # Given addresses resolution is failing + given_error = RuntimeError("I don't know him!") + + def failing_getaddrinfo(name, service): + raise given_error + + self.getaddrinfo.side_effect = failing_getaddrinfo + + # When invalid name is requested + self.assertRaises( + RuntimeError, utils.get_addresses_by_name, 'some_host_name') + + # When invalid name is requested again + self.assertRaises( + RuntimeError, utils.get_addresses_by_name, 'some_host_name') + + # Then result is fetched only once + self.getaddrinfo.assert_has_calls( + [mock.call('some_host_name', None)]) + cache_logger.warning.assert_has_calls( + [mock.call( + 'Error fetching values for keys: %r', "'some_host_name'", + exc_info=(RuntimeError, given_error, mock.ANY)), + mock.call( + 'Error fetching values for keys: %r', "'some_host_name'", + exc_info=(RuntimeError, given_error, mock.ANY))]) + + @mock.patch.object(cache, 'LOG') + def test_get_addresses_failing_when_expired_in_cache(self, cache_logger): + self.getaddrinfo.return_value = [ + (2, 1, 6, '', ('127.0.0.1', 0)), + (2, 2, 17, '', ('127.0.0.1', 0)), + (2, 3, 0, '', ('127.0.0.1', 0)), + (2, 1, 6, '', ('10.237.214.247', 0)), + (2, 2, 17, '', ('10.237.214.247', 0)), + (2, 3, 0, '', ('10.237.214.247', 0))] + + # Given valid result is in chache but expired + utils.get_addresses_by_name('some_host_name') + self.clock.return_value = 1.0e6 + + # Given addresses resolution is now failing + given_error = RuntimeError("This is top secret.") + + def failing_getaddrinfo(name, service): + raise given_error + + self.getaddrinfo.side_effect = failing_getaddrinfo + + self.assertRaises( + RuntimeError, utils.get_addresses_by_name, 'some_host_name') + + # Then result is fetched more times + self.getaddrinfo.assert_has_calls( + [mock.call('some_host_name', None), + mock.call('some_host_name', None)]) + cache_logger.warning.assert_called_once_with( + 'Error fetching values for keys: %r', "'some_host_name'", + exc_info=(RuntimeError, given_error, mock.ANY)) diff --git a/networking-odl/networking_odl/tests/unit/db/__init__.py b/networking-odl/networking_odl/tests/unit/db/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/db/__init__.py diff --git a/networking-odl/networking_odl/tests/unit/db/test_db.py b/networking-odl/networking_odl/tests/unit/db/test_db.py new file mode 100644 index 0000000..72749ad --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/db/test_db.py @@ -0,0 +1,243 @@ +# +# Copyright (C) 2016 Red Hat, Inc. +# +# 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. +# + +import mock + +from datetime import datetime +from datetime import timedelta + +from networking_odl.common import constants as odl_const +from networking_odl.db import db +from networking_odl.db import models + +from neutron.db import api as neutron_db_api +from neutron.tests.unit.testlib_api import SqlTestCaseLight +from oslo_db.exception import DBDeadlock +from unittest2.case import TestCase + + +class DbTestCase(SqlTestCaseLight, TestCase): + + UPDATE_ROW = [odl_const.ODL_NETWORK, 'id', odl_const.ODL_UPDATE, + {'test': 'data'}] + + def setUp(self): + super(DbTestCase, self).setUp() + self.db_session = neutron_db_api.get_session() + self.addCleanup(self._db_cleanup) + + def _db_cleanup(self): + self.db_session.query(models.OpendaylightJournal).delete() + self.db_session.query(models.OpendaylightMaintenance).delete() + + def _update_row(self, row): + self.db_session.merge(row) + self.db_session.flush() + + def _test_validate_updates(self, rows, time_deltas, expected_validations): + for row in rows: + db.create_pending_row(self.db_session, *row) + + # update row created_at + rows = db.get_all_db_rows(self.db_session) + now = datetime.now() + for row, time_delta in zip(rows, time_deltas): + row.created_at = now - timedelta(hours=time_delta) + self._update_row(row) + + # validate if there are older rows + for row, expected_valid in zip(rows, expected_validations): + valid = not db.check_for_older_ops(self.db_session, row) + self.assertEqual(expected_valid, valid) + + def _test_retry_count(self, retry_num, max_retry, + expected_retry_count, expected_state): + # add new pending row + db.create_pending_row(self.db_session, *self.UPDATE_ROW) + + # update the row with the requested retry_num + row = db.get_all_db_rows(self.db_session)[0] + row.retry_count = retry_num - 1 + db.update_pending_db_row_retry(self.db_session, row, max_retry) + + # validate the state and the retry_count of the row + row = db.get_all_db_rows(self.db_session)[0] + self.assertEqual(expected_state, row.state) + self.assertEqual(expected_retry_count, row.retry_count) + + def _test_update_row_state(self, from_state, to_state): + # add new pending row + db.create_pending_row(self.db_session, *self.UPDATE_ROW) + + row = db.get_all_db_rows(self.db_session)[0] + for state in [from_state, to_state]: + # update the row state + db.update_db_row_state(self.db_session, row, state) + + # validate the new state + row = db.get_all_db_rows(self.db_session)[0] + self.assertEqual(state, row.state) + + def test_validate_updates_same_object_uuid(self): + self._test_validate_updates( + [self.UPDATE_ROW, self.UPDATE_ROW], [1, 0], [True, False]) + + def test_validate_updates_same_created_time(self): + self._test_validate_updates( + [self.UPDATE_ROW, self.UPDATE_ROW], [0, 0], [True, True]) + + def test_validate_updates_different_object_uuid(self): + other_row = list(self.UPDATE_ROW) + other_row[1] += 'a' + self._test_validate_updates( + [self.UPDATE_ROW, other_row], [1, 0], [True, True]) + + def test_validate_updates_different_object_type(self): + other_row = list(self.UPDATE_ROW) + other_row[0] = odl_const.ODL_PORT + other_row[1] += 'a' + self._test_validate_updates( + [self.UPDATE_ROW, other_row], [1, 0], [True, True]) + + def test_get_oldest_pending_row_none_when_no_rows(self): + row = db.get_oldest_pending_db_row_with_lock(self.db_session) + self.assertIsNone(row) + + def _test_get_oldest_pending_row_none(self, state): + db.create_pending_row(self.db_session, *self.UPDATE_ROW) + row = db.get_all_db_rows(self.db_session)[0] + row.state = state + self._update_row(row) + + row = db.get_oldest_pending_db_row_with_lock(self.db_session) + self.assertIsNone(row) + + def test_get_oldest_pending_row_none_when_row_processing(self): + self._test_get_oldest_pending_row_none(odl_const.PROCESSING) + + def test_get_oldest_pending_row_none_when_row_failed(self): + self._test_get_oldest_pending_row_none(odl_const.FAILED) + + def test_get_oldest_pending_row_none_when_row_completed(self): + self._test_get_oldest_pending_row_none(odl_const.COMPLETED) + + def test_get_oldest_pending_row(self): + db.create_pending_row(self.db_session, *self.UPDATE_ROW) + row = db.get_oldest_pending_db_row_with_lock(self.db_session) + self.assertIsNotNone(row) + self.assertEqual(odl_const.PROCESSING, row.state) + + def test_get_oldest_pending_row_order(self): + db.create_pending_row(self.db_session, *self.UPDATE_ROW) + older_row = db.get_all_db_rows(self.db_session)[0] + older_row.last_retried -= timedelta(minutes=1) + self._update_row(older_row) + + db.create_pending_row(self.db_session, *self.UPDATE_ROW) + row = db.get_oldest_pending_db_row_with_lock(self.db_session) + self.assertEqual(older_row, row) + + def test_get_oldest_pending_row_when_deadlock(self): + db.create_pending_row(self.db_session, *self.UPDATE_ROW) + update_mock = mock.MagicMock(side_effect=(DBDeadlock, mock.DEFAULT)) + + # Mocking is mandatory to achieve a deadlock regardless of the DB + # backend being used when running the tests + with mock.patch.object(db, 'update_db_row_state', new=update_mock): + row = db.get_oldest_pending_db_row_with_lock(self.db_session) + self.assertIsNotNone(row) + + self.assertEqual(2, update_mock.call_count) + + def _test_delete_rows_by_state_and_time(self, last_retried, row_retention, + state, expected_rows): + db.create_pending_row(self.db_session, *self.UPDATE_ROW) + + # update state and last retried + row = db.get_all_db_rows(self.db_session)[0] + row.state = state + row.last_retried = row.last_retried - timedelta(seconds=last_retried) + self._update_row(row) + + db.delete_rows_by_state_and_time(self.db_session, + odl_const.COMPLETED, + timedelta(seconds=row_retention)) + + # validate the number of rows in the journal + rows = db.get_all_db_rows(self.db_session) + self.assertEqual(expected_rows, len(rows)) + + def test_delete_completed_rows_no_new_rows(self): + self._test_delete_rows_by_state_and_time(0, 10, odl_const.COMPLETED, 1) + + def test_delete_completed_rows_one_new_row(self): + self._test_delete_rows_by_state_and_time(6, 5, odl_const.COMPLETED, 0) + + def test_delete_completed_rows_wrong_state(self): + self._test_delete_rows_by_state_and_time(10, 8, odl_const.PENDING, 1) + + def test_valid_retry_count(self): + self._test_retry_count(1, 1, 1, odl_const.PENDING) + + def test_invalid_retry_count(self): + self._test_retry_count(2, 1, 1, odl_const.FAILED) + + def test_update_row_state_to_pending(self): + self._test_update_row_state(odl_const.PROCESSING, odl_const.PENDING) + + def test_update_row_state_to_processing(self): + self._test_update_row_state(odl_const.PENDING, odl_const.PROCESSING) + + def test_update_row_state_to_failed(self): + self._test_update_row_state(odl_const.PROCESSING, odl_const.FAILED) + + def test_update_row_state_to_completed(self): + self._test_update_row_state(odl_const.PROCESSING, odl_const.COMPLETED) + + def _test_maintenance_lock_unlock(self, db_func, existing_state, + expected_state, expected_result): + row = models.OpendaylightMaintenance(id='test', + state=existing_state) + self.db_session.add(row) + self.db_session.flush() + + self.assertEqual(expected_result, db_func(self.db_session)) + row = self.db_session.query(models.OpendaylightMaintenance).one() + self.assertEqual(expected_state, row['state']) + + def test_lock_maintenance(self): + self._test_maintenance_lock_unlock(db.lock_maintenance, + odl_const.PENDING, + odl_const.PROCESSING, + True) + + def test_lock_maintenance_fails_when_processing(self): + self._test_maintenance_lock_unlock(db.lock_maintenance, + odl_const.PROCESSING, + odl_const.PROCESSING, + False) + + def test_unlock_maintenance(self): + self._test_maintenance_lock_unlock(db.unlock_maintenance, + odl_const.PROCESSING, + odl_const.PENDING, + True) + + def test_unlock_maintenance_fails_when_pending(self): + self._test_maintenance_lock_unlock(db.unlock_maintenance, + odl_const.PENDING, + odl_const.PENDING, + False) diff --git a/networking-odl/networking_odl/tests/unit/fwaas/__init__.py b/networking-odl/networking_odl/tests/unit/fwaas/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/fwaas/__init__.py diff --git a/networking-odl/networking_odl/tests/unit/fwaas/test_fwaas_odl.py b/networking-odl/networking_odl/tests/unit/fwaas/test_fwaas_odl.py new file mode 100644 index 0000000..b50016c --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/fwaas/test_fwaas_odl.py @@ -0,0 +1,29 @@ +# 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. + +""" +test_fwaas_odl +---------------------------------- + +Tests for the L3 FWaaS plugin for networking-odl. +""" + +from networking_odl.fwaas import driver as fwaas_odl + +from neutron.tests import base + + +class TestODL_FWaaS(base.BaseTestCase): + + def test_init(self): + # just create an instance of OpenDaylightFwaasDriver + fwaas_odl.OpenDaylightFwaasDriver() diff --git a/networking-odl/networking_odl/tests/unit/journal/__init__.py b/networking-odl/networking_odl/tests/unit/journal/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/journal/__init__.py diff --git a/networking-odl/networking_odl/tests/unit/journal/test_dependency_validations.py b/networking-odl/networking_odl/tests/unit/journal/test_dependency_validations.py new file mode 100644 index 0000000..39a4b98 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/journal/test_dependency_validations.py @@ -0,0 +1,44 @@ +# +# Copyright (C) 2016 Intel Corp. Isaku Yamahata <isaku.yamahata@gmail com> +# 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. +# + +import mock + +from neutron.tests import base + +from networking_odl.journal import dependency_validations + + +class DependencyValidationsTestCase(base.DietTestCase): + _RESOURCE_DUMMY = 'test_type' + + def setUp(self): + super(DependencyValidationsTestCase, self).setUp() + mock_validation_map = mock.patch.dict( + dependency_validations._VALIDATION_MAP) + mock_validation_map.start() + self.addCleanup(mock_validation_map.stop) + + def test_register_validator(self): + mock_session = mock.Mock() + mock_validator = mock.Mock(return_value=False) + mock_row = mock.Mock() + mock_row.object_type = self._RESOURCE_DUMMY + dependency_validations.register_validator(self._RESOURCE_DUMMY, + mock_validator) + valid = dependency_validations.validate(mock_session, mock_row) + mock_validator.assert_called_once_with(mock_session, mock_row) + self.assertFalse(valid) diff --git a/networking-odl/networking_odl/tests/unit/journal/test_full_sync.py b/networking-odl/networking_odl/tests/unit/journal/test_full_sync.py new file mode 100644 index 0000000..cedccbd --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/journal/test_full_sync.py @@ -0,0 +1,152 @@ +# +# Copyright (C) 2016 Red Hat, Inc. +# +# 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. +# + +import mock +import requests + +from neutron.db import api as neutron_db_api +from neutron import manager +from neutron.tests.unit.testlib_api import SqlTestCaseLight + +from networking_odl.common import constants as odl_const +from networking_odl.db import db +from networking_odl.db import models +from networking_odl.journal import full_sync + + +class FullSyncTestCase(SqlTestCaseLight): + def setUp(self): + super(FullSyncTestCase, self).setUp() + self.db_session = neutron_db_api.get_session() + + full_sync._CLIENT = mock.MagicMock() + self.plugin_mock = mock.patch.object(manager.NeutronManager, + 'get_plugin').start() + self.l3_plugin_mock = mock.patch.object(manager.NeutronManager, + 'get_service_plugins').start() + + self.addCleanup(self._db_cleanup) + + def _db_cleanup(self): + self.db_session.query(models.OpendaylightJournal).delete() + + def test_no_full_sync_when_canary_exists(self): + full_sync.full_sync(self.db_session) + self.assertEqual([], db.get_all_db_rows(self.db_session)) + + def _mock_l2_resources(self): + expected_journal = {odl_const.ODL_NETWORK: '1', + odl_const.ODL_SUBNET: '2', + odl_const.ODL_PORT: '3'} + plugin_instance = self.plugin_mock.return_value + plugin_instance.get_networks.return_value = [ + {'id': expected_journal[odl_const.ODL_NETWORK]}] + plugin_instance.get_subnets.return_value = [ + {'id': expected_journal[odl_const.ODL_SUBNET]}] + plugin_instance.get_ports.side_effect = ([ + {'id': expected_journal[odl_const.ODL_PORT]}], []) + return expected_journal + + def _filter_out_canary(self, rows): + return [row for row in rows if row['object_uuid'] != + full_sync._CANARY_NETWORK_ID] + + def _test_no_full_sync_when_canary_in_journal(self, state): + self._mock_canary_missing() + self._mock_l2_resources() + db.create_pending_row(self.db_session, odl_const.ODL_NETWORK, + full_sync._CANARY_NETWORK_ID, + odl_const.ODL_CREATE, {}) + row = db.get_all_db_rows(self.db_session)[0] + db.update_db_row_state(self.db_session, row, state) + + full_sync.full_sync(self.db_session) + + rows = db.get_all_db_rows(self.db_session) + self.assertEqual([], self._filter_out_canary(rows)) + + def test_no_full_sync_when_canary_pending_creation(self): + self._test_no_full_sync_when_canary_in_journal(odl_const.PENDING) + + def test_no_full_sync_when_canary_is_processing(self): + self._test_no_full_sync_when_canary_in_journal(odl_const.PROCESSING) + + def test_client_error_propagates(self): + class TestException(Exception): + def __init__(self): + pass + + full_sync._CLIENT.get.side_effect = TestException() + self.assertRaises(TestException, full_sync.full_sync, self.db_session) + + def _mock_canary_missing(self): + get_return = mock.MagicMock() + get_return.status_code = requests.codes.not_found + full_sync._CLIENT.get.return_value = get_return + + def _assert_canary_created(self): + rows = db.get_all_db_rows(self.db_session) + self.assertTrue(any(r['object_uuid'] == full_sync._CANARY_NETWORK_ID + for r in rows)) + return rows + + def _test_full_sync_resources(self, expected_journal): + self._mock_canary_missing() + + full_sync.full_sync(self.db_session) + + rows = self._assert_canary_created() + rows = self._filter_out_canary(rows) + self.assertItemsEqual(expected_journal.keys(), + [row['object_type'] for row in rows]) + for row in rows: + self.assertEqual(expected_journal[row['object_type']], + row['object_uuid']) + + def test_full_sync_removes_pending_rows(self): + db.create_pending_row(self.db_session, odl_const.ODL_NETWORK, "uuid", + odl_const.ODL_CREATE, {'foo': 'bar'}) + self._test_full_sync_resources({}) + + def test_full_sync_no_resources(self): + self._test_full_sync_resources({}) + + def test_full_sync_l2_resources(self): + self._test_full_sync_resources(self._mock_l2_resources()) + + def _mock_router_port(self, port_id): + router_port = {'id': port_id, + 'device_id': '1', + 'tenant_id': '1', + 'fixed_ips': [{'subnet_id': '1'}]} + plugin_instance = self.plugin_mock.return_value + plugin_instance.get_ports.side_effect = ([], [router_port]) + + def _mock_l3_resources(self): + expected_journal = {odl_const.ODL_ROUTER: '1', + odl_const.ODL_FLOATINGIP: '2', + odl_const.ODL_ROUTER_INTF: '3'} + plugin_instance = self.l3_plugin_mock.return_value.get.return_value + plugin_instance.get_routers.return_value = [ + {'id': expected_journal[odl_const.ODL_ROUTER]}] + plugin_instance.get_floatingips.return_value = [ + {'id': expected_journal[odl_const.ODL_FLOATINGIP]}] + self._mock_router_port(expected_journal[odl_const.ODL_ROUTER_INTF]) + + return expected_journal + + def test_full_sync_l3_resources(self): + self._test_full_sync_resources(self._mock_l3_resources()) diff --git a/networking-odl/networking_odl/tests/unit/journal/test_maintenance.py b/networking-odl/networking_odl/tests/unit/journal/test_maintenance.py new file mode 100644 index 0000000..eb823cd --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/journal/test_maintenance.py @@ -0,0 +1,93 @@ +# +# Copyright (C) 2016 Red Hat, Inc. +# +# 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. +# + +import mock +import threading +from unittest2.case import TestCase + +from neutron.db import api as neutron_db_api +from neutron.tests.unit.testlib_api import SqlTestCaseLight + +from networking_odl.common import constants as odl_const +from networking_odl.db import models +from networking_odl.journal import maintenance + + +class MaintenanceThreadTestCase(SqlTestCaseLight, TestCase): + def setUp(self): + super(MaintenanceThreadTestCase, self).setUp() + self.db_session = neutron_db_api.get_session() + + row = models.OpendaylightMaintenance(state=odl_const.PENDING) + self.db_session.add(row) + self.db_session.flush() + + self.thread = maintenance.MaintenanceThread() + self.thread.maintenance_interval = 0.01 + + def test__execute_op_no_exception(self): + with mock.patch.object(maintenance, 'LOG') as mock_log: + operation = mock.MagicMock() + operation.__name__ = "test" + self.thread._execute_op(operation, self.db_session) + self.assertTrue(operation.called) + self.assertTrue(mock_log.info.called) + self.assertFalse(mock_log.exception.called) + + def test__execute_op_with_exception(self): + with mock.patch.object(maintenance, 'LOG') as mock_log: + operation = mock.MagicMock(side_effect=Exception()) + operation.__name__ = "test" + self.thread._execute_op(operation, self.db_session) + self.assertTrue(mock_log.exception.called) + + def test_thread_works(self): + callback_event = threading.Event() + count = [0] + + def callback_op(**kwargs): + count[0] += 1 + + # The following should be true on the second call, so we're making + # sure that the thread runs more than once. + if count[0] > 1: + callback_event.set() + + self.thread.register_operation(callback_op) + self.thread.start() + + # Make sure the callback event was called and not timed out + self.assertTrue(callback_event.wait(timeout=5)) + + def test_thread_continues_after_exception(self): + exception_event = threading.Event() + callback_event = threading.Event() + + def exception_op(**kwargs): + if not exception_event.is_set(): + exception_event.set() + raise Exception() + + def callback_op(**kwargs): + callback_event.set() + + for op in [exception_op, callback_op]: + self.thread.register_operation(op) + + self.thread.start() + + # Make sure the callback event was called and not timed out + self.assertTrue(callback_event.wait(timeout=5)) diff --git a/networking-odl/networking_odl/tests/unit/l2gateway/__init__.py b/networking-odl/networking_odl/tests/unit/l2gateway/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/l2gateway/__init__.py diff --git a/networking-odl/networking_odl/tests/unit/l2gateway/test_driver.py b/networking-odl/networking_odl/tests/unit/l2gateway/test_driver.py new file mode 100644 index 0000000..2506332 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/l2gateway/test_driver.py @@ -0,0 +1,127 @@ +# +# Copyright (C) 2016 Ericsson India Global Services Pvt Ltd. +# +# 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. +# + +import copy +import mock + +from networking_odl.l2gateway import driver +from neutron.tests import base + + +class TestOpenDaylightL2gwDriver(base.DietTestCase): + + def setUp(self): + self.mocked_odlclient = mock.patch( + 'networking_odl.common.client' + '.OpenDaylightRestClient.create_client').start().return_value + self.driver = driver.OpenDaylightL2gwDriver(service_plugin=None, + validator=None) + super(TestOpenDaylightL2gwDriver, self).setUp() + + def _get_fake_l2_gateway(self): + fake_l2_gateway_id = "5227c228-6bba-4bbe-bdb8-6942768ff0f1" + fake_l2_gateway = { + "tenant_id": "de0a7495-05c4-4be0-b796-1412835c6820", + "id": "5227c228-6bba-4bbe-bdb8-6942768ff0f1", + "name": "test-gateway", + "devices": [ + { + "device_name": "switch1", + "interfaces": [ + { + "name": "port1", + "segmentation_id": [100] + }, + { + "name": "port2", + "segmentation_id": [151, 152] + } + ] + }, + { + "device_name": "switch2", + "interfaces": [ + { + "name": "port5", + "segmentation_id": [200] + }, + { + "name": "port6", + "segmentation_id": [251, 252] + } + ] + } + ] + } + return fake_l2_gateway_id, fake_l2_gateway + + def _get_fake_l2_gateway_connection(self): + fake_l2_gateway_connection_id = "5227c228-6bba-4bbe-bdb8-6942768ff02f" + fake_l2_gateway_connection = { + "tenant_id": "de0a7495-05c4-4be0-b796-1412835c6820", + "id": "5227c228-6bba-4bbe-bdb8-6942768ff02f", + "network_id": "be0a7495-05c4-4be0-b796-1412835c6830", + "default_segmentation_id": 77, + "l2_gateway_id": "5227c228-6bba-4bbe-bdb8-6942768ff0f1" + } + return fake_l2_gateway_connection_id, fake_l2_gateway_connection + + def test_create_l2_gateway_postcommit(self): + mocked_sendjson = self.mocked_odlclient.sendjson + fake_l2gateway_id, fake_l2gateway = self._get_fake_l2_gateway() + expected = {"l2_gateway": fake_l2gateway} + self.driver.create_l2_gateway_postcommit(mock.ANY, fake_l2gateway) + mocked_sendjson.assert_called_once_with('post', driver.L2GATEWAYS, + expected) + + def test_delete_l2_gateway_postcommit(self): + mocked_trydelete = self.mocked_odlclient.try_delete + fake_l2gateway_id, fake_l2gateway = self._get_fake_l2_gateway() + self.driver.delete_l2_gateway_postcommit(mock.ANY, fake_l2gateway_id) + url = driver.L2GATEWAYS + '/' + fake_l2gateway_id + mocked_trydelete.assert_called_once_with(url) + + def test_update_l2_gateway_postcommit(self): + mocked_sendjson = self.mocked_odlclient.sendjson + fake_l2gateway_id, fake_l2gateway = self._get_fake_l2_gateway() + expected = {"l2_gateway": fake_l2gateway} + self.driver.update_l2_gateway_postcommit(mock.ANY, fake_l2gateway) + url = driver.L2GATEWAYS + '/' + fake_l2gateway_id + mocked_sendjson.assert_called_once_with('put', url, expected) + + def test_create_l2_gateway_connection_postcommit(self): + mocked_sendjson = self.mocked_odlclient.sendjson + (fake_l2gateway_conn_id, + fake_l2gateway_conn) = self._get_fake_l2_gateway_connection() + expected_l2gateway_conn = copy.deepcopy(fake_l2gateway_conn) + expected_l2gateway_conn['gateway_id'] = ( + fake_l2gateway_conn['l2_gateway_id']) + expected_l2gateway_conn.pop('l2_gateway_id') + expected = {"l2gateway_connection": expected_l2gateway_conn} + self.driver.create_l2_gateway_connection_postcommit( + mock.ANY, fake_l2gateway_conn) + mocked_sendjson.assert_called_once_with('post', + driver.L2GATEWAY_CONNECTIONS, + expected) + + def test_delete_l2_gateway_connection_postcommit(self): + mocked_trydelete = self.mocked_odlclient.try_delete + (fake_l2gateway_conn_id, + fake_l2gateway_conn) = self._get_fake_l2_gateway_connection() + url = driver.L2GATEWAY_CONNECTIONS + '/' + fake_l2gateway_conn_id + self.driver.delete_l2_gateway_connection_postcommit( + mock.ANY, fake_l2gateway_conn_id) + mocked_trydelete.assert_called_once_with(url) diff --git a/networking-odl/networking_odl/tests/unit/l3/__init__.py b/networking-odl/networking_odl/tests/unit/l3/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/l3/__init__.py diff --git a/networking-odl/networking_odl/tests/unit/l3/test_l3_odl.py b/networking-odl/networking_odl/tests/unit/l3/test_l3_odl.py new file mode 100644 index 0000000..232864d --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/l3/test_l3_odl.py @@ -0,0 +1,310 @@ +# -*- coding: utf-8 -*- + +# 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. + +""" +test_l3_odl +---------------------------------- + +Tests for the L3 service plugin for networking-odl. +""" +import copy +import mock + +from neutron.extensions import l3 +from neutron.extensions import l3_ext_gw_mode +from neutron.tests.unit.api.v2 import test_base +from neutron.tests.unit.extensions import base as test_extensions_base +from webob import exc + +_get_path = test_base._get_path + + +class Testodll3(test_extensions_base.ExtensionTestCase): + + fmt = 'json' + + def setUp(self): + super(Testodll3, self).setUp() + # support ext-gw-mode + for key in l3.RESOURCE_ATTRIBUTE_MAP.keys(): + l3.RESOURCE_ATTRIBUTE_MAP[key].update( + l3_ext_gw_mode.EXTENDED_ATTRIBUTES_2_0.get(key, {})) + self._setUpExtension( + 'neutron.extensions.l3.RouterPluginBase', None, + l3.RESOURCE_ATTRIBUTE_MAP, l3.L3, '', + allow_pagination=True, allow_sorting=True, + supported_extension_aliases=['router', 'ext-gw-mode'], + use_quota=True) + + @staticmethod + def _get_mock_network_operation_context(): + current = {'status': 'ACTIVE', + 'subnets': [], + 'name': 'net1', + 'provider:physical_network': None, + 'admin_state_up': True, + 'tenant_id': 'test-tenant', + 'provider:network_type': 'local', + 'router:external': False, + 'shared': False, + 'id': 'd897e21a-dfd6-4331-a5dd-7524fa421c3e', + 'provider:segmentation_id': None} + context = mock.Mock(current=current) + return context + + @staticmethod + def _get_router_test(): + router_id = "234237d4-1e7f-11e5-9bd7-080027328c3a" + router = {'router': {'name': 'router1', 'admin_state_up': True, + 'tenant_id': router_id, + 'external_gateway_info': None}} + return router_id, router + + @staticmethod + def _get_floating_ip_test(): + floating_ip_id = "e4997650-6a83-4230-950a-8adab8e524b2" + floating_ip = { + "floatingip": {"fixed_ip_address": None, + "floating_ip_address": None, + "floating_network_id": None, + "id": floating_ip_id, + "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72", + "port_id": None, + "status": None, + "tenant_id": "test-tenant" + } + } + return floating_ip_id, floating_ip + + @staticmethod + def _get_port_test(): + port_id = "3a44f4e5-1694-493a-a1fb-393881c673a4" + subnet_id = "a2f1f29d-571b-4533-907f-5803ab96ead1" + port = {'id': port_id, + 'network_id': "84b126bb-f45e-4b2e-8202-7e5ce9e21fe7", + 'fixed_ips': [{'ip_address': '19.4.4.4', + 'prefixlen': 24, + 'subnet_id': subnet_id}], + 'subnets': [{'id': subnet_id, + 'cidr': '19.4.4.0/24', + 'gateway_ip': '19.4.4.1'}]} + return port_id, port + + def test_create_router(self): + router_id, router = self._get_router_test() + + return_value = copy.deepcopy(router['router']) + return_value.update({'status': "ACTIVE", 'id': router_id}) + + instance = self.plugin.return_value + instance.create_router.return_value = return_value + instance.get_routers_count.return_value = 0 + + res = self.api.post(_get_path('routers', fmt=self.fmt), + self.serialize(router), + content_type='application/%s' % self.fmt) + + instance.create_router.assert_called_once_with(mock.ANY, router=router) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + res = self.deserialize(res) + self.assertIn('router', res) + router = res['router'] + self.assertEqual(router_id, router['id']) + self.assertEqual("ACTIVE", router['status']) + self.assertEqual(True, router['admin_state_up']) + + def test_update_router(self): + router_id, router = self._get_router_test() + + router_request_info = {'external_gateway_info': { + "network_id": "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8", + "enable_snat": True} + } + return_value = copy.deepcopy(router['router']) + return_value.update(router_request_info) + return_value.update({'status': "ACTIVE", 'id': router_id}) + + instance = self.plugin.return_value + instance.update_router.return_value = return_value + + router_request = {'router': router_request_info} + res = self.api.put(_get_path('routers', id=router_id, fmt=self.fmt), + self.serialize(router_request)) + instance.update_router.assert_called_once_with(mock.ANY, router_id, + router=router_request) + + self.assertEqual(exc.HTTPOk.code, res.status_int) + res = self.deserialize(res) + self.assertIn('router', res) + router = res['router'] + self.assertEqual(router_id, router['id']) + self.assertEqual("3c5bcddd-6af9-4e6b-9c3e-c153e521cab8", + router["external_gateway_info"]['network_id']) + self.assertEqual(True, router["external_gateway_info"]['enable_snat']) + + def test_delete_router(self): + router_id, router = self._get_router_test() + + instance = self.plugin.return_value + + res = self.api.delete(_get_path('routers', id=router_id, fmt=self.fmt)) + instance.delete_router.assert_called_once_with(mock.ANY, router_id) + + self.assertEqual(exc.HTTPNoContent.code, res.status_int) + + def test_create_floating_ip(self): + floating_ip_id, floating_ip = self._get_floating_ip_test() + port_id, port = self._get_port_test() + + floating_ip_request_info = {"floating_network_id": + "376da547-b977-4cfe-9cba-275c80debf57", + "tenant_id": "test-tenant", + "fixed_ip_address": "10.0.0.3", + "subnet_id": port['subnets'][0]['id'], + "port_id": port_id, + "floating_ip_address": "172.24.4.228" + } + + return_value = copy.deepcopy(floating_ip['floatingip']) + return_value.update(floating_ip_request_info) + return_value.update({'status': "ACTIVE"}) + + instance = self.plugin.return_value + instance.create_floatingip.return_value = return_value + instance.get_floatingips_count.return_value = 0 + instance.get_port = mock.Mock(return_value=port) + + floating_ip_request = {'floatingip': floating_ip_request_info} + + res = self.api.post(_get_path('floatingips', fmt=self.fmt), + self.serialize(floating_ip_request)) + + instance.create_floatingip.\ + assert_called_once_with(mock.ANY, + floatingip=floating_ip_request) + + self.assertEqual(exc.HTTPCreated.code, res.status_int) + res = self.deserialize(res) + self.assertIn('floatingip', res) + floatingip = res['floatingip'] + self.assertEqual(floating_ip_id, floatingip['id']) + self.assertEqual("ACTIVE", floatingip['status']) + + def test_update_floating_ip(self): + floating_ip_id, floating_ip = self._get_floating_ip_test() + + floating_ip_request_info = {"port_id": None} + + return_value = copy.deepcopy(floating_ip['floatingip']) + return_value.update(floating_ip_request_info) + return_value.update({"status": "ACTIVE", + "tenant_id": "test-tenant", + "floating_network_id": + "376da547-b977-4cfe-9cba-275c80debf57", + "fixed_ip_address": None, + "floating_ip_address": "172.24.4.228" + }) + + instance = self.plugin.return_value + instance.update_floatingip.return_value = return_value + port_id, port = self._get_port_test() + instance.get_port = mock.Mock(return_value=port) + + floating_ip_request = {'floatingip': floating_ip_request_info} + + res = self.api.put(_get_path('floatingips', id=floating_ip_id, + fmt=self.fmt), + self.serialize(floating_ip_request)) + + instance.update_floatingip.\ + assert_called_once_with(mock.ANY, + floating_ip_id, + floatingip=floating_ip_request) + + self.assertEqual(exc.HTTPOk.code, res.status_int) + res = self.deserialize(res) + self.assertIn('floatingip', res) + floatingip = res['floatingip'] + self.assertEqual(floating_ip_id, floatingip['id']) + self.assertIsNone(floatingip['port_id']) + self.assertIsNone(floatingip['fixed_ip_address']) + + def test_delete_floating_ip(self): + floating_ip_id, floating_ip = self._get_floating_ip_test() + + instance = self.plugin.return_value + port_id, port = self._get_port_test() + instance.get_port = mock.Mock(return_value=port) + res = self.api.delete(_get_path('floatingips', id=floating_ip_id)) + instance.delete_floatingip.assert_called_once_with(mock.ANY, + floating_ip_id) + + self.assertEqual(exc.HTTPNoContent.code, res.status_int) + + def test_add_router_interface(self): + router_id, router = self._get_router_test() + interface_info = {"subnet_id": "a2f1f29d-571b-4533-907f-5803ab96ead1"} + return_value = {"tenant_id": "6ba032e4730d42e2ad928f430f5da33e", + "port_id": "3a44f4e5-1694-493a-a1fb-393881c673a4", + "id": router_id + } + return_value.update(interface_info) + + instance = self.plugin.return_value + instance.add_router_interface.return_value = return_value + + res = self.api.put(_get_path('routers', id=router_id, + action="add_router_interface", + fmt=self.fmt), + self.serialize(interface_info) + ) + + instance.add_router_interface.assert_called_once_with(mock.ANY, + router_id, + interface_info) + + self.assertEqual(exc.HTTPOk.code, res.status_int) + res = self.deserialize(res) + self.assertEqual(router_id, res['id']) + self.assertEqual("a2f1f29d-571b-4533-907f-5803ab96ead1", + res['subnet_id']) + + def test_remove_router_interface(self): + router_id, router = self._get_router_test() + interface_info = {"subnet_id": "a2f1f29d-571b-4533-907f-5803ab96ead1", + "port_id": "3a44f4e5-1694-493a-a1fb-393881c673a4" + } + return_value = {"tenant_id": "6ba032e4730d42e2ad928f430f5da33e", + "id": router_id + } + return_value.update(interface_info) + + instance = self.plugin.return_value + instance.remove_router_interface.return_value = return_value + res = self.api.put(_get_path('routers', id=router_id, + action="remove_router_interface", + fmt=self.fmt), + self.serialize(interface_info) + ) + + instance.remove_router_interface.\ + assert_called_once_with(mock.ANY, + router_id, + interface_info) + + self.assertEqual(exc.HTTPOk.code, res.status_int) + res = self.deserialize(res) + self.assertEqual(router_id, res['id']) + self.assertEqual("a2f1f29d-571b-4533-907f-5803ab96ead1", + res['subnet_id']) diff --git a/networking-odl/networking_odl/tests/unit/l3/test_l3_odl_v2.py b/networking-odl/networking_odl/tests/unit/l3/test_l3_odl_v2.py new file mode 100644 index 0000000..da3f644 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/l3/test_l3_odl_v2.py @@ -0,0 +1,526 @@ +# Copyright (c) 2016 OpenStack Foundation +# 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. + +from networking_odl.common import client +from networking_odl.common import constants as odl_const +from networking_odl.common import filters +from networking_odl.db import db +from networking_odl.journal import journal +from networking_odl.l3 import l3_odl_v2 +from networking_odl.ml2 import mech_driver_v2 + +import mock +from oslo_serialization import jsonutils +import requests + +from neutron import context +from neutron.db import api as neutron_db_api +from neutron.extensions import external_net as external_net +from neutron import manager +from neutron.plugins.ml2 import config as config +from neutron.plugins.ml2 import plugin +from neutron.tests import base +from neutron.tests.unit.db import test_db_base_plugin_v2 +from neutron.tests.unit import testlib_api + +EMPTY_DEP = [] +FLOATINGIP_ID = 'floatingip_uuid' +NETWORK_ID = 'network_uuid' +ROUTER_ID = 'router_uuid' +SUBNET_ID = 'subnet_uuid' +PORT_ID = 'port_uuid' + + +class OpenDayLightMechanismConfigTests(testlib_api.SqlTestCase): + + def _set_config(self, url='http://127.0.0.1:9999', username='someuser', + password='somepass'): + config.cfg.CONF.set_override('mechanism_drivers', + ['logger', 'opendaylight'], + 'ml2') + config.cfg.CONF.set_override('url', url, 'ml2_odl') + config.cfg.CONF.set_override('username', username, 'ml2_odl') + config.cfg.CONF.set_override('password', password, 'ml2_odl') + + def _test_missing_config(self, **kwargs): + self._set_config(**kwargs) + self.assertRaises(config.cfg.RequiredOptError, + plugin.Ml2Plugin) + + def test_valid_config(self): + self._set_config() + plugin.Ml2Plugin() + + def test_missing_url_raises_exception(self): + self._test_missing_config(url=None) + + def test_missing_username_raises_exception(self): + self._test_missing_config(username=None) + + def test_missing_password_raises_exception(self): + self._test_missing_config(password=None) + + +class DataMatcher(object): + + def __init__(self, operation, object_type, object_dict): + self._data = object_dict.copy() + self._object_type = object_type + filters.filter_for_odl(object_type, operation, self._data) + + def __eq__(self, s): + data = jsonutils.loads(s) + if self._object_type == odl_const.ODL_ROUTER_INTF: + return self._data == data + else: + return self._data == data[self._object_type] + + def __ne__(self, s): + return not self.__eq__(s) + + +class OpenDaylightL3TestCase(test_db_base_plugin_v2.NeutronDbPluginV2TestCase, + base.BaseTestCase): + def setUp(self): + config.cfg.CONF.set_override("core_plugin", + 'neutron.plugins.ml2.plugin.Ml2Plugin') + core_plugin = config.cfg.CONF.core_plugin + super(OpenDaylightL3TestCase, self).setUp(plugin=core_plugin) + 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') + mock.patch.object(journal.OpendaylightJournalThread, + 'start_odl_sync_thread').start() + self.db_session = neutron_db_api.get_session() + self.mech = mech_driver_v2.OpenDaylightMechanismDriver() + self.plugin = manager.NeutronManager.get_plugin() + self.plugin._network_is_external = mock.Mock(return_value=True) + self.driver = l3_odl_v2.OpenDaylightL3RouterPlugin() + self.thread = journal.OpendaylightJournalThread() + self.driver.get_floatingip = mock.Mock( + return_value={'router_id': ROUTER_ID, + 'floating_network_id': NETWORK_ID}) + self.addCleanup(self._db_cleanup) + + @staticmethod + def _get_mock_router_operation_info(network, subnet): + router_context = context.get_admin_context() + router = {odl_const.ODL_ROUTER: + {'name': 'router1', + 'admin_state_up': True, + 'tenant_id': network['network']['tenant_id'], + 'external_gateway_info': {'network_id': + network['network']['id']}}} + return router_context, router + + @staticmethod + def _get_mock_floatingip_operation_info(network, subnet): + floatingip_context = context.get_admin_context() + floatingip = {odl_const.ODL_FLOATINGIP: + {'floating_network_id': network['network']['id'], + 'tenant_id': network['network']['tenant_id']}} + return floatingip_context, floatingip + + @staticmethod + def _get_mock_router_interface_operation_info(network, subnet): + router_intf_context = context.get_admin_context() + router_intf_dict = {'subnet_id': subnet['subnet']['id'], + 'id': network['network']['id']} + return router_intf_context, router_intf_dict + + @classmethod + def _get_mock_operation_info(cls, object_type, *args): + getter = getattr(cls, '_get_mock_' + object_type + '_operation_info') + return getter(*args) + + def _db_cleanup(self): + rows = db.get_all_db_rows(self.db_session) + for row in rows: + db.delete_row(self.db_session, row=row) + + @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_operation(self, status_code, expected_calls, *args, **kwargs): + request_response = self._get_mock_request_response(status_code) + with mock.patch('requests.request', + return_value=request_response) as mock_method: + with mock.patch.object(self.thread.event, 'wait', + return_value=False): + self.thread.run_sync_thread(exit_after_run=True) + + if expected_calls: + mock_method.assert_called_with( + headers={'Content-Type': 'application/json'}, + auth=(config.cfg.CONF.ml2_odl.username, + config.cfg.CONF.ml2_odl.password), + timeout=config.cfg.CONF.ml2_odl.timeout, *args, **kwargs) + self.assertEqual(expected_calls, mock_method.call_count) + + def _call_operation_object(self, operation, object_type, object_id, + network, subnet): + object_context, object_dict = self._get_mock_operation_info( + object_type, network, subnet) + method = getattr(self.driver, operation + '_' + object_type) + + if operation == odl_const.ODL_CREATE: + new_object_dict = method(object_context, object_dict) + elif operation == odl_const.ODL_UPDATE: + new_object_dict = method(object_context, object_id, object_dict) + elif operation in [odl_const.ODL_ADD, odl_const.ODL_REMOVE]: + router_dict = method(object_context, object_id, object_dict) + new_object_dict = self.driver._generate_router_dict( + object_id, object_dict, router_dict) + else: + new_object_dict = method(object_context, object_id) + + return object_context, new_object_dict + + def _test_operation_thread_processing(self, object_type, operation, + network, subnet, object_id, + expected_calls=1): + http_requests = {odl_const.ODL_CREATE: 'post', + odl_const.ODL_UPDATE: 'put', + odl_const.ODL_DELETE: 'delete', + odl_const.ODL_ADD: 'put', + odl_const.ODL_REMOVE: 'put'} + status_codes = {odl_const.ODL_CREATE: requests.codes.created, + odl_const.ODL_UPDATE: requests.codes.ok, + odl_const.ODL_DELETE: requests.codes.no_content, + odl_const.ODL_ADD: requests.codes.created, + odl_const.ODL_REMOVE: requests.codes.created} + + http_request = http_requests[operation] + status_code = status_codes[operation] + + # Create database entry. + object_context, new_object_dict = self._call_operation_object( + operation, object_type, object_id, network, subnet) + + # Setup expected results. + if operation in [odl_const.ODL_UPDATE, odl_const.ODL_DELETE]: + url = (config.cfg.CONF.ml2_odl.url + '/' + object_type + 's/' + + object_id) + elif operation in [odl_const.ODL_ADD, odl_const.ODL_REMOVE]: + url = (config.cfg.CONF.ml2_odl.url + '/' + odl_const.ODL_ROUTER + + 's/' + object_id + '/' + operation + '_router_interface') + else: + url = config.cfg.CONF.ml2_odl.url + '/' + object_type + 's' + + if operation in [odl_const.ODL_CREATE, odl_const.ODL_UPDATE, + odl_const.ODL_ADD, odl_const.ODL_REMOVE]: + kwargs = { + 'url': url, + 'data': DataMatcher(operation, object_type, new_object_dict)} + else: + kwargs = {'url': url, 'data': None} + + # Call threading routine to process database entry. Test results. + self._test_operation(status_code, expected_calls, http_request, + **kwargs) + + return new_object_dict + + def _test_thread_processing(self, object_type): + # Create network and subnet. + kwargs = {'arg_list': (external_net.EXTERNAL,), + external_net.EXTERNAL: True} + with self.network(**kwargs) as network: + with self.subnet(network=network, cidr='10.0.0.0/24'): + # Add and process create request. + new_object_dict = self._test_operation_thread_processing( + object_type, odl_const.ODL_CREATE, network, None, None) + object_id = new_object_dict['id'] + rows = db.get_all_db_rows_by_state(self.db_session, + odl_const.COMPLETED) + self.assertEqual(1, len(rows)) + + # Add and process 'update' request. Adds to database. + self._test_operation_thread_processing( + object_type, odl_const.ODL_UPDATE, network, None, + object_id) + rows = db.get_all_db_rows_by_state(self.db_session, + odl_const.COMPLETED) + self.assertEqual(2, len(rows)) + + # Add and process 'delete' request. Adds to database. + self._test_operation_thread_processing( + object_type, odl_const.ODL_DELETE, network, None, + object_id) + rows = db.get_all_db_rows_by_state(self.db_session, + odl_const.COMPLETED) + self.assertEqual(3, len(rows)) + + def _test_db_results(self, object_id, operation, object_type): + rows = db.get_all_db_rows(self.db_session) + + self.assertEqual(1, len(rows)) + self.assertEqual(operation, rows[0]['operation']) + self.assertEqual(object_type, rows[0]['object_type']) + self.assertEqual(object_id, rows[0]['object_uuid']) + + self._db_cleanup() + + def _test_object_db(self, object_type): + # Create network and subnet for testing. + kwargs = {'arg_list': (external_net.EXTERNAL,), + external_net.EXTERNAL: True} + with self.network(**kwargs) as network: + with self.subnet(network=network): + object_context, object_dict = self._get_mock_operation_info( + object_type, network, None) + + # Add and test 'create' database entry. + method = getattr(self.driver, + odl_const.ODL_CREATE + '_' + object_type) + new_object_dict = method(object_context, object_dict) + object_id = new_object_dict['id'] + self._test_db_results(object_id, odl_const.ODL_CREATE, + object_type) + + # Add and test 'update' database entry. + method = getattr(self.driver, + odl_const.ODL_UPDATE + '_' + object_type) + method(object_context, object_id, object_dict) + self._test_db_results(object_id, odl_const.ODL_UPDATE, + object_type) + + # Add and test 'delete' database entry. + method = getattr(self.driver, + odl_const.ODL_DELETE + '_' + object_type) + method(object_context, object_id) + self._test_db_results(object_id, odl_const.ODL_DELETE, + object_type) + + def _test_dependency_processing( + self, test_operation, test_object, test_id, test_context, + dep_operation, dep_object, dep_id, dep_context): + + # Mock sendjson to verify that it never gets called. + mock_sendjson = mock.patch.object(client.OpenDaylightRestClient, + 'sendjson').start() + + # Create dependency db row and mark as 'processing' so it won't + # be processed by the journal thread. + db.create_pending_row(self.db_session, dep_object, + dep_id, dep_operation, dep_context) + row = db.get_all_db_rows_by_state(self.db_session, odl_const.PENDING) + db.update_db_row_state(self.db_session, row[0], odl_const.PROCESSING) + + # Create test row with dependent ID. + db.create_pending_row(self.db_session, test_object, + test_id, test_operation, test_context) + + # Call journal thread. + with mock.patch.object(self.thread.event, 'wait', + return_value=False): + self.thread.run_sync_thread(exit_after_run=True) + + # Verify that dependency row is still set at 'processing'. + rows = db.get_all_db_rows_by_state(self.db_session, + odl_const.PROCESSING) + self.assertEqual(1, len(rows)) + + # Verify that the test row was processed and set back to 'pending' + # to be processed again. + rows = db.get_all_db_rows_by_state(self.db_session, odl_const.PENDING) + self.assertEqual(1, len(rows)) + + # Verify that _json_data was not called. + self.assertFalse(mock_sendjson.call_count) + + def test_router_db(self): + self._test_object_db(odl_const.ODL_ROUTER) + + def test_floatingip_db(self): + self._test_object_db(odl_const.ODL_FLOATINGIP) + + def test_router_intf_db(self): + # Create network, subnet and router for testing. + kwargs = {'arg_list': (external_net.EXTERNAL,), + external_net.EXTERNAL: True} + with self.network(**kwargs) as network: + with self.subnet(cidr='10.0.0.0/24') as subnet: + router_context, router_dict = ( + self._get_mock_router_operation_info(network, None)) + new_router_dict = self.driver.create_router(router_context, + router_dict) + router_id = new_router_dict['id'] + + object_type = odl_const.ODL_ROUTER_INTF + router_intf_context, router_intf_dict = \ + self._get_mock_router_interface_operation_info(network, + subnet) + + # Remove 'router' database entry to allow tests to pass. + self._db_cleanup() + + # Add and test router interface 'add' database entry. + # Note that router interface events do not generate unique + # UUIDs. + self.driver.add_router_interface(router_intf_context, + router_id, router_intf_dict) + self._test_db_results(odl_const.ODL_UUID_NOT_USED, + odl_const.ODL_ADD, object_type) + + # Add and test 'remove' database entry. + self.driver.remove_router_interface(router_intf_context, + router_id, + router_intf_dict) + self._test_db_results(odl_const.ODL_UUID_NOT_USED, + odl_const.ODL_REMOVE, object_type) + + def test_router_threading(self): + self._test_thread_processing(odl_const.ODL_ROUTER) + + def test_floatingip_threading(self): + self._test_thread_processing(odl_const.ODL_FLOATINGIP) + + def test_router_intf_threading(self): + # Create network, subnet and router for testing. + kwargs = {'arg_list': (external_net.EXTERNAL,), + external_net.EXTERNAL: True} + with self.network(**kwargs) as network: + with self.subnet(cidr='10.0.0.0/24') as subnet: + router_context, router_dict = ( + self._get_mock_router_operation_info(network, None)) + new_router_dict = self.driver.create_router(router_context, + router_dict) + router_id = new_router_dict['id'] + object_type = odl_const.ODL_ROUTER_INTF + + # Add and process router interface 'add' request. Adds to + # database. Expected calls = 2 because the create_router db + # entry is also processed. + self._test_operation_thread_processing( + object_type, odl_const.ODL_ADD, network, subnet, router_id, + expected_calls=2) + rows = db.get_all_db_rows_by_state(self.db_session, + odl_const.COMPLETED) + self.assertEqual(2, len(rows)) + + # Add and process 'remove' request. Adds to database. + self._test_operation_thread_processing( + object_type, odl_const.ODL_REMOVE, network, subnet, + router_id) + rows = db.get_all_db_rows_by_state(self.db_session, + odl_const.COMPLETED) + self.assertEqual(3, len(rows)) + + def test_delete_network_validate_ext_delete_router_dep(self): + router_context = [NETWORK_ID] + self._test_dependency_processing( + odl_const.ODL_DELETE, odl_const.ODL_NETWORK, NETWORK_ID, None, + odl_const.ODL_DELETE, odl_const.ODL_ROUTER, ROUTER_ID, + router_context) + + def test_create_router_validate_ext_create_port_dep(self): + router_context = {'gw_port_id': PORT_ID} + self._test_dependency_processing( + odl_const.ODL_CREATE, odl_const.ODL_ROUTER, ROUTER_ID, + router_context, + odl_const.ODL_CREATE, odl_const.ODL_PORT, PORT_ID, None) + + def test_delete_router_validate_ext_delete_floatingip_dep(self): + floatingip_context = [ROUTER_ID] + self._test_dependency_processing( + odl_const.ODL_DELETE, odl_const.ODL_ROUTER, ROUTER_ID, None, + odl_const.ODL_DELETE, odl_const.ODL_FLOATINGIP, FLOATINGIP_ID, + floatingip_context) + + def test_delete_router_validate_ext_remove_routerintf_dep(self): + router_intf_dict = {'id': ROUTER_ID} + self._test_dependency_processing( + odl_const.ODL_DELETE, odl_const.ODL_ROUTER, ROUTER_ID, None, + odl_const.ODL_REMOVE, odl_const.ODL_ROUTER_INTF, + odl_const.ODL_UUID_NOT_USED, router_intf_dict) + + def test_delete_router_validate_self_create_dep(self): + self._test_dependency_processing( + odl_const.ODL_DELETE, odl_const.ODL_ROUTER, ROUTER_ID, EMPTY_DEP, + odl_const.ODL_CREATE, odl_const.ODL_ROUTER, ROUTER_ID, None) + + def test_delete_router_validate_self_update_dep(self): + self._test_dependency_processing( + odl_const.ODL_DELETE, odl_const.ODL_ROUTER, ROUTER_ID, EMPTY_DEP, + odl_const.ODL_UPDATE, odl_const.ODL_ROUTER, ROUTER_ID, None) + + def test_update_router_validate_self_create_dep(self): + router_context = {'gw_port_id': None} + self._test_dependency_processing( + odl_const.ODL_UPDATE, odl_const.ODL_ROUTER, ROUTER_ID, + router_context, + odl_const.ODL_CREATE, odl_const.ODL_ROUTER, ROUTER_ID, None) + + def test_create_floatingip_validate_ext_create_network_dep(self): + floatingip_context = {'floating_network_id': NETWORK_ID} + self._test_dependency_processing( + odl_const.ODL_CREATE, odl_const.ODL_FLOATINGIP, FLOATINGIP_ID, + floatingip_context, + odl_const.ODL_CREATE, odl_const.ODL_NETWORK, NETWORK_ID, None) + + def test_update_floatingip_validate_self_create_dep(self): + floatingip_context = {'floating_network_id': NETWORK_ID} + self._test_dependency_processing( + odl_const.ODL_UPDATE, odl_const.ODL_FLOATINGIP, FLOATINGIP_ID, + floatingip_context, + odl_const.ODL_CREATE, odl_const.ODL_FLOATINGIP, FLOATINGIP_ID, + EMPTY_DEP) + + def test_delete_floatingip_validate_self_create_dep(self): + self._test_dependency_processing( + odl_const.ODL_DELETE, odl_const.ODL_FLOATINGIP, FLOATINGIP_ID, + EMPTY_DEP, + odl_const.ODL_CREATE, odl_const.ODL_FLOATINGIP, FLOATINGIP_ID, + None) + + def test_delete_floatingip_validate_self_update_dep(self): + self._test_dependency_processing( + odl_const.ODL_DELETE, odl_const.ODL_FLOATINGIP, FLOATINGIP_ID, + EMPTY_DEP, + odl_const.ODL_UPDATE, odl_const.ODL_FLOATINGIP, FLOATINGIP_ID, + None) + + def test_add_router_intf_validate_ext_create_router_dep(self): + router_intf_context = {'subnet_id': SUBNET_ID, + 'id': ROUTER_ID} + self._test_dependency_processing( + odl_const.ODL_ADD, odl_const.ODL_ROUTER_INTF, + odl_const.ODL_UUID_NOT_USED, router_intf_context, + odl_const.ODL_CREATE, odl_const.ODL_ROUTER, ROUTER_ID, None) + + def test_add_router_intf_validate_ext_create_subnet_dep(self): + router_intf_context = {'subnet_id': SUBNET_ID, + 'id': ROUTER_ID} + self._test_dependency_processing( + odl_const.ODL_ADD, odl_const.ODL_ROUTER_INTF, + odl_const.ODL_UUID_NOT_USED, router_intf_context, + odl_const.ODL_CREATE, odl_const.ODL_SUBNET, SUBNET_ID, None) + + def test_remove_router_intf_validate_self_remove_router_intf_dep(self): + router_intf_context = {'subnet_id': SUBNET_ID, + 'id': ROUTER_ID} + self._test_dependency_processing( + odl_const.ODL_REMOVE, odl_const.ODL_ROUTER_INTF, + odl_const.ODL_UUID_NOT_USED, router_intf_context, + odl_const.ODL_ADD, odl_const.ODL_ROUTER_INTF, + odl_const.ODL_UUID_NOT_USED, router_intf_context) diff --git a/networking-odl/networking_odl/tests/unit/lbaas/__init__.py b/networking-odl/networking_odl/tests/unit/lbaas/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/lbaas/__init__.py diff --git a/networking-odl/networking_odl/tests/unit/lbaas/test_lbaas_odl_v1.py b/networking-odl/networking_odl/tests/unit/lbaas/test_lbaas_odl_v1.py new file mode 100644 index 0000000..bca0ccb --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/lbaas/test_lbaas_odl_v1.py @@ -0,0 +1,32 @@ +# 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. + +""" +test_lbaas_odl +---------------------------------- + +Tests for the LBaaS plugin for networking-odl. +""" + +import mock + +from networking_odl.lbaas import driver_v1 as lbaas_odl + +from neutron.tests import base + + +class TestODL_LBaaS(base.BaseTestCase): + + def test_init(self): + # just create an instance of OpenDaylightLbaasDriverV1 + self.plugin = mock.Mock() + lbaas_odl.OpenDaylightLbaasDriverV1(self.plugin) diff --git a/networking-odl/networking_odl/tests/unit/lbaas/test_lbaas_odl_v2.py b/networking-odl/networking_odl/tests/unit/lbaas/test_lbaas_odl_v2.py new file mode 100644 index 0000000..f8292f6 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/lbaas/test_lbaas_odl_v2.py @@ -0,0 +1,32 @@ +# 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. + +""" +test_lbaas_odl +---------------------------------- + +Tests for the LBaaS plugin for networking-odl. +""" + +import mock + +from networking_odl.lbaas import driver_v2 as lbaas_odl + +from neutron.tests import base + + +class TestODL_LBaaS(base.BaseTestCase): + + def test_init(self): + # just create an instance of OpenDaylightLbaasDriverV2 + self.plugin = mock.Mock() + lbaas_odl.OpenDaylightLbaasDriverV2(self.plugin) diff --git a/networking-odl/networking_odl/tests/unit/ml2/__init__.py b/networking-odl/networking_odl/tests/unit/ml2/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/ml2/__init__.py diff --git a/networking-odl/networking_odl/tests/unit/ml2/config-ovs-external_ids.sh b/networking-odl/networking_odl/tests/unit/ml2/config-ovs-external_ids.sh new file mode 100755 index 0000000..15f9b93 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/ml2/config-ovs-external_ids.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Copyright (c) 2016 OpenStack Foundation +# 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. + +uuid=$(sudo ovs-vsctl get Open_vSwitch . _uuid) + +# Test data +sudo ovs-vsctl set Open_vSwitch $uuid \ + external_ids:odl_os_hostconfig_hostid="devstack" + +# sudo ovs-vsctl set Open_vSwitch $uuid \ +# external_ids:odl_os_hostconfig_hosttype="ODL L2" + +config=$(cat <<____CONFIG +{"supported_vnic_types":[ + {"vnic_type":"normal","vif_type":"ovs","vif_details":{}}], + "allowed_network_types":["local","vlan","vxlan","gre"], + "bridge_mappings":{"physnet1":"br-ex"}} +____CONFIG +) + +echo config: $config + +sudo ovs-vsctl set Open_vSwitch $uuid \ + external_ids:odl_os_hostconfig_config_odl_l2="$config" diff --git a/networking-odl/networking_odl/tests/unit/ml2/odl_teststub.js b/networking-odl/networking_odl/tests/unit/ml2/odl_teststub.js new file mode 100644 index 0000000..1ee02d5 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/ml2/odl_teststub.js @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016 OpenStack Foundation + * 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. + * + * $nodejs odl_teststub.js + * + * local.conf or ml2_conf.ini should be set to the following: + * + * [ml2_odl] + * port_binding_controller = pseudo-agentdb-binding + * password = admin + * username = admin + * url = http://localhost:8080/controller/nb/v2/neutron + * restconf_uri = http://localhost:8125/ # for this stub + * + * To test with ODL *end to end* use below URL for restconf_uri and configure + * ovsdb external_ids using the test script: config-ovs-external_ids.sh + * + * http://localhost:8181/restconf/operational/neutron:neutron/hostconfigs + */ + +var http = require('http'); + +const PORT=8125; + +__test_odl_hconfig = {"hostconfigs": {"hostconfig": [ + {"host-id": "devstack", + "host-type": "ODL L2", + "config": { + "supported_vnic_types": [ + {"vnic_type": "normal", + "vif_type": "ovs", + "vif_details": {}}], + "allowed_network_types": ["local", "vlan", "vxlan", "gre"], + "bridge_mappings": {"physnet1":"br-ex"} + } + }] + }} + + +function handleRequest(req, res){ + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(__test_odl_hconfig)); +} + +var server = http.createServer(handleRequest); + +server.listen(PORT, function(){ + console.log("Server listening on: http://localhost:%s", PORT); + }); diff --git a/networking-odl/networking_odl/tests/unit/ml2/ovs_topology.json b/networking-odl/networking_odl/tests/unit/ml2/ovs_topology.json new file mode 100644 index 0000000..f855ce7 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/ml2/ovs_topology.json @@ -0,0 +1,171 @@ +{ + "network-topology": { + "topology": [ + { + "topology-id": "flow:1" + }, + { + "node": [ + { + "node-id": "ovsdb://uuid/c4ad780f-8f91-4fa4-804e-dd16beb191e2/bridge/br-ex", + "ovsdb:bridge-external-ids": [ + { + "bridge-external-id-key": "bridge-id", + "bridge-external-id-value": "br-ex" + } + ], + "ovsdb:bridge-name": "br-ex", + "ovsdb:bridge-other-configs": [ + { + "bridge-other-config-key": "disable-in-band", + "bridge-other-config-value": "true" + } + ], + "ovsdb:bridge-uuid": "4ba78705-3ac2-4e36-a2e1-32f1647d97a7", + "ovsdb:datapath-id": "00:00:06:87:a7:4b:36:4e", + "ovsdb:datapath-type": "ovsdb:datapath-type-netdev", + "ovsdb:managed-by": "/network-topology:network-topology/network-topology:topology[network-topology:topology-id='ovsdb:1']/network-topology:node[network-topology:node-id='ovsdb://uuid/c4ad780f-8f91-4fa4-804e-dd16beb191e2']", + "termination-point": [ + { + "ovsdb:interface-external-ids": [ + { + "external-id-key": "iface-id", + "external-id-value": "c44000c6-f199-4609-9325-afd8c72b6777" + }, + { + "external-id-key": "iface-status", + "external-id-value": "active" + }, + { + "external-id-key": "attached-mac", + "external-id-value": "fa:16:3e:a0:d5:49" + } + ], + "ovsdb:interface-type": "ovsdb:interface-type-internal", + "ovsdb:interface-uuid": "c1081aa3-607f-404e-a71e-ea1dd334b263", + "ovsdb:name": "qg-c44000c6-f1", + "ovsdb:ofport": 1, + "ovsdb:port-uuid": "1a2ef41e-4836-420c-977f-7a662c7abe62", + "tp-id": "qg-c44000c6-f1" + }, + { + "ovsdb:interface-type": "ovsdb:interface-type-internal", + "ovsdb:interface-uuid": "54439f6a-7a88-4cf6-84b7-0645642618f9", + "ovsdb:name": "br-ex", + "ovsdb:ofport": 65534, + "ovsdb:port-uuid": "9bf4c1ab-d111-479d-84ab-1874f166153b", + "tp-id": "br-ex" + } + ] + }, + { + "node-id": "ovsdb://uuid/c4ad780f-8f91-4fa4-804e-dd16beb191e2", + "ovsdb:connection-info": { + "local-ip": "10.237.214.247", + "local-port": 6640, + "remote-ip": "10.237.214.247", + "remote-port": 43247 + }, + "ovsdb:managed-node-entry": [ + { + "bridge-ref": "/network-topology:network-topology/network-topology:topology[network-topology:topology-id='ovsdb:1']/network-topology:node[network-topology:node-id='ovsdb://uuid/c4ad780f-8f91-4fa4-804e-dd16beb191e2/bridge/br-int']" + }, + { + "bridge-ref": "/network-topology:network-topology/network-topology:topology[network-topology:topology-id='ovsdb:1']/network-topology:node[network-topology:node-id='ovsdb://uuid/c4ad780f-8f91-4fa4-804e-dd16beb191e2/bridge/br-ex']" + } + ], + "ovsdb:openvswitch-external-ids": [ + { + "external-id-key": "system-id", + "external-id-value": "c4dcfd6c-8f0e-43a6-9cf5-d2a0c37f5c52" + } + ], + "ovsdb:openvswitch-other-configs": [ + { + "other-config-key": "local_ip", + "other-config-value": "10.237.214.247" + }, + { + "other-config-key": "provider_mappings", + "other-config-value": "default:ens786f0" + } + ], + "ovsdb:ovs-version": "2.3.2" + }, + { + "node-id": "ovsdb://uuid/c4ad780f-8f91-4fa4-804e-dd16beb191e2/bridge/br-int", + "ovsdb:bridge-external-ids": [ + { + "bridge-external-id-key": "bridge-id", + "bridge-external-id-value": "br-int" + } + ], + "ovsdb:bridge-name": "br-int", + "ovsdb:bridge-uuid": "d3acbe7f-cdab-4ef1-80b8-68e5db3b3b7b", + "ovsdb:datapath-id": "00:00:7e:be:ac:d3:f1:4e", + "ovsdb:datapath-type": "ovsdb:datapath-type-system", + "ovsdb:managed-by": "/network-topology:network-topology/network-topology:topology[network-topology:topology-id='ovsdb:1']/network-topology:node[network-topology:node-id='ovsdb://uuid/c4ad780f-8f91-4fa4-804e-dd16beb191e2']", + "termination-point": [ + { + "ovsdb:interface-type": "ovsdb:interface-type-internal", + "ovsdb:interface-uuid": "8164bb4f-2b8c-4405-b8de-4b6b776baa27", + "ovsdb:name": "br-int", + "ovsdb:ofport": 65534, + "ovsdb:port-uuid": "c34e1347-6757-4770-a05e-66cfb4b65167", + "tp-id": "br-int" + }, + { + "ovsdb:interface-external-ids": [ + { + "external-id-key": "iface-id", + "external-id-value": "1d5780fc-da03-4c98-8082-089d70cb65e3" + }, + { + "external-id-key": "iface-status", + "external-id-value": "active" + }, + { + "external-id-key": "attached-mac", + "external-id-value": "fa:16:3e:ee:3e:36" + } + ], + "ovsdb:interface-type": "ovsdb:interface-type-internal", + "ovsdb:interface-uuid": "00d8d482-abf9-4459-8cb1-9c8e80df4943", + "ovsdb:name": "tap1d5780fc-da", + "ovsdb:ofport": 1, + "ovsdb:port-uuid": "743a236a-a34c-4084-a5ed-8dac56371ca8", + "tp-id": "tap1d5780fc-da" + }, + { + "ovsdb:interface-external-ids": [ + { + "external-id-key": "iface-id", + "external-id-value": "674fd914-74c0-4065-a88a-929919446555" + }, + { + "external-id-key": "iface-status", + "external-id-value": "active" + }, + { + "external-id-key": "attached-mac", + "external-id-value": "fa:16:3e:62:0c:d3" + } + ], + "ovsdb:interface-type": "ovsdb:interface-type-internal", + "ovsdb:interface-uuid": "41bde142-61bc-4297-a39d-8b0ee86a0731", + "ovsdb:name": "qr-674fd914-74", + "ovsdb:ofport": 2, + "ovsdb:port-uuid": "1c505a53-ccfd-4745-9526-211016d9cbb3", + "tp-id": "qr-674fd914-74" + } + ] + } + ], + "topology-id": "ovsdb:1" + }, + { + "topology-id": "netvirt:1" + } + ] + } +}
\ No newline at end of file diff --git a/networking-odl/networking_odl/tests/unit/ml2/test_driver.py b/networking-odl/networking_odl/tests/unit/ml2/test_driver.py new file mode 100644 index 0000000..661eb55 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/ml2/test_driver.py @@ -0,0 +1,99 @@ +# Copyright (c) 2013-2015 OpenStack Foundation +# 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. + +import mock +from neutron import context +from neutron.tests.unit.plugins.ml2 import test_plugin + +from networking_odl.common import constants as const +from networking_odl.ml2 import mech_driver as driver + + +class TestODLShim(test_plugin.Ml2PluginV2TestCase): + + def setUp(self): + super(TestODLShim, self).setUp() + self.context = context.get_admin_context() + self.plugin = mock.Mock() + self.driver = driver.OpenDaylightMechanismDriver() + self.driver.odl_drv = mock.Mock() + + def test_create_network_postcommit(self): + self.driver.create_network_postcommit(self.context) + self.driver.odl_drv.synchronize.assert_called_with(const.ODL_CREATE, + const.ODL_NETWORKS, + self.context) + + def test_update_network_postcommit(self): + self.driver.update_network_postcommit(self.context) + self.driver.odl_drv.synchronize.assert_called_with(const.ODL_UPDATE, + const.ODL_NETWORKS, + self.context) + + def test_delete_network_postcommit(self): + self.driver.delete_network_postcommit(self.context) + self.driver.odl_drv.synchronize.assert_called_with(const.ODL_DELETE, + const.ODL_NETWORKS, + self.context) + + def test_create_subnet_postcommit(self): + self.driver.create_subnet_postcommit(self.context) + self.driver.odl_drv.synchronize.assert_called_with(const.ODL_CREATE, + const.ODL_SUBNETS, + self.context) + + def test_update_subnet_postcommit(self): + self.driver.update_subnet_postcommit(self.context) + self.driver.odl_drv.synchronize.assert_called_with(const.ODL_UPDATE, + const.ODL_SUBNETS, + self.context) + + def test_delete_subnet_postcommit(self): + self.driver.delete_subnet_postcommit(self.context) + self.driver.odl_drv.synchronize.assert_called_with(const.ODL_DELETE, + const.ODL_SUBNETS, + self.context) + + def test_create_port_postcommit(self): + self.driver.create_port_postcommit(self.context) + self.driver.odl_drv.synchronize.assert_called_with(const.ODL_CREATE, + const.ODL_PORTS, + self.context) + + def test_update_port_postcommit(self): + self.driver.update_port_postcommit(self.context) + self.driver.odl_drv.synchronize.assert_called_with(const.ODL_UPDATE, + const.ODL_PORTS, + self.context) + + def test_delete_port_postcommit(self): + self.driver.delete_port_postcommit(self.context) + self.driver.odl_drv.synchronize.assert_called_with(const.ODL_DELETE, + const.ODL_PORTS, + self.context) + + def test_bind_port_delegation(self): + # given front-end with attached back-end + front_end = self.driver + front_end.odl_drv = back_end = mock.MagicMock( + spec=driver.OpenDaylightDriver) + # given PortContext to be forwarded to back-end without using + context = object() + + # when binding port + front_end.bind_port(context) + + # then port is bound by back-end + back_end.bind_port.assert_called_once_with(context) diff --git a/networking-odl/networking_odl/tests/unit/ml2/test_legacy_port_binding.py b/networking-odl/networking_odl/tests/unit/ml2/test_legacy_port_binding.py new file mode 100644 index 0000000..932c961 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/ml2/test_legacy_port_binding.py @@ -0,0 +1,89 @@ +# Copyright (c) 2016 OpenStack Foundation +# 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. + +import mock + +from neutron.extensions import portbindings +from neutron.plugins.common import constants +from neutron.plugins.ml2 import driver_api as api +from neutron.plugins.ml2 import driver_context as ctx +from neutron_lib import constants as n_constants + +from networking_odl.ml2 import legacy_port_binding +from networking_odl.tests import base + + +class TestLegacyPortBindingManager(base.DietTestCase): + # valid and invalid segments + valid_segment = { + api.ID: 'API_ID', + api.NETWORK_TYPE: constants.TYPE_LOCAL, + api.SEGMENTATION_ID: 'API_SEGMENTATION_ID', + api.PHYSICAL_NETWORK: 'API_PHYSICAL_NETWORK'} + + invalid_segment = { + api.ID: 'API_ID', + api.NETWORK_TYPE: constants.TYPE_NONE, + api.SEGMENTATION_ID: 'API_SEGMENTATION_ID', + api.PHYSICAL_NETWORK: 'API_PHYSICAL_NETWORK'} + + def test_check_segment(self): + """Validate the _check_segment method.""" + + all_network_types = [constants.TYPE_FLAT, constants.TYPE_GRE, + constants.TYPE_LOCAL, constants.TYPE_VXLAN, + constants.TYPE_VLAN, constants.TYPE_NONE] + + mgr = legacy_port_binding.LegacyPortBindingManager() + + valid_types = { + network_type + for network_type in all_network_types + if mgr._check_segment({api.NETWORK_TYPE: network_type})} + + self.assertEqual({ + constants.TYPE_LOCAL, constants.TYPE_GRE, constants.TYPE_VXLAN, + constants.TYPE_VLAN}, valid_types) + + def test_bind_port(self): + + network = mock.MagicMock(spec=api.NetworkContext) + + port_context = mock.MagicMock( + spec=ctx.PortContext, current={'id': 'CURRENT_CONTEXT_ID'}, + segments_to_bind=[self.valid_segment, self.invalid_segment], + network=network) + + mgr = legacy_port_binding.LegacyPortBindingManager() + vif_type = mgr._get_vif_type(port_context) + + mgr.bind_port(port_context) + + port_context.set_binding.assert_called_once_with( + self.valid_segment[api.ID], vif_type, + mgr.vif_details, status=n_constants.PORT_STATUS_ACTIVE) + + def test_bind_port_unsupported_vnic_type(self): + network = mock.MagicMock(spec=api.NetworkContext) + port_context = mock.MagicMock( + spec=ctx.PortContext, + current={'id': 'CURRENT_CONTEXT_ID', + portbindings.VNIC_TYPE: portbindings.VNIC_DIRECT}, + segments_to_bind=[self.valid_segment, self.invalid_segment], + network=network) + + mgr = legacy_port_binding.LegacyPortBindingManager() + mgr.bind_port(port_context) + port_context.set_binding.assert_not_called() diff --git a/networking-odl/networking_odl/tests/unit/ml2/test_mechanism_odl.py b/networking-odl/networking_odl/tests/unit/ml2/test_mechanism_odl.py new file mode 100644 index 0000000..95de10c --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/ml2/test_mechanism_odl.py @@ -0,0 +1,596 @@ +# Copyright (c) 2013-2014 OpenStack Foundation +# 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. + + +import copy +import mock +import socket + +from oslo_config import cfg +from oslo_serialization import jsonutils +import requests +import webob.exc + +from neutron.db import segments_db +from neutron.extensions import portbindings +from neutron.plugins.common import constants +from neutron.plugins.ml2 import config as config +from neutron.plugins.ml2 import driver_api as api +from neutron.plugins.ml2 import driver_context as driver_context +from neutron.plugins.ml2 import plugin +from neutron.tests import base +from neutron.tests.unit.plugins.ml2 import test_plugin +from neutron.tests.unit import testlib_api +from neutron_lib import constants as n_constants + +from networking_odl.common import client +from networking_odl.common import constants as odl_const +from networking_odl.ml2 import legacy_port_binding +from networking_odl.ml2 import mech_driver +from networking_odl.ml2 import network_topology + + +cfg.CONF.import_group('ml2_odl', 'networking_odl.common.config') + + +HOST = 'fake-host' +PLUGIN_NAME = 'neutron.plugins.ml2.plugin.Ml2Plugin' +FAKE_NETWORK = {'status': 'ACTIVE', + 'subnets': [], + 'name': 'net1', + 'provider:physical_network': None, + 'admin_state_up': True, + 'tenant_id': 'test-tenant', + 'provider:network_type': 'local', + 'router:external': False, + 'shared': False, + 'id': 'd897e21a-dfd6-4331-a5dd-7524fa421c3e', + 'provider:segmentation_id': None} + +FAKE_SUBNET = {'ipv6_ra_mode': None, + 'allocation_pools': [{'start': '10.0.0.2', + 'end': '10.0.1.254'}], + 'host_routes': [], + 'ipv6_address_mode': None, + 'cidr': '10.0.0.0/23', + 'id': '72c56c48-e9b8-4dcf-b3a7-0813bb3bd839', + 'name': '', + 'enable_dhcp': True, + 'network_id': 'd897e21a-dfd6-4331-a5dd-7524fa421c3e', + 'tenant_id': 'test-tenant', + 'dns_nameservers': [], + 'gateway_ip': '10.0.0.1', + 'ip_version': 4, + 'shared': False} + +FAKE_PORT = {'status': 'DOWN', + 'binding:host_id': '', + 'allowed_address_pairs': [], + 'device_owner': 'fake_owner', + 'binding:profile': {}, + 'fixed_ips': [], + 'id': '72c56c48-e9b8-4dcf-b3a7-0813bb3bd839', + 'security_groups': [], + 'device_id': 'fake_device', + 'name': '', + 'admin_state_up': True, + 'network_id': 'c13bba05-eb07-45ba-ace2-765706b2d701', + 'tenant_id': 'bad_tenant_id', + 'binding:vif_details': {}, + 'binding:vnic_type': 'normal', + 'binding:vif_type': 'unbound', + 'mac_address': '12:34:56:78:21:b6'} + +FAKE_SECURITY_GROUP = {'description': 'Default security group', + 'id': '6875fc07-853f-4230-9ab9-23d1af894240', + 'name': 'default', + 'security_group_rules': [], + 'tenant_id': '04bb5f9a0fa14ad18203035c791ffae2'} + +FAKE_SECURITY_GROUP_RULE = {'direction': 'ingress', + 'ethertype': 'IPv4', + 'id': '399029df-cefe-4a7a-b6d6-223558627d23', + 'port_range_max': 0, + 'port_range_min': 0, + 'protocol': 0, + 'remote_group_id': '6875fc07-853f-4230-9ab9', + 'remote_ip_prefix': 0, + 'security_group_id': '6875fc07-853f-4230-9ab9', + 'tenant_id': '04bb5f9a0fa14ad18203035c791ffae2'} + + +class OpenDaylightTestCase(test_plugin.Ml2PluginV2TestCase): + _mechanism_drivers = ['opendaylight'] + + def setUp(self): + # Set URL/user/pass so init doesn't throw a cfg required error. + # They are not used in these tests since sendjson is overwritten. + 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') + + super(OpenDaylightTestCase, self).setUp() + self.port_create_status = 'DOWN' + self.mech = mech_driver.OpenDaylightMechanismDriver() + mock.patch.object( + client.OpenDaylightRestClient, + 'sendjson', + new=self.check_sendjson).start() + + # Prevent test from accidentally connecting to any web service + mock.patch.object( + network_topology, 'NetworkTopologyClient', + return_value=mock.Mock( + specs=network_topology.NetworkTopologyClient, + get=mock.Mock(side_effect=requests.HTTPError))).start() + + # Prevent hosts resolution from changing the behaviour of tests + mock.patch.object( + network_topology.utils, + 'get_addresses_by_name', + side_effect=socket.gaierror).start() + + def check_sendjson(self, method, urlpath, obj): + self.assertFalse(urlpath.startswith("http://")) + + +class OpenDayLightMechanismConfigTests(testlib_api.SqlTestCase): + + def _set_config(self, url='http://127.0.0.1:9999', username='someuser', + password='somepass'): + config.cfg.CONF.set_override('mechanism_drivers', + ['logger', 'opendaylight'], + 'ml2') + config.cfg.CONF.set_override('url', url, 'ml2_odl') + config.cfg.CONF.set_override('username', username, 'ml2_odl') + config.cfg.CONF.set_override('password', password, 'ml2_odl') + + def _test_missing_config(self, **kwargs): + self._set_config(**kwargs) + self.assertRaises(config.cfg.RequiredOptError, + plugin.Ml2Plugin) + + def test_valid_config(self): + self._set_config() + plugin.Ml2Plugin() + + def test_missing_url_raises_exception(self): + self._test_missing_config(url=None) + + def test_missing_username_raises_exception(self): + self._test_missing_config(username=None) + + def test_missing_password_raises_exception(self): + self._test_missing_config(password=None) + + +class OpenDaylightMechanismTestBasicGet(test_plugin.TestMl2BasicGet, + OpenDaylightTestCase): + pass + + +class OpenDaylightMechanismTestNetworksV2(test_plugin.TestMl2NetworksV2, + OpenDaylightTestCase): + pass + + +class OpenDaylightMechanismTestSubnetsV2(test_plugin.TestMl2SubnetsV2, + OpenDaylightTestCase): + pass + + +class OpenDaylightMechanismTestPortsV2(test_plugin.TestMl2PortsV2, + OpenDaylightTestCase): + + def setUp(self): + mock.patch.object( + mech_driver.OpenDaylightDriver, + 'out_of_sync', + new_callable=mock.PropertyMock(return_value=False)).start() + super(OpenDaylightMechanismTestPortsV2, self).setUp() + + def test_update_port_mac(self): + self.check_update_port_mac( + host_arg={portbindings.HOST_ID: HOST}, + arg_list=(portbindings.HOST_ID,), + expected_status=webob.exc.HTTPConflict.code, + expected_error='PortBound') + + +class DataMatcher(object): + + def __init__(self, operation, object_type, context): + self._data = context.current.copy() + self._object_type = object_type + filter_cls = mech_driver.OpenDaylightDriver.FILTER_MAP[ + '%ss' % object_type] + attr_filter = getattr(filter_cls, 'filter_%s_attributes' % operation) + attr_filter(self._data, context) + + def __eq__(self, s): + data = jsonutils.loads(s) + return self._data == data[self._object_type] + + def __ne__(self, s): + return not self.__eq__(s) + + +class OpenDaylightSyncTestCase(OpenDaylightTestCase): + + def setUp(self): + super(OpenDaylightSyncTestCase, self).setUp() + self.given_back_end = mech_driver.OpenDaylightDriver() + + def test_simple_sync_all_with_HTTPError_not_found(self): + self.given_back_end.out_of_sync = True + ml2_plugin = plugin.Ml2Plugin() + + response = mock.Mock(status_code=requests.codes.not_found) + fake_exception = requests.exceptions.HTTPError('Test', + response=response) + + def side_eff(*args, **kwargs): + # HTTP ERROR exception with 404 status code will be raised when use + # sendjson to get the object in ODL DB + if args[0] == 'get': + raise fake_exception + + with mock.patch.object(client.OpenDaylightRestClient, 'sendjson', + side_effect=side_eff), \ + mock.patch.object(plugin.Ml2Plugin, 'get_networks', + return_value=[FAKE_NETWORK.copy()]), \ + mock.patch.object(plugin.Ml2Plugin, 'get_network', + return_value=FAKE_NETWORK.copy()), \ + mock.patch.object(plugin.Ml2Plugin, 'get_subnets', + return_value=[FAKE_SUBNET.copy()]), \ + mock.patch.object(plugin.Ml2Plugin, 'get_ports', + return_value=[FAKE_PORT.copy()]), \ + mock.patch.object(plugin.Ml2Plugin, 'get_security_groups', + return_value=[FAKE_SECURITY_GROUP.copy()]), \ + mock.patch.object(plugin.Ml2Plugin, 'get_security_group_rules', + return_value=[FAKE_SECURITY_GROUP_RULE.copy()]): + self.given_back_end.sync_full(ml2_plugin) + + sync_id_list = [FAKE_NETWORK['id'], FAKE_SUBNET['id'], + FAKE_PORT['id'], + FAKE_SECURITY_GROUP['id'], + FAKE_SECURITY_GROUP_RULE['id']] + + act = [] + for args, kwargs in \ + client.OpenDaylightRestClient.sendjson.call_args_list: + if args[0] == 'post': + for key in args[2]: + act.append(args[2][key][0]['id']) + self.assertEqual(act, sync_id_list) + + def test_simple_sync_all_with_all_synced(self): + self.given_back_end.out_of_sync = True + ml2_plugin = plugin.Ml2Plugin() + + with mock.patch.object(client.OpenDaylightRestClient, 'sendjson', + return_value=None), \ + mock.patch.object(plugin.Ml2Plugin, 'get_networks', + return_value=[FAKE_NETWORK.copy()]), \ + mock.patch.object(plugin.Ml2Plugin, 'get_subnets', + return_value=[FAKE_SUBNET.copy()]), \ + mock.patch.object(plugin.Ml2Plugin, 'get_ports', + return_value=[FAKE_PORT.copy()]), \ + mock.patch.object(plugin.Ml2Plugin, 'get_security_groups', + return_value=[FAKE_SECURITY_GROUP.copy()]), \ + mock.patch.object(plugin.Ml2Plugin, 'get_security_group_rules', + return_value=[FAKE_SECURITY_GROUP_RULE.copy()]): + self.given_back_end.sync_full(ml2_plugin) + + # it's only called for GET, there is no call for PUT + # 5 = network, subnet, port, security_group, security_group_rule + self.assertEqual(5, + client.OpenDaylightRestClient.sendjson.call_count) + + +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 = mech_driver.OpenDaylightMechanismDriver() + self.mech.initialize() + + @staticmethod + def _get_mock_network_operation_context(): + context = mock.Mock(current=FAKE_NETWORK.copy()) + return context + + @staticmethod + def _get_mock_subnet_operation_context(): + context = mock.Mock(current=FAKE_SUBNET.copy()) + return context + + @staticmethod + def _get_mock_port_operation_context(): + context = mock.Mock(current=FAKE_PORT.copy()) + context._plugin.get_security_group = mock.Mock(return_value={}) + return context + + @classmethod + def _get_mock_operation_context(cls, object_type): + getter = getattr(cls, '_get_mock_%s_operation_context' % object_type) + return getter() + + _status_code_msgs = { + 200: '', + 201: '', + 204: '', + 400: '400 Client Error: Bad Request', + 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', + 503: '503 Server Error: Service Unavailable', + } + + @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], response=response))) + return response + + def _test_single_operation(self, method, context, status_code, + exc_class=None, *args, **kwargs): + self.mech.odl_drv.out_of_sync = False + 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) + mock_method.assert_called_once_with( + headers={'Content-Type': 'application/json'}, + auth=(config.cfg.CONF.ml2_odl.username, + config.cfg.CONF.ml2_odl.password), + timeout=config.cfg.CONF.ml2_odl.timeout, *args, **kwargs) + + def _test_create_resource_postcommit(self, object_type, status_code, + exc_class=None): + method = getattr(self.mech, 'create_%s_postcommit' % object_type) + context = self._get_mock_operation_context(object_type) + url = '%s/%ss' % (config.cfg.CONF.ml2_odl.url, object_type) + kwargs = {'url': url, + 'data': DataMatcher(odl_const.ODL_CREATE, object_type, + context)} + self._test_single_operation(method, context, status_code, exc_class, + 'post', **kwargs) + + def _test_update_resource_postcommit(self, object_type, status_code, + exc_class=None): + method = getattr(self.mech, 'update_%s_postcommit' % object_type) + context = self._get_mock_operation_context(object_type) + url = '%s/%ss/%s' % (config.cfg.CONF.ml2_odl.url, object_type, + context.current['id']) + kwargs = {'url': url, + 'data': DataMatcher(odl_const.ODL_UPDATE, object_type, + context)} + self._test_single_operation(method, context, status_code, exc_class, + 'put', **kwargs) + + def _test_delete_resource_postcommit(self, object_type, status_code, + exc_class=None): + method = getattr(self.mech, 'delete_%s_postcommit' % object_type) + context = self._get_mock_operation_context(object_type) + url = '%s/%ss/%s' % (config.cfg.CONF.ml2_odl.url, object_type, + context.current['id']) + kwargs = {'url': url, 'data': None} + self._test_single_operation(method, context, status_code, exc_class, + odl_const.ODL_DELETE, **kwargs) + + def test_create_network_postcommit(self): + self._test_create_resource_postcommit(odl_const.ODL_NETWORK, + requests.codes.created) + for status_code in (requests.codes.bad_request, + requests.codes.unauthorized): + self._test_create_resource_postcommit( + odl_const.ODL_NETWORK, status_code, + requests.exceptions.HTTPError) + + def test_create_subnet_postcommit(self): + self._test_create_resource_postcommit(odl_const.ODL_SUBNET, + requests.codes.created) + for status_code in (requests.codes.bad_request, + requests.codes.unauthorized, + requests.codes.forbidden, + requests.codes.not_found, + requests.codes.conflict, + requests.codes.not_implemented): + self._test_create_resource_postcommit( + odl_const.ODL_SUBNET, status_code, + requests.exceptions.HTTPError) + + def test_create_port_postcommit(self): + self._test_create_resource_postcommit(odl_const.ODL_PORT, + requests.codes.created) + for status_code in (requests.codes.bad_request, + requests.codes.unauthorized, + requests.codes.forbidden, + requests.codes.not_found, + requests.codes.conflict, + requests.codes.not_implemented, + requests.codes.service_unavailable): + self._test_create_resource_postcommit( + odl_const.ODL_PORT, status_code, + requests.exceptions.HTTPError) + + def test_update_network_postcommit(self): + self._test_update_resource_postcommit(odl_const.ODL_NETWORK, + requests.codes.ok) + for status_code in (requests.codes.bad_request, + requests.codes.forbidden, + requests.codes.not_found): + self._test_update_resource_postcommit( + odl_const.ODL_NETWORK, status_code, + requests.exceptions.HTTPError) + + def test_update_subnet_postcommit(self): + self._test_update_resource_postcommit(odl_const.ODL_SUBNET, + requests.codes.ok) + for status_code in (requests.codes.bad_request, + requests.codes.unauthorized, + requests.codes.forbidden, + requests.codes.not_found, + requests.codes.not_implemented): + self._test_update_resource_postcommit( + odl_const.ODL_SUBNET, status_code, + requests.exceptions.HTTPError) + + def test_update_port_postcommit(self): + self._test_update_resource_postcommit(odl_const.ODL_PORT, + requests.codes.ok) + for status_code in (requests.codes.bad_request, + requests.codes.unauthorized, + requests.codes.forbidden, + requests.codes.not_found, + requests.codes.conflict, + requests.codes.not_implemented): + self._test_update_resource_postcommit( + odl_const.ODL_PORT, status_code, + requests.exceptions.HTTPError) + + def test_delete_network_postcommit(self): + self._test_delete_resource_postcommit(odl_const.ODL_NETWORK, + requests.codes.no_content) + self._test_delete_resource_postcommit(odl_const.ODL_NETWORK, + requests.codes.not_found) + for status_code in (requests.codes.unauthorized, + requests.codes.conflict): + self._test_delete_resource_postcommit( + odl_const.ODL_NETWORK, status_code, + requests.exceptions.HTTPError) + + def test_delete_subnet_postcommit(self): + self._test_delete_resource_postcommit(odl_const.ODL_SUBNET, + requests.codes.no_content) + self._test_delete_resource_postcommit(odl_const.ODL_SUBNET, + requests.codes.not_found) + for status_code in (requests.codes.unauthorized, + requests.codes.conflict, + requests.codes.not_implemented): + self._test_delete_resource_postcommit( + odl_const.ODL_SUBNET, status_code, + requests.exceptions.HTTPError) + + def test_delete_port_postcommit(self): + self._test_delete_resource_postcommit(odl_const.ODL_PORT, + requests.codes.no_content) + self._test_delete_resource_postcommit(odl_const.ODL_PORT, + requests.codes.not_found) + for status_code in (requests.codes.unauthorized, + requests.codes.forbidden, + requests.codes.not_implemented): + self._test_delete_resource_postcommit( + odl_const.ODL_PORT, status_code, + requests.exceptions.HTTPError) + + def test_port_emtpy_tenant_id_work_around(self): + """Validate the work around code of port creation""" + plugin = mock.Mock() + plugin_context = mock.Mock() + network = self._get_mock_operation_context( + odl_const.ODL_NETWORK).current + port = self._get_mock_operation_context(odl_const.ODL_PORT).current + tenant_id = network['tenant_id'] + port['tenant_id'] = '' + + with mock.patch.object(segments_db, 'get_network_segments'): + context = driver_context.PortContext( + plugin, plugin_context, port, network, {}, 0, None) + self.mech.odl_drv.FILTER_MAP[ + odl_const.ODL_PORTS].filter_create_attributes(port, context) + self.assertEqual(tenant_id, port['tenant_id']) + + def test_update_port_filter(self): + """Validate the filter code on update port operation""" + items_to_filter = ['network_id', 'id', 'status', 'tenant_id'] + plugin_context = mock.Mock() + network = self._get_mock_operation_context( + odl_const.ODL_NETWORK).current + subnet = self._get_mock_operation_context(odl_const.ODL_SUBNET).current + port = self._get_mock_operation_context(odl_const.ODL_PORT).current + port['fixed_ips'] = [{'subnet_id': subnet['id'], + 'ip_address': '10.0.0.10'}] + port['mac_address'] = port['mac_address'].upper() + orig_port = copy.deepcopy(port) + + with mock.patch.object(segments_db, 'get_network_segments'): + context = driver_context.PortContext( + plugin, plugin_context, port, network, {}, 0, None) + self.mech.odl_drv.FILTER_MAP[ + odl_const.ODL_PORTS].filter_update_attributes(port, context) + for key, value in port.items(): + if key not in items_to_filter: + self.assertEqual(orig_port[key], value) + + +class TestOpenDaylightMechanismDriver(base.DietTestCase): + + # given valid and invalid segments + valid_segment = { + api.ID: 'API_ID', + api.NETWORK_TYPE: constants.TYPE_LOCAL, + api.SEGMENTATION_ID: 'API_SEGMENTATION_ID', + api.PHYSICAL_NETWORK: 'API_PHYSICAL_NETWORK'} + + invalid_segment = { + api.ID: 'API_ID', + api.NETWORK_TYPE: constants.TYPE_NONE, + api.SEGMENTATION_ID: 'API_SEGMENTATION_ID', + api.PHYSICAL_NETWORK: 'API_PHYSICAL_NETWORK'} + + def test_bind_port_front_end(self): + given_front_end = mech_driver.OpenDaylightMechanismDriver() + given_port_context = self.given_port_context() + given_back_end = mech_driver.OpenDaylightDriver() + given_front_end.odl_drv = given_back_end + given_back_end.port_binding_controller = \ + legacy_port_binding.LegacyPortBindingManager() + + # when port is bound + given_front_end.bind_port(given_port_context) + + # then context binding is setup with returned vif_type and valid + # segment API ID + given_port_context.set_binding.assert_called_once_with( + self.valid_segment[api.ID], portbindings.VIF_TYPE_OVS, + given_back_end.port_binding_controller.vif_details, + status=n_constants.PORT_STATUS_ACTIVE) + + def given_port_context(self): + from neutron.plugins.ml2 import driver_context as ctx + + # given NetworkContext + network = mock.MagicMock(spec=api.NetworkContext) + + # given port context + return mock.MagicMock( + spec=ctx.PortContext, current={'id': 'CURRENT_CONTEXT_ID'}, + segments_to_bind=[self.valid_segment, self.invalid_segment], + network=network, + _new_bound_segment=self.valid_segment) diff --git a/networking-odl/networking_odl/tests/unit/ml2/test_mechanism_odl_v2.py b/networking-odl/networking_odl/tests/unit/ml2/test_mechanism_odl_v2.py new file mode 100644 index 0000000..7e8c7fc --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/ml2/test_mechanism_odl_v2.py @@ -0,0 +1,577 @@ +# Copyright (c) 2015 OpenStack Foundation +# 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. +import datetime + +from networking_odl.common import callback +from networking_odl.common import client +from networking_odl.common import constants as odl_const +from networking_odl.common import filters +from networking_odl.db import db +from networking_odl.journal import cleanup +from networking_odl.journal import journal +from networking_odl.ml2 import mech_driver_v2 + +import mock +from oslo_config import cfg +from oslo_serialization import jsonutils +import requests + +from neutron.db import api as neutron_db_api +from neutron import manager +from neutron.plugins.ml2 import config as config +from neutron.plugins.ml2 import plugin +from neutron.tests.unit.plugins.ml2 import test_plugin +from neutron.tests.unit import testlib_api + +cfg.CONF.import_group('ml2_odl', 'networking_odl.common.config') + +SECURITY_GROUP = '2f9244b4-9bee-4e81-bc4a-3f3c2045b3d7' +SG_FAKE_ID = 'sg_fake_uuid' +SG_RULE_FAKE_ID = 'sg_rule_fake_uuid' + + +class OpenDaylightConfigBase(test_plugin.Ml2PluginV2TestCase): + def setUp(self): + super(OpenDaylightConfigBase, 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') + + +class OpenDaylightTestCase(OpenDaylightConfigBase): + def setUp(self): + super(OpenDaylightTestCase, self).setUp() + self.port_create_status = 'DOWN' + self.mech = mech_driver_v2.OpenDaylightMechanismDriver() + mock.patch.object(journal.OpendaylightJournalThread, + 'start_odl_sync_thread').start() + self.mock_sendjson = mock.patch.object(client.OpenDaylightRestClient, + 'sendjson').start() + self.mock_sendjson.side_effect = self.check_sendjson + + def check_sendjson(self, method, urlpath, obj): + self.assertFalse(urlpath.startswith("http://")) + + +class OpenDayLightMechanismConfigTests(testlib_api.SqlTestCase): + def _set_config(self, url='http://127.0.0.1:9999', username='someuser', + password='somepass'): + config.cfg.CONF.set_override('mechanism_drivers', + ['logger', 'opendaylight'], + 'ml2') + config.cfg.CONF.set_override('url', url, 'ml2_odl') + config.cfg.CONF.set_override('username', username, 'ml2_odl') + config.cfg.CONF.set_override('password', password, 'ml2_odl') + + def _test_missing_config(self, **kwargs): + self._set_config(**kwargs) + self.assertRaises(config.cfg.RequiredOptError, + plugin.Ml2Plugin) + + def test_valid_config(self): + self._set_config() + plugin.Ml2Plugin() + + def test_missing_url_raises_exception(self): + self._test_missing_config(url=None) + + def test_missing_username_raises_exception(self): + self._test_missing_config(username=None) + + def test_missing_password_raises_exception(self): + self._test_missing_config(password=None) + + +class OpenDaylightMechanismTestBasicGet(test_plugin.TestMl2BasicGet, + OpenDaylightTestCase): + pass + + +class OpenDaylightMechanismTestNetworksV2(test_plugin.TestMl2NetworksV2, + OpenDaylightTestCase): + pass + + +class OpenDaylightMechanismTestSubnetsV2(test_plugin.TestMl2SubnetsV2, + OpenDaylightTestCase): + pass + + +class OpenDaylightMechanismTestPortsV2(test_plugin.TestMl2PortsV2, + OpenDaylightTestCase): + pass + + +class DataMatcher(object): + + def __init__(self, operation, object_type, context): + if object_type in [odl_const.ODL_SG, odl_const.ODL_SG_RULE]: + self._data = context[object_type].copy() + else: + self._data = context.current.copy() + self._object_type = object_type + filters.filter_for_odl(object_type, operation, self._data) + + def __eq__(self, s): + data = jsonutils.loads(s) + return self._data == data[self._object_type] + + def __ne__(self, s): + return not self.__eq__(s) + + +class AttributeDict(dict): + def __init__(self, *args, **kwargs): + super(AttributeDict, self).__init__(*args, **kwargs) + self.__dict__ = self + + +class OpenDaylightMechanismDriverTestCase(OpenDaylightConfigBase): + def setUp(self): + super(OpenDaylightMechanismDriverTestCase, self).setUp() + self.db_session = neutron_db_api.get_session() + self.mech = mech_driver_v2.OpenDaylightMechanismDriver() + self.mock_sync_thread = mock.patch.object( + journal.OpendaylightJournalThread, 'start_odl_sync_thread').start() + self.mech.initialize() + self.thread = journal.OpendaylightJournalThread() + self.addCleanup(self._db_cleanup) + + @staticmethod + def _get_mock_network_operation_context(): + current = {'status': 'ACTIVE', + 'subnets': [], + 'name': 'net1', + 'provider:physical_network': None, + 'admin_state_up': True, + 'tenant_id': 'test-tenant', + 'provider:network_type': 'local', + 'router:external': False, + 'shared': False, + 'id': 'd897e21a-dfd6-4331-a5dd-7524fa421c3e', + 'provider:segmentation_id': None} + context = mock.Mock(current=current) + context._plugin_context.session = neutron_db_api.get_session() + return context + + @staticmethod + def _get_mock_subnet_operation_context(): + current = {'ipv6_ra_mode': None, + 'allocation_pools': [{'start': '10.0.0.2', + 'end': '10.0.1.254'}], + 'host_routes': [], + 'ipv6_address_mode': None, + 'cidr': '10.0.0.0/23', + 'id': '72c56c48-e9b8-4dcf-b3a7-0813bb3bd839', + 'name': '', + 'enable_dhcp': True, + 'network_id': 'd897e21a-dfd6-4331-a5dd-7524fa421c3e', + 'tenant_id': 'test-tenant', + 'dns_nameservers': [], + 'gateway_ip': '10.0.0.1', + 'ip_version': 4, + 'shared': False} + context = mock.Mock(current=current) + context._plugin_context.session = neutron_db_api.get_session() + return context + + @staticmethod + def _get_mock_port_operation_context(): + current = {'status': 'DOWN', + 'binding:host_id': '', + 'allowed_address_pairs': [], + 'device_owner': 'fake_owner', + 'binding:profile': {}, + 'fixed_ips': [{ + 'subnet_id': '72c56c48-e9b8-4dcf-b3a7-0813bb3bd839'}], + 'id': '83d56c48-e9b8-4dcf-b3a7-0813bb3bd940', + 'security_groups': [SECURITY_GROUP], + 'device_id': 'fake_device', + 'name': '', + 'admin_state_up': True, + 'network_id': 'd897e21a-dfd6-4331-a5dd-7524fa421c3e', + 'tenant_id': 'test-tenant', + 'binding:vif_details': {}, + 'binding:vnic_type': 'normal', + 'binding:vif_type': 'unbound', + 'mac_address': '12:34:56:78:21:b6'} + _network = OpenDaylightMechanismDriverTestCase.\ + _get_mock_network_operation_context().current + _plugin = manager.NeutronManager.get_plugin() + _plugin.get_security_group = mock.Mock(return_value=SECURITY_GROUP) + _plugin.get_port = mock.Mock(return_value=current) + _plugin.get_network = mock.Mock(return_value=_network) + _plugin_context_mock = {'session': neutron_db_api.get_session()} + _network_context_mock = {'_network': _network} + context = {'current': AttributeDict(current), + '_plugin': _plugin, + '_plugin_context': AttributeDict(_plugin_context_mock), + '_network_context': AttributeDict(_network_context_mock)} + return AttributeDict(context) + + @staticmethod + def _get_mock_security_group_operation_context(): + context = {odl_const.ODL_SG: {'name': 'test_sg', + 'id': SG_FAKE_ID}} + return context + + @staticmethod + def _get_mock_security_group_rule_operation_context(): + context = {odl_const.ODL_SG_RULE: {'security_group_id': SG_FAKE_ID, + 'id': SG_RULE_FAKE_ID}} + return context + + @classmethod + def _get_mock_operation_context(cls, object_type): + getter = getattr(cls, '_get_mock_%s_operation_context' % object_type) + return getter() + + _status_code_msgs = { + 200: '', + 201: '', + 204: '', + 400: '400 Client Error: Bad Request', + 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', + 503: '503 Server Error: Service Unavailable', + } + + def _db_cleanup(self): + rows = db.get_all_db_rows(self.db_session) + for row in rows: + db.delete_row(self.db_session, row=row) + + @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_operation(self, method, status_code, expected_calls, + *args, **kwargs): + request_response = self._get_mock_request_response(status_code) + with mock.patch('requests.request', + return_value=request_response) as mock_method: + method(exit_after_run=True) + + if expected_calls: + mock_method.assert_called_with( + headers={'Content-Type': 'application/json'}, + auth=(config.cfg.CONF.ml2_odl.username, + config.cfg.CONF.ml2_odl.password), + timeout=config.cfg.CONF.ml2_odl.timeout, *args, **kwargs) + self.assertEqual(expected_calls, mock_method.call_count) + + def _call_operation_object(self, operation, object_type): + context = self._get_mock_operation_context(object_type) + + if object_type in [odl_const.ODL_SG, odl_const.ODL_SG_RULE]: + res_type = [rt for rt in callback._RESOURCE_MAPPING.values() + if rt.singular == object_type][0] + self.mech.sync_from_callback(operation, res_type, + context[object_type]['id'], context) + else: + method = getattr(self.mech, '%s_%s_precommit' % (operation, + object_type)) + method(context) + + def _test_operation_object(self, operation, object_type): + self._call_operation_object(operation, object_type) + + context = self._get_mock_operation_context(object_type) + row = db.get_oldest_pending_db_row_with_lock(self.db_session) + self.assertEqual(operation, row['operation']) + self.assertEqual(object_type, row['object_type']) + self.assertEqual(context.current['id'], row['object_uuid']) + + def _test_thread_processing(self, operation, object_type, + expected_calls=1): + http_requests = {odl_const.ODL_CREATE: 'post', + odl_const.ODL_UPDATE: 'put', + odl_const.ODL_DELETE: 'delete'} + status_codes = {odl_const.ODL_CREATE: requests.codes.created, + odl_const.ODL_UPDATE: requests.codes.ok, + odl_const.ODL_DELETE: requests.codes.no_content} + + http_request = http_requests[operation] + status_code = status_codes[operation] + + self._call_operation_object(operation, object_type) + + context = self._get_mock_operation_context(object_type) + url_object_type = object_type.replace('_', '-') + if operation in [odl_const.ODL_UPDATE, odl_const.ODL_DELETE]: + if object_type in [odl_const.ODL_SG, odl_const.ODL_SG_RULE]: + uuid = context[object_type]['id'] + else: + uuid = context.current['id'] + url = '%s/%ss/%s' % (config.cfg.CONF.ml2_odl.url, url_object_type, + uuid) + else: + url = '%s/%ss' % (config.cfg.CONF.ml2_odl.url, url_object_type) + + if operation in [odl_const.ODL_CREATE, odl_const.ODL_UPDATE]: + kwargs = { + 'url': url, + 'data': DataMatcher(operation, object_type, context)} + else: + kwargs = {'url': url, 'data': None} + with mock.patch.object(self.thread.event, 'wait', + return_value=False): + self._test_operation(self.thread.run_sync_thread, status_code, + expected_calls, http_request, **kwargs) + + def _test_object_type(self, object_type): + # Add and process create request. + self._test_thread_processing(odl_const.ODL_CREATE, object_type) + rows = db.get_all_db_rows_by_state(self.db_session, + odl_const.COMPLETED) + self.assertEqual(1, len(rows)) + + # Add and process update request. Adds to database. + self._test_thread_processing(odl_const.ODL_UPDATE, object_type) + rows = db.get_all_db_rows_by_state(self.db_session, + odl_const.COMPLETED) + self.assertEqual(2, len(rows)) + + # Add and process update request. Adds to database. + self._test_thread_processing(odl_const.ODL_DELETE, object_type) + rows = db.get_all_db_rows_by_state(self.db_session, + odl_const.COMPLETED) + self.assertEqual(3, len(rows)) + + def _test_object_type_pending_network(self, object_type): + # Create a network (creates db row in pending state). + self._call_operation_object(odl_const.ODL_CREATE, + odl_const.ODL_NETWORK) + + # Create object_type database row and process. This results in both + # the object_type and network rows being processed. + self._test_thread_processing(odl_const.ODL_CREATE, object_type, + expected_calls=2) + + # Verify both rows are now marked as completed. + rows = db.get_all_db_rows_by_state(self.db_session, + odl_const.COMPLETED) + self.assertEqual(2, len(rows)) + + def _test_object_type_processing_network(self, object_type): + self._test_object_operation_pending_another_object_operation( + object_type, odl_const.ODL_CREATE, odl_const.ODL_NETWORK, + odl_const.ODL_CREATE) + + def _test_object_operation_pending_object_operation( + self, object_type, operation, pending_operation): + self._test_object_operation_pending_another_object_operation( + object_type, operation, object_type, pending_operation) + + def _test_object_operation_pending_another_object_operation( + self, object_type, operation, pending_type, pending_operation): + # Create the object_type (creates db row in pending state). + self._call_operation_object(pending_operation, + pending_type) + + # Get pending row and mark as processing so that + # this row will not be processed by journal thread. + row = db.get_all_db_rows_by_state(self.db_session, odl_const.PENDING) + db.update_db_row_state(self.db_session, row[0], odl_const.PROCESSING) + + # Create the object_type database row and process. + # Verify that object request is not processed because the + # dependent object operation has not been marked as 'completed'. + self._test_thread_processing(operation, + object_type, + expected_calls=0) + + # Verify that all rows are still in the database. + rows = db.get_all_db_rows_by_state(self.db_session, + odl_const.PROCESSING) + self.assertEqual(1, len(rows)) + rows = db.get_all_db_rows_by_state(self.db_session, odl_const.PENDING) + self.assertEqual(1, len(rows)) + + def _test_parent_delete_pending_child_delete(self, parent, child): + self._test_object_operation_pending_another_object_operation( + parent, odl_const.ODL_DELETE, child, odl_const.ODL_DELETE) + + def _test_cleanup_processing_rows(self, last_retried, expected_state): + # Create a dummy network (creates db row in pending state). + self._call_operation_object(odl_const.ODL_CREATE, + odl_const.ODL_NETWORK) + + # Get pending row and mark as processing and update + # the last_retried time + row = db.get_all_db_rows_by_state(self.db_session, + odl_const.PENDING)[0] + row.last_retried = last_retried + db.update_db_row_state(self.db_session, row, odl_const.PROCESSING) + + # Test if the cleanup marks this in the desired state + # based on the last_retried timestamp + cleanup.JournalCleanup().cleanup_processing_rows(self.db_session) + + # Verify that the Db row is in the desired state + rows = db.get_all_db_rows_by_state(self.db_session, expected_state) + self.assertEqual(1, len(rows)) + + def test_driver(self): + for operation in [odl_const.ODL_CREATE, odl_const.ODL_UPDATE, + odl_const.ODL_DELETE]: + for object_type in [odl_const.ODL_NETWORK, odl_const.ODL_SUBNET, + odl_const.ODL_PORT]: + self._test_operation_object(operation, object_type) + + def test_port_precommit_no_tenant(self): + context = self._get_mock_operation_context(odl_const.ODL_PORT) + context.current['tenant_id'] = '' + + method = getattr(self.mech, 'create_port_precommit') + method(context) + + # Verify that the Db row has a tenant + rows = db.get_all_db_rows_by_state(self.db_session, odl_const.PENDING) + self.assertEqual(1, len(rows)) + _network = OpenDaylightMechanismDriverTestCase.\ + _get_mock_network_operation_context().current + self.assertEqual(_network['tenant_id'], rows[0]['data']['tenant_id']) + + def test_network(self): + self._test_object_type(odl_const.ODL_NETWORK) + + def test_network_update_pending_network_create(self): + self._test_object_operation_pending_object_operation( + odl_const.ODL_NETWORK, odl_const.ODL_UPDATE, odl_const.ODL_CREATE) + + def test_network_delete_pending_network_create(self): + self._test_object_operation_pending_object_operation( + odl_const.ODL_NETWORK, odl_const.ODL_DELETE, odl_const.ODL_CREATE) + + def test_network_delete_pending_network_update(self): + self._test_object_operation_pending_object_operation( + odl_const.ODL_NETWORK, odl_const.ODL_DELETE, odl_const.ODL_UPDATE) + + def test_network_delete_pending_subnet_delete(self): + self._test_parent_delete_pending_child_delete( + odl_const.ODL_NETWORK, odl_const.ODL_SUBNET) + + def test_network_delete_pending_port_delete(self): + self._test_parent_delete_pending_child_delete( + odl_const.ODL_NETWORK, odl_const.ODL_PORT) + + def test_subnet(self): + self._test_object_type(odl_const.ODL_SUBNET) + + def test_subnet_update_pending_subnet_create(self): + self._test_object_operation_pending_object_operation( + odl_const.ODL_SUBNET, odl_const.ODL_UPDATE, odl_const.ODL_CREATE) + + def test_subnet_delete_pending_subnet_create(self): + self._test_object_operation_pending_object_operation( + odl_const.ODL_SUBNET, odl_const.ODL_DELETE, odl_const.ODL_CREATE) + + def test_subnet_delete_pending_subnet_update(self): + self._test_object_operation_pending_object_operation( + odl_const.ODL_SUBNET, odl_const.ODL_DELETE, odl_const.ODL_UPDATE) + + def test_subnet_pending_network(self): + self._test_object_type_pending_network(odl_const.ODL_SUBNET) + + def test_subnet_processing_network(self): + self._test_object_type_processing_network(odl_const.ODL_SUBNET) + + def test_subnet_delete_pending_port_delete(self): + self._test_parent_delete_pending_child_delete( + odl_const.ODL_SUBNET, odl_const.ODL_PORT) + + def test_port(self): + self._test_object_type(odl_const.ODL_PORT) + + def test_port_update_pending_port_create(self): + self._test_object_operation_pending_object_operation( + odl_const.ODL_PORT, odl_const.ODL_UPDATE, odl_const.ODL_CREATE) + + def test_port_delete_pending_port_create(self): + self._test_object_operation_pending_object_operation( + odl_const.ODL_PORT, odl_const.ODL_DELETE, odl_const.ODL_CREATE) + + def test_port_delete_pending_port_update(self): + self._test_object_operation_pending_object_operation( + odl_const.ODL_PORT, odl_const.ODL_DELETE, odl_const.ODL_UPDATE) + + def test_port_pending_network(self): + self._test_object_type_pending_network(odl_const.ODL_PORT) + + def test_port_processing_network(self): + self._test_object_type_processing_network(odl_const.ODL_PORT) + + def test_cleanup_processing_rows_time_not_expired(self): + self._test_cleanup_processing_rows(datetime.datetime.utcnow(), + odl_const.PROCESSING) + + def test_cleanup_processing_rows_time_expired(self): + old_time = datetime.datetime.utcnow() - datetime.timedelta(hours=24) + self._test_cleanup_processing_rows(old_time, odl_const.PENDING) + + def test_thread_call(self): + """Verify that the sync thread method is called.""" + + # Create any object that would spin up the sync thread via the + # decorator call_thread_on_end() used by all the event handlers. + self._call_operation_object(odl_const.ODL_CREATE, + odl_const.ODL_NETWORK) + + # Verify that the thread call was made. + self.assertTrue(self.mock_sync_thread.called) + + def test_sg(self): + self._test_object_type(odl_const.ODL_SG) + + def test_sg_rule(self): + self._test_object_type(odl_const.ODL_SG_RULE) + + def _decrease_row_created_time(self, row): + row.created_at -= datetime.timedelta(hours=1) + self.db_session.merge(row) + self.db_session.flush() + + def test_sync_multiple_updates(self): + # add 2 updates + for i in range(2): + self._call_operation_object(odl_const.ODL_UPDATE, + odl_const.ODL_NETWORK) + + # get the last update row + last_row = db.get_all_db_rows(self.db_session)[-1] + + # change the last update created time + self._decrease_row_created_time(last_row) + + # create 1 more operation to trigger the sync thread + # verify that there are no calls to ODL controller, because the + # first row was not valid (exit_after_run = true) + self._test_thread_processing(odl_const.ODL_UPDATE, + odl_const.ODL_NETWORK, expected_calls=0) + + # validate that all the rows are in 'pending' state + # first row should be set back to 'pending' because it was not valid + rows = db.get_all_db_rows_by_state(self.db_session, 'pending') + self.assertEqual(3, len(rows)) diff --git a/networking-odl/networking_odl/tests/unit/ml2/test_networking_topology.py b/networking-odl/networking_odl/tests/unit/ml2/test_networking_topology.py new file mode 100644 index 0000000..fb83a7b --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/ml2/test_networking_topology.py @@ -0,0 +1,475 @@ +# Copyright (c) 2015-2016 OpenStack Foundation +# 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. + + +from os import path + +import mock +from oslo_log import log +from oslo_serialization import jsonutils +import requests + +from neutron.extensions import portbindings +from neutron.plugins.common import constants +from neutron.plugins.ml2 import driver_api +from neutron.plugins.ml2 import driver_context +from neutron_lib import constants as n_constants + +from networking_odl.common import cache +from networking_odl.ml2 import mech_driver +from networking_odl.ml2 import mech_driver_v2 +from networking_odl.ml2 import network_topology +from networking_odl.tests import base + + +LOG = log.getLogger(__name__) + + +class TestNetworkTopologyManager(base.DietTestCase): + + # pylint: disable=protected-access + + # given valid and invalid segments + valid_segment = { + driver_api.ID: 'API_ID', + driver_api.NETWORK_TYPE: constants.TYPE_LOCAL, + driver_api.SEGMENTATION_ID: 'API_SEGMENTATION_ID', + driver_api.PHYSICAL_NETWORK: 'API_PHYSICAL_NETWORK'} + + invalid_segment = { + driver_api.ID: 'API_ID', + driver_api.NETWORK_TYPE: constants.TYPE_NONE, + driver_api.SEGMENTATION_ID: 'API_SEGMENTATION_ID', + driver_api.PHYSICAL_NETWORK: 'API_PHYSICAL_NETWORK'} + + segments_to_bind = [valid_segment, invalid_segment] + + def setUp(self): + super(TestNetworkTopologyManager, self).setUp() + self.patch(network_topology.LOG, 'isEnabledFor', lambda level: True) + # patch given configuration + self.cfg = mocked_cfg = self.patch(network_topology.client, 'cfg') + mocked_cfg.CONF.ml2_odl.url =\ + 'http://localhost:8181/controller/nb/v2/neutron' + mocked_cfg.CONF.ml2_odl.username = 'admin' + mocked_cfg.CONF.ml2_odl.password = 'admin' + mocked_cfg.CONF.ml2_odl.timeout = 5 + + @mock.patch.object(cache, 'LOG') + @mock.patch.object(network_topology, 'LOG') + def test_fetch_elements_by_host_with_no_entry( + self, network_topology_logger, cache_logger): + given_client = self.mock_client('ovs_topology.json') + self.mock_get_addresses_by_name(['127.0.0.1', '192.168.0.1']) + given_network_topology = network_topology.NetworkTopologyManager( + client=given_client) + + try: + next(given_network_topology._fetch_elements_by_host( + 'some_host_name')) + except ValueError as error: + cache_logger.warning.assert_called_once_with( + 'Error fetching values for keys: %r', + "'some_host_name', '127.0.0.1', '192.168.0.1'", + exc_info=(ValueError, error, mock.ANY)) + network_topology_logger.exception.assert_called_once_with( + 'No such network topology elements for given host ' + '%(host_name)r and given IPs: %(ip_addresses)s.', + {'ip_addresses': '127.0.0.1, 192.168.0.1', + 'host_name': 'some_host_name'}) + else: + self.fail('Expected ValueError being raised.') + + def test_fetch_element_with_ovs_entry(self): + given_client = self.mock_client('ovs_topology.json') + self.mock_get_addresses_by_name(['127.0.0.1', '10.237.214.247']) + given_network_topology = network_topology.NetworkTopologyManager( + client=given_client) + + elements = given_network_topology._fetch_elements_by_host( + 'some_host_name.') + + self.assertEqual([ + {'class': + 'networking_odl.ml2.ovsdb_topology.OvsdbNetworkTopologyElement', + 'has_datapath_type_netdev': False, + 'host_addresses': ['10.237.214.247'], + 'support_vhost_user': False, + 'uuid': 'c4ad780f-8f91-4fa4-804e-dd16beb191e2', + 'valid_vif_types': [portbindings.VIF_TYPE_OVS]}], + [e.to_dict() for e in elements]) + + def test_fetch_elements_with_vhost_user_entry(self): + given_client = self.mock_client('vhostuser_topology.json') + self.mock_get_addresses_by_name(['127.0.0.1', '192.168.66.1']) + given_network_topology = network_topology.NetworkTopologyManager( + client=given_client) + + elements = given_network_topology._fetch_elements_by_host( + 'some_host_name.') + + self.assertEqual([ + {'class': + 'networking_odl.ml2.ovsdb_topology.OvsdbNetworkTopologyElement', + 'has_datapath_type_netdev': True, + 'host_addresses': ['192.168.66.1'], + 'support_vhost_user': True, + 'uuid': 'c805d82d-a5d8-419d-bc89-6e3713ff9f6c', + 'valid_vif_types': [portbindings.VIF_TYPE_VHOST_USER, + portbindings.VIF_TYPE_OVS], + 'port_prefix': 'vhu', + 'vhostuser_socket_dir': '/var/run/openvswitch'}], + [e.to_dict() for e in elements]) + + def mock_get_addresses_by_name(self, ips): + utils = self.patch( + network_topology, 'utils', + mock.Mock( + get_addresses_by_name=mock.Mock(return_value=tuple(ips)))) + return utils.get_addresses_by_name + + def mock_client(self, topology_name=None): + + mocked_client = mock.NonCallableMock( + specs=network_topology.NetworkTopologyClient) + + if topology_name: + cached_file_path = path.join(path.dirname(__file__), topology_name) + + with open(cached_file_path, 'rt') as fd: + topology = jsonutils.loads(str(fd.read()), encoding='utf-8') + + mocked_client.get().json.return_value = topology + + return mocked_client + + def test_bind_port_from_mech_driver_with_ovs(self): + + given_client = self.mock_client('ovs_topology.json') + self.mock_get_addresses_by_name(['127.0.0.1', '10.237.214.247']) + given_network_topology = network_topology.NetworkTopologyManager( + vif_details={'some': 'detail'}, + client=given_client) + self.patch( + network_topology, 'NetworkTopologyManager', + return_value=given_network_topology) + + given_driver = mech_driver.OpenDaylightMechanismDriver() + given_driver.odl_drv = mech_driver.OpenDaylightDriver() + given_port_context = self.given_port_context() + + # when port is bound + given_driver.bind_port(given_port_context) + + # then context binding is setup with returned vif_type and valid + # segment api ID + given_port_context.set_binding.assert_called_once_with( + self.valid_segment[driver_api.ID], portbindings.VIF_TYPE_OVS, + {'some': 'detail'}, status=n_constants.PORT_STATUS_ACTIVE) + + def test_bind_port_from_mech_driver_with_vhostuser(self): + + given_client = self.mock_client('vhostuser_topology.json') + self.mock_get_addresses_by_name(['127.0.0.1', '192.168.66.1']) + given_network_topology = network_topology.NetworkTopologyManager( + vif_details={'some': 'detail'}, + client=given_client) + self.patch( + network_topology, 'NetworkTopologyManager', + return_value=given_network_topology) + + given_driver = mech_driver.OpenDaylightMechanismDriver() + given_driver.odl_drv = mech_driver.OpenDaylightDriver() + given_port_context = self.given_port_context() + + # when port is bound + given_driver.bind_port(given_port_context) + + expected_vif_details = { + 'vhostuser_socket': '/var/run/openvswitch/vhuCURRENT_CON', + 'vhostuser_ovs_plug': True, + 'some': 'detail', + 'vhostuser_mode': 'client'} + + # then context binding is setup with returned vif_type and valid + # segment api ID + given_port_context.set_binding.assert_called_once_with( + self.valid_segment[driver_api.ID], + portbindings.VIF_TYPE_VHOST_USER, + expected_vif_details, status=n_constants.PORT_STATUS_ACTIVE) + + def test_bind_port_from_mech_driver_v2_with_ovs(self): + given_client = self.mock_client('ovs_topology.json') + self.mock_get_addresses_by_name(['127.0.0.1', '10.237.214.247']) + given_network_topology = network_topology.NetworkTopologyManager( + vif_details={'some': 'detail'}, + client=given_client) + self.patch( + network_topology, 'NetworkTopologyManager', + return_value=given_network_topology) + + given_driver = mech_driver_v2.OpenDaylightMechanismDriver() + given_port_context = self.given_port_context() + + given_driver.initialize() + # when port is bound + given_driver.bind_port(given_port_context) + + # then context binding is setup with returned vif_type and valid + # segment api ID + given_port_context.set_binding.assert_called_once_with( + self.valid_segment[driver_api.ID], portbindings.VIF_TYPE_OVS, + {'some': 'detail'}, status=n_constants.PORT_STATUS_ACTIVE) + + def test_bind_port_from_mech_driver_v2_with_vhostuser(self): + given_client = self.mock_client('vhostuser_topology.json') + self.mock_get_addresses_by_name(['127.0.0.1', '192.168.66.1']) + given_network_topology = network_topology.NetworkTopologyManager( + vif_details={'some': 'detail'}, + client=given_client) + self.patch( + network_topology, 'NetworkTopologyManager', + return_value=given_network_topology) + + given_driver = mech_driver_v2.OpenDaylightMechanismDriver() + given_driver._network_topology = given_network_topology + given_port_context = self.given_port_context() + + given_driver.initialize() + # when port is bound + given_driver.bind_port(given_port_context) + + expected_vif_details = { + 'vhostuser_socket': '/var/run/openvswitch/vhuCURRENT_CON', + 'vhostuser_ovs_plug': True, + 'some': 'detail', + 'vhostuser_mode': 'client'} + + # then context binding is setup with returned vif_type and valid + # segment api ID + given_port_context.set_binding.assert_called_once_with( + self.valid_segment[driver_api.ID], + portbindings.VIF_TYPE_VHOST_USER, + expected_vif_details, status=n_constants.PORT_STATUS_ACTIVE) + + def test_bind_port_with_vif_type_ovs(self): + given_topology = self._mock_network_topology( + 'ovs_topology.json', vif_details={'much': 'details'}) + given_port_context = self.given_port_context() + + # when port is bound + given_topology.bind_port(given_port_context) + + # then context binding is setup wit returned vif_type and valid + # segment api ID + given_port_context.set_binding.assert_called_once_with( + self.valid_segment[driver_api.ID], portbindings.VIF_TYPE_OVS, + {'much': 'details'}, status=n_constants.PORT_STATUS_ACTIVE) + + def test_bind_port_with_vif_type_vhost_user(self): + given_topology = self._mock_network_topology( + 'vhostuser_topology.json', vif_details={'much': 'details'}) + given_port_context = self.given_port_context() + + # when port is bound + given_topology.bind_port(given_port_context) + + # then context binding is setup wit returned vif_type and valid + # segment api ID + given_port_context.set_binding.assert_called_once_with( + self.valid_segment[driver_api.ID], + portbindings.VIF_TYPE_VHOST_USER, + {'vhostuser_socket': '/var/run/openvswitch/vhuCURRENT_CON', + 'vhostuser_ovs_plug': True, 'vhostuser_mode': 'client', + 'much': 'details'}, + status=n_constants.PORT_STATUS_ACTIVE) + + @mock.patch.object(network_topology, 'LOG') + def test_bind_port_without_valid_segment(self, logger): + given_topology = self._mock_network_topology('ovs_topology.json') + given_port_context = self.given_port_context( + given_segments=[self.invalid_segment]) + + # when port is bound + given_topology.bind_port(given_port_context) + + self.assertFalse(given_port_context.set_binding.called) + logger.exception.assert_called_once_with( + 'Network topology element has failed binding port:\n%(element)s', + {'element': mock.ANY}) + logger.error.assert_called_once_with( + 'Unable to bind port element for given host and valid VIF types:\n' + '\thostname: %(host_name)s\n' + '\tvalid VIF types: %(valid_vif_types)s', + {'host_name': 'some_host', 'valid_vif_types': 'vhostuser, ovs'}) + + def _mock_network_topology(self, given_topology, vif_details=None): + self.mock_get_addresses_by_name( + ['127.0.0.1', '10.237.214.247', '192.168.66.1']) + return network_topology.NetworkTopologyManager( + client=self.mock_client(given_topology), + vif_details=vif_details) + + def given_port_context(self, given_segments=None): + # given NetworkContext + network = mock.MagicMock(spec=driver_api.NetworkContext) + + if given_segments is None: + given_segments = self.segments_to_bind + + # given port context + return mock.MagicMock( + spec=driver_context.PortContext, + current={'id': 'CURRENT_CONTEXT_ID'}, + host='some_host', + segments_to_bind=given_segments, + network=network, + _new_bound_segment=self.valid_segment) + + NETOWORK_TOPOLOGY_URL =\ + 'http://localhost:8181/'\ + 'restconf/operational/network-topology:network-topology/' + + def mock_request_network_topology(self, file_name): + cached_file_path = path.join( + path.dirname(__file__), file_name + '.json') + + if path.isfile(cached_file_path): + LOG.debug('Loading topology from file: %r', cached_file_path) + with open(cached_file_path, 'rt') as fd: + topology = jsonutils.loads(str(fd.read()), encoding='utf-8') + else: + LOG.debug( + 'Getting topology from ODL: %r', self.NETOWORK_TOPOLOGY_URL) + request = requests.get( + self.NETOWORK_TOPOLOGY_URL, auth=('admin', 'admin'), + headers={'Content-Type': 'application/json'}) + request.raise_for_status() + + with open(cached_file_path, 'wt') as fd: + LOG.debug('Saving topology to file: %r', cached_file_path) + topology = request.json() + jsonutils.dump( + topology, fd, sort_keys=True, indent=4, + separators=(',', ': ')) + + mocked_request = self.patch( + mech_driver.odl_client.requests, 'request', + return_value=mock.MagicMock( + spec=requests.Response, + json=mock.MagicMock(return_value=topology))) + + return mocked_request + + +class TestNetworkTopologyClient(base.DietTestCase): + + given_host = 'given.host' + given_port = 1234 + given_url_with_port = 'http://{}:{}/'.format( + given_host, given_port) + given_url_without_port = 'http://{}/'.format(given_host) + given_username = 'GIVEN_USERNAME' + given_password = 'GIVEN_PASSWORD' + given_timeout = 20 + + def given_client( + self, url=None, username=None, password=None, timeout=None): + return network_topology.NetworkTopologyClient( + url=url or self.given_url_with_port, + username=username or self.given_username, + password=password or self.given_password, + timeout=timeout or self.given_timeout) + + def test_constructor(self): + # When client is created + rest_client = network_topology.NetworkTopologyClient( + url=self.given_url_with_port, + username=self.given_username, + password=self.given_password, + timeout=self.given_timeout) + + self.assertEqual( + self.given_url_with_port + + 'restconf/operational/network-topology:network-topology', + rest_client.url) + self.assertEqual( + (self.given_username, self.given_password), rest_client.auth) + self.assertEqual(self.given_timeout, rest_client.timeout) + + def test_request_with_port(self): + # Given rest client and used 'requests' module + given_client = self.given_client() + mocked_requests_module = self.mocked_requests() + + # When a request is performed + result = given_client.request( + 'GIVEN_METHOD', 'given/path', 'GIVEN_DATA') + + # Then request method is called + mocked_requests_module.request.assert_called_once_with( + 'GIVEN_METHOD', + url='http://given.host:1234/restconf/operational/' + + 'network-topology:network-topology/given/path', + auth=(self.given_username, self.given_password), + data='GIVEN_DATA', headers={'Content-Type': 'application/json'}, + timeout=self.given_timeout) + + # Then request method result is returned + self.assertIs(mocked_requests_module.request.return_value, result) + + def test_request_without_port(self): + # Given rest client and used 'requests' module + given_client = self.given_client(url=self.given_url_without_port) + mocked_requests_module = self.mocked_requests() + + # When a request is performed + result = given_client.request( + 'GIVEN_METHOD', 'given/path', 'GIVEN_DATA') + + # Then request method is called + mocked_requests_module.request.assert_called_once_with( + 'GIVEN_METHOD', + url='http://given.host/restconf/operational/' + + 'network-topology:network-topology/given/path', + auth=(self.given_username, self.given_password), + data='GIVEN_DATA', headers={'Content-Type': 'application/json'}, + timeout=self.given_timeout) + + # Then request method result is returned + self.assertIs(mocked_requests_module.request.return_value, result) + + def test_get(self): + # Given rest client and used 'requests' module + given_client = self.given_client() + mocked_requests_module = self.mocked_requests() + + # When a request is performed + result = given_client.get('given/path', 'GIVEN_DATA') + + # Then request method is called + mocked_requests_module.request.assert_called_once_with( + 'get', + url='http://given.host:1234/restconf/operational/' + + 'network-topology:network-topology/given/path', + auth=(self.given_username, self.given_password), + data='GIVEN_DATA', headers={'Content-Type': 'application/json'}, + timeout=self.given_timeout) + + # Then request method result is returned + self.assertIs(mocked_requests_module.request.return_value, result) + + def mocked_requests(self): + return self.patch(network_topology.client, 'requests') diff --git a/networking-odl/networking_odl/tests/unit/ml2/test_ovsdb_topology.py b/networking-odl/networking_odl/tests/unit/ml2/test_ovsdb_topology.py new file mode 100644 index 0000000..228154d --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/ml2/test_ovsdb_topology.py @@ -0,0 +1,248 @@ +# Copyright (c) 2015 OpenStack Foundation +# 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. + +from os import path + +import mock +from oslo_log import log +from oslo_serialization import jsonutils + +from neutron.extensions import portbindings +from neutron.plugins.common import constants +from neutron.plugins.ml2 import driver_api +from neutron.plugins.ml2 import driver_context +from neutron_lib import constants as n_constants + +from networking_odl.ml2 import ovsdb_topology +from networking_odl.tests import base + + +LOG = log.getLogger(__name__) + + +class TestOvsdbTopologyParser(base.DietTestCase): + + def test_parse_network_topology_ovs(self): + given_parser = ovsdb_topology.OvsdbNetworkTopologyParser() + given_topology = self.load_network_topology('ovs_topology.json') + + # when parse topology + elements = list(given_parser.parse_network_topology(given_topology)) + + # then parser yields one element supporting only OVS vif type + self.assertEqual( + [{'class': + 'networking_odl.ml2.ovsdb_topology.OvsdbNetworkTopologyElement', + 'has_datapath_type_netdev': False, + 'host_addresses': ['10.237.214.247'], + 'support_vhost_user': False, + 'uuid': 'c4ad780f-8f91-4fa4-804e-dd16beb191e2', + 'valid_vif_types': [portbindings.VIF_TYPE_OVS]}], + [e.to_dict() for e in elements]) + + def test_parse_network_topology_vhostuser(self): + given_parser = ovsdb_topology.OvsdbNetworkTopologyParser() + given_topology = self.load_network_topology('vhostuser_topology.json') + + # when parse topology + elements = list(given_parser.parse_network_topology(given_topology)) + + # then parser yields one element supporting VHOSTUSER and OVS vif types + self.assertEqual( + [{'class': + 'networking_odl.ml2.ovsdb_topology.OvsdbNetworkTopologyElement', + 'has_datapath_type_netdev': True, + 'host_addresses': ['192.168.66.1'], + 'port_prefix': 'vhu', + 'support_vhost_user': True, + 'uuid': 'c805d82d-a5d8-419d-bc89-6e3713ff9f6c', + 'valid_vif_types': [portbindings.VIF_TYPE_VHOST_USER, + portbindings.VIF_TYPE_OVS], + 'vhostuser_socket_dir': '/var/run/openvswitch'}], + [e.to_dict() for e in elements]) + + def load_network_topology(self, file_name): + file_path = path.join(path.dirname(__file__), file_name) + LOG.debug('Loading topology from file: %r', file_path) + with open(file_path, 'rt') as fd: + return jsonutils.loads(str(fd.read()), encoding='utf-8') + + +class TestOvsdbNetworkingTopologyElement(base.DietTestCase): + + # given valid and invalid segments + VALID_SEGMENT = { + driver_api.ID: 'API_ID', + driver_api.NETWORK_TYPE: constants.TYPE_LOCAL, + driver_api.SEGMENTATION_ID: 'API_SEGMENTATION_ID', + driver_api.PHYSICAL_NETWORK: 'API_PHYSICAL_NETWORK'} + + INVALID_SEGMENT = { + driver_api.ID: 'API_ID', + driver_api.NETWORK_TYPE: constants.TYPE_NONE, + driver_api.SEGMENTATION_ID: 'API_SEGMENTATION_ID', + driver_api.PHYSICAL_NETWORK: 'API_PHYSICAL_NETWORK'} + + segments_to_bind = [INVALID_SEGMENT, VALID_SEGMENT] + + def given_element(self, uuid='some_uuid', **kwargs): + return ovsdb_topology.OvsdbNetworkTopologyElement(uuid=uuid, **kwargs) + + def test_valid_vif_types_with_no_positive_value(self): + given_element = self.given_element( + has_datapath_type_netdev=False, support_vhost_user=False) + valid_vif_types = given_element.valid_vif_types + self.assertEqual([portbindings.VIF_TYPE_OVS], valid_vif_types) + + def test_valid_vif_types_with_datapath_type_netdev(self): + given_element = self.given_element( + has_datapath_type_netdev=True, support_vhost_user=False) + valid_vif_types = given_element.valid_vif_types + self.assertEqual([portbindings.VIF_TYPE_OVS], valid_vif_types) + + def test_valid_vif_types_with_support_vhost_user(self): + given_element = self.given_element( + has_datapath_type_netdev=False, support_vhost_user=True) + valid_vif_types = given_element.valid_vif_types + self.assertEqual([portbindings.VIF_TYPE_OVS], valid_vif_types) + + def test_valid_vif_types_with_all_positive_values(self): + given_element = self.given_element( + has_datapath_type_netdev=True, support_vhost_user=True) + valid_vif_types = given_element.valid_vif_types + self.assertEqual( + [portbindings.VIF_TYPE_VHOST_USER, portbindings.VIF_TYPE_OVS], + valid_vif_types) + + def test_to_json_ovs(self): + given_element = self.given_element( + has_datapath_type_netdev=False, support_vhost_user=True, + remote_ip='192.168.99.33') + json = given_element.to_json() + self.assertEqual( + {'class': + 'networking_odl.ml2.ovsdb_topology.OvsdbNetworkTopologyElement', + 'uuid': 'some_uuid', + 'host_addresses': ['192.168.99.33'], + 'has_datapath_type_netdev': False, + 'support_vhost_user': True, + 'valid_vif_types': [portbindings.VIF_TYPE_OVS]}, + jsonutils.loads(json)) + + def test_to_json_vhost_user(self): + given_element = self.given_element( + has_datapath_type_netdev=True, support_vhost_user=True, + remote_ip='192.168.99.66') + json = given_element.to_json() + self.assertEqual( + {'class': + 'networking_odl.ml2.ovsdb_topology.OvsdbNetworkTopologyElement', + 'uuid': 'some_uuid', + 'host_addresses': ['192.168.99.66'], + 'has_datapath_type_netdev': True, + 'support_vhost_user': True, + 'valid_vif_types': + [portbindings.VIF_TYPE_VHOST_USER, portbindings.VIF_TYPE_OVS], + 'port_prefix': 'vhu', + 'vhostuser_socket_dir': '/var/run/openvswitch'}, + jsonutils.loads(json)) + + def test_set_attr_with_invalid_name(self): + element = self.given_element() + self.assertRaises( + AttributeError, lambda: setattr(element, 'invalid_attribute', 10)) + + def test_is_valid_segment(self): + """Validate the _check_segment method.""" + + # given driver and all network types + given_element = self.given_element( + has_datapath_type_netdev=True, support_vhost_user=True, + remote_ip='192.168.99.66') + all_network_types = [constants.TYPE_FLAT, constants.TYPE_GRE, + constants.TYPE_LOCAL, constants.TYPE_VXLAN, + constants.TYPE_VLAN, constants.TYPE_NONE] + + # when checking segments network type + valid_types = { + network_type + for network_type in all_network_types + if given_element._is_valid_segment( + {driver_api.NETWORK_TYPE: network_type})} + + # then true is returned only for valid network types + self.assertEqual({ + constants.TYPE_LOCAL, constants.TYPE_GRE, constants.TYPE_VXLAN, + constants.TYPE_VLAN}, valid_types) + + def test_bind_port_with_vif_type_ovs(self): + given_port_context = self.given_port_context( + given_segments=[self.INVALID_SEGMENT, self.VALID_SEGMENT]) + given_element = self.given_element('some_uuid') + + # When bind port + given_element.bind_port( + port_context=given_port_context, + vif_type=portbindings.VIF_TYPE_OVS, + vif_details={'some_details': None}) + + given_port_context.set_binding.assert_called_once_with( + self.VALID_SEGMENT[driver_api.ID], portbindings.VIF_TYPE_OVS, + {'some_details': None}, status=n_constants.PORT_STATUS_ACTIVE) + + def test_bind_port_with_vif_type_vhost_user(self): + given_port_context = self.given_port_context( + given_segments=[self.INVALID_SEGMENT, self.VALID_SEGMENT]) + given_element = self.given_element('some_uuid') + + # When bind port + given_element.bind_port( + port_context=given_port_context, + vif_type=portbindings.VIF_TYPE_VHOST_USER, + vif_details={'some_details': None}) + + given_port_context.set_binding.assert_called_once_with( + self.VALID_SEGMENT[driver_api.ID], + portbindings.VIF_TYPE_VHOST_USER, + {'vhostuser_socket': '/var/run/openvswitch/vhuCURRENT_CON', + 'some_details': None, 'vhostuser_ovs_plug': True, + 'vhostuser_mode': 'client'}, + status=n_constants.PORT_STATUS_ACTIVE) + + @mock.patch.object(ovsdb_topology, 'LOG') + def test_bind_port_without_valid_segment(self, logger): + given_port_context = self.given_port_context( + given_segments=[self.INVALID_SEGMENT]) + given_element = self.given_element('some_uuid') + + # when port is bound + self.assertRaises( + ValueError, lambda: given_element.bind_port( + port_context=given_port_context, + vif_type=portbindings.VIF_TYPE_OVS, + vif_details={'some_details': None})) + + self.assertFalse(given_port_context.set_binding.called) + + def given_port_context(self, given_segments): + # given NetworkContext + network = mock.MagicMock(spec=driver_api.NetworkContext) + + # given port context + return mock.MagicMock( + spec=driver_context.PortContext, + current={'id': 'CURRENT_CONTEXT_ID'}, + segments_to_bind=given_segments, + network=network) diff --git a/networking-odl/networking_odl/tests/unit/ml2/test_port_binding.py b/networking-odl/networking_odl/tests/unit/ml2/test_port_binding.py new file mode 100644 index 0000000..35ae9ec --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/ml2/test_port_binding.py @@ -0,0 +1,44 @@ +# Copyright (c) 2016 OpenStack Foundation +# 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. + +import mock + +from networking_odl.ml2 import legacy_port_binding +from networking_odl.ml2 import port_binding +from networking_odl.tests import base + + +class TestPortBindingManager(base.DietTestCase): + + def test_create(self): + mgr = port_binding.PortBindingManager.create( + name="legacy-port-binding") + self.assertEqual("legacy-port-binding", mgr.name) + self.assertIsInstance(mgr.controller, + legacy_port_binding.LegacyPortBindingManager) + + def test_create_with_nonexist_name(self): + self.assertRaises(AssertionError, + port_binding.PortBindingManager.create, + name="nonexist-port-binding") + + @mock.patch.object(legacy_port_binding.LegacyPortBindingManager, + "bind_port") + def test_bind_port(self, mock_method): + port_context = mock.Mock() + mgr = port_binding.PortBindingManager.create( + name="legacy-port-binding") + mgr.controller.bind_port(port_context) + mock_method.assert_called_once_with(port_context) diff --git a/networking-odl/networking_odl/tests/unit/ml2/test_pseudo_agentdb_binding.py b/networking-odl/networking_odl/tests/unit/ml2/test_pseudo_agentdb_binding.py new file mode 100644 index 0000000..d69150c --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/ml2/test_pseudo_agentdb_binding.py @@ -0,0 +1,334 @@ +# Copyright (c) 2016 OpenStack Foundation +# 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. + +from copy import deepcopy +import mock +from os import path as os_path +from string import Template + +from neutron.extensions import portbindings +from neutron.plugins.common import constants +from neutron.plugins.ml2 import config +from neutron.plugins.ml2 import driver_api as api +from neutron.plugins.ml2 import driver_context as ctx +from neutron_lib import constants as n_const + +from networking_odl.ml2 import pseudo_agentdb_binding +from networking_odl.tests import base + +AGENTDB_BINARY = 'neutron-odlagent-portbinding' +L2_TYPE = "ODL L2" + + +class TestPseudoAgentDBBindingController(base.DietTestCase): + """Test class for AgentDBPortBinding.""" + + # test data hostconfig and hostconfig-dbget + sample_odl_hconfigs = {"hostconfigs": {"hostconfig": [ + {"host-id": "devstack", + "host-type": "ODL L2", + "config": """{"supported_vnic_types": [ + {"vnic_type": "normal", "vif_type": "ovs", + "vif_details": {}}], + "allowed_network_types": [ + "local", "vlan", "vxlan", "gre"], + "bridge_mappings": {"physnet1": "br-ex"}}"""} + ]}} + + # Test data for string interpolation of substitutable identifers + # e.g. $PORT_ID identifier in the configurations JSON string below shall + # be substituted with portcontext.current['id'] eliminating the check + # for specific vif_type making port-binding truly switch agnostic. + # Refer: Python string templates and interpolation (string.Template) + sample_hconf_str_tmpl_subs_vpp = { + "host": "devstack", # host-id in ODL JSON + "agent_type": "ODL L2", # host-type in ODL JSON + # config in ODL JSON + "configurations": """{"supported_vnic_types": [ + {"vnic_type": "normal", "vif_type": "vhostuser", + "vif_details": { + "uuid": "TEST_UUID", + "has_datapath_type_netdev": true, + "support_vhost_user": true, + "port_prefix": "socket_", + "vhostuser_socket_dir": "/tmp", + "vhostuser_ovs_plug": true, + "vhostuser_mode": "server", + "vhostuser_socket": + "/tmp/socket_$PORT_ID" + }}], + "allowed_network_types": [ + "local", "vlan", "vxlan", "gre"], + "bridge_mappings": {"physnet1": "br-ex"}}""" + } + + sample_hconf_str_tmpl_subs_ovs = { + "host": "devstack", # host-id in ODL JSON + "agent_type": "ODL L2", # host-type in ODL JSON + # config in ODL JSON + "configurations": """{"supported_vnic_types": [ + {"vnic_type": "normal", "vif_type": "vhostuser", + "vif_details": { + "uuid": "TEST_UUID", + "has_datapath_type_netdev": true, + "support_vhost_user": true, + "port_prefix": "vhu_", + "vhostuser_socket_dir": "/var/run/openvswitch", + "vhostuser_ovs_plug": true, + "vhostuser_mode": "client", + "vhostuser_socket": + "/var/run/openvswitch/vhu_$PORT_ID" + }}], + "allowed_network_types": [ + "local", "vlan", "vxlan", "gre"], + "bridge_mappings": {"physnet1": "br-ex"}}""" + } + + sample_hconf_str_tmpl_nosubs = { + "host": "devstack", # host-id in ODL JSON + "agent_type": "ODL L2", # host-type in ODL JSON + # config in ODL JSON + "configurations": """{"supported_vnic_types": [ + {"vnic_type": "normal", "vif_type": "ovs", + "vif_details": { + "uuid": "TEST_UUID", + "has_datapath_type_netdev": true, + "support_vhost_user": true, + "port_prefix": "socket_", + "vhostuser_socket_dir": "/tmp", + "vhostuser_ovs_plug": true, + "vhostuser_mode": "server", + "vhostuser_socket": + "/var/run/openvswitch/PORT_NOSUBS" + }}], + "allowed_network_types": [ + "local", "vlan", "vxlan", "gre"], + "bridge_mappings": {"physnet1": "br-ex"}}""" + } + + # Test data for vanilla OVS + sample_hconfig_dbget_ovs = {"configurations": {"supported_vnic_types": [ + {"vnic_type": "normal", "vif_type": portbindings.VIF_TYPE_OVS, + "vif_details": { + "some_test_details": None + }}], + "allowed_network_types": ["local", "vlan", "vxlan", "gre"], + "bridge_mappings": {"physnet1": "br-ex"}}} + + # Test data for OVS-DPDK + sample_hconfig_dbget_ovs_dpdk = {"configurations": { + "supported_vnic_types": [{ + "vnic_type": "normal", + "vif_type": portbindings.VIF_TYPE_VHOST_USER, + "vif_details": { + "uuid": "TEST_UUID", + "has_datapath_type_netdev": True, + "support_vhost_user": True, + "port_prefix": "vhu_", + # Assumption: /var/run mounted as tmpfs + "vhostuser_socket_dir": "/var/run/openvswitch", + "vhostuser_ovs_plug": True, + "vhostuser_mode": "client", + "vhostuser_socket": "/var/run/openvswitch/vhu_$PORT_ID"}}], + "allowed_network_types": ["local", "vlan", "vxlan", "gre"], + "bridge_mappings": {"physnet1": "br-ex"}}} + + # Test data for VPP + sample_hconfig_dbget_vpp = {"configurations": {"supported_vnic_types": [ + {"vnic_type": "normal", "vif_type": portbindings.VIF_TYPE_VHOST_USER, + "vif_details": { + "uuid": "TEST_UUID", + "has_datapath_type_netdev": True, + "support_vhost_user": True, + "port_prefix": "socket_", + "vhostuser_socket_dir": "/tmp", + "vhostuser_ovs_plug": True, + "vhostuser_mode": "server", + "vhostuser_socket": "/tmp/socket_$PORT_ID" + }}], + "allowed_network_types": ["local", "vlan", "vxlan", "gre"], + "bridge_mappings": {"physnet1": "br-ex"}}} + + # test data valid and invalid segments + test_valid_segment = { + api.ID: 'API_ID', + api.NETWORK_TYPE: constants.TYPE_LOCAL, + api.SEGMENTATION_ID: 'API_SEGMENTATION_ID', + api.PHYSICAL_NETWORK: 'API_PHYSICAL_NETWORK'} + + test_invalid_segment = { + api.ID: 'API_ID', + api.NETWORK_TYPE: constants.TYPE_NONE, + api.SEGMENTATION_ID: 'API_SEGMENTATION_ID', + api.PHYSICAL_NETWORK: 'API_PHYSICAL_NETWORK'} + + def setUp(self): + """Setup test.""" + super(TestPseudoAgentDBBindingController, self).setUp() + + config.cfg.CONF.set_override('url', + 'http://localhost:8080' + '/controller/nb/v2/neutron', 'ml2_odl') + + fake_agents_db = mock.MagicMock() + fake_agents_db.create_or_update_agent = mock.MagicMock() + + self.mgr = pseudo_agentdb_binding.PseudoAgentDBBindingController( + db_plugin=fake_agents_db) + + def test_make_hostconf_uri(self): + """test make uri.""" + test_path = '/restconf/neutron:neutron/hostconfigs' + expected = "http://localhost:8080/restconf/neutron:neutron/hostconfigs" + test_uri = self.mgr._make_hostconf_uri(path=test_path) + + self.assertEqual(expected, test_uri) + + def test_update_agents_db(self): + """test agent update.""" + self.mgr._update_agents_db( + hostconfigs=self.sample_odl_hconfigs['hostconfigs']['hostconfig']) + self.mgr.agents_db.create_or_update_agent.assert_called_once() + + def test_is_valid_segment(self): + """Validate the _check_segment method.""" + all_network_types = [constants.TYPE_FLAT, constants.TYPE_GRE, + constants.TYPE_LOCAL, constants.TYPE_VXLAN, + constants.TYPE_VLAN, constants.TYPE_NONE] + + valid_types = { + network_type + for network_type in all_network_types + if self.mgr._is_valid_segment({api.NETWORK_TYPE: network_type}, { + 'allowed_network_types': [ + constants.TYPE_LOCAL, constants.TYPE_GRE, + constants.TYPE_VXLAN, constants.TYPE_VLAN]})} + + self.assertEqual({ + constants.TYPE_LOCAL, constants.TYPE_GRE, constants.TYPE_VXLAN, + constants.TYPE_VLAN}, valid_types) + + def test_bind_port_with_vif_type_ovs(self): + """test bind_port with vanilla ovs.""" + port_context = self._fake_port_context( + fake_segments=[self.test_invalid_segment, self.test_valid_segment]) + + vif_type = portbindings.VIF_TYPE_OVS + vif_details = {'some_test_details': None} + + self.mgr._hconfig_bind_port( + port_context, self.sample_hconfig_dbget_ovs) + + port_context.set_binding.assert_called_once_with( + self.test_valid_segment[api.ID], vif_type, + vif_details, status=n_const.PORT_STATUS_ACTIVE) + + def _set_pass_vif_details(self, port_context, vif_details): + """extract vif_details and update vif_details if needed.""" + vhostuser_socket_dir = vif_details.get( + 'vhostuser_socket_dir', '/var/run/openvswitch') + port_spec = vif_details.get( + 'port_prefix', 'vhu_') + port_context.current['id'] + socket_path = os_path.join(vhostuser_socket_dir, port_spec) + vif_details.update({portbindings.VHOST_USER_SOCKET: socket_path}) + + return vif_details + + def test_bind_port_with_vif_type_vhost_user(self): + """test bind_port with ovs-dpdk.""" + port_context = self._fake_port_context( + fake_segments=[self.test_invalid_segment, self.test_valid_segment], + host_agents=[deepcopy(self.sample_hconf_str_tmpl_subs_ovs)]) + + self.mgr.bind_port(port_context) + + pass_vif_type = portbindings.VIF_TYPE_VHOST_USER + pass_vif_details = self.sample_hconfig_dbget_ovs_dpdk[ + 'configurations']['supported_vnic_types'][0]['vif_details'] + self._set_pass_vif_details(port_context, pass_vif_details) + + port_context.set_binding.assert_called_once_with( + self.test_valid_segment[api.ID], pass_vif_type, + pass_vif_details, status=n_const.PORT_STATUS_ACTIVE) + + def test_bind_port_with_vif_type_vhost_user_vpp(self): + """test bind_port with vpp.""" + port_context = self._fake_port_context( + fake_segments=[self.test_invalid_segment, self.test_valid_segment], + host_agents=[deepcopy(self.sample_hconf_str_tmpl_subs_vpp)]) + + self.mgr.bind_port(port_context) + + pass_vif_type = portbindings.VIF_TYPE_VHOST_USER + pass_vif_details = self.sample_hconfig_dbget_vpp['configurations'][ + 'supported_vnic_types'][0]['vif_details'] + self._set_pass_vif_details(port_context, pass_vif_details) + + port_context.set_binding.assert_called_once_with( + self.test_valid_segment[api.ID], pass_vif_type, + pass_vif_details, status=n_const.PORT_STATUS_ACTIVE) + + def test_bind_port_without_valid_segment(self): + """test bind_port without a valid segment.""" + port_context = self._fake_port_context( + fake_segments=[self.test_invalid_segment]) + + self.mgr._hconfig_bind_port( + port_context, self.sample_hconfig_dbget_ovs) + + port_context.set_binding.assert_not_called() + + def test_no_str_template_substitution_in_configuration_string(self): + """Test for no identifier substituion in config JSON string.""" + port_context = self._fake_port_context( + fake_segments=[self.test_invalid_segment, self.test_valid_segment]) + + hconf_dict = self.mgr._substitute_hconfig_tmpl( + port_context, self.sample_hconf_str_tmpl_nosubs) + + test_string = hconf_dict['configurations'][ + 'supported_vnic_types'][0][ + 'vif_details'][portbindings.VHOST_USER_SOCKET] + + expected_str = '/var/run/openvswitch/PORT_NOSUBS' + + self.assertEqual(expected_str, test_string) + + def test_str_template_substitution_in_configuration_string(self): + """Test for identifier substitution in config JSON string.""" + port_context = self._fake_port_context( + fake_segments=[self.test_invalid_segment, self.test_valid_segment]) + + hconf_dict = self.mgr._substitute_hconfig_tmpl( + port_context, self.sample_hconf_str_tmpl_subs_vpp) + + test_string = hconf_dict['configurations'][ + 'supported_vnic_types'][0][ + 'vif_details'][portbindings.VHOST_USER_SOCKET] + + expected_str = Template('/tmp/socket_$PORT_ID') + expected_str = expected_str.safe_substitute({ + 'PORT_ID': port_context.current['id']}) + + self.assertEqual(expected_str, test_string) + + def _fake_port_context(self, fake_segments, host_agents=None): + network = mock.MagicMock(spec=api.NetworkContext) + return mock.MagicMock( + spec=ctx.PortContext, + current={'id': 'CONTEXT_ID', + portbindings.VNIC_TYPE: portbindings.VNIC_NORMAL}, + segments_to_bind=fake_segments, network=network, + host_agents=lambda agent_type: host_agents) diff --git a/networking-odl/networking_odl/tests/unit/ml2/vhostuser_topology.json b/networking-odl/networking_odl/tests/unit/ml2/vhostuser_topology.json new file mode 100644 index 0000000..5d6b994 --- /dev/null +++ b/networking-odl/networking_odl/tests/unit/ml2/vhostuser_topology.json @@ -0,0 +1,182 @@ +{ + "network-topology": { + "topology": [ + { + "topology-id": "flow:1" + }, + { + "node": [ + { + "node-id": "ovsdb://uuid/c805d82d-a5d8-419d-bc89-6e3713ff9f6c/bridge/br-int", + "ovsdb:bridge-external-ids": [ + { + "bridge-external-id-key": "opendaylight-iid", + "bridge-external-id-value": "/network-topology:network-topology/network-topology:topology[network-topology:topology-id='ovsdb:1']/network-topology:node[network-topology:node-id='ovsdb://uuid/c805d82d-a5d8-419d-bc89-6e3713ff9f6c/bridge/br-int']" + } + ], + "ovsdb:bridge-name": "br-int", + "ovsdb:bridge-uuid": "e92ec02d-dba8-46d8-8047-680cab5ee8b0", + "ovsdb:controller-entry": [ + { + "controller-uuid": "8521e6df-54bd-48ac-a249-3bb810fd812c", + "is-connected": false, + "target": "tcp:192.168.66.1:6653" + } + ], + "ovsdb:datapath-type": "ovsdb:datapath-type-netdev", + "ovsdb:fail-mode": "ovsdb:ovsdb-fail-mode-secure", + "ovsdb:managed-by": "/network-topology:network-topology/network-topology:topology[network-topology:topology-id='ovsdb:1']/network-topology:node[network-topology:node-id='ovsdb://uuid/c805d82d-a5d8-419d-bc89-6e3713ff9f6c']", + "ovsdb:protocol-entry": [ + { + "protocol": "ovsdb:ovsdb-bridge-protocol-openflow-13" + } + ], + "termination-point": [ + { + "ovsdb:interface-type": "ovsdb:interface-type-internal", + "ovsdb:interface-uuid": "d21472db-5c3c-4b38-bf18-6ed3a32edff1", + "ovsdb:name": "br-int", + "ovsdb:port-uuid": "30adf59e-ff0d-478f-b37a-e37ea20dddd3", + "tp-id": "br-int" + } + ] + }, + { + "node-id": "ovsdb://uuid/c805d82d-a5d8-419d-bc89-6e3713ff9f6c/bridge/br-nian1_1", + "ovsdb:bridge-name": "br-nian1_1", + "ovsdb:bridge-uuid": "243e01cb-e413-4615-a044-b254141e407d", + "ovsdb:datapath-id": "00:00:ca:01:3e:24:15:46", + "ovsdb:datapath-type": "ovsdb:datapath-type-netdev", + "ovsdb:managed-by": "/network-topology:network-topology/network-topology:topology[network-topology:topology-id='ovsdb:1']/network-topology:node[network-topology:node-id='ovsdb://uuid/c805d82d-a5d8-419d-bc89-6e3713ff9f6c']", + "termination-point": [ + { + "ovsdb:interface-type": "ovsdb:interface-type-internal", + "ovsdb:interface-uuid": "45184fd2-31eb-4c87-a071-2d64a0893662", + "ovsdb:name": "br-nian1_1", + "ovsdb:ofport": 65534, + "ovsdb:port-uuid": "f5952c1b-6b6d-4fd2-b2cd-201b8c9e0779", + "tp-id": "br-nian1_1" + } + ] + }, + { + "node-id": "ovsdb://uuid/c805d82d-a5d8-419d-bc89-6e3713ff9f6c/bridge/br-ex", + "ovsdb:bridge-external-ids": [ + { + "bridge-external-id-key": "bridge-id", + "bridge-external-id-value": "br-ex" + } + ], + "ovsdb:bridge-name": "br-ex", + "ovsdb:bridge-other-configs": [ + { + "bridge-other-config-key": "disable-in-band", + "bridge-other-config-value": "true" + } + ], + "ovsdb:bridge-uuid": "43f7768e-c2f9-4ae7-8099-8aee5a17add7", + "ovsdb:datapath-id": "00:00:8e:76:f7:43:e7:4a", + "ovsdb:datapath-type": "ovsdb:datapath-type-netdev", + "ovsdb:managed-by": "/network-topology:network-topology/network-topology:topology[network-topology:topology-id='ovsdb:1']/network-topology:node[network-topology:node-id='ovsdb://uuid/c805d82d-a5d8-419d-bc89-6e3713ff9f6c']", + "termination-point": [ + { + "ovsdb:interface-type": "ovsdb:interface-type-internal", + "ovsdb:interface-uuid": "bdec1830-e6a5-4476-adff-569c455adb33", + "ovsdb:name": "br-ex", + "ovsdb:ofport": 65534, + "ovsdb:port-uuid": "7ba5939b-ff13-409d-86de-67556021ddff", + "tp-id": "br-ex" + } + ] + }, + { + "node-id": "ovsdb://uuid/c805d82d-a5d8-419d-bc89-6e3713ff9f6c", + "ovsdb:connection-info": { + "local-ip": "192.168.66.1", + "local-port": 6640, + "remote-ip": "192.168.66.1", + "remote-port": 41817 + }, + "ovsdb:datapath-type-entry": [ + { + "datapath-type": "ovsdb:datapath-type-netdev" + }, + { + "datapath-type": "ovsdb:datapath-type-system" + } + ], + "ovsdb:interface-type-entry": [ + { + "interface-type": "ovsdb:interface-type-ipsec-gre" + }, + { + "interface-type": "ovsdb:interface-type-gre" + }, + { + "interface-type": "ovsdb:interface-type-gre64" + }, + { + "interface-type": "ovsdb:interface-type-dpdkr" + }, + { + "interface-type": "ovsdb:interface-type-vxlan" + }, + { + "interface-type": "ovsdb:interface-type-dpdkvhostuser" + }, + { + "interface-type": "ovsdb:interface-type-tap" + }, + { + "interface-type": "ovsdb:interface-type-geneve" + }, + { + "interface-type": "ovsdb:interface-type-dpdk" + }, + { + "interface-type": "ovsdb:interface-type-internal" + }, + { + "interface-type": "ovsdb:interface-type-system" + }, + { + "interface-type": "ovsdb:interface-type-lisp" + }, + { + "interface-type": "ovsdb:interface-type-patch" + }, + { + "interface-type": "ovsdb:interface-type-ipsec-gre64" + }, + { + "interface-type": "ovsdb:interface-type-stt" + } + ], + "ovsdb:managed-node-entry": [ + { + "bridge-ref": "/network-topology:network-topology/network-topology:topology[network-topology:topology-id='ovsdb:1']/network-topology:node[network-topology:node-id='ovsdb://uuid/c805d82d-a5d8-419d-bc89-6e3713ff9f6c/bridge/br-ex']" + }, + { + "bridge-ref": "/network-topology:network-topology/network-topology:topology[network-topology:topology-id='ovsdb:1']/network-topology:node[network-topology:node-id='ovsdb://uuid/c805d82d-a5d8-419d-bc89-6e3713ff9f6c/bridge/br-int']" + }, + { + "bridge-ref": "/network-topology:network-topology/network-topology:topology[network-topology:topology-id='ovsdb:1']/network-topology:node[network-topology:node-id='ovsdb://uuid/c805d82d-a5d8-419d-bc89-6e3713ff9f6c/bridge/br-nian1_1']" + } + ], + "ovsdb:openvswitch-other-configs": [ + { + "other-config-key": "local_ip", + "other-config-value": "192.168.66.1" + }, + { + "other-config-key": "pmd-cpu-mask", + "other-config-value": "400004" + } + ] + } + ], + "topology-id": "ovsdb:1" + } + ] + } +} |