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_plugin
.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"""
35 def __init__(self
, message
, http_code
=HTTPStatus
.BAD_REQUEST
.value
):
36 Exception.__init
__(self
, message
)
37 self
.http_code
= http_code
40 class OpenflowConnConnectionException(OpenflowConnException
):
41 """Connectivity error with the VIM"""
43 def __init__(self
, message
, http_code
=HTTPStatus
.SERVICE_UNAVAILABLE
.value
):
44 OpenflowConnException
.__init
__(self
, message
, http_code
)
47 class OpenflowConnUnexpectedResponse(OpenflowConnException
):
48 """Get an wrong response from VIM"""
50 def __init__(self
, message
, http_code
=HTTPStatus
.INTERNAL_SERVER_ERROR
.value
):
51 OpenflowConnException
.__init
__(self
, message
, http_code
)
54 class OpenflowConnAuthException(OpenflowConnException
):
55 """Invalid credentials or authorization to perform this action over the VIM"""
57 def __init__(self
, message
, http_code
=HTTPStatus
.UNAUTHORIZED
.value
):
58 OpenflowConnException
.__init
__(self
, message
, http_code
)
61 class OpenflowConnNotFoundException(OpenflowConnException
):
62 """The item is not found at VIM"""
64 def __init__(self
, message
, http_code
=HTTPStatus
.NOT_FOUND
.value
):
65 OpenflowConnException
.__init
__(self
, message
, http_code
)
68 class OpenflowConnConflictException(OpenflowConnException
):
69 """There is a conflict, e.g. more item found than one"""
71 def __init__(self
, message
, http_code
=HTTPStatus
.CONFLICT
.value
):
72 OpenflowConnException
.__init
__(self
, message
, http_code
)
75 class OpenflowConnNotSupportedException(OpenflowConnException
):
76 """The request is not supported by connector"""
78 def __init__(self
, message
, http_code
=HTTPStatus
.SERVICE_UNAVAILABLE
.value
):
79 OpenflowConnException
.__init
__(self
, message
, http_code
)
82 class OpenflowConnNotImplemented(OpenflowConnException
):
83 """The method is not implemented by the connected"""
85 def __init__(self
, message
, http_code
=HTTPStatus
.NOT_IMPLEMENTED
.value
):
86 OpenflowConnException
.__init
__(self
, message
, http_code
)
91 Openflow controller connector abstract implementeation.
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")
100 def get_of_switches(self
):
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
105 raise OpenflowConnNotImplemented("Should have implemented this")
107 def obtain_port_correspondence(self
):
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
112 raise OpenflowConnNotImplemented("Should have implemented this")
114 def get_of_rules(self
, translate_of_ports
=True):
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
131 raise OpenflowConnNotImplemented("Should have implemented this")
133 def del_flow(self
, flow_name
):
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
139 raise OpenflowConnNotImplemented("Should have implemented this")
141 def new_flow(self
, data
):
143 Insert a new static rule
144 :param data: dictionary with the following content:
145 priority: rule priority
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
155 raise OpenflowConnNotImplemented("Should have implemented this")
157 def clear_all_flows(self
):
159 Delete all existing rules
160 :return: None if ok, text_error if fails
162 raise OpenflowConnNotImplemented("Should have implemented this")
165 class SdnConnectorOpenFlow(SdnConnectorBase
):
167 This class is the base engine of SDN plugins base on openflow rules
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
188 def check_credentials(self
):
190 self
.openflow_conn
.obtain_port_correspondence()
191 except OpenflowConnException
as e
:
192 raise SdnConnectorError(e
, http_code
=e
.http_code
)
194 def get_connectivity_service_status(self
, service_uuid
, conn_info
=None):
195 conn_info
= conn_info
or {}
197 "sdn_status": conn_info
.get("status", "ERROR"),
198 "error_msg": conn_info
.get("error_msg", "Variable conn_info not provided"),
200 # TODO check rules connectirng to of_connector
202 def create_connectivity_service(self
, service_type
, connection_points
, **kwargs
):
203 net_id
= str(uuid4())
206 for cp
in connection_points
:
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(
218 created_items
= self
._set
_openflow
_rules
(
219 service_type
, net_id
, ports
, created_items
=None
222 return net_id
, created_items
223 except (SdnConnectorError
, OpenflowConnException
) as e
:
224 raise SdnConnectorError(e
, http_code
=e
.http_code
)
226 def delete_connectivity_service(self
, service_uuid
, conn_info
=None):
228 service_type
= "ELAN"
230 self
._set
_openflow
_rules
(
231 service_type
, service_uuid
, ports
, created_items
=conn_info
235 except (SdnConnectorError
, OpenflowConnException
) as e
:
236 raise SdnConnectorError(e
, http_code
=e
.http_code
)
238 def edit_connectivity_service(
239 self
, service_uuid
, conn_info
=None, connection_points
=None, **kwargs
242 for cp
in connection_points
:
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(
253 service_type
= "ELAN" # TODO. Store at conn_info for later use
256 created_items
= self
._set
_openflow
_rules
(
257 service_type
, service_uuid
, ports
, created_items
=conn_info
261 except (SdnConnectorError
, OpenflowConnException
) as e
:
262 raise SdnConnectorError(e
, http_code
=e
.http_code
)
264 def clear_all_connectivity_services(self
):
265 """Delete all WAN Links corresponding to a WIM"""
268 def get_all_active_connectivity_services(self
):
269 """Provide information about all active connections provisioned by a
274 def _set_openflow_rules(self
, net_type
, net_id
, ports
, created_items
=None):
275 ifaces_nb
= len(ports
)
277 if not created_items
:
281 "installed_rules_ids": [],
283 rules_to_delete
= created_items
.get("installed_rules_ids") or []
284 new_installed_rules_ids
= []
288 step
= "Checking ports and network type compatibility"
291 elif net_type
== "ELINE":
293 raise SdnConnectorError(
294 "'ELINE' type network cannot connect {} interfaces, only 2".format(
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
304 if port
["vlan"] not in vlan_tags
:
305 vlan_tags
.append(port
["vlan"])
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"
313 raise SdnConnectorError(
314 "Only ELINE or ELAN network types are supported for openflow"
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
]
322 # calculate new flows to be inserted
323 step
= "Compute needed openflow rules"
324 new_flows
= self
._compute
_net
_flows
(net_id
, ports
)
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
)
332 flow_id
= existing_flows
[index
]["name"]
333 self
.logger
.debug("Skipping already present flow %s", str(flow
))
335 # 2 look for a non used name
336 flow_name
= flow
["net_id"] + "." + str(name_index
)
338 while flow_name
in existing_flows_ids
:
340 flow_name
= flow
["net_id"] + "." + str(name_index
)
342 flow
["name"] = flow_name
344 # 3 insert at openflow
346 self
.of_connector
.new_flow(flow
)
347 flow_id
= flow
["name"]
348 existing_flows_ids
.append(flow_id
)
349 except OpenflowConnException
as e
:
352 "Cannot create rule for ingress_port={}, dst_mac={}: {}".format(
353 flow
["ingress_port"], flow
["dst_mac"], e
357 # 4 insert at database
359 new_installed_rules_ids
.append(flow_id
)
360 if flow_id
in rules_to_delete
:
361 rules_to_delete
.remove(flow_id
)
363 # delete not needed old flows from openflow
364 for flow_id
in rules_to_delete
:
367 self
.of_connector
.del_flow(flow_id
)
368 except OpenflowConnNotFoundException
:
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
)
375 created_items
["installed_rules_ids"] = new_installed_rules_ids
378 created_items
["error_msg"] = ";".join(error_list
)[:1000]
379 created_items
["error_msg"] = "ERROR"
381 created_items
["error_msg"] = None
382 created_items
["status"] = "ACTIVE"
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
)
392 def _compute_net_flows(self
, net_id
, ports
):
394 new_broadcast_flows
= {}
395 nb_ports
= len(ports
)
397 # Check switch_port information is right
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(
408 priority
= 1000 # 1100
410 for src_port
in ports
:
411 # if src_port.get("groups")
412 vlan_in
= src_port
["vlan"]
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
]
420 "priority": priority
,
422 "dst_mac": "ff:ff:ff:ff:ff:ff",
423 "ingress_port": str(src_port
["switch_port"]),
427 new_broadcast_flows
[broadcast_key
] = flow_broadcast
429 if vlan_in
is not None:
430 flow_broadcast
["vlan_id"] = str(vlan_in
)
432 for dst_port
in ports
:
433 vlan_out
= dst_port
["vlan"]
436 src_port
["switch_port"] == dst_port
["switch_port"]
437 and vlan_in
== vlan_out
442 "priority": priority
,
444 "ingress_port": str(src_port
["switch_port"]),
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
454 flow
["dst_mac"] = str(dst_port
["mac"])
458 flow
["actions"].append(("vlan", None))
460 flow
["actions"].append(("vlan", vlan_out
))
462 flow
["actions"].append(("out", str(dst_port
["switch_port"])))
464 if self
._check
_flow
_already
_present
(flow
, new_flows
) >= 0:
465 self
.logger
.debug("Skipping repeated flow '%s'", str(flow
))
468 new_flows
.append(flow
)
471 # point to multipoint or nets with more than 2 elements
475 out
= (vlan_out
, str(dst_port
["switch_port"]))
477 if out
not in flow_broadcast
["actions"]:
478 flow_broadcast
["actions"].append(out
)
481 for flow_broadcast
in new_broadcast_flows
.values():
482 if len(flow_broadcast
["actions"]) == 0:
483 continue # nothing to do, skip
485 flow_broadcast
["actions"].sort()
487 if "vlan_id" in flow_broadcast
:
488 # indicates that a packet contains a vlan, and the vlan
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]
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."
508 final_actions
.append(("out", action
[1]))
509 flow_broadcast
["actions"] = final_actions
511 if self
._check
_flow
_already
_present
(flow_broadcast
, new_flows
) >= 0:
512 self
.logger
.debug("Skipping repeated flow '%s'", str(flow_broadcast
))
515 new_flows
.append(flow_broadcast
)
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
522 for flow
in new_flows
:
523 key
= str(flow
.get("vlan_id")) + ":" + flow
["ingress_port"]
526 flow_dict
[key
].append(flow
)
528 flow_dict
[key
] = [flow
]
532 for flow_list
in flow_dict
.values():
535 if len(flow_list
) >= 2:
539 if f
["actions"] != flow_list
[0]["actions"]:
543 if convert2ptp
: # add only one unified rule without dst_mac
545 "Convert flow rules to NON mac dst_address " + str(flow_list
)
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
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
560 for index
, flow
in enumerate(flow_list
):
561 for f
in self
.flow_fields
:
562 if flow
.get(f
) != new_flow
.get(f
):