├── .gitignore ├── LICENSE.txt ├── OpenDaylight.py ├── README.md └── test-OpenDaylight.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /OpenDaylight.py: -------------------------------------------------------------------------------- 1 | """ 2 | OpenDaylight REST API 3 | 4 | Copyright 2013 The University of Wisconsin Board of Regents 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | 18 | 19 | Written by: Dale W. Carder, dwcarder@wisc.edu 20 | Network Services Group 21 | Division of Information Technology 22 | University of Wisconsin at Madison 23 | 24 | This material is based upon work supported by the National Science Foundation 25 | under Grant No. 1247322. 26 | """ 27 | 28 | from __future__ import print_function 29 | import json 30 | import requests 31 | from requests.auth import HTTPBasicAuth 32 | 33 | class OpenDaylight(object): 34 | """An object holding details to talk to the OpenDaylight REST API 35 | 36 | OpenDaylight.setup is a dictionary loaded with the following 37 | default values: 38 | {'hostname':'localhost', 39 | 'port':'8080', 40 | 'username':'admin', 41 | 'password':'admin', 42 | 'path':'/controller/nb/v2/', 43 | 'container':'default', 44 | 'http':'http://' } 45 | 46 | Your code should change these as required for your installation. 47 | OpenDaylight.url holds the url for each REST query. Typically 48 | you would let OpenDaylight.prepare() build this for you. 49 | 50 | OpenDaylight.auth holds an auth object for Requests to use 51 | for each REST query. Typically you would also let 52 | OpenDaylight.prepare() build this for you. 53 | """ 54 | 55 | def __init__(self): 56 | """Set some mostly reasonable defaults. 57 | """ 58 | self.setup = {'hostname':'localhost', 59 | 'port':'8080', 60 | 'username':'admin', 61 | 'password':'admin', 62 | 'path':'/controller/nb/v2/', 63 | 'container':'default', 64 | 'http':'http://'} 65 | 66 | self._base_url = None 67 | self.url = None 68 | self.auth = None 69 | 70 | def prepare(self, app, path): 71 | """Sets up the necessary details for the REST connection by calling 72 | prepare_url and prepare_auth. 73 | 74 | Arguments: 75 | 'app' - which OpenDaylight northbound api component (application) 76 | we want to talk to. 77 | 'path' - the specific rest query for the application. 78 | """ 79 | self.prepare_url(app, path) 80 | self.prepare_auth() 81 | 82 | def prepare_url(self, app, path): 83 | """Build the URL for this REST connection which is then stored as 84 | OpenDaylight.url 85 | 86 | If you use prepare(), you shouldn't need to call prepare_url() 87 | yourself. However, if there were a URL you wanted to construct that 88 | was so whacked out custom, then by all means build it yourself and don't 89 | bother to call this function. 90 | 91 | Arguments: 92 | 'app' - which OpenDaylight northbound api component (application) 93 | we want to talk to. 94 | 'path' - the specific rest query for the application. 95 | 96 | Note that other attributes, including 'container' are specified 97 | in the OpenDaylight.setup dictionary. 98 | """ 99 | 100 | # the base url we will use for the connection 101 | self._base_url = self.setup['http'] + self.setup['hostname'] + ':' + \ 102 | self.setup['port'] + self.setup['path'] 103 | 104 | # the specific path we are building 105 | self.url = self._base_url + app + '/' + self.setup['container'] + path 106 | 107 | def prepare_auth(self): 108 | """Set up the credentials for the REST connection by creating 109 | an auth object for Requests and shoving it into OpenDaylight.auth 110 | 111 | Currently, as far as I know, the OpenDaylight controller uses 112 | http basic auth. If/when that changes this function should be 113 | updated. 114 | 115 | If you use prepare(), you shouldn't need to call prepare_auth() 116 | yourself. However, if there were something you wanted to do 117 | that was so whacked out custom, then by all means build it yourself 118 | and don't bother to call this function. 119 | """ 120 | 121 | # stuff an HTTPBasicAuth object in here ready for use 122 | self.auth = HTTPBasicAuth(self.setup['username'], 123 | self.setup['password']) 124 | #print("Prepare set up auth: " + self.setup['username'] + ', ' + \ 125 | # self.setup['password']) 126 | 127 | 128 | class OpenDaylightFlow(object): 129 | """OpenDaylightFlow is an object that talks to the OpenDaylight 130 | Flow Programmer application REST API 131 | 132 | OpenDaylight.odl holds an OpenDaylight object containing details 133 | on how to communicate with the controller. 134 | 135 | OpenDaylightFlow.request holds a Requests object for the REST 136 | session. Take a look at the Requests documentation for all of 137 | the methods available, but here are a few handy examples: 138 | OpenDaylightFlow.request.status_code - returns the http code 139 | OpenDaylightFlow.request.text - returns the response as text 140 | 141 | OpenDaylightFlow.flows holds a dictionary that corresponds to 142 | the flowConfig element in the OpenDaylight REST API. Note that 143 | we don't statically define what those fields are here in this 144 | object. This makes this library code more flexible as flowConfig 145 | changes over time. After all, this is REST, not RPC. 146 | """ 147 | 148 | def __init__(self, odl): 149 | """Mandatory argument: 150 | odl - an OpenDaylight object 151 | """ 152 | self.odl = odl 153 | self.__app = 'flow' 154 | self.request = None 155 | self.flows = None 156 | 157 | def get(self, node_id=None, flow_name=None): 158 | """Get Flows specified on the Controller and stuffs the results into 159 | the OpenDaylightFlow.flows dictionary. 160 | 161 | Optional Arguments: 162 | node_id - returns flows just for that switch dpid 163 | flow_name - returns the specifically named flow on that switch 164 | """ 165 | 166 | # clear out any remaining crud from previous calls 167 | if hasattr(self, 'request'): 168 | del self.request 169 | if hasattr(self, 'flows'): 170 | del self.flows 171 | 172 | if node_id is None: 173 | self.odl.prepare(self.__app, '/') 174 | elif flow_name is None: 175 | self.odl.prepare(self.__app, '/' + 'OF/' + node_id + '/') 176 | else: 177 | self.odl.prepare(self.__app, '/' + 'OF/' + node_id + '/' 178 | + flow_name + '/') 179 | 180 | self.request = requests.get(url=self.odl.url, auth=self.odl.auth) 181 | 182 | if self.request.status_code == 200: 183 | self.flows = self.request.json() 184 | if 'flowConfig' in self.flows: 185 | self.flows = self.flows.get('flowConfig') 186 | else: 187 | raise OpenDaylightError({'url':self.odl.url, 188 | 'http_code':self.request.status_code, 189 | 'msg':self.request.text}) 190 | 191 | 192 | def add(self, flow): 193 | """Given a dictionary corresponding to a flowConfig, add this flow to 194 | the Controller. Note that the switch dpid and the flow's name is 195 | specified in the flowConfig passed in. 196 | """ 197 | if hasattr(self, 'request'): 198 | del self.request 199 | #print(flow) 200 | self.odl.prepare(self.__app, '/' + flow['node']['@type'] + '/' + 201 | flow['node']['@id'] + '/' + flow['name'] + '/') 202 | headers = {'Content-type': 'application/json'} 203 | body = json.dumps(flow) 204 | self.request = requests.post(url=self.odl.url, auth=self.odl.auth, 205 | data=body, headers=headers) 206 | 207 | if self.request.status_code != 201: 208 | raise OpenDaylightError({'url':self.odl.url, 209 | 'http_code':self.request.status_code, 210 | 'msg':self.request.text}) 211 | 212 | #def update(self): 213 | # """Update a flow to a Node on the Controller 214 | # """ 215 | # raise NotImplementedError("update()") 216 | 217 | def delete(self, node_id, flow_name): 218 | """Delete a flow to a Node on the Controller 219 | 220 | Mandatory Arguments: 221 | node_id - the switch dpid 222 | flow_name - the specifically named flow on that switch 223 | """ 224 | if hasattr(self, 'request'): 225 | del self.request 226 | 227 | self.odl.prepare(self.__app, '/' + 'OF/' + node_id + '/' + 228 | flow_name + '/') 229 | self.request = requests.delete(url=self.odl.url, auth=self.odl.auth) 230 | 231 | # note, if you wanted to pass in a flowConfig style dictionary, 232 | # this is how you would do it. This is what I did initially, but 233 | # it seemed clunky to pass in an entire flow. 234 | #self.prepare(self.__app, '/' + flow['node']['@type'] + '/' + 235 | # flow['node']['@id'] + '/' + flow['name'] + '/') 236 | 237 | if self.request.status_code != 200: 238 | raise OpenDaylightError({'url':self.odl.url, 239 | 'http_code':self.request.status_code, 240 | 'msg':self.request.text}) 241 | 242 | 243 | #pylint: disable=R0921 244 | class OpenDaylightNode(object): 245 | """A way to talk to the OpenDaylight Switch Manager REST API 246 | 247 | OpenDaylight.odl holds an OpenDaylight object containing details 248 | on how to communicate with the controller. 249 | 250 | OpenDaylightNode.request holds a Requests object for the REST 251 | session. Take a look at the Requests documentation for all of 252 | the methods available, but here are a few handy examples: 253 | OpenDaylightNode.request.status_code - returns the http code 254 | OpenDaylightNode.request.text - returns the response as text 255 | 256 | OpenDaylightNode.nodes holds a dictionary that corresponds to 257 | the 'nodes' element in the OpenDaylight REST API. 258 | 259 | OpenDaylightNode.node_connectors holds a dictionary that corresponds to 260 | the 'nodeConnectors' element in the OpenDaylight REST API. 261 | 262 | Note that we don't statically define what those fields are contained 263 | in the 'nodes' or 'nodeConnectors' elements here in this object. 264 | """ 265 | 266 | # Just a note that there are more functions available on 267 | # the controller that could be implemented, but it is not 268 | # clear at this time if that is useful 269 | 270 | def __init__(self, odl): 271 | """Mandatory argument: 272 | odl - an OpenDaylight object 273 | """ 274 | self.odl = odl 275 | self.__app = 'switch' 276 | self.nodes = None 277 | self.node_connectors = None 278 | self.request = None 279 | 280 | def get_nodes(self): 281 | """Get information about Nodes on the Controller and stuffs the 282 | result into the OpenDaylightNode.notes dictionary. 283 | 284 | """ 285 | if hasattr(self, 'request'): 286 | del self.request 287 | if hasattr(self, 'nodes'): 288 | del self.nodes 289 | 290 | self.odl.prepare(self.__app, '/nodes/') 291 | self.request = requests.get(url=self.odl.url, auth=self.odl.auth) 292 | 293 | if self.request.status_code == 200: 294 | self.nodes = self.request.json() 295 | if 'nodeProperties' in self.nodes: 296 | self.nodes = self.nodes.get('nodeProperties') 297 | else: 298 | raise OpenDaylightError({'url':self.odl.url, 299 | 'http_code':self.request.status_code, 300 | 'msg':self.request.text}) 301 | 302 | def get_node_connectors(self, node_id): 303 | """Get information about NodeConnectors on the Controller and stuffs the 304 | result into the OpenDaylightNode.node_connectors dictionary. 305 | 306 | Mandatory Arguments: 307 | node_id - returns flows just for that switch dpid 308 | """ 309 | 310 | if hasattr(self, 'request'): 311 | del self.request 312 | if hasattr(self, 'node_connectors'): 313 | del self.node_connectors 314 | 315 | self.odl.prepare(self.__app, '/node/' + 'OF/' + node_id + '/') 316 | self.request = requests.get(url=self.odl.url, auth=self.odl.auth) 317 | if self.request.status_code == 200: 318 | self.node_connectors = self.request.json() 319 | if 'nodeConnectorProperties' in self.node_connectors: 320 | self.node_connectors = self.node_connectors.get( 321 | 'nodeConnectorProperties') 322 | else: 323 | raise OpenDaylightError({'url':self.odl.url, 324 | 'http_code':self.request.status_code, 325 | 'msg':self.request.text}) 326 | 327 | def save(self): 328 | """Save current switch configurations 329 | 330 | The REST API documentation says: 331 | "Save the current switch configurations", but I am not sure what 332 | that actually means. If you think you do, then here you go. 333 | """ 334 | 335 | if hasattr(self, 'request'): 336 | del self.request 337 | 338 | self.odl.prepare(self.__app, '/switch-config/') 339 | self.request = requests.post(url=self.odl.url, auth=self.odl.auth) 340 | if self.request.status_code != 200: 341 | raise OpenDaylightError({'url':self.odl.url, 342 | 'http_code':self.request.status_code, 343 | 'msg':self.request.text}) 344 | 345 | def delete_node_property(self): 346 | """Delete a property of a Node on the Controller 347 | """ 348 | raise NotImplementedError("delete_node_property()") 349 | 350 | def add_node_property(self): 351 | """Add a property of a Node on the Controller 352 | """ 353 | raise NotImplementedError("add_node_property()") 354 | 355 | def delete_node_connector_property(self): 356 | """Delete a property of a Node on the Controller 357 | """ 358 | raise NotImplementedError("delete_node_connector_property()") 359 | 360 | def add_node_connector_property(self): 361 | """Add a property of a Node on the Controller 362 | """ 363 | raise NotImplementedError("add_node_connector_property()") 364 | 365 | 366 | class OpenDaylightError(Exception): 367 | """OpenDaylight Exception Class 368 | """ 369 | pass 370 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-OpenDaylight 2 | =================== 3 | 4 | A python interface to the OpenDaylight REST API 5 | 6 | Copyright 2013 The University of Wisconsin Board of Regents 7 | Written by: Dale W. Carder, dwcarder@wisc.edu 8 | Network Services Group 9 | Division of Information Technology 10 | University of Wisconsin at Madison 11 | 12 | Find out more about this project here: 13 | http://net.doit.wisc.edu/~dwcarder/scripts/opendaylight/ 14 | 15 | 16 | ### Acknowledgements: 17 | 18 | This material is based upon work supported by the National Science Foundation 19 | under Grant No. 1247322. 20 | 21 | -------------------------------------------------------------------------------- /test-OpenDaylight.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Tests for the OpenDaylight REST API interface 4 | 5 | Copyright 2013 The University of Wisconsin Board of Regents 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | 19 | Written by: Dale W. Carder, dwcarder@wisc.edu 20 | Network Services Group 21 | Division of Information Technology 22 | University of Wisconsin at Madison 23 | 24 | This material is based upon work supported by the National Science 25 | Foundation under Grant No. 1247322 26 | """ 27 | 28 | import time 29 | import unittest 30 | from OpenDaylight import OpenDaylight 31 | from OpenDaylight import OpenDaylightFlow 32 | from OpenDaylight import OpenDaylightNode 33 | from OpenDaylight import OpenDaylightError 34 | from mininet.net import Mininet 35 | #from mininet.util import dumpNodeConnections 36 | #from mininet.log import setLogLevel 37 | from mininet.topo import Topo 38 | from mininet.node import RemoteController 39 | 40 | # Edit these as necessary for your organization 41 | CONTROLLER = '10.10.10.1' 42 | USERNAME = 'admin' 43 | PASSWORD = 'admin' 44 | SWITCH_1 = '99:99:99:00:00:00:01:00' 45 | # This is chosen so that it does not conflict with any other 46 | # switches that may be associated to a controller that is not 47 | # dedicated explicitly for testing 48 | 49 | class TestSequenceFunctions(unittest.TestCase): 50 | """Tests for OpenDaylight 51 | 52 | At this point, tests for OpenDaylightFlow and OpenDaylightNode 53 | are intermingled. These could be seperated out into seperate 54 | suites. 55 | """ 56 | 57 | def setUp(self): 58 | odl = OpenDaylight() 59 | odl.setup['hostname'] = CONTROLLER 60 | odl.setup['username'] = USERNAME 61 | odl.setup['password'] = PASSWORD 62 | self.flow = OpenDaylightFlow(odl) 63 | self.node = OpenDaylightNode(odl) 64 | 65 | self.switch_id_1 = SWITCH_1 66 | 67 | self.odl_test_flow_1 = {u'actions': u'DROP', 68 | u'etherType': u'0x800', 69 | u'ingressPort': u'1', 70 | u'installInHw': u'true', 71 | u'name': u'odl-test-flow1', 72 | u'node': {u'@id': self.switch_id_1, u'@type': u'OF'}, 73 | u'priority': u'500'} 74 | 75 | self.odl_test_flow_2 = {u'actions': u'DROP', 76 | u'etherType': u'0x800', 77 | u'ingressPort': u'2', 78 | u'installInHw': u'true', 79 | u'name': u'odl-test-flow2', 80 | u'node': {u'@id': self.switch_id_1, u'@type': u'OF'}, 81 | u'priority': u'500'} 82 | 83 | 84 | def test_01_delete_flows(self): 85 | """Clean up from any previous test run, just delete these 86 | flows if they exist. 87 | """ 88 | try: 89 | self.flow.delete(self.odl_test_flow_1['node']['@id'], 90 | self.odl_test_flow_1['name']) 91 | except: 92 | pass 93 | 94 | try: 95 | self.flow.delete(self.odl_test_flow_2['node']['@id'], 96 | self.odl_test_flow_2['name']) 97 | except: 98 | pass 99 | 100 | def test_10_add_flow(self): 101 | """Add a sample flow onto the controller 102 | """ 103 | self.flow.add(self.odl_test_flow_1) 104 | self.assertEqual(self.flow.request.status_code, 201) 105 | 106 | def test_10_add_flow2(self): 107 | """Add a sample flow onto the controller 108 | """ 109 | self.flow.add(self.odl_test_flow_2) 110 | self.assertEqual(self.flow.request.status_code, 201) 111 | 112 | def test_15_add_flow2(self): 113 | """Add a duplicate flow onto the controller 114 | """ 115 | try: 116 | self.flow.add(self.odl_test_flow_2) 117 | except OpenDaylightError: 118 | pass 119 | except e: 120 | self.fail('Unexpected exception thrown:', e) 121 | else: 122 | self.fail('Expected Exception not thrown') 123 | 124 | def test_20_get_flow(self): 125 | """Retrieve the specific flow back from the controller 126 | """ 127 | self.flow.get(node_id=self.switch_id_1, flow_name='odl-test-flow1') 128 | self.assertEqual(self.flow.flows, self.odl_test_flow_1) 129 | self.assertEqual(self.flow.request.status_code, 200) 130 | 131 | def test_20_get_flow2(self): 132 | """Retrieve the specific flow back from the controller 133 | """ 134 | self.flow.get(node_id=self.switch_id_1, flow_name='odl-test-flow1') 135 | self.assertEqual(self.flow.flows, self.odl_test_flow_1) 136 | self.assertEqual(self.flow.request.status_code, 200) 137 | 138 | 139 | def test_30_get_all_switch_flows(self): 140 | """Retrieve all flows from this switch back from the controller 141 | """ 142 | self.flow.get(node_id=self.switch_id_1) 143 | self.assertTrue(self.odl_test_flow_1 in self.flow.flows) 144 | self.assertTrue(self.odl_test_flow_2 in self.flow.flows) 145 | self.assertEqual(self.flow.request.status_code, 200) 146 | 147 | def test_30_get_all_flows(self): 148 | """Retrieve all flows back from the controller 149 | """ 150 | self.flow.get() 151 | self.assertTrue(self.odl_test_flow_1 in self.flow.flows) 152 | self.assertTrue(self.odl_test_flow_2 in self.flow.flows) 153 | self.assertEqual(self.flow.request.status_code, 200) 154 | 155 | def test_30_get_flows_invalid_switch(self): 156 | """Try to get a flow from a non-existant switch 157 | """ 158 | try: 159 | # This dpid is specifically chosen figuring that it 160 | # would not be in use in a production system. Plus, I 161 | # simply just like the number 53. 162 | self.flow.get(node_id='53:53:53:53:53:53:53:53') 163 | except OpenDaylightError: 164 | pass 165 | except e: 166 | self.fail('Unexpected exception thrown:', e) 167 | else: 168 | self.fail('Expected Exception not thrown') 169 | 170 | def test_40_get_flows_invalid_flowname(self): 171 | """Try to get a flow that does not exist. 172 | """ 173 | try: 174 | self.flow.get(node_id=self.switch_id_1, flow_name='foo-foo-foo-bar') 175 | except OpenDaylightError: 176 | pass 177 | except e: 178 | self.fail('Unexpected exception thrown:', e) 179 | else: 180 | self.fail('Expected Exception not thrown') 181 | 182 | def test_50_delete_flow(self): 183 | """Delete flow 1. 184 | """ 185 | self.flow.delete(self.odl_test_flow_1['node']['@id'], 186 | self.odl_test_flow_1['name']) 187 | self.assertEqual(self.flow.request.status_code, 200) 188 | 189 | 190 | def test_51_deleted_flow_get(self): 191 | """Verify that the deleted flow does not exist. 192 | """ 193 | try: 194 | self.flow.get(node_id=self.switch_id_1, flow_name='odl-test-flow1') 195 | except OpenDaylightError: 196 | pass 197 | except e: 198 | self.fail('Unexpected exception thrown:', e) 199 | else: 200 | self.fail('Expected Exception not thrown') 201 | 202 | def test_55_delete_flow2(self): 203 | """Delete flow 2 204 | """ 205 | self.flow.delete(self.odl_test_flow_2['node']['@id'], 206 | self.odl_test_flow_2['name']) 207 | self.assertEqual(self.flow.request.status_code, 200) 208 | 209 | 210 | #TODO: Add invalid flow that has a bad port 211 | #TODO: Add invalid flow that has a non-existant switch 212 | #TODO: Add invalid flow that has an invalid switch name (non-hexadecimal), 213 | # see https://bugs.opendaylight.org/show_bug.cgi?id=27 214 | 215 | def test_60_get_all_nodes(self): 216 | """Get all of the nodes on the controller 217 | 218 | TODO: verify that SWITCH_1 is contained in the response 219 | """ 220 | self.node.get_nodes() 221 | self.assertEqual(self.node.request.status_code, 200) 222 | 223 | def test_60_get_node_connector(self): 224 | """Retrieve a list of all the node connectors and their properties 225 | in a given node 226 | 227 | TODO: verify that SWITCH_1 is contained in the response 228 | """ 229 | self.node.get_node_connectors(SWITCH_1) 230 | self.assertEqual(self.node.request.status_code, 200) 231 | 232 | 233 | def test_60_get_bad_node_connector(self): 234 | """Retrieve a list of all the node connectors and their properties 235 | in a given node for a node that does not exist 236 | """ 237 | try: 238 | self.node.get_node_connectors('53:53:53:53:53:53:53:53') 239 | except OpenDaylightError: 240 | pass 241 | except e: 242 | self.fail('Unexpected exception thrown:', e) 243 | else: 244 | self.fail('Expected Exception not thrown') 245 | 246 | 247 | def test_60_save(self): 248 | """Save the switch configurations. 249 | It's not clear that this can be easily tested, so we just 250 | see if this call works or not based on the http status code. 251 | """ 252 | self.node.save() 253 | self.assertEqual(self.node.request.status_code, 200) 254 | 255 | 256 | class SingleSwitchTopo(Topo): 257 | "Single switch connected to n hosts." 258 | def __init__(self, n=2, **opts): 259 | # Initialize topology and default options 260 | Topo.__init__(self, **opts) 261 | # mininet/ovswitch does not want ':'s in the dpid 262 | switch_id = SWITCH_1.translate(None, ':') 263 | switch = self.addSwitch('s1', dpid=switch_id) 264 | # Python's range(N) generates 0..N-1 265 | for h in range(n): 266 | host = self.addHost('h%s' % (h + 1)) 267 | self.addLink(host, switch) 268 | 269 | def setup_mininet_simpleTest(): 270 | "Create and test a simple network" 271 | topo = SingleSwitchTopo(n=4) 272 | #net = Mininet(topo) 273 | net = Mininet( topo=topo, controller=lambda name: RemoteController( 274 | name, ip=CONTROLLER ) ) 275 | net.start() 276 | #print "Dumping host connections" 277 | #dumpNodeConnections(net.hosts) 278 | 279 | #time.sleep(300) 280 | 281 | #print "Testing network connectivity" 282 | #net.pingAll() 283 | #net.stop() 284 | 285 | if __name__ == '__main__': 286 | # Tell mininet to print useful information 287 | #setLogLevel('info') 288 | 289 | setup_mininet_simpleTest() 290 | time.sleep(10) 291 | unittest.main() 292 | 293 | 294 | --------------------------------------------------------------------------------