2 # -*- coding: utf-8 -*-
5 # Copyright 2015 Telefónica Investigación 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$"
40 ''' Openflow Connector for Floodlight.
41 No MAC learning is used
42 version 0.9 or 1.X is autodetected
43 version 1.X is in progress, not finished!!!
45 def __init__(self
, params
):
47 params is a dictionay with the following keys:
48 of_dpid: DPID to use for this controller
49 of_ip: controller IP address
50 of_port: controller TCP port
51 of_version: version, can be "0.9" or "1.X". By default it is autodetected
52 of_debug: debug level for logging. Default to ERROR
53 other keys are ignored
54 Raise an exception if same parameter is missing or wrong
57 if "of_ip" not in params
or params
["of_ip"]==None or "of_port" not in params
or params
["of_port"]==None:
58 raise ValueError("IP address and port must be provided")
60 self
.name
= "Floodlight"
61 self
.dpid
= str(params
["of_dpid"])
62 self
.url
= "http://%s:%s" %( str(params
["of_ip"]), str(params
["of_port"]) )
64 self
.pp2ofi
={} # From Physical Port to OpenFlow Index
65 self
.ofi2pp
={} # From OpenFlow Index to Physical Port
66 self
.headers
= {'content-type':'application/json', 'Accept':'application/json'}
68 self
.logger
= logging
.getLogger('vim.OF.FL')
69 self
.logger
.setLevel( getattr(logging
, params
.get("of_debug", "ERROR") ) )
70 self
._set
_version
(params
.get("of_version") )
72 def _set_version(self
, version
):
73 '''set up a version of the controller.
74 Depending on the version it fills the self.ver_names with the naming used in this version
81 self
.name
= "Floodlightv0.9"
84 "URLmodifier": "staticflowentrypusher",
87 "inport": "ingress-port",
88 "setvlan": "set-vlan-id",
89 "stripvlan": "strip-vlan",
91 elif version
[0]=="1" : #version 1.X
93 self
.name
= "Floodlightv1.X"
96 "URLmodifier": "staticflowpusher",
98 "vlanid": "eth_vlan_vid",
100 "setvlan": "set_vlan_vid",
101 "stripvlan": "strip_vlan",
104 raise ValueError("Invalid version for floodlight controller")
106 def get_of_switches(self
):
107 ''' Obtain a a list of switches or DPID detected by this controller
109 >=0, list: list length, and a list where each element a tuple pair (DPID, IP address)
110 <0, text_error: if fails
113 of_response
= requests
.get(self
.url
+"/wm/core/controller/switches/json", headers
=self
.headers
)
114 error_text
= "Openflow response %d: %s" % (of_response
.status_code
, of_response
.text
)
115 if of_response
.status_code
!= 200:
116 self
.logger
.warning("get_of_switches " + error_text
)
117 return -1 , error_text
118 self
.logger
.debug("get_of_switches " + error_text
)
119 info
= of_response
.json()
120 if type(info
) != list and type(info
) != tuple:
121 self
.logger
.error("get_of_switches. Unexpected response not a list %s", str(type(info
)))
122 return -1, "Unexpected response, not a list. Wrong version?"
125 #autodiscover version
126 if self
.version
== None:
127 if 'dpid' in info
[0] and 'inetAddress' in info
[0]:
128 self
._set
_version
("0.9")
129 elif 'switchDPID' in info
[0] and 'inetAddress' in info
[0]:
130 self
._set
_version
("1.X")
132 self
.logger
.error("get_of_switches. Unexpected response, not found 'dpid' or 'switchDPID' field: %s", str(info
[0]))
133 return -1, "Unexpected response, not found 'dpid' or 'switchDPID' field. Wrong version?"
137 switch_list
.append( (switch
[ self
.ver_names
["dpid"] ], switch
['inetAddress']) )
138 return len(switch_list
), switch_list
139 except (requests
.exceptions
.RequestException
, ValueError) as e
:
140 #ValueError in the case that JSON can not be decoded
141 error_text
= type(e
).__name
__ + ": " + str(e
)
142 self
.logger
.error("get_of_switches " + error_text
)
143 return -1, error_text
145 def get_of_rules(self
, translate_of_ports
=True):
146 ''' Obtain the rules inserted at openflow controller
148 translate_of_ports: if True it translates ports from openflow index to physical switch name
150 0, dict if ok: with the rule name as key and value is another dictionary with the following content:
151 priority: rule priority
152 name: rule name (present also as the master dict key)
153 ingress_port: match input port of the rule
154 dst_mac: match destination mac address of the rule, can be missing or None if not apply
155 vlan_id: match vlan tag of the rule, can be missing or None if not apply
156 actions: list of actions, composed by a pair tuples:
157 (vlan, None/int): for stripping/setting a vlan tag
158 (out, port): send to this port
160 -1, text_error if fails
163 #get translation, autodiscover version
164 if len(self
.ofi2pp
) == 0:
165 r
,c
= self
.obtain_port_correspondence()
170 of_response
= requests
.get(self
.url
+"/wm/%s/list/%s/json" %(self
.ver_names
["URLmodifier"], self
.dpid
),
171 headers
=self
.headers
)
172 error_text
= "Openflow response %d: %s" % (of_response
.status_code
, of_response
.text
)
173 if of_response
.status_code
!= 200:
174 self
.logger
.warning("get_of_rules " + error_text
)
175 return -1 , error_text
176 self
.logger
.debug("get_of_rules " + error_text
)
177 info
= of_response
.json()
178 if type(info
) != dict:
179 self
.logger
.error("get_of_rules. Unexpected response not a dict %s", str(type(info
)))
180 return -1, "Unexpected response, not a dict. Wrong version?"
182 for switch
,switch_info
in info
.iteritems():
183 if switch_info
== None:
185 if str(switch
) != self
.dpid
:
187 for name
,details
in switch_info
.iteritems():
189 rule
["switch"] = str(switch
)
190 #rule["active"] = "true"
191 rule
["priority"] = int(details
["priority"])
192 if self
.version
[0]=="0":
193 if translate_of_ports
:
194 rule
["ingress_port"] = self
.ofi2pp
[ details
["match"]["inputPort"] ]
196 rule
["ingress_port"] = str(details
["match"]["inputPort"])
197 dst_mac
= details
["match"]["dataLayerDestination"]
198 if dst_mac
!= "00:00:00:00:00:00":
199 rule
["dst_mac"] = dst_mac
200 vlan
= details
["match"]["dataLayerVirtualLan"]
202 rule
["vlan_id"] = vlan
204 for action
in details
["actions"]:
205 if action
["type"]=="OUTPUT":
206 if translate_of_ports
:
207 port
= self
.ofi2pp
[ action
["port"] ]
209 port
= action
["port"]
210 actionlist
.append( ("out", port
) )
211 elif action
["type"]=="STRIP_VLAN":
212 actionlist
.append( ("vlan",None) )
213 elif action
["type"]=="SET_VLAN_ID":
214 actionlist
.append( ("vlan", action
["virtualLanIdentifier"]) )
216 actionlist
.append( (action
["type"], str(action
) ))
217 self
.logger
.warning("get_of_rules() Unknown action in rule %s: %s", rule
["name"], str(action
))
218 rule
["actions"] = actionlist
219 elif self
.version
[0]=="1":
220 if translate_of_ports
:
221 rule
["ingress_port"] = self
.ofi2pp
[ details
["match"]["in_port"] ]
223 rule
["ingress_port"] = details
["match"]["in_port"]
224 if "eth_dst" in details
["match"]:
225 dst_mac
= details
["match"]["eth_dst"]
226 if dst_mac
!= "00:00:00:00:00:00":
227 rule
["dst_mac"] = dst_mac
228 if "eth_vlan_vid" in details
["match"]:
229 vlan
= int(details
["match"]["eth_vlan_vid"],16) & 0xFFF
230 rule
["vlan_id"] = str(vlan
)
232 for action
in details
["instructions"]["instruction_apply_actions"]:
234 if translate_of_ports
:
235 port
= self
.ofi2pp
[ details
["instructions"]["instruction_apply_actions"]["output"] ]
237 port
= details
["instructions"]["instruction_apply_actions"]["output"]
238 actionlist
.append( ("out",port
) )
239 elif action
=="strip_vlan":
240 actionlist
.append( ("vlan",None) )
241 elif action
=="set_vlan_vid":
242 actionlist
.append( ("vlan", details
["instructions"]["instruction_apply_actions"]["set_vlan_vid"]) )
244 self
.logger
.error("get_of_rules Unknown action in rule %s: %s", rule
["name"], str(action
))
245 #actionlist.append( (action, str(details["instructions"]["instruction_apply_actions"]) ))
246 rule_dict
[str(name
)] = rule
248 except (requests
.exceptions
.RequestException
, ValueError) as e
:
249 #ValueError in the case that JSON can not be decoded
250 error_text
= type(e
).__name
__ + ": " + str(e
)
251 self
.logger
.error("get_of_rules " + error_text
)
252 return -1, error_text
254 def obtain_port_correspondence(self
):
255 '''Obtain the correspondence between physical and openflow port names
257 0, dictionary: with physical name as key, openflow name as value
258 -1, error_text: if fails
261 of_response
= requests
.get(self
.url
+"/wm/core/controller/switches/json", headers
=self
.headers
)
262 #print vim_response.status_code
263 error_text
= "Openflow response %d: %s" % (of_response
.status_code
, of_response
.text
)
264 if of_response
.status_code
!= 200:
265 self
.logger
.warning("obtain_port_correspondence " + error_text
)
266 return -1 , error_text
267 self
.logger
.debug("obtain_port_correspondence " + error_text
)
268 info
= of_response
.json()
270 if type(info
) != list and type(info
) != tuple:
271 return -1, "unexpected openflow response, not a list. Wrong version?"
275 #autodiscover version
276 if self
.version
== None:
277 if 'dpid' in info
[0] and 'ports' in info
[0]:
278 self
._set
_version
("0.9")
279 elif 'switchDPID' in info
[0]:
280 self
._set
_version
("1.X")
282 return -1, "unexpected openflow response, Wrong version?"
284 for i
in range(0,len(info
)):
285 if info
[i
][ self
.ver_names
["dpid"] ] == self
.dpid
:
289 text
= "DPID '"+self
.dpid
+"' not present in controller "+self
.url
290 #print self.name, ": get_of_controller_info ERROR", text
293 if self
.version
[0]=="0":
294 ports
= info
[index
]["ports"]
296 of_response
= requests
.get(self
.url
+"/wm/core/switch/%s/port-desc/json" %self
.dpid
, headers
=self
.headers
)
297 #print vim_response.status_code
298 error_text
= "Openflow response %d: %s" % (of_response
.status_code
, of_response
.text
)
299 if of_response
.status_code
!= 200:
300 self
.logger
.warning("obtain_port_correspondence " + error_text
)
301 return -1 , error_text
302 self
.logger
.debug("obtain_port_correspondence " + error_text
)
303 info
= of_response
.json()
304 if type(info
) != dict:
305 return -1, "unexpected openflow port-desc response, not a dict. Wrong version?"
306 if "portDesc" not in info
:
307 return -1, "unexpected openflow port-desc response, 'portDesc' not found. Wrong version?"
308 if type(info
["portDesc"]) != list and type(info
["portDesc"]) != tuple:
309 return -1, "unexpected openflow port-desc response at 'portDesc', not a list. Wrong version?"
310 ports
= info
["portDesc"]
312 self
.pp2ofi
[ str(port
["name"]) ] = str(port
["portNumber"] )
313 self
.ofi2pp
[ port
["portNumber"]] = str(port
["name"])
314 #print self.name, ": get_of_controller_info ports:", self.pp2ofi
315 return 0, self
.pp2ofi
316 except (requests
.exceptions
.RequestException
, ValueError) as e
:
317 #ValueError in the case that JSON can not be decoded
318 error_text
= type(e
).__name
__ + ": " + str(e
)
319 self
.logger
.error("obtain_port_correspondence " + error_text
)
320 return -1, error_text
322 def del_flow(self
, flow_name
):
323 ''' Delete an existing rule
324 Params: flow_name, this is the rule name
327 -1, text_error if fails
329 #autodiscover version
330 if self
.version
== None:
331 r
,c
= self
.get_of_switches()
335 return -1, "No dpid found "
337 of_response
= requests
.delete(self
.url
+"/wm/%s/json" % self
.ver_names
["URLmodifier"],
338 headers
=self
.headers
, data
='{"switch":"%s","name":"%s"}' %(self
.dpid
, flow_name
)
340 error_text
= "Openflow response %d: %s" % (of_response
.status_code
, of_response
.text
)
341 if of_response
.status_code
!= 200:
342 self
.logger
.warning("del_flow " + error_text
)
343 return -1 , error_text
344 self
.logger
.debug("del_flow OK " + error_text
)
347 except requests
.exceptions
.RequestException
as e
:
348 error_text
= type(e
).__name
__ + ": " + str(e
)
349 self
.logger
.error("del_flow " + error_text
)
350 return -1, error_text
352 def new_flow(self
, data
):
353 ''' Insert a new static rule
354 Params: data: dictionary with the following content:
355 priority: rule priority
357 ingress_port: match input port of the rule
358 dst_mac: match destination mac address of the rule, missing or None if not apply
359 vlan_id: match vlan tag of the rule, missing or None if not apply
360 actions: list of actions, composed by a pair tuples with these posibilities:
361 ('vlan', None/int): for stripping/setting a vlan tag
362 ('out', port): send to this port
365 -1, text_error if fails
367 #get translation, autodiscover version
368 if len(self
.pp2ofi
) == 0:
369 r
,c
= self
.obtain_port_correspondence()
373 #We have to build the data for the floodlight call from the generic data
374 sdata
= {'active': "true", "name":data
["name"]}
375 if data
.get("priority"):
376 sdata
["priority"] = str(data
["priority"])
377 if data
.get("vlan_id"):
378 sdata
[ self
.ver_names
["vlanid"] ] = data
["vlan_id"]
379 if data
.get("dst_mac"):
380 sdata
[ self
.ver_names
["destmac"] ] = data
["dst_mac"]
381 sdata
['switch'] = self
.dpid
382 if not data
['ingress_port'] in self
.pp2ofi
:
383 error_text
= 'Error. Port '+data
['ingress_port']+' is not present in the switch'
384 self
.logger
.warning("new_flow " + error_text
)
385 return -1, error_text
387 sdata
[ self
.ver_names
["inport"] ] = self
.pp2ofi
[data
['ingress_port']]
388 sdata
['actions'] = ""
390 for action
in data
['actions']:
391 if len(sdata
['actions']) > 0:
392 sdata
['actions'] += ','
393 if action
[0] == "vlan":
395 sdata
['actions'] += self
.ver_names
["stripvlan"]
397 sdata
['actions'] += self
.ver_names
["setvlan"] + "=" + str(action
[1])
398 elif action
[0] == 'out':
399 sdata
['actions'] += "output=" + self
.pp2ofi
[ action
[1] ]
402 of_response
= requests
.post(self
.url
+"/wm/%s/json" % self
.ver_names
["URLmodifier"],
403 headers
=self
.headers
, data
=json
.dumps(sdata
) )
404 error_text
= "Openflow response %d: %s" % (of_response
.status_code
, of_response
.text
)
405 if of_response
.status_code
!= 200:
406 self
.logger
.warning("new_flow " + error_text
)
407 return -1 , error_text
408 self
.logger
.debug("new_flow OK" + error_text
)
411 except requests
.exceptions
.RequestException
as e
:
412 error_text
= type(e
).__name
__ + ": " + str(e
)
413 self
.logger
.error("new_flow " + error_text
)
414 return -1, error_text
416 def clear_all_flows(self
):
417 ''' Delete all existing rules
420 -1, text_error if fails
422 #autodiscover version
423 if self
.version
== None:
424 r
,c
= self
.get_of_switches()
430 url
= self
.url
+"/wm/%s/clear/%s/json" % (self
.ver_names
["URLmodifier"], self
.dpid
)
431 of_response
= requests
.get(url
)
432 error_text
= "Openflow response %d: %s" % (of_response
.status_code
, of_response
.text
)
433 if of_response
.status_code
< 200 or of_response
.status_code
>= 300:
434 self
.logger
.warning("clear_all_flows " + error_text
)
435 return -1 , error_text
436 self
.logger
.debug("clear_all_flows OK " + error_text
)
438 except requests
.exceptions
.RequestException
as e
:
439 error_text
= type(e
).__name
__ + ": " + str(e
)
440 self
.logger
.error("clear_all_flows " + error_text
)
441 return -1, error_text