summaryrefslogtreecommitdiffstats
path: root/networking-odl/networking_odl/tests/unit/ml2
diff options
context:
space:
mode:
Diffstat (limited to 'networking-odl/networking_odl/tests/unit/ml2')
-rw-r--r--networking-odl/networking_odl/tests/unit/ml2/__init__.py0
-rwxr-xr-xnetworking-odl/networking_odl/tests/unit/ml2/config-ovs-external_ids.sh37
-rw-r--r--networking-odl/networking_odl/tests/unit/ml2/odl_teststub.js62
-rw-r--r--networking-odl/networking_odl/tests/unit/ml2/ovs_topology.json171
-rw-r--r--networking-odl/networking_odl/tests/unit/ml2/test_driver.py99
-rw-r--r--networking-odl/networking_odl/tests/unit/ml2/test_legacy_port_binding.py89
-rw-r--r--networking-odl/networking_odl/tests/unit/ml2/test_mechanism_odl.py596
-rw-r--r--networking-odl/networking_odl/tests/unit/ml2/test_mechanism_odl_v2.py577
-rw-r--r--networking-odl/networking_odl/tests/unit/ml2/test_networking_topology.py475
-rw-r--r--networking-odl/networking_odl/tests/unit/ml2/test_ovsdb_topology.py248
-rw-r--r--networking-odl/networking_odl/tests/unit/ml2/test_port_binding.py44
-rw-r--r--networking-odl/networking_odl/tests/unit/ml2/test_pseudo_agentdb_binding.py334
-rw-r--r--networking-odl/networking_odl/tests/unit/ml2/vhostuser_topology.json182
13 files changed, 2914 insertions, 0 deletions
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"
+ }
+ ]
+ }
+}