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
,
42 # OpenflowConnException, OpenflowConnAuthException, OpenflowConnNotFoundException,
43 # OpenflowConnConflictException, OpenflowConnNotSupportedException, OpenflowConnNotImplemented
45 __author__
= "Pablo Montes, Alfonso Tierno"
46 __date__
= "$28-oct-2014 12:07:15$"
49 class OfConnOdl(OpenflowConn
):
50 """OpenDayLight connector. No MAC learning is used"""
52 def __init__(self
, params
):
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
63 OpenflowConn
.__init
__(self
, params
)
66 url
= params
.get("of_url")
69 raise ValueError("'url' must be provided")
71 if not url
.startswith("http"):
74 if not url
.endswith("/"):
80 self
.name
= "OpenDayLight"
82 "content-type": "application/json",
83 "Accept": "application/json",
86 self
.pp2ofi
= {} # From Physical Port to OpenFlow Index
87 self
.ofi2pp
= {} # From OpenFlow Index to Physical Port
89 self
.dpid
= str(params
["of_dpid"])
90 self
.id = "openflow:" + str(int(self
.dpid
.replace(":", ""), 16))
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")
97 self
.auth
= self
.auth
.decode()
98 self
.headers
["authorization"] = "Basic " + self
.auth
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")
104 def get_of_switches(self
):
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
111 of_response
= requests
.get(
112 self
.url
+ "restconf/operational/opendaylight-inventory:nodes",
113 headers
=self
.headers
,
115 error_text
= "Openflow response {}: {}".format(
116 of_response
.status_code
, of_response
.text
119 if of_response
.status_code
!= 200:
120 self
.logger
.warning("get_of_switches " + error_text
)
122 raise OpenflowConnUnexpectedResponse(
123 "Error get_of_switches " + error_text
126 self
.logger
.debug("get_of_switches " + error_text
)
127 info
= of_response
.json()
129 if not isinstance(info
, dict):
131 "get_of_switches. Unexpected response, not a dict: %s",
135 raise OpenflowConnUnexpectedResponse(
136 "Unexpected response, not a dict. Wrong version?"
139 nodes
= info
.get("nodes")
140 if type(nodes
) is not dict:
142 "get_of_switches. Unexpected response at 'nodes', not found or not a dict: %s",
146 raise OpenflowConnUnexpectedResponse(
147 "Unexpected response at 'nodes', not found or not a dict."
151 node_list
= nodes
.get("node")
152 if type(node_list
) is not list:
154 "get_of_switches. Unexpected response, at 'nodes':'node', "
155 "not found or not a list: %s",
156 str(type(node_list
)),
159 raise OpenflowConnUnexpectedResponse(
160 "Unexpected response, at 'nodes':'node', not found "
161 "or not a list. Wrong version?"
165 for node
in node_list
:
166 node_id
= node
.get("id")
169 "get_of_switches. Unexpected response at 'nodes':'node'[]:'id', not found: %s",
173 raise OpenflowConnUnexpectedResponse(
174 "Unexpected response at 'nodes':'node'[]:'id', not found. "
178 if node_id
== "controller-config":
181 node_ip_address
= node
.get("flow-node-inventory:ip-address")
182 if node_ip_address
is None:
184 "get_of_switches. Unexpected response at 'nodes':'node'[]:'flow-node-inventory:"
185 "ip-address', not found: %s",
189 raise OpenflowConnUnexpectedResponse(
190 "Unexpected response at 'nodes':'node'[]:"
191 "'flow-node-inventory:ip-address', not found. Wrong version?"
194 node_id_hex
= hex(int(node_id
.split(":")[1])).split("x")[1].zfill(16)
198 a
+ b
for a
, b
in zip(node_id_hex
[::2], node_id_hex
[1::2])
205 except requests
.exceptions
.RequestException
as e
:
206 error_text
= type(e
).__name
__ + ": " + str(e
)
207 self
.logger
.error("get_of_switches " + error_text
)
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
)
215 raise OpenflowConnUnexpectedResponse(error_text
)
217 def obtain_port_correspondence(self
):
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
224 of_response
= requests
.get(
225 self
.url
+ "restconf/operational/opendaylight-inventory:nodes",
226 headers
=self
.headers
,
228 error_text
= "Openflow response {}: {}".format(
229 of_response
.status_code
, of_response
.text
232 if of_response
.status_code
!= 200:
233 self
.logger
.warning("obtain_port_correspondence " + error_text
)
235 raise OpenflowConnUnexpectedResponse(error_text
)
237 self
.logger
.debug("obtain_port_correspondence " + error_text
)
238 info
= of_response
.json()
240 if not isinstance(info
, dict):
242 "obtain_port_correspondence. Unexpected response not a dict: %s",
246 raise OpenflowConnUnexpectedResponse(
247 "Unexpected openflow response, not a dict. Wrong version?"
250 nodes
= info
.get("nodes")
251 if not isinstance(nodes
, dict):
253 "obtain_port_correspondence. Unexpected response at 'nodes', "
254 "not found or not a dict: %s",
258 raise OpenflowConnUnexpectedResponse(
259 "Unexpected response at 'nodes',not found or not a dict. "
263 node_list
= nodes
.get("node")
264 if not isinstance(node_list
, list):
266 "obtain_port_correspondence. Unexpected response, at 'nodes':'node', "
267 "not found or not a list: %s",
268 str(type(node_list
)),
271 raise OpenflowConnUnexpectedResponse(
272 "Unexpected response, at 'nodes':'node', not found or not a list."
276 for node
in node_list
:
277 node_id
= node
.get("id")
280 "obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:'id', "
285 raise OpenflowConnUnexpectedResponse(
286 "Unexpected response at 'nodes':'node'[]:'id', not found. "
290 if node_id
== "controller-config":
293 # Figure out if this is the appropriate switch. The 'id' is 'openflow:' plus the decimal value
295 # In case this is not the desired switch, continue
296 if self
.id != node_id
:
299 node_connector_list
= node
.get("node-connector")
300 if not isinstance(node_connector_list
, list):
302 "obtain_port_correspondence. Unexpected response at "
303 "'nodes':'node'[]:'node-connector', not found or not a list: %s",
307 raise OpenflowConnUnexpectedResponse(
308 "Unexpected response at 'nodes':'node'[]:'node-connector', "
309 "not found or not a list. Wrong version?"
312 for node_connector
in node_connector_list
:
313 self
.pp2ofi
[str(node_connector
["flow-node-inventory:name"])] = str(
316 self
.ofi2pp
[node_connector
["id"]] = str(
317 node_connector
["flow-node-inventory:name"]
320 node_ip_address
= node
.get("flow-node-inventory:ip-address")
321 if node_ip_address
is None:
323 "obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:"
324 "'flow-node-inventory:ip-address', not found: %s",
328 raise OpenflowConnUnexpectedResponse(
329 "Unexpected response at 'nodes':'node'[]:"
330 "'flow-node-inventory:ip-address', not found. Wrong version?"
333 # If we found the appropriate dpid no need to continue in the for loop
336 # print self.name, ": obtain_port_correspondence ports:", 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
)
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
)
348 raise OpenflowConnUnexpectedResponse(error_text
)
350 def get_of_rules(self
, translate_of_ports
=True):
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
364 Raise a OpenflowConnConnectionException exception in case of failure
369 if len(self
.ofi2pp
) == 0:
370 self
.obtain_port_correspondence()
372 of_response
= requests
.get(
374 + "restconf/config/opendaylight-inventory:nodes/node/"
377 headers
=self
.headers
,
379 error_text
= "Openflow response {}: {}".format(
380 of_response
.status_code
, of_response
.text
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:
386 elif of_response
.status_code
!= 200:
387 self
.logger
.warning("get_of_rules " + error_text
)
389 raise OpenflowConnUnexpectedResponse(error_text
)
391 self
.logger
.debug("get_of_rules " + error_text
)
393 info
= of_response
.json()
395 if not isinstance(info
, dict):
397 "get_of_rules. Unexpected response not a dict: %s", str(info
)
400 raise OpenflowConnUnexpectedResponse(
401 "Unexpected openflow response, not a dict. Wrong version?"
404 table
= info
.get("flow-node-inventory:table")
405 if not isinstance(table
, list):
407 "get_of_rules. Unexpected response at 'flow-node-inventory:table', "
412 raise OpenflowConnUnexpectedResponse(
413 "Unexpected response at 'flow-node-inventory:table', not a list. "
417 flow_list
= table
[0].get("flow")
418 if flow_list
is None:
421 if not isinstance(flow_list
, list):
423 "get_of_rules. Unexpected response at 'flow-node-inventory:table'[0]:'flow', not a "
425 str(type(flow_list
)),
428 raise OpenflowConnUnexpectedResponse(
429 "Unexpected response at 'flow-node-inventory:table'[0]:'flow', "
430 "not a list. Wrong version?"
433 # TODO translate ports according to translate_of_ports parameter
435 rules
= [] # Response list
436 for flow
in flow_list
:
440 and "instructions" in flow
441 and "instruction" in flow
["instructions"]
442 and "apply-actions" in flow
["instructions"]["instruction"][0]
444 in flow
["instructions"]["instruction"][0]["apply-actions"]
446 raise OpenflowConnUnexpectedResponse(
447 "unexpected openflow response, one or more elements are "
448 "missing. Wrong version?"
451 flow
["instructions"]["instruction"][0]["apply-actions"]["action"]
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(
467 if translate_of_ports
:
468 in_port
= self
.ofi2pp
[in_port
]
470 rule
["ingress_port"] = in_port
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"]
480 rule
["vlan_id"] = flow
["match"]["vlan-match"]["vlan-id"][
485 "ethernet-match" in flow
["match"]
486 and "ethernet-destination" in flow
["match"]["ethernet-match"]
488 in flow
["match"]["ethernet-match"]["ethernet-destination"]
490 rule
["dst_mac"] = flow
["match"]["ethernet-match"][
491 "ethernet-destination"
494 instructions
= flow
["instructions"]["instruction"][0]["apply-actions"][
499 for instruction
in instructions
:
500 if instruction
["order"] > max_index
:
501 max_index
= instruction
["order"]
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?"
512 out_port
= instruction
["output-action"]["output-node-connector"]
514 if out_port
not in self
.ofi2pp
:
515 raise OpenflowConnUnexpectedResponse(
516 "Error: Output port {} is not in switch port list".format(
521 if translate_of_ports
:
522 out_port
= self
.ofi2pp
[out_port
]
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
:
529 "vlan-match" in instruction
["set-field"]
530 and "vlan-id" in instruction
["set-field"]["vlan-match"]
532 in instruction
["set-field"]["vlan-match"]["vlan-id"]
534 raise OpenflowConnUnexpectedResponse(
535 "unexpected openflow response, one or more elements "
536 "are missing. Wrong version?"
539 actions
[instruction
["order"]] = (
541 instruction
["set-field"]["vlan-match"]["vlan-id"][
546 actions
= [x
for x
in actions
if x
is not None]
548 rule
["actions"] = list(actions
)
552 except requests
.exceptions
.RequestException
as e
:
553 error_text
= type(e
).__name
__ + ": " + str(e
)
554 self
.logger
.error("get_of_rules " + error_text
)
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
)
562 raise OpenflowConnUnexpectedResponse(error_text
)
564 def del_flow(self
, flow_name
):
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
571 of_response
= requests
.delete(
573 + "restconf/config/opendaylight-inventory:nodes/node/"
577 headers
=self
.headers
,
579 error_text
= "Openflow response {}: {}".format(
580 of_response
.status_code
, of_response
.text
583 if of_response
.status_code
!= 200:
584 self
.logger
.warning("del_flow " + error_text
)
586 raise OpenflowConnUnexpectedResponse(error_text
)
588 self
.logger
.debug("del_flow OK " + error_text
)
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
)
596 raise OpenflowConnConnectionException(error_text
)
598 def new_flow(self
, data
):
600 Insert a new static rule
601 :param data: dictionary with the following content:
602 priority: rule priority
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
613 self
.logger
.debug("new_flow data: {}".format(data
))
615 if len(self
.pp2ofi
) == 0:
616 self
.obtain_port_correspondence()
618 # We have to build the data for the opendaylight call from the generic data
621 "flow-name": data
["name"],
625 "priority": data
.get("priority"),
628 sdata
= {"flow-node-inventory:flow": [flow
]}
630 if not data
["ingress_port"] in self
.pp2ofi
:
633 + data
["ingress_port"]
634 + " is not present in the switch"
636 self
.logger
.warning("new_flow " + error_text
)
638 raise OpenflowConnUnexpectedResponse(error_text
)
640 flow
["match"]["in-port"] = self
.pp2ofi
[data
["ingress_port"]]
642 if data
.get("dst_mac"):
643 flow
["match"]["ethernet-match"] = {
644 "ethernet-destination": {"address": data
["dst_mac"]}
647 if data
.get("vlan_id"):
648 flow
["match"]["vlan-match"] = {
650 "vlan-id-present": True,
651 "vlan-id": int(data
["vlan_id"]),
656 flow
["instructions"] = {
657 "instruction": [{"order": 1, "apply-actions": {"action": actions
}}]
661 for action
in data
["actions"]:
662 new_action
= {"order": order
}
663 if action
[0] == "vlan":
664 if action
[1] is None:
666 new_action
["strip-vlan-action"] = {}
668 new_action
["set-field"] = {
671 "vlan-id-present": True,
672 "vlan-id": int(action
[1]),
676 elif action
[0] == "out":
677 new_action
["output-action"] = {}
679 if not action
[1] in self
.pp2ofi
:
681 "Port " + action
[1] + " is not present in the switch"
684 raise OpenflowConnUnexpectedResponse(error_msg
)
686 new_action
["output-action"]["output-node-connector"] = self
.pp2ofi
[
690 error_msg
= "Unknown item '{}' in action list".format(action
[0])
691 self
.logger
.error("new_flow " + error_msg
)
693 raise OpenflowConnUnexpectedResponse(error_msg
)
695 actions
.append(new_action
)
698 # print json.dumps(sdata)
699 of_response
= requests
.put(
701 + "restconf/config/opendaylight-inventory:nodes/node/"
705 headers
=self
.headers
,
706 data
=json
.dumps(sdata
),
708 error_text
= "Openflow response {}: {}".format(
709 of_response
.status_code
, of_response
.text
712 if of_response
.status_code
!= 200:
713 self
.logger
.warning("new_flow " + error_text
)
715 raise OpenflowConnUnexpectedResponse(error_text
)
717 self
.logger
.debug("new_flow OK " + error_text
)
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
)
725 raise OpenflowConnConnectionException(error_text
)
727 def clear_all_flows(self
):
729 Delete all existing rules
730 :return: Raise a OpenflowConnConnectionException expection in case of failure
733 of_response
= requests
.delete(
735 + "restconf/config/opendaylight-inventory:nodes/node/"
738 headers
=self
.headers
,
740 error_text
= "Openflow response {}: {}".format(
741 of_response
.status_code
, of_response
.text
744 if of_response
.status_code
!= 200 and of_response
.status_code
!= 404:
745 self
.logger
.warning("clear_all_flows " + error_text
)
747 raise OpenflowConnUnexpectedResponse(error_text
)
749 self
.logger
.debug("clear_all_flows OK " + error_text
)
752 except requests
.exceptions
.RequestException
as e
:
753 error_text
= type(e
).__name
__ + ": " + str(e
)
754 self
.logger
.error("clear_all_flows " + error_text
)
756 raise OpenflowConnConnectionException(error_text
)