ed1bca4b99853bf3400058eca2f99e0126efbc80
[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, OpenflowConnConnectionException, OpenflowConnUnexpectedResponse
36 # OpenflowConnException, 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])),
145 node_ip_address))
146 return switch_list
147
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)
157
158 def obtain_port_correspondence(self):
159 """
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
163 """
164 try:
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()
173
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?")
177
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. "
183 "Wrong version?")
184
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."
190 " Wrong version?")
191
192 for node in node_list:
193 node_id = node.get('id')
194 if node_id is None:
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. "
198 "Wrong version?")
199
200 if node_id == 'controller-config':
201 continue
202
203 # Figure out if this is the appropriate switch. The 'id' is 'openflow:' plus the decimal value
204 # of the dpid
205 # In case this is not the desired switch, continue
206 if self.id != node_id:
207 continue
208
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?")
215
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'])
219
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?")
226
227 # If we found the appropriate dpid no need to continue in the for loop
228 break
229
230 # print self.name, ": obtain_port_correspondence ports:", self.pp2ofi
231 return 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)
241
242 def get_of_rules(self, translate_of_ports=True):
243 """
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
255 switch: DPID, all
256 Raise a OpenflowConnConnectionException exception in case of failure
257
258 """
259
260 try:
261 # get rules
262 if len(self.ofi2pp) == 0:
263 self.obtain_port_correspondence()
264
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)
268
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:
271 return []
272
273 elif of_response.status_code != 200:
274 self.logger.warning("get_of_rules " + error_text)
275 raise OpenflowConnUnexpectedResponse(error_text)
276
277 self.logger.debug("get_of_rules " + error_text)
278
279 info = of_response.json()
280
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?")
284
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. "
290 "Wrong version?")
291
292 flow_list = table[0].get('flow')
293 if flow_list is None:
294 return []
295
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?")
301
302 # TODO translate ports according to translate_of_ports parameter
303
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?")
312
313 flow['instructions']['instruction'][0]['apply-actions']['action']
314
315 rule = dict()
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".
324 format(in_port))
325
326 if translate_of_ports:
327 in_port = self.ofi2pp[in_port]
328
329 rule['ingress_port'] = in_port
330
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']
336
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']
340
341 instructions = flow['instructions']['instruction'][0]['apply-actions']['action']
342
343 max_index = 0
344 for instruction in instructions:
345 if instruction['order'] > max_index:
346 max_index = instruction['order']
347
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?")
354
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".
358 format(out_port))
359
360 if translate_of_ports:
361 out_port = self.ofi2pp[out_port]
362
363 actions[instruction['order']] = ('out', out_port)
364
365 elif 'strip-vlan-action' in instruction:
366 actions[instruction['order']] = ('vlan', None)
367
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?")
374
375 actions[instruction['order']] = ('vlan',
376 instruction['set-field']['vlan-match']['vlan-id']['vlan-id'])
377
378 actions = [x for x in actions if x is not None]
379
380 rule['actions'] = list(actions)
381 rules.append(rule)
382
383 return rules
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)
393
394 def del_flow(self, flow_name):
395 """
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
399 """
400
401 try:
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)
409 return None
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)
415
416 def new_flow(self, data):
417 """
418 Insert a new static rule
419 :param data: dictionary with the following content:
420 priority: rule priority
421 name: rule name
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
429 """
430
431 try:
432 self.logger.debug("new_flow data: {}".format(data))
433 if len(self.pp2ofi) == 0:
434 self.obtain_port_correspondence()
435
436 # We have to build the data for the opendaylight call from the generic data
437 flow = {
438 'id': data['name'],
439 'flow-name': data['name'],
440 'idle-timeout': 0,
441 'hard-timeout': 0,
442 'table_id': 0,
443 'priority': data.get('priority'),
444 'match': {}
445 }
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']}
455 }
456 if data.get('vlan_id'):
457 flow['match']['vlan-match'] = {
458 'vlan-id': {
459 'vlan-id-present': True,
460 'vlan-id': int(data['vlan_id'])
461 }
462 }
463 actions = []
464 flow['instructions'] = {
465 'instruction': [{
466 'order': 1,
467 'apply-actions': {'action': actions}
468 }]
469 }
470
471 order = 0
472 for action in data['actions']:
473 new_action = {'order': order}
474 if action[0] == "vlan":
475 if action[1] is None:
476 # strip vlan
477 new_action['strip-vlan-action'] = {}
478 else:
479 new_action['set-field'] = {
480 'vlan-match': {
481 'vlan-id': {
482 'vlan-id-present': True,
483 'vlan-id': int(action[1])
484 }
485 }
486 }
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)
492
493 new_action['output-action']['output-node-connector'] = self.pp2ofi[action[1]]
494 else:
495 error_msg = "Unknown item '{}' in action list".format(action[0])
496 self.logger.error("new_flow " + error_msg)
497 raise OpenflowConnUnexpectedResponse(error_msg)
498
499 actions.append(new_action)
500 order += 1
501
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)
510 return None
511
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)
517
518 def clear_all_flows(self):
519 """
520 Delete all existing rules
521 :return: Raise a OpenflowConnConnectionException expection in case of failure
522 """
523 try:
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)
531 return None
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)