Enabling Flake8 and import sorting
[osm/RO.git] / RO-SDN-floodlight_openflow / osm_rosdn_floodlightof / floodlight_of.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 ##
5 # Copyright 2015 Telefonica Investigacion y Desarrollo, S.A.U.
6 # This file is part of openvim
7 # All Rights Reserved.
8 #
9 # Licensed under the Apache License, Version 2.0 (the "License"); you may
10 # not use this file except in compliance with the License. You may obtain
11 # a copy of the License at
12 #
13 # http://www.apache.org/licenses/LICENSE-2.0
14 #
15 # Unless required by applicable law or agreed to in writing, software
16 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
17 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
18 # License for the specific language governing permissions and limitations
19 # under the License.
20 #
21 # For those usages not covered by the Apache License, Version 2.0 please
22 # contact with: nfvlabs@tid.es
23 ##
24
25 """
26 Implement the plugging for floodligth openflow controller
27 It creates the class OF_conn to create dataplane connections
28 with static rules based on packet destination MAC address
29 """
30
31 import json
32 import logging
33
34 from osm_ro_plugin.openflow_conn import (
35 OpenflowConn,
36 OpenflowConnConnectionException,
37 OpenflowConnUnexpectedResponse,
38 )
39 import requests
40
41 __author__ = "Pablo Montes, Alfonso Tierno"
42 __date__ = "$28-oct-2014 12:07:15$"
43
44
45 class OfConnFloodLight(OpenflowConn):
46 """
47 Openflow Connector for Floodlight.
48 No MAC learning is used
49 version 0.9 or 1.X is autodetected
50 version 1.X is in progress, not finished!!!
51 """
52
53 def __init__(self, params):
54 """
55 Constructor
56 :param params: dictionary with the following keys:
57 of_dpid: DPID to use for this controller ?? Does a controller have a dpid?
58 url: must be [http://HOST:PORT/]
59 of_user: user credentials, can be missing or None
60 of_password: password credentials
61 of_debug: debug level for logging. Default to ERROR
62 other keys are ignored
63 Raise an exception if same parameter is missing or wrong
64 """
65 # check params
66 url = params.get("of_url")
67
68 if not url:
69 raise ValueError("'url' must be provided")
70
71 if not url.startswith("http"):
72 url = "http://" + url
73
74 if not url.endswith("/"):
75 url = url + "/"
76
77 self.url = url
78
79 OpenflowConn.__init__(self, params)
80
81 self.name = "Floodlight"
82 self.dpid = str(params["of_dpid"])
83
84 self.pp2ofi = {} # From Physical Port to OpenFlow Index
85 self.ofi2pp = {} # From OpenFlow Index to Physical Port
86 self.headers = {
87 "content-type": "application/json",
88 "Accept": "application/json",
89 }
90 self.version = None
91 self.logger = logging.getLogger("ro.sdn.floodlightof")
92 self.logger.setLevel(params.get("of_debug", "ERROR"))
93 self._set_version(params.get("of_version"))
94
95 def _set_version(self, version):
96 """
97 set up a version of the controller.
98 Depending on the version it fills the self.ver_names with the naming used in this version
99 :param version: Openflow controller version
100 :return: Raise an ValueError exception if same parameter is missing or wrong
101 """
102 # static version names
103 if version is None:
104 self.version = None
105 elif version == "0.9":
106 self.version = version
107 self.name = "Floodlightv0.9"
108 self.ver_names = {
109 "dpid": "dpid",
110 "URLmodifier": "staticflowentrypusher",
111 "destmac": "dst-mac",
112 "vlanid": "vlan-id",
113 "inport": "ingress-port",
114 "setvlan": "set-vlan-id",
115 "stripvlan": "strip-vlan",
116 }
117 elif version[0] == "1": # version 1.X
118 self.version = version
119 self.name = "Floodlightv1.X"
120 self.ver_names = {
121 "dpid": "switchDPID",
122 "URLmodifier": "staticflowpusher",
123 "destmac": "eth_dst",
124 "vlanid": "eth_vlan_vid",
125 "inport": "in_port",
126 "setvlan": "set_vlan_vid",
127 "stripvlan": "strip_vlan",
128 }
129 else:
130 raise ValueError("Invalid version for floodlight controller")
131
132 def get_of_switches(self):
133 """
134 Obtain a a list of switches or DPID detected by this controller
135 :return: list where each element a tuple pair (DPID, IP address)
136 Raise an OpenflowconnConnectionException or OpenflowconnConnectionException exception if same
137 parameter is missing or wrong
138 """
139 try:
140 of_response = requests.get(
141 self.url + "wm/core/controller/switches/json", headers=self.headers
142 )
143 error_text = "Openflow response {}: {}".format(
144 of_response.status_code, of_response.text
145 )
146
147 if of_response.status_code != 200:
148 self.logger.warning("get_of_switches " + error_text)
149
150 raise OpenflowConnUnexpectedResponse(error_text)
151
152 self.logger.debug("get_of_switches " + error_text)
153 info = of_response.json()
154
155 if not isinstance(info, (list, tuple)):
156 self.logger.error(
157 "get_of_switches. Unexpected response not a list %s",
158 str(type(info)),
159 )
160
161 raise OpenflowConnUnexpectedResponse(
162 "Unexpected response, not a list. Wrong version?"
163 )
164
165 if len(info) == 0:
166 return info
167
168 # autodiscover version
169 if self.version is None:
170 if "dpid" in info[0] and "inetAddress" in info[0]:
171 self._set_version("0.9")
172 # elif 'switchDPID' in info[0] and 'inetAddress' in info[0]:
173 # self._set_version("1.X")
174 else:
175 self.logger.error(
176 "get_of_switches. Unexpected response, not found 'dpid' or 'switchDPID' "
177 "field: %s",
178 str(info[0]),
179 )
180
181 raise OpenflowConnUnexpectedResponse(
182 "Unexpected response, not found 'dpid' or "
183 "'switchDPID' field. Wrong version?"
184 )
185
186 switch_list = []
187 for switch in info:
188 switch_list.append(
189 (switch[self.ver_names["dpid"]], switch["inetAddress"])
190 )
191
192 return switch_list
193 except requests.exceptions.RequestException as e:
194 error_text = type(e).__name__ + ": " + str(e)
195 self.logger.error("get_of_switches " + error_text)
196
197 raise OpenflowConnConnectionException(error_text)
198 except Exception as e:
199 # ValueError in the case that JSON can not be decoded
200 error_text = type(e).__name__ + ": " + str(e)
201 self.logger.error("get_of_switches " + error_text)
202
203 raise OpenflowConnUnexpectedResponse(error_text)
204
205 def get_of_rules(self, translate_of_ports=True):
206 """
207 Obtain the rules inserted at openflow controller
208 :param translate_of_ports: if True it translates ports from openflow index to physical switch name
209 :return: list where each item is a dictionary with the following content:
210 priority: rule priority
211 name: rule name (present also as the master dict key)
212 ingress_port: match input port of the rule
213 dst_mac: match destination mac address of the rule, can be missing or None if not apply
214 vlan_id: match vlan tag of the rule, can be missing or None if not apply
215 actions: list of actions, composed by a pair tuples:
216 (vlan, None/int): for stripping/setting a vlan tag
217 (out, port): send to this port
218 switch: DPID, all
219 Raise an openflowconnUnexpectedResponse exception if fails with text_error
220 """
221 try:
222 # get translation, autodiscover version
223
224 if len(self.ofi2pp) == 0:
225 self.obtain_port_correspondence()
226
227 of_response = requests.get(
228 self.url
229 + "wm/{}/list/{}/json".format(self.ver_names["URLmodifier"], self.dpid),
230 headers=self.headers,
231 )
232 error_text = "Openflow response {}: {}".format(
233 of_response.status_code, of_response.text
234 )
235
236 if of_response.status_code != 200:
237 self.logger.warning("get_of_rules " + error_text)
238
239 raise OpenflowConnUnexpectedResponse(error_text)
240
241 self.logger.debug("get_of_rules " + error_text)
242 info = of_response.json()
243
244 if type(info) != dict:
245 self.logger.error(
246 "get_of_rules. Unexpected response not a dict %s", str(type(info))
247 )
248
249 raise OpenflowConnUnexpectedResponse(
250 "Unexpected response, not a dict. Wrong version?"
251 )
252
253 rule_list = []
254 for switch, switch_info in info.items():
255 if switch_info is None:
256 continue
257
258 if str(switch) != self.dpid:
259 continue
260
261 for name, details in switch_info.items():
262 rule = {"name": name, "switch": str(switch)}
263 # rule["active"] = "true"
264 rule["priority"] = int(details["priority"])
265
266 if self.version[0] == "0":
267 if translate_of_ports:
268 rule["ingress_port"] = self.ofi2pp[
269 details["match"]["inputPort"]
270 ]
271 else:
272 rule["ingress_port"] = str(details["match"]["inputPort"])
273
274 dst_mac = details["match"]["dataLayerDestination"]
275
276 if dst_mac != "00:00:00:00:00:00":
277 rule["dst_mac"] = dst_mac
278
279 vlan = details["match"]["dataLayerVirtualLan"]
280
281 if vlan != -1:
282 rule["vlan_id"] = vlan
283
284 actionlist = []
285
286 for action in details["actions"]:
287 if action["type"] == "OUTPUT":
288 if translate_of_ports:
289 port = self.ofi2pp[action["port"]]
290 else:
291 port = action["port"]
292 actionlist.append(("out", port))
293 elif action["type"] == "STRIP_VLAN":
294 actionlist.append(("vlan", None))
295 elif action["type"] == "SET_VLAN_ID":
296 actionlist.append(
297 ("vlan", action["virtualLanIdentifier"])
298 )
299 else:
300 actionlist.append((action["type"], str(action)))
301 self.logger.warning(
302 "get_of_rules() Unknown action in rule %s: %s",
303 rule["name"],
304 str(action),
305 )
306
307 rule["actions"] = actionlist
308 elif self.version[0] == "1":
309 if translate_of_ports:
310 rule["ingress_port"] = self.ofi2pp[
311 details["match"]["in_port"]
312 ]
313 else:
314 rule["ingress_port"] = details["match"]["in_port"]
315
316 if "eth_dst" in details["match"]:
317 dst_mac = details["match"]["eth_dst"]
318 if dst_mac != "00:00:00:00:00:00":
319 rule["dst_mac"] = dst_mac
320
321 if "eth_vlan_vid" in details["match"]:
322 vlan = int(details["match"]["eth_vlan_vid"], 16) & 0xFFF
323 rule["vlan_id"] = str(vlan)
324
325 actionlist = []
326 for action in details["instructions"][
327 "instruction_apply_actions"
328 ]:
329 if action == "output":
330 if translate_of_ports:
331 port = self.ofi2pp[
332 details["instructions"][
333 "instruction_apply_actions"
334 ]["output"]
335 ]
336 else:
337 port = details["instructions"][
338 "instruction_apply_actions"
339 ]["output"]
340 actionlist.append(("out", port))
341 elif action == "strip_vlan":
342 actionlist.append(("vlan", None))
343 elif action == "set_vlan_vid":
344 actionlist.append(
345 (
346 "vlan",
347 details["instructions"][
348 "instruction_apply_actions"
349 ]["set_vlan_vid"],
350 )
351 )
352 else:
353 self.logger.error(
354 "get_of_rules Unknown action in rule %s: %s",
355 rule["name"],
356 str(action),
357 )
358 # actionlist.append((action, str(details["instructions"]["instruction_apply_actions"])))
359
360 rule_list.append(rule)
361 return rule_list
362 except requests.exceptions.RequestException as e:
363 error_text = type(e).__name__ + ": " + str(e)
364 self.logger.error("get_of_rules " + error_text)
365
366 raise OpenflowConnConnectionException(error_text)
367 except Exception as e:
368 # ValueError in the case that JSON can not be decoded
369 error_text = type(e).__name__ + ": " + str(e)
370 self.logger.error("get_of_rules " + error_text)
371
372 raise OpenflowConnUnexpectedResponse(error_text)
373
374 def obtain_port_correspondence(self):
375 """
376 Obtain the correspondence between physical and openflow port names
377 :return: dictionary: with physical name as key, openflow name as value
378 Raise an openflowconnUnexpectedResponse exception if fails with text_error
379 """
380 try:
381 of_response = requests.get(
382 self.url + "wm/core/controller/switches/json", headers=self.headers
383 )
384 # print vim_response.status_code
385 error_text = "Openflow response {}: {}".format(
386 of_response.status_code, of_response.text
387 )
388
389 if of_response.status_code != 200:
390 self.logger.warning("obtain_port_correspondence " + error_text)
391
392 raise OpenflowConnUnexpectedResponse(error_text)
393
394 self.logger.debug("obtain_port_correspondence " + error_text)
395 info = of_response.json()
396
397 if not isinstance(info, (list, tuple)):
398 raise OpenflowConnUnexpectedResponse(
399 "unexpected openflow response, not a list. Wrong version?"
400 )
401
402 index = -1
403 if len(info) > 0:
404 # autodiscover version
405 if self.version is None:
406 if "dpid" in info[0] and "ports" in info[0]:
407 self._set_version("0.9")
408 elif "switchDPID" in info[0]:
409 self._set_version("1.X")
410 else:
411 raise OpenflowConnUnexpectedResponse(
412 "unexpected openflow response, Wrong version?"
413 )
414
415 for i, info_item in enumerate(info):
416 if info_item[self.ver_names["dpid"]] == self.dpid:
417 index = i
418 break
419
420 if index == -1:
421 text = "DPID '{}' not present in controller {}".format(
422 self.dpid, self.url
423 )
424 # print self.name, ": get_of_controller_info ERROR", text
425
426 raise OpenflowConnUnexpectedResponse(text)
427 else:
428 if self.version[0] == "0":
429 ports = info[index]["ports"]
430 else: # version 1.X
431 of_response = requests.get(
432 self.url + "wm/core/switch/{}/port-desc/json".format(self.dpid),
433 headers=self.headers,
434 )
435 # print vim_response.status_code
436 error_text = "Openflow response {}: {}".format(
437 of_response.status_code, of_response.text
438 )
439
440 if of_response.status_code != 200:
441 self.logger.warning("obtain_port_correspondence " + error_text)
442
443 raise OpenflowConnUnexpectedResponse(error_text)
444
445 self.logger.debug("obtain_port_correspondence " + error_text)
446 info = of_response.json()
447
448 if type(info) != dict:
449 raise OpenflowConnUnexpectedResponse(
450 "unexpected openflow port-desc response, "
451 "not a dict. Wrong version?"
452 )
453
454 if "portDesc" not in info:
455 raise OpenflowConnUnexpectedResponse(
456 "unexpected openflow port-desc response, "
457 "'portDesc' not found. Wrong version?"
458 )
459
460 if (
461 type(info["portDesc"]) != list
462 and type(info["portDesc"]) != tuple
463 ):
464 raise OpenflowConnUnexpectedResponse(
465 "unexpected openflow port-desc response at "
466 "'portDesc', not a list. Wrong version?"
467 )
468
469 ports = info["portDesc"]
470
471 for port in ports:
472 self.pp2ofi[str(port["name"])] = str(port["portNumber"])
473 self.ofi2pp[port["portNumber"]] = str(port["name"])
474 # print self.name, ": get_of_controller_info ports:", self.pp2ofi
475
476 return self.pp2ofi
477 except requests.exceptions.RequestException as e:
478 error_text = type(e).__name__ + ": " + str(e)
479 self.logger.error("obtain_port_correspondence " + error_text)
480
481 raise OpenflowConnConnectionException(error_text)
482 except Exception as e:
483 # ValueError in the case that JSON can not be decoded
484 error_text = type(e).__name__ + ": " + str(e)
485 self.logger.error("obtain_port_correspondence " + error_text)
486
487 raise OpenflowConnUnexpectedResponse(error_text)
488
489 def del_flow(self, flow_name):
490 """
491 Delete an existing rule
492 :param flow_name: this is the rule name
493 :return: None if ok
494 Raise an openflowconnUnexpectedResponse exception if fails with text_error
495 """
496 try:
497 if self.version is None:
498 self.get_of_switches()
499
500 of_response = requests.delete(
501 self.url + "wm/{}/json".format(self.ver_names["URLmodifier"]),
502 headers=self.headers,
503 data='{{"switch":"{}","name":"{}"}}'.format(self.dpid, flow_name),
504 )
505 error_text = "Openflow response {}: {}".format(
506 of_response.status_code, of_response.text
507 )
508
509 if of_response.status_code != 200:
510 self.logger.warning("del_flow " + error_text)
511
512 raise OpenflowConnUnexpectedResponse(error_text)
513
514 self.logger.debug("del_flow OK " + error_text)
515
516 return None
517
518 except requests.exceptions.RequestException as e:
519 error_text = type(e).__name__ + ": " + str(e)
520 self.logger.error("del_flow " + error_text)
521
522 raise OpenflowConnConnectionException(error_text)
523 except Exception as e:
524 # ValueError in the case that JSON can not be decoded
525 error_text = type(e).__name__ + ": " + str(e)
526 self.logger.error("del_flow " + error_text)
527
528 raise OpenflowConnUnexpectedResponse(error_text)
529
530 def new_flow(self, data):
531 """
532 Insert a new static rule
533 :param data: dictionary with the following content:
534 priority: rule priority
535 name: rule name
536 ingress_port: match input port of the rule
537 dst_mac: match destination mac address of the rule, missing or None if not apply
538 vlan_id: match vlan tag of the rule, missing or None if not apply
539 actions: list of actions, composed by a pair tuples with these posibilities:
540 ('vlan', None/int): for stripping/setting a vlan tag
541 ('out', port): send to this port
542 :return: None if ok
543 Raise an openflowconnUnexpectedResponse exception if fails with text_error
544 """
545 # get translation, autodiscover version
546 if len(self.pp2ofi) == 0:
547 self.obtain_port_correspondence()
548
549 try:
550 # We have to build the data for the floodlight call from the generic data
551 sdata = {"active": "true", "name": data["name"]}
552
553 if data.get("priority"):
554 sdata["priority"] = str(data["priority"])
555
556 if data.get("vlan_id"):
557 sdata[self.ver_names["vlanid"]] = data["vlan_id"]
558
559 if data.get("dst_mac"):
560 sdata[self.ver_names["destmac"]] = data["dst_mac"]
561
562 sdata["switch"] = self.dpid
563 if not data["ingress_port"] in self.pp2ofi:
564 error_text = "Error. Port {} is not present in the switch".format(
565 data["ingress_port"]
566 )
567 self.logger.warning("new_flow " + error_text)
568 raise OpenflowConnUnexpectedResponse(error_text)
569
570 sdata[self.ver_names["inport"]] = self.pp2ofi[data["ingress_port"]]
571 sdata["actions"] = ""
572
573 for action in data["actions"]:
574 if len(sdata["actions"]) > 0:
575 sdata["actions"] += ","
576
577 if action[0] == "vlan":
578 if action[1] is None:
579 sdata["actions"] += self.ver_names["stripvlan"]
580 else:
581 sdata["actions"] += (
582 self.ver_names["setvlan"] + "=" + str(action[1])
583 )
584 elif action[0] == "out":
585 sdata["actions"] += "output=" + self.pp2ofi[action[1]]
586
587 of_response = requests.post(
588 self.url + "wm/{}/json".format(self.ver_names["URLmodifier"]),
589 headers=self.headers,
590 data=json.dumps(sdata),
591 )
592 error_text = "Openflow response {}: {}".format(
593 of_response.status_code, of_response.text
594 )
595
596 if of_response.status_code != 200:
597 self.logger.warning("new_flow " + error_text)
598 raise OpenflowConnUnexpectedResponse(error_text)
599
600 self.logger.debug("new_flow OK" + error_text)
601
602 return None
603
604 except requests.exceptions.RequestException as e:
605 error_text = type(e).__name__ + ": " + str(e)
606 self.logger.error("new_flow " + error_text)
607 raise OpenflowConnConnectionException(error_text)
608 except Exception as e:
609 # ValueError in the case that JSON can not be decoded
610 error_text = type(e).__name__ + ": " + str(e)
611 self.logger.error("new_flow " + error_text)
612 raise OpenflowConnUnexpectedResponse(error_text)
613
614 def clear_all_flows(self):
615 """
616 Delete all existing rules
617 :return: None if ok
618 Raise an openflowconnUnexpectedResponse exception if fails with text_error
619 """
620
621 try:
622 # autodiscover version
623 if self.version is None:
624 sw_list = self.get_of_switches()
625 if len(sw_list) == 0: # empty
626 return None
627
628 url = self.url + "wm/{}/clear/{}/json".format(
629 self.ver_names["URLmodifier"], self.dpid
630 )
631 of_response = requests.get(url)
632 error_text = "Openflow response {}: {}".format(
633 of_response.status_code, of_response.text
634 )
635
636 if of_response.status_code < 200 or of_response.status_code >= 300:
637 self.logger.warning("clear_all_flows " + error_text)
638 raise OpenflowConnUnexpectedResponse(error_text)
639
640 self.logger.debug("clear_all_flows OK " + error_text)
641
642 return None
643 except requests.exceptions.RequestException as e:
644 error_text = type(e).__name__ + ": " + str(e)
645 self.logger.error("clear_all_flows " + error_text)
646
647 raise OpenflowConnConnectionException(error_text)
648 except Exception as e:
649 # ValueError in the case that JSON can not be decoded
650 error_text = type(e).__name__ + ": " + str(e)
651 self.logger.error("clear_all_flows " + error_text)
652
653 raise OpenflowConnUnexpectedResponse(error_text)