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
, OpenflowConnException
, OpenflowConnConnectionException
, \
36 OpenflowConnUnexpectedResponse
, 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("/"):
68 self
.url
= url
+ "onos/v1/"
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('openmano.sdnconn.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])), node_ip_address
))
147 except requests
.exceptions
.RequestException
as e
:
148 error_text
= type(e
).__name
__ + ": " + str(e
)
149 self
.logger
.error("get_of_switches " + error_text
)
150 raise OpenflowconnConnectionException(error_text
)
151 except ValueError as e
:
152 # ValueError in the case that JSON can not be decoded
153 error_text
= type(e
).__name
__ + ": " + str(e
)
154 self
.logger
.error("get_of_switches " + error_text
)
155 raise OpenflowconnUnexpectedResponse(error_text
)
157 def obtain_port_correspondence(self
):
159 Obtain the correspondence between physical and openflow port names
160 :return: dictionary: with physical name as key, openflow name as value,
161 Raise a OpenflowconnConnectionException expection in case of failure
164 of_response
= requests
.get(self
.url
+ "restconf/operational/opendaylight-inventory:nodes",
165 headers
=self
.headers
)
166 error_text
= "Openflow response {}: {}".format(of_response
.status_code
, of_response
.text
)
167 if of_response
.status_code
!= 200:
168 self
.logger
.warning("obtain_port_correspondence " + error_text
)
169 raise OpenflowconnUnexpectedResponse(error_text
)
170 self
.logger
.debug("obtain_port_correspondence " + error_text
)
171 info
= of_response
.json()
173 if not isinstance(info
, dict):
174 self
.logger
.error("obtain_port_correspondence. Unexpected response not a dict: %s", str(info
))
175 raise OpenflowconnUnexpectedResponse("Unexpected openflow response, not a dict. Wrong version?")
177 nodes
= info
.get('nodes')
178 if not isinstance(nodes
, dict):
179 self
.logger
.error("obtain_port_correspondence. Unexpected response at 'nodes', "
180 "not found or not a dict: %s", str(type(nodes
)))
181 raise OpenflowconnUnexpectedResponse("Unexpected response at 'nodes',not found or not a dict. "
184 node_list
= nodes
.get('node')
185 if not isinstance(node_list
, list):
186 self
.logger
.error("obtain_port_correspondence. Unexpected response, at 'nodes':'node', "
187 "not found or not a list: %s", str(type(node_list
)))
188 raise OpenflowconnUnexpectedResponse("Unexpected response, at 'nodes':'node', not found or not a list."
191 for node
in node_list
:
192 node_id
= node
.get('id')
194 self
.logger
.error("obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:'id', "
195 "not found: %s", str(node
))
196 raise OpenflowconnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:'id', not found. "
199 if node_id
== 'controller-config':
202 # Figure out if this is the appropriate switch. The 'id' is 'openflow:' plus the decimal value
204 # In case this is not the desired switch, continue
205 if self
.id != node_id
:
208 node_connector_list
= node
.get('node-connector')
209 if not isinstance(node_connector_list
, list):
210 self
.logger
.error("obtain_port_correspondence. Unexpected response at "
211 "'nodes':'node'[]:'node-connector', not found or not a list: %s", str(node
))
212 raise OpenflowconnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:'node-connector', "
213 "not found or not a list. Wrong version?")
215 for node_connector
in node_connector_list
:
216 self
.pp2ofi
[str(node_connector
['flow-node-inventory:name'])] = str(node_connector
['id'])
217 self
.ofi2pp
[node_connector
['id']] = str(node_connector
['flow-node-inventory:name'])
219 node_ip_address
= node
.get('flow-node-inventory:ip-address')
220 if node_ip_address
is None:
221 self
.logger
.error("obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:"
222 "'flow-node-inventory:ip-address', not found: %s", str(node
))
223 raise OpenflowconnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:"
224 "'flow-node-inventory:ip-address', not found. Wrong version?")
226 # If we found the appropriate dpid no need to continue in the for loop
229 # print self.name, ": obtain_port_correspondence ports:", self.pp2ofi
231 except requests
.exceptions
.RequestException
as e
:
232 error_text
= type(e
).__name
__ + ": " + str(e
)
233 self
.logger
.error("obtain_port_correspondence " + error_text
)
234 raise OpenflowconnConnectionException(error_text
)
235 except ValueError as e
:
236 # ValueError in the case that JSON can not be decoded
237 error_text
= type(e
).__name
__ + ": " + str(e
)
238 self
.logger
.error("obtain_port_correspondence " + error_text
)
239 raise OpenflowconnUnexpectedResponse(error_text
)
241 def get_of_rules(self
, translate_of_ports
=True):
243 Obtain the rules inserted at openflow controller
244 :param translate_of_ports:
245 :return: list where each item is a dictionary with the following content:
246 priority: rule priority
247 name: rule name (present also as the master dict key)
248 ingress_port: match input port of the rule
249 dst_mac: match destination mac address of the rule, can be missing or None if not apply
250 vlan_id: match vlan tag of the rule, can be missing or None if not apply
251 actions: list of actions, composed by a pair tuples:
252 (vlan, None/int): for stripping/setting a vlan tag
253 (out, port): send to this port
255 Raise a OpenflowconnConnectionException exception in case of failure
261 if len(self
.ofi2pp
) == 0:
262 self
.obtain_port_correspondence()
264 of_response
= requests
.get(self
.url
+ "restconf/config/opendaylight-inventory:nodes/node/" + self
.id +
265 "/table/0", headers
=self
.headers
)
266 error_text
= "Openflow response {}: {}".format(of_response
.status_code
, of_response
.text
)
268 # The configured page does not exist if there are no rules installed. In that case we return an empty dict
269 if of_response
.status_code
== 404:
272 elif of_response
.status_code
!= 200:
273 self
.logger
.warning("get_of_rules " + error_text
)
274 raise OpenflowconnUnexpectedResponse(error_text
)
276 self
.logger
.debug("get_of_rules " + error_text
)
278 info
= of_response
.json()
280 if not isinstance(info
, dict):
281 self
.logger
.error("get_of_rules. Unexpected response not a dict: %s", str(info
))
282 raise OpenflowconnUnexpectedResponse("Unexpected openflow response, not a dict. Wrong version?")
284 table
= info
.get('flow-node-inventory:table')
285 if not isinstance(table
, list):
286 self
.logger
.error("get_of_rules. Unexpected response at 'flow-node-inventory:table', "
287 "not a list: %s", str(type(table
)))
288 raise OpenflowconnUnexpectedResponse("Unexpected response at 'flow-node-inventory:table', not a list. "
291 flow_list
= table
[0].get('flow')
292 if flow_list
is None:
295 if not isinstance(flow_list
, list):
296 self
.logger
.error("get_of_rules. Unexpected response at 'flow-node-inventory:table'[0]:'flow', not a "
297 "list: %s", str(type(flow_list
)))
298 raise OpenflowconnUnexpectedResponse("Unexpected response at 'flow-node-inventory:table'[0]:'flow', "
299 "not a list. Wrong version?")
301 # TODO translate ports according to translate_of_ports parameter
303 rules
= [] # Response list
304 for flow
in flow_list
:
305 if not ('id' in flow
and 'match' in flow
and 'instructions' in flow
and
306 'instruction' in flow
['instructions'] and
307 'apply-actions' in flow
['instructions']['instruction'][0] and
308 'action' in flow
['instructions']['instruction'][0]['apply-actions']):
309 raise OpenflowconnUnexpectedResponse("unexpected openflow response, one or more elements are "
310 "missing. Wrong version?")
312 flow
['instructions']['instruction'][0]['apply-actions']['action']
315 rule
['switch'] = self
.dpid
316 rule
['priority'] = flow
.get('priority')
317 # rule['name'] = flow['id']
318 # rule['cookie'] = flow['cookie']
319 if 'in-port' in flow
['match']:
320 in_port
= flow
['match']['in-port']
321 if in_port
not in self
.ofi2pp
:
322 raise OpenflowconnUnexpectedResponse("Error: Ingress port {} is not in switch port list".
325 if translate_of_ports
:
326 in_port
= self
.ofi2pp
[in_port
]
328 rule
['ingress_port'] = in_port
330 if 'vlan-match' in flow
['match'] and 'vlan-id' in flow
['match']['vlan-match'] and \
331 'vlan-id' in flow
['match']['vlan-match']['vlan-id'] and \
332 'vlan-id-present' in flow
['match']['vlan-match']['vlan-id'] and \
333 flow
['match']['vlan-match']['vlan-id']['vlan-id-present'] == True:
334 rule
['vlan_id'] = flow
['match']['vlan-match']['vlan-id']['vlan-id']
336 if 'ethernet-match' in flow
['match'] and 'ethernet-destination' in flow
['match']['ethernet-match'] \
337 and 'address' in flow
['match']['ethernet-match']['ethernet-destination']:
338 rule
['dst_mac'] = flow
['match']['ethernet-match']['ethernet-destination']['address']
340 instructions
= flow
['instructions']['instruction'][0]['apply-actions']['action']
343 for instruction
in instructions
:
344 if instruction
['order'] > max_index
:
345 max_index
= instruction
['order']
347 actions
= [None]*(max_index
+1)
348 for instruction
in instructions
:
349 if 'output-action' in instruction
:
350 if 'output-node-connector' not in instruction
['output-action']:
351 raise OpenflowconnUnexpectedResponse("unexpected openflow response, one or more elementa "
352 "are missing. Wrong version?")
354 out_port
= instruction
['output-action']['output-node-connector']
355 if out_port
not in self
.ofi2pp
:
356 raise OpenflowconnUnexpectedResponse("Error: Output port {} is not in switch port list".
359 if translate_of_ports
:
360 out_port
= self
.ofi2pp
[out_port
]
362 actions
[instruction
['order']] = ('out', out_port
)
364 elif 'strip-vlan-action' in instruction
:
365 actions
[instruction
['order']] = ('vlan', None)
367 elif 'set-field' in instruction
:
368 if not ('vlan-match' in instruction
['set-field'] and
369 'vlan-id' in instruction
['set-field']['vlan-match'] and
370 'vlan-id' in instruction
['set-field']['vlan-match']['vlan-id']):
371 raise OpenflowconnUnexpectedResponse("unexpected openflow response, one or more elements "
372 "are missing. Wrong version?")
374 actions
[instruction
['order']] = ('vlan',
375 instruction
['set-field']['vlan-match']['vlan-id']['vlan-id'])
377 actions
= [x
for x
in actions
if x
is not None]
379 rule
['actions'] = list(actions
)
383 except requests
.exceptions
.RequestException
as e
:
384 error_text
= type(e
).__name
__ + ": " + str(e
)
385 self
.logger
.error("get_of_rules " + error_text
)
386 raise OpenflowconnConnectionException(error_text
)
387 except ValueError as e
:
388 # ValueError in the case that JSON can not be decoded
389 error_text
= type(e
).__name
__ + ": " + str(e
)
390 self
.logger
.error("get_of_rules " + error_text
)
391 raise OpenflowconnUnexpectedResponse(error_text
)
393 def del_flow(self
, flow_name
):
395 Delete an existing rule
396 :param flow_name: flow_name, this is the rule name
397 :return: Raise a OpenflowconnConnectionException expection in case of failure
401 of_response
= requests
.delete(self
.url
+ "restconf/config/opendaylight-inventory:nodes/node/" + self
.id +
402 "/table/0/flow/" + flow_name
, headers
=self
.headers
)
403 error_text
= "Openflow response {}: {}".format(of_response
.status_code
, of_response
.text
)
404 if of_response
.status_code
!= 200:
405 self
.logger
.warning("del_flow " + error_text
)
406 raise OpenflowconnUnexpectedResponse(error_text
)
407 self
.logger
.debug("del_flow OK " + error_text
)
409 except requests
.exceptions
.RequestException
as e
:
410 # raise an exception in case of contection error
411 error_text
= type(e
).__name
__ + ": " + str(e
)
412 self
.logger
.error("del_flow " + error_text
)
413 raise OpenflowconnConnectionException(error_text
)
415 def new_flow(self
, data
):
417 Insert a new static rule
418 :param data: dictionary with the following content:
419 priority: rule priority
421 ingress_port: match input port of the rule
422 dst_mac: match destination mac address of the rule, missing or None if not apply
423 vlan_id: match vlan tag of the rule, missing or None if not apply
424 actions: list of actions, composed by a pair tuples with these posibilities:
425 ('vlan', None/int): for stripping/setting a vlan tag
426 ('out', port): send to this port
427 :return: Raise a OpenflowconnConnectionException exception in case of failure
431 self
.logger
.debug("new_flow data: {}".format(data
))
432 if len(self
.pp2ofi
) == 0:
433 self
.obtain_port_correspondence()
435 # We have to build the data for the opendaylight call from the generic data
438 'flow-name': data
['name'],
442 'priority': data
.get('priority'),
445 sdata
= {'flow-node-inventory:flow': [flow
]}
446 if not data
['ingress_port'] in self
.pp2ofi
:
447 error_text
= 'Error. Port ' + data
['ingress_port'] + ' is not present in the switch'
448 self
.logger
.warning("new_flow " + error_text
)
449 raise OpenflowconnUnexpectedResponse(error_text
)
450 flow
['match']['in-port'] = self
.pp2ofi
[data
['ingress_port']]
451 if data
.get('dst_mac'):
452 flow
['match']['ethernet-match'] = {
453 'ethernet-destination': {'address': data
['dst_mac']}
455 if data
.get('vlan_id'):
456 flow
['match']['vlan-match'] = {
458 'vlan-id-present': True,
459 'vlan-id': int(data
['vlan_id'])
463 flow
['instructions'] = {
466 'apply-actions': {'action': actions
}
471 for action
in data
['actions']:
472 new_action
= {'order': order
}
473 if action
[0] == "vlan":
474 if action
[1] is None:
476 new_action
['strip-vlan-action'] = {}
478 new_action
['set-field'] = {
481 'vlan-id-present': True,
482 'vlan-id': int(action
[1])
486 elif action
[0] == 'out':
487 new_action
['output-action'] = {}
488 if not action
[1] in self
.pp2ofi
:
489 error_msg
= 'Port ' + action
[1] + ' is not present in the switch'
490 raise OpenflowconnUnexpectedResponse(error_msg
)
492 new_action
['output-action']['output-node-connector'] = self
.pp2ofi
[action
[1]]
494 error_msg
= "Unknown item '%s' in action list".format(action
[0])
495 self
.logger
.error("new_flow " + error_msg
)
496 raise OpenflowconnUnexpectedResponse(error_msg
)
498 actions
.append(new_action
)
501 # print json.dumps(sdata)
502 of_response
= requests
.put(self
.url
+ "restconf/config/opendaylight-inventory:nodes/node/" + self
.id +
503 "/table/0/flow/" + data
['name'], headers
=self
.headers
, data
=json
.dumps(sdata
))
504 error_text
= "Openflow response {}: {}".format(of_response
.status_code
, of_response
.text
)
505 if of_response
.status_code
!= 200:
506 self
.logger
.warning("new_flow " + error_text
)
507 raise OpenflowconnUnexpectedResponse(error_text
)
508 self
.logger
.debug("new_flow OK " + error_text
)
511 except requests
.exceptions
.RequestException
as e
:
512 # raise an exception in case of contection error
513 error_text
= type(e
).__name
__ + ": " + str(e
)
514 self
.logger
.error("new_flow " + error_text
)
515 raise OpenflowconnConnectionException(error_text
)
517 def clear_all_flows(self
):
519 Delete all existing rules
520 :return: Raise a OpenflowconnConnectionException expection in case of failure
523 of_response
= requests
.delete(self
.url
+ "restconf/config/opendaylight-inventory:nodes/node/" + self
.id +
524 "/table/0", headers
=self
.headers
)
525 error_text
= "Openflow response {}: {}".format(of_response
.status_code
, of_response
.text
)
526 if of_response
.status_code
!= 200 and of_response
.status_code
!= 404: # HTTP_Not_Found
527 self
.logger
.warning("clear_all_flows " + error_text
)
528 raise OpenflowconnUnexpectedResponse(error_text
)
529 self
.logger
.debug("clear_all_flows OK " + error_text
)
531 except requests
.exceptions
.RequestException
as e
:
532 error_text
= type(e
).__name
__ + ": " + str(e
)
533 self
.logger
.error("clear_all_flows " + error_text
)
534 raise OpenflowconnConnectionException(error_text
)