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
31 __author__
= "Pablo Montes, Alfonso Tierno"
32 __date__
= "$28-oct-2014 12:07:15$"
37 from osm_ro_plugin
.openflow_conn
import (
39 OpenflowConnUnexpectedResponse
,
40 OpenflowConnConnectionException
,
44 class OfConnFloodLight(OpenflowConn
):
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!!!
52 def __init__(self
, params
):
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
65 url
= params
.get("of_url")
68 raise ValueError("'url' must be provided")
70 if not url
.startswith("http"):
73 if not url
.endswith("/"):
78 OpenflowConn
.__init
__(self
, params
)
80 self
.name
= "Floodlight"
81 self
.dpid
= str(params
["of_dpid"])
83 self
.pp2ofi
= {} # From Physical Port to OpenFlow Index
84 self
.ofi2pp
= {} # From OpenFlow Index to Physical Port
86 "content-type": "application/json",
87 "Accept": "application/json",
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"))
94 def _set_version(self
, version
):
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
101 # static version names
104 elif version
== "0.9":
105 self
.version
= version
106 self
.name
= "Floodlightv0.9"
109 "URLmodifier": "staticflowentrypusher",
110 "destmac": "dst-mac",
112 "inport": "ingress-port",
113 "setvlan": "set-vlan-id",
114 "stripvlan": "strip-vlan",
116 elif version
[0] == "1": # version 1.X
117 self
.version
= version
118 self
.name
= "Floodlightv1.X"
120 "dpid": "switchDPID",
121 "URLmodifier": "staticflowpusher",
122 "destmac": "eth_dst",
123 "vlanid": "eth_vlan_vid",
125 "setvlan": "set_vlan_vid",
126 "stripvlan": "strip_vlan",
129 raise ValueError("Invalid version for floodlight controller")
131 def get_of_switches(self
):
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
139 of_response
= requests
.get(
140 self
.url
+ "wm/core/controller/switches/json", headers
=self
.headers
142 error_text
= "Openflow response {}: {}".format(
143 of_response
.status_code
, of_response
.text
146 if of_response
.status_code
!= 200:
147 self
.logger
.warning("get_of_switches " + error_text
)
149 raise OpenflowConnUnexpectedResponse(error_text
)
151 self
.logger
.debug("get_of_switches " + error_text
)
152 info
= of_response
.json()
154 if not isinstance(info
, (list, tuple)):
156 "get_of_switches. Unexpected response not a list %s",
160 raise OpenflowConnUnexpectedResponse(
161 "Unexpected response, not a list. Wrong version?"
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")
175 "get_of_switches. Unexpected response, not found 'dpid' or 'switchDPID' "
180 raise OpenflowConnUnexpectedResponse(
181 "Unexpected response, not found 'dpid' or "
182 "'switchDPID' field. Wrong version?"
188 (switch
[self
.ver_names
["dpid"]], switch
["inetAddress"])
192 except requests
.exceptions
.RequestException
as e
:
193 error_text
= type(e
).__name
__ + ": " + str(e
)
194 self
.logger
.error("get_of_switches " + error_text
)
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
)
202 raise OpenflowConnUnexpectedResponse(error_text
)
204 def get_of_rules(self
, translate_of_ports
=True):
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
218 Raise an openflowconnUnexpectedResponse exception if fails with text_error
221 # get translation, autodiscover version
223 if len(self
.ofi2pp
) == 0:
224 self
.obtain_port_correspondence()
226 of_response
= requests
.get(
228 + "wm/{}/list/{}/json".format(self
.ver_names
["URLmodifier"], self
.dpid
),
229 headers
=self
.headers
,
231 error_text
= "Openflow response {}: {}".format(
232 of_response
.status_code
, of_response
.text
235 if of_response
.status_code
!= 200:
236 self
.logger
.warning("get_of_rules " + error_text
)
238 raise OpenflowConnUnexpectedResponse(error_text
)
240 self
.logger
.debug("get_of_rules " + error_text
)
241 info
= of_response
.json()
243 if type(info
) != dict:
245 "get_of_rules. Unexpected response not a dict %s", str(type(info
))
248 raise OpenflowConnUnexpectedResponse(
249 "Unexpected response, not a dict. Wrong version?"
253 for switch
, switch_info
in info
.items():
254 if switch_info
is None:
257 if str(switch
) != self
.dpid
:
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"])
265 if self
.version
[0] == "0":
266 if translate_of_ports
:
267 rule
["ingress_port"] = self
.ofi2pp
[
268 details
["match"]["inputPort"]
271 rule
["ingress_port"] = str(details
["match"]["inputPort"])
273 dst_mac
= details
["match"]["dataLayerDestination"]
275 if dst_mac
!= "00:00:00:00:00:00":
276 rule
["dst_mac"] = dst_mac
278 vlan
= details
["match"]["dataLayerVirtualLan"]
281 rule
["vlan_id"] = vlan
285 for action
in details
["actions"]:
286 if action
["type"] == "OUTPUT":
287 if translate_of_ports
:
288 port
= self
.ofi2pp
[action
["port"]]
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":
296 ("vlan", action
["virtualLanIdentifier"])
299 actionlist
.append((action
["type"], str(action
)))
301 "get_of_rules() Unknown action in rule %s: %s",
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"]
313 rule
["ingress_port"] = details
["match"]["in_port"]
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
320 if "eth_vlan_vid" in details
["match"]:
321 vlan
= int(details
["match"]["eth_vlan_vid"], 16) & 0xFFF
322 rule
["vlan_id"] = str(vlan
)
325 for action
in details
["instructions"][
326 "instruction_apply_actions"
328 if action
== "output":
329 if translate_of_ports
:
331 details
["instructions"][
332 "instruction_apply_actions"
336 port
= details
["instructions"][
337 "instruction_apply_actions"
339 actionlist
.append(("out", port
))
340 elif action
== "strip_vlan":
341 actionlist
.append(("vlan", None))
342 elif action
== "set_vlan_vid":
346 details
["instructions"][
347 "instruction_apply_actions"
353 "get_of_rules Unknown action in rule %s: %s",
357 # actionlist.append((action, str(details["instructions"]["instruction_apply_actions"])))
359 rule_list
.append(rule
)
361 except requests
.exceptions
.RequestException
as e
:
362 error_text
= type(e
).__name
__ + ": " + str(e
)
363 self
.logger
.error("get_of_rules " + error_text
)
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
)
371 raise OpenflowConnUnexpectedResponse(error_text
)
373 def obtain_port_correspondence(self
):
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
380 of_response
= requests
.get(
381 self
.url
+ "wm/core/controller/switches/json", headers
=self
.headers
383 # print vim_response.status_code
384 error_text
= "Openflow response {}: {}".format(
385 of_response
.status_code
, of_response
.text
388 if of_response
.status_code
!= 200:
389 self
.logger
.warning("obtain_port_correspondence " + error_text
)
391 raise OpenflowConnUnexpectedResponse(error_text
)
393 self
.logger
.debug("obtain_port_correspondence " + error_text
)
394 info
= of_response
.json()
396 if not isinstance(info
, (list, tuple)):
397 raise OpenflowConnUnexpectedResponse(
398 "unexpected openflow response, not a list. Wrong version?"
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")
410 raise OpenflowConnUnexpectedResponse(
411 "unexpected openflow response, Wrong version?"
414 for i
, info_item
in enumerate(info
):
415 if info_item
[self
.ver_names
["dpid"]] == self
.dpid
:
420 text
= "DPID '{}' not present in controller {}".format(
423 # print self.name, ": get_of_controller_info ERROR", text
425 raise OpenflowConnUnexpectedResponse(text
)
427 if self
.version
[0] == "0":
428 ports
= info
[index
]["ports"]
430 of_response
= requests
.get(
431 self
.url
+ "wm/core/switch/{}/port-desc/json".format(self
.dpid
),
432 headers
=self
.headers
,
434 # print vim_response.status_code
435 error_text
= "Openflow response {}: {}".format(
436 of_response
.status_code
, of_response
.text
439 if of_response
.status_code
!= 200:
440 self
.logger
.warning("obtain_port_correspondence " + error_text
)
442 raise OpenflowConnUnexpectedResponse(error_text
)
444 self
.logger
.debug("obtain_port_correspondence " + error_text
)
445 info
= of_response
.json()
447 if type(info
) != dict:
448 raise OpenflowConnUnexpectedResponse(
449 "unexpected openflow port-desc response, "
450 "not a dict. Wrong version?"
453 if "portDesc" not in info
:
454 raise OpenflowConnUnexpectedResponse(
455 "unexpected openflow port-desc response, "
456 "'portDesc' not found. Wrong version?"
460 type(info
["portDesc"]) != list
461 and type(info
["portDesc"]) != tuple
463 raise OpenflowConnUnexpectedResponse(
464 "unexpected openflow port-desc response at "
465 "'portDesc', not a list. Wrong version?"
468 ports
= info
["portDesc"]
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
476 except requests
.exceptions
.RequestException
as e
:
477 error_text
= type(e
).__name
__ + ": " + str(e
)
478 self
.logger
.error("obtain_port_correspondence " + error_text
)
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
)
486 raise OpenflowConnUnexpectedResponse(error_text
)
488 def del_flow(self
, flow_name
):
490 Delete an existing rule
491 :param flow_name: this is the rule name
493 Raise an openflowconnUnexpectedResponse exception if fails with text_error
496 if self
.version
is None:
497 self
.get_of_switches()
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
),
504 error_text
= "Openflow response {}: {}".format(
505 of_response
.status_code
, of_response
.text
508 if of_response
.status_code
!= 200:
509 self
.logger
.warning("del_flow " + error_text
)
511 raise OpenflowConnUnexpectedResponse(error_text
)
513 self
.logger
.debug("del_flow OK " + error_text
)
517 except requests
.exceptions
.RequestException
as e
:
518 error_text
= type(e
).__name
__ + ": " + str(e
)
519 self
.logger
.error("del_flow " + error_text
)
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
)
527 raise OpenflowConnUnexpectedResponse(error_text
)
529 def new_flow(self
, data
):
531 Insert a new static rule
532 :param data: dictionary with the following content:
533 priority: rule priority
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
542 Raise an openflowconnUnexpectedResponse exception if fails with text_error
544 # get translation, autodiscover version
545 if len(self
.pp2ofi
) == 0:
546 self
.obtain_port_correspondence()
549 # We have to build the data for the floodlight call from the generic data
550 sdata
= {"active": "true", "name": data
["name"]}
552 if data
.get("priority"):
553 sdata
["priority"] = str(data
["priority"])
555 if data
.get("vlan_id"):
556 sdata
[self
.ver_names
["vlanid"]] = data
["vlan_id"]
558 if data
.get("dst_mac"):
559 sdata
[self
.ver_names
["destmac"]] = data
["dst_mac"]
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(
566 self
.logger
.warning("new_flow " + error_text
)
567 raise OpenflowConnUnexpectedResponse(error_text
)
569 sdata
[self
.ver_names
["inport"]] = self
.pp2ofi
[data
["ingress_port"]]
570 sdata
["actions"] = ""
572 for action
in data
["actions"]:
573 if len(sdata
["actions"]) > 0:
574 sdata
["actions"] += ","
576 if action
[0] == "vlan":
577 if action
[1] is None:
578 sdata
["actions"] += self
.ver_names
["stripvlan"]
580 sdata
["actions"] += (
581 self
.ver_names
["setvlan"] + "=" + str(action
[1])
583 elif action
[0] == "out":
584 sdata
["actions"] += "output=" + self
.pp2ofi
[action
[1]]
586 of_response
= requests
.post(
587 self
.url
+ "wm/{}/json".format(self
.ver_names
["URLmodifier"]),
588 headers
=self
.headers
,
589 data
=json
.dumps(sdata
),
591 error_text
= "Openflow response {}: {}".format(
592 of_response
.status_code
, of_response
.text
595 if of_response
.status_code
!= 200:
596 self
.logger
.warning("new_flow " + error_text
)
597 raise OpenflowConnUnexpectedResponse(error_text
)
599 self
.logger
.debug("new_flow OK" + error_text
)
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
)
613 def clear_all_flows(self
):
615 Delete all existing rules
617 Raise an openflowconnUnexpectedResponse exception if fails with text_error
621 # autodiscover version
622 if self
.version
is None:
623 sw_list
= self
.get_of_switches()
624 if len(sw_list
) == 0: # empty
627 url
= self
.url
+ "wm/{}/clear/{}/json".format(
628 self
.ver_names
["URLmodifier"], self
.dpid
630 of_response
= requests
.get(url
)
631 error_text
= "Openflow response {}: {}".format(
632 of_response
.status_code
, of_response
.text
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
)
639 self
.logger
.debug("clear_all_flows OK " + error_text
)
642 except requests
.exceptions
.RequestException
as e
:
643 error_text
= type(e
).__name
__ + ": " + str(e
)
644 self
.logger
.error("clear_all_flows " + error_text
)
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
)
652 raise OpenflowConnUnexpectedResponse(error_text
)