fixing flake8 tests
[osm/RO.git] / RO-SDN-onos_openflow / osm_rosdn_onosof / onos_of.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 ##
5 # Copyright 2016, I2T Research Group (UPV/EHU)
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: alaitz.mendiola@ehu.eus or alaitz.mendiola@gmail.com
23 ##
24
25 """
26 ImplementS the pluging for the Open Network Operating System (ONOS) openflow
27 controller. 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__ = "Alaitz Mendiola"
40 __date__ = "$22-nov-2016$"
41
42
43 class OfConnOnos(OpenflowConn):
44 """
45 ONOS connector. No MAC learning is used
46 """
47 def __init__(self, params):
48 """ Constructor.
49 :param params: dictionary with the following keys:
50 of_dpid: DPID to use for this controller ?? Does a controller have a dpid?
51 of_url: must be [http://HOST:PORT/]
52 of_user: user credentials, can be missing or None
53 of_password: password credentials
54 of_debug: debug level for logging. Default to ERROR
55 other keys are ignored
56 Raise an exception if same parameter is missing or wrong
57 """
58
59 OpenflowConn.__init__(self, params)
60
61 # check params
62 url = params.get("of_url")
63 if not url:
64 raise ValueError("'url' must be provided")
65 if not url.startswith("http"):
66 url = "http://" + url
67 if not url.endswith("/"):
68 url = url + "/"
69 self.url = url + "onos/v1/"
70
71 # internal variables
72 self.name = "onosof"
73 self.headers = {'content-type': 'application/json', 'accept': 'application/json'}
74
75 self.auth = "None"
76 self.pp2ofi = {} # From Physical Port to OpenFlow Index
77 self.ofi2pp = {} # From OpenFlow Index to Physical Port
78
79 self.dpid = str(params["of_dpid"])
80 self.id = 'of:'+str(self.dpid.replace(':', ''))
81
82 # TODO This may not be straightforward
83 if params.get("of_user"):
84 of_password = params.get("of_password", "")
85 self.auth = base64.b64encode(bytes(params["of_user"] + ":" + of_password, "utf-8"))
86 self.auth = self.auth.decode()
87 self.headers['authorization'] = 'Basic ' + self.auth
88
89 self.logger = logging.getLogger('openmano.sdnconn.onosof')
90 # self.logger.setLevel( getattr(logging, params.get("of_debug", "ERROR")) )
91 self.logger.debug("onosof plugin initialized")
92 self.ip_address = None
93
94 def get_of_switches(self):
95 """
96 Obtain a a list of switches or DPID detected by this controller
97 :return: list where each element a tuple pair (DPID, IP address)
98 Raise a openflowconnUnexpectedResponse expection in case of failure
99 """
100 try:
101 self.headers['content-type'] = 'text/plain'
102 of_response = requests.get(self.url + "devices", headers=self.headers)
103 error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text)
104 if of_response.status_code != 200:
105 self.logger.warning("get_of_switches " + error_text)
106 raise OpenflowConnUnexpectedResponse(error_text)
107
108 self.logger.debug("get_of_switches " + error_text)
109 info = of_response.json()
110
111 if type(info) != dict:
112 self.logger.error("get_of_switches. Unexpected response, not a dict: %s", str(info))
113 raise OpenflowConnUnexpectedResponse("Unexpected response, not a dict. Wrong version?")
114
115 node_list = info.get('devices')
116
117 if type(node_list) is not list:
118 self.logger.error(
119 "get_of_switches. Unexpected response, at 'devices', not found or not a list: %s",
120 str(type(node_list)))
121 raise OpenflowConnUnexpectedResponse("Unexpected response, at 'devices', 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 'device':'id', not found: %s",
129 str(node))
130 raise OpenflowConnUnexpectedResponse("Unexpected response at 'device':'id', "
131 "not found . Wrong version?")
132
133 node_ip_address = node.get('annotations').get('managementAddress')
134 if node_ip_address is None:
135 self.logger.error(
136 "get_of_switches. Unexpected response at 'device':'managementAddress', not found: %s",
137 str(node))
138 raise OpenflowConnUnexpectedResponse(
139 "Unexpected response at 'device':'managementAddress', not found. Wrong version?")
140
141 node_id_hex = hex(int(node_id.split(':')[1])).split('x')[1].zfill(16)
142
143 switch_list.append(
144 (':'.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 openflowconnUnexpectedResponse expection in case of failure
162 """
163 try:
164 self.headers['content-type'] = 'text/plain'
165 of_response = requests.get(self.url + "devices/" + self.id + "/ports", 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
171 self.logger.debug("obtain_port_correspondence " + error_text)
172 info = of_response.json()
173
174 node_connector_list = info.get('ports')
175 if type(node_connector_list) is not list:
176 self.logger.error(
177 "obtain_port_correspondence. Unexpected response at 'ports', not found or not a list: %s",
178 str(node_connector_list))
179 raise OpenflowConnUnexpectedResponse("Unexpected response at 'ports', not found or not "
180 "a list. Wrong version?")
181
182 for node_connector in node_connector_list:
183 if node_connector['port'] != "local":
184 self.pp2ofi[str(node_connector['annotations']['portName'])] = str(node_connector['port'])
185 self.ofi2pp[str(node_connector['port'])] = str(node_connector['annotations']['portName'])
186
187 node_ip_address = info['annotations']['managementAddress']
188 if node_ip_address is None:
189 self.logger.error(
190 "obtain_port_correspondence. Unexpected response at 'managementAddress', not found: %s",
191 str(self.id))
192 raise OpenflowConnUnexpectedResponse("Unexpected response at 'managementAddress', "
193 "not found. Wrong version?")
194 self.ip_address = node_ip_address
195
196 # print self.name, ": obtain_port_correspondence ports:", self.pp2ofi
197 return self.pp2ofi
198 except requests.exceptions.RequestException as e:
199 error_text = type(e).__name__ + ": " + str(e)
200 self.logger.error("obtain_port_correspondence " + error_text)
201 raise OpenflowConnConnectionException(error_text)
202 except 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 raise OpenflowConnUnexpectedResponse(error_text)
207
208 def get_of_rules(self, translate_of_ports=True):
209 """
210 Obtain the rules inserted at openflow controller
211 :param translate_of_ports: if True it translates ports from openflow index to physical switch name
212 :return: list where each item is a dictionary with the following content:
213 priority: rule priority
214 name: rule name (present also as the master dict key)
215 ingress_port: match input port of the rule
216 dst_mac: match destination mac address of the rule, can be missing or None if not apply
217 vlan_id: match vlan tag of the rule, can be missing or None if not apply
218 actions: list of actions, composed by a pair tuples:
219 (vlan, None/int): for stripping/setting a vlan tag
220 (out, port): send to this port
221 switch: DPID, all
222 Raise a openflowconnUnexpectedResponse exception in case of failure
223 """
224
225 try:
226
227 if len(self.ofi2pp) == 0:
228 self.obtain_port_correspondence()
229
230 # get rules
231 self.headers['content-type'] = 'text/plain'
232 of_response = requests.get(self.url + "flows/" + self.id, headers=self.headers)
233 error_text = "Openflow response %d: %s" % (of_response.status_code, of_response.text)
234
235 # The configured page does not exist if there are no rules installed. In that case we return an empty dict
236 if of_response.status_code == 404:
237 return []
238
239 elif of_response.status_code != 200:
240 self.logger.warning("get_of_rules " + error_text)
241 raise OpenflowConnUnexpectedResponse(error_text)
242 self.logger.debug("get_of_rules " + error_text)
243
244 info = of_response.json()
245
246 if type(info) != dict:
247 self.logger.error("get_of_rules. Unexpected response, not a dict: %s", str(info))
248 raise OpenflowConnUnexpectedResponse("Unexpected openflow response, not a dict. "
249 "Wrong version?")
250
251 flow_list = info.get('flows')
252
253 if flow_list is None:
254 return []
255 if type(flow_list) is not list:
256 self.logger.error(
257 "get_of_rules. Unexpected response at 'flows', not a list: %s",
258 str(type(flow_list)))
259 raise OpenflowConnUnexpectedResponse("Unexpected response at 'flows', not a list. "
260 "Wrong version?")
261
262 rules = [] # Response list
263 for flow in flow_list:
264 if not ('id' in flow and 'selector' in flow and 'treatment' in flow and
265 'instructions' in flow['treatment'] and 'criteria' in
266 flow['selector']):
267 raise OpenflowConnUnexpectedResponse("unexpected openflow response, one or more "
268 "elements are missing. Wrong version?")
269
270 rule = dict()
271 rule['switch'] = self.dpid
272 rule['priority'] = flow.get('priority')
273 rule['name'] = flow['id']
274
275 for criteria in flow['selector']['criteria']:
276 if criteria['type'] == 'IN_PORT':
277 in_port = str(criteria['port'])
278 if in_port != "CONTROLLER":
279 if in_port not in self.ofi2pp:
280 raise OpenflowConnUnexpectedResponse("Error: Ingress port {} is not "
281 "in switch port list".format(in_port))
282 if translate_of_ports:
283 in_port = self.ofi2pp[in_port]
284 rule['ingress_port'] = in_port
285
286 elif criteria['type'] == 'VLAN_VID':
287 rule['vlan_id'] = criteria['vlanId']
288
289 elif criteria['type'] == 'ETH_DST':
290 rule['dst_mac'] = str(criteria['mac']).lower()
291
292 actions = []
293 for instruction in flow['treatment']['instructions']:
294 if instruction['type'] == "OUTPUT":
295 out_port = str(instruction['port'])
296 if out_port != "CONTROLLER":
297 if out_port not in self.ofi2pp:
298 raise OpenflowConnUnexpectedResponse("Error: Output port {} is not in "
299 "switch port list".format(out_port))
300
301 if translate_of_ports:
302 out_port = self.ofi2pp[out_port]
303
304 actions.append(('out', out_port))
305
306 if instruction['type'] == "L2MODIFICATION" and instruction['subtype'] == "VLAN_POP":
307 actions.append(('vlan', 'None'))
308 if instruction['type'] == "L2MODIFICATION" and instruction['subtype'] == "VLAN_ID":
309 actions.append(('vlan', instruction['vlanId']))
310
311 rule['actions'] = actions
312 rules.append(rule)
313 return rules
314
315 except requests.exceptions.RequestException as e:
316 # ValueError in the case that JSON can not be decoded
317 error_text = type(e).__name__ + ": " + str(e)
318 self.logger.error("get_of_rules " + error_text)
319 raise OpenflowConnConnectionException(error_text)
320 except ValueError as e:
321 # ValueError in the case that JSON can not be decoded
322 error_text = type(e).__name__ + ": " + str(e)
323 self.logger.error("get_of_rules " + error_text)
324 raise OpenflowConnUnexpectedResponse(error_text)
325
326 def del_flow(self, flow_name):
327 """
328 Delete an existing rule
329 :param flow_name:
330 :return: Raise a openflowconnUnexpectedResponse expection in case of failure
331 """
332
333 try:
334 self.logger.debug("del_flow: delete flow name {}".format(flow_name))
335 self.headers['content-type'] = None
336 of_response = requests.delete(self.url + "flows/" + self.id + "/" + flow_name, headers=self.headers)
337 error_text = "Openflow response {}: {}".format(of_response.status_code, of_response.text)
338
339 if of_response.status_code != 204:
340 self.logger.warning("del_flow " + error_text)
341 raise OpenflowConnUnexpectedResponse(error_text)
342
343 self.logger.debug("del_flow: {} OK,: {} ".format(flow_name, error_text))
344 return None
345
346 except requests.exceptions.RequestException as e:
347 error_text = type(e).__name__ + ": " + str(e)
348 self.logger.error("del_flow " + error_text)
349 raise OpenflowConnConnectionException(error_text)
350
351 def new_flow(self, data):
352 """
353 Insert a new static rule
354 :param data: dictionary with the following content:
355 priority: rule priority
356 name: rule name
357 ingress_port: match input port of the rule
358 dst_mac: match destination mac address of the rule, missing or None if not apply
359 vlan_id: match vlan tag of the rule, missing or None if not apply
360 actions: list of actions, composed by a pair tuples with these posibilities:
361 ('vlan', None/int): for stripping/setting a vlan tag
362 ('out', port): send to this port
363 :return: Raise a openflowconnUnexpectedResponse exception in case of failure
364 """
365 try:
366 self.logger.debug("new_flow data: {}".format(data))
367
368 if len(self.pp2ofi) == 0:
369 self.obtain_port_correspondence()
370
371 # Build the dictionary with the flow rule information for ONOS
372 flow = dict()
373 # flow['id'] = data['name']
374 flow['tableId'] = 0
375 flow['priority'] = data.get('priority')
376 flow['timeout'] = 0
377 flow['isPermanent'] = "true"
378 flow['appId'] = 10 # FIXME We should create an appId for OSM
379 flow['selector'] = dict()
380 flow['selector']['criteria'] = list()
381
382 # Flow rule matching criteria
383 if not data['ingress_port'] in self.pp2ofi:
384 error_text = 'Error. Port ' + data['ingress_port'] + ' is not present in the switch'
385 self.logger.warning("new_flow " + error_text)
386 raise OpenflowConnUnexpectedResponse(error_text)
387
388 ingress_port_criteria = dict()
389 ingress_port_criteria['type'] = "IN_PORT"
390 ingress_port_criteria['port'] = self.pp2ofi[data['ingress_port']]
391 flow['selector']['criteria'].append(ingress_port_criteria)
392
393 if 'dst_mac' in data:
394 dst_mac_criteria = dict()
395 dst_mac_criteria["type"] = "ETH_DST"
396 dst_mac_criteria["mac"] = data['dst_mac']
397 flow['selector']['criteria'].append(dst_mac_criteria)
398
399 if data.get('vlan_id'):
400 vlan_criteria = dict()
401 vlan_criteria["type"] = "VLAN_VID"
402 vlan_criteria["vlanId"] = int(data['vlan_id'])
403 flow['selector']['criteria'].append(vlan_criteria)
404
405 # Flow rule treatment
406 flow['treatment'] = dict()
407 flow['treatment']['instructions'] = list()
408 flow['treatment']['deferred'] = list()
409
410 for action in data['actions']:
411 new_action = dict()
412 if action[0] == "vlan":
413 new_action['type'] = "L2MODIFICATION"
414 if action[1] is None:
415 new_action['subtype'] = "VLAN_POP"
416 else:
417 new_action['subtype'] = "VLAN_ID"
418 new_action['vlanId'] = int(action[1])
419 elif action[0] == 'out':
420 new_action['type'] = "OUTPUT"
421 if not action[1] in self.pp2ofi:
422 error_msj = 'Port ' + action[1] + ' is not present in the switch'
423 raise OpenflowConnUnexpectedResponse(error_msj)
424 new_action['port'] = self.pp2ofi[action[1]]
425 else:
426 error_msj = "Unknown item '%s' in action list" % action[0]
427 self.logger.error("new_flow " + error_msj)
428 raise OpenflowConnUnexpectedResponse(error_msj)
429
430 flow['treatment']['instructions'].append(new_action)
431
432 self.headers['content-type'] = 'application/json'
433 path = self.url + "flows/" + self.id
434 self.logger.debug("new_flow post: {}".format(flow))
435 of_response = requests.post(path, headers=self.headers, data=json.dumps(flow))
436
437 error_text = "Openflow response {}: {}".format(of_response.status_code, of_response.text)
438 if of_response.status_code != 201:
439 self.logger.warning("new_flow " + error_text)
440 raise OpenflowConnUnexpectedResponse(error_text)
441
442 flowId = of_response.headers['location'][path.__len__() + 1:]
443
444 data['name'] = flowId
445
446 self.logger.debug("new_flow id: {},: {} ".format(flowId, error_text))
447 return None
448
449 except requests.exceptions.RequestException as e:
450 error_text = type(e).__name__ + ": " + str(e)
451 self.logger.error("new_flow " + error_text)
452 raise OpenflowConnConnectionException(error_text)
453
454 def clear_all_flows(self):
455 """
456 Delete all existing rules
457 :return: Raise a openflowconnUnexpectedResponse expection in case of failure
458 """
459 try:
460 rules = self.get_of_rules(True)
461
462 for rule in rules:
463 self.del_flow(rule)
464
465 self.logger.debug("clear_all_flows OK ")
466 return None
467
468 except requests.exceptions.RequestException as e:
469 error_text = type(e).__name__ + ": " + str(e)
470 self.logger.error("clear_all_flows " + error_text)
471 raise OpenflowConnConnectionException(error_text)