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