-##
-# 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.wim.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