2 # Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U.
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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
19 from http
import HTTPStatus
20 from osm_ro
.wim
.sdnconn
import SdnConnectorBase
, SdnConnectorError
21 from uuid
import uuid4
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.
28 __author__
= "Alfonso Tierno"
29 __date__
= "2019-11-11"
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
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
)
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
)
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
)
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
)
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
)
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
)
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
)
83 Openflow controller connector abstract implementeation.
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')
91 def get_of_switches(self
):
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
96 raise OpenflowConnNotImplemented("Should have implemented this")
98 def obtain_port_correspondence(self
):
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
103 raise OpenflowConnNotImplemented("Should have implemented this")
105 def get_of_rules(self
, translate_of_ports
=True):
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
122 raise OpenflowConnNotImplemented("Should have implemented this")
124 def del_flow(self
, flow_name
):
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
130 raise OpenflowConnNotImplemented("Should have implemented this")
132 def new_flow(self
, data
):
134 Insert a new static rule
135 :param data: dictionary with the following content:
136 priority: rule priority
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
146 raise OpenflowConnNotImplemented("Should have implemented this")
148 def clear_all_flows(self
):
150 Delete all existing rules
151 :return: None if ok, text_error if fails
153 raise OpenflowConnNotImplemented("Should have implemented this")
156 class SdnConnectorOpenFlow(SdnConnectorBase
):
158 This class is the base engine of SDN plugins base on openflow rules
160 flow_fields
= ('priority', 'vlan', 'ingress_port', 'actions', 'dst_mac', 'src_mac', 'net_id')
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 self
.of_controller_nets_with_same_vlan
= config
.get("of_controller_nets_with_same_vlan", False)
167 def check_credentials(self
):
169 self
.openflow_conn
.obtain_port_correspondence()
170 except OpenflowConnException
as e
:
171 raise SdnConnectorError(e
, http_code
=e
.http_code
)
173 def get_connectivity_service_status(self
, service_uuid
, conn_info
=None):
174 conn_info
= conn_info
or {}
176 "sdn_status": conn_info
.get("status", "ERROR"),
177 "error_msg": conn_info
.get("error_msg", "Variable conn_info not provided"),
179 # TODO check rules connectirng to of_connector
181 def create_connectivity_service(self
, service_type
, connection_points
, **kwargs
):
182 net_id
= str(uuid4())
184 for cp
in connection_points
:
186 "uuid": cp
["service_endpoint_id"],
187 "vlan": cp
.get("service_endpoint_encapsulation_info", {}).get("vlan"),
188 "mac": cp
.get("service_endpoint_encapsulation_info", {}).get("mac"),
189 "switch_port": cp
.get("service_endpoint_encapsulation_info", {}).get("switch_port"),
193 created_items
= self
._set
_openflow
_rules
(service_type
, net_id
, ports
, created_items
=None)
194 return net_id
, created_items
195 except (SdnConnectorError
, OpenflowConnException
) as e
:
196 raise SdnConnectorError(e
, http_code
=e
.http_code
)
198 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
200 service_type
= "ELAN"
202 self
._set
_openflow
_rules
(service_type
, service_uuid
, ports
, created_items
=conn_info
)
204 except (SdnConnectorError
, OpenflowConnException
) as e
:
205 raise SdnConnectorError(e
, http_code
=e
.http_code
)
207 def edit_connectivity_service(self
, service_uuid
, conn_info
=None, connection_points
=None, **kwargs
):
209 for cp
in connection_points
:
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("switch_port"),
217 service_type
= "ELAN" # TODO. Store at conn_info for later use
219 created_items
= self
._set
_openflow
_rules
(service_type
, service_uuid
, ports
, created_items
=conn_info
)
221 except (SdnConnectorError
, OpenflowConnException
) as e
:
222 raise SdnConnectorError(e
, http_code
=e
.http_code
)
224 def clear_all_connectivity_services(self
):
225 """Delete all WAN Links corresponding to a WIM"""
228 def get_all_active_connectivity_services(self
):
229 """Provide information about all active connections provisioned by a
234 def _set_openflow_rules(self
, net_type
, net_id
, ports
, created_items
=None):
235 ifaces_nb
= len(ports
)
236 if not created_items
:
237 created_items
= {"status": None, "error_msg": None, "installed_rules_ids": []}
238 rules_to_delete
= created_items
.get("installed_rules_ids") or []
239 new_installed_rules_ids
= []
243 step
= "Checking ports and network type compatibility"
246 elif net_type
== 'ELINE':
248 raise SdnConnectorError("'ELINE' type network cannot connect {} interfaces, only 2".format(
250 elif net_type
== 'ELAN':
251 if ifaces_nb
> 2 and self
.of_controller_nets_with_same_vlan
:
252 # check all ports are VLAN (tagged) or none
255 if port
["vlan"] not in vlan_tags
:
256 vlan_tags
.append(port
["vlan"])
257 if len(vlan_tags
) > 1:
258 raise SdnConnectorError("This pluging cannot connect ports with diferent VLAN tags when flag "
259 "'of_controller_nets_with_same_vlan' is active")
261 raise SdnConnectorError('Only ELINE or ELAN network types are supported for openflow')
263 # Get the existing flows at openflow controller
264 step
= "Getting installed openflow rules"
265 existing_flows
= self
.of_connector
.get_of_rules()
266 existing_flows_ids
= [flow
["name"] for flow
in existing_flows
]
268 # calculate new flows to be inserted
269 step
= "Compute needed openflow rules"
270 new_flows
= self
._compute
_net
_flows
(net_id
, ports
)
273 for flow
in new_flows
:
274 # 1 check if an equal flow is already present
275 index
= self
._check
_flow
_already
_present
(flow
, existing_flows
)
277 flow_id
= existing_flows
[index
]["name"]
278 self
.logger
.debug("Skipping already present flow %s", str(flow
))
280 # 2 look for a non used name
281 flow_name
= flow
["net_id"] + "." + str(name_index
)
282 while flow_name
in existing_flows_ids
:
284 flow_name
= flow
["net_id"] + "." + str(name_index
)
285 flow
['name'] = flow_name
286 # 3 insert at openflow
288 self
.of_connector
.new_flow(flow
)
289 flow_id
= flow
["name"]
290 existing_flows_ids
.append(flow_id
)
291 except OpenflowConnException
as e
:
293 error_list
.append("Cannot create rule for ingress_port={}, dst_mac={}: {}"
294 .format(flow
["ingress_port"], flow
["dst_mac"], e
))
296 # 4 insert at database
298 new_installed_rules_ids
.append(flow_id
)
299 if flow_id
in rules_to_delete
:
300 rules_to_delete
.remove(flow_id
)
302 # delete not needed old flows from openflow
303 for flow_id
in rules_to_delete
:
306 self
.of_connector
.del_flow(flow_id
)
307 except OpenflowConnNotFoundException
:
309 except OpenflowConnException
as e
:
310 error_text
= "Cannot remove rule '{}': {}".format(flow_id
, e
)
311 error_list
.append(error_text
)
312 self
.logger
.error(error_text
)
313 created_items
["installed_rules_ids"] = new_installed_rules_ids
315 created_items
["error_msg"] = ";".join(error_list
)[:1000]
316 created_items
["error_msg"] = "ERROR"
318 created_items
["error_msg"] = None
319 created_items
["status"] = "ACTIVE"
321 except (SdnConnectorError
, OpenflowConnException
) as e
:
322 raise SdnConnectorError("Error while {}: {}".format(step
, e
)) from e
323 except Exception as e
:
324 error_text
= "Error while {}: {}".format(step
, e
)
325 self
.logger
.critical(error_text
, exc_info
=True)
326 raise SdnConnectorError(error_text
)
328 def _compute_net_flows(self
, net_id
, ports
):
330 new_broadcast_flows
= {}
331 nb_ports
= len(ports
)
333 # Check switch_port information is right
336 if str(port
['switch_port']) not in self
.of_connector
.pp2ofi
:
337 raise SdnConnectorError("switch port name '{}' is not valid for the openflow controller".
338 format(port
['switch_port']))
339 priority
= 1000 # 1100
341 for src_port
in ports
:
342 # if src_port.get("groups")
343 vlan_in
= src_port
['vlan']
346 broadcast_key
= src_port
['uuid'] + "." + str(vlan_in
)
347 if broadcast_key
in new_broadcast_flows
:
348 flow_broadcast
= new_broadcast_flows
[broadcast_key
]
350 flow_broadcast
= {'priority': priority
,
352 'dst_mac': 'ff:ff:ff:ff:ff:ff',
353 "ingress_port": str(src_port
['switch_port']),
357 new_broadcast_flows
[broadcast_key
] = flow_broadcast
358 if vlan_in
is not None:
359 flow_broadcast
['vlan_id'] = str(vlan_in
)
361 for dst_port
in ports
:
362 vlan_out
= dst_port
['vlan']
363 if src_port
['switch_port'] == dst_port
['switch_port'] and vlan_in
== vlan_out
:
366 "priority": priority
,
368 "ingress_port": str(src_port
['switch_port']),
372 # allow that one port have no mac
373 if dst_port
['mac'] is None or nb_ports
== 2: # point to point or nets with 2 elements
374 flow
['priority'] = priority
- 5 # less priority
376 flow
['dst_mac'] = str(dst_port
['mac'])
380 flow
['actions'].append(('vlan', None))
382 flow
['actions'].append(('vlan', vlan_out
))
383 flow
['actions'].append(('out', str(dst_port
['switch_port'])))
385 if self
._check
_flow
_already
_present
(flow
, new_flows
) >= 0:
386 self
.logger
.debug("Skipping repeated flow '%s'", str(flow
))
389 new_flows
.append(flow
)
392 if nb_ports
<= 2: # point to multipoint or nets with more than 2 elements
394 out
= (vlan_out
, str(dst_port
['switch_port']))
395 if out
not in flow_broadcast
['actions']:
396 flow_broadcast
['actions'].append(out
)
399 for flow_broadcast
in new_broadcast_flows
.values():
400 if len(flow_broadcast
['actions']) == 0:
401 continue # nothing to do, skip
402 flow_broadcast
['actions'].sort()
403 if 'vlan_id' in flow_broadcast
:
404 previous_vlan
= 0 # indicates that a packet contains a vlan, and the vlan
409 for action
in flow_broadcast
['actions']:
410 if action
[0] != previous_vlan
:
411 final_actions
.append(('vlan', action
[0]))
412 previous_vlan
= action
[0]
413 if self
.of_controller_nets_with_same_vlan
and action_number
:
414 raise SdnConnectorError("Cannot interconnect different vlan tags in a network when flag "
415 "'of_controller_nets_with_same_vlan' is True.")
417 final_actions
.append(('out', action
[1]))
418 flow_broadcast
['actions'] = final_actions
420 if self
._check
_flow
_already
_present
(flow_broadcast
, new_flows
) >= 0:
421 self
.logger
.debug("Skipping repeated flow '%s'", str(flow_broadcast
))
424 new_flows
.append(flow_broadcast
)
426 # UNIFY openflow rules with the same input port and vlan and the same output actions
427 # These flows differ at the dst_mac; and they are unified by not filtering by dst_mac
428 # this can happen if there is only two ports. It is converted to a point to point connection
429 flow_dict
= {} # use as key vlan_id+ingress_port and as value the list of flows matching these values
430 for flow
in new_flows
:
431 key
= str(flow
.get("vlan_id")) + ":" + flow
["ingress_port"]
433 flow_dict
[key
].append(flow
)
435 flow_dict
[key
] = [flow
]
437 for flow_list
in flow_dict
.values():
439 if len(flow_list
) >= 2:
442 if f
['actions'] != flow_list
[0]['actions']:
445 if convert2ptp
: # add only one unified rule without dst_mac
446 self
.logger
.debug("Convert flow rules to NON mac dst_address " + str(flow_list
))
447 flow_list
[0].pop('dst_mac')
448 flow_list
[0]["priority"] -= 5
449 new_flows2
.append(flow_list
[0])
450 else: # add all the rules
451 new_flows2
+= flow_list
454 def _check_flow_already_present(self
, new_flow
, flow_list
):
455 '''check if the same flow is already present in the flow list
456 The flow is repeated if all the fields, apart from name, are equal
457 Return the index of matching flow, -1 if not match'''
458 for index
, flow
in enumerate(flow_list
):
459 for f
in self
.flow_fields
:
460 if flow
.get(f
) != new_flow
.get(f
):