| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # -*- coding: utf-8 -*- |
| 3 | |
| 4 | ## |
| 5 | # Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U. |
| tierno | 9a61c6b | 2016-09-08 10:57:02 +0200 | [diff] [blame] | 6 | # This file is part of openvim |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 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 | |
| 35 | import json |
| 36 | import requests |
| 37 | import logging |
| 38 | |
| 39 | class OF_conn(): |
| 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!!! |
| 44 | ''' |
| 45 | def __init__(self, params): |
| 46 | ''' Constructor. |
| 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 |
| 55 | ''' |
| 56 | #check params |
| 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") |
| 59 | |
| 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"]) ) |
| 63 | |
| 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'} |
| 67 | self.version= None |
| 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") ) |
| 71 | |
| 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 |
| 75 | ''' |
| 76 | #static version names |
| 77 | if version==None: |
| 78 | self.version= None |
| 79 | elif version=="0.9": |
| 80 | self.version= version |
| 81 | self.name = "Floodlightv0.9" |
| 82 | self.ver_names={ |
| 83 | "dpid": "dpid", |
| 84 | "URLmodifier": "staticflowentrypusher", |
| 85 | "destmac": "dst-mac", |
| 86 | "vlanid": "vlan-id", |
| 87 | "inport": "ingress-port", |
| 88 | "setvlan": "set-vlan-id", |
| 89 | "stripvlan": "strip-vlan", |
| 90 | } |
| 91 | elif version[0]=="1" : #version 1.X |
| 92 | self.version= version |
| 93 | self.name = "Floodlightv1.X" |
| 94 | self.ver_names={ |
| 95 | "dpid": "switchDPID", |
| 96 | "URLmodifier": "staticflowpusher", |
| 97 | "destmac": "eth_dst", |
| 98 | "vlanid": "eth_vlan_vid", |
| 99 | "inport": "in_port", |
| 100 | "setvlan": "set_vlan_vid", |
| 101 | "stripvlan": "strip_vlan", |
| 102 | } |
| 103 | else: |
| 104 | raise ValueError("Invalid version for floodlight controller") |
| 105 | |
| 106 | def get_of_switches(self): |
| 107 | ''' Obtain a a list of switches or DPID detected by this controller |
| 108 | Return |
| 109 | >=0, list: list length, and a list where each element a tuple pair (DPID, IP address) |
| 110 | <0, text_error: if fails |
| 111 | ''' |
| 112 | try: |
| 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?" |
| 123 | if len(info)==0: |
| 124 | return 0, info |
| 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") |
| 131 | else: |
| 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?" |
| 134 | |
| 135 | switch_list=[] |
| 136 | for switch in info: |
| 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 |
| 144 | |
| 145 | def get_of_rules(self, translate_of_ports=True): |
| 146 | ''' Obtain the rules inserted at openflow controller |
| 147 | Params: |
| 148 | translate_of_ports: if True it translates ports from openflow index to physical switch name |
| 149 | Return: |
| 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 |
| 159 | switch: DPID, all |
| 160 | -1, text_error if fails |
| 161 | ''' |
| 162 | |
| 163 | #get translation, autodiscover version |
| 164 | if len(self.ofi2pp) == 0: |
| 165 | r,c = self.obtain_port_correspondence() |
| 166 | if r<0: |
| 167 | return r,c |
| 168 | #get rules |
| 169 | try: |
| 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?" |
| 181 | rule_dict={} |
| 182 | for switch,switch_info in info.iteritems(): |
| 183 | if switch_info == None: |
| 184 | continue |
| 185 | if str(switch) != self.dpid: |
| 186 | continue |
| 187 | for name,details in switch_info.iteritems(): |
| 188 | rule = {} |
| 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"] ] |
| 195 | else: |
| 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"] |
| 201 | if vlan != -1: |
| 202 | rule["vlan_id"] = vlan |
| 203 | actionlist=[] |
| 204 | for action in details["actions"]: |
| 205 | if action["type"]=="OUTPUT": |
| 206 | if translate_of_ports: |
| 207 | port = self.ofi2pp[ action["port"] ] |
| 208 | else: |
| 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"]) ) |
| 215 | else: |
| 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"] ] |
| 222 | else: |
| 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) |
| 231 | actionlist=[] |
| 232 | for action in details["instructions"]["instruction_apply_actions"]: |
| 233 | if action=="output": |
| 234 | if translate_of_ports: |
| 235 | port = self.ofi2pp[ details["instructions"]["instruction_apply_actions"]["output"] ] |
| 236 | else: |
| 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"]) ) |
| 243 | else: |
| 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 |
| 247 | return 0, rule_dict |
| 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 |
| 253 | |
| 254 | def obtain_port_correspondence(self): |
| 255 | '''Obtain the correspondence between physical and openflow port names |
| 256 | return: |
| 257 | 0, dictionary: with physical name as key, openflow name as value |
| 258 | -1, error_text: if fails |
| 259 | ''' |
| 260 | try: |
| 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() |
| 269 | |
| 270 | if type(info) != list and type(info) != tuple: |
| 271 | return -1, "unexpected openflow response, not a list. Wrong version?" |
| 272 | |
| 273 | index = -1 |
| 274 | if len(info)>0: |
| 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") |
| 281 | else: |
| 282 | return -1, "unexpected openflow response, Wrong version?" |
| 283 | |
| 284 | for i in range(0,len(info)): |
| 285 | if info[i][ self.ver_names["dpid"] ] == self.dpid: |
| 286 | index = i |
| 287 | break |
| 288 | if index == -1: |
| 289 | text = "DPID '"+self.dpid+"' not present in controller "+self.url |
| 290 | #print self.name, ": get_of_controller_info ERROR", text |
| 291 | return -1, text |
| 292 | else: |
| 293 | if self.version[0]=="0": |
| 294 | ports = info[index]["ports"] |
| 295 | else: #version 1.X |
| 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"] |
| 311 | for port in ports: |
| 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 |
| 321 | |
| 322 | def del_flow(self, flow_name): |
| 323 | ''' Delete an existing rule |
| 324 | Params: flow_name, this is the rule name |
| 325 | Return |
| 326 | 0, None if ok |
| 327 | -1, text_error if fails |
| 328 | ''' |
| 329 | #autodiscover version |
| 330 | if self.version == None: |
| 331 | r,c = self.get_of_switches() |
| 332 | if r<0: |
| 333 | return r,c |
| 334 | elif r==0: |
| 335 | return -1, "No dpid found " |
| 336 | try: |
| 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) |
| 339 | ) |
| 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) |
| 345 | return 0, None |
| 346 | |
| 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 |
| 351 | |
| 352 | def new_flow(self, data): |
| 353 | ''' Insert a new static rule |
| 354 | Params: data: dictionary with the following content: |
| 355 | priority: rule priority |
| 356 | name: rule name |
| 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 |
| 363 | Return |
| 364 | 0, None if ok |
| 365 | -1, text_error if fails |
| 366 | ''' |
| 367 | #get translation, autodiscover version |
| 368 | if len(self.pp2ofi) == 0: |
| 369 | r,c = self.obtain_port_correspondence() |
| 370 | if r<0: |
| 371 | return r,c |
| 372 | try: |
| 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 |
| 386 | |
| 387 | sdata[ self.ver_names["inport"] ] = self.pp2ofi[data['ingress_port']] |
| 388 | sdata['actions'] = "" |
| 389 | |
| 390 | for action in data['actions']: |
| 391 | if len(sdata['actions']) > 0: |
| 392 | sdata['actions'] += ',' |
| 393 | if action[0] == "vlan": |
| 394 | if action[1]==None: |
| 395 | sdata['actions'] += self.ver_names["stripvlan"] |
| 396 | else: |
| 397 | sdata['actions'] += self.ver_names["setvlan"] + "=" + str(action[1]) |
| 398 | elif action[0] == 'out': |
| 399 | sdata['actions'] += "output=" + self.pp2ofi[ action[1] ] |
| 400 | |
| 401 | |
| 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) |
| 409 | return 0, None |
| 410 | |
| 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 |
| 415 | |
| 416 | def clear_all_flows(self): |
| 417 | ''' Delete all existing rules |
| 418 | Return: |
| 419 | 0, None if ok |
| 420 | -1, text_error if fails |
| 421 | ''' |
| 422 | #autodiscover version |
| 423 | if self.version == None: |
| 424 | r,c = self.get_of_switches() |
| 425 | if r<0: |
| 426 | return r,c |
| 427 | elif r==0: #empty |
| 428 | return 0, None |
| 429 | try: |
| 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) |
| 437 | return 0, None |
| 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 |