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
,
41 # OpenflowConnException, OpenflowConnAuthException, OpenflowConnNotFoundException, \
42 # OpenflowConnConflictException, OpenflowConnNotSupportedException, OpenflowConnNotImplemented
44 __author__
= "Alaitz Mendiola"
45 __date__
= "$22-nov-2016$"
48 class OfConnOnos(OpenflowConn
):
50 ONOS connector. No MAC learning is used
53 def __init__(self
, params
):
55 :param params: dictionary with the following keys:
56 of_dpid: DPID to use for this controller ?? Does a controller have a dpid?
57 of_url: must be [http://HOST:PORT/]
58 of_user: user credentials, can be missing or None
59 of_password: password credentials
60 of_debug: debug level for logging. Default to ERROR
61 other keys are ignored
62 Raise an exception if same parameter is missing or wrong
64 OpenflowConn
.__init
__(self
, params
)
67 url
= params
.get("of_url")
70 raise ValueError("'url' must be provided")
72 if not url
.startswith("http"):
75 if not url
.endswith("/"):
78 self
.url
= url
+ "onos/v1/"
83 "content-type": "application/json",
84 "accept": "application/json",
88 self
.pp2ofi
= {} # From Physical Port to OpenFlow Index
89 self
.ofi2pp
= {} # From OpenFlow Index to Physical Port
91 self
.dpid
= str(params
["of_dpid"])
92 self
.id = "of:" + str(self
.dpid
.replace(":", ""))
94 # TODO This may not be straightforward
95 if params
.get("of_user"):
96 of_password
= params
.get("of_password", "")
97 self
.auth
= base64
.b64encode(
98 bytes(params
["of_user"] + ":" + of_password
, "utf-8")
100 self
.auth
= self
.auth
.decode()
101 self
.headers
["authorization"] = "Basic " + self
.auth
103 self
.logger
= logging
.getLogger("ro.sdn.onosof")
104 # self.logger.setLevel( getattr(logging, params.get("of_debug", "ERROR")) )
105 self
.logger
.debug("onosof plugin initialized")
106 self
.ip_address
= None
108 def get_of_switches(self
):
110 Obtain a a list of switches or DPID detected by this controller
111 :return: list where each element a tuple pair (DPID, IP address)
112 Raise a openflowconnUnexpectedResponse expection in case of failure
115 self
.headers
["content-type"] = "text/plain"
116 of_response
= requests
.get(self
.url
+ "devices", headers
=self
.headers
)
117 error_text
= "Openflow response %d: %s" % (
118 of_response
.status_code
,
122 if of_response
.status_code
!= 200:
123 self
.logger
.warning("get_of_switches " + error_text
)
125 raise OpenflowConnUnexpectedResponse(error_text
)
127 self
.logger
.debug("get_of_switches " + error_text
)
128 info
= of_response
.json()
130 if type(info
) != dict:
132 "get_of_switches. Unexpected response, not a dict: %s", str(info
)
135 raise OpenflowConnUnexpectedResponse(
136 "Unexpected response, not a dict. Wrong version?"
139 node_list
= info
.get("devices")
141 if type(node_list
) is not list:
143 "get_of_switches. Unexpected response, at 'devices', not found or not a list: %s",
144 str(type(node_list
)),
147 raise OpenflowConnUnexpectedResponse(
148 "Unexpected response, at 'devices', not found "
149 "or not a list. Wrong version?"
153 for node
in node_list
:
154 node_id
= node
.get("id")
157 "get_of_switches. Unexpected response at 'device':'id', not found: %s",
161 raise OpenflowConnUnexpectedResponse(
162 "Unexpected response at 'device':'id', "
163 "not found . Wrong version?"
166 node_ip_address
= node
.get("annotations").get("managementAddress")
167 if node_ip_address
is None:
169 "get_of_switches. Unexpected response at 'device':'managementAddress', not found: %s",
173 raise OpenflowConnUnexpectedResponse(
174 "Unexpected response at 'device':'managementAddress', not found. Wrong version?"
177 node_id_hex
= hex(int(node_id
.split(":")[1])).split("x")[1].zfill(16)
181 a
+ b
for a
, b
in zip(node_id_hex
[::2], node_id_hex
[1::2])
188 except requests
.exceptions
.RequestException
as e
:
189 error_text
= type(e
).__name
__ + ": " + str(e
)
190 self
.logger
.error("get_of_switches " + error_text
)
192 raise OpenflowConnConnectionException(error_text
)
193 except ValueError as e
:
194 # ValueError in the case that JSON can not be decoded
195 error_text
= type(e
).__name
__ + ": " + str(e
)
196 self
.logger
.error("get_of_switches " + error_text
)
198 raise OpenflowConnUnexpectedResponse(error_text
)
200 def obtain_port_correspondence(self
):
202 Obtain the correspondence between physical and openflow port names
203 :return: dictionary with physical name as key, openflow name as value
204 Raise a openflowconnUnexpectedResponse expection in case of failure
207 self
.headers
["content-type"] = "text/plain"
208 of_response
= requests
.get(
209 self
.url
+ "devices/" + self
.id + "/ports", headers
=self
.headers
211 error_text
= "Openflow response {}: {}".format(
212 of_response
.status_code
, of_response
.text
215 if of_response
.status_code
!= 200:
216 self
.logger
.warning("obtain_port_correspondence " + error_text
)
218 raise OpenflowConnUnexpectedResponse(error_text
)
220 self
.logger
.debug("obtain_port_correspondence " + error_text
)
221 info
= of_response
.json()
223 node_connector_list
= info
.get("ports")
224 if type(node_connector_list
) is not list:
226 "obtain_port_correspondence. Unexpected response at 'ports', not found or not a list: %s",
227 str(node_connector_list
),
230 raise OpenflowConnUnexpectedResponse(
231 "Unexpected response at 'ports', not found or not "
232 "a list. Wrong version?"
235 for node_connector
in node_connector_list
:
236 if node_connector
["port"] != "local":
237 self
.pp2ofi
[str(node_connector
["annotations"]["portName"])] = str(
238 node_connector
["port"]
240 self
.ofi2pp
[str(node_connector
["port"])] = str(
241 node_connector
["annotations"]["portName"]
244 node_ip_address
= info
["annotations"]["managementAddress"]
245 if node_ip_address
is None:
247 "obtain_port_correspondence. Unexpected response at 'managementAddress', not found: %s",
251 raise OpenflowConnUnexpectedResponse(
252 "Unexpected response at 'managementAddress', "
253 "not found. Wrong version?"
256 self
.ip_address
= node_ip_address
258 # print self.name, ": obtain_port_correspondence ports:", self.pp2ofi
260 except requests
.exceptions
.RequestException
as e
:
261 error_text
= type(e
).__name
__ + ": " + str(e
)
262 self
.logger
.error("obtain_port_correspondence " + error_text
)
264 raise OpenflowConnConnectionException(error_text
)
265 except ValueError as e
:
266 # ValueError in the case that JSON can not be decoded
267 error_text
= type(e
).__name
__ + ": " + str(e
)
268 self
.logger
.error("obtain_port_correspondence " + error_text
)
270 raise OpenflowConnUnexpectedResponse(error_text
)
272 def get_of_rules(self
, translate_of_ports
=True):
274 Obtain the rules inserted at openflow controller
275 :param translate_of_ports: if True it translates ports from openflow index to physical switch name
276 :return: list where each item is a dictionary with the following content:
277 priority: rule priority
278 name: rule name (present also as the master dict key)
279 ingress_port: match input port of the rule
280 dst_mac: match destination mac address of the rule, can be missing or None if not apply
281 vlan_id: match vlan tag of the rule, can be missing or None if not apply
282 actions: list of actions, composed by a pair tuples:
283 (vlan, None/int): for stripping/setting a vlan tag
284 (out, port): send to this port
286 Raise a openflowconnUnexpectedResponse exception in case of failure
289 if len(self
.ofi2pp
) == 0:
290 self
.obtain_port_correspondence()
293 self
.headers
["content-type"] = "text/plain"
294 of_response
= requests
.get(
295 self
.url
+ "flows/" + self
.id, headers
=self
.headers
297 error_text
= "Openflow response %d: %s" % (
298 of_response
.status_code
,
302 # The configured page does not exist if there are no rules installed. In that case we return an empty dict
303 if of_response
.status_code
== 404:
305 elif of_response
.status_code
!= 200:
306 self
.logger
.warning("get_of_rules " + error_text
)
308 raise OpenflowConnUnexpectedResponse(error_text
)
310 self
.logger
.debug("get_of_rules " + error_text
)
312 info
= of_response
.json()
314 if type(info
) != dict:
316 "get_of_rules. Unexpected response, not a dict: %s",
320 raise OpenflowConnUnexpectedResponse(
321 "Unexpected openflow response, not a dict. Wrong version?"
324 flow_list
= info
.get("flows")
326 if flow_list
is None:
329 if type(flow_list
) is not list:
331 "get_of_rules. Unexpected response at 'flows', not a list: %s",
332 str(type(flow_list
)),
335 raise OpenflowConnUnexpectedResponse(
336 "Unexpected response at 'flows', not a list. Wrong version?"
339 rules
= [] # Response list
340 for flow
in flow_list
:
343 and "selector" in flow
344 and "treatment" in flow
345 and "instructions" in flow
["treatment"]
346 and "criteria" in flow
["selector"]
348 raise OpenflowConnUnexpectedResponse(
349 "unexpected openflow response, one or more "
350 "elements are missing. Wrong version?"
354 rule
["switch"] = self
.dpid
355 rule
["priority"] = flow
.get("priority")
356 rule
["name"] = flow
["id"]
358 for criteria
in flow
["selector"]["criteria"]:
359 if criteria
["type"] == "IN_PORT":
360 in_port
= str(criteria
["port"])
361 if in_port
!= "CONTROLLER":
362 if in_port
not in self
.ofi2pp
:
363 raise OpenflowConnUnexpectedResponse(
364 "Error: Ingress port {} is not "
365 "in switch port list".format(in_port
)
368 if translate_of_ports
:
369 in_port
= self
.ofi2pp
[in_port
]
371 rule
["ingress_port"] = in_port
372 elif criteria
["type"] == "VLAN_VID":
373 rule
["vlan_id"] = criteria
["vlanId"]
374 elif criteria
["type"] == "ETH_DST":
375 rule
["dst_mac"] = str(criteria
["mac"]).lower()
378 for instruction
in flow
["treatment"]["instructions"]:
379 if instruction
["type"] == "OUTPUT":
380 out_port
= str(instruction
["port"])
381 if out_port
!= "CONTROLLER":
382 if out_port
not in self
.ofi2pp
:
383 raise OpenflowConnUnexpectedResponse(
384 "Error: Output port {} is not in "
385 "switch port list".format(out_port
)
388 if translate_of_ports
:
389 out_port
= self
.ofi2pp
[out_port
]
391 actions
.append(("out", out_port
))
394 instruction
["type"] == "L2MODIFICATION"
395 and instruction
["subtype"] == "VLAN_POP"
397 actions
.append(("vlan", "None"))
400 instruction
["type"] == "L2MODIFICATION"
401 and instruction
["subtype"] == "VLAN_ID"
403 actions
.append(("vlan", instruction
["vlanId"]))
405 rule
["actions"] = actions
409 except requests
.exceptions
.RequestException
as e
:
410 # ValueError in the case that JSON can not be decoded
411 error_text
= type(e
).__name
__ + ": " + str(e
)
412 self
.logger
.error("get_of_rules " + error_text
)
414 raise OpenflowConnConnectionException(error_text
)
415 except ValueError as e
:
416 # ValueError in the case that JSON can not be decoded
417 error_text
= type(e
).__name
__ + ": " + str(e
)
418 self
.logger
.error("get_of_rules " + error_text
)
420 raise OpenflowConnUnexpectedResponse(error_text
)
422 def del_flow(self
, flow_name
):
424 Delete an existing rule
426 :return: Raise a openflowconnUnexpectedResponse expection in case of failure
429 self
.logger
.debug("del_flow: delete flow name {}".format(flow_name
))
430 self
.headers
["content-type"] = None
431 of_response
= requests
.delete(
432 self
.url
+ "flows/" + self
.id + "/" + flow_name
, headers
=self
.headers
434 error_text
= "Openflow response {}: {}".format(
435 of_response
.status_code
, of_response
.text
438 if of_response
.status_code
!= 204:
439 self
.logger
.warning("del_flow " + error_text
)
441 raise OpenflowConnUnexpectedResponse(error_text
)
443 self
.logger
.debug("del_flow: {} OK,: {} ".format(flow_name
, error_text
))
446 except requests
.exceptions
.RequestException
as e
:
447 error_text
= type(e
).__name
__ + ": " + str(e
)
448 self
.logger
.error("del_flow " + error_text
)
450 raise OpenflowConnConnectionException(error_text
)
452 def new_flow(self
, data
):
454 Insert a new static rule
455 :param data: dictionary with the following content:
456 priority: rule priority
458 ingress_port: match input port of the rule
459 dst_mac: match destination mac address of the rule, missing or None if not apply
460 vlan_id: match vlan tag of the rule, missing or None if not apply
461 actions: list of actions, composed by a pair tuples with these posibilities:
462 ('vlan', None/int): for stripping/setting a vlan tag
463 ('out', port): send to this port
464 :return: Raise a openflowconnUnexpectedResponse exception in case of failure
467 self
.logger
.debug("new_flow data: {}".format(data
))
469 if len(self
.pp2ofi
) == 0:
470 self
.obtain_port_correspondence()
472 # Build the dictionary with the flow rule information for ONOS
474 # flow["id"] = data["name"]
476 flow
["priority"] = data
.get("priority")
478 flow
["isPermanent"] = "true"
479 flow
["appId"] = 10 # FIXME We should create an appId for OSM
480 flow
["selector"] = dict()
481 flow
["selector"]["criteria"] = list()
483 # Flow rule matching criteria
484 if not data
["ingress_port"] in self
.pp2ofi
:
487 + data
["ingress_port"]
488 + " is not present in the switch"
490 self
.logger
.warning("new_flow " + error_text
)
492 raise OpenflowConnUnexpectedResponse(error_text
)
494 ingress_port_criteria
= dict()
495 ingress_port_criteria
["type"] = "IN_PORT"
496 ingress_port_criteria
["port"] = self
.pp2ofi
[data
["ingress_port"]]
497 flow
["selector"]["criteria"].append(ingress_port_criteria
)
499 if "dst_mac" in data
:
500 dst_mac_criteria
= dict()
501 dst_mac_criteria
["type"] = "ETH_DST"
502 dst_mac_criteria
["mac"] = data
["dst_mac"]
503 flow
["selector"]["criteria"].append(dst_mac_criteria
)
505 if data
.get("vlan_id"):
506 vlan_criteria
= dict()
507 vlan_criteria
["type"] = "VLAN_VID"
508 vlan_criteria
["vlanId"] = int(data
["vlan_id"])
509 flow
["selector"]["criteria"].append(vlan_criteria
)
511 # Flow rule treatment
512 flow
["treatment"] = dict()
513 flow
["treatment"]["instructions"] = list()
514 flow
["treatment"]["deferred"] = list()
516 for action
in data
["actions"]:
518 if action
[0] == "vlan":
519 new_action
["type"] = "L2MODIFICATION"
521 if action
[1] is None:
522 new_action
["subtype"] = "VLAN_POP"
524 new_action
["subtype"] = "VLAN_ID"
525 new_action
["vlanId"] = int(action
[1])
526 elif action
[0] == "out":
527 new_action
["type"] = "OUTPUT"
529 if not action
[1] in self
.pp2ofi
:
531 "Port " + action
[1] + " is not present in the switch"
534 raise OpenflowConnUnexpectedResponse(error_msj
)
536 new_action
["port"] = self
.pp2ofi
[action
[1]]
538 error_msj
= "Unknown item '%s' in action list" % action
[0]
539 self
.logger
.error("new_flow " + error_msj
)
541 raise OpenflowConnUnexpectedResponse(error_msj
)
543 flow
["treatment"]["instructions"].append(new_action
)
545 self
.headers
["content-type"] = "application/json"
546 path
= self
.url
+ "flows/" + self
.id
547 self
.logger
.debug("new_flow post: {}".format(flow
))
548 of_response
= requests
.post(
549 path
, headers
=self
.headers
, data
=json
.dumps(flow
)
552 error_text
= "Openflow response {}: {}".format(
553 of_response
.status_code
, of_response
.text
555 if of_response
.status_code
!= 201:
556 self
.logger
.warning("new_flow " + error_text
)
558 raise OpenflowConnUnexpectedResponse(error_text
)
560 flowId
= of_response
.headers
["location"][path
.__len
__() + 1 :]
561 data
["name"] = flowId
563 self
.logger
.debug("new_flow id: {},: {} ".format(flowId
, error_text
))
566 except requests
.exceptions
.RequestException
as e
:
567 error_text
= type(e
).__name
__ + ": " + str(e
)
568 self
.logger
.error("new_flow " + error_text
)
570 raise OpenflowConnConnectionException(error_text
)
572 def clear_all_flows(self
):
574 Delete all existing rules
575 :return: Raise a openflowconnUnexpectedResponse expection in case of failure
578 rules
= self
.get_of_rules(True)
583 self
.logger
.debug("clear_all_flows OK ")
586 except requests
.exceptions
.RequestException
as e
:
587 error_text
= type(e
).__name
__ + ": " + str(e
)
588 self
.logger
.error("clear_all_flows " + error_text
)
590 raise OpenflowConnConnectionException(error_text
)