X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=RO-plugin%2Fosm_ro_plugin%2Fopenflow_conn.py;fp=RO-plugin%2Fosm_ro_plugin%2Fopenflow_conn.py;h=f7910c93d07d47c19d5295cdee456b5c379f31e5;hb=7277486065c905f91477bb064da86855a8fa269a;hp=0000000000000000000000000000000000000000;hpb=667d158c0d3ee7b4c176ad0b27ac428c81b0ddbc;p=osm%2FRO.git diff --git a/RO-plugin/osm_ro_plugin/openflow_conn.py b/RO-plugin/osm_ro_plugin/openflow_conn.py new file mode 100644 index 00000000..f7910c93 --- /dev/null +++ b/RO-plugin/osm_ro_plugin/openflow_conn.py @@ -0,0 +1,464 @@ +## +# Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +## +import logging +from http import HTTPStatus +from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError +from uuid import uuid4 + +""" +Implement an Abstract class 'OpenflowConn' and an engine 'SdnConnectorOpenFlow' used for base class for SDN plugings +that implements a pro-active opeflow rules. +""" + +__author__ = "Alfonso Tierno" +__date__ = "2019-11-11" + + +class OpenflowConnException(Exception): + """Common and base class Exception for all vimconnector exceptions""" + def __init__(self, message, http_code=HTTPStatus.BAD_REQUEST.value): + Exception.__init__(self, message) + self.http_code = http_code + + +class OpenflowConnConnectionException(OpenflowConnException): + """Connectivity error with the VIM""" + def __init__(self, message, http_code=HTTPStatus.SERVICE_UNAVAILABLE.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConnUnexpectedResponse(OpenflowConnException): + """Get an wrong response from VIM""" + def __init__(self, message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConnAuthException(OpenflowConnException): + """Invalid credentials or authorization to perform this action over the VIM""" + def __init__(self, message, http_code=HTTPStatus.UNAUTHORIZED.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConnNotFoundException(OpenflowConnException): + """The item is not found at VIM""" + def __init__(self, message, http_code=HTTPStatus.NOT_FOUND.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConnConflictException(OpenflowConnException): + """There is a conflict, e.g. more item found than one""" + def __init__(self, message, http_code=HTTPStatus.CONFLICT.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConnNotSupportedException(OpenflowConnException): + """The request is not supported by connector""" + def __init__(self, message, http_code=HTTPStatus.SERVICE_UNAVAILABLE.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConnNotImplemented(OpenflowConnException): + """The method is not implemented by the connected""" + def __init__(self, message, http_code=HTTPStatus.NOT_IMPLEMENTED.value): + OpenflowConnException.__init__(self, message, http_code) + + +class OpenflowConn: + """ + Openflow controller connector abstract implementeation. + """ + def __init__(self, params): + self.name = "openflow_conector" + self.pp2ofi = {} # From Physical Port to OpenFlow Index + self.ofi2pp = {} # From OpenFlow Index to Physical Port + self.logger = logging.getLogger('openmano.sdn.openflow_conn') + + def get_of_switches(self): + """" + Obtain a a list of switches or DPID detected by this controller + :return: list length, and a list where each element a tuple pair (DPID, IP address), text_error: if fails + """ + raise OpenflowConnNotImplemented("Should have implemented this") + + def obtain_port_correspondence(self): + """ + Obtain the correspondence between physical and openflow port names + :return: dictionary: with physical name as key, openflow name as value, error_text: if fails + """ + raise OpenflowConnNotImplemented("Should have implemented this") + + def get_of_rules(self, translate_of_ports=True): + """ + Obtain the rules inserted at openflow controller + :param translate_of_ports: if True it translates ports from openflow index to physical switch name + :return: list where each item is a dictionary with the following content: + priority: rule priority + priority: rule priority + name: rule name (present also as the master dict key) + ingress_port: match input port of the rule + dst_mac: match destination mac address of the rule, can be missing or None if not apply + vlan_id: match vlan tag of the rule, can be missing or None if not apply + actions: list of actions, composed by a pair tuples: + (vlan, None/int): for stripping/setting a vlan tag + (out, port): send to this port + switch: DPID, all + text_error if fails + """ + raise OpenflowConnNotImplemented("Should have implemented this") + + def del_flow(self, flow_name): + """ + Delete all existing rules + :param flow_name: flow_name, this is the rule name + :return: None if ok, text_error if fails + """ + raise OpenflowConnNotImplemented("Should have implemented this") + + def new_flow(self, data): + """ + Insert a new static rule + :param data: dictionary with the following content: + priority: rule priority + name: rule name + ingress_port: match input port of the rule + dst_mac: match destination mac address of the rule, missing or None if not apply + vlan_id: match vlan tag of the rule, missing or None if not apply + actions: list of actions, composed by a pair tuples with these posibilities: + ('vlan', None/int): for stripping/setting a vlan tag + ('out', port): send to this port + :return: None if ok, text_error if fails + """ + raise OpenflowConnNotImplemented("Should have implemented this") + + def clear_all_flows(self): + """" + Delete all existing rules + :return: None if ok, text_error if fails + """ + raise OpenflowConnNotImplemented("Should have implemented this") + + +class SdnConnectorOpenFlow(SdnConnectorBase): + """ + This class is the base engine of SDN plugins base on openflow rules + """ + flow_fields = ('priority', 'vlan', 'ingress_port', 'actions', 'dst_mac', 'src_mac', 'net_id') + + def __init__(self, wim, wim_account, config=None, logger=None, of_connector=None): + self.logger = logger or logging.getLogger('openmano.sdn.openflow_conn') + self.of_connector = of_connector + self.of_controller_nets_with_same_vlan = config.get("of_controller_nets_with_same_vlan", False) + + def check_credentials(self): + try: + self.openflow_conn.obtain_port_correspondence() + except OpenflowConnException as e: + raise SdnConnectorError(e, http_code=e.http_code) + + def get_connectivity_service_status(self, service_uuid, conn_info=None): + conn_info = conn_info or {} + return { + "sdn_status": conn_info.get("status", "ERROR"), + "error_msg": conn_info.get("error_msg", "Variable conn_info not provided"), + } + # TODO check rules connectirng to of_connector + + def create_connectivity_service(self, service_type, connection_points, **kwargs): + net_id = str(uuid4()) + ports = [] + for cp in connection_points: + port = { + "uuid": cp["service_endpoint_id"], + "vlan": cp.get("service_endpoint_encapsulation_info", {}).get("vlan"), + "mac": cp.get("service_endpoint_encapsulation_info", {}).get("mac"), + "switch_port": cp.get("service_endpoint_encapsulation_info", {}).get("switch_port"), + } + ports.append(port) + try: + created_items = self._set_openflow_rules(service_type, net_id, ports, created_items=None) + return net_id, created_items + except (SdnConnectorError, OpenflowConnException) as e: + raise SdnConnectorError(e, http_code=e.http_code) + + def delete_connectivity_service(self, service_uuid, conn_info=None): + try: + service_type = "ELAN" + ports = [] + self._set_openflow_rules(service_type, service_uuid, ports, created_items=conn_info) + return None + except (SdnConnectorError, OpenflowConnException) as e: + raise SdnConnectorError(e, http_code=e.http_code) + + def edit_connectivity_service(self, service_uuid, conn_info=None, connection_points=None, **kwargs): + ports = [] + for cp in connection_points: + port = { + "uuid": cp["service_endpoint_id"], + "vlan": cp.get("service_endpoint_encapsulation_info", {}).get("vlan"), + "mac": cp.get("service_endpoint_encapsulation_info", {}).get("mac"), + "switch_port": cp.get("service_endpoint_encapsulation_info", {}).get("switch_port"), + } + ports.append(port) + service_type = "ELAN" # TODO. Store at conn_info for later use + try: + created_items = self._set_openflow_rules(service_type, service_uuid, ports, created_items=conn_info) + return created_items + except (SdnConnectorError, OpenflowConnException) as e: + raise SdnConnectorError(e, http_code=e.http_code) + + def clear_all_connectivity_services(self): + """Delete all WAN Links corresponding to a WIM""" + pass + + def get_all_active_connectivity_services(self): + """Provide information about all active connections provisioned by a + WIM + """ + pass + + def _set_openflow_rules(self, net_type, net_id, ports, created_items=None): + ifaces_nb = len(ports) + if not created_items: + created_items = {"status": None, "error_msg": None, "installed_rules_ids": []} + rules_to_delete = created_items.get("installed_rules_ids") or [] + new_installed_rules_ids = [] + error_list = [] + + try: + step = "Checking ports and network type compatibility" + if ifaces_nb < 2: + pass + elif net_type == 'ELINE': + if ifaces_nb > 2: + raise SdnConnectorError("'ELINE' type network cannot connect {} interfaces, only 2".format( + ifaces_nb)) + elif net_type == 'ELAN': + if ifaces_nb > 2 and self.of_controller_nets_with_same_vlan: + # check all ports are VLAN (tagged) or none + vlan_tags = [] + for port in ports: + if port["vlan"] not in vlan_tags: + vlan_tags.append(port["vlan"]) + if len(vlan_tags) > 1: + raise SdnConnectorError("This pluging cannot connect ports with diferent VLAN tags when flag " + "'of_controller_nets_with_same_vlan' is active") + else: + raise SdnConnectorError('Only ELINE or ELAN network types are supported for openflow') + + # Get the existing flows at openflow controller + step = "Getting installed openflow rules" + existing_flows = self.of_connector.get_of_rules() + existing_flows_ids = [flow["name"] for flow in existing_flows] + + # calculate new flows to be inserted + step = "Compute needed openflow rules" + new_flows = self._compute_net_flows(net_id, ports) + + name_index = 0 + for flow in new_flows: + # 1 check if an equal flow is already present + index = self._check_flow_already_present(flow, existing_flows) + if index >= 0: + flow_id = existing_flows[index]["name"] + self.logger.debug("Skipping already present flow %s", str(flow)) + else: + # 2 look for a non used name + flow_name = flow["net_id"] + "." + str(name_index) + while flow_name in existing_flows_ids: + name_index += 1 + flow_name = flow["net_id"] + "." + str(name_index) + flow['name'] = flow_name + # 3 insert at openflow + try: + self.of_connector.new_flow(flow) + flow_id = flow["name"] + existing_flows_ids.append(flow_id) + except OpenflowConnException as e: + flow_id = None + error_list.append("Cannot create rule for ingress_port={}, dst_mac={}: {}" + .format(flow["ingress_port"], flow["dst_mac"], e)) + + # 4 insert at database + if flow_id: + new_installed_rules_ids.append(flow_id) + if flow_id in rules_to_delete: + rules_to_delete.remove(flow_id) + + # delete not needed old flows from openflow + for flow_id in rules_to_delete: + # Delete flow + try: + self.of_connector.del_flow(flow_id) + except OpenflowConnNotFoundException: + pass + except OpenflowConnException as e: + error_text = "Cannot remove rule '{}': {}".format(flow_id, e) + error_list.append(error_text) + self.logger.error(error_text) + created_items["installed_rules_ids"] = new_installed_rules_ids + if error_list: + created_items["error_msg"] = ";".join(error_list)[:1000] + created_items["error_msg"] = "ERROR" + else: + created_items["error_msg"] = None + created_items["status"] = "ACTIVE" + return created_items + except (SdnConnectorError, OpenflowConnException) as e: + raise SdnConnectorError("Error while {}: {}".format(step, e)) from e + except Exception as e: + error_text = "Error while {}: {}".format(step, e) + self.logger.critical(error_text, exc_info=True) + raise SdnConnectorError(error_text) + + def _compute_net_flows(self, net_id, ports): + new_flows = [] + new_broadcast_flows = {} + nb_ports = len(ports) + + # Check switch_port information is right + for port in ports: + nb_ports += 1 + if str(port['switch_port']) not in self.of_connector.pp2ofi: + raise SdnConnectorError("switch port name '{}' is not valid for the openflow controller". + format(port['switch_port'])) + priority = 1000 # 1100 + + for src_port in ports: + # if src_port.get("groups") + vlan_in = src_port['vlan'] + + # BROADCAST: + broadcast_key = src_port['uuid'] + "." + str(vlan_in) + if broadcast_key in new_broadcast_flows: + flow_broadcast = new_broadcast_flows[broadcast_key] + else: + flow_broadcast = {'priority': priority, + 'net_id': net_id, + 'dst_mac': 'ff:ff:ff:ff:ff:ff', + "ingress_port": str(src_port['switch_port']), + 'vlan_id': vlan_in, + 'actions': [] + } + new_broadcast_flows[broadcast_key] = flow_broadcast + if vlan_in is not None: + flow_broadcast['vlan_id'] = str(vlan_in) + + for dst_port in ports: + vlan_out = dst_port['vlan'] + if src_port['switch_port'] == dst_port['switch_port'] and vlan_in == vlan_out: + continue + flow = { + "priority": priority, + 'net_id': net_id, + "ingress_port": str(src_port['switch_port']), + 'vlan_id': vlan_in, + 'actions': [] + } + # allow that one port have no mac + if dst_port['mac'] is None or nb_ports == 2: # point to point or nets with 2 elements + flow['priority'] = priority - 5 # less priority + else: + flow['dst_mac'] = str(dst_port['mac']) + + if vlan_out is None: + if vlan_in: + flow['actions'].append(('vlan', None)) + else: + flow['actions'].append(('vlan', vlan_out)) + flow['actions'].append(('out', str(dst_port['switch_port']))) + + if self._check_flow_already_present(flow, new_flows) >= 0: + self.logger.debug("Skipping repeated flow '%s'", str(flow)) + continue + + new_flows.append(flow) + + # BROADCAST: + if nb_ports <= 2: # point to multipoint or nets with more than 2 elements + continue + out = (vlan_out, str(dst_port['switch_port'])) + if out not in flow_broadcast['actions']: + flow_broadcast['actions'].append(out) + + # BROADCAST + for flow_broadcast in new_broadcast_flows.values(): + if len(flow_broadcast['actions']) == 0: + continue # nothing to do, skip + flow_broadcast['actions'].sort() + if 'vlan_id' in flow_broadcast: + previous_vlan = 0 # indicates that a packet contains a vlan, and the vlan + else: + previous_vlan = None + final_actions = [] + action_number = 0 + for action in flow_broadcast['actions']: + if action[0] != previous_vlan: + final_actions.append(('vlan', action[0])) + previous_vlan = action[0] + if self.of_controller_nets_with_same_vlan and action_number: + raise SdnConnectorError("Cannot interconnect different vlan tags in a network when flag " + "'of_controller_nets_with_same_vlan' is True.") + action_number += 1 + final_actions.append(('out', action[1])) + flow_broadcast['actions'] = final_actions + + if self._check_flow_already_present(flow_broadcast, new_flows) >= 0: + self.logger.debug("Skipping repeated flow '%s'", str(flow_broadcast)) + continue + + new_flows.append(flow_broadcast) + + # UNIFY openflow rules with the same input port and vlan and the same output actions + # These flows differ at the dst_mac; and they are unified by not filtering by dst_mac + # this can happen if there is only two ports. It is converted to a point to point connection + flow_dict = {} # use as key vlan_id+ingress_port and as value the list of flows matching these values + for flow in new_flows: + key = str(flow.get("vlan_id")) + ":" + flow["ingress_port"] + if key in flow_dict: + flow_dict[key].append(flow) + else: + flow_dict[key] = [flow] + new_flows2 = [] + for flow_list in flow_dict.values(): + convert2ptp = False + if len(flow_list) >= 2: + convert2ptp = True + for f in flow_list: + if f['actions'] != flow_list[0]['actions']: + convert2ptp = False + break + if convert2ptp: # add only one unified rule without dst_mac + self.logger.debug("Convert flow rules to NON mac dst_address " + str(flow_list)) + flow_list[0].pop('dst_mac') + flow_list[0]["priority"] -= 5 + new_flows2.append(flow_list[0]) + else: # add all the rules + new_flows2 += flow_list + return new_flows2 + + def _check_flow_already_present(self, new_flow, flow_list): + '''check if the same flow is already present in the flow list + The flow is repeated if all the fields, apart from name, are equal + Return the index of matching flow, -1 if not match''' + for index, flow in enumerate(flow_list): + for f in self.flow_fields: + if flow.get(f) != new_flow.get(f): + break + else: + return index + return -1