2 # -*- coding: utf-8 -*-
5 # Copyright 2015 Telefonica Investigacion y Desarrollo, S.A.U.
6 # This file is part of openvim
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
13 # http://www.apache.org/licenses/LICENSE-2.0
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
21 # For those usages not covered by the Apache License, Version 2.0 please
22 # contact with: nfvlabs@tid.es
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
35 from osm_ro_plugin
.openflow_conn
import (
37 OpenflowConnConnectionException
,
38 OpenflowConnUnexpectedResponse
,
41 # OpenflowConnException, OpenflowConnAuthException, OpenflowConnNotFoundException,
42 # OpenflowConnConflictException, OpenflowConnNotSupportedException, OpenflowConnNotImplemented
44 __author__
= "Pablo Montes, Alfonso Tierno"
45 __date__
= "$28-oct-2014 12:07:15$"
48 class OfConnOdl(OpenflowConn
):
49 """OpenDayLight connector. No MAC learning is used"""
51 def __init__(self
, params
):
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
62 OpenflowConn
.__init
__(self
, params
)
65 url
= params
.get("of_url")
68 raise ValueError("'url' must be provided")
70 if not url
.startswith("http"):
73 if not url
.endswith("/"):
79 self
.name
= "OpenDayLight"
81 "content-type": "application/json",
82 "Accept": "application/json",
85 self
.pp2ofi
= {} # From Physical Port to OpenFlow Index
86 self
.ofi2pp
= {} # From OpenFlow Index to Physical Port
88 self
.dpid
= str(params
["of_dpid"])
89 self
.id = "openflow:" + str(int(self
.dpid
.replace(":", ""), 16))
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")
96 self
.auth
= self
.auth
.decode()
97 self
.headers
["authorization"] = "Basic " + self
.auth
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")
103 def get_of_switches(self
):
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
110 of_response
= requests
.get(
111 self
.url
+ "restconf/operational/opendaylight-inventory:nodes",
112 headers
=self
.headers
,
114 error_text
= "Openflow response {}: {}".format(
115 of_response
.status_code
, of_response
.text
118 if of_response
.status_code
!= 200:
119 self
.logger
.warning("get_of_switches " + error_text
)
121 raise OpenflowConnUnexpectedResponse(
122 "Error get_of_switches " + error_text
125 self
.logger
.debug("get_of_switches " + error_text
)
126 info
= of_response
.json()
128 if not isinstance(info
, dict):
130 "get_of_switches. Unexpected response, not a dict: %s",
134 raise OpenflowConnUnexpectedResponse(
135 "Unexpected response, not a dict. Wrong version?"
138 nodes
= info
.get("nodes")
139 if type(nodes
) is not dict:
141 "get_of_switches. Unexpected response at 'nodes', not found or not a dict: %s",
145 raise OpenflowConnUnexpectedResponse(
146 "Unexpected response at 'nodes', not found or not a dict."
150 node_list
= nodes
.get("node")
151 if type(node_list
) is not list:
153 "get_of_switches. Unexpected response, at 'nodes':'node', "
154 "not found or not a list: %s",
155 str(type(node_list
)),
158 raise OpenflowConnUnexpectedResponse(
159 "Unexpected response, at 'nodes':'node', not found "
160 "or not a list. Wrong version?"
164 for node
in node_list
:
165 node_id
= node
.get("id")
168 "get_of_switches. Unexpected response at 'nodes':'node'[]:'id', not found: %s",
172 raise OpenflowConnUnexpectedResponse(
173 "Unexpected response at 'nodes':'node'[]:'id', not found. "
177 if node_id
== "controller-config":
180 node_ip_address
= node
.get("flow-node-inventory:ip-address")
181 if node_ip_address
is None:
183 "get_of_switches. Unexpected response at 'nodes':'node'[]:'flow-node-inventory:"
184 "ip-address', not found: %s",
188 raise OpenflowConnUnexpectedResponse(
189 "Unexpected response at 'nodes':'node'[]:"
190 "'flow-node-inventory:ip-address', not found. Wrong version?"
193 node_id_hex
= hex(int(node_id
.split(":")[1])).split("x")[1].zfill(16)
197 a
+ b
for a
, b
in zip(node_id_hex
[::2], node_id_hex
[1::2])
204 except requests
.exceptions
.RequestException
as e
:
205 error_text
= type(e
).__name
__ + ": " + str(e
)
206 self
.logger
.error("get_of_switches " + error_text
)
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
)
214 raise OpenflowConnUnexpectedResponse(error_text
)
216 def obtain_port_correspondence(self
):
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
223 of_response
= requests
.get(
224 self
.url
+ "restconf/operational/opendaylight-inventory:nodes",
225 headers
=self
.headers
,
227 error_text
= "Openflow response {}: {}".format(
228 of_response
.status_code
, of_response
.text
231 if of_response
.status_code
!= 200:
232 self
.logger
.warning("obtain_port_correspondence " + error_text
)
234 raise OpenflowConnUnexpectedResponse(error_text
)
236 self
.logger
.debug("obtain_port_correspondence " + error_text
)
237 info
= of_response
.json()
239 if not isinstance(info
, dict):
241 "obtain_port_correspondence. Unexpected response not a dict: %s",
245 raise OpenflowConnUnexpectedResponse(
246 "Unexpected openflow response, not a dict. Wrong version?"
249 nodes
= info
.get("nodes")
250 if not isinstance(nodes
, dict):
252 "obtain_port_correspondence. Unexpected response at 'nodes', "
253 "not found or not a dict: %s",
257 raise OpenflowConnUnexpectedResponse(
258 "Unexpected response at 'nodes',not found or not a dict. "
262 node_list
= nodes
.get("node")
263 if not isinstance(node_list
, list):
265 "obtain_port_correspondence. Unexpected response, at 'nodes':'node', "
266 "not found or not a list: %s",
267 str(type(node_list
)),
270 raise OpenflowConnUnexpectedResponse(
271 "Unexpected response, at 'nodes':'node', not found or not a list."
275 for node
in node_list
:
276 node_id
= node
.get("id")
279 "obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:'id', "
284 raise OpenflowConnUnexpectedResponse(
285 "Unexpected response at 'nodes':'node'[]:'id', not found. "
289 if node_id
== "controller-config":
292 # Figure out if this is the appropriate switch. The 'id' is 'openflow:' plus the decimal value
294 # In case this is not the desired switch, continue
295 if self
.id != node_id
:
298 node_connector_list
= node
.get("node-connector")
299 if not isinstance(node_connector_list
, list):
301 "obtain_port_correspondence. Unexpected response at "
302 "'nodes':'node'[]:'node-connector', not found or not a list: %s",
306 raise OpenflowConnUnexpectedResponse(
307 "Unexpected response at 'nodes':'node'[]:'node-connector', "
308 "not found or not a list. Wrong version?"
311 for node_connector
in node_connector_list
:
312 self
.pp2ofi
[str(node_connector
["flow-node-inventory:name"])] = str(
315 self
.ofi2pp
[node_connector
["id"]] = str(
316 node_connector
["flow-node-inventory:name"]
319 node_ip_address
= node
.get("flow-node-inventory:ip-address")
320 if node_ip_address
is None:
322 "obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:"
323 "'flow-node-inventory:ip-address', not found: %s",
327 raise OpenflowConnUnexpectedResponse(
328 "Unexpected response at 'nodes':'node'[]:"
329 "'flow-node-inventory:ip-address', not found. Wrong version?"
332 # If we found the appropriate dpid no need to continue in the for loop
335 # print self.name, ": obtain_port_correspondence ports:", 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
)
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
)
347 raise OpenflowConnUnexpectedResponse(error_text
)
349 def get_of_rules(self
, translate_of_ports
=True):
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
363 Raise a OpenflowConnConnectionException exception in case of failure
368 if len(self
.ofi2pp
) == 0:
369 self
.obtain_port_correspondence()
371 of_response
= requests
.get(
373 + "restconf/config/opendaylight-inventory:nodes/node/"
376 headers
=self
.headers
,
378 error_text
= "Openflow response {}: {}".format(
379 of_response
.status_code
, of_response
.text
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:
385 elif of_response
.status_code
!= 200:
386 self
.logger
.warning("get_of_rules " + error_text
)
388 raise OpenflowConnUnexpectedResponse(error_text
)
390 self
.logger
.debug("get_of_rules " + error_text
)
392 info
= of_response
.json()
394 if not isinstance(info
, dict):
396 "get_of_rules. Unexpected response not a dict: %s", str(info
)
399 raise OpenflowConnUnexpectedResponse(
400 "Unexpected openflow response, not a dict. Wrong version?"
403 table
= info
.get("flow-node-inventory:table")
404 if not isinstance(table
, list):
406 "get_of_rules. Unexpected response at 'flow-node-inventory:table', "
411 raise OpenflowConnUnexpectedResponse(
412 "Unexpected response at 'flow-node-inventory:table', not a list. "
416 flow_list
= table
[0].get("flow")
417 if flow_list
is None:
420 if not isinstance(flow_list
, list):
422 "get_of_rules. Unexpected response at 'flow-node-inventory:table'[0]:'flow', not a "
424 str(type(flow_list
)),
427 raise OpenflowConnUnexpectedResponse(
428 "Unexpected response at 'flow-node-inventory:table'[0]:'flow', "
429 "not a list. Wrong version?"
432 # TODO translate ports according to translate_of_ports parameter
434 rules
= [] # Response list
435 for flow
in flow_list
:
439 and "instructions" in flow
440 and "instruction" in flow
["instructions"]
441 and "apply-actions" in flow
["instructions"]["instruction"][0]
443 in flow
["instructions"]["instruction"][0]["apply-actions"]
445 raise OpenflowConnUnexpectedResponse(
446 "unexpected openflow response, one or more elements are "
447 "missing. Wrong version?"
450 flow
["instructions"]["instruction"][0]["apply-actions"]["action"]
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(
466 if translate_of_ports
:
467 in_port
= self
.ofi2pp
[in_port
]
469 rule
["ingress_port"] = in_port
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"]
479 rule
["vlan_id"] = flow
["match"]["vlan-match"]["vlan-id"][
484 "ethernet-match" in flow
["match"]
485 and "ethernet-destination" in flow
["match"]["ethernet-match"]
487 in flow
["match"]["ethernet-match"]["ethernet-destination"]
489 rule
["dst_mac"] = flow
["match"]["ethernet-match"][
490 "ethernet-destination"
493 instructions
= flow
["instructions"]["instruction"][0]["apply-actions"][
498 for instruction
in instructions
:
499 if instruction
["order"] > max_index
:
500 max_index
= instruction
["order"]
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?"
511 out_port
= instruction
["output-action"]["output-node-connector"]
513 if out_port
not in self
.ofi2pp
:
514 raise OpenflowConnUnexpectedResponse(
515 "Error: Output port {} is not in switch port list".format(
520 if translate_of_ports
:
521 out_port
= self
.ofi2pp
[out_port
]
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
:
528 "vlan-match" in instruction
["set-field"]
529 and "vlan-id" in instruction
["set-field"]["vlan-match"]
531 in instruction
["set-field"]["vlan-match"]["vlan-id"]
533 raise OpenflowConnUnexpectedResponse(
534 "unexpected openflow response, one or more elements "
535 "are missing. Wrong version?"
538 actions
[instruction
["order"]] = (
540 instruction
["set-field"]["vlan-match"]["vlan-id"][
545 actions
= [x
for x
in actions
if x
is not None]
547 rule
["actions"] = list(actions
)
551 except requests
.exceptions
.RequestException
as e
:
552 error_text
= type(e
).__name
__ + ": " + str(e
)
553 self
.logger
.error("get_of_rules " + error_text
)
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
)
561 raise OpenflowConnUnexpectedResponse(error_text
)
563 def del_flow(self
, flow_name
):
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
570 of_response
= requests
.delete(
572 + "restconf/config/opendaylight-inventory:nodes/node/"
576 headers
=self
.headers
,
578 error_text
= "Openflow response {}: {}".format(
579 of_response
.status_code
, of_response
.text
582 if of_response
.status_code
!= 200:
583 self
.logger
.warning("del_flow " + error_text
)
585 raise OpenflowConnUnexpectedResponse(error_text
)
587 self
.logger
.debug("del_flow OK " + error_text
)
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
)
595 raise OpenflowConnConnectionException(error_text
)
597 def new_flow(self
, data
):
599 Insert a new static rule
600 :param data: dictionary with the following content:
601 priority: rule priority
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
612 self
.logger
.debug("new_flow data: {}".format(data
))
614 if len(self
.pp2ofi
) == 0:
615 self
.obtain_port_correspondence()
617 # We have to build the data for the opendaylight call from the generic data
620 "flow-name": data
["name"],
624 "priority": data
.get("priority"),
627 sdata
= {"flow-node-inventory:flow": [flow
]}
629 if not data
["ingress_port"] in self
.pp2ofi
:
632 + data
["ingress_port"]
633 + " is not present in the switch"
635 self
.logger
.warning("new_flow " + error_text
)
637 raise OpenflowConnUnexpectedResponse(error_text
)
639 flow
["match"]["in-port"] = self
.pp2ofi
[data
["ingress_port"]]
641 if data
.get("dst_mac"):
642 flow
["match"]["ethernet-match"] = {
643 "ethernet-destination": {"address": data
["dst_mac"]}
646 if data
.get("vlan_id"):
647 flow
["match"]["vlan-match"] = {
649 "vlan-id-present": True,
650 "vlan-id": int(data
["vlan_id"]),
655 flow
["instructions"] = {
656 "instruction": [{"order": 1, "apply-actions": {"action": actions
}}]
660 for action
in data
["actions"]:
661 new_action
= {"order": order
}
662 if action
[0] == "vlan":
663 if action
[1] is None:
665 new_action
["strip-vlan-action"] = {}
667 new_action
["set-field"] = {
670 "vlan-id-present": True,
671 "vlan-id": int(action
[1]),
675 elif action
[0] == "out":
676 new_action
["output-action"] = {}
678 if not action
[1] in self
.pp2ofi
:
680 "Port " + action
[1] + " is not present in the switch"
683 raise OpenflowConnUnexpectedResponse(error_msg
)
685 new_action
["output-action"]["output-node-connector"] = self
.pp2ofi
[
689 error_msg
= "Unknown item '{}' in action list".format(action
[0])
690 self
.logger
.error("new_flow " + error_msg
)
692 raise OpenflowConnUnexpectedResponse(error_msg
)
694 actions
.append(new_action
)
697 # print json.dumps(sdata)
698 of_response
= requests
.put(
700 + "restconf/config/opendaylight-inventory:nodes/node/"
704 headers
=self
.headers
,
705 data
=json
.dumps(sdata
),
707 error_text
= "Openflow response {}: {}".format(
708 of_response
.status_code
, of_response
.text
711 if of_response
.status_code
!= 200:
712 self
.logger
.warning("new_flow " + error_text
)
714 raise OpenflowConnUnexpectedResponse(error_text
)
716 self
.logger
.debug("new_flow OK " + error_text
)
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
)
724 raise OpenflowConnConnectionException(error_text
)
726 def clear_all_flows(self
):
728 Delete all existing rules
729 :return: Raise a OpenflowConnConnectionException expection in case of failure
732 of_response
= requests
.delete(
734 + "restconf/config/opendaylight-inventory:nodes/node/"
737 headers
=self
.headers
,
739 error_text
= "Openflow response {}: {}".format(
740 of_response
.status_code
, of_response
.text
743 if of_response
.status_code
!= 200 and of_response
.status_code
!= 404:
744 self
.logger
.warning("clear_all_flows " + error_text
)
746 raise OpenflowConnUnexpectedResponse(error_text
)
748 self
.logger
.debug("clear_all_flows OK " + error_text
)
751 except requests
.exceptions
.RequestException
as e
:
752 error_text
= type(e
).__name
__ + ": " + str(e
)
753 self
.logger
.error("clear_all_flows " + error_text
)
755 raise OpenflowConnConnectionException(error_text
)