329cb6621f873731e68cdac82cc2da5d298d00dd
[osm/RO.git] / RO-SDN-odl_openflow / osm_rosdn_odlof / odl_of.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 ##
5 # Copyright 2015 Telefonica Investigacion 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 import json
32 import requests
33 import base64
34 import logging
35 from osm_ro_plugin.openflow_conn import OpenflowConn, OpenflowConnException, OpenflowConnConnectionException, \
36 OpenflowConnUnexpectedResponse, OpenflowConnAuthException, OpenflowConnNotFoundException, \
37 OpenflowConnConflictException, OpenflowConnNotSupportedException, OpenflowConnNotImplemented
38
39 __author__ = "Pablo Montes, Alfonso Tierno"
40 __date__ = "$28-oct-2014 12:07:15$"
41
42
43 class OfConnOdl(OpenflowConn):
44 """OpenDayLight connector. No MAC learning is used"""
45
46 def __init__(self, params):
47 """ Constructor.
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
56 """
57
58 OpenflowConn.__init__(self, params)
59
60 # check params
61 url = params.get("of_url")
62 if not url:
63 raise ValueError("'url' must be provided")
64 if not url.startswith("http"):
65 url = "http://" + url
66 if not url.endswith("/"):
67 url = url + "/"
68 self.url = url + "onos/v1/"
69
70 # internal variables
71 self.name = "OpenDayLight"
72 self.headers = {'content-type': 'application/json', 'Accept': 'application/json'}
73 self.auth = None
74 self.pp2ofi = {} # From Physical Port to OpenFlow Index
75 self.ofi2pp = {} # From OpenFlow Index to Physical Port
76
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
84
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")
88
89 def get_of_switches(self):
90 """
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
94 """
95 try:
96 of_response = requests.get(self.url + "restconf/operational/opendaylight-inventory:nodes",
97 headers=self.headers)
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)
102
103 self.logger.debug("get_of_switches " + error_text)
104 info = of_response.json()
105
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?")
109
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",
113 str(type(info)))
114 raise OpenflowconnUnexpectedResponse("Unexpected response at 'nodes', not found or not a dict."
115 " Wrong version?")
116
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?")
123
124 switch_list = []
125 for node in node_list:
126 node_id = node.get('id')
127 if node_id is None:
128 self.logger.error("get_of_switches. Unexpected response at 'nodes':'node'[]:'id', not found: %s",
129 str(node))
130 raise OpenflowconnUnexpectedResponse("Unexpected response at 'nodes':'node'[]:'id', not found. "
131 "Wrong version?")
132
133 if node_id == 'controller-config':
134 continue
135
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?")
142
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))
145 return switch_list
146
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)
156
157 def obtain_port_correspondence(self):
158 """
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
162 """
163 try:
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()
172
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?")
176
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. "
182 "Wrong version?")
183
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."
189 " Wrong version?")
190
191 for node in node_list:
192 node_id = node.get('id')
193 if node_id is None:
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. "
197 "Wrong version?")
198
199 if node_id == 'controller-config':
200 continue
201
202 # Figure out if this is the appropriate switch. The 'id' is 'openflow:' plus the decimal value
203 # of the dpid
204 # In case this is not the desired switch, continue
205 if self.id != node_id:
206 continue
207
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?")
214
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'])
218
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?")
225
226 # If we found the appropriate dpid no need to continue in the for loop
227 break
228
229 # print self.name, ": obtain_port_correspondence ports:", self.pp2ofi
230 return 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)
240
241 def get_of_rules(self, translate_of_ports=True):
242 """
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
254 switch: DPID, all
255 Raise a OpenflowconnConnectionException exception in case of failure
256
257 """
258
259 try:
260 # get rules
261 if len(self.ofi2pp) == 0:
262 self.obtain_port_correspondence()
263
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)
267
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:
270 return []
271
272 elif of_response.status_code != 200:
273 self.logger.warning("get_of_rules " + error_text)
274 raise OpenflowconnUnexpectedResponse(error_text)
275
276 self.logger.debug("get_of_rules " + error_text)
277
278 info = of_response.json()
279
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?")
283
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. "
289 "Wrong version?")
290
291 flow_list = table[0].get('flow')
292 if flow_list is None:
293 return []
294
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?")
300
301 # TODO translate ports according to translate_of_ports parameter
302
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?")
311
312 flow['instructions']['instruction'][0]['apply-actions']['action']
313
314 rule = dict()
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".
323 format(in_port))
324
325 if translate_of_ports:
326 in_port = self.ofi2pp[in_port]
327
328 rule['ingress_port'] = in_port
329
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']
335
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']
339
340 instructions = flow['instructions']['instruction'][0]['apply-actions']['action']
341
342 max_index = 0
343 for instruction in instructions:
344 if instruction['order'] > max_index:
345 max_index = instruction['order']
346
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?")
353
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".
357 format(out_port))
358
359 if translate_of_ports:
360 out_port = self.ofi2pp[out_port]
361
362 actions[instruction['order']] = ('out', out_port)
363
364 elif 'strip-vlan-action' in instruction:
365 actions[instruction['order']] = ('vlan', None)
366
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?")
373
374 actions[instruction['order']] = ('vlan',
375 instruction['set-field']['vlan-match']['vlan-id']['vlan-id'])
376
377 actions = [x for x in actions if x is not None]
378
379 rule['actions'] = list(actions)
380 rules.append(rule)
381
382 return rules
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)
392
393 def del_flow(self, flow_name):
394 """
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
398 """
399
400 try:
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)
408 return None
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)
414
415 def new_flow(self, data):
416 """
417 Insert a new static rule
418 :param data: dictionary with the following content:
419 priority: rule priority
420 name: rule name
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
428 """
429
430 try:
431 self.logger.debug("new_flow data: {}".format(data))
432 if len(self.pp2ofi) == 0:
433 self.obtain_port_correspondence()
434
435 # We have to build the data for the opendaylight call from the generic data
436 flow = {
437 'id': data['name'],
438 'flow-name': data['name'],
439 'idle-timeout': 0,
440 'hard-timeout': 0,
441 'table_id': 0,
442 'priority': data.get('priority'),
443 'match': {}
444 }
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']}
454 }
455 if data.get('vlan_id'):
456 flow['match']['vlan-match'] = {
457 'vlan-id': {
458 'vlan-id-present': True,
459 'vlan-id': int(data['vlan_id'])
460 }
461 }
462 actions = []
463 flow['instructions'] = {
464 'instruction': [{
465 'order': 1,
466 'apply-actions': {'action': actions}
467 }]
468 }
469
470 order = 0
471 for action in data['actions']:
472 new_action = {'order': order}
473 if action[0] == "vlan":
474 if action[1] is None:
475 # strip vlan
476 new_action['strip-vlan-action'] = {}
477 else:
478 new_action['set-field'] = {
479 'vlan-match': {
480 'vlan-id': {
481 'vlan-id-present': True,
482 'vlan-id': int(action[1])
483 }
484 }
485 }
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)
491
492 new_action['output-action']['output-node-connector'] = self.pp2ofi[action[1]]
493 else:
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)
497
498 actions.append(new_action)
499 order += 1
500
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)
509 return None
510
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)
516
517 def clear_all_flows(self):
518 """
519 Delete all existing rules
520 :return: Raise a OpenflowconnConnectionException expection in case of failure
521 """
522 try:
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)
530 return None
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)