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
21 from uuid
import uuid4
23 from osm_ro_plugin
.sdnconn
import SdnConnectorBase
, SdnConnectorError
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.
31 __author__
= "Alfonso Tierno"
32 __date__
= "2019-11-11"
35 class OpenflowConnException(Exception):
36 """Common and base class Exception for all vimconnector exceptions"""
38 def __init__(self
, message
, http_code
=HTTPStatus
.BAD_REQUEST
.value
):
39 Exception.__init
__(self
, message
)
40 self
.http_code
= http_code
43 class OpenflowConnConnectionException(OpenflowConnException
):
44 """Connectivity error with the VIM"""
46 def __init__(self
, message
, http_code
=HTTPStatus
.SERVICE_UNAVAILABLE
.value
):
47 OpenflowConnException
.__init
__(self
, message
, http_code
)
50 class OpenflowConnUnexpectedResponse(OpenflowConnException
):
51 """Get an wrong response from VIM"""
53 def __init__(self
, message
, http_code
=HTTPStatus
.INTERNAL_SERVER_ERROR
.value
):
54 OpenflowConnException
.__init
__(self
, message
, http_code
)
57 class OpenflowConnAuthException(OpenflowConnException
):
58 """Invalid credentials or authorization to perform this action over the VIM"""
60 def __init__(self
, message
, http_code
=HTTPStatus
.UNAUTHORIZED
.value
):
61 OpenflowConnException
.__init
__(self
, message
, http_code
)
64 class OpenflowConnNotFoundException(OpenflowConnException
):
65 """The item is not found at VIM"""
67 def __init__(self
, message
, http_code
=HTTPStatus
.NOT_FOUND
.value
):
68 OpenflowConnException
.__init
__(self
, message
, http_code
)
71 class OpenflowConnConflictException(OpenflowConnException
):
72 """There is a conflict, e.g. more item found than one"""
74 def __init__(self
, message
, http_code
=HTTPStatus
.CONFLICT
.value
):
75 OpenflowConnException
.__init
__(self
, message
, http_code
)
78 class OpenflowConnNotSupportedException(OpenflowConnException
):
79 """The request is not supported by connector"""
81 def __init__(self
, message
, http_code
=HTTPStatus
.SERVICE_UNAVAILABLE
.value
):
82 OpenflowConnException
.__init
__(self
, message
, http_code
)
85 class OpenflowConnNotImplemented(OpenflowConnException
):
86 """The method is not implemented by the connected"""
88 def __init__(self
, message
, http_code
=HTTPStatus
.NOT_IMPLEMENTED
.value
):
89 OpenflowConnException
.__init
__(self
, message
, http_code
)
94 Openflow controller connector abstract implementeation.
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")
103 def get_of_switches(self
):
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
108 raise OpenflowConnNotImplemented("Should have implemented this")
110 def obtain_port_correspondence(self
):
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
115 raise OpenflowConnNotImplemented("Should have implemented this")
117 def get_of_rules(self
, translate_of_ports
=True):
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
134 raise OpenflowConnNotImplemented("Should have implemented this")
136 def del_flow(self
, flow_name
):
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
142 raise OpenflowConnNotImplemented("Should have implemented this")
144 def new_flow(self
, data
):
146 Insert a new static rule
147 :param data: dictionary with the following content:
148 priority: rule priority
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
158 raise OpenflowConnNotImplemented("Should have implemented this")
160 def clear_all_flows(self
):
162 Delete all existing rules
163 :return: None if ok, text_error if fails
165 raise OpenflowConnNotImplemented("Should have implemented this")
168 class SdnConnectorOpenFlow(SdnConnectorBase
):
170 This class is the base engine of SDN plugins base on openflow rules
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
191 def check_credentials(self
):
193 self
.openflow_conn
.obtain_port_correspondence()
194 except OpenflowConnException
as e
:
195 raise SdnConnectorError(e
, http_code
=e
.http_code
)
197 def get_connectivity_service_status(self
, service_uuid
, conn_info
=None):
198 conn_info
= conn_info
or {}
200 "sdn_status": conn_info
.get("status", "ERROR"),
201 "error_msg": conn_info
.get("error_msg", "Variable conn_info not provided"),
203 # TODO check rules connectirng to of_connector
205 def create_connectivity_service(self
, service_type
, connection_points
, **kwargs
):
206 net_id
= str(uuid4())
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(
221 created_items
= self
._set
_openflow
_rules
(
222 service_type
, net_id
, ports
, created_items
=None
225 return net_id
, created_items
226 except (SdnConnectorError
, OpenflowConnException
) as e
:
227 raise SdnConnectorError(e
, http_code
=e
.http_code
)
229 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
231 service_type
= "ELAN"
233 self
._set
_openflow
_rules
(
234 service_type
, service_uuid
, ports
, created_items
=conn_info
238 except (SdnConnectorError
, OpenflowConnException
) as e
:
239 raise SdnConnectorError(e
, http_code
=e
.http_code
)
241 def edit_connectivity_service(
242 self
, service_uuid
, conn_info
=None, connection_points
=None, **kwargs
245 for cp
in connection_points
:
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(
256 service_type
= "ELAN" # TODO. Store at conn_info for later use
259 created_items
= self
._set
_openflow
_rules
(
260 service_type
, service_uuid
, ports
, created_items
=conn_info
264 except (SdnConnectorError
, OpenflowConnException
) as e
:
265 raise SdnConnectorError(e
, http_code
=e
.http_code
)
267 def clear_all_connectivity_services(self
):
268 """Delete all WAN Links corresponding to a WIM"""
271 def get_all_active_connectivity_services(self
):
272 """Provide information about all active connections provisioned by a
277 def _set_openflow_rules(self
, net_type
, net_id
, ports
, created_items
=None):
278 ifaces_nb
= len(ports
)
280 if not created_items
:
284 "installed_rules_ids": [],
286 rules_to_delete
= created_items
.get("installed_rules_ids") or []
287 new_installed_rules_ids
= []
291 step
= "Checking ports and network type compatibility"
294 elif net_type
== "ELINE":
296 raise SdnConnectorError(
297 "'ELINE' type network cannot connect {} interfaces, only 2".format(
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
307 if port
["vlan"] not in vlan_tags
:
308 vlan_tags
.append(port
["vlan"])
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"
316 raise SdnConnectorError(
317 "Only ELINE or ELAN network types are supported for openflow"
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
]
325 # calculate new flows to be inserted
326 step
= "Compute needed openflow rules"
327 new_flows
= self
._compute
_net
_flows
(net_id
, ports
)
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
)
335 flow_id
= existing_flows
[index
]["name"]
336 self
.logger
.debug("Skipping already present flow %s", str(flow
))
338 # 2 look for a non used name
339 flow_name
= flow
["net_id"] + "." + str(name_index
)
341 while flow_name
in existing_flows_ids
:
343 flow_name
= flow
["net_id"] + "." + str(name_index
)
345 flow
["name"] = flow_name
347 # 3 insert at openflow
349 self
.of_connector
.new_flow(flow
)
350 flow_id
= flow
["name"]
351 existing_flows_ids
.append(flow_id
)
352 except OpenflowConnException
as e
:
355 "Cannot create rule for ingress_port={}, dst_mac={}: {}".format(
356 flow
["ingress_port"], flow
["dst_mac"], e
360 # 4 insert at database
362 new_installed_rules_ids
.append(flow_id
)
363 if flow_id
in rules_to_delete
:
364 rules_to_delete
.remove(flow_id
)
366 # delete not needed old flows from openflow
367 for flow_id
in rules_to_delete
:
370 self
.of_connector
.del_flow(flow_id
)
371 except OpenflowConnNotFoundException
:
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
)
378 created_items
["installed_rules_ids"] = new_installed_rules_ids
381 created_items
["error_msg"] = ";".join(error_list
)[:1000]
382 created_items
["error_msg"] = "ERROR"
384 created_items
["error_msg"] = None
385 created_items
["status"] = "ACTIVE"
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
)
395 def _compute_net_flows(self
, net_id
, ports
):
397 new_broadcast_flows
= {}
398 nb_ports
= len(ports
)
400 # Check switch_port information is right
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(
411 priority
= 1000 # 1100
413 for src_port
in ports
:
414 # if src_port.get("groups")
415 vlan_in
= src_port
["vlan"]
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
]
423 "priority": priority
,
425 "dst_mac": "ff:ff:ff:ff:ff:ff",
426 "ingress_port": str(src_port
["switch_port"]),
430 new_broadcast_flows
[broadcast_key
] = flow_broadcast
432 if vlan_in
is not None:
433 flow_broadcast
["vlan_id"] = str(vlan_in
)
435 for dst_port
in ports
:
436 vlan_out
= dst_port
["vlan"]
439 src_port
["switch_port"] == dst_port
["switch_port"]
440 and vlan_in
== vlan_out
445 "priority": priority
,
447 "ingress_port": str(src_port
["switch_port"]),
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
457 flow
["dst_mac"] = str(dst_port
["mac"])
461 flow
["actions"].append(("vlan", None))
463 flow
["actions"].append(("vlan", vlan_out
))
465 flow
["actions"].append(("out", str(dst_port
["switch_port"])))
467 if self
._check
_flow
_already
_present
(flow
, new_flows
) >= 0:
468 self
.logger
.debug("Skipping repeated flow '%s'", str(flow
))
471 new_flows
.append(flow
)
474 # point to multipoint or nets with more than 2 elements
478 out
= (vlan_out
, str(dst_port
["switch_port"]))
480 if out
not in flow_broadcast
["actions"]:
481 flow_broadcast
["actions"].append(out
)
484 for flow_broadcast
in new_broadcast_flows
.values():
485 if len(flow_broadcast
["actions"]) == 0:
486 continue # nothing to do, skip
488 flow_broadcast
["actions"].sort()
490 if "vlan_id" in flow_broadcast
:
491 # indicates that a packet contains a vlan, and the vlan
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]
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."
511 final_actions
.append(("out", action
[1]))
512 flow_broadcast
["actions"] = final_actions
514 if self
._check
_flow
_already
_present
(flow_broadcast
, new_flows
) >= 0:
515 self
.logger
.debug("Skipping repeated flow '%s'", str(flow_broadcast
))
518 new_flows
.append(flow_broadcast
)
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
525 for flow
in new_flows
:
526 key
= str(flow
.get("vlan_id")) + ":" + flow
["ingress_port"]
529 flow_dict
[key
].append(flow
)
531 flow_dict
[key
] = [flow
]
535 for flow_list
in flow_dict
.values():
538 if len(flow_list
) >= 2:
542 if f
["actions"] != flow_list
[0]["actions"]:
546 if convert2ptp
: # add only one unified rule without dst_mac
548 "Convert flow rules to NON mac dst_address " + str(flow_list
)
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
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
563 for index
, flow
in enumerate(flow_list
):
564 for f
in self
.flow_fields
:
565 if flow
.get(f
) != new_flow
.get(f
):