From f2f34e6fee1d7326b2c700f89bd0943736b9aef6 Mon Sep 17 00:00:00 2001 From: "Dale W. Carder" Date: Tue, 9 Jul 2013 11:14:10 -0500 Subject: [PATCH] Add initial files to repo new file: LICENSE.txt new file: OpenDaylight.py new file: test-OpenDaylight.py --- LICENSE.txt | 202 +++++++++++++++++++++++ OpenDaylight.py | 369 +++++++++++++++++++++++++++++++++++++++++++ test-OpenDaylight.py | 293 ++++++++++++++++++++++++++++++++++ 3 files changed, 864 insertions(+) create mode 100644 LICENSE.txt create mode 100644 OpenDaylight.py create mode 100755 test-OpenDaylight.py diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/OpenDaylight.py b/OpenDaylight.py new file mode 100644 index 0000000..d6e9e1c --- /dev/null +++ b/OpenDaylight.py @@ -0,0 +1,369 @@ +""" +OpenDaylight REST API + +Copyright 2013 The University of Wisconsin Board of Regents + + 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. + + + Written by: Dale W. Carder, dwcarder@wisc.edu + Network Services Group + Division of Information Technology + University of Wisconsin at Madison + +This material is based upon work supported by the National Science Foundation +under Grant No. 1247322. +""" + +from __future__ import print_function +import json +import requests +from requests.auth import HTTPBasicAuth + +class OpenDaylight(object): + """An object holding details to talk to the OpenDaylight REST API + + OpenDaylight.setup is a dictionary loaded with the following + default values: + {'hostname':'localhost', + 'port':'8080', + 'username':'admin', + 'password':'admin', + 'path':'/controller/nb/v2/', + 'container':'default', + 'http':'http://' } + + Your code should change these as required for your installation. + OpenDaylight.url holds the url for each REST query. Typically + you would let OpenDaylight.prepare() build this for you. + + OpenDaylight.auth holds an auth object for Requests to use + for each REST query. Typically you would also let + OpenDaylight.prepare() build this for you. + """ + + def __init__(self): + """Set some mostly reasonable defaults. + """ + self.setup = {'hostname':'localhost', + 'port':'8080', + 'username':'admin', + 'password':'admin', + 'path':'/controller/nb/v2/', + 'container':'default', + 'http':'http://'} + + self._base_url = None + self.url = None + self.auth = None + + def prepare(self, app, path): + """Sets up the necessary details for the REST connection by calling + prepare_url and prepare_auth. + + Arguments: + 'app' - which OpenDaylight northbound api component (application) + we want to talk to. + 'path' - the specific rest query for the application. + """ + self.prepare_url(app, path) + self.prepare_auth() + + def prepare_url(self, app, path): + """Build the URL for this REST connection which is then stored as + OpenDaylight.url + + If you use prepare(), you shouldn't need to call prepare_url() + yourself. However, if there were a URL you wanted to construct that + was so whacked out custom, then by all means build it yourself and don't + bother to call this function. + + Arguments: + 'app' - which OpenDaylight northbound api component (application) + we want to talk to. + 'path' - the specific rest query for the application. + + Note that other attributes, including 'container' are specified + in the OpenDaylight.setup dictionary. + """ + + # the base url we will use for the connection + self._base_url = self.setup['http'] + self.setup['hostname'] + ':' + \ + self.setup['port'] + self.setup['path'] + + # the specific path we are building + self.url = self._base_url + app + '/' + self.setup['container'] + path + + def prepare_auth(self): + """Set up the credentials for the REST connection by creating + an auth object for Requests and shoving it into OpenDaylight.auth + + Currently, as far as I know, the OpenDaylight controller uses + http basic auth. If/when that changes this function should be + updated. + + If you use prepare(), you shouldn't need to call prepare_auth() + yourself. However, if there were something you wanted to do + that was so whacked out custom, then by all means build it yourself + and don't bother to call this function. + """ + + # stuff an HTTPBasicAuth object in here ready for use + self.auth = HTTPBasicAuth(self.setup['username'], + self.setup['password']) + #print("Prepare set up auth: " + self.setup['username'] + ', ' + \ + # self.setup['password']) + + +class OpenDaylightFlow(object): + """OpenDaylightFlow is an object that talks to the OpenDaylight + Flow Programmer application REST API + + OpenDaylight.odl holds an OpenDaylight object containing details + on how to communicate with the controller. + + OpenDaylightFlow.request holds a Requests object for the REST + session. Take a look at the Requests documentation for all of + the methods available, but here are a few handy examples: + OpenDaylightFlow.request.status_code - returns the http code + OpenDaylightFlow.request.text - returns the response as text + + OpenDaylightFlow.flows holds a dictionary that corresponds to + the flowConfig element in the OpenDaylight REST API. Note that + we don't statically define what those fields are here in this + object. This makes this library code more flexible as flowConfig + changes over time. After all, this is REST, not RPC. + """ + + def __init__(self, odl): + """Mandatory argument: + odl - an OpenDaylight object + """ + self.odl = odl + self.__app = 'flow' + self.request = None + self.flows = None + + def get(self, node_id=None, flow_name=None): + """Get Flows specified on the Controller and stuffs the results into + the OpenDaylightFlow.flows dictionary. + + Optional Arguments: + node_id - returns flows just for that switch dpid + flow_name - returns the specifically named flow on that switch + """ + + # clear out any remaining crud from previous calls + if hasattr(self, 'request'): + del self.request + if hasattr(self, 'flows'): + del self.flows + + if node_id is None: + self.odl.prepare(self.__app, '/') + elif flow_name is None: + self.odl.prepare(self.__app, '/' + 'OF/' + node_id + '/') + else: + self.odl.prepare(self.__app, '/' + 'OF/' + node_id + '/' + + flow_name + '/') + + self.request = requests.get(url=self.odl.url, auth=self.odl.auth) + + if self.request.status_code == 200: + self.flows = self.request.json() + if 'flowConfig' in self.flows: + self.flows = self.flows.get('flowConfig') + else: + raise OpenDaylightError({'url':self.odl.url, + 'http_code':self.request.status_code, + 'msg':self.request.text}) + + + def add(self, flow): + """Given a dictionary corresponding to a flowConfig, add this flow to + the Controller. Note that the switch dpid and the flow's name is + specified in the flowConfig passed in. + """ + if hasattr(self, 'request'): + del self.request + #print(flow) + self.odl.prepare(self.__app, '/' + flow['node']['@type'] + '/' + + flow['node']['@id'] + '/' + flow['name'] + '/') + headers = {'Content-type': 'application/json'} + body = json.dumps(flow) + self.request = requests.post(url=self.odl.url, auth=self.odl.auth, + data=body, headers=headers) + + if self.request.status_code != 201: + raise OpenDaylightError({'url':self.odl.url, + 'http_code':self.request.status_code, + 'msg':self.request.text}) + + #def update(self): + # """Update a flow to a Node on the Controller + # """ + # raise NotImplementedError("update()") + + def delete(self, node_id, flow_name): + """Delete a flow to a Node on the Controller + + Mandatory Arguments: + node_id - the switch dpid + flow_name - the specifically named flow on that switch + """ + if hasattr(self, 'request'): + del self.request + + self.odl.prepare(self.__app, '/' + 'OF/' + node_id + '/' + + flow_name + '/') + self.request = requests.delete(url=self.odl.url, auth=self.odl.auth) + + # note, if you wanted to pass in a flowConfig style dictionary, + # this is how you would do it. This is what I did initially, but + # it seemed clunky to pass in an entire flow. + #self.prepare(self.__app, '/' + flow['node']['@type'] + '/' + + # flow['node']['@id'] + '/' + flow['name'] + '/') + + if self.request.status_code != 200: + raise OpenDaylightError({'url':self.odl.url, + 'http_code':self.request.status_code, + 'msg':self.request.text}) + + +#pylint: disable=R0921 +class OpenDaylightNode(object): + """A way to talk to the OpenDaylight Switch Manager REST API + + OpenDaylight.odl holds an OpenDaylight object containing details + on how to communicate with the controller. + + OpenDaylightNode.request holds a Requests object for the REST + session. Take a look at the Requests documentation for all of + the methods available, but here are a few handy examples: + OpenDaylightNode.request.status_code - returns the http code + OpenDaylightNode.request.text - returns the response as text + + OpenDaylightNode.nodes holds a dictionary that corresponds to + the 'nodes' element in the OpenDaylight REST API. + + OpenDaylightNode.node_connectors holds a dictionary that corresponds to + the 'nodeConnectors' element in the OpenDaylight REST API. + + Note that we don't statically define what those fields are contained + in the 'nodes' or 'nodeConnectors' elements here in this object. + """ + + # Just a note that there are more functions available on + # the controller that could be implemented, but it is not + # clear at this time if that is useful + + def __init__(self, odl): + """Mandatory argument: + odl - an OpenDaylight object + """ + self.odl = odl + self.__app = 'switch' + self.nodes = None + self.node_connectors = None + self.request = None + + def get_nodes(self): + """Get information about Nodes on the Controller and stuffs the + result into the OpenDaylightNode.notes dictionary. + + """ + if hasattr(self, 'request'): + del self.request + if hasattr(self, 'nodes'): + del self.nodes + + self.odl.prepare(self.__app, '/nodes/') + self.request = requests.get(url=self.odl.url, auth=self.odl.auth) + + if self.request.status_code == 200: + self.nodes = self.request.json() + if 'nodeProperties' in self.nodes: + self.nodes = self.nodes.get('nodeProperties') + else: + raise OpenDaylightError({'url':self.odl.url, + 'http_code':self.request.status_code, + 'msg':self.request.text}) + + def get_node_connectors(self, node_id): + """Get information about NodeConnectors on the Controller and stuffs the + result into the OpenDaylightNode.node_connectors dictionary. + + Mandatory Arguments: + node_id - returns flows just for that switch dpid + """ + + if hasattr(self, 'request'): + del self.request + if hasattr(self, 'node_connectors'): + del self.node_connectors + + self.odl.prepare(self.__app, '/node/' + 'OF/' + node_id + '/') + self.request = requests.get(url=self.odl.url, auth=self.odl.auth) + if self.request.status_code == 200: + self.node_connectors = self.request.json() + if 'nodeConnectorProperties' in self.node_connectors: + self.node_connectors = self.node_connectors.get( + 'nodeConnectorProperties') + else: + raise OpenDaylightError({'url':self.odl.url, + 'http_code':self.request.status_code, + 'msg':self.request.text}) + + def save(self): + """Save current switch configurations + + The REST API documentation says: + "Save the current switch configurations", but I am not sure what + that actually means. If you think you do, then here you go. + """ + + if hasattr(self, 'request'): + del self.request + + self.odl.prepare(self.__app, '/switch-config/') + self.request = requests.post(url=self.odl.url, auth=self.odl.auth) + if self.request.status_code != 200: + raise OpenDaylightError({'url':self.odl.url, + 'http_code':self.request.status_code, + 'msg':self.request.text}) + + def delete_node_property(self): + """Delete a property of a Node on the Controller + """ + raise NotImplementedError("delete_node_property()") + + def add_node_property(self): + """Add a property of a Node on the Controller + """ + raise NotImplementedError("add_node_property()") + + def delete_node_connector_property(self): + """Delete a property of a Node on the Controller + """ + raise NotImplementedError("delete_node_connector_property()") + + def add_node_connector_property(self): + """Add a property of a Node on the Controller + """ + raise NotImplementedError("add_node_connector_property()") + + +class OpenDaylightError(Exception): + """OpenDaylight Exception Class + """ + pass diff --git a/test-OpenDaylight.py b/test-OpenDaylight.py new file mode 100755 index 0000000..9689a1e --- /dev/null +++ b/test-OpenDaylight.py @@ -0,0 +1,293 @@ +#!/usr/bin/python +""" +Tests for the OpenDaylight REST API interface + +Copyright 2013 The University of Wisconsin Board of Regents + + 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. + + Written by: Dale W. Carder, dwcarder@wisc.edu + Network Services Group + Division of Information Technology + University of Wisconsin at Madison + +This material is based upon work supported by the National Science +Foundation under Grant No. 1247322 +""" + +import time +import unittest +from OpenDaylight import OpenDaylight +from OpenDaylight import OpenDaylightFlow +from OpenDaylight import OpenDaylightNode +from OpenDaylight import OpenDaylightError +from mininet.net import Mininet +#from mininet.util import dumpNodeConnections +#from mininet.log import setLogLevel +from mininet.topo import Topo +from mininet.node import RemoteController + +# Edit these as necessary for your organization +CONTROLLER = '10.10.10.1' +USERNAME = 'admin' +PASSWORD = 'admin' +SWITCH_1 = '99:99:99:00:00:00:01:00' +# This is chosen so that it does not conflict with any other +# switches that may be associated to a controller that is not +# dedicated explicitly for testing + +class TestSequenceFunctions(unittest.TestCase): + """Tests for OpenDaylight + + At this point, tests for OpenDaylightFlow and OpenDaylightNode + are intermingled. These could be seperated out into seperate + suites. + """ + + def setUp(self): + odl = OpenDaylight() + odl.setup['hostname'] = CONTROLLER + odl.setup['username'] = USERNAME + odl.setup['password'] = PASSWORD + self.flow = OpenDaylightFlow(odl) + self.node = OpenDaylightNode(odl) + + self.switch_id_1 = SWITCH_1 + + self.odl_test_flow_1 = {u'actions': u'DROP', + u'etherType': u'0x800', + u'ingressPort': u'1', + u'installInHw': u'true', + u'name': u'odl-test-flow1', + u'node': {u'@id': self.switch_id_1, u'@type': u'OF'}, + u'priority': u'500'} + + self.odl_test_flow_2 = {u'actions': u'DROP', + u'etherType': u'0x800', + u'ingressPort': u'2', + u'installInHw': u'true', + u'name': u'odl-test-flow2', + u'node': {u'@id': self.switch_id_1, u'@type': u'OF'}, + u'priority': u'500'} + + + def test_01_delete_flows(self): + """Clean up from any previous test run, just delete these + flows if they exist. + """ + try: + self.flow.delete(self.odl_test_flow_1['node']['@id'], + self.odl_test_flow_1['name']) + except: + pass + + try: + self.flow.delete(self.odl_test_flow_2['node']['@id'], + self.odl_test_flow_2['name']) + except: + pass + + def test_10_add_flow(self): + """Add a sample flow onto the controller + """ + self.flow.add(self.odl_test_flow_1) + self.assertEqual(self.flow.request.status_code, 201) + + def test_10_add_flow2(self): + """Add a sample flow onto the controller + """ + self.flow.add(self.odl_test_flow_2) + self.assertEqual(self.flow.request.status_code, 201) + + def test_15_add_flow2(self): + """Add a duplicate flow onto the controller + """ + try: + self.flow.add(self.odl_test_flow_2) + except OpenDaylightError: + pass + except e: + self.fail('Unexpected exception thrown:', e) + else: + self.fail('Expected Exception not thrown') + + def test_20_get_flow(self): + """Retrieve the specific flow back from the controller + """ + self.flow.get(node_id=self.switch_id_1, flow_name='odl-test-flow1') + self.assertEqual(self.flow.flows, self.odl_test_flow_1) + self.assertEqual(self.flow.request.status_code, 200) + + def test_20_get_flow2(self): + """Retrieve the specific flow back from the controller + """ + self.flow.get(node_id=self.switch_id_1, flow_name='odl-test-flow1') + self.assertEqual(self.flow.flows, self.odl_test_flow_1) + self.assertEqual(self.flow.request.status_code, 200) + + + def test_30_get_all_switch_flows(self): + """Retrieve all flows from this switch back from the controller + """ + self.flow.get(node_id=self.switch_id_1) + self.assertTrue(self.odl_test_flow_1 in self.flow.flows) + self.assertTrue(self.odl_test_flow_2 in self.flow.flows) + self.assertEqual(self.flow.request.status_code, 200) + + def test_30_get_all_flows(self): + """Retrieve all flows back from the controller + """ + self.flow.get() + self.assertTrue(self.odl_test_flow_1 in self.flow.flows) + self.assertTrue(self.odl_test_flow_2 in self.flow.flows) + self.assertEqual(self.flow.request.status_code, 200) + + def test_30_get_flows_invalid_switch(self): + """Try to get a flow from a non-existant switch + """ + try: + # This dpid is specifically chosen figuring that it + # would not be in use in a production system. Plus, I + # simply just like the number 53. + self.flow.get(node_id='53:53:53:53:53:53:53:53') + except OpenDaylightError: + pass + except e: + self.fail('Unexpected exception thrown:', e) + else: + self.fail('Expected Exception not thrown') + + def test_40_get_flows_invalid_flowname(self): + """Try to get a flow that does not exist. + """ + try: + self.flow.get(node_id=self.switch_id_1, flow_name='foo-foo-foo-bar') + except OpenDaylightError: + pass + except e: + self.fail('Unexpected exception thrown:', e) + else: + self.fail('Expected Exception not thrown') + + def test_50_delete_flow(self): + """Delete flow 1. + """ + self.flow.delete(self.odl_test_flow_1['node']['@id'], + self.odl_test_flow_1['name']) + self.assertEqual(self.flow.request.status_code, 200) + + + def test_51_deleted_flow_get(self): + """Verify that the deleted flow does not exist. + """ + try: + self.flow.get(node_id=self.switch_id_1, flow_name='odl-test-flow1') + except OpenDaylightError: + pass + except e: + self.fail('Unexpected exception thrown:', e) + else: + self.fail('Expected Exception not thrown') + + def test_55_delete_flow2(self): + """Delete flow 2 + """ + self.flow.delete(self.odl_test_flow_2['node']['@id'], + self.odl_test_flow_2['name']) + self.assertEqual(self.flow.request.status_code, 200) + + + #TODO: Add invalid flow that has a bad port + #TODO: Add invalid flow that has a non-existant switch + #TODO: Add invalid flow that has an invalid switch name (non-hexadecimal), + # see https://bugs.opendaylight.org/show_bug.cgi?id=27 + + def test_60_get_all_nodes(self): + """Get all of the nodes on the controller + + TODO: verify that SWITCH_1 is contained in the response + """ + self.node.get_nodes() + self.assertEqual(self.node.request.status_code, 200) + + def test_60_get_node_connector(self): + """Retrieve a list of all the node connectors and their properties + in a given node + + TODO: verify that SWITCH_1 is contained in the response + """ + self.node.get_node_connectors(SWITCH_1) + self.assertEqual(self.node.request.status_code, 200) + + + def test_60_get_bad_node_connector(self): + """Retrieve a list of all the node connectors and their properties + in a given node for a node that does not exist + """ + try: + self.node.get_node_connectors('53:53:53:53:53:53:53:53') + except OpenDaylightError: + pass + except e: + self.fail('Unexpected exception thrown:', e) + else: + self.fail('Expected Exception not thrown') + + + def test_60_save(self): + """Save the switch configurations. + It's not clear that this can be easily tested, so we just + see if this call works or not based on the http status code. + """ + self.node.save() + self.assertEqual(self.node.request.status_code, 200) + + +class SingleSwitchTopo(Topo): + "Single switch connected to n hosts." + def __init__(self, n=2, **opts): + # Initialize topology and default options + Topo.__init__(self, **opts) + # mininet/ovswitch does not want ':'s in the dpid + switch_id = SWITCH_1.translate(None, ':') + switch = self.addSwitch('s1', dpid=switch_id) + # Python's range(N) generates 0..N-1 + for h in range(n): + host = self.addHost('h%s' % (h + 1)) + self.addLink(host, switch) + +def setup_mininet_simpleTest(): + "Create and test a simple network" + topo = SingleSwitchTopo(n=4) + #net = Mininet(topo) + net = Mininet( topo=topo, controller=lambda name: RemoteController( + name, ip=CONTROLLER ) ) + net.start() + #print "Dumping host connections" + #dumpNodeConnections(net.hosts) + + #time.sleep(300) + + #print "Testing network connectivity" + #net.pingAll() + #net.stop() + +if __name__ == '__main__': + # Tell mininet to print useful information + #setLogLevel('info') + + setup_mininet_simpleTest() + time.sleep(10) + unittest.main() + +