| # |
| # 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 twisted.internet.defer import inlineCallbacks, returnValue, succeed |
| |
| log = structlog.get_logger() |
| |
| _acl_list = {} # Key -> device-id -> Name: List of encoded EVCs |
| |
| ACL_NAME_FORMAT = 'VOLTHA-ACL-{}-{}' # format(flow_entry.flow_id, flow-entry-hash) |
| ACL_NAME_REGEX_ALL = 'VOLTHA-ACL-*' |
| ACE_NAME_FORMAT = 'VOLTHA-ACE-{}' # format(flow_entry.flow_id) |
| |
| |
| class ACL(object): |
| """ |
| Class to wrap Trap-to-Controller functionality |
| """ |
| def __init__(self, flow_entry): |
| self._installed = False |
| self._status_message = None |
| self._parent = flow_entry # FlowEntry parent |
| self._flow = flow_entry.flow |
| self._handler = flow_entry.handler |
| self._name = ACL.flow_to_name(flow_entry) |
| self._rule_name = ACL.flow_to_ace_name(flow_entry) |
| self._eth_type = flow_entry.eth_type |
| self._ip_protocol = flow_entry.ip_protocol |
| self._ipv4_dst = flow_entry.ipv4_dst |
| self._src_port = flow_entry.udp_src |
| self._dst_port = flow_entry.udp_dst |
| self._exception = False |
| self._enabled = True |
| self._valid = self._decode() |
| |
| def __str__(self): |
| return 'ACL: {}, Installed: {}, L2: {}, L3/4: {}'.\ |
| format(self.name, self._installed, self.is_l2_exception, |
| self.is_l3_l4_exception) |
| |
| @property |
| def name(self): |
| return self._name |
| |
| @property |
| def installed(self): |
| return self._installed |
| |
| @property |
| def is_l2_exception(self): |
| from flow_entry import FlowEntry |
| return self._eth_type not in (None, |
| FlowEntry.EtherType.IPv4, |
| FlowEntry.EtherType.IPv6) |
| |
| @property |
| def is_l3_l4_exception(self): |
| return not self.is_l2_exception and self._ip_protocol is not None |
| |
| @staticmethod |
| def _xml_header(operation=None): |
| return '<access-lists xmlns="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list"\ |
| xmlns:adtn-ietf-ns-acl="http://www.adtran.com/ns/yang/adtran-ietf-ns-access-control-list"><acl{}>'.\ |
| format('' if operation is None else ' xc:operation="{}"'.format(operation)) |
| |
| @staticmethod |
| def _xml_trailer(): |
| return '</acl></access-lists>' |
| |
| def _xml_action(self): |
| xml = '<actions>' |
| if self._exception: |
| xml += '<adtn-ietf-ns-acl:exception-to-cpu/>' |
| else: |
| xml += '<permit/>' |
| xml += '</actions>' |
| return xml |
| |
| def _ace_l2(self): |
| xml = '<ace>' |
| xml += '<rule-name>{}</rule-name>'.format(self._rule_name) |
| xml += '<matches><l2-acl><ether-type>{:04x}</ether-type></l2-acl></matches>'.format(self._eth_type) |
| xml += self._xml_action() |
| xml += '</ace>' |
| return xml |
| |
| def _ace_l2_l3_ipv4(self): |
| xml = '<ace>' |
| xml += '<rule-name>{}</rule-name>'.format(self._rule_name) |
| xml += '<matches><l2-l3-ipv4-acl>' |
| xml += '<ether-type>{:04X}</ether-type>'.format(self._eth_type) |
| |
| if self._ip_protocol is not None: |
| xml += '<protocol>{}</protocol>'.format(self._ip_protocol) |
| if self._ipv4_dst is not None: |
| xml += '<destination-ipv4-network>{}/32</destination-ipv4-network>'.format(self._ipv4_dst) |
| if self._src_port is not None: |
| xml += '<source-port-range><lower-port>{}</lower-port><operation>eq</operation></source-port-range>'.\ |
| format(self._src_port) |
| if self._dst_port is not None: |
| xml += '<destination-port-range><lower-port>' + \ |
| '{}</lower-port><operations>eq</operations></destination-port-range>'.format(self._dst_port) |
| |
| xml += '</l2-l3-ipv4-acl></matches>' |
| xml += self._xml_action() |
| xml += '</ace>' |
| return xml |
| |
| def _ace_any(self): |
| xml = '<ace>' |
| xml += '<rule-name>{}</rule-name>'.format(self._rule_name) |
| xml += '<matches><any-acl/></matches>' |
| xml += self._xml_action() |
| xml += '</ace>' |
| return xml |
| |
| def _acl_eth(self): |
| xml = '<acl-type>eth-acl</acl-type>' |
| xml += '<acl-name>{}</acl-name>'.format(self._name) |
| return xml |
| |
| def _acl_l4(self): |
| xml = '<acl-type>mixed-l2-l3-ipv4-acl</acl-type>' |
| xml += '<acl-name>{}</acl-name>'.format(self._name) |
| return xml |
| |
| def _acl_any(self): |
| xml = '<acl-type>any-acl</acl-type>' |
| xml += '<acl-name>{}</acl-name>'.format(self._name) |
| return xml |
| |
| def _install_xml(self): |
| xml = ACL._xml_header('create') |
| if self.is_l2_exception: |
| xml += self._acl_eth() |
| xml += '<aces>{}</aces>'.format(self._ace_l2()) |
| elif self.is_l3_l4_exception: |
| xml += self._acl_l4() |
| xml += '<aces>{}</aces>'.format(self._ace_l2_l3_ipv4()) |
| else: |
| xml += self._acl_any() |
| xml += '<aces>{}</aces>'.format(self._ace_any()) |
| |
| xml += ACL._xml_trailer() |
| return xml |
| |
| def _remove_xml(self): |
| xml = ACL._xml_header('delete') |
| if self.is_l2_exception: |
| xml += self._acl_eth() |
| elif self.is_l3_l4_exception: |
| xml += self._acl_l4() |
| else: |
| xml += self._acl_any() |
| xml += ACL._xml_trailer() |
| return xml |
| |
| def evc_map_ingress_xml(self): |
| """ Individual ACL specific XML for the EVC MAP """ |
| |
| xml = '<adtn-evc-map-acl:acl-type ' |
| fmt = 'xmlns:adtn-ietf-acl="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list">adtn-ietf-acl:{}'\ |
| '</adtn-evc-map-acl:acl-type>' |
| |
| if self.is_l2_exception: |
| xml += fmt.format('eth-acl') |
| |
| elif self.is_l3_l4_exception: |
| xml += fmt.format('mixed-l2-l3-ipv4-acl') |
| |
| else: |
| xml += fmt.format('any-acl') |
| |
| xml += '<adtn-evc-map-acl:acl-name>{}</adtn-evc-map-acl:acl-name>'.format(self.name) |
| return xml |
| |
| @staticmethod |
| def create(flow_entry): |
| acl = ACL(flow_entry) |
| |
| # Already created and installed, return that one |
| acls_installed = _acl_list.get(flow_entry.handler.device_id) |
| if acls_installed is not None: |
| entry = acls_installed.get(acl._name) |
| if entry is not None: |
| return entry |
| |
| return acl |
| |
| @staticmethod |
| def flow_to_name(flow_entry): |
| return ACL_NAME_FORMAT.format(flow_entry.flow_id, ACL.acl_hash(flow_entry)) |
| |
| @staticmethod |
| def flow_to_ace_name(flow_entry): |
| return ACE_NAME_FORMAT.format(flow_entry.flow_id) |
| |
| @staticmethod |
| def acl_hash(flow_entry): |
| from hashlib import md5 |
| in_port = flow_entry.in_port or 0 |
| eth_type = flow_entry.eth_type or 0 |
| ip_protocol = flow_entry.ip_protocol or 0 |
| ipv4_dst = flow_entry.ipv4_dst or 0 |
| src_port = flow_entry.udp_src or 0 |
| dst_port = flow_entry.udp_dst or 0 |
| hex_string = md5('{},{},{},{},{},{}'.format(in_port, eth_type, ip_protocol, |
| ipv4_dst, src_port, dst_port)).hexdigest() |
| return hex_string |
| |
| @property |
| def valid(self): |
| return self._valid |
| |
| @property |
| def installed(self): |
| return self._installed |
| |
| @property |
| def status(self): |
| return self._status_message |
| |
| @inlineCallbacks |
| def install(self): |
| log.debug('installing-acl', installed=self._installed) |
| |
| if not self._installed and self._enabled: |
| if self._handler.device_id not in _acl_list: |
| _acl_list[self._handler.device_id] = {} |
| |
| acls_installed = _acl_list[self._handler.device_id] |
| if self._name in acls_installed: |
| # Return OK |
| returnValue(self._enabled) |
| |
| try: |
| acl_xml = self._install_xml() |
| log.debug('install-xml', xml=acl_xml, name=self._name) |
| |
| results = yield self._handler.netconf_client.edit_config(acl_xml) |
| self._installed = results.ok |
| self._status_message = '' if results.ok else results.error |
| |
| if self._installed: |
| acls_installed[self._name] = self |
| |
| except Exception as e: |
| log.exception('install-failure', name=self._name, e=e) |
| raise |
| |
| returnValue(self._installed and self._enabled) |
| |
| @inlineCallbacks |
| def remove(self): |
| log.debug('removing-acl', installed=self._installed) |
| |
| if self._installed: |
| acl_xml = self._remove_xml() |
| log.info('remove-xml', xml=acl_xml, name=self._name) |
| |
| results = yield self._handler.netconf_client.edit_config(acl_xml) |
| self._installed = not results.ok |
| self._status_message = '' if results.ok else results.error |
| |
| if not self._installed: |
| acls_installed = _acl_list.get(self._handler.device_id) |
| if acls_installed is not None and self._name in acls_installed: |
| del acls_installed[self._name] |
| |
| returnValue(not self._installed) |
| |
| def enable(self): |
| if not self._enabled: |
| self._enabled = False |
| raise NotImplemented("TODO: Implement this") |
| |
| def disable(self): |
| if self._enabled: |
| self._enabled = True |
| raise NotImplemented("TODO: Implement this") |
| |
| def _decode(self): |
| """ |
| Examine the field settings and set ACL up for requested fields |
| """ |
| # If EtherType is not None and not IP, this is an L2 exception |
| self._exception = self.is_l2_exception or self.is_l3_l4_exception |
| return True |
| |
| # BULK operations |
| |
| @staticmethod |
| def enable_all(): |
| raise NotImplemented("TODO: Implement this") |
| |
| @staticmethod |
| def disable_all(): |
| raise NotImplemented("TODO: Implement this") |
| |
| @staticmethod |
| def clear_all(device_id): |
| """ |
| Clear all acls for this device id from the list |
| :param device_id: id of the device |
| """ |
| if device_id in _acl_list: |
| del _acl_list[device_id] |
| |
| @staticmethod |
| def remove_all(client, regex_=ACL_NAME_REGEX_ALL): |
| """ |
| Remove all matching ACLs 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 config an you should get the names |
| get_xml = """ |
| <filter> |
| <access-lists xmlns="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list"> |
| <acl><acl-type/><acl-name/></acl> |
| </access-lists> |
| </filter> |
| """ |
| log.debug('query', xml=get_xml, regex=regex_) |
| |
| def request_failed(results, operation): |
| log.error('{}-failed'.format(operation), results=results) |
| |
| 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']['access-lists'] if 'access-lists' in result_dict['data'] else {} |
| |
| if 'acl' in entries: |
| p = re.compile(regexpr) |
| |
| pairs = [] |
| if isinstance(entries['acl'], list): |
| pairs = {(entry['acl-type'], entry['acl-name']) for entry in entries['acl'] |
| if 'acl-name' in entry and 'acl-type' in entry and p.match(entry['acl-name'])} |
| else: |
| if 'acl' in entries: |
| entry = entries['acl'] |
| if 'acl-name' in entry and 'acl-type' in entry and p.match(entry['acl-name']): |
| pairs = [(entry['acl-type'], entry['acl-name'])] |
| |
| if len(pairs) > 0: |
| del_xml = '<access-lists xmlns="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list">' |
| for pair in pairs: |
| del_xml += '<acl xc:operation = "delete">' |
| del_xml += '<acl-type>{}</acl-type>'.format(pair[0]) |
| del_xml += '<acl-name>{}</acl-name>'.format(pair[1]) |
| del_xml += '</acl>' |
| del_xml += '</access-lists>' |
| log.debug('removing', xml=del_xml) |
| |
| return client.edit_config(del_xml) |
| |
| 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 |