2 # -*- coding: utf-8 -*-
5 # Copyright 2015 Telefonica Investigacion y Desarrollo, S.A.U.
6 # This file is part of openvim
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
13 # http://www.apache.org/licenses/LICENSE-2.0
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
21 # For those usages not covered by the Apache License, Version 2.0 please
22 # contact with: nfvlabs@tid.es
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
34 from osm_ro_plugin
.openflow_conn
import (
36 OpenflowConnConnectionException
,
37 OpenflowConnUnexpectedResponse
,
41 __author__
= "Pablo Montes, Alfonso Tierno"
42 __date__
= "$28-oct-2014 12:07:15$"
45 class OfConnFloodLight(OpenflowConn
):
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!!!
53 def __init__(self
, params
):
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
66 url
= params
.get("of_url")
69 raise ValueError("'url' must be provided")
71 if not url
.startswith("http"):
74 if not url
.endswith("/"):
79 OpenflowConn
.__init
__(self
, params
)
81 self
.name
= "Floodlight"
82 self
.dpid
= str(params
["of_dpid"])
84 self
.pp2ofi
= {} # From Physical Port to OpenFlow Index
85 self
.ofi2pp
= {} # From OpenFlow Index to Physical Port
87 "content-type": "application/json",
88 "Accept": "application/json",
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"))
95 def _set_version(self
, version
):
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
102 # static version names
105 elif version
== "0.9":
106 self
.version
= version
107 self
.name
= "Floodlightv0.9"
110 "URLmodifier": "staticflowentrypusher",
111 "destmac": "dst-mac",
113 "inport": "ingress-port",
114 "setvlan": "set-vlan-id",
115 "stripvlan": "strip-vlan",
117 elif version
[0] == "1": # version 1.X
118 self
.version
= version
119 self
.name
= "Floodlightv1.X"
121 "dpid": "switchDPID",
122 "URLmodifier": "staticflowpusher",
123 "destmac": "eth_dst",
124 "vlanid": "eth_vlan_vid",
126 "setvlan": "set_vlan_vid",
127 "stripvlan": "strip_vlan",
130 raise ValueError("Invalid version for floodlight controller")
132 def get_of_switches(self
):
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
140 of_response
= requests
.get(
141 self
.url
+ "wm/core/controller/switches/json", headers
=self
.headers
143 error_text
= "Openflow response {}: {}".format(
144 of_response
.status_code
, of_response
.text
147 if of_response
.status_code
!= 200:
148 self
.logger
.warning("get_of_switches " + error_text
)
150 raise OpenflowConnUnexpectedResponse(error_text
)
152 self
.logger
.debug("get_of_switches " + error_text
)
153 info
= of_response
.json()
155 if not isinstance(info
, (list, tuple)):
157 "get_of_switches. Unexpected response not a list %s",
161 raise OpenflowConnUnexpectedResponse(
162 "Unexpected response, not a list. Wrong version?"
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")
176 "get_of_switches. Unexpected response, not found 'dpid' or 'switchDPID' "
181 raise OpenflowConnUnexpectedResponse(
182 "Unexpected response, not found 'dpid' or "
183 "'switchDPID' field. Wrong version?"
189 (switch
[self
.ver_names
["dpid"]], switch
["inetAddress"])
193 except requests
.exceptions
.RequestException
as e
:
194 error_text
= type(e
).__name
__ + ": " + str(e
)
195 self
.logger
.error("get_of_switches " + error_text
)
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
)
203 raise OpenflowConnUnexpectedResponse(error_text
)
205 def get_of_rules(self
, translate_of_ports
=True):
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
219 Raise an openflowconnUnexpectedResponse exception if fails with text_error
222 # get translation, autodiscover version
224 if len(self
.ofi2pp
) == 0:
225 self
.obtain_port_correspondence()
227 of_response
= requests
.get(
229 + "wm/{}/list/{}/json".format(self
.ver_names
["URLmodifier"], self
.dpid
),
230 headers
=self
.headers
,
232 error_text
= "Openflow response {}: {}".format(
233 of_response
.status_code
, of_response
.text
236 if of_response
.status_code
!= 200:
237 self
.logger
.warning("get_of_rules " + error_text
)
239 raise OpenflowConnUnexpectedResponse(error_text
)
241 self
.logger
.debug("get_of_rules " + error_text
)
242 info
= of_response
.json()
244 if type(info
) != dict:
246 "get_of_rules. Unexpected response not a dict %s", str(type(info
))
249 raise OpenflowConnUnexpectedResponse(
250 "Unexpected response, not a dict. Wrong version?"
254 for switch
, switch_info
in info
.items():
255 if switch_info
is None:
258 if str(switch
) != self
.dpid
:
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"])
266 if self
.version
[0] == "0":
267 if translate_of_ports
:
268 rule
["ingress_port"] = self
.ofi2pp
[
269 details
["match"]["inputPort"]
272 rule
["ingress_port"] = str(details
["match"]["inputPort"])
274 dst_mac
= details
["match"]["dataLayerDestination"]
276 if dst_mac
!= "00:00:00:00:00:00":
277 rule
["dst_mac"] = dst_mac
279 vlan
= details
["match"]["dataLayerVirtualLan"]
282 rule
["vlan_id"] = vlan
286 for action
in details
["actions"]:
287 if action
["type"] == "OUTPUT":
288 if translate_of_ports
:
289 port
= self
.ofi2pp
[action
["port"]]
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":
297 ("vlan", action
["virtualLanIdentifier"])
300 actionlist
.append((action
["type"], str(action
)))
302 "get_of_rules() Unknown action in rule %s: %s",
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"]
314 rule
["ingress_port"] = details
["match"]["in_port"]
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
321 if "eth_vlan_vid" in details
["match"]:
322 vlan
= int(details
["match"]["eth_vlan_vid"], 16) & 0xFFF
323 rule
["vlan_id"] = str(vlan
)
326 for action
in details
["instructions"][
327 "instruction_apply_actions"
329 if action
== "output":
330 if translate_of_ports
:
332 details
["instructions"][
333 "instruction_apply_actions"
337 port
= details
["instructions"][
338 "instruction_apply_actions"
340 actionlist
.append(("out", port
))
341 elif action
== "strip_vlan":
342 actionlist
.append(("vlan", None))
343 elif action
== "set_vlan_vid":
347 details
["instructions"][
348 "instruction_apply_actions"
354 "get_of_rules Unknown action in rule %s: %s",
358 # actionlist.append((action, str(details["instructions"]["instruction_apply_actions"])))
360 rule_list
.append(rule
)
362 except requests
.exceptions
.RequestException
as e
:
363 error_text
= type(e
).__name
__ + ": " + str(e
)
364 self
.logger
.error("get_of_rules " + error_text
)
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
)
372 raise OpenflowConnUnexpectedResponse(error_text
)
374 def obtain_port_correspondence(self
):
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
381 of_response
= requests
.get(
382 self
.url
+ "wm/core/controller/switches/json", headers
=self
.headers
384 # print vim_response.status_code
385 error_text
= "Openflow response {}: {}".format(
386 of_response
.status_code
, of_response
.text
389 if of_response
.status_code
!= 200:
390 self
.logger
.warning("obtain_port_correspondence " + error_text
)
392 raise OpenflowConnUnexpectedResponse(error_text
)
394 self
.logger
.debug("obtain_port_correspondence " + error_text
)
395 info
= of_response
.json()
397 if not isinstance(info
, (list, tuple)):
398 raise OpenflowConnUnexpectedResponse(
399 "unexpected openflow response, not a list. Wrong version?"
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")
411 raise OpenflowConnUnexpectedResponse(
412 "unexpected openflow response, Wrong version?"
415 for i
, info_item
in enumerate(info
):
416 if info_item
[self
.ver_names
["dpid"]] == self
.dpid
:
421 text
= "DPID '{}' not present in controller {}".format(
424 # print self.name, ": get_of_controller_info ERROR", text
426 raise OpenflowConnUnexpectedResponse(text
)
428 if self
.version
[0] == "0":
429 ports
= info
[index
]["ports"]
431 of_response
= requests
.get(
432 self
.url
+ "wm/core/switch/{}/port-desc/json".format(self
.dpid
),
433 headers
=self
.headers
,
435 # print vim_response.status_code
436 error_text
= "Openflow response {}: {}".format(
437 of_response
.status_code
, of_response
.text
440 if of_response
.status_code
!= 200:
441 self
.logger
.warning("obtain_port_correspondence " + error_text
)
443 raise OpenflowConnUnexpectedResponse(error_text
)
445 self
.logger
.debug("obtain_port_correspondence " + error_text
)
446 info
= of_response
.json()
448 if type(info
) != dict:
449 raise OpenflowConnUnexpectedResponse(
450 "unexpected openflow port-desc response, "
451 "not a dict. Wrong version?"
454 if "portDesc" not in info
:
455 raise OpenflowConnUnexpectedResponse(
456 "unexpected openflow port-desc response, "
457 "'portDesc' not found. Wrong version?"
461 type(info
["portDesc"]) != list
462 and type(info
["portDesc"]) != tuple
464 raise OpenflowConnUnexpectedResponse(
465 "unexpected openflow port-desc response at "
466 "'portDesc', not a list. Wrong version?"
469 ports
= info
["portDesc"]
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
477 except requests
.exceptions
.RequestException
as e
:
478 error_text
= type(e
).__name
__ + ": " + str(e
)
479 self
.logger
.error("obtain_port_correspondence " + error_text
)
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
)
487 raise OpenflowConnUnexpectedResponse(error_text
)
489 def del_flow(self
, flow_name
):
491 Delete an existing rule
492 :param flow_name: this is the rule name
494 Raise an openflowconnUnexpectedResponse exception if fails with text_error
497 if self
.version
is None:
498 self
.get_of_switches()
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
),
505 error_text
= "Openflow response {}: {}".format(
506 of_response
.status_code
, of_response
.text
509 if of_response
.status_code
!= 200:
510 self
.logger
.warning("del_flow " + error_text
)
512 raise OpenflowConnUnexpectedResponse(error_text
)
514 self
.logger
.debug("del_flow OK " + error_text
)
518 except requests
.exceptions
.RequestException
as e
:
519 error_text
= type(e
).__name
__ + ": " + str(e
)
520 self
.logger
.error("del_flow " + error_text
)
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
)
528 raise OpenflowConnUnexpectedResponse(error_text
)
530 def new_flow(self
, data
):
532 Insert a new static rule
533 :param data: dictionary with the following content:
534 priority: rule priority
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
543 Raise an openflowconnUnexpectedResponse exception if fails with text_error
545 # get translation, autodiscover version
546 if len(self
.pp2ofi
) == 0:
547 self
.obtain_port_correspondence()
550 # We have to build the data for the floodlight call from the generic data
551 sdata
= {"active": "true", "name": data
["name"]}
553 if data
.get("priority"):
554 sdata
["priority"] = str(data
["priority"])
556 if data
.get("vlan_id"):
557 sdata
[self
.ver_names
["vlanid"]] = data
["vlan_id"]
559 if data
.get("dst_mac"):
560 sdata
[self
.ver_names
["destmac"]] = data
["dst_mac"]
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(
567 self
.logger
.warning("new_flow " + error_text
)
568 raise OpenflowConnUnexpectedResponse(error_text
)
570 sdata
[self
.ver_names
["inport"]] = self
.pp2ofi
[data
["ingress_port"]]
571 sdata
["actions"] = ""
573 for action
in data
["actions"]:
574 if len(sdata
["actions"]) > 0:
575 sdata
["actions"] += ","
577 if action
[0] == "vlan":
578 if action
[1] is None:
579 sdata
["actions"] += self
.ver_names
["stripvlan"]
581 sdata
["actions"] += (
582 self
.ver_names
["setvlan"] + "=" + str(action
[1])
584 elif action
[0] == "out":
585 sdata
["actions"] += "output=" + self
.pp2ofi
[action
[1]]
587 of_response
= requests
.post(
588 self
.url
+ "wm/{}/json".format(self
.ver_names
["URLmodifier"]),
589 headers
=self
.headers
,
590 data
=json
.dumps(sdata
),
592 error_text
= "Openflow response {}: {}".format(
593 of_response
.status_code
, of_response
.text
596 if of_response
.status_code
!= 200:
597 self
.logger
.warning("new_flow " + error_text
)
598 raise OpenflowConnUnexpectedResponse(error_text
)
600 self
.logger
.debug("new_flow OK" + error_text
)
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
)
614 def clear_all_flows(self
):
616 Delete all existing rules
618 Raise an openflowconnUnexpectedResponse exception if fails with text_error
622 # autodiscover version
623 if self
.version
is None:
624 sw_list
= self
.get_of_switches()
625 if len(sw_list
) == 0: # empty
628 url
= self
.url
+ "wm/{}/clear/{}/json".format(
629 self
.ver_names
["URLmodifier"], self
.dpid
631 of_response
= requests
.get(url
)
632 error_text
= "Openflow response {}: {}".format(
633 of_response
.status_code
, of_response
.text
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
)
640 self
.logger
.debug("clear_all_flows OK " + error_text
)
643 except requests
.exceptions
.RequestException
as e
:
644 error_text
= type(e
).__name
__ + ": " + str(e
)
645 self
.logger
.error("clear_all_flows " + error_text
)
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
)
653 raise OpenflowConnUnexpectedResponse(error_text
)