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