create base package 'osm_ro_plugin' for plugin
[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 OpenflowConn, OpenflowConnUnexpectedResponse, OpenflowConnConnectionException
38
39
40 class OfConnFloodLight(OpenflowConn):
41 """
42 Openflow Connector for Floodlight.
43 No MAC learning is used
44 version 0.9 or 1.X is autodetected
45 version 1.X is in progress, not finished!!!
46 """
47
48 def __init__(self, params):
49 """
50 Constructor
51 :param params: dictionary with the following keys:
52 of_dpid: DPID to use for this controller ?? Does a controller have a dpid?
53 url: must be [http://HOST:PORT/]
54 of_user: user credentials, can be missing or None
55 of_password: password credentials
56 of_debug: debug level for logging. Default to ERROR
57 other keys are ignored
58 Raise an exception if same parameter is missing or wrong
59 """
60 # check params
61 url = params.get("of_url")
62 if not url:
63 raise ValueError("'url' must be provided")
64 if not url.startswith("http"):
65 url = "http://" + url
66 if not url.endswith("/"):
67 url = url + "/"
68 self.url = url
69
70 OpenflowConn.__init__(self, params)
71
72 self.name = "Floodlight"
73 self.dpid = str(params["of_dpid"])
74
75 self.pp2ofi = {} # From Physical Port to OpenFlow Index
76 self.ofi2pp = {} # From OpenFlow Index to Physical Port
77 self.headers = {'content-type': 'application/json', 'Accept': 'application/json'}
78 self.version = None
79 self.logger = logging.getLogger('SDN.floodlightOF')
80 self.logger.setLevel(params.get("of_debug", "ERROR"))
81 self._set_version(params.get("of_version"))
82
83 def _set_version(self, version):
84 """
85 set up a version of the controller.
86 Depending on the version it fills the self.ver_names with the naming used in this version
87 :param version: Openflow controller version
88 :return: Raise an ValueError exception if same parameter is missing or wrong
89 """
90 # static version names
91 if version is None:
92 self.version = None
93 elif version == "0.9":
94 self.version = version
95 self.name = "Floodlightv0.9"
96 self.ver_names = {
97 "dpid": "dpid",
98 "URLmodifier": "staticflowentrypusher",
99 "destmac": "dst-mac",
100 "vlanid": "vlan-id",
101 "inport": "ingress-port",
102 "setvlan": "set-vlan-id",
103 "stripvlan": "strip-vlan",
104 }
105 elif version[0] == "1": # version 1.X
106 self.version = version
107 self.name = "Floodlightv1.X"
108 self.ver_names = {
109 "dpid": "switchDPID",
110 "URLmodifier": "staticflowpusher",
111 "destmac": "eth_dst",
112 "vlanid": "eth_vlan_vid",
113 "inport": "in_port",
114 "setvlan": "set_vlan_vid",
115 "stripvlan": "strip_vlan",
116 }
117 else:
118 raise ValueError("Invalid version for floodlight controller")
119
120 def get_of_switches(self):
121 """
122 Obtain a a list of switches or DPID detected by this controller
123 :return: list where each element a tuple pair (DPID, IP address)
124 Raise an OpenflowconnConnectionException or OpenflowconnConnectionException exception if same
125 parameter is missing or wrong
126 """
127 try:
128 of_response = requests.get(self.url + "wm/core/controller/switches/json", headers=self.headers)
129 error_text = "Openflow response {}: {}".format(of_response.status_code, of_response.text)
130 if of_response.status_code != 200:
131 self.logger.warning("get_of_switches " + error_text)
132 raise OpenflowConnUnexpectedResponse(error_text)
133 self.logger.debug("get_of_switches " + error_text)
134 info = of_response.json()
135 if not isinstance(info, (list, tuple)):
136 self.logger.error("get_of_switches. Unexpected response not a list %s", str(type(info)))
137 raise OpenflowConnUnexpectedResponse("Unexpected response, not a list. Wrong version?")
138 if len(info) == 0:
139 return info
140 # autodiscover version
141 if self.version is None:
142 if 'dpid' in info[0] and 'inetAddress' in info[0]:
143 self._set_version("0.9")
144 # elif 'switchDPID' in info[0] and 'inetAddress' in info[0]:
145 # self._set_version("1.X")
146 else:
147 self.logger.error("get_of_switches. Unexpected response, not found 'dpid' or 'switchDPID' "
148 "field: %s", str(info[0]))
149 raise OpenflowConnUnexpectedResponse("Unexpected response, not found 'dpid' or "
150 "'switchDPID' field. Wrong version?")
151
152 switch_list = []
153 for switch in info:
154 switch_list.append((switch[self.ver_names["dpid"]], switch['inetAddress']))
155 return switch_list
156 except requests.exceptions.RequestException as e:
157 error_text = type(e).__name__ + ": " + str(e)
158 self.logger.error("get_of_switches " + error_text)
159 raise OpenflowConnConnectionException(error_text)
160 except Exception as e:
161 # ValueError in the case that JSON can not be decoded
162 error_text = type(e).__name__ + ": " + str(e)
163 self.logger.error("get_of_switches " + error_text)
164 raise OpenflowConnUnexpectedResponse(error_text)
165
166 def get_of_rules(self, translate_of_ports=True):
167 """
168 Obtain the rules inserted at openflow controller
169 :param translate_of_ports: if True it translates ports from openflow index to physical switch name
170 :return: list where each item is a dictionary with the following content:
171 priority: rule priority
172 name: rule name (present also as the master dict key)
173 ingress_port: match input port of the rule
174 dst_mac: match destination mac address of the rule, can be missing or None if not apply
175 vlan_id: match vlan tag of the rule, can be missing or None if not apply
176 actions: list of actions, composed by a pair tuples:
177 (vlan, None/int): for stripping/setting a vlan tag
178 (out, port): send to this port
179 switch: DPID, all
180 Raise an openflowconnUnexpectedResponse exception if fails with text_error
181 """
182
183 try:
184 # get translation, autodiscover version
185 if len(self.ofi2pp) == 0:
186 self.obtain_port_correspondence()
187
188 of_response = requests.get(self.url + "wm/{}/list/{}/json".format(self.ver_names["URLmodifier"], self.dpid),
189 headers=self.headers)
190 error_text = "Openflow response {}: {}".format(of_response.status_code, of_response.text)
191 if of_response.status_code != 200:
192 self.logger.warning("get_of_rules " + error_text)
193 raise OpenflowConnUnexpectedResponse(error_text)
194 self.logger.debug("get_of_rules " + error_text)
195 info = of_response.json()
196 if type(info) != dict:
197 self.logger.error("get_of_rules. Unexpected response not a dict %s", str(type(info)))
198 raise OpenflowConnUnexpectedResponse("Unexpected response, not a dict. Wrong version?")
199 rule_list = []
200 for switch, switch_info in info.items():
201 if switch_info is None:
202 continue
203 if str(switch) != self.dpid:
204 continue
205 for name, details in switch_info.items():
206 rule = {
207 "name": name,
208 "switch": str(switch)
209 }
210 # rule["active"] = "true"
211 rule["priority"] = int(details["priority"])
212 if self.version[0] == "0":
213 if translate_of_ports:
214 rule["ingress_port"] = self.ofi2pp[details["match"]["inputPort"]]
215 else:
216 rule["ingress_port"] = str(details["match"]["inputPort"])
217 dst_mac = details["match"]["dataLayerDestination"]
218 if dst_mac != "00:00:00:00:00:00":
219 rule["dst_mac"] = dst_mac
220 vlan = details["match"]["dataLayerVirtualLan"]
221 if vlan != -1:
222 rule["vlan_id"] = vlan
223 actionlist = []
224 for action in details["actions"]:
225 if action["type"] == "OUTPUT":
226 if translate_of_ports:
227 port = self.ofi2pp[action["port"]]
228 else:
229 port = action["port"]
230 actionlist.append(("out", port))
231 elif action["type"] == "STRIP_VLAN":
232 actionlist.append(("vlan", None))
233 elif action["type"] == "SET_VLAN_ID":
234 actionlist.append(("vlan", action["virtualLanIdentifier"]))
235 else:
236 actionlist.append((action["type"], str(action)))
237 self.logger.warning("get_of_rules() Unknown action in rule %s: %s", rule["name"],
238 str(action))
239 rule["actions"] = actionlist
240 elif self.version[0] == "1":
241 if translate_of_ports:
242 rule["ingress_port"] = self.ofi2pp[details["match"]["in_port"]]
243 else:
244 rule["ingress_port"] = details["match"]["in_port"]
245 if "eth_dst" in details["match"]:
246 dst_mac = details["match"]["eth_dst"]
247 if dst_mac != "00:00:00:00:00:00":
248 rule["dst_mac"] = dst_mac
249 if "eth_vlan_vid" in details["match"]:
250 vlan = int(details["match"]["eth_vlan_vid"], 16) & 0xFFF
251 rule["vlan_id"] = str(vlan)
252 actionlist = []
253 for action in details["instructions"]["instruction_apply_actions"]:
254 if action == "output":
255 if translate_of_ports:
256 port = self.ofi2pp[details["instructions"]["instruction_apply_actions"]["output"]]
257 else:
258 port = details["instructions"]["instruction_apply_actions"]["output"]
259 actionlist.append(("out", port))
260 elif action == "strip_vlan":
261 actionlist.append(("vlan", None))
262 elif action == "set_vlan_vid":
263 actionlist.append(
264 ("vlan", details["instructions"]["instruction_apply_actions"]["set_vlan_vid"]))
265 else:
266 self.logger.error("get_of_rules Unknown action in rule %s: %s", rule["name"],
267 str(action))
268 # actionlist.append((action, str(details["instructions"]["instruction_apply_actions"])))
269 rule_list.append(rule)
270 return rule_list
271 except requests.exceptions.RequestException as e:
272 error_text = type(e).__name__ + ": " + str(e)
273 self.logger.error("get_of_rules " + error_text)
274 raise OpenflowConnConnectionException(error_text)
275 except Exception as e:
276 # ValueError in the case that JSON can not be decoded
277 error_text = type(e).__name__ + ": " + str(e)
278 self.logger.error("get_of_rules " + error_text)
279 raise OpenflowConnUnexpectedResponse(error_text)
280
281 def obtain_port_correspondence(self):
282 """
283 Obtain the correspondence between physical and openflow port names
284 :return: dictionary: with physical name as key, openflow name as value
285 Raise an openflowconnUnexpectedResponse exception if fails with text_error
286 """
287 try:
288 of_response = requests.get(self.url + "wm/core/controller/switches/json", headers=self.headers)
289 # print vim_response.status_code
290 error_text = "Openflow response {}: {}".format(of_response.status_code, of_response.text)
291 if of_response.status_code != 200:
292 self.logger.warning("obtain_port_correspondence " + error_text)
293 raise OpenflowConnUnexpectedResponse(error_text)
294 self.logger.debug("obtain_port_correspondence " + error_text)
295 info = of_response.json()
296
297 if not isinstance(info, (list, tuple)):
298 raise OpenflowConnUnexpectedResponse("unexpected openflow response, not a list. Wrong version?")
299
300 index = -1
301 if len(info) > 0:
302 # autodiscover version
303 if self.version is None:
304 if 'dpid' in info[0] and 'ports' in info[0]:
305 self._set_version("0.9")
306 elif 'switchDPID' in info[0]:
307 self._set_version("1.X")
308 else:
309 raise OpenflowConnUnexpectedResponse("unexpected openflow response, Wrong version?")
310
311 for i, info_item in enumerate(info):
312 if info_item[self.ver_names["dpid"]] == self.dpid:
313 index = i
314 break
315 if index == -1:
316 text = "DPID '{}' not present in controller {}".format(self.dpid, self.url)
317 # print self.name, ": get_of_controller_info ERROR", text
318 raise OpenflowConnUnexpectedResponse(text)
319 else:
320 if self.version[0] == "0":
321 ports = info[index]["ports"]
322 else: # version 1.X
323 of_response = requests.get(self.url + "wm/core/switch/{}/port-desc/json".format(self.dpid),
324 headers=self.headers)
325 # print vim_response.status_code
326 error_text = "Openflow response {}: {}".format(of_response.status_code, of_response.text)
327 if of_response.status_code != 200:
328 self.logger.warning("obtain_port_correspondence " + error_text)
329 raise OpenflowConnUnexpectedResponse(error_text)
330 self.logger.debug("obtain_port_correspondence " + error_text)
331 info = of_response.json()
332 if type(info) != dict:
333 raise OpenflowConnUnexpectedResponse("unexpected openflow port-desc response, "
334 "not a dict. Wrong version?")
335 if "portDesc" not in info:
336 raise OpenflowConnUnexpectedResponse("unexpected openflow port-desc response, "
337 "'portDesc' not found. Wrong version?")
338 if type(info["portDesc"]) != list and type(info["portDesc"]) != tuple:
339 raise OpenflowConnUnexpectedResponse("unexpected openflow port-desc response at "
340 "'portDesc', not a list. Wrong version?")
341 ports = info["portDesc"]
342 for port in ports:
343 self.pp2ofi[str(port["name"])] = str(port["portNumber"])
344 self.ofi2pp[port["portNumber"]] = str(port["name"])
345 # print self.name, ": get_of_controller_info ports:", self.pp2ofi
346 return self.pp2ofi
347 except requests.exceptions.RequestException as e:
348 error_text = type(e).__name__ + ": " + str(e)
349 self.logger.error("obtain_port_correspondence " + error_text)
350 raise OpenflowConnConnectionException(error_text)
351 except Exception as e:
352 # ValueError in the case that JSON can not be decoded
353 error_text = type(e).__name__ + ": " + str(e)
354 self.logger.error("obtain_port_correspondence " + error_text)
355 raise OpenflowConnUnexpectedResponse(error_text)
356
357 def del_flow(self, flow_name):
358 """
359 Delete an existing rule
360 :param flow_name: this is the rule name
361 :return: None if ok
362 Raise an openflowconnUnexpectedResponse exception if fails with text_error
363 """
364 try:
365 if self.version is None:
366 self.get_of_switches()
367
368 of_response = requests.delete(self.url + "wm/{}/json".format(self.ver_names["URLmodifier"]),
369 headers=self.headers,
370 data='{{"switch":"{}","name":"{}"}}'.format(self.dpid, flow_name))
371 error_text = "Openflow response {}: {}".format(of_response.status_code, of_response.text)
372 if of_response.status_code != 200:
373 self.logger.warning("del_flow " + error_text)
374 raise OpenflowConnUnexpectedResponse(error_text)
375 self.logger.debug("del_flow OK " + error_text)
376 return None
377
378 except requests.exceptions.RequestException as e:
379 error_text = type(e).__name__ + ": " + str(e)
380 self.logger.error("del_flow " + error_text)
381 raise OpenflowConnConnectionException(error_text)
382 except Exception as e:
383 # ValueError in the case that JSON can not be decoded
384 error_text = type(e).__name__ + ": " + str(e)
385 self.logger.error("del_flow " + error_text)
386 raise OpenflowConnUnexpectedResponse(error_text)
387
388 def new_flow(self, data):
389 """
390 Insert a new static rule
391 :param data: dictionary with the following content:
392 priority: rule priority
393 name: rule name
394 ingress_port: match input port of the rule
395 dst_mac: match destination mac address of the rule, missing or None if not apply
396 vlan_id: match vlan tag of the rule, missing or None if not apply
397 actions: list of actions, composed by a pair tuples with these posibilities:
398 ('vlan', None/int): for stripping/setting a vlan tag
399 ('out', port): send to this port
400 :return: None if ok
401 Raise an openflowconnUnexpectedResponse exception if fails with text_error
402 """
403 # get translation, autodiscover version
404 if len(self.pp2ofi) == 0:
405 self.obtain_port_correspondence()
406
407 try:
408 # We have to build the data for the floodlight call from the generic data
409 sdata = {'active': "true", "name": data["name"]}
410 if data.get("priority"):
411 sdata["priority"] = str(data["priority"])
412 if data.get("vlan_id"):
413 sdata[self.ver_names["vlanid"]] = data["vlan_id"]
414 if data.get("dst_mac"):
415 sdata[self.ver_names["destmac"]] = data["dst_mac"]
416 sdata['switch'] = self.dpid
417 if not data['ingress_port'] in self.pp2ofi:
418 error_text = 'Error. Port {} is not present in the switch'.format(data['ingress_port'])
419 self.logger.warning("new_flow " + error_text)
420 raise OpenflowConnUnexpectedResponse(error_text)
421
422 sdata[self.ver_names["inport"]] = self.pp2ofi[data['ingress_port']]
423 sdata['actions'] = ""
424
425 for action in data['actions']:
426 if len(sdata['actions']) > 0:
427 sdata['actions'] += ','
428 if action[0] == "vlan":
429 if action[1] is None:
430 sdata['actions'] += self.ver_names["stripvlan"]
431 else:
432 sdata['actions'] += self.ver_names["setvlan"] + "=" + str(action[1])
433 elif action[0] == 'out':
434 sdata['actions'] += "output=" + self.pp2ofi[action[1]]
435
436 of_response = requests.post(self.url + "wm/{}/json".format(self.ver_names["URLmodifier"]),
437 headers=self.headers, data=json.dumps(sdata))
438 error_text = "Openflow response {}: {}".format(of_response.status_code, of_response.text)
439 if of_response.status_code != 200:
440 self.logger.warning("new_flow " + error_text)
441 raise OpenflowConnUnexpectedResponse(error_text)
442 self.logger.debug("new_flow OK" + error_text)
443 return None
444
445 except requests.exceptions.RequestException as e:
446 error_text = type(e).__name__ + ": " + str(e)
447 self.logger.error("new_flow " + error_text)
448 raise OpenflowConnConnectionException(error_text)
449 except Exception as e:
450 # ValueError in the case that JSON can not be decoded
451 error_text = type(e).__name__ + ": " + str(e)
452 self.logger.error("new_flow " + error_text)
453 raise OpenflowConnUnexpectedResponse(error_text)
454
455 def clear_all_flows(self):
456 """
457 Delete all existing rules
458 :return: None if ok
459 Raise an openflowconnUnexpectedResponse exception if fails with text_error
460 """
461
462 try:
463 # autodiscover version
464 if self.version is None:
465 sw_list = self.get_of_switches()
466 if len(sw_list) == 0: # empty
467 return None
468
469 url = self.url + "wm/{}/clear/{}/json".format(self.ver_names["URLmodifier"], self.dpid)
470 of_response = requests.get(url)
471 error_text = "Openflow response {}: {}".format(of_response.status_code, of_response.text)
472 if of_response.status_code < 200 or of_response.status_code >= 300:
473 self.logger.warning("clear_all_flows " + error_text)
474 raise OpenflowConnUnexpectedResponse(error_text)
475 self.logger.debug("clear_all_flows OK " + error_text)
476 return None
477 except requests.exceptions.RequestException as e:
478 error_text = type(e).__name__ + ": " + str(e)
479 self.logger.error("clear_all_flows " + 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("clear_all_flows " + error_text)
485 raise OpenflowConnUnexpectedResponse(error_text)