blob: 170d99741a72b2f10d7951589f1cc4920c765440 [file] [log] [blame]
#
# 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.handler.device_id, flow_entry.flow.id)
ACL_NAME_REGEX_ALL = 'VOLTHA-ACL-*'
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):
return ACL(flow_entry)
@staticmethod
def flow_to_name(flow_entry):
return 'VOLTHA-ACL-{}-{}'.format(flow_entry.handler.device_id, flow_entry.flow.id)
@staticmethod
def flow_to_ace_name(flow_entry):
return 'VOLTHA-ACE-{}-{}'.format(flow_entry.handler.device_id, flow_entry.flow.id)
@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:
self._status_message = "ACL '{}' id already installed".format(self._name)
raise Exception(self._status_message)
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 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.info('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