| # Copyright 2017-present Adtran, Inc. |
| # |
| # 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 xmltodict |
| import re |
| import structlog |
| from enum import Enum |
| from twisted.internet.defer import inlineCallbacks, returnValue, succeed |
| |
| log = structlog.get_logger() |
| |
| # NOTE: For the EVC Map name, the ingress-port number is the VOLTHA port number (not pon-id since |
| # it covers NNI ports as well in order to handle the NNI-NNI case. For flows that |
| # cover an entire pon, the name will have the ONU ID and GEM ID appended to it upon |
| # installation with a period as a separator. |
| |
| EVC_MAP_NAME_FORMAT = 'VOLTHA-{}-{}' # format(ingress-port, flow.id) |
| EVC_MAP_NAME_REGEX_ALL = 'VOLTHA-*' |
| |
| |
| class EVCMap(object): |
| """ |
| Class to wrap EVC functionality |
| """ |
| class EvcConnection(Enum): |
| NO_EVC_CONNECTION = 0 |
| EVC = 1 |
| DISCARD = 2 |
| DEFAULT = NO_EVC_CONNECTION |
| |
| @staticmethod |
| def xml(value): |
| # Note we do not have XML for 'EVC' enumeration. |
| if value is None: |
| value = EVCMap.EvcConnection.DEFAULT |
| if value == EVCMap.EvcConnection.DISCARD: |
| return '<no-evc-connection/>' |
| elif value == EVCMap.EvcConnection.DISCARD: |
| return 'discard/' |
| raise ValueError('Invalid EvcConnection enumeration') |
| |
| class PriorityOption(Enum): |
| INHERIT_PRIORITY = 0 |
| EXPLICIT_PRIORITY = 1 |
| DEFAULT = INHERIT_PRIORITY |
| |
| @staticmethod |
| def xml(value): |
| if value is None: |
| value = EVCMap.PriorityOption.DEFAULT |
| if value == EVCMap.PriorityOption.INHERIT_PRIORITY: |
| return '<inherit-pri/>' |
| elif value == EVCMap.PriorityOption.EXPLICIT_PRIORITY: |
| return '<explicit-pri/>' |
| raise ValueError('Invalid PriorityOption enumeration') |
| |
| def __init__(self, flow, evc, is_ingress_map): |
| self._flow = flow |
| self._evc = evc |
| self._gem_ids_and_vid = None # { key -> onu-id, value -> tuple(sorted GEM Port IDs, onu_vid) } |
| self._is_ingress_map = is_ingress_map |
| self._pon_id = None |
| self._installed = False |
| self._status_message = None |
| |
| self._name = None |
| self._enabled = True |
| self._uni_port = None |
| self._evc_connection = EVCMap.EvcConnection.DEFAULT |
| self._evc_name = None |
| self._men_priority = EVCMap.PriorityOption.DEFAULT |
| self._men_pri = 0 # If Explicit Priority |
| |
| self._c_tag = None |
| self._men_ctag_priority = EVCMap.PriorityOption.DEFAULT |
| self._men_ctag_pri = 0 # If Explicit Priority |
| |
| self._match_ce_vlan_id = None |
| self._match_untagged = False |
| self._match_destination_mac_address = None |
| self._match_l2cp = False |
| self._match_broadcast = False |
| self._match_multicast = False |
| self._match_unicast = False |
| self._match_igmp = False |
| |
| # ACL logic |
| self._eth_type = None |
| self._ip_protocol = None |
| self._ipv4_dst = None |
| self._udp_dst = None |
| self._udp_src = None |
| |
| try: |
| self._valid = self._decode() |
| |
| except Exception as e: |
| log.exception('decode', e=e) |
| self._valid = False |
| |
| if self._valid: |
| evc.add_evc_map(self) |
| else: |
| self._evc = None |
| |
| def __str__(self): |
| return "EVCMap-{}: UNI: {}, isACL: {}".format(self._name, self._uni_port, |
| self._needs_acl_support) |
| |
| @staticmethod |
| def create_ingress_map(flow, evc): |
| return EVCMap(flow, evc, True) |
| |
| @staticmethod |
| def create_egress_map(flow, evc): |
| return EVCMap(flow, evc, False) |
| |
| @property |
| def valid(self): |
| return self._valid |
| |
| @property |
| def installed(self): |
| return self._installed |
| |
| @installed.setter |
| def installed(self, value): |
| assert not value, 'installed can only be reset' # Can only reset |
| self._installed = False |
| |
| @property |
| def name(self): |
| return self._name |
| |
| @property |
| def status(self): |
| return self._status_message |
| |
| @status.setter |
| def status(self, value): |
| self._status_message = value |
| |
| @property |
| def _needs_acl_support(self): |
| if self._ipv4_dst is not None: # In case MCAST downstream has ACL on it |
| return False |
| |
| return self._eth_type is not None or self._ip_protocol is not None or\ |
| self._udp_dst is not None or self._udp_src is not None |
| |
| @property |
| def pon_id(self): |
| return self._pon_id # May be None |
| |
| @property |
| def onu_ids(self): |
| return self._gem_ids_and_vid.keys() |
| |
| @property |
| def gem_ids_and_vid(self): |
| return self._gem_ids_and_vid.copy() |
| |
| @staticmethod |
| def _xml_header(operation=None): |
| return '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"{}><evc-map>'.\ |
| format('' if operation is None else ' xc:operation="{}"'.format(operation)) |
| |
| @staticmethod |
| def _xml_trailer(): |
| return '</evc-map></evc-maps>' |
| |
| def _common_install_xml(self): |
| xml = '<enabled>{}</enabled>'.format('true' if self._enabled else 'false') |
| xml += '<uni>{}</uni>'.format(self._uni_port) |
| |
| if self._evc_name is not None: |
| xml += '<evc>{}</evc>'.format(self._evc_name) |
| else: |
| xml += EVCMap.EvcConnection.xml(self._evc_connection) |
| |
| xml += '<match-untagged>{}</match-untagged>'.format('true' |
| if self._match_untagged |
| else 'false') |
| # if self._c_tag is not None: |
| # xml += '<ctag>{}</ctag>'.format(self._c_tag) |
| # TODO: The following is not yet supported (and in some cases, not decoded) |
| # self._men_priority = EVCMap.PriorityOption.INHERIT_PRIORITY |
| # self._men_pri = 0 # If Explicit Priority |
| # |
| # self._men_ctag_priority = EVCMap.PriorityOption.INHERIT_PRIORITY |
| # self._men_ctag_pri = 0 # If Explicit Priority |
| # |
| # self._match_ce_vlan_id = None |
| # self._match_untagged = True |
| # self._match_destination_mac_address = None |
| # self._eth_type = None |
| # self._ip_protocol = None |
| # self._ipv4_dst = None |
| # self._udp_dst = None |
| # self._udp_src = None |
| return xml |
| |
| def _ingress_install_xml(self, onu_s_gem_ids_and_vid): |
| from ..onu import Onu |
| |
| xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps">' |
| |
| for onu_or_vlan_id, gem_ids_and_vid in onu_s_gem_ids_and_vid.iteritems(): |
| first_gem_id = True |
| vid = gem_ids_and_vid[1] |
| ident = '{}.{}'.format(self._pon_id, onu_or_vlan_id) if vid is None \ |
| else onu_or_vlan_id |
| |
| for gem_id in gem_ids_and_vid[0]: |
| xml += '<evc-map>' |
| xml += '<name>{}.{}.{}</name>'.format(self.name, ident, gem_id) |
| xml += '<ce-vlan-id>{}</ce-vlan-id>'.format(Onu.gem_id_to_gvid(gem_id)) |
| |
| # GEM-IDs are a sorted list (ascending). First gemport handles downstream traffic |
| if first_gem_id and vid is not None: |
| first_gem_id = False |
| xml += '<network-ingress-filter>' |
| xml += '<men-ctag>{}</men-ctag>'.format(vid) # Added in August 2017 model |
| xml += '</network-ingress-filter>' |
| |
| xml += self._common_install_xml() |
| xml += '</evc-map>' |
| xml += '</evc-maps>' |
| return xml |
| |
| def _egress_install_xml(self): |
| xml = EVCMap._xml_header() |
| xml += '<name>{}</name>'.format(self.name) |
| xml += self._common_install_xml() |
| xml += EVCMap._xml_trailer() |
| return xml |
| |
| @inlineCallbacks |
| def install(self): |
| if self._valid and not self._installed and len(self._gem_ids_and_vid) > 0: |
| try: |
| # TODO: create generator of XML once we have MANY to install at once |
| map_xml = self._ingress_install_xml(self._gem_ids_and_vid) \ |
| if self._is_ingress_map else self._egress_install_xml() |
| |
| log.debug('install', xml=map_xml, name=self.name) |
| results = yield self._flow.handler.netconf_client.edit_config(map_xml, |
| lock_timeout=30) |
| self._installed = results.ok |
| self.status = '' if results.ok else results.error |
| |
| except Exception as e: |
| log.exception('install', name=self.name, e=e) |
| raise |
| |
| returnValue(self._installed and self._valid) |
| |
| def _ingress_remove_xml(self, onus_gem_ids_and_vid): |
| xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"' + \ |
| ' xc:operation="delete">' |
| |
| for onu_id, gem_ids_and_vid in onus_gem_ids_and_vid.iteritems(): |
| for gem_id in gem_ids_and_vid[0]: |
| xml += '<evc-map>' |
| xml += '<name>{}.{}.{}</name>'.format(self.name, onu_id, gem_id) |
| xml += '</evc-map>' |
| xml += '</evc-maps>' |
| return xml |
| |
| def _egress_remove_xml(self): |
| return EVCMap._xml_header('delete') + \ |
| '<name>{}</name>'.format(self.name) + EVCMap._xml_trailer() |
| |
| @inlineCallbacks |
| def remove(self): |
| if not self.installed: |
| returnValue(succeed('Not installed')) |
| |
| log.info('removing', evc_map=self) |
| |
| def _success(rpc_reply): |
| log.debug('remove-success', rpc_reply=rpc_reply) |
| self._installed = False |
| |
| def _failure(failure): |
| log.error('remove-failed', failure=failure) |
| self._installed = False |
| |
| # TODO: create generator of XML once we have MANY to install at once |
| map_xml = self._ingress_remove_xml(self._gem_ids_and_vid) if self._is_ingress_map \ |
| else self._egress_remove_xml() |
| |
| d = self._flow.handler.netconf_client.edit_config(map_xml, lock_timeout=30) |
| d.addCallbacks(_success, _failure) |
| return d |
| |
| @inlineCallbacks |
| def delete(self): |
| """ |
| Remove from hardware and delete/clean-up EVC-MAP Object |
| """ |
| if self._evc is not None: |
| self._evc.remove_evc_map(self) |
| self._evc = None |
| |
| self._flow = None |
| self._valid = False |
| |
| try: |
| yield self.remove() |
| |
| except Exception as e: |
| log.exception('removal', e=e) |
| |
| returnValue('Done') |
| |
| @staticmethod |
| def create_evc_map_name(flow): |
| return EVC_MAP_NAME_FORMAT.format(flow.in_port, flow.flow_id) |
| |
| def _decode(self): |
| from evc import EVC |
| from flow_entry import FlowEntry |
| |
| flow = self._flow # TODO: Drop saving of flow once debug complete |
| |
| self._name = EVCMap.create_evc_map_name(flow) |
| |
| if self._evc: |
| self._evc_connection = EVCMap.EvcConnection.EVC |
| self._evc_name = self._evc.name |
| else: |
| self._status_message = 'Can only create EVC-MAP if EVC supplied' |
| return False |
| |
| is_pon = flow.handler.is_pon_port(flow.in_port) |
| is_uni = flow.handler.is_uni_port(flow.in_port) |
| |
| if is_pon or is_uni: |
| self._uni_port = flow.handler.get_port_name(flow.in_port) |
| self._evc.ce_vlan_preservation = False |
| else: |
| self._status_message = 'EVC-MAPS without UNI or PON ports are not supported' |
| return False # UNI Ports handled in the EVC Maps |
| |
| # ACL logic |
| |
| self._eth_type = flow.eth_type |
| |
| if self._eth_type == FlowEntry.EtherType.IPv4.value: |
| self._ip_protocol = flow.ip_protocol |
| self._ipv4_dst = flow.ipv4_dst |
| |
| if self._ip_protocol == FlowEntry.IpProtocol.UDP.value: |
| self._udp_dst = flow.udp_dst |
| self._udp_src = flow.udp_src |
| |
| # If no match of VLAN this may be for untagged traffic or upstream and needs to |
| # match the gem-port vid |
| |
| if self._is_ingress_map and is_pon: |
| pon_port = flow.handler.get_southbound_port(flow.in_port) |
| |
| if pon_port is not None: |
| self._pon_id = pon_port.pon_id |
| self._gem_ids_and_vid = pon_port.gem_ids(flow.logical_port, |
| self._needs_acl_support, |
| flow.is_multicast_flow) |
| |
| # TODO: Only EAPOL ACL support for the first demo - FIXED_ONU |
| if self._needs_acl_support and self._eth_type != FlowEntry.EtherType.EAPOL.value: |
| self._gem_ids_and_vid = dict() |
| |
| # self._match_untagged = flow.vlan_id is None and flow.inner_vid is None |
| self._c_tag = flow.inner_vid |
| |
| # If a push of a single VLAN is present with a POP of the VLAN in the EVC's |
| # flow, then this is a traditional EVC flow |
| |
| self._evc.men_to_uni_tag_manipulation = EVC.Men2UniManipulation.POP_OUT_TAG_ONLY |
| self._evc.switching_method = EVC.SwitchingMethod.DOUBLE_TAGGED |
| |
| # if len(flow.push_vlan_id) == 1 and self._evc.flow_entry.pop_vlan == 1: |
| # self._evc.men_to_uni_tag_manipulation = EVC.Men2UniManipulation.SYMMETRIC |
| # self._evc.switching_method = EVC.SwitchingMethod.SINGLE_TAGGED |
| # self._evc.stpid = flow.push_vlan_tpid[0] |
| # |
| # elif len(flow.push_vlan_id) == 2 and self._evc.flow_entry.pop_vlan == 1: |
| # self._evc.men_to_uni_tag_manipulation = EVC.Men2UniManipulation.POP_OUT_TAG_ONLY |
| # self._evc.switching_method = EVC.SwitchingMethod.DOUBLE_TAGGED |
| # # self._match_ce_vlan_id = 'something maybe' |
| # raise NotImplementedError('TODO: Not supported/needed yet') |
| |
| return True |
| |
| # Bulk operations |
| |
| @staticmethod |
| def remove_all(client, regex_=EVC_MAP_NAME_REGEX_ALL): |
| """ |
| Remove all matching EVC Maps from hardware |
| |
| :param client: (ncclient) NETCONF Client to use |
| :param regex_: (String) Regular expression for name matching |
| :return: (deferred) |
| """ |
| # Do a 'get' on the evc-map config an you should get the names |
| get_xml = """ |
| <filter> |
| <evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"> |
| <evc-map> |
| <name/> |
| </evc-map> |
| </evc-maps> |
| </filter> |
| """ |
| log.info('query', xml=get_xml, regex=regex_) |
| |
| def request_failed(results, operation): |
| log.error('{}-failed'.format(operation), results=results) |
| # No further actions. Periodic poll later on will scrub any old EVC-Maps if needed |
| |
| def delete_complete(results): |
| log.debug('delete-complete', results=results) |
| |
| def do_delete(rpc_reply, regexpr): |
| log.debug('query-complete', rpc_reply=rpc_reply) |
| |
| if rpc_reply.ok: |
| result_dict = xmltodict.parse(rpc_reply.data_xml) |
| entries = result_dict['data']['evc-maps'] if 'evc-maps' in result_dict['data'] else {} |
| |
| if 'evc-map' in entries: |
| p = re.compile(regexpr) |
| |
| if isinstance(entries['evc-map'], list): |
| names = {entry['name'] for entry in entries['evc-map'] |
| if 'name' in entry and p.match(entry['name'])} |
| else: |
| names = set() |
| for item in entries['evc-map'].items(): |
| if isinstance(item, tuple) and item[0] == 'name': |
| names.add(item[1]) |
| break |
| |
| if len(names) > 0: |
| del_xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"' + \ |
| ' xc:operation = "delete">' |
| for name in names: |
| del_xml += '<evc-map>' |
| del_xml += '<name>{}</name>'.format(name) |
| del_xml += '</evc-map>' |
| del_xml += '</evc-maps>' |
| log.debug('removing', xml=del_xml) |
| |
| return client.edit_config(del_xml, lock_timeout=30) |
| |
| return succeed('no entries') |
| |
| d = client.get(get_xml) |
| d.addCallbacks(do_delete, request_failed, callbackArgs=[regex_], errbackArgs=['get']) |
| d.addCallbacks(delete_complete, request_failed, errbackArgs=['edit-config']) |
| return d |