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 OpenflowConn
, OpenflowConnConnectionException
, OpenflowConnUnexpectedResponse
36 # OpenflowConnException, OpenflowConnAuthException, OpenflowConnNotFoundException,
37 # OpenflowConnConflictException, OpenflowConnNotSupportedException, OpenflowConnNotImplemented
39 __author__
= "Pablo Montes, Alfonso Tierno"
40 __date__
= "$28-oct-2014 12:07:15$"
43 class OfConnOdl(OpenflowConn
):
44 """OpenDayLight connector. No MAC learning is used"""
46 def __init__(self
, params
):
48 Params: dictionary with the following keys:
49 of_dpid: DPID to use for this controller
50 of_url: must be [http://HOST:PORT/]
51 of_user: user credentials, can be missing or None
52 of_password: password credentials
53 of_debug: debug level for logging. Default to ERROR
54 other keys are ignored
55 Raise an exception if same parameter is missing or wrong
58 OpenflowConn
.__init
__(self
, params
)
61 url
= params
.get("of_url")
63 raise ValueError("'url' must be provided")
64 if not url
.startswith("http"):
66 if not url
.endswith("/"):
71 self
.name
= "OpenDayLight"
72 self
.headers
= {'content-type': 'application/json', 'Accept': 'application/json'}
74 self
.pp2ofi
= {} # From Physical Port to OpenFlow Index
75 self
.ofi2pp
= {} # From OpenFlow Index to Physical Port
77 self
.dpid
= str(params
["of_dpid"])
78 self
.id = 'openflow:'+str(int(self
.dpid
.replace(':', ''), 16))
79 if params
and params
.get("of_user"):
80 of_password
= params
.get("of_password", "")
81 self
.auth
= base64
.b64encode(bytes(params
["of_user"] + ":" + of_password
, "utf-8"))
82 self
.auth
= self
.auth
.decode()
83 self
.headers
['authorization'] = 'Basic ' + self
.auth
85 self
.logger
= logging
.getLogger('ro.sdn.onosof')
86 # self.logger.setLevel(getattr(logging, params.get("of_debug", "ERROR")))
87 self
.logger
.debug("odlof plugin initialized")
89 def get_of_switches(self
):
91 Obtain a a list of switches or DPID detected by this controller
92 :return: list length, and a list where each element a tuple pair (DPID, IP address)
93 Raise an OpenflowConnConnectionException exception if fails with text_error
96 of_response
= requests
.get(self
.url
+ "restconf/operational/opendaylight-inventory:nodes",
98 error_text
= "Openflow response {}: {}".format(of_response
.status_code
, of_response
.text
)
99 if of_response
.status_code
!= 200:
100 self
.logger
.warning("get_of_switches " + error_text
)
101 raise OpenflowConnUnexpectedResponse("Error get_of_switches " + error_text
)
103 self
.logger
.debug("get_of_switches " + error_text
)
104 info
= of_response
.json()
106 if not isinstance(info
, dict):
107 self
.logger
.error("get_of_switches. Unexpected response, not a dict: %s", str(info
))
108 raise OpenflowConnUnexpectedResponse("Unexpected response, not a dict. Wrong version?")
110 nodes
= info
.get('nodes')
111 if type(nodes
) is not dict:
112 self
.logger
.error("get_of_switches. Unexpected response at 'nodes', not found or not a dict: %s",
114 raise OpenflowConnUnexpectedResponse("Unexpected response at 'nodes', not found or not a dict."
117 node_list
= nodes
.get('node')
118 if type(node_list
) is not list:
119 self
.logger
.error("get_of_switches. Unexpected response, at 'nodes':'node', "
120 "not found or not a list: %s", str(type(node_list
)))
121 raise OpenflowConnUnexpectedResponse("Unexpected response, at 'nodes':'node', not found "
122 "or not a list. Wrong version?")
125 for node
in node_list
:
126 node_id
= node
.get('id')
128 self
.logger
.error("get_of_switches. Unexpected response at 'nodes':'node'[]:'id', not found: %s",
130 raise OpenflowConnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:'id', not found. "
133 if node_id
== 'controller-config':
136 node_ip_address
= node
.get('flow-node-inventory:ip-address')
137 if node_ip_address
is None:
138 self
.logger
.error("get_of_switches. Unexpected response at 'nodes':'node'[]:'flow-node-inventory:"
139 "ip-address', not found: %s", str(node
))
140 raise OpenflowConnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:"
141 "'flow-node-inventory:ip-address', not found. Wrong version?")
143 node_id_hex
= hex(int(node_id
.split(':')[1])).split('x')[1].zfill(16)
144 switch_list
.append((':'.join(a
+b
for a
, b
in zip(node_id_hex
[::2], node_id_hex
[1::2])),
148 except requests
.exceptions
.RequestException
as e
:
149 error_text
= type(e
).__name
__ + ": " + str(e
)
150 self
.logger
.error("get_of_switches " + error_text
)
151 raise OpenflowConnConnectionException(error_text
)
152 except ValueError as e
:
153 # ValueError in the case that JSON can not be decoded
154 error_text
= type(e
).__name
__ + ": " + str(e
)
155 self
.logger
.error("get_of_switches " + error_text
)
156 raise OpenflowConnUnexpectedResponse(error_text
)
158 def obtain_port_correspondence(self
):
160 Obtain the correspondence between physical and openflow port names
161 :return: dictionary: with physical name as key, openflow name as value,
162 Raise a OpenflowConnConnectionException expection in case of failure
165 of_response
= requests
.get(self
.url
+ "restconf/operational/opendaylight-inventory:nodes",
166 headers
=self
.headers
)
167 error_text
= "Openflow response {}: {}".format(of_response
.status_code
, of_response
.text
)
168 if of_response
.status_code
!= 200:
169 self
.logger
.warning("obtain_port_correspondence " + error_text
)
170 raise OpenflowConnUnexpectedResponse(error_text
)
171 self
.logger
.debug("obtain_port_correspondence " + error_text
)
172 info
= of_response
.json()
174 if not isinstance(info
, dict):
175 self
.logger
.error("obtain_port_correspondence. Unexpected response not a dict: %s", str(info
))
176 raise OpenflowConnUnexpectedResponse("Unexpected openflow response, not a dict. Wrong version?")
178 nodes
= info
.get('nodes')
179 if not isinstance(nodes
, dict):
180 self
.logger
.error("obtain_port_correspondence. Unexpected response at 'nodes', "
181 "not found or not a dict: %s", str(type(nodes
)))
182 raise OpenflowConnUnexpectedResponse("Unexpected response at 'nodes',not found or not a dict. "
185 node_list
= nodes
.get('node')
186 if not isinstance(node_list
, list):
187 self
.logger
.error("obtain_port_correspondence. Unexpected response, at 'nodes':'node', "
188 "not found or not a list: %s", str(type(node_list
)))
189 raise OpenflowConnUnexpectedResponse("Unexpected response, at 'nodes':'node', not found or not a list."
192 for node
in node_list
:
193 node_id
= node
.get('id')
195 self
.logger
.error("obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:'id', "
196 "not found: %s", str(node
))
197 raise OpenflowConnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:'id', not found. "
200 if node_id
== 'controller-config':
203 # Figure out if this is the appropriate switch. The 'id' is 'openflow:' plus the decimal value
205 # In case this is not the desired switch, continue
206 if self
.id != node_id
:
209 node_connector_list
= node
.get('node-connector')
210 if not isinstance(node_connector_list
, list):
211 self
.logger
.error("obtain_port_correspondence. Unexpected response at "
212 "'nodes':'node'[]:'node-connector', not found or not a list: %s", str(node
))
213 raise OpenflowConnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:'node-connector', "
214 "not found or not a list. Wrong version?")
216 for node_connector
in node_connector_list
:
217 self
.pp2ofi
[str(node_connector
['flow-node-inventory:name'])] = str(node_connector
['id'])
218 self
.ofi2pp
[node_connector
['id']] = str(node_connector
['flow-node-inventory:name'])
220 node_ip_address
= node
.get('flow-node-inventory:ip-address')
221 if node_ip_address
is None:
222 self
.logger
.error("obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:"
223 "'flow-node-inventory:ip-address', not found: %s", str(node
))
224 raise OpenflowConnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:"
225 "'flow-node-inventory:ip-address', not found. Wrong version?")
227 # If we found the appropriate dpid no need to continue in the for loop
230 # print self.name, ": obtain_port_correspondence ports:", self.pp2ofi
232 except requests
.exceptions
.RequestException
as e
:
233 error_text
= type(e
).__name
__ + ": " + str(e
)
234 self
.logger
.error("obtain_port_correspondence " + error_text
)
235 raise OpenflowConnConnectionException(error_text
)
236 except ValueError as e
:
237 # ValueError in the case that JSON can not be decoded
238 error_text
= type(e
).__name
__ + ": " + str(e
)
239 self
.logger
.error("obtain_port_correspondence " + error_text
)
240 raise OpenflowConnUnexpectedResponse(error_text
)
242 def get_of_rules(self
, translate_of_ports
=True):
244 Obtain the rules inserted at openflow controller
245 :param translate_of_ports:
246 :return: list where each item is a dictionary with the following content:
247 priority: rule priority
248 name: rule name (present also as the master dict key)
249 ingress_port: match input port of the rule
250 dst_mac: match destination mac address of the rule, can be missing or None if not apply
251 vlan_id: match vlan tag of the rule, can be missing or None if not apply
252 actions: list of actions, composed by a pair tuples:
253 (vlan, None/int): for stripping/setting a vlan tag
254 (out, port): send to this port
256 Raise a OpenflowConnConnectionException exception in case of failure
262 if len(self
.ofi2pp
) == 0:
263 self
.obtain_port_correspondence()
265 of_response
= requests
.get(self
.url
+ "restconf/config/opendaylight-inventory:nodes/node/" + self
.id +
266 "/table/0", headers
=self
.headers
)
267 error_text
= "Openflow response {}: {}".format(of_response
.status_code
, of_response
.text
)
269 # The configured page does not exist if there are no rules installed. In that case we return an empty dict
270 if of_response
.status_code
== 404:
273 elif of_response
.status_code
!= 200:
274 self
.logger
.warning("get_of_rules " + error_text
)
275 raise OpenflowConnUnexpectedResponse(error_text
)
277 self
.logger
.debug("get_of_rules " + error_text
)
279 info
= of_response
.json()
281 if not isinstance(info
, dict):
282 self
.logger
.error("get_of_rules. Unexpected response not a dict: %s", str(info
))
283 raise OpenflowConnUnexpectedResponse("Unexpected openflow response, not a dict. Wrong version?")
285 table
= info
.get('flow-node-inventory:table')
286 if not isinstance(table
, list):
287 self
.logger
.error("get_of_rules. Unexpected response at 'flow-node-inventory:table', "
288 "not a list: %s", str(type(table
)))
289 raise OpenflowConnUnexpectedResponse("Unexpected response at 'flow-node-inventory:table', not a list. "
292 flow_list
= table
[0].get('flow')
293 if flow_list
is None:
296 if not isinstance(flow_list
, list):
297 self
.logger
.error("get_of_rules. Unexpected response at 'flow-node-inventory:table'[0]:'flow', not a "
298 "list: %s", str(type(flow_list
)))
299 raise OpenflowConnUnexpectedResponse("Unexpected response at 'flow-node-inventory:table'[0]:'flow', "
300 "not a list. Wrong version?")
302 # TODO translate ports according to translate_of_ports parameter
304 rules
= [] # Response list
305 for flow
in flow_list
:
306 if not ('id' in flow
and 'match' in flow
and 'instructions' in flow
and
307 'instruction' in flow
['instructions'] and
308 'apply-actions' in flow
['instructions']['instruction'][0] and
309 'action' in flow
['instructions']['instruction'][0]['apply-actions']):
310 raise OpenflowConnUnexpectedResponse("unexpected openflow response, one or more elements are "
311 "missing. Wrong version?")
313 flow
['instructions']['instruction'][0]['apply-actions']['action']
316 rule
['switch'] = self
.dpid
317 rule
['priority'] = flow
.get('priority')
318 # rule['name'] = flow['id']
319 # rule['cookie'] = flow['cookie']
320 if 'in-port' in flow
['match']:
321 in_port
= flow
['match']['in-port']
322 if in_port
not in self
.ofi2pp
:
323 raise OpenflowConnUnexpectedResponse("Error: Ingress port {} is not in switch port list".
326 if translate_of_ports
:
327 in_port
= self
.ofi2pp
[in_port
]
329 rule
['ingress_port'] = in_port
331 if 'vlan-match' in flow
['match'] and 'vlan-id' in flow
['match']['vlan-match'] and \
332 'vlan-id' in flow
['match']['vlan-match']['vlan-id'] and \
333 'vlan-id-present' in flow
['match']['vlan-match']['vlan-id'] and \
334 flow
['match']['vlan-match']['vlan-id']['vlan-id-present'] is True:
335 rule
['vlan_id'] = flow
['match']['vlan-match']['vlan-id']['vlan-id']
337 if 'ethernet-match' in flow
['match'] and 'ethernet-destination' in flow
['match']['ethernet-match'] \
338 and 'address' in flow
['match']['ethernet-match']['ethernet-destination']:
339 rule
['dst_mac'] = flow
['match']['ethernet-match']['ethernet-destination']['address']
341 instructions
= flow
['instructions']['instruction'][0]['apply-actions']['action']
344 for instruction
in instructions
:
345 if instruction
['order'] > max_index
:
346 max_index
= instruction
['order']
348 actions
= [None]*(max_index
+1)
349 for instruction
in instructions
:
350 if 'output-action' in instruction
:
351 if 'output-node-connector' not in instruction
['output-action']:
352 raise OpenflowConnUnexpectedResponse("unexpected openflow response, one or more elementa "
353 "are missing. Wrong version?")
355 out_port
= instruction
['output-action']['output-node-connector']
356 if out_port
not in self
.ofi2pp
:
357 raise OpenflowConnUnexpectedResponse("Error: Output port {} is not in switch port list".
360 if translate_of_ports
:
361 out_port
= self
.ofi2pp
[out_port
]
363 actions
[instruction
['order']] = ('out', out_port
)
365 elif 'strip-vlan-action' in instruction
:
366 actions
[instruction
['order']] = ('vlan', None)
368 elif 'set-field' in instruction
:
369 if not ('vlan-match' in instruction
['set-field'] and
370 'vlan-id' in instruction
['set-field']['vlan-match'] and
371 'vlan-id' in instruction
['set-field']['vlan-match']['vlan-id']):
372 raise OpenflowConnUnexpectedResponse("unexpected openflow response, one or more elements "
373 "are missing. Wrong version?")
375 actions
[instruction
['order']] = ('vlan',
376 instruction
['set-field']['vlan-match']['vlan-id']['vlan-id'])
378 actions
= [x
for x
in actions
if x
is not None]
380 rule
['actions'] = list(actions
)
384 except requests
.exceptions
.RequestException
as e
:
385 error_text
= type(e
).__name
__ + ": " + str(e
)
386 self
.logger
.error("get_of_rules " + error_text
)
387 raise OpenflowConnConnectionException(error_text
)
388 except ValueError as e
:
389 # ValueError in the case that JSON can not be decoded
390 error_text
= type(e
).__name
__ + ": " + str(e
)
391 self
.logger
.error("get_of_rules " + error_text
)
392 raise OpenflowConnUnexpectedResponse(error_text
)
394 def del_flow(self
, flow_name
):
396 Delete an existing rule
397 :param flow_name: flow_name, this is the rule name
398 :return: Raise a OpenflowConnConnectionException expection in case of failure
402 of_response
= requests
.delete(self
.url
+ "restconf/config/opendaylight-inventory:nodes/node/" + self
.id +
403 "/table/0/flow/" + flow_name
, headers
=self
.headers
)
404 error_text
= "Openflow response {}: {}".format(of_response
.status_code
, of_response
.text
)
405 if of_response
.status_code
!= 200:
406 self
.logger
.warning("del_flow " + error_text
)
407 raise OpenflowConnUnexpectedResponse(error_text
)
408 self
.logger
.debug("del_flow OK " + error_text
)
410 except requests
.exceptions
.RequestException
as e
:
411 # raise an exception in case of contection error
412 error_text
= type(e
).__name
__ + ": " + str(e
)
413 self
.logger
.error("del_flow " + error_text
)
414 raise OpenflowConnConnectionException(error_text
)
416 def new_flow(self
, data
):
418 Insert a new static rule
419 :param data: dictionary with the following content:
420 priority: rule priority
422 ingress_port: match input port of the rule
423 dst_mac: match destination mac address of the rule, missing or None if not apply
424 vlan_id: match vlan tag of the rule, missing or None if not apply
425 actions: list of actions, composed by a pair tuples with these posibilities:
426 ('vlan', None/int): for stripping/setting a vlan tag
427 ('out', port): send to this port
428 :return: Raise a OpenflowConnConnectionException exception in case of failure
432 self
.logger
.debug("new_flow data: {}".format(data
))
433 if len(self
.pp2ofi
) == 0:
434 self
.obtain_port_correspondence()
436 # We have to build the data for the opendaylight call from the generic data
439 'flow-name': data
['name'],
443 'priority': data
.get('priority'),
446 sdata
= {'flow-node-inventory:flow': [flow
]}
447 if not data
['ingress_port'] in self
.pp2ofi
:
448 error_text
= 'Error. Port ' + data
['ingress_port'] + ' is not present in the switch'
449 self
.logger
.warning("new_flow " + error_text
)
450 raise OpenflowConnUnexpectedResponse(error_text
)
451 flow
['match']['in-port'] = self
.pp2ofi
[data
['ingress_port']]
452 if data
.get('dst_mac'):
453 flow
['match']['ethernet-match'] = {
454 'ethernet-destination': {'address': data
['dst_mac']}
456 if data
.get('vlan_id'):
457 flow
['match']['vlan-match'] = {
459 'vlan-id-present': True,
460 'vlan-id': int(data
['vlan_id'])
464 flow
['instructions'] = {
467 'apply-actions': {'action': actions
}
472 for action
in data
['actions']:
473 new_action
= {'order': order
}
474 if action
[0] == "vlan":
475 if action
[1] is None:
477 new_action
['strip-vlan-action'] = {}
479 new_action
['set-field'] = {
482 'vlan-id-present': True,
483 'vlan-id': int(action
[1])
487 elif action
[0] == 'out':
488 new_action
['output-action'] = {}
489 if not action
[1] in self
.pp2ofi
:
490 error_msg
= 'Port ' + action
[1] + ' is not present in the switch'
491 raise OpenflowConnUnexpectedResponse(error_msg
)
493 new_action
['output-action']['output-node-connector'] = self
.pp2ofi
[action
[1]]
495 error_msg
= "Unknown item '{}' in action list".format(action
[0])
496 self
.logger
.error("new_flow " + error_msg
)
497 raise OpenflowConnUnexpectedResponse(error_msg
)
499 actions
.append(new_action
)
502 # print json.dumps(sdata)
503 of_response
= requests
.put(self
.url
+ "restconf/config/opendaylight-inventory:nodes/node/" + self
.id +
504 "/table/0/flow/" + data
['name'], headers
=self
.headers
, data
=json
.dumps(sdata
))
505 error_text
= "Openflow response {}: {}".format(of_response
.status_code
, of_response
.text
)
506 if of_response
.status_code
!= 200:
507 self
.logger
.warning("new_flow " + error_text
)
508 raise OpenflowConnUnexpectedResponse(error_text
)
509 self
.logger
.debug("new_flow OK " + error_text
)
512 except requests
.exceptions
.RequestException
as e
:
513 # raise an exception in case of contection error
514 error_text
= type(e
).__name
__ + ": " + str(e
)
515 self
.logger
.error("new_flow " + error_text
)
516 raise OpenflowConnConnectionException(error_text
)
518 def clear_all_flows(self
):
520 Delete all existing rules
521 :return: Raise a OpenflowConnConnectionException expection in case of failure
524 of_response
= requests
.delete(self
.url
+ "restconf/config/opendaylight-inventory:nodes/node/" + self
.id +
525 "/table/0", headers
=self
.headers
)
526 error_text
= "Openflow response {}: {}".format(of_response
.status_code
, of_response
.text
)
527 if of_response
.status_code
!= 200 and of_response
.status_code
!= 404: # HTTP_Not_Found
528 self
.logger
.warning("clear_all_flows " + error_text
)
529 raise OpenflowConnUnexpectedResponse(error_text
)
530 self
.logger
.debug("clear_all_flows OK " + error_text
)
532 except requests
.exceptions
.RequestException
as e
:
533 error_text
= type(e
).__name
__ + ": " + str(e
)
534 self
.logger
.error("clear_all_flows " + error_text
)
535 raise OpenflowConnConnectionException(error_text
)