Enabling Flake8 and import sorting
[osm/RO.git] / RO-SDN-odl_openflow / osm_rosdn_odlof / odl_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 OpenDayLight 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 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__ = "Pablo Montes, Alfonso Tierno"
46 __date__ = "$28-oct-2014 12:07:15$"
47
48
49 class OfConnOdl(OpenflowConn):
50 """OpenDayLight connector. No MAC learning is used"""
51
52 def __init__(self, params):
53 """Constructor.
54 Params: dictionary with the following keys:
55 of_dpid: DPID to use for this controller
56 of_url: must be [http://HOST:PORT/]
57 of_user: user credentials, can be missing or None
58 of_password: password credentials
59 of_debug: debug level for logging. Default to ERROR
60 other keys are ignored
61 Raise an exception if same parameter is missing or wrong
62 """
63 OpenflowConn.__init__(self, params)
64
65 # check params
66 url = params.get("of_url")
67
68 if not url:
69 raise ValueError("'url' must be provided")
70
71 if not url.startswith("http"):
72 url = "http://" + url
73
74 if not url.endswith("/"):
75 url = url + "/"
76
77 self.url = url
78
79 # internal variables
80 self.name = "OpenDayLight"
81 self.headers = {
82 "content-type": "application/json",
83 "Accept": "application/json",
84 }
85 self.auth = None
86 self.pp2ofi = {} # From Physical Port to OpenFlow Index
87 self.ofi2pp = {} # From OpenFlow Index to Physical Port
88
89 self.dpid = str(params["of_dpid"])
90 self.id = "openflow:" + str(int(self.dpid.replace(":", ""), 16))
91
92 if params and params.get("of_user"):
93 of_password = params.get("of_password", "")
94 self.auth = base64.b64encode(
95 bytes(params["of_user"] + ":" + of_password, "utf-8")
96 )
97 self.auth = self.auth.decode()
98 self.headers["authorization"] = "Basic " + self.auth
99
100 self.logger = logging.getLogger("ro.sdn.onosof")
101 # self.logger.setLevel(getattr(logging, params.get("of_debug", "ERROR")))
102 self.logger.debug("odlof plugin initialized")
103
104 def get_of_switches(self):
105 """
106 Obtain a a list of switches or DPID detected by this controller
107 :return: list length, and a list where each element a tuple pair (DPID, IP address)
108 Raise an OpenflowConnConnectionException exception if fails with text_error
109 """
110 try:
111 of_response = requests.get(
112 self.url + "restconf/operational/opendaylight-inventory:nodes",
113 headers=self.headers,
114 )
115 error_text = "Openflow response {}: {}".format(
116 of_response.status_code, of_response.text
117 )
118
119 if of_response.status_code != 200:
120 self.logger.warning("get_of_switches " + error_text)
121
122 raise OpenflowConnUnexpectedResponse(
123 "Error get_of_switches " + error_text
124 )
125
126 self.logger.debug("get_of_switches " + error_text)
127 info = of_response.json()
128
129 if not isinstance(info, dict):
130 self.logger.error(
131 "get_of_switches. Unexpected response, not a dict: %s",
132 str(info),
133 )
134
135 raise OpenflowConnUnexpectedResponse(
136 "Unexpected response, not a dict. Wrong version?"
137 )
138
139 nodes = info.get("nodes")
140 if type(nodes) is not dict:
141 self.logger.error(
142 "get_of_switches. Unexpected response at 'nodes', not found or not a dict: %s",
143 str(type(info)),
144 )
145
146 raise OpenflowConnUnexpectedResponse(
147 "Unexpected response at 'nodes', not found or not a dict."
148 " Wrong version?"
149 )
150
151 node_list = nodes.get("node")
152 if type(node_list) is not list:
153 self.logger.error(
154 "get_of_switches. Unexpected response, at 'nodes':'node', "
155 "not found or not a list: %s",
156 str(type(node_list)),
157 )
158
159 raise OpenflowConnUnexpectedResponse(
160 "Unexpected response, at 'nodes':'node', not found "
161 "or not a list. Wrong version?"
162 )
163
164 switch_list = []
165 for node in node_list:
166 node_id = node.get("id")
167 if node_id is None:
168 self.logger.error(
169 "get_of_switches. Unexpected response at 'nodes':'node'[]:'id', not found: %s",
170 str(node),
171 )
172
173 raise OpenflowConnUnexpectedResponse(
174 "Unexpected response at 'nodes':'node'[]:'id', not found. "
175 "Wrong version?"
176 )
177
178 if node_id == "controller-config":
179 continue
180
181 node_ip_address = node.get("flow-node-inventory:ip-address")
182 if node_ip_address is None:
183 self.logger.error(
184 "get_of_switches. Unexpected response at 'nodes':'node'[]:'flow-node-inventory:"
185 "ip-address', not found: %s",
186 str(node),
187 )
188
189 raise OpenflowConnUnexpectedResponse(
190 "Unexpected response at 'nodes':'node'[]:"
191 "'flow-node-inventory:ip-address', not found. Wrong version?"
192 )
193
194 node_id_hex = hex(int(node_id.split(":")[1])).split("x")[1].zfill(16)
195 switch_list.append(
196 (
197 ":".join(
198 a + b for a, b in zip(node_id_hex[::2], node_id_hex[1::2])
199 ),
200 node_ip_address,
201 )
202 )
203
204 return switch_list
205 except requests.exceptions.RequestException as e:
206 error_text = type(e).__name__ + ": " + str(e)
207 self.logger.error("get_of_switches " + error_text)
208
209 raise OpenflowConnConnectionException(error_text)
210 except ValueError as e:
211 # ValueError in the case that JSON can not be decoded
212 error_text = type(e).__name__ + ": " + str(e)
213 self.logger.error("get_of_switches " + error_text)
214
215 raise OpenflowConnUnexpectedResponse(error_text)
216
217 def obtain_port_correspondence(self):
218 """
219 Obtain the correspondence between physical and openflow port names
220 :return: dictionary: with physical name as key, openflow name as value,
221 Raise a OpenflowConnConnectionException expection in case of failure
222 """
223 try:
224 of_response = requests.get(
225 self.url + "restconf/operational/opendaylight-inventory:nodes",
226 headers=self.headers,
227 )
228 error_text = "Openflow response {}: {}".format(
229 of_response.status_code, of_response.text
230 )
231
232 if of_response.status_code != 200:
233 self.logger.warning("obtain_port_correspondence " + error_text)
234
235 raise OpenflowConnUnexpectedResponse(error_text)
236
237 self.logger.debug("obtain_port_correspondence " + error_text)
238 info = of_response.json()
239
240 if not isinstance(info, dict):
241 self.logger.error(
242 "obtain_port_correspondence. Unexpected response not a dict: %s",
243 str(info),
244 )
245
246 raise OpenflowConnUnexpectedResponse(
247 "Unexpected openflow response, not a dict. Wrong version?"
248 )
249
250 nodes = info.get("nodes")
251 if not isinstance(nodes, dict):
252 self.logger.error(
253 "obtain_port_correspondence. Unexpected response at 'nodes', "
254 "not found or not a dict: %s",
255 str(type(nodes)),
256 )
257
258 raise OpenflowConnUnexpectedResponse(
259 "Unexpected response at 'nodes',not found or not a dict. "
260 "Wrong version?"
261 )
262
263 node_list = nodes.get("node")
264 if not isinstance(node_list, list):
265 self.logger.error(
266 "obtain_port_correspondence. Unexpected response, at 'nodes':'node', "
267 "not found or not a list: %s",
268 str(type(node_list)),
269 )
270
271 raise OpenflowConnUnexpectedResponse(
272 "Unexpected response, at 'nodes':'node', not found or not a list."
273 " Wrong version?"
274 )
275
276 for node in node_list:
277 node_id = node.get("id")
278 if node_id is None:
279 self.logger.error(
280 "obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:'id', "
281 "not found: %s",
282 str(node),
283 )
284
285 raise OpenflowConnUnexpectedResponse(
286 "Unexpected response at 'nodes':'node'[]:'id', not found. "
287 "Wrong version?"
288 )
289
290 if node_id == "controller-config":
291 continue
292
293 # Figure out if this is the appropriate switch. The 'id' is 'openflow:' plus the decimal value
294 # of the dpid
295 # In case this is not the desired switch, continue
296 if self.id != node_id:
297 continue
298
299 node_connector_list = node.get("node-connector")
300 if not isinstance(node_connector_list, list):
301 self.logger.error(
302 "obtain_port_correspondence. Unexpected response at "
303 "'nodes':'node'[]:'node-connector', not found or not a list: %s",
304 str(node),
305 )
306
307 raise OpenflowConnUnexpectedResponse(
308 "Unexpected response at 'nodes':'node'[]:'node-connector', "
309 "not found or not a list. Wrong version?"
310 )
311
312 for node_connector in node_connector_list:
313 self.pp2ofi[str(node_connector["flow-node-inventory:name"])] = str(
314 node_connector["id"]
315 )
316 self.ofi2pp[node_connector["id"]] = str(
317 node_connector["flow-node-inventory:name"]
318 )
319
320 node_ip_address = node.get("flow-node-inventory:ip-address")
321 if node_ip_address is None:
322 self.logger.error(
323 "obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:"
324 "'flow-node-inventory:ip-address', not found: %s",
325 str(node),
326 )
327
328 raise OpenflowConnUnexpectedResponse(
329 "Unexpected response at 'nodes':'node'[]:"
330 "'flow-node-inventory:ip-address', not found. Wrong version?"
331 )
332
333 # If we found the appropriate dpid no need to continue in the for loop
334 break
335
336 # print self.name, ": obtain_port_correspondence ports:", self.pp2ofi
337 return self.pp2ofi
338 except requests.exceptions.RequestException as e:
339 error_text = type(e).__name__ + ": " + str(e)
340 self.logger.error("obtain_port_correspondence " + error_text)
341
342 raise OpenflowConnConnectionException(error_text)
343 except ValueError as e:
344 # ValueError in the case that JSON can not be decoded
345 error_text = type(e).__name__ + ": " + str(e)
346 self.logger.error("obtain_port_correspondence " + error_text)
347
348 raise OpenflowConnUnexpectedResponse(error_text)
349
350 def get_of_rules(self, translate_of_ports=True):
351 """
352 Obtain the rules inserted at openflow controller
353 :param translate_of_ports:
354 :return: list where each item is a dictionary with the following content:
355 priority: rule priority
356 name: rule name (present also as the master dict key)
357 ingress_port: match input port of the rule
358 dst_mac: match destination mac address of the rule, can be missing or None if not apply
359 vlan_id: match vlan tag of the rule, can be missing or None if not apply
360 actions: list of actions, composed by a pair tuples:
361 (vlan, None/int): for stripping/setting a vlan tag
362 (out, port): send to this port
363 switch: DPID, all
364 Raise a OpenflowConnConnectionException exception in case of failure
365
366 """
367 try:
368 # get rules
369 if len(self.ofi2pp) == 0:
370 self.obtain_port_correspondence()
371
372 of_response = requests.get(
373 self.url
374 + "restconf/config/opendaylight-inventory:nodes/node/"
375 + self.id
376 + "/table/0",
377 headers=self.headers,
378 )
379 error_text = "Openflow response {}: {}".format(
380 of_response.status_code, of_response.text
381 )
382
383 # The configured page does not exist if there are no rules installed. In that case we return an empty dict
384 if of_response.status_code == 404:
385 return []
386 elif of_response.status_code != 200:
387 self.logger.warning("get_of_rules " + error_text)
388
389 raise OpenflowConnUnexpectedResponse(error_text)
390
391 self.logger.debug("get_of_rules " + error_text)
392
393 info = of_response.json()
394
395 if not isinstance(info, dict):
396 self.logger.error(
397 "get_of_rules. Unexpected response not a dict: %s", str(info)
398 )
399
400 raise OpenflowConnUnexpectedResponse(
401 "Unexpected openflow response, not a dict. Wrong version?"
402 )
403
404 table = info.get("flow-node-inventory:table")
405 if not isinstance(table, list):
406 self.logger.error(
407 "get_of_rules. Unexpected response at 'flow-node-inventory:table', "
408 "not a list: %s",
409 str(type(table)),
410 )
411
412 raise OpenflowConnUnexpectedResponse(
413 "Unexpected response at 'flow-node-inventory:table', not a list. "
414 "Wrong version?"
415 )
416
417 flow_list = table[0].get("flow")
418 if flow_list is None:
419 return []
420
421 if not isinstance(flow_list, list):
422 self.logger.error(
423 "get_of_rules. Unexpected response at 'flow-node-inventory:table'[0]:'flow', not a "
424 "list: %s",
425 str(type(flow_list)),
426 )
427
428 raise OpenflowConnUnexpectedResponse(
429 "Unexpected response at 'flow-node-inventory:table'[0]:'flow', "
430 "not a list. Wrong version?"
431 )
432
433 # TODO translate ports according to translate_of_ports parameter
434
435 rules = [] # Response list
436 for flow in flow_list:
437 if not (
438 "id" in flow
439 and "match" in flow
440 and "instructions" in flow
441 and "instruction" in flow["instructions"]
442 and "apply-actions" in flow["instructions"]["instruction"][0]
443 and "action"
444 in flow["instructions"]["instruction"][0]["apply-actions"]
445 ):
446 raise OpenflowConnUnexpectedResponse(
447 "unexpected openflow response, one or more elements are "
448 "missing. Wrong version?"
449 )
450
451 flow["instructions"]["instruction"][0]["apply-actions"]["action"]
452
453 rule = dict()
454 rule["switch"] = self.dpid
455 rule["priority"] = flow.get("priority")
456 # rule['name'] = flow['id']
457 # rule['cookie'] = flow['cookie']
458 if "in-port" in flow["match"]:
459 in_port = flow["match"]["in-port"]
460 if in_port not in self.ofi2pp:
461 raise OpenflowConnUnexpectedResponse(
462 "Error: Ingress port {} is not in switch port list".format(
463 in_port
464 )
465 )
466
467 if translate_of_ports:
468 in_port = self.ofi2pp[in_port]
469
470 rule["ingress_port"] = in_port
471
472 if (
473 "vlan-match" in flow["match"]
474 and "vlan-id" in flow["match"]["vlan-match"]
475 and "vlan-id" in flow["match"]["vlan-match"]["vlan-id"]
476 and "vlan-id-present" in flow["match"]["vlan-match"]["vlan-id"]
477 and flow["match"]["vlan-match"]["vlan-id"]["vlan-id-present"]
478 is True
479 ):
480 rule["vlan_id"] = flow["match"]["vlan-match"]["vlan-id"][
481 "vlan-id"
482 ]
483
484 if (
485 "ethernet-match" in flow["match"]
486 and "ethernet-destination" in flow["match"]["ethernet-match"]
487 and "address"
488 in flow["match"]["ethernet-match"]["ethernet-destination"]
489 ):
490 rule["dst_mac"] = flow["match"]["ethernet-match"][
491 "ethernet-destination"
492 ]["address"]
493
494 instructions = flow["instructions"]["instruction"][0]["apply-actions"][
495 "action"
496 ]
497
498 max_index = 0
499 for instruction in instructions:
500 if instruction["order"] > max_index:
501 max_index = instruction["order"]
502
503 actions = [None] * (max_index + 1)
504 for instruction in instructions:
505 if "output-action" in instruction:
506 if "output-node-connector" not in instruction["output-action"]:
507 raise OpenflowConnUnexpectedResponse(
508 "unexpected openflow response, one or more elementa "
509 "are missing. Wrong version?"
510 )
511
512 out_port = instruction["output-action"]["output-node-connector"]
513
514 if out_port not in self.ofi2pp:
515 raise OpenflowConnUnexpectedResponse(
516 "Error: Output port {} is not in switch port list".format(
517 out_port
518 )
519 )
520
521 if translate_of_ports:
522 out_port = self.ofi2pp[out_port]
523
524 actions[instruction["order"]] = ("out", out_port)
525 elif "strip-vlan-action" in instruction:
526 actions[instruction["order"]] = ("vlan", None)
527 elif "set-field" in instruction:
528 if not (
529 "vlan-match" in instruction["set-field"]
530 and "vlan-id" in instruction["set-field"]["vlan-match"]
531 and "vlan-id"
532 in instruction["set-field"]["vlan-match"]["vlan-id"]
533 ):
534 raise OpenflowConnUnexpectedResponse(
535 "unexpected openflow response, one or more elements "
536 "are missing. Wrong version?"
537 )
538
539 actions[instruction["order"]] = (
540 "vlan",
541 instruction["set-field"]["vlan-match"]["vlan-id"][
542 "vlan-id"
543 ],
544 )
545
546 actions = [x for x in actions if x is not None]
547
548 rule["actions"] = list(actions)
549 rules.append(rule)
550
551 return rules
552 except requests.exceptions.RequestException as e:
553 error_text = type(e).__name__ + ": " + str(e)
554 self.logger.error("get_of_rules " + error_text)
555
556 raise OpenflowConnConnectionException(error_text)
557 except ValueError as e:
558 # ValueError in the case that JSON can not be decoded
559 error_text = type(e).__name__ + ": " + str(e)
560 self.logger.error("get_of_rules " + error_text)
561
562 raise OpenflowConnUnexpectedResponse(error_text)
563
564 def del_flow(self, flow_name):
565 """
566 Delete an existing rule
567 :param flow_name: flow_name, this is the rule name
568 :return: Raise a OpenflowConnConnectionException expection in case of failure
569 """
570 try:
571 of_response = requests.delete(
572 self.url
573 + "restconf/config/opendaylight-inventory:nodes/node/"
574 + self.id
575 + "/table/0/flow/"
576 + flow_name,
577 headers=self.headers,
578 )
579 error_text = "Openflow response {}: {}".format(
580 of_response.status_code, of_response.text
581 )
582
583 if of_response.status_code != 200:
584 self.logger.warning("del_flow " + error_text)
585
586 raise OpenflowConnUnexpectedResponse(error_text)
587
588 self.logger.debug("del_flow OK " + error_text)
589
590 return None
591 except requests.exceptions.RequestException as e:
592 # raise an exception in case of contection error
593 error_text = type(e).__name__ + ": " + str(e)
594 self.logger.error("del_flow " + error_text)
595
596 raise OpenflowConnConnectionException(error_text)
597
598 def new_flow(self, data):
599 """
600 Insert a new static rule
601 :param data: dictionary with the following content:
602 priority: rule priority
603 name: rule name
604 ingress_port: match input port of the rule
605 dst_mac: match destination mac address of the rule, missing or None if not apply
606 vlan_id: match vlan tag of the rule, missing or None if not apply
607 actions: list of actions, composed by a pair tuples with these posibilities:
608 ('vlan', None/int): for stripping/setting a vlan tag
609 ('out', port): send to this port
610 :return: Raise a OpenflowConnConnectionException exception in case of failure
611 """
612 try:
613 self.logger.debug("new_flow data: {}".format(data))
614
615 if len(self.pp2ofi) == 0:
616 self.obtain_port_correspondence()
617
618 # We have to build the data for the opendaylight call from the generic data
619 flow = {
620 "id": data["name"],
621 "flow-name": data["name"],
622 "idle-timeout": 0,
623 "hard-timeout": 0,
624 "table_id": 0,
625 "priority": data.get("priority"),
626 "match": {},
627 }
628 sdata = {"flow-node-inventory:flow": [flow]}
629
630 if not data["ingress_port"] in self.pp2ofi:
631 error_text = (
632 "Error. Port "
633 + data["ingress_port"]
634 + " is not present in the switch"
635 )
636 self.logger.warning("new_flow " + error_text)
637
638 raise OpenflowConnUnexpectedResponse(error_text)
639
640 flow["match"]["in-port"] = self.pp2ofi[data["ingress_port"]]
641
642 if data.get("dst_mac"):
643 flow["match"]["ethernet-match"] = {
644 "ethernet-destination": {"address": data["dst_mac"]}
645 }
646
647 if data.get("vlan_id"):
648 flow["match"]["vlan-match"] = {
649 "vlan-id": {
650 "vlan-id-present": True,
651 "vlan-id": int(data["vlan_id"]),
652 }
653 }
654
655 actions = []
656 flow["instructions"] = {
657 "instruction": [{"order": 1, "apply-actions": {"action": actions}}]
658 }
659
660 order = 0
661 for action in data["actions"]:
662 new_action = {"order": order}
663 if action[0] == "vlan":
664 if action[1] is None:
665 # strip vlan
666 new_action["strip-vlan-action"] = {}
667 else:
668 new_action["set-field"] = {
669 "vlan-match": {
670 "vlan-id": {
671 "vlan-id-present": True,
672 "vlan-id": int(action[1]),
673 }
674 }
675 }
676 elif action[0] == "out":
677 new_action["output-action"] = {}
678
679 if not action[1] in self.pp2ofi:
680 error_msg = (
681 "Port " + action[1] + " is not present in the switch"
682 )
683
684 raise OpenflowConnUnexpectedResponse(error_msg)
685
686 new_action["output-action"]["output-node-connector"] = self.pp2ofi[
687 action[1]
688 ]
689 else:
690 error_msg = "Unknown item '{}' in action list".format(action[0])
691 self.logger.error("new_flow " + error_msg)
692
693 raise OpenflowConnUnexpectedResponse(error_msg)
694
695 actions.append(new_action)
696 order += 1
697
698 # print json.dumps(sdata)
699 of_response = requests.put(
700 self.url
701 + "restconf/config/opendaylight-inventory:nodes/node/"
702 + self.id
703 + "/table/0/flow/"
704 + data["name"],
705 headers=self.headers,
706 data=json.dumps(sdata),
707 )
708 error_text = "Openflow response {}: {}".format(
709 of_response.status_code, of_response.text
710 )
711
712 if of_response.status_code != 200:
713 self.logger.warning("new_flow " + error_text)
714
715 raise OpenflowConnUnexpectedResponse(error_text)
716
717 self.logger.debug("new_flow OK " + error_text)
718
719 return None
720 except requests.exceptions.RequestException as e:
721 # raise an exception in case of contection error
722 error_text = type(e).__name__ + ": " + str(e)
723 self.logger.error("new_flow " + error_text)
724
725 raise OpenflowConnConnectionException(error_text)
726
727 def clear_all_flows(self):
728 """
729 Delete all existing rules
730 :return: Raise a OpenflowConnConnectionException expection in case of failure
731 """
732 try:
733 of_response = requests.delete(
734 self.url
735 + "restconf/config/opendaylight-inventory:nodes/node/"
736 + self.id
737 + "/table/0",
738 headers=self.headers,
739 )
740 error_text = "Openflow response {}: {}".format(
741 of_response.status_code, of_response.text
742 )
743
744 if of_response.status_code != 200 and of_response.status_code != 404:
745 self.logger.warning("clear_all_flows " + error_text)
746
747 raise OpenflowConnUnexpectedResponse(error_text)
748
749 self.logger.debug("clear_all_flows OK " + error_text)
750
751 return None
752 except requests.exceptions.RequestException as e:
753 error_text = type(e).__name__ + ": " + str(e)
754 self.logger.error("clear_all_flows " + error_text)
755
756 raise OpenflowConnConnectionException(error_text)