cba505cb9855cbcc77dd61ea63dacf95bbe4ddbe
[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 (
36 OpenflowConn,
37 OpenflowConnConnectionException,
38 OpenflowConnUnexpectedResponse,
39 )
40
41 # OpenflowConnException, OpenflowConnAuthException, OpenflowConnNotFoundException, \
42 # OpenflowConnConflictException, OpenflowConnNotSupportedException, OpenflowConnNotImplemented
43
44 __author__ = "Alaitz Mendiola"
45 __date__ = "$22-nov-2016$"
46
47
48 class OfConnOnos(OpenflowConn):
49 """
50 ONOS connector. No MAC learning is used
51 """
52
53 def __init__(self, params):
54 """Constructor.
55 :param params: dictionary with the following keys:
56 of_dpid: DPID to use for this controller ?? Does a controller have a dpid?
57 of_url: must be [http://HOST:PORT/]
58 of_user: user credentials, can be missing or None
59 of_password: password credentials
60 of_debug: debug level for logging. Default to ERROR
61 other keys are ignored
62 Raise an exception if same parameter is missing or wrong
63 """
64 OpenflowConn.__init__(self, params)
65
66 # check params
67 url = params.get("of_url")
68
69 if not url:
70 raise ValueError("'url' must be provided")
71
72 if not url.startswith("http"):
73 url = "http://" + url
74
75 if not url.endswith("/"):
76 url = url + "/"
77
78 self.url = url + "onos/v1/"
79
80 # internal variables
81 self.name = "onosof"
82 self.headers = {
83 "content-type": "application/json",
84 "accept": "application/json",
85 }
86
87 self.auth = "None"
88 self.pp2ofi = {} # From Physical Port to OpenFlow Index
89 self.ofi2pp = {} # From OpenFlow Index to Physical Port
90
91 self.dpid = str(params["of_dpid"])
92 self.id = "of:" + str(self.dpid.replace(":", ""))
93
94 # TODO This may not be straightforward
95 if params.get("of_user"):
96 of_password = params.get("of_password", "")
97 self.auth = base64.b64encode(
98 bytes(params["of_user"] + ":" + of_password, "utf-8")
99 )
100 self.auth = self.auth.decode()
101 self.headers["authorization"] = "Basic " + self.auth
102
103 self.logger = logging.getLogger("ro.sdn.onosof")
104 # self.logger.setLevel( getattr(logging, params.get("of_debug", "ERROR")) )
105 self.logger.debug("onosof plugin initialized")
106 self.ip_address = None
107
108 def get_of_switches(self):
109 """
110 Obtain a a list of switches or DPID detected by this controller
111 :return: list where each element a tuple pair (DPID, IP address)
112 Raise a openflowconnUnexpectedResponse expection in case of failure
113 """
114 try:
115 self.headers["content-type"] = "text/plain"
116 of_response = requests.get(self.url + "devices", headers=self.headers)
117 error_text = "Openflow response %d: %s" % (
118 of_response.status_code,
119 of_response.text,
120 )
121
122 if of_response.status_code != 200:
123 self.logger.warning("get_of_switches " + error_text)
124
125 raise OpenflowConnUnexpectedResponse(error_text)
126
127 self.logger.debug("get_of_switches " + error_text)
128 info = of_response.json()
129
130 if type(info) != dict:
131 self.logger.error(
132 "get_of_switches. Unexpected response, not a dict: %s", str(info)
133 )
134
135 raise OpenflowConnUnexpectedResponse(
136 "Unexpected response, not a dict. Wrong version?"
137 )
138
139 node_list = info.get("devices")
140
141 if type(node_list) is not list:
142 self.logger.error(
143 "get_of_switches. Unexpected response, at 'devices', not found or not a list: %s",
144 str(type(node_list)),
145 )
146
147 raise OpenflowConnUnexpectedResponse(
148 "Unexpected response, at 'devices', not found "
149 "or not a list. Wrong version?"
150 )
151
152 switch_list = []
153 for node in node_list:
154 node_id = node.get("id")
155 if node_id is None:
156 self.logger.error(
157 "get_of_switches. Unexpected response at 'device':'id', not found: %s",
158 str(node),
159 )
160
161 raise OpenflowConnUnexpectedResponse(
162 "Unexpected response at 'device':'id', "
163 "not found . Wrong version?"
164 )
165
166 node_ip_address = node.get("annotations").get("managementAddress")
167 if node_ip_address is None:
168 self.logger.error(
169 "get_of_switches. Unexpected response at 'device':'managementAddress', not found: %s",
170 str(node),
171 )
172
173 raise OpenflowConnUnexpectedResponse(
174 "Unexpected response at 'device':'managementAddress', not found. Wrong version?"
175 )
176
177 node_id_hex = hex(int(node_id.split(":")[1])).split("x")[1].zfill(16)
178 switch_list.append(
179 (
180 ":".join(
181 a + b for a, b in zip(node_id_hex[::2], node_id_hex[1::2])
182 ),
183 node_ip_address,
184 )
185 )
186
187 return switch_list
188 except requests.exceptions.RequestException as e:
189 error_text = type(e).__name__ + ": " + str(e)
190 self.logger.error("get_of_switches " + error_text)
191
192 raise OpenflowConnConnectionException(error_text)
193 except ValueError as e:
194 # ValueError in the case that JSON can not be decoded
195 error_text = type(e).__name__ + ": " + str(e)
196 self.logger.error("get_of_switches " + error_text)
197
198 raise OpenflowConnUnexpectedResponse(error_text)
199
200 def obtain_port_correspondence(self):
201 """
202 Obtain the correspondence between physical and openflow port names
203 :return: dictionary with physical name as key, openflow name as value
204 Raise a openflowconnUnexpectedResponse expection in case of failure
205 """
206 try:
207 self.headers["content-type"] = "text/plain"
208 of_response = requests.get(
209 self.url + "devices/" + self.id + "/ports", headers=self.headers
210 )
211 error_text = "Openflow response {}: {}".format(
212 of_response.status_code, of_response.text
213 )
214
215 if of_response.status_code != 200:
216 self.logger.warning("obtain_port_correspondence " + error_text)
217
218 raise OpenflowConnUnexpectedResponse(error_text)
219
220 self.logger.debug("obtain_port_correspondence " + error_text)
221 info = of_response.json()
222
223 node_connector_list = info.get("ports")
224 if type(node_connector_list) is not list:
225 self.logger.error(
226 "obtain_port_correspondence. Unexpected response at 'ports', not found or not a list: %s",
227 str(node_connector_list),
228 )
229
230 raise OpenflowConnUnexpectedResponse(
231 "Unexpected response at 'ports', not found or not "
232 "a list. Wrong version?"
233 )
234
235 for node_connector in node_connector_list:
236 if node_connector["port"] != "local":
237 self.pp2ofi[str(node_connector["annotations"]["portName"])] = str(
238 node_connector["port"]
239 )
240 self.ofi2pp[str(node_connector["port"])] = str(
241 node_connector["annotations"]["portName"]
242 )
243
244 node_ip_address = info["annotations"]["managementAddress"]
245 if node_ip_address is None:
246 self.logger.error(
247 "obtain_port_correspondence. Unexpected response at 'managementAddress', not found: %s",
248 str(self.id),
249 )
250
251 raise OpenflowConnUnexpectedResponse(
252 "Unexpected response at 'managementAddress', "
253 "not found. Wrong version?"
254 )
255
256 self.ip_address = node_ip_address
257
258 # print self.name, ": obtain_port_correspondence ports:", self.pp2ofi
259 return self.pp2ofi
260 except requests.exceptions.RequestException as e:
261 error_text = type(e).__name__ + ": " + str(e)
262 self.logger.error("obtain_port_correspondence " + error_text)
263
264 raise OpenflowConnConnectionException(error_text)
265 except ValueError as e:
266 # ValueError in the case that JSON can not be decoded
267 error_text = type(e).__name__ + ": " + str(e)
268 self.logger.error("obtain_port_correspondence " + error_text)
269
270 raise OpenflowConnUnexpectedResponse(error_text)
271
272 def get_of_rules(self, translate_of_ports=True):
273 """
274 Obtain the rules inserted at openflow controller
275 :param translate_of_ports: if True it translates ports from openflow index to physical switch name
276 :return: list where each item is a dictionary with the following content:
277 priority: rule priority
278 name: rule name (present also as the master dict key)
279 ingress_port: match input port of the rule
280 dst_mac: match destination mac address of the rule, can be missing or None if not apply
281 vlan_id: match vlan tag of the rule, can be missing or None if not apply
282 actions: list of actions, composed by a pair tuples:
283 (vlan, None/int): for stripping/setting a vlan tag
284 (out, port): send to this port
285 switch: DPID, all
286 Raise a openflowconnUnexpectedResponse exception in case of failure
287 """
288 try:
289 if len(self.ofi2pp) == 0:
290 self.obtain_port_correspondence()
291
292 # get rules
293 self.headers["content-type"] = "text/plain"
294 of_response = requests.get(
295 self.url + "flows/" + self.id, headers=self.headers
296 )
297 error_text = "Openflow response %d: %s" % (
298 of_response.status_code,
299 of_response.text,
300 )
301
302 # The configured page does not exist if there are no rules installed. In that case we return an empty dict
303 if of_response.status_code == 404:
304 return []
305 elif of_response.status_code != 200:
306 self.logger.warning("get_of_rules " + error_text)
307
308 raise OpenflowConnUnexpectedResponse(error_text)
309
310 self.logger.debug("get_of_rules " + error_text)
311
312 info = of_response.json()
313
314 if type(info) != dict:
315 self.logger.error(
316 "get_of_rules. Unexpected response, not a dict: %s",
317 str(info),
318 )
319
320 raise OpenflowConnUnexpectedResponse(
321 "Unexpected openflow response, not a dict. Wrong version?"
322 )
323
324 flow_list = info.get("flows")
325
326 if flow_list is None:
327 return []
328
329 if type(flow_list) is not list:
330 self.logger.error(
331 "get_of_rules. Unexpected response at 'flows', not a list: %s",
332 str(type(flow_list)),
333 )
334
335 raise OpenflowConnUnexpectedResponse(
336 "Unexpected response at 'flows', not a list. Wrong version?"
337 )
338
339 rules = [] # Response list
340 for flow in flow_list:
341 if not (
342 "id" in flow
343 and "selector" in flow
344 and "treatment" in flow
345 and "instructions" in flow["treatment"]
346 and "criteria" in flow["selector"]
347 ):
348 raise OpenflowConnUnexpectedResponse(
349 "unexpected openflow response, one or more "
350 "elements are missing. Wrong version?"
351 )
352
353 rule = dict()
354 rule["switch"] = self.dpid
355 rule["priority"] = flow.get("priority")
356 rule["name"] = flow["id"]
357
358 for criteria in flow["selector"]["criteria"]:
359 if criteria["type"] == "IN_PORT":
360 in_port = str(criteria["port"])
361 if in_port != "CONTROLLER":
362 if in_port not in self.ofi2pp:
363 raise OpenflowConnUnexpectedResponse(
364 "Error: Ingress port {} is not "
365 "in switch port list".format(in_port)
366 )
367
368 if translate_of_ports:
369 in_port = self.ofi2pp[in_port]
370
371 rule["ingress_port"] = in_port
372 elif criteria["type"] == "VLAN_VID":
373 rule["vlan_id"] = criteria["vlanId"]
374 elif criteria["type"] == "ETH_DST":
375 rule["dst_mac"] = str(criteria["mac"]).lower()
376
377 actions = []
378 for instruction in flow["treatment"]["instructions"]:
379 if instruction["type"] == "OUTPUT":
380 out_port = str(instruction["port"])
381 if out_port != "CONTROLLER":
382 if out_port not in self.ofi2pp:
383 raise OpenflowConnUnexpectedResponse(
384 "Error: Output port {} is not in "
385 "switch port list".format(out_port)
386 )
387
388 if translate_of_ports:
389 out_port = self.ofi2pp[out_port]
390
391 actions.append(("out", out_port))
392
393 if (
394 instruction["type"] == "L2MODIFICATION"
395 and instruction["subtype"] == "VLAN_POP"
396 ):
397 actions.append(("vlan", "None"))
398
399 if (
400 instruction["type"] == "L2MODIFICATION"
401 and instruction["subtype"] == "VLAN_ID"
402 ):
403 actions.append(("vlan", instruction["vlanId"]))
404
405 rule["actions"] = actions
406 rules.append(rule)
407
408 return rules
409 except requests.exceptions.RequestException as e:
410 # ValueError in the case that JSON can not be decoded
411 error_text = type(e).__name__ + ": " + str(e)
412 self.logger.error("get_of_rules " + error_text)
413
414 raise OpenflowConnConnectionException(error_text)
415 except ValueError as e:
416 # ValueError in the case that JSON can not be decoded
417 error_text = type(e).__name__ + ": " + str(e)
418 self.logger.error("get_of_rules " + error_text)
419
420 raise OpenflowConnUnexpectedResponse(error_text)
421
422 def del_flow(self, flow_name):
423 """
424 Delete an existing rule
425 :param flow_name:
426 :return: Raise a openflowconnUnexpectedResponse expection in case of failure
427 """
428 try:
429 self.logger.debug("del_flow: delete flow name {}".format(flow_name))
430 self.headers["content-type"] = None
431 of_response = requests.delete(
432 self.url + "flows/" + self.id + "/" + flow_name, headers=self.headers
433 )
434 error_text = "Openflow response {}: {}".format(
435 of_response.status_code, of_response.text
436 )
437
438 if of_response.status_code != 204:
439 self.logger.warning("del_flow " + error_text)
440
441 raise OpenflowConnUnexpectedResponse(error_text)
442
443 self.logger.debug("del_flow: {} OK,: {} ".format(flow_name, error_text))
444
445 return None
446 except requests.exceptions.RequestException as e:
447 error_text = type(e).__name__ + ": " + str(e)
448 self.logger.error("del_flow " + error_text)
449
450 raise OpenflowConnConnectionException(error_text)
451
452 def new_flow(self, data):
453 """
454 Insert a new static rule
455 :param data: dictionary with the following content:
456 priority: rule priority
457 name: rule name
458 ingress_port: match input port of the rule
459 dst_mac: match destination mac address of the rule, missing or None if not apply
460 vlan_id: match vlan tag of the rule, missing or None if not apply
461 actions: list of actions, composed by a pair tuples with these posibilities:
462 ('vlan', None/int): for stripping/setting a vlan tag
463 ('out', port): send to this port
464 :return: Raise a openflowconnUnexpectedResponse exception in case of failure
465 """
466 try:
467 self.logger.debug("new_flow data: {}".format(data))
468
469 if len(self.pp2ofi) == 0:
470 self.obtain_port_correspondence()
471
472 # Build the dictionary with the flow rule information for ONOS
473 flow = dict()
474 # flow["id"] = data["name"]
475 flow["tableId"] = 0
476 flow["priority"] = data.get("priority")
477 flow["timeout"] = 0
478 flow["isPermanent"] = "true"
479 flow["appId"] = 10 # FIXME We should create an appId for OSM
480 flow["selector"] = dict()
481 flow["selector"]["criteria"] = list()
482
483 # Flow rule matching criteria
484 if not data["ingress_port"] in self.pp2ofi:
485 error_text = (
486 "Error. Port "
487 + data["ingress_port"]
488 + " is not present in the switch"
489 )
490 self.logger.warning("new_flow " + error_text)
491
492 raise OpenflowConnUnexpectedResponse(error_text)
493
494 ingress_port_criteria = dict()
495 ingress_port_criteria["type"] = "IN_PORT"
496 ingress_port_criteria["port"] = self.pp2ofi[data["ingress_port"]]
497 flow["selector"]["criteria"].append(ingress_port_criteria)
498
499 if "dst_mac" in data:
500 dst_mac_criteria = dict()
501 dst_mac_criteria["type"] = "ETH_DST"
502 dst_mac_criteria["mac"] = data["dst_mac"]
503 flow["selector"]["criteria"].append(dst_mac_criteria)
504
505 if data.get("vlan_id"):
506 vlan_criteria = dict()
507 vlan_criteria["type"] = "VLAN_VID"
508 vlan_criteria["vlanId"] = int(data["vlan_id"])
509 flow["selector"]["criteria"].append(vlan_criteria)
510
511 # Flow rule treatment
512 flow["treatment"] = dict()
513 flow["treatment"]["instructions"] = list()
514 flow["treatment"]["deferred"] = list()
515
516 for action in data["actions"]:
517 new_action = dict()
518 if action[0] == "vlan":
519 new_action["type"] = "L2MODIFICATION"
520
521 if action[1] is None:
522 new_action["subtype"] = "VLAN_POP"
523 else:
524 new_action["subtype"] = "VLAN_ID"
525 new_action["vlanId"] = int(action[1])
526 elif action[0] == "out":
527 new_action["type"] = "OUTPUT"
528
529 if not action[1] in self.pp2ofi:
530 error_msj = (
531 "Port " + action[1] + " is not present in the switch"
532 )
533
534 raise OpenflowConnUnexpectedResponse(error_msj)
535
536 new_action["port"] = self.pp2ofi[action[1]]
537 else:
538 error_msj = "Unknown item '%s' in action list" % action[0]
539 self.logger.error("new_flow " + error_msj)
540
541 raise OpenflowConnUnexpectedResponse(error_msj)
542
543 flow["treatment"]["instructions"].append(new_action)
544
545 self.headers["content-type"] = "application/json"
546 path = self.url + "flows/" + self.id
547 self.logger.debug("new_flow post: {}".format(flow))
548 of_response = requests.post(
549 path, headers=self.headers, data=json.dumps(flow)
550 )
551
552 error_text = "Openflow response {}: {}".format(
553 of_response.status_code, of_response.text
554 )
555 if of_response.status_code != 201:
556 self.logger.warning("new_flow " + error_text)
557
558 raise OpenflowConnUnexpectedResponse(error_text)
559
560 flowId = of_response.headers["location"][path.__len__() + 1 :]
561 data["name"] = flowId
562
563 self.logger.debug("new_flow id: {},: {} ".format(flowId, error_text))
564
565 return None
566 except requests.exceptions.RequestException as e:
567 error_text = type(e).__name__ + ": " + str(e)
568 self.logger.error("new_flow " + error_text)
569
570 raise OpenflowConnConnectionException(error_text)
571
572 def clear_all_flows(self):
573 """
574 Delete all existing rules
575 :return: Raise a openflowconnUnexpectedResponse expection in case of failure
576 """
577 try:
578 rules = self.get_of_rules(True)
579
580 for rule in rules:
581 self.del_flow(rule)
582
583 self.logger.debug("clear_all_flows OK ")
584
585 return None
586 except requests.exceptions.RequestException as e:
587 error_text = type(e).__name__ + ": " + str(e)
588 self.logger.error("clear_all_flows " + error_text)
589
590 raise OpenflowConnConnectionException(error_text)