2 # -*- coding: utf-8 -*-
5 # Copyright 2016, I2T Research Group (UPV/EHU)
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: alaitz.mendiola@ehu.eus or alaitz.mendiola@gmail.com
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
35 from osm_ro_plugin
.openflow_conn
import (
37 OpenflowConnConnectionException
,
38 OpenflowConnUnexpectedResponse
,
42 # OpenflowConnException, OpenflowConnAuthException, OpenflowConnNotFoundException, \
43 # OpenflowConnConflictException, OpenflowConnNotSupportedException, OpenflowConnNotImplemented
45 __author__
= "Alaitz Mendiola"
46 __date__
= "$22-nov-2016$"
49 class OfConnOnos(OpenflowConn
):
51 ONOS connector. No MAC learning is used
54 def __init__(self
, params
):
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
65 OpenflowConn
.__init
__(self
, params
)
68 url
= params
.get("of_url")
71 raise ValueError("'url' must be provided")
73 if not url
.startswith("http"):
76 if not url
.endswith("/"):
79 self
.url
= url
+ "onos/v1/"
84 "content-type": "application/json",
85 "accept": "application/json",
89 self
.pp2ofi
= {} # From Physical Port to OpenFlow Index
90 self
.ofi2pp
= {} # From OpenFlow Index to Physical Port
92 self
.dpid
= str(params
["of_dpid"])
93 self
.id = "of:" + str(self
.dpid
.replace(":", ""))
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")
101 self
.auth
= self
.auth
.decode()
102 self
.headers
["authorization"] = "Basic " + self
.auth
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
109 def get_of_switches(self
):
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
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
,
123 if of_response
.status_code
!= 200:
124 self
.logger
.warning("get_of_switches " + error_text
)
126 raise OpenflowConnUnexpectedResponse(error_text
)
128 self
.logger
.debug("get_of_switches " + error_text
)
129 info
= of_response
.json()
131 if type(info
) != dict:
133 "get_of_switches. Unexpected response, not a dict: %s", str(info
)
136 raise OpenflowConnUnexpectedResponse(
137 "Unexpected response, not a dict. Wrong version?"
140 node_list
= info
.get("devices")
142 if type(node_list
) is not list:
144 "get_of_switches. Unexpected response, at 'devices', not found or not a list: %s",
145 str(type(node_list
)),
148 raise OpenflowConnUnexpectedResponse(
149 "Unexpected response, at 'devices', not found "
150 "or not a list. Wrong version?"
154 for node
in node_list
:
155 node_id
= node
.get("id")
158 "get_of_switches. Unexpected response at 'device':'id', not found: %s",
162 raise OpenflowConnUnexpectedResponse(
163 "Unexpected response at 'device':'id', "
164 "not found . Wrong version?"
167 node_ip_address
= node
.get("annotations").get("managementAddress")
168 if node_ip_address
is None:
170 "get_of_switches. Unexpected response at 'device':'managementAddress', not found: %s",
174 raise OpenflowConnUnexpectedResponse(
175 "Unexpected response at 'device':'managementAddress', not found. Wrong version?"
178 node_id_hex
= hex(int(node_id
.split(":")[1])).split("x")[1].zfill(16)
182 a
+ b
for a
, b
in zip(node_id_hex
[::2], node_id_hex
[1::2])
189 except requests
.exceptions
.RequestException
as e
:
190 error_text
= type(e
).__name
__ + ": " + str(e
)
191 self
.logger
.error("get_of_switches " + error_text
)
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
)
199 raise OpenflowConnUnexpectedResponse(error_text
)
201 def obtain_port_correspondence(self
):
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
208 self
.headers
["content-type"] = "text/plain"
209 of_response
= requests
.get(
210 self
.url
+ "devices/" + self
.id + "/ports", headers
=self
.headers
212 error_text
= "Openflow response {}: {}".format(
213 of_response
.status_code
, of_response
.text
216 if of_response
.status_code
!= 200:
217 self
.logger
.warning("obtain_port_correspondence " + error_text
)
219 raise OpenflowConnUnexpectedResponse(error_text
)
221 self
.logger
.debug("obtain_port_correspondence " + error_text
)
222 info
= of_response
.json()
224 node_connector_list
= info
.get("ports")
225 if type(node_connector_list
) is not list:
227 "obtain_port_correspondence. Unexpected response at 'ports', not found or not a list: %s",
228 str(node_connector_list
),
231 raise OpenflowConnUnexpectedResponse(
232 "Unexpected response at 'ports', not found or not "
233 "a list. Wrong version?"
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"]
241 self
.ofi2pp
[str(node_connector
["port"])] = str(
242 node_connector
["annotations"]["portName"]
245 node_ip_address
= info
["annotations"]["managementAddress"]
246 if node_ip_address
is None:
248 "obtain_port_correspondence. Unexpected response at 'managementAddress', not found: %s",
252 raise OpenflowConnUnexpectedResponse(
253 "Unexpected response at 'managementAddress', "
254 "not found. Wrong version?"
257 self
.ip_address
= node_ip_address
259 # print self.name, ": obtain_port_correspondence ports:", 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
)
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
)
271 raise OpenflowConnUnexpectedResponse(error_text
)
273 def get_of_rules(self
, translate_of_ports
=True):
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
287 Raise a openflowconnUnexpectedResponse exception in case of failure
290 if len(self
.ofi2pp
) == 0:
291 self
.obtain_port_correspondence()
294 self
.headers
["content-type"] = "text/plain"
295 of_response
= requests
.get(
296 self
.url
+ "flows/" + self
.id, headers
=self
.headers
298 error_text
= "Openflow response %d: %s" % (
299 of_response
.status_code
,
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:
306 elif of_response
.status_code
!= 200:
307 self
.logger
.warning("get_of_rules " + error_text
)
309 raise OpenflowConnUnexpectedResponse(error_text
)
311 self
.logger
.debug("get_of_rules " + error_text
)
313 info
= of_response
.json()
315 if type(info
) != dict:
317 "get_of_rules. Unexpected response, not a dict: %s",
321 raise OpenflowConnUnexpectedResponse(
322 "Unexpected openflow response, not a dict. Wrong version?"
325 flow_list
= info
.get("flows")
327 if flow_list
is None:
330 if type(flow_list
) is not list:
332 "get_of_rules. Unexpected response at 'flows', not a list: %s",
333 str(type(flow_list
)),
336 raise OpenflowConnUnexpectedResponse(
337 "Unexpected response at 'flows', not a list. Wrong version?"
340 rules
= [] # Response list
341 for flow
in flow_list
:
344 and "selector" in flow
345 and "treatment" in flow
346 and "instructions" in flow
["treatment"]
347 and "criteria" in flow
["selector"]
349 raise OpenflowConnUnexpectedResponse(
350 "unexpected openflow response, one or more "
351 "elements are missing. Wrong version?"
355 rule
["switch"] = self
.dpid
356 rule
["priority"] = flow
.get("priority")
357 rule
["name"] = flow
["id"]
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
)
369 if translate_of_ports
:
370 in_port
= self
.ofi2pp
[in_port
]
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()
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
)
389 if translate_of_ports
:
390 out_port
= self
.ofi2pp
[out_port
]
392 actions
.append(("out", out_port
))
395 instruction
["type"] == "L2MODIFICATION"
396 and instruction
["subtype"] == "VLAN_POP"
398 actions
.append(("vlan", "None"))
401 instruction
["type"] == "L2MODIFICATION"
402 and instruction
["subtype"] == "VLAN_ID"
404 actions
.append(("vlan", instruction
["vlanId"]))
406 rule
["actions"] = actions
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
)
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
)
421 raise OpenflowConnUnexpectedResponse(error_text
)
423 def del_flow(self
, flow_name
):
425 Delete an existing rule
427 :return: Raise a openflowconnUnexpectedResponse expection in case of failure
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
435 error_text
= "Openflow response {}: {}".format(
436 of_response
.status_code
, of_response
.text
439 if of_response
.status_code
!= 204:
440 self
.logger
.warning("del_flow " + error_text
)
442 raise OpenflowConnUnexpectedResponse(error_text
)
444 self
.logger
.debug("del_flow: {} OK,: {} ".format(flow_name
, error_text
))
447 except requests
.exceptions
.RequestException
as e
:
448 error_text
= type(e
).__name
__ + ": " + str(e
)
449 self
.logger
.error("del_flow " + error_text
)
451 raise OpenflowConnConnectionException(error_text
)
453 def new_flow(self
, data
):
455 Insert a new static rule
456 :param data: dictionary with the following content:
457 priority: rule priority
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
468 self
.logger
.debug("new_flow data: {}".format(data
))
470 if len(self
.pp2ofi
) == 0:
471 self
.obtain_port_correspondence()
473 # Build the dictionary with the flow rule information for ONOS
475 # flow["id"] = data["name"]
477 flow
["priority"] = data
.get("priority")
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()
484 # Flow rule matching criteria
485 if not data
["ingress_port"] in self
.pp2ofi
:
488 + data
["ingress_port"]
489 + " is not present in the switch"
491 self
.logger
.warning("new_flow " + error_text
)
493 raise OpenflowConnUnexpectedResponse(error_text
)
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
)
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
)
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
)
512 # Flow rule treatment
513 flow
["treatment"] = dict()
514 flow
["treatment"]["instructions"] = list()
515 flow
["treatment"]["deferred"] = list()
517 for action
in data
["actions"]:
519 if action
[0] == "vlan":
520 new_action
["type"] = "L2MODIFICATION"
522 if action
[1] is None:
523 new_action
["subtype"] = "VLAN_POP"
525 new_action
["subtype"] = "VLAN_ID"
526 new_action
["vlanId"] = int(action
[1])
527 elif action
[0] == "out":
528 new_action
["type"] = "OUTPUT"
530 if not action
[1] in self
.pp2ofi
:
532 "Port " + action
[1] + " is not present in the switch"
535 raise OpenflowConnUnexpectedResponse(error_msj
)
537 new_action
["port"] = self
.pp2ofi
[action
[1]]
539 error_msj
= "Unknown item '%s' in action list" % action
[0]
540 self
.logger
.error("new_flow " + error_msj
)
542 raise OpenflowConnUnexpectedResponse(error_msj
)
544 flow
["treatment"]["instructions"].append(new_action
)
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
)
553 error_text
= "Openflow response {}: {}".format(
554 of_response
.status_code
, of_response
.text
556 if of_response
.status_code
!= 201:
557 self
.logger
.warning("new_flow " + error_text
)
559 raise OpenflowConnUnexpectedResponse(error_text
)
561 flowId
= of_response
.headers
["location"][path
.__len
__() + 1 :]
562 data
["name"] = flowId
564 self
.logger
.debug("new_flow id: {},: {} ".format(flowId
, error_text
))
567 except requests
.exceptions
.RequestException
as e
:
568 error_text
= type(e
).__name
__ + ": " + str(e
)
569 self
.logger
.error("new_flow " + error_text
)
571 raise OpenflowConnConnectionException(error_text
)
573 def clear_all_flows(self
):
575 Delete all existing rules
576 :return: Raise a openflowconnUnexpectedResponse expection in case of failure
579 rules
= self
.get_of_rules(True)
584 self
.logger
.debug("clear_all_flows OK ")
587 except requests
.exceptions
.RequestException
as e
:
588 error_text
= type(e
).__name
__ + ": " + str(e
)
589 self
.logger
.error("clear_all_flows " + error_text
)
591 raise OpenflowConnConnectionException(error_text
)