Enabling Flake8 and import sorting
[osm/RO.git] / RO-plugin / osm_ro_plugin / openflow_conn.py
1 ##
2 # Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U.
3 # All Rights Reserved.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License"); you may
6 # not use this file except in compliance with the License. You may obtain
7 # a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 # License for the specific language governing permissions and limitations
15 # under the License.
16 #
17 ##
18
19 from http import HTTPStatus
20 import logging
21 from uuid import uuid4
22
23 from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError
24
25
26 """
27 Implement an Abstract class 'OpenflowConn' and an engine 'SdnConnectorOpenFlow' used for base class for SDN plugings
28 that implements a pro-active opeflow rules.
29 """
30
31 __author__ = "Alfonso Tierno"
32 __date__ = "2019-11-11"
33
34
35 class OpenflowConnException(Exception):
36 """Common and base class Exception for all vimconnector exceptions"""
37
38 def __init__(self, message, http_code=HTTPStatus.BAD_REQUEST.value):
39 Exception.__init__(self, message)
40 self.http_code = http_code
41
42
43 class OpenflowConnConnectionException(OpenflowConnException):
44 """Connectivity error with the VIM"""
45
46 def __init__(self, message, http_code=HTTPStatus.SERVICE_UNAVAILABLE.value):
47 OpenflowConnException.__init__(self, message, http_code)
48
49
50 class OpenflowConnUnexpectedResponse(OpenflowConnException):
51 """Get an wrong response from VIM"""
52
53 def __init__(self, message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR.value):
54 OpenflowConnException.__init__(self, message, http_code)
55
56
57 class OpenflowConnAuthException(OpenflowConnException):
58 """Invalid credentials or authorization to perform this action over the VIM"""
59
60 def __init__(self, message, http_code=HTTPStatus.UNAUTHORIZED.value):
61 OpenflowConnException.__init__(self, message, http_code)
62
63
64 class OpenflowConnNotFoundException(OpenflowConnException):
65 """The item is not found at VIM"""
66
67 def __init__(self, message, http_code=HTTPStatus.NOT_FOUND.value):
68 OpenflowConnException.__init__(self, message, http_code)
69
70
71 class OpenflowConnConflictException(OpenflowConnException):
72 """There is a conflict, e.g. more item found than one"""
73
74 def __init__(self, message, http_code=HTTPStatus.CONFLICT.value):
75 OpenflowConnException.__init__(self, message, http_code)
76
77
78 class OpenflowConnNotSupportedException(OpenflowConnException):
79 """The request is not supported by connector"""
80
81 def __init__(self, message, http_code=HTTPStatus.SERVICE_UNAVAILABLE.value):
82 OpenflowConnException.__init__(self, message, http_code)
83
84
85 class OpenflowConnNotImplemented(OpenflowConnException):
86 """The method is not implemented by the connected"""
87
88 def __init__(self, message, http_code=HTTPStatus.NOT_IMPLEMENTED.value):
89 OpenflowConnException.__init__(self, message, http_code)
90
91
92 class OpenflowConn:
93 """
94 Openflow controller connector abstract implementeation.
95 """
96
97 def __init__(self, params):
98 self.name = "openflow_conector"
99 self.pp2ofi = {} # From Physical Port to OpenFlow Index
100 self.ofi2pp = {} # From OpenFlow Index to Physical Port
101 self.logger = logging.getLogger("ro.sdn.openflow_conn")
102
103 def get_of_switches(self):
104 """
105 Obtain a a list of switches or DPID detected by this controller
106 :return: list length, and a list where each element a tuple pair (DPID, IP address), text_error: if fails
107 """
108 raise OpenflowConnNotImplemented("Should have implemented this")
109
110 def obtain_port_correspondence(self):
111 """
112 Obtain the correspondence between physical and openflow port names
113 :return: dictionary: with physical name as key, openflow name as value, error_text: if fails
114 """
115 raise OpenflowConnNotImplemented("Should have implemented this")
116
117 def get_of_rules(self, translate_of_ports=True):
118 """
119 Obtain the rules inserted at openflow controller
120 :param translate_of_ports: if True it translates ports from openflow index to physical switch name
121 :return: list where each item is a dictionary with the following content:
122 priority: rule priority
123 priority: rule priority
124 name: rule name (present also as the master dict key)
125 ingress_port: match input port of the rule
126 dst_mac: match destination mac address of the rule, can be missing or None if not apply
127 vlan_id: match vlan tag of the rule, can be missing or None if not apply
128 actions: list of actions, composed by a pair tuples:
129 (vlan, None/int): for stripping/setting a vlan tag
130 (out, port): send to this port
131 switch: DPID, all
132 text_error if fails
133 """
134 raise OpenflowConnNotImplemented("Should have implemented this")
135
136 def del_flow(self, flow_name):
137 """
138 Delete all existing rules
139 :param flow_name: flow_name, this is the rule name
140 :return: None if ok, text_error if fails
141 """
142 raise OpenflowConnNotImplemented("Should have implemented this")
143
144 def new_flow(self, data):
145 """
146 Insert a new static rule
147 :param data: dictionary with the following content:
148 priority: rule priority
149 name: rule name
150 ingress_port: match input port of the rule
151 dst_mac: match destination mac address of the rule, missing or None if not apply
152 vlan_id: match vlan tag of the rule, missing or None if not apply
153 actions: list of actions, composed by a pair tuples with these posibilities:
154 ('vlan', None/int): for stripping/setting a vlan tag
155 ('out', port): send to this port
156 :return: None if ok, text_error if fails
157 """
158 raise OpenflowConnNotImplemented("Should have implemented this")
159
160 def clear_all_flows(self):
161 """
162 Delete all existing rules
163 :return: None if ok, text_error if fails
164 """
165 raise OpenflowConnNotImplemented("Should have implemented this")
166
167
168 class SdnConnectorOpenFlow(SdnConnectorBase):
169 """
170 This class is the base engine of SDN plugins base on openflow rules
171 """
172
173 flow_fields = (
174 "priority",
175 "vlan",
176 "ingress_port",
177 "actions",
178 "dst_mac",
179 "src_mac",
180 "net_id",
181 )
182
183 def __init__(self, wim, wim_account, config=None, logger=None, of_connector=None):
184 self.logger = logger or logging.getLogger("ro.sdn.openflow_conn")
185 self.of_connector = of_connector
186 config = config or {}
187 self.of_controller_nets_with_same_vlan = config.get(
188 "of_controller_nets_with_same_vlan", False
189 )
190
191 def check_credentials(self):
192 try:
193 self.openflow_conn.obtain_port_correspondence()
194 except OpenflowConnException as e:
195 raise SdnConnectorError(e, http_code=e.http_code)
196
197 def get_connectivity_service_status(self, service_uuid, conn_info=None):
198 conn_info = conn_info or {}
199 return {
200 "sdn_status": conn_info.get("status", "ERROR"),
201 "error_msg": conn_info.get("error_msg", "Variable conn_info not provided"),
202 }
203 # TODO check rules connectirng to of_connector
204
205 def create_connectivity_service(self, service_type, connection_points, **kwargs):
206 net_id = str(uuid4())
207 ports = []
208
209 for cp in connection_points:
210 port = {
211 "uuid": cp["service_endpoint_id"],
212 "vlan": cp.get("service_endpoint_encapsulation_info", {}).get("vlan"),
213 "mac": cp.get("service_endpoint_encapsulation_info", {}).get("mac"),
214 "switch_port": cp.get("service_endpoint_encapsulation_info", {}).get(
215 "switch_port"
216 ),
217 }
218 ports.append(port)
219
220 try:
221 created_items = self._set_openflow_rules(
222 service_type, net_id, ports, created_items=None
223 )
224
225 return net_id, created_items
226 except (SdnConnectorError, OpenflowConnException) as e:
227 raise SdnConnectorError(e, http_code=e.http_code)
228
229 def delete_connectivity_service(self, service_uuid, conn_info=None):
230 try:
231 service_type = "ELAN"
232 ports = []
233 self._set_openflow_rules(
234 service_type, service_uuid, ports, created_items=conn_info
235 )
236
237 return None
238 except (SdnConnectorError, OpenflowConnException) as e:
239 raise SdnConnectorError(e, http_code=e.http_code)
240
241 def edit_connectivity_service(
242 self, service_uuid, conn_info=None, connection_points=None, **kwargs
243 ):
244 ports = []
245 for cp in connection_points:
246 port = {
247 "uuid": cp["service_endpoint_id"],
248 "vlan": cp.get("service_endpoint_encapsulation_info", {}).get("vlan"),
249 "mac": cp.get("service_endpoint_encapsulation_info", {}).get("mac"),
250 "switch_port": cp.get("service_endpoint_encapsulation_info", {}).get(
251 "switch_port"
252 ),
253 }
254 ports.append(port)
255
256 service_type = "ELAN" # TODO. Store at conn_info for later use
257
258 try:
259 created_items = self._set_openflow_rules(
260 service_type, service_uuid, ports, created_items=conn_info
261 )
262
263 return created_items
264 except (SdnConnectorError, OpenflowConnException) as e:
265 raise SdnConnectorError(e, http_code=e.http_code)
266
267 def clear_all_connectivity_services(self):
268 """Delete all WAN Links corresponding to a WIM"""
269 pass
270
271 def get_all_active_connectivity_services(self):
272 """Provide information about all active connections provisioned by a
273 WIM
274 """
275 pass
276
277 def _set_openflow_rules(self, net_type, net_id, ports, created_items=None):
278 ifaces_nb = len(ports)
279
280 if not created_items:
281 created_items = {
282 "status": None,
283 "error_msg": None,
284 "installed_rules_ids": [],
285 }
286 rules_to_delete = created_items.get("installed_rules_ids") or []
287 new_installed_rules_ids = []
288 error_list = []
289
290 try:
291 step = "Checking ports and network type compatibility"
292 if ifaces_nb < 2:
293 pass
294 elif net_type == "ELINE":
295 if ifaces_nb > 2:
296 raise SdnConnectorError(
297 "'ELINE' type network cannot connect {} interfaces, only 2".format(
298 ifaces_nb
299 )
300 )
301 elif net_type == "ELAN":
302 if ifaces_nb > 2 and self.of_controller_nets_with_same_vlan:
303 # check all ports are VLAN (tagged) or none
304 vlan_tags = []
305
306 for port in ports:
307 if port["vlan"] not in vlan_tags:
308 vlan_tags.append(port["vlan"])
309
310 if len(vlan_tags) > 1:
311 raise SdnConnectorError(
312 "This pluging cannot connect ports with diferent VLAN tags when flag "
313 "'of_controller_nets_with_same_vlan' is active"
314 )
315 else:
316 raise SdnConnectorError(
317 "Only ELINE or ELAN network types are supported for openflow"
318 )
319
320 # Get the existing flows at openflow controller
321 step = "Getting installed openflow rules"
322 existing_flows = self.of_connector.get_of_rules()
323 existing_flows_ids = [flow["name"] for flow in existing_flows]
324
325 # calculate new flows to be inserted
326 step = "Compute needed openflow rules"
327 new_flows = self._compute_net_flows(net_id, ports)
328
329 name_index = 0
330 for flow in new_flows:
331 # 1 check if an equal flow is already present
332 index = self._check_flow_already_present(flow, existing_flows)
333
334 if index >= 0:
335 flow_id = existing_flows[index]["name"]
336 self.logger.debug("Skipping already present flow %s", str(flow))
337 else:
338 # 2 look for a non used name
339 flow_name = flow["net_id"] + "." + str(name_index)
340
341 while flow_name in existing_flows_ids:
342 name_index += 1
343 flow_name = flow["net_id"] + "." + str(name_index)
344
345 flow["name"] = flow_name
346
347 # 3 insert at openflow
348 try:
349 self.of_connector.new_flow(flow)
350 flow_id = flow["name"]
351 existing_flows_ids.append(flow_id)
352 except OpenflowConnException as e:
353 flow_id = None
354 error_list.append(
355 "Cannot create rule for ingress_port={}, dst_mac={}: {}".format(
356 flow["ingress_port"], flow["dst_mac"], e
357 )
358 )
359
360 # 4 insert at database
361 if flow_id:
362 new_installed_rules_ids.append(flow_id)
363 if flow_id in rules_to_delete:
364 rules_to_delete.remove(flow_id)
365
366 # delete not needed old flows from openflow
367 for flow_id in rules_to_delete:
368 # Delete flow
369 try:
370 self.of_connector.del_flow(flow_id)
371 except OpenflowConnNotFoundException:
372 pass
373 except OpenflowConnException as e:
374 error_text = "Cannot remove rule '{}': {}".format(flow_id, e)
375 error_list.append(error_text)
376 self.logger.error(error_text)
377
378 created_items["installed_rules_ids"] = new_installed_rules_ids
379
380 if error_list:
381 created_items["error_msg"] = ";".join(error_list)[:1000]
382 created_items["error_msg"] = "ERROR"
383 else:
384 created_items["error_msg"] = None
385 created_items["status"] = "ACTIVE"
386
387 return created_items
388 except (SdnConnectorError, OpenflowConnException) as e:
389 raise SdnConnectorError("Error while {}: {}".format(step, e)) from e
390 except Exception as e:
391 error_text = "Error while {}: {}".format(step, e)
392 self.logger.critical(error_text, exc_info=True)
393 raise SdnConnectorError(error_text)
394
395 def _compute_net_flows(self, net_id, ports):
396 new_flows = []
397 new_broadcast_flows = {}
398 nb_ports = len(ports)
399
400 # Check switch_port information is right
401 for port in ports:
402 nb_ports += 1
403
404 if str(port["switch_port"]) not in self.of_connector.pp2ofi:
405 raise SdnConnectorError(
406 "switch port name '{}' is not valid for the openflow controller".format(
407 port["switch_port"]
408 )
409 )
410
411 priority = 1000 # 1100
412
413 for src_port in ports:
414 # if src_port.get("groups")
415 vlan_in = src_port["vlan"]
416
417 # BROADCAST:
418 broadcast_key = src_port["uuid"] + "." + str(vlan_in)
419 if broadcast_key in new_broadcast_flows:
420 flow_broadcast = new_broadcast_flows[broadcast_key]
421 else:
422 flow_broadcast = {
423 "priority": priority,
424 "net_id": net_id,
425 "dst_mac": "ff:ff:ff:ff:ff:ff",
426 "ingress_port": str(src_port["switch_port"]),
427 "vlan_id": vlan_in,
428 "actions": [],
429 }
430 new_broadcast_flows[broadcast_key] = flow_broadcast
431
432 if vlan_in is not None:
433 flow_broadcast["vlan_id"] = str(vlan_in)
434
435 for dst_port in ports:
436 vlan_out = dst_port["vlan"]
437
438 if (
439 src_port["switch_port"] == dst_port["switch_port"]
440 and vlan_in == vlan_out
441 ):
442 continue
443
444 flow = {
445 "priority": priority,
446 "net_id": net_id,
447 "ingress_port": str(src_port["switch_port"]),
448 "vlan_id": vlan_in,
449 "actions": [],
450 }
451
452 # allow that one port have no mac
453 # point to point or nets with 2 elements
454 if dst_port["mac"] is None or nb_ports == 2:
455 flow["priority"] = priority - 5 # less priority
456 else:
457 flow["dst_mac"] = str(dst_port["mac"])
458
459 if vlan_out is None:
460 if vlan_in:
461 flow["actions"].append(("vlan", None))
462 else:
463 flow["actions"].append(("vlan", vlan_out))
464
465 flow["actions"].append(("out", str(dst_port["switch_port"])))
466
467 if self._check_flow_already_present(flow, new_flows) >= 0:
468 self.logger.debug("Skipping repeated flow '%s'", str(flow))
469 continue
470
471 new_flows.append(flow)
472
473 # BROADCAST:
474 # point to multipoint or nets with more than 2 elements
475 if nb_ports <= 2:
476 continue
477
478 out = (vlan_out, str(dst_port["switch_port"]))
479
480 if out not in flow_broadcast["actions"]:
481 flow_broadcast["actions"].append(out)
482
483 # BROADCAST
484 for flow_broadcast in new_broadcast_flows.values():
485 if len(flow_broadcast["actions"]) == 0:
486 continue # nothing to do, skip
487
488 flow_broadcast["actions"].sort()
489
490 if "vlan_id" in flow_broadcast:
491 # indicates that a packet contains a vlan, and the vlan
492 previous_vlan = 0
493 else:
494 previous_vlan = None
495
496 final_actions = []
497 action_number = 0
498
499 for action in flow_broadcast["actions"]:
500 if action[0] != previous_vlan:
501 final_actions.append(("vlan", action[0]))
502 previous_vlan = action[0]
503
504 if self.of_controller_nets_with_same_vlan and action_number:
505 raise SdnConnectorError(
506 "Cannot interconnect different vlan tags in a network when flag "
507 "'of_controller_nets_with_same_vlan' is True."
508 )
509
510 action_number += 1
511 final_actions.append(("out", action[1]))
512 flow_broadcast["actions"] = final_actions
513
514 if self._check_flow_already_present(flow_broadcast, new_flows) >= 0:
515 self.logger.debug("Skipping repeated flow '%s'", str(flow_broadcast))
516 continue
517
518 new_flows.append(flow_broadcast)
519
520 # UNIFY openflow rules with the same input port and vlan and the same output actions
521 # These flows differ at the dst_mac; and they are unified by not filtering by dst_mac
522 # this can happen if there is only two ports. It is converted to a point to point connection
523 # use as key vlan_id+ingress_port and as value the list of flows matching these values
524 flow_dict = {}
525 for flow in new_flows:
526 key = str(flow.get("vlan_id")) + ":" + flow["ingress_port"]
527
528 if key in flow_dict:
529 flow_dict[key].append(flow)
530 else:
531 flow_dict[key] = [flow]
532
533 new_flows2 = []
534
535 for flow_list in flow_dict.values():
536 convert2ptp = False
537
538 if len(flow_list) >= 2:
539 convert2ptp = True
540
541 for f in flow_list:
542 if f["actions"] != flow_list[0]["actions"]:
543 convert2ptp = False
544 break
545
546 if convert2ptp: # add only one unified rule without dst_mac
547 self.logger.debug(
548 "Convert flow rules to NON mac dst_address " + str(flow_list)
549 )
550 flow_list[0].pop("dst_mac")
551 flow_list[0]["priority"] -= 5
552 new_flows2.append(flow_list[0])
553 else: # add all the rules
554 new_flows2 += flow_list
555
556 return new_flows2
557
558 def _check_flow_already_present(self, new_flow, flow_list):
559 """check if the same flow is already present in the flow list
560 The flow is repeated if all the fields, apart from name, are equal
561 Return the index of matching flow, -1 if not match
562 """
563 for index, flow in enumerate(flow_list):
564 for f in self.flow_fields:
565 if flow.get(f) != new_flow.get(f):
566 break
567 else:
568 return index
569
570 return -1