fix moving openvim version/date/db_version to ovim
[osm/openvim.git] / ODL.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 ##
5 # Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U.
6 # This file is part of openvim
7 # All Rights Reserved.
8 #
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
12 #
13 # http://www.apache.org/licenses/LICENSE-2.0
14 #
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
19 # under the License.
20 #
21 # For those usages not covered by the Apache License, Version 2.0 please
22 # contact with: nfvlabs@tid.es
23 ##
24
25 '''
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
29 '''
30
31 __author__="Pablo Montes, Alfonso Tierno"
32 __date__ ="$28-oct-2014 12:07:15$"
33
34
35 import json
36 import requests
37 import base64
38 import logging
39
40 class OF_conn():
41 '''OpenDayLight connector. No MAC learning is used'''
42 def __init__(self, params):
43 ''' Constructor.
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
53 '''
54 #check params
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")
57 #internal variables
58 self.name = "OpenDayLight"
59 self.headers = {'content-type':'application/json',
60 'Accept':'application/json'
61 }
62 self.auth=None
63 self.pp2ofi={} # From Physical Port to OpenFlow Index
64 self.ofi2pp={} # From OpenFlow Index to Physical Port
65
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"):
71 of_password=""
72 else:
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
76
77
78 self.logger = logging.getLogger('vim.OF.ODL')
79 self.logger.setLevel( getattr(logging, params.get("of_debug", "ERROR")) )
80
81 def get_of_switches(self):
82 ''' Obtain a a list of switches or DPID detected by this controller
83 Return
84 >=0, list: list length, and a list where each element a tuple pair (DPID, IP address)
85 <0, text_error: if fails
86 '''
87 try:
88 of_response = requests.get(self.url+"/restconf/operational/opendaylight-inventory:nodes",
89 headers=self.headers)
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()
96
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?"
100
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?"
105
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?"
110
111 switch_list=[]
112 for node in node_list:
113 node_id = node.get('id')
114 if node_id is None:
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?"
117
118 if node_id == 'controller-config':
119 continue
120
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?"
125
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))
128
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
135
136 def obtain_port_correspondence(self):
137 '''Obtain the correspondence between physical and openflow port names
138 return:
139 0, dictionary: with physical name as key, openflow name as value
140 -1, error_text: if fails
141 '''
142 try:
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()
151
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?"
155
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?"
160
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?"
165
166 for node in node_list:
167 node_id = node.get('id')
168 if node_id is None:
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?"
171
172 if node_id == 'controller-config':
173 continue
174
175 # Figure out if this is the appropriate switch. The 'id' is 'openflow:' plus the decimal value
176 # of the dpid
177 # In case this is not the desired switch, continue
178 if self.id != node_id:
179 continue
180
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?"
185
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'])
189
190
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
196
197 #If we found the appropriate dpid no need to continue in the for loop
198 break
199
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
207
208 def get_of_rules(self, translate_of_ports=True):
209 ''' Obtain the rules inserted at openflow controller
210 Params:
211 translate_of_ports: if True it translates ports from openflow index to physical switch name
212 Return:
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
222 switch: DPID, all
223 -1, text_error if fails
224 '''
225
226 if len(self.ofi2pp) == 0:
227 r,c = self.obtain_port_correspondence()
228 if r<0:
229 return r,c
230 #get rules
231 try:
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)
235
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:
238 return 0, {}
239
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)
244
245 info = of_response.json()
246
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?"
250
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?"
255
256 flow_list = table[0].get('flow')
257 if flow_list is None:
258 return 0, {}
259
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?"
263
264 #TODO translate ports according to translate_of_ports parameter
265
266 rules = dict()
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?"
272
273 flow['instructions']['instruction'][0]['apply-actions']['action']
274
275 rule = dict()
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"
284
285 if translate_of_ports:
286 in_port = self.ofi2pp[in_port]
287
288 rule['ingress_port'] = in_port
289
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']
295
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']
299
300 instructions=flow['instructions']['instruction'][0]['apply-actions']['action']
301
302 max_index=0
303 for instruction in instructions:
304 if instruction['order'] > max_index:
305 max_index = instruction['order']
306
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?"
312
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"
316
317 if translate_of_ports:
318 out_port = self.ofi2pp[out_port]
319
320 actions[instruction['order']] = ('out',out_port)
321
322 elif 'strip-vlan-action' in instruction:
323 actions[instruction['order']] = ('vlan', None)
324
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?"
328
329 actions[instruction['order']] = ('vlan', instruction['set-field']['vlan-match']['vlan-id']['vlan-id'])
330
331 actions = [x for x in actions if x != None]
332
333 rule['actions'] = list(actions)
334 rules[flow['id']] = dict(rule)
335
336 #flow['id']
337 #flow['priority']
338 #flow['cookie']
339 #flow['match']['in-port']
340 #flow['match']['vlan-match']['vlan-id']['vlan-id']
341 # match -> in-port
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:
348 #max_index=0
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'])
360 #
361 #actions = [x for x in actions if x != None]
362 # -> output-action -> output-node-connector
363 # -> pop-vlan-action
364
365 return 0, rules
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
371
372 def del_flow(self, flow_name):
373 ''' Delete an existing rule
374 Params: flow_name, this is the rule name
375 Return
376 0, None if ok
377 -1, text_error if fails
378 '''
379 try:
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)
387 return 0, None
388
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
393
394 def new_flow(self, data):
395 ''' Insert a new static rule
396 Params: data: dictionary with the following content:
397 priority: rule priority
398 name: rule name
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
405 Return
406 0, None if ok
407 -1, text_error if fails
408 '''
409 if len(self.pp2ofi) == 0:
410 r,c = self.obtain_port_correspondence()
411 if r<0:
412 return r,c
413 try:
414 #We have to build the data for the opendaylight call from the generic data
415 sdata = dict()
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
423 flow['table_id'] = 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']
447
448 order = 0
449 for action in data['actions']:
450 new_action = { 'order': order }
451 if action[0] == "vlan":
452 if action[1] == None:
453 #strip vlan
454 new_action['strip-vlan-action'] = dict()
455 else:
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'
465 return -1, error_msj
466
467 new_action['output-action']['output-node-connector'] = self.pp2ofi[ action[1] ]
468 else:
469 error_msj = "Unknown item '%s' in action list" % action[0]
470 self.logger.error("new_flow " + error_msj)
471 return -1, error_msj
472
473 actions.append(new_action)
474 order += 1
475
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)
485 return 0, None
486
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
491
492 def clear_all_flows(self):
493 ''' Delete all existing rules
494 Return:
495 0, None if ok
496 -1, text_error if fails
497 '''
498 try:
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)
506 return 0, None
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