Enabling Flake8 and import sorting
[osm/RO.git] / RO-SDN-onos_openflow / osm_rosdn_onosof / onos_of.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 ##
5 # Copyright 2016, I2T Research Group (UPV/EHU)
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: alaitz.mendiola@ehu.eus or alaitz.mendiola@gmail.com
23 ##
24
25 """
26 ImplementS the pluging for the Open Network Operating System (ONOS) openflow
27 controller. It creates the class OF_conn to create dataplane connections
28 with static rules based on packet destination MAC address
29 """
30
31 import base64
32 import json
33 import logging
34
35 from osm_ro_plugin.openflow_conn import (
36 OpenflowConn,
37 OpenflowConnConnectionException,
38 OpenflowConnUnexpectedResponse,
39 )
40 import requests
41
42 # OpenflowConnException, OpenflowConnAuthException, OpenflowConnNotFoundException, \
43 # OpenflowConnConflictException, OpenflowConnNotSupportedException, OpenflowConnNotImplemented
44
45 __author__ = "Alaitz Mendiola"
46 __date__ = "$22-nov-2016$"
47
48
49 class OfConnOnos(OpenflowConn):
50 """
51 ONOS connector. No MAC learning is used
52 """
53
54 def __init__(self, params):
55 """Constructor.
56 :param params: dictionary with the following keys:
57 of_dpid: DPID to use for this controller ?? Does a controller have a dpid?
58 of_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
64 """
65 OpenflowConn.__init__(self, params)
66
67 # check params
68 url = params.get("of_url")
69
70 if not url:
71 raise ValueError("'url' must be provided")
72
73 if not url.startswith("http"):
74 url = "http://" + url
75
76 if not url.endswith("/"):
77 url = url + "/"
78
79 self.url = url + "onos/v1/"
80
81 # internal variables
82 self.name = "onosof"
83 self.headers = {
84 "content-type": "application/json",
85 "accept": "application/json",
86 }
87
88 self.auth = "None"
89 self.pp2ofi = {} # From Physical Port to OpenFlow Index
90 self.ofi2pp = {} # From OpenFlow Index to Physical Port
91
92 self.dpid = str(params["of_dpid"])
93 self.id = "of:" + str(self.dpid.replace(":", ""))
94
95 # TODO This may not be straightforward
96 if params.get("of_user"):
97 of_password = params.get("of_password", "")
98 self.auth = base64.b64encode(
99 bytes(params["of_user"] + ":" + of_password, "utf-8")
100 )
101 self.auth = self.auth.decode()
102 self.headers["authorization"] = "Basic " + self.auth
103
104 self.logger = logging.getLogger("ro.sdn.onosof")
105 # self.logger.setLevel( getattr(logging, params.get("of_debug", "ERROR")) )
106 self.logger.debug("onosof plugin initialized")
107 self.ip_address = None
108
109 def get_of_switches(self):
110 """
111 Obtain a a list of switches or DPID detected by this controller
112 :return: list where each element a tuple pair (DPID, IP address)
113 Raise a openflowconnUnexpectedResponse expection in case of failure
114 """
115 try:
116 self.headers["content-type"] = "text/plain"
117 of_response = requests.get(self.url + "devices", headers=self.headers)
118 error_text = "Openflow response %d: %s" % (
119 of_response.status_code,
120 of_response.text,
121 )
122
123 if of_response.status_code != 200:
124 self.logger.warning("get_of_switches " + error_text)
125
126 raise OpenflowConnUnexpectedResponse(error_text)
127
128 self.logger.debug("get_of_switches " + error_text)
129 info = of_response.json()
130
131 if type(info) != dict:
132 self.logger.error(
133 "get_of_switches. Unexpected response, not a dict: %s", str(info)
134 )
135
136 raise OpenflowConnUnexpectedResponse(
137 "Unexpected response, not a dict. Wrong version?"
138 )
139
140 node_list = info.get("devices")
141
142 if type(node_list) is not list:
143 self.logger.error(
144 "get_of_switches. Unexpected response, at 'devices', not found or not a list: %s",
145 str(type(node_list)),
146 )
147
148 raise OpenflowConnUnexpectedResponse(
149 "Unexpected response, at 'devices', not found "
150 "or not a list. Wrong version?"
151 )
152
153 switch_list = []
154 for node in node_list:
155 node_id = node.get("id")
156 if node_id is None:
157 self.logger.error(
158 "get_of_switches. Unexpected response at 'device':'id', not found: %s",
159 str(node),
160 )
161
162 raise OpenflowConnUnexpectedResponse(
163 "Unexpected response at 'device':'id', "
164 "not found . Wrong version?"
165 )
166
167 node_ip_address = node.get("annotations").get("managementAddress")
168 if node_ip_address is None:
169 self.logger.error(
170 "get_of_switches. Unexpected response at 'device':'managementAddress', not found: %s",
171 str(node),
172 )
173
174 raise OpenflowConnUnexpectedResponse(
175 "Unexpected response at 'device':'managementAddress', not found. Wrong version?"
176 )
177
178 node_id_hex = hex(int(node_id.split(":")[1])).split("x")[1].zfill(16)
179 switch_list.append(
180 (
181 ":".join(
182 a + b for a, b in zip(node_id_hex[::2], node_id_hex[1::2])
183 ),
184 node_ip_address,
185 )
186 )
187
188 return switch_list
189 except requests.exceptions.RequestException as e:
190 error_text = type(e).__name__ + ": " + str(e)
191 self.logger.error("get_of_switches " + error_text)
192
193 raise OpenflowConnConnectionException(error_text)
194 except ValueError as e:
195 # ValueError in the case that JSON can not be decoded
196 error_text = type(e).__name__ + ": " + str(e)
197 self.logger.error("get_of_switches " + error_text)
198
199 raise OpenflowConnUnexpectedResponse(error_text)
200
201 def obtain_port_correspondence(self):
202 """
203 Obtain the correspondence between physical and openflow port names
204 :return: dictionary with physical name as key, openflow name as value
205 Raise a openflowconnUnexpectedResponse expection in case of failure
206 """
207 try:
208 self.headers["content-type"] = "text/plain"
209 of_response = requests.get(
210 self.url + "devices/" + self.id + "/ports", headers=self.headers
211 )
212 error_text = "Openflow response {}: {}".format(
213 of_response.status_code, of_response.text
214 )
215
216 if of_response.status_code != 200:
217 self.logger.warning("obtain_port_correspondence " + error_text)
218
219 raise OpenflowConnUnexpectedResponse(error_text)
220
221 self.logger.debug("obtain_port_correspondence " + error_text)
222 info = of_response.json()
223
224 node_connector_list = info.get("ports")
225 if type(node_connector_list) is not list:
226 self.logger.error(
227 "obtain_port_correspondence. Unexpected response at 'ports', not found or not a list: %s",
228 str(node_connector_list),
229 )
230
231 raise OpenflowConnUnexpectedResponse(
232 "Unexpected response at 'ports', not found or not "
233 "a list. Wrong version?"
234 )
235
236 for node_connector in node_connector_list:
237 if node_connector["port"] != "local":
238 self.pp2ofi[str(node_connector["annotations"]["portName"])] = str(
239 node_connector["port"]
240 )
241 self.ofi2pp[str(node_connector["port"])] = str(
242 node_connector["annotations"]["portName"]
243 )
244
245 node_ip_address = info["annotations"]["managementAddress"]
246 if node_ip_address is None:
247 self.logger.error(
248 "obtain_port_correspondence. Unexpected response at 'managementAddress', not found: %s",
249 str(self.id),
250 )
251
252 raise OpenflowConnUnexpectedResponse(
253 "Unexpected response at 'managementAddress', "
254 "not found. Wrong version?"
255 )
256
257 self.ip_address = node_ip_address
258
259 # print self.name, ": obtain_port_correspondence ports:", self.pp2ofi
260 return self.pp2ofi
261 except requests.exceptions.RequestException as e:
262 error_text = type(e).__name__ + ": " + str(e)
263 self.logger.error("obtain_port_correspondence " + error_text)
264
265 raise OpenflowConnConnectionException(error_text)
266 except ValueError as e:
267 # ValueError in the case that JSON can not be decoded
268 error_text = type(e).__name__ + ": " + str(e)
269 self.logger.error("obtain_port_correspondence " + error_text)
270
271 raise OpenflowConnUnexpectedResponse(error_text)
272
273 def get_of_rules(self, translate_of_ports=True):
274 """
275 Obtain the rules inserted at openflow controller
276 :param translate_of_ports: if True it translates ports from openflow index to physical switch name
277 :return: list where each item is a dictionary with the following content:
278 priority: rule priority
279 name: rule name (present also as the master dict key)
280 ingress_port: match input port of the rule
281 dst_mac: match destination mac address of the rule, can be missing or None if not apply
282 vlan_id: match vlan tag of the rule, can be missing or None if not apply
283 actions: list of actions, composed by a pair tuples:
284 (vlan, None/int): for stripping/setting a vlan tag
285 (out, port): send to this port
286 switch: DPID, all
287 Raise a openflowconnUnexpectedResponse exception in case of failure
288 """
289 try:
290 if len(self.ofi2pp) == 0:
291 self.obtain_port_correspondence()
292
293 # get rules
294 self.headers["content-type"] = "text/plain"
295 of_response = requests.get(
296 self.url + "flows/" + self.id, headers=self.headers
297 )
298 error_text = "Openflow response %d: %s" % (
299 of_response.status_code,
300 of_response.text,
301 )
302
303 # The configured page does not exist if there are no rules installed. In that case we return an empty dict
304 if of_response.status_code == 404:
305 return []
306 elif of_response.status_code != 200:
307 self.logger.warning("get_of_rules " + error_text)
308
309 raise OpenflowConnUnexpectedResponse(error_text)
310
311 self.logger.debug("get_of_rules " + error_text)
312
313 info = of_response.json()
314
315 if type(info) != dict:
316 self.logger.error(
317 "get_of_rules. Unexpected response, not a dict: %s",
318 str(info),
319 )
320
321 raise OpenflowConnUnexpectedResponse(
322 "Unexpected openflow response, not a dict. Wrong version?"
323 )
324
325 flow_list = info.get("flows")
326
327 if flow_list is None:
328 return []
329
330 if type(flow_list) is not list:
331 self.logger.error(
332 "get_of_rules. Unexpected response at 'flows', not a list: %s",
333 str(type(flow_list)),
334 )
335
336 raise OpenflowConnUnexpectedResponse(
337 "Unexpected response at 'flows', not a list. Wrong version?"
338 )
339
340 rules = [] # Response list
341 for flow in flow_list:
342 if not (
343 "id" in flow
344 and "selector" in flow
345 and "treatment" in flow
346 and "instructions" in flow["treatment"]
347 and "criteria" in flow["selector"]
348 ):
349 raise OpenflowConnUnexpectedResponse(
350 "unexpected openflow response, one or more "
351 "elements are missing. Wrong version?"
352 )
353
354 rule = dict()
355 rule["switch"] = self.dpid
356 rule["priority"] = flow.get("priority")
357 rule["name"] = flow["id"]
358
359 for criteria in flow["selector"]["criteria"]:
360 if criteria["type"] == "IN_PORT":
361 in_port = str(criteria["port"])
362 if in_port != "CONTROLLER":
363 if in_port not in self.ofi2pp:
364 raise OpenflowConnUnexpectedResponse(
365 "Error: Ingress port {} is not "
366 "in switch port list".format(in_port)
367 )
368
369 if translate_of_ports:
370 in_port = self.ofi2pp[in_port]
371
372 rule["ingress_port"] = in_port
373 elif criteria["type"] == "VLAN_VID":
374 rule["vlan_id"] = criteria["vlanId"]
375 elif criteria["type"] == "ETH_DST":
376 rule["dst_mac"] = str(criteria["mac"]).lower()
377
378 actions = []
379 for instruction in flow["treatment"]["instructions"]:
380 if instruction["type"] == "OUTPUT":
381 out_port = str(instruction["port"])
382 if out_port != "CONTROLLER":
383 if out_port not in self.ofi2pp:
384 raise OpenflowConnUnexpectedResponse(
385 "Error: Output port {} is not in "
386 "switch port list".format(out_port)
387 )
388
389 if translate_of_ports:
390 out_port = self.ofi2pp[out_port]
391
392 actions.append(("out", out_port))
393
394 if (
395 instruction["type"] == "L2MODIFICATION"
396 and instruction["subtype"] == "VLAN_POP"
397 ):
398 actions.append(("vlan", "None"))
399
400 if (
401 instruction["type"] == "L2MODIFICATION"
402 and instruction["subtype"] == "VLAN_ID"
403 ):
404 actions.append(("vlan", instruction["vlanId"]))
405
406 rule["actions"] = actions
407 rules.append(rule)
408
409 return rules
410 except requests.exceptions.RequestException as e:
411 # ValueError in the case that JSON can not be decoded
412 error_text = type(e).__name__ + ": " + str(e)
413 self.logger.error("get_of_rules " + error_text)
414
415 raise OpenflowConnConnectionException(error_text)
416 except ValueError as e:
417 # ValueError in the case that JSON can not be decoded
418 error_text = type(e).__name__ + ": " + str(e)
419 self.logger.error("get_of_rules " + error_text)
420
421 raise OpenflowConnUnexpectedResponse(error_text)
422
423 def del_flow(self, flow_name):
424 """
425 Delete an existing rule
426 :param flow_name:
427 :return: Raise a openflowconnUnexpectedResponse expection in case of failure
428 """
429 try:
430 self.logger.debug("del_flow: delete flow name {}".format(flow_name))
431 self.headers["content-type"] = None
432 of_response = requests.delete(
433 self.url + "flows/" + self.id + "/" + flow_name, headers=self.headers
434 )
435 error_text = "Openflow response {}: {}".format(
436 of_response.status_code, of_response.text
437 )
438
439 if of_response.status_code != 204:
440 self.logger.warning("del_flow " + error_text)
441
442 raise OpenflowConnUnexpectedResponse(error_text)
443
444 self.logger.debug("del_flow: {} OK,: {} ".format(flow_name, error_text))
445
446 return None
447 except requests.exceptions.RequestException as e:
448 error_text = type(e).__name__ + ": " + str(e)
449 self.logger.error("del_flow " + error_text)
450
451 raise OpenflowConnConnectionException(error_text)
452
453 def new_flow(self, data):
454 """
455 Insert a new static rule
456 :param data: dictionary with the following content:
457 priority: rule priority
458 name: rule name
459 ingress_port: match input port of the rule
460 dst_mac: match destination mac address of the rule, missing or None if not apply
461 vlan_id: match vlan tag of the rule, missing or None if not apply
462 actions: list of actions, composed by a pair tuples with these posibilities:
463 ('vlan', None/int): for stripping/setting a vlan tag
464 ('out', port): send to this port
465 :return: Raise a openflowconnUnexpectedResponse exception in case of failure
466 """
467 try:
468 self.logger.debug("new_flow data: {}".format(data))
469
470 if len(self.pp2ofi) == 0:
471 self.obtain_port_correspondence()
472
473 # Build the dictionary with the flow rule information for ONOS
474 flow = dict()
475 # flow["id"] = data["name"]
476 flow["tableId"] = 0
477 flow["priority"] = data.get("priority")
478 flow["timeout"] = 0
479 flow["isPermanent"] = "true"
480 flow["appId"] = 10 # FIXME We should create an appId for OSM
481 flow["selector"] = dict()
482 flow["selector"]["criteria"] = list()
483
484 # Flow rule matching criteria
485 if not data["ingress_port"] in self.pp2ofi:
486 error_text = (
487 "Error. Port "
488 + data["ingress_port"]
489 + " is not present in the switch"
490 )
491 self.logger.warning("new_flow " + error_text)
492
493 raise OpenflowConnUnexpectedResponse(error_text)
494
495 ingress_port_criteria = dict()
496 ingress_port_criteria["type"] = "IN_PORT"
497 ingress_port_criteria["port"] = self.pp2ofi[data["ingress_port"]]
498 flow["selector"]["criteria"].append(ingress_port_criteria)
499
500 if "dst_mac" in data:
501 dst_mac_criteria = dict()
502 dst_mac_criteria["type"] = "ETH_DST"
503 dst_mac_criteria["mac"] = data["dst_mac"]
504 flow["selector"]["criteria"].append(dst_mac_criteria)
505
506 if data.get("vlan_id"):
507 vlan_criteria = dict()
508 vlan_criteria["type"] = "VLAN_VID"
509 vlan_criteria["vlanId"] = int(data["vlan_id"])
510 flow["selector"]["criteria"].append(vlan_criteria)
511
512 # Flow rule treatment
513 flow["treatment"] = dict()
514 flow["treatment"]["instructions"] = list()
515 flow["treatment"]["deferred"] = list()
516
517 for action in data["actions"]:
518 new_action = dict()
519 if action[0] == "vlan":
520 new_action["type"] = "L2MODIFICATION"
521
522 if action[1] is None:
523 new_action["subtype"] = "VLAN_POP"
524 else:
525 new_action["subtype"] = "VLAN_ID"
526 new_action["vlanId"] = int(action[1])
527 elif action[0] == "out":
528 new_action["type"] = "OUTPUT"
529
530 if not action[1] in self.pp2ofi:
531 error_msj = (
532 "Port " + action[1] + " is not present in the switch"
533 )
534
535 raise OpenflowConnUnexpectedResponse(error_msj)
536
537 new_action["port"] = self.pp2ofi[action[1]]
538 else:
539 error_msj = "Unknown item '%s' in action list" % action[0]
540 self.logger.error("new_flow " + error_msj)
541
542 raise OpenflowConnUnexpectedResponse(error_msj)
543
544 flow["treatment"]["instructions"].append(new_action)
545
546 self.headers["content-type"] = "application/json"
547 path = self.url + "flows/" + self.id
548 self.logger.debug("new_flow post: {}".format(flow))
549 of_response = requests.post(
550 path, headers=self.headers, data=json.dumps(flow)
551 )
552
553 error_text = "Openflow response {}: {}".format(
554 of_response.status_code, of_response.text
555 )
556 if of_response.status_code != 201:
557 self.logger.warning("new_flow " + error_text)
558
559 raise OpenflowConnUnexpectedResponse(error_text)
560
561 flowId = of_response.headers["location"][path.__len__() + 1 :]
562 data["name"] = flowId
563
564 self.logger.debug("new_flow id: {},: {} ".format(flowId, error_text))
565
566 return None
567 except requests.exceptions.RequestException as e:
568 error_text = type(e).__name__ + ": " + str(e)
569 self.logger.error("new_flow " + error_text)
570
571 raise OpenflowConnConnectionException(error_text)
572
573 def clear_all_flows(self):
574 """
575 Delete all existing rules
576 :return: Raise a openflowconnUnexpectedResponse expection in case of failure
577 """
578 try:
579 rules = self.get_of_rules(True)
580
581 for rule in rules:
582 self.del_flow(rule)
583
584 self.logger.debug("clear_all_flows OK ")
585
586 return None
587 except requests.exceptions.RequestException as e:
588 error_text = type(e).__name__ + ": " + str(e)
589 self.logger.error("clear_all_flows " + error_text)
590
591 raise OpenflowConnConnectionException(error_text)