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