blob: 170d99741a72b2f10d7951589f1cc4920c765440 [file] [log] [blame]
Chip Boling7294b252017-06-15 16:16:55 -05001#
2# Copyright 2017-present Adtran, Inc.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
Chip Bolingab8863d2018-03-22 14:50:31 -050016import xmltodict
17import re
Chip Boling7294b252017-06-15 16:16:55 -050018import structlog
Chip Bolingab8863d2018-03-22 14:50:31 -050019from twisted.internet.defer import inlineCallbacks, returnValue, succeed
Chip Boling7294b252017-06-15 16:16:55 -050020
21log = structlog.get_logger()
22
Chip Boling8956b642018-04-11 13:47:01 -050023_acl_list = {} # Key -> device-id -> Name: List of encoded EVCs
Chip Boling7294b252017-06-15 16:16:55 -050024
Chip Bolingab8863d2018-03-22 14:50:31 -050025ACL_NAME_FORMAT = 'VOLTHA-ACL-{}-{}' # format(flow_entry.handler.device_id, flow_entry.flow.id)
26ACL_NAME_REGEX_ALL = 'VOLTHA-ACL-*'
Chip Boling7294b252017-06-15 16:16:55 -050027
28class ACL(object):
29 """
30 Class to wrap Trap-to-Controller functionality
31 """
Chip Boling7294b252017-06-15 16:16:55 -050032 def __init__(self, flow_entry):
33 self._installed = False
34 self._status_message = None
35 self._parent = flow_entry # FlowEntry parent
36 self._flow = flow_entry.flow
37 self._handler = flow_entry.handler
Chip Boling5561d552017-07-07 15:11:26 -050038 self._name = ACL.flow_to_name(flow_entry)
Chip Bolingab8863d2018-03-22 14:50:31 -050039 self._rule_name = ACL.flow_to_ace_name(flow_entry)
40 self._eth_type = flow_entry.eth_type
41 self._ip_protocol = flow_entry.ip_protocol
42 self._ipv4_dst = flow_entry.ipv4_dst
43 self._src_port = flow_entry.udp_src
44 self._dst_port = flow_entry.udp_dst
45 self._exception = False
46 self._enabled = True
Chip Boling7294b252017-06-15 16:16:55 -050047 self._valid = self._decode()
48
Chip Bolingab8863d2018-03-22 14:50:31 -050049 def __str__(self):
50 return 'ACL: {}, Installed: {}, L2: {}, L3/4: {}'.format(self.name,
51 self._installed,
52 self.is_l2_exception,
53 self.is_l3_l4_exception)
54
55 @property
56 def name(self):
57 return self._name
58
59 @property
60 def installed(self):
61 return self._installed
62
63 @property
64 def is_l2_exception(self):
65 from flow_entry import FlowEntry
66 return self._eth_type not in (None,
67 FlowEntry.EtherType.IPv4,
68 FlowEntry.EtherType.IPv6)
69
70 @property
71 def is_l3_l4_exception(self):
72 return not self.is_l2_exception and self._ip_protocol is not None
73
74 @staticmethod
75 def _xml_header(operation=None):
76 return '<access-lists xmlns="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list"\
77 xmlns:adtn-ietf-ns-acl="http://www.adtran.com/ns/yang/adtran-ietf-ns-access-control-list"><acl{}>'.\
78 format('' if operation is None else ' xc:operation="{}"'.format(operation))
79
80 @staticmethod
81 def _xml_trailer():
82 return '</acl></access-lists>'
83
84 def _xml_action(self):
85 xml = '<actions>'
86 if self._exception:
87 xml += '<adtn-ietf-ns-acl:exception-to-cpu/>'
88 else:
89 xml += '<permit/>'
90 xml += '</actions>'
91 return xml
92
93 def _ace_l2(self):
94 xml = '<ace>'
95 xml += '<rule-name>{}</rule-name>'.format(self._rule_name)
96 xml += '<matches><l2-acl><ether-type>{:04x}</ether-type></l2-acl></matches>'.format(self._eth_type)
97 xml += self._xml_action()
98 xml += '</ace>'
99 return xml
100
101 def _ace_l2_l3_ipv4(self):
102 xml = '<ace>'
103 xml += '<rule-name>{}</rule-name>'.format(self._rule_name)
104 xml += '<matches><l2-l3-ipv4-acl>'
105 xml += '<ether-type>{:04X}</ether-type>'.format(self._eth_type)
106
107 if self._ip_protocol is not None:
108 xml += '<protocol>{}</protocol>'.format(self._ip_protocol)
109 if self._ipv4_dst is not None:
110 xml += '<destination-ipv4-network>{}/32</destination-ipv4-network>'.format(self._ipv4_dst)
111 if self._src_port is not None:
112 xml += '<source-port-range><lower-port>{}</lower-port><operation>eq</operation></source-port-range>'.\
113 format(self._src_port)
114 if self._dst_port is not None:
115 xml += '<destination-port-range><lower-port>' + \
116 '{}</lower-port><operations>eq</operations></destination-port-range>'.format(self._dst_port)
117
118 xml += '</l2-l3-ipv4-acl></matches>'
119 xml += self._xml_action()
120 xml += '</ace>'
121 return xml
122
123 def _ace_any(self):
124 xml = '<ace>'
125 xml += '<rule-name>{}</rule-name>'.format(self._rule_name)
126 xml += '<matches><any-acl/></matches>'
127 xml += self._xml_action()
128 xml += '</ace>'
129 return xml
130
131 def _acl_eth(self):
132 xml = '<acl-type>eth-acl</acl-type>'
133 xml += '<acl-name>{}</acl-name>'.format(self._name)
134 return xml
135
136 def _acl_l4(self):
137 xml = '<acl-type>mixed-l2-l3-ipv4-acl</acl-type>'
138 xml += '<acl-name>{}</acl-name>'.format(self._name)
139 return xml
140
141 def _acl_any(self):
142 xml = '<acl-type>any-acl</acl-type>'
143 xml += '<acl-name>{}</acl-name>'.format(self._name)
144 return xml
145
146 def _install_xml(self):
147 xml = ACL._xml_header('create')
148 if self.is_l2_exception:
149 xml += self._acl_eth()
150 xml += '<aces>{}</aces>'.format(self._ace_l2())
151 elif self.is_l3_l4_exception:
152 xml += self._acl_l4()
153 xml += '<aces>{}</aces>'.format(self._ace_l2_l3_ipv4())
154 else:
155 xml += self._acl_any()
156 xml += '<aces>{}</aces>'.format(self._ace_any())
157
158 xml += ACL._xml_trailer()
159 return xml
160
161 def _remove_xml(self):
162 xml = ACL._xml_header('delete')
163 if self.is_l2_exception:
164 xml += self._acl_eth()
165 elif self.is_l3_l4_exception:
166 xml += self._acl_l4()
167 else:
168 xml += self._acl_any()
169 xml += ACL._xml_trailer()
170 return xml
171
172 def evc_map_ingress_xml(self):
173 """ Individual ACL specific XML for the EVC MAP """
174
175 xml = '<adtn-evc-map-acl:acl-type '
176 fmt = 'xmlns:adtn-ietf-acl="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list">adtn-ietf-acl:{}'\
177 '</adtn-evc-map-acl:acl-type>'
178
179 if self.is_l2_exception:
180 xml += fmt.format('eth-acl')
181
182 elif self.is_l3_l4_exception:
183 xml += fmt.format('mixed-l2-l3-ipv4-acl')
184
185 else:
186 xml += fmt.format('any-acl')
187
188 xml += '<adtn-evc-map-acl:acl-name>{}</adtn-evc-map-acl:acl-name>'.format(self.name)
189 return xml
190
Chip Boling7294b252017-06-15 16:16:55 -0500191 @staticmethod
192 def create(flow_entry):
Chip Bolingab8863d2018-03-22 14:50:31 -0500193 return ACL(flow_entry)
Chip Boling7294b252017-06-15 16:16:55 -0500194
195 @staticmethod
Chip Boling5561d552017-07-07 15:11:26 -0500196 def flow_to_name(flow_entry):
Chip Bolingab8863d2018-03-22 14:50:31 -0500197 return 'VOLTHA-ACL-{}-{}'.format(flow_entry.handler.device_id, flow_entry.flow.id)
198
199 @staticmethod
200 def flow_to_ace_name(flow_entry):
201 return 'VOLTHA-ACE-{}-{}'.format(flow_entry.handler.device_id, flow_entry.flow.id)
Chip Boling7294b252017-06-15 16:16:55 -0500202
203 @property
204 def valid(self):
205 return self._valid
206
207 @property
208 def installed(self):
209 return self._installed
210
211 @property
212 def status(self):
213 return self._status_message
214
Chip Bolingab8863d2018-03-22 14:50:31 -0500215 @inlineCallbacks
Chip Boling7294b252017-06-15 16:16:55 -0500216 def install(self):
Chip Bolingab8863d2018-03-22 14:50:31 -0500217 log.debug('installing-acl', installed=self._installed)
218
219 if not self._installed and self._enabled:
Chip Boling8956b642018-04-11 13:47:01 -0500220 if self._handler.device_id not in _acl_list:
221 _acl_list[self._handler.device_id] = {}
222
223 acls_installed = _acl_list[self._handler.device_id]
224 if self._name in acls_installed:
225 self._status_message = "ACL '{}' id already installed".format(self._name)
Chip Bolingab8863d2018-03-22 14:50:31 -0500226 raise Exception(self._status_message)
Chip Boling7294b252017-06-15 16:16:55 -0500227
Chip Bolingab8863d2018-03-22 14:50:31 -0500228 try:
229 acl_xml = self._install_xml()
230 log.debug('install-xml', xml=acl_xml, name=self._name)
Chip Boling7294b252017-06-15 16:16:55 -0500231
Chip Bolingab8863d2018-03-22 14:50:31 -0500232 results = yield self._handler.netconf_client.edit_config(acl_xml)
233 self._installed = results.ok
234 self._status_message = '' if results.ok else results.error
Chip Boling7294b252017-06-15 16:16:55 -0500235
Chip Bolingab8863d2018-03-22 14:50:31 -0500236 if self._installed:
Chip Boling8956b642018-04-11 13:47:01 -0500237 acls_installed[self._name] = self
Chip Boling7294b252017-06-15 16:16:55 -0500238
Chip Bolingab8863d2018-03-22 14:50:31 -0500239 except Exception as e:
240 log.exception('install-failure', name=self._name, e=e)
241 raise
242
243 returnValue(self._installed and self._enabled)
244
245 @inlineCallbacks
Chip Boling7294b252017-06-15 16:16:55 -0500246 def remove(self):
Chip Bolingab8863d2018-03-22 14:50:31 -0500247 log.debug('removing-acl', installed=self._installed)
248
Chip Boling7294b252017-06-15 16:16:55 -0500249 if self._installed:
Chip Bolingab8863d2018-03-22 14:50:31 -0500250 acl_xml = self._remove_xml()
251 log.info('remove-xml', xml=acl_xml, name=self._name)
Chip Boling7294b252017-06-15 16:16:55 -0500252
Chip Bolingab8863d2018-03-22 14:50:31 -0500253 results = yield self._handler.netconf_client.edit_config(acl_xml)
254 self._installed = not results.ok
255 self._status_message = '' if results.ok else results.error
Chip Boling7294b252017-06-15 16:16:55 -0500256
Chip Bolingab8863d2018-03-22 14:50:31 -0500257 if not self._installed:
Chip Boling8956b642018-04-11 13:47:01 -0500258 acls_installed = _acl_list.get(self._handler.device_id)
259 if acls_installed is not None and self._name in acls_installed:
260 del acls_installed[self._name]
Chip Bolingab8863d2018-03-22 14:50:31 -0500261
262 returnValue(not self._installed)
Chip Boling7294b252017-06-15 16:16:55 -0500263
264 def enable(self):
265 if not self._enabled:
Chip Boling7294b252017-06-15 16:16:55 -0500266 self._enabled = False
Chip Bolingab8863d2018-03-22 14:50:31 -0500267 raise NotImplemented("TODO: Implement this")
Chip Boling7294b252017-06-15 16:16:55 -0500268
269 def disable(self):
270 if self._enabled:
Chip Boling7294b252017-06-15 16:16:55 -0500271 self._enabled = True
Chip Bolingab8863d2018-03-22 14:50:31 -0500272 raise NotImplemented("TODO: Implement this")
Chip Boling7294b252017-06-15 16:16:55 -0500273
274 def _decode(self):
275 """
Chip Bolingab8863d2018-03-22 14:50:31 -0500276 Examine the field settings and set ACL up for requested fields
Chip Boling7294b252017-06-15 16:16:55 -0500277 """
Chip Bolingab8863d2018-03-22 14:50:31 -0500278 # If EtherType is not None and not IP, this is an L2 exception
279 self._exception = self.is_l2_exception or self.is_l3_l4_exception
Chip Boling7294b252017-06-15 16:16:55 -0500280 return True
281
282 # BULK operations
283
284 @staticmethod
285 def enable_all():
286 raise NotImplemented("TODO: Implement this")
287
288 @staticmethod
289 def disable_all():
290 raise NotImplemented("TODO: Implement this")
291
292 @staticmethod
Chip Bolingab8863d2018-03-22 14:50:31 -0500293 def remove_all(client, regex_=ACL_NAME_REGEX_ALL):
Chip Boling7294b252017-06-15 16:16:55 -0500294 """
Chip Bolingab8863d2018-03-22 14:50:31 -0500295 Remove all matching ACLs from hardware
296 :param client: (ncclient) NETCONF Client to use
297 :param regex_: (String) Regular expression for name matching
298 :return: (deferred)
Chip Boling7294b252017-06-15 16:16:55 -0500299 """
Chip Bolingab8863d2018-03-22 14:50:31 -0500300 # Do a 'get' on the evc config an you should get the names
301 get_xml = """
302 <filter>
303 <access-lists xmlns="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list">
304 <acl><acl-type/><acl-name/></acl>
305 </access-lists>
306 </filter>
307 """
308 log.info('query', xml=get_xml, regex=regex_)
Chip Boling7294b252017-06-15 16:16:55 -0500309
Chip Bolingab8863d2018-03-22 14:50:31 -0500310 def request_failed(results, operation):
311 log.error('{}-failed'.format(operation), results=results)
312
313 def delete_complete(results):
314 log.debug('delete-complete', results=results)
315
316 def do_delete(rpc_reply, regexpr):
317 log.debug('query-complete', rpc_reply=rpc_reply)
318
319 if rpc_reply.ok:
320 result_dict = xmltodict.parse(rpc_reply.data_xml)
321 entries = result_dict['data']['access-lists'] if 'access-lists' in result_dict['data'] else {}
322
323 if 'acl' in entries:
324 p = re.compile(regexpr)
325
326 pairs = []
327 if isinstance(entries['acl'], list):
328 pairs = { (entry['acl-type'], entry['acl-name']) for entry in entries['acl']
329 if 'acl-name' in entry and 'acl-type' in entry and p.match(entry['acl-name'])}
330 else:
331 if 'acl' in entries:
332 entry = entries['acl']
333 if 'acl-name' in entry and 'acl-type' in entry and p.match(entry['acl-name']):
334 pairs = [ (entry['acl-type'], entry['acl-name']) ]
335
336 if len(pairs) > 0:
337 del_xml = '<access-lists xmlns="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list">'
338 for pair in pairs:
339 del_xml += '<acl xc:operation = "delete">'
340 del_xml += '<acl-type>{}</acl-type>'.format(pair[0])
341 del_xml += '<acl-name>{}</acl-name>'.format(pair[1])
342 del_xml += '</acl>'
343 del_xml += '</access-lists>'
344 log.debug('removing', xml=del_xml)
345
346 return client.edit_config(del_xml)
347
348 return succeed('no entries')
349
350 d = client.get(get_xml)
351 d.addCallbacks(do_delete, request_failed, callbackArgs=[regex_], errbackArgs=['get'])
352 d.addCallbacks(delete_complete, request_failed, errbackArgs=['edit-config'])
353 return d