2 # -*- coding: utf-8 -*-
5 # Copyright 2015 Telefónica Investigación 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
31 __author__
="Pablo Montes, Alfonso Tierno"
32 __date__
="$28-oct-2014 12:07:15$"
41 '''OpenDayLight connector. No MAC learning is used'''
42 def __init__(self
, params
):
44 Params: dictionary with the following keys:
45 of_dpid: DPID to use for this controller
46 of_ip: controller IP address
47 of_port: controller TCP port
48 of_user: user credentials, can be missing or None
49 of_password: password credentials
50 of_debug: debug level for logging. Default to ERROR
51 other keys are ignored
52 Raise an exception if same parameter is missing or wrong
55 if "of_ip" not in params
or params
["of_ip"]==None or "of_port" not in params
or params
["of_port"]==None:
56 raise ValueError("IP address and port must be provided")
58 self
.name
= "OpenDayLight"
59 self
.headers
= {'content-type':'application/json',
60 'Accept':'application/json'
63 self
.pp2ofi
={} # From Physical Port to OpenFlow Index
64 self
.ofi2pp
={} # From OpenFlow Index to Physical Port
66 self
.dpid
= str(params
["of_dpid"])
67 self
.id = 'openflow:'+str(int(self
.dpid
.replace(':', ''), 16))
68 self
.url
= "http://%s:%s" %( str(params
["of_ip"]), str(params
["of_port"] ) )
69 if "of_user" in params
and params
["of_user"]!=None:
70 if not params
.get("of_password"):
73 of_password
=str(params
["of_password"])
74 self
.auth
= base64
.b64encode(str(params
["of_user"])+":"+of_password
)
75 self
.headers
['Authorization'] = 'Basic '+self
.auth
78 self
.logger
= logging
.getLogger('vim.OF.ODL')
79 self
.logger
.setLevel( getattr(logging
, params
.get("of_debug", "ERROR")) )
81 def get_of_switches(self
):
82 ''' Obtain a a list of switches or DPID detected by this controller
84 >=0, list: list length, and a list where each element a tuple pair (DPID, IP address)
85 <0, text_error: if fails
88 of_response
= requests
.get(self
.url
+"/restconf/operational/opendaylight-inventory:nodes",
90 error_text
= "Openflow response %d: %s" % (of_response
.status_code
, of_response
.text
)
91 if of_response
.status_code
!= 200:
92 self
.logger
.warning("get_of_switches " + error_text
)
93 return -1 , error_text
94 self
.logger
.debug("get_of_switches " + error_text
)
95 info
= of_response
.json()
97 if type(info
) != dict:
98 self
.logger
.error("get_of_switches. Unexpected response, not a dict: %s", str(info
))
99 return -1, "Unexpected response, not a dict. Wrong version?"
101 nodes
= info
.get('nodes')
102 if type(nodes
) is not dict:
103 self
.logger
.error("get_of_switches. Unexpected response at 'nodes', not found or not a dict: %s", str(type(info
)))
104 return -1, "Unexpected response at 'nodes', not found or not a dict. Wrong version?"
106 node_list
= nodes
.get('node')
107 if type(node_list
) is not list:
108 self
.logger
.error("get_of_switches. Unexpected response, at 'nodes':'node', not found or not a list: %s", str(type(node_list
)))
109 return -1, "Unexpected response, at 'nodes':'node', not found or not a list. Wrong version?"
112 for node
in node_list
:
113 node_id
= node
.get('id')
115 self
.logger
.error("get_of_switches. Unexpected response at 'nodes':'node'[]:'id', not found: %s", str(node
))
116 return -1, "Unexpected response at 'nodes':'node'[]:'id', not found . Wrong version?"
118 if node_id
== 'controller-config':
121 node_ip_address
= node
.get('flow-node-inventory:ip-address')
122 if node_ip_address
is None:
123 self
.logger
.error("get_of_switches. Unexpected response at 'nodes':'node'[]:'flow-node-inventory:ip-address', not found: %s", str(node
))
124 return -1, "Unexpected response at 'nodes':'node'[]:'flow-node-inventory:ip-address', not found. Wrong version?"
126 node_id_hex
=hex(int(node_id
.split(':')[1])).split('x')[1].zfill(16)
127 switch_list
.append( (':'.join(a
+b
for a
,b
in zip(node_id_hex
[::2], node_id_hex
[1::2])), node_ip_address
))
129 return len(switch_list
), switch_list
130 except (requests
.exceptions
.RequestException
, ValueError) as e
:
131 #ValueError in the case that JSON can not be decoded
132 error_text
= type(e
).__name
__ + ": " + str(e
)
133 self
.logger
.error("get_of_switches " + error_text
)
134 return -1, error_text
136 def obtain_port_correspondence(self
):
137 '''Obtain the correspondence between physical and openflow port names
139 0, dictionary: with physical name as key, openflow name as value
140 -1, error_text: if fails
143 of_response
= requests
.get(self
.url
+"/restconf/operational/opendaylight-inventory:nodes",
144 headers
=self
.headers
)
145 error_text
= "Openflow response %d: %s" % (of_response
.status_code
, of_response
.text
)
146 if of_response
.status_code
!= 200:
147 self
.logger
.warning("obtain_port_correspondence " + error_text
)
148 return -1 , error_text
149 self
.logger
.debug("obtain_port_correspondence " + error_text
)
150 info
= of_response
.json()
152 if type(info
) != dict:
153 self
.logger
.error("obtain_port_correspondence. Unexpected response not a dict: %s", str(info
))
154 return -1, "Unexpected openflow response, not a dict. Wrong version?"
156 nodes
= info
.get('nodes')
157 if type(nodes
) is not dict:
158 self
.logger
.error("obtain_port_correspondence. Unexpected response at 'nodes', not found or not a dict: %s", str(type(nodes
)))
159 return -1, "Unexpected response at 'nodes',not found or not a dict. Wrong version?"
161 node_list
= nodes
.get('node')
162 if type(node_list
) is not list:
163 self
.logger
.error("obtain_port_correspondence. Unexpected response, at 'nodes':'node', not found or not a list: %s", str(type(node_list
)))
164 return -1, "Unexpected response, at 'nodes':'node', not found or not a list. Wrong version?"
166 for node
in node_list
:
167 node_id
= node
.get('id')
169 self
.logger
.error("obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:'id', not found: %s", str(node
))
170 return -1, "Unexpected response at 'nodes':'node'[]:'id', not found . Wrong version?"
172 if node_id
== 'controller-config':
175 # Figure out if this is the appropriate switch. The 'id' is 'openflow:' plus the decimal value
177 # In case this is not the desired switch, continue
178 if self
.id != node_id
:
181 node_connector_list
= node
.get('node-connector')
182 if type(node_connector_list
) is not list:
183 self
.logger
.error("obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:'node-connector', not found or not a list: %s", str(node
))
184 return -1, "Unexpected response at 'nodes':'node'[]:'node-connector', not found or not a list. Wrong version?"
186 for node_connector
in node_connector_list
:
187 self
.pp2ofi
[ str(node_connector
['flow-node-inventory:name']) ] = str(node_connector
['id'] )
188 self
.ofi2pp
[ node_connector
['id'] ] = str(node_connector
['flow-node-inventory:name'])
191 node_ip_address
= node
.get('flow-node-inventory:ip-address')
192 if node_ip_address
is None:
193 self
.logger
.error("obtain_port_correspondence. Unexpected response at 'nodes':'node'[]:'flow-node-inventory:ip-address', not found: %s", str(node
))
194 return -1, "Unexpected response at 'nodes':'node'[]:'flow-node-inventory:ip-address', not found. Wrong version?"
195 self
.ip_address
= node_ip_address
197 #If we found the appropriate dpid no need to continue in the for loop
200 #print self.name, ": obtain_port_correspondence ports:", self.pp2ofi
201 return 0, self
.pp2ofi
202 except (requests
.exceptions
.RequestException
, ValueError) as e
:
203 #ValueError in the case that JSON can not be decoded
204 error_text
= type(e
).__name
__ + ": " + str(e
)
205 self
.logger
.error("obtain_port_correspondence " + error_text
)
206 return -1, error_text
208 def get_of_rules(self
, translate_of_ports
=True):
209 ''' Obtain the rules inserted at openflow controller
211 translate_of_ports: if True it translates ports from openflow index to physical switch name
213 0, dict if ok: with the rule name as key and value is another dictionary with the following content:
214 priority: rule priority
215 name: rule name (present also as the master dict key)
216 ingress_port: match input port of the rule
217 dst_mac: match destination mac address of the rule, can be missing or None if not apply
218 vlan_id: match vlan tag of the rule, can be missing or None if not apply
219 actions: list of actions, composed by a pair tuples:
220 (vlan, None/int): for stripping/setting a vlan tag
221 (out, port): send to this port
223 -1, text_error if fails
226 if len(self
.ofi2pp
) == 0:
227 r
,c
= self
.obtain_port_correspondence()
232 of_response
= requests
.get(self
.url
+"/restconf/config/opendaylight-inventory:nodes/node/" + self
.id +
233 "/table/0", headers
=self
.headers
)
234 error_text
= "Openflow response %d: %s" % (of_response
.status_code
, of_response
.text
)
236 # The configured page does not exist if there are no rules installed. In that case we return an empty dict
237 if of_response
.status_code
== 404:
240 elif of_response
.status_code
!= 200:
241 self
.logger
.warning("get_of_rules " + error_text
)
242 return -1 , error_text
243 self
.logger
.debug("get_of_rules " + error_text
)
245 info
= of_response
.json()
247 if type(info
) != dict:
248 self
.logger
.error("get_of_rules. Unexpected response not a dict: %s", str(info
))
249 return -1, "Unexpected openflow response, not a dict. Wrong version?"
251 table
= info
.get('flow-node-inventory:table')
252 if type(table
) is not list:
253 self
.logger
.error("get_of_rules. Unexpected response at 'flow-node-inventory:table', not a list: %s", str(type(table
)))
254 return -1, "Unexpected response at 'flow-node-inventory:table', not a list. Wrong version?"
256 flow_list
= table
[0].get('flow')
257 if flow_list
is None:
260 if type(flow_list
) is not list:
261 self
.logger
.error("get_of_rules. Unexpected response at 'flow-node-inventory:table'[0]:'flow', not a list: %s", str(type(flow_list
)))
262 return -1, "Unexpected response at 'flow-node-inventory:table'[0]:'flow', not a list. Wrong version?"
264 #TODO translate ports according to translate_of_ports parameter
267 for flow
in flow_list
:
268 if not ('id' in flow
and 'match' in flow
and 'instructions' in flow
and \
269 'instruction' in flow
['instructions'] and 'apply-actions' in flow
['instructions']['instruction'][0] and \
270 'action' in flow
['instructions']['instruction'][0]['apply-actions']):
271 return -1, "unexpected openflow response, one or more elements are missing. Wrong version?"
273 flow
['instructions']['instruction'][0]['apply-actions']['action']
276 rule
['switch'] = self
.dpid
277 rule
['priority'] = flow
.get('priority')
278 #rule['name'] = flow['id']
279 #rule['cookie'] = flow['cookie']
280 if 'in-port' in flow
['match']:
281 in_port
= flow
['match']['in-port']
282 if not in_port
in self
.ofi2pp
:
283 return -1, "Error: Ingress port "+in_port
+" is not in switch port list"
285 if translate_of_ports
:
286 in_port
= self
.ofi2pp
[in_port
]
288 rule
['ingress_port'] = in_port
290 if 'vlan-match' in flow
['match'] and 'vlan-id' in flow
['match']['vlan-match'] and \
291 'vlan-id' in flow
['match']['vlan-match']['vlan-id'] and \
292 'vlan-id-present' in flow
['match']['vlan-match']['vlan-id'] and \
293 flow
['match']['vlan-match']['vlan-id']['vlan-id-present'] == True:
294 rule
['vlan_id'] = flow
['match']['vlan-match']['vlan-id']['vlan-id']
296 if 'ethernet-match' in flow
['match'] and 'ethernet-destination' in flow
['match']['ethernet-match'] and \
297 'address' in flow
['match']['ethernet-match']['ethernet-destination']:
298 rule
['dst_mac'] = flow
['match']['ethernet-match']['ethernet-destination']['address']
300 instructions
=flow
['instructions']['instruction'][0]['apply-actions']['action']
303 for instruction
in instructions
:
304 if instruction
['order'] > max_index
:
305 max_index
= instruction
['order']
307 actions
=[None]*(max_index
+1)
308 for instruction
in instructions
:
309 if 'output-action' in instruction
:
310 if not 'output-node-connector' in instruction
['output-action']:
311 return -1, "unexpected openflow response, one or more elementa are missing. Wrong version?"
313 out_port
= instruction
['output-action']['output-node-connector']
314 if not out_port
in self
.ofi2pp
:
315 return -1, "Error: Output port "+out_port
+" is not in switch port list"
317 if translate_of_ports
:
318 out_port
= self
.ofi2pp
[out_port
]
320 actions
[instruction
['order']] = ('out',out_port
)
322 elif 'strip-vlan-action' in instruction
:
323 actions
[instruction
['order']] = ('vlan', None)
325 elif 'set-field' in instruction
:
326 if not ('vlan-match' in instruction
['set-field'] and 'vlan-id' in instruction
['set-field']['vlan-match'] and 'vlan-id' in instruction
['set-field']['vlan-match']['vlan-id']):
327 return -1, "unexpected openflow response, one or more elements are missing. Wrong version?"
329 actions
[instruction
['order']] = ('vlan', instruction
['set-field']['vlan-match']['vlan-id']['vlan-id'])
331 actions
= [x
for x
in actions
if x
!= None]
333 rule
['actions'] = list(actions
)
334 rules
[flow
['id']] = dict(rule
)
339 #flow['match']['in-port']
340 #flow['match']['vlan-match']['vlan-id']['vlan-id']
342 # -> vlan-match -> vlan-id -> vlan-id
343 #flow['match']['vlan-match']['vlan-id']['vlan-id-present']
344 #TODO se asume que no se usan reglas con vlan-id-present:false
345 #instructions -> instruction -> apply-actions -> action
346 #instructions=flow['instructions']['instruction'][0]['apply-actions']['action']
347 #Es una lista. Posibles elementos:
349 #for instruction in instructions:
350 # if instruction['order'] > max_index:
351 # max_index = instruction['order']
352 #actions=[None]*(max_index+1)
353 #for instruction in instructions:
354 # if 'output-action' in instruction:
355 # actions[instruction['order']] = ('out',instruction['output-action']['output-node-connector'])
356 # elif 'strip-vlan-action' in instruction:
357 # actions[instruction['order']] = ('vlan', None)
358 # elif 'set-field' in instruction:
359 # actions[instruction['order']] = ('vlan', instruction['set-field']['vlan-match']['vlan-id']['vlan-id'])
361 #actions = [x for x in actions if x != None]
362 # -> output-action -> output-node-connector
366 except (requests
.exceptions
.RequestException
, ValueError) as e
:
367 #ValueError in the case that JSON can not be decoded
368 error_text
= type(e
).__name
__ + ": " + str(e
)
369 self
.logger
.error("get_of_rules " + error_text
)
370 return -1, error_text
372 def del_flow(self
, flow_name
):
373 ''' Delete an existing rule
374 Params: flow_name, this is the rule name
377 -1, text_error if fails
380 of_response
= requests
.delete(self
.url
+"/restconf/config/opendaylight-inventory:nodes/node/" + self
.id +
381 "/table/0/flow/"+flow_name
, headers
=self
.headers
)
382 error_text
= "Openflow response %d: %s" % (of_response
.status_code
, of_response
.text
)
383 if of_response
.status_code
!= 200:
384 self
.logger
.warning("del_flow " + error_text
)
385 return -1 , error_text
386 self
.logger
.debug("del_flow OK " + error_text
)
389 except requests
.exceptions
.RequestException
as e
:
390 error_text
= type(e
).__name
__ + ": " + str(e
)
391 self
.logger
.error("del_flow " + error_text
)
392 return -1, error_text
394 def new_flow(self
, data
):
395 ''' Insert a new static rule
396 Params: data: dictionary with the following content:
397 priority: rule priority
399 ingress_port: match input port of the rule
400 dst_mac: match destination mac address of the rule, missing or None if not apply
401 vlan_id: match vlan tag of the rule, missing or None if not apply
402 actions: list of actions, composed by a pair tuples with these posibilities:
403 ('vlan', None/int): for stripping/setting a vlan tag
404 ('out', port): send to this port
407 -1, text_error if fails
409 if len(self
.pp2ofi
) == 0:
410 r
,c
= self
.obtain_port_correspondence()
414 #We have to build the data for the opendaylight call from the generic data
416 sdata
['flow-node-inventory:flow'] = list()
417 sdata
['flow-node-inventory:flow'].append(dict())
418 flow
= sdata
['flow-node-inventory:flow'][0]
419 flow
['id'] = data
['name']
420 flow
['flow-name'] = data
['name']
421 flow
['idle-timeout'] = 0
422 flow
['hard-timeout'] = 0
424 flow
['priority'] = data
.get('priority')
425 flow
['match'] = dict()
426 if not data
['ingress_port'] in self
.pp2ofi
:
427 error_text
= 'Error. Port '+data
['ingress_port']+' is not present in the switch'
428 self
.logger
.warning("new_flow " + error_text
)
429 return -1, error_text
430 flow
['match']['in-port'] = self
.pp2ofi
[data
['ingress_port']]
431 if 'dst_mac' in data
:
432 flow
['match']['ethernet-match'] = dict()
433 flow
['match']['ethernet-match']['ethernet-destination'] = dict()
434 flow
['match']['ethernet-match']['ethernet-destination']['address'] = data
['dst_mac']
435 if data
.get('vlan_id'):
436 flow
['match']['vlan-match'] = dict()
437 flow
['match']['vlan-match']['vlan-id'] = dict()
438 flow
['match']['vlan-match']['vlan-id']['vlan-id-present'] = True
439 flow
['match']['vlan-match']['vlan-id']['vlan-id'] = int(data
['vlan_id'])
440 flow
['instructions'] = dict()
441 flow
['instructions']['instruction'] = list()
442 flow
['instructions']['instruction'].append(dict())
443 flow
['instructions']['instruction'][0]['order'] = 1
444 flow
['instructions']['instruction'][0]['apply-actions'] = dict()
445 flow
['instructions']['instruction'][0]['apply-actions']['action'] = list()
446 actions
= flow
['instructions']['instruction'][0]['apply-actions']['action']
449 for action
in data
['actions']:
450 new_action
= { 'order': order
}
451 if action
[0] == "vlan":
452 if action
[1] == None:
454 new_action
['strip-vlan-action'] = dict()
456 new_action
['set-field'] = dict()
457 new_action
['set-field']['vlan-match'] = dict()
458 new_action
['set-field']['vlan-match']['vlan-id'] = dict()
459 new_action
['set-field']['vlan-match']['vlan-id']['vlan-id-present'] = True
460 new_action
['set-field']['vlan-match']['vlan-id']['vlan-id'] = int(action
[1])
461 elif action
[0] == 'out':
462 new_action
['output-action'] = dict()
463 if not action
[1] in self
.pp2ofi
:
464 error_msj
= 'Port '+action
[1]+' is not present in the switch'
467 new_action
['output-action']['output-node-connector'] = self
.pp2ofi
[ action
[1] ]
469 error_msj
= "Unknown item '%s' in action list" % action
[0]
470 self
.logger
.error("new_flow " + error_msj
)
473 actions
.append(new_action
)
476 #print json.dumps(sdata)
477 of_response
= requests
.put(self
.url
+"/restconf/config/opendaylight-inventory:nodes/node/" + self
.id +
478 "/table/0/flow/" + data
['name'],
479 headers
=self
.headers
, data
=json
.dumps(sdata
) )
480 error_text
= "Openflow response %d: %s" % (of_response
.status_code
, of_response
.text
)
481 if of_response
.status_code
!= 200:
482 self
.logger
.warning("new_flow " + error_text
)
483 return -1 , error_text
484 self
.logger
.debug("new_flow OK " + error_text
)
487 except requests
.exceptions
.RequestException
as e
:
488 error_text
= type(e
).__name
__ + ": " + str(e
)
489 self
.logger
.error("new_flow " + error_text
)
490 return -1, error_text
492 def clear_all_flows(self
):
493 ''' Delete all existing rules
496 -1, text_error if fails
499 of_response
= requests
.delete(self
.url
+"/restconf/config/opendaylight-inventory:nodes/node/" + self
.id +
500 "/table/0", headers
=self
.headers
)
501 error_text
= "Openflow response %d: %s" % (of_response
.status_code
, of_response
.text
)
502 if of_response
.status_code
!= 200 and of_response
.status_code
!= 404: #HTTP_Not_Found
503 self
.logger
.warning("clear_all_flows " + error_text
)
504 return -1 , error_text
505 self
.logger
.debug("clear_all_flows OK " + error_text
)
507 except requests
.exceptions
.RequestException
as e
:
508 error_text
= type(e
).__name
__ + ": " + str(e
)
509 self
.logger
.error("clear_all_flows " + error_text
)
510 return -1, error_text