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