blob: 67f8c083233b9064d483444fba03870ecaf1fd2c [file] [log] [blame]
Chip Bolingf5af85d2019-02-12 15:36:17 -06001#
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#
16import xmltodict
17import re
18import structlog
19from twisted.internet.defer import inlineCallbacks, returnValue, succeed
20
21log = structlog.get_logger()
22
23_acl_list = {} # Key -> device-id -> Name: List of encoded EVCs
24
25ACL_NAME_FORMAT = 'VOLTHA-ACL-{}-{}' # format(flow_entry.flow_id, flow-entry-hash)
26ACL_NAME_REGEX_ALL = 'VOLTHA-ACL-*'
27ACE_NAME_FORMAT = 'VOLTHA-ACE-{}' # format(flow_entry.flow_id)
28
29
30class ACL(object):
31 """
32 Class to wrap Trap-to-Controller functionality
33 """
34 def __init__(self, flow_entry):
35 self._installed = False
36 self._status_message = None
37 self._parent = flow_entry # FlowEntry parent
38 self._flow = flow_entry.flow
39 self._handler = flow_entry.handler
40 self._name = ACL.flow_to_name(flow_entry)
41 self._rule_name = ACL.flow_to_ace_name(flow_entry)
42 self._eth_type = flow_entry.eth_type
43 self._ip_protocol = flow_entry.ip_protocol
44 self._ipv4_dst = flow_entry.ipv4_dst
45 self._src_port = flow_entry.udp_src
46 self._dst_port = flow_entry.udp_dst
47 self._exception = False
48 self._enabled = True
49 self._valid = self._decode()
50
51 def __str__(self):
52 return 'ACL: {}, Installed: {}, L2: {}, L3/4: {}'.\
53 format(self.name, self._installed, self.is_l2_exception,
54 self.is_l3_l4_exception)
55
56 @property
57 def name(self):
58 return self._name
59
60 @property
61 def installed(self):
62 return self._installed
63
64 @property
65 def is_l2_exception(self):
66 from flow_entry import FlowEntry
67 return self._eth_type not in (None,
68 FlowEntry.EtherType.IPv4,
69 FlowEntry.EtherType.IPv6)
70
71 @property
72 def is_l3_l4_exception(self):
73 return not self.is_l2_exception and self._ip_protocol is not None
74
75 @staticmethod
76 def _xml_header(operation=None):
77 return '<access-lists xmlns="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list"\
78 xmlns:adtn-ietf-ns-acl="http://www.adtran.com/ns/yang/adtran-ietf-ns-access-control-list"><acl{}>'.\
79 format('' if operation is None else ' xc:operation="{}"'.format(operation))
80
81 @staticmethod
82 def _xml_trailer():
83 return '</acl></access-lists>'
84
85 def _xml_action(self):
86 xml = '<actions>'
87 if self._exception:
88 xml += '<adtn-ietf-ns-acl:exception-to-cpu/>'
89 else:
90 xml += '<permit/>'
91 xml += '</actions>'
92 return xml
93
94 def _ace_l2(self):
95 xml = '<ace>'
96 xml += '<rule-name>{}</rule-name>'.format(self._rule_name)
97 xml += '<matches><l2-acl><ether-type>{:04x}</ether-type></l2-acl></matches>'.format(self._eth_type)
98 xml += self._xml_action()
99 xml += '</ace>'
100 return xml
101
102 def _ace_l2_l3_ipv4(self):
103 xml = '<ace>'
104 xml += '<rule-name>{}</rule-name>'.format(self._rule_name)
105 xml += '<matches><l2-l3-ipv4-acl>'
106 xml += '<ether-type>{:04X}</ether-type>'.format(self._eth_type)
107
108 if self._ip_protocol is not None:
109 xml += '<protocol>{}</protocol>'.format(self._ip_protocol)
110 if self._ipv4_dst is not None:
111 xml += '<destination-ipv4-network>{}/32</destination-ipv4-network>'.format(self._ipv4_dst)
112 if self._src_port is not None:
113 xml += '<source-port-range><lower-port>{}</lower-port><operation>eq</operation></source-port-range>'.\
114 format(self._src_port)
115 if self._dst_port is not None:
116 xml += '<destination-port-range><lower-port>' + \
117 '{}</lower-port><operations>eq</operations></destination-port-range>'.format(self._dst_port)
118
119 xml += '</l2-l3-ipv4-acl></matches>'
120 xml += self._xml_action()
121 xml += '</ace>'
122 return xml
123
124 def _ace_any(self):
125 xml = '<ace>'
126 xml += '<rule-name>{}</rule-name>'.format(self._rule_name)
127 xml += '<matches><any-acl/></matches>'
128 xml += self._xml_action()
129 xml += '</ace>'
130 return xml
131
132 def _acl_eth(self):
133 xml = '<acl-type>eth-acl</acl-type>'
134 xml += '<acl-name>{}</acl-name>'.format(self._name)
135 return xml
136
137 def _acl_l4(self):
138 xml = '<acl-type>mixed-l2-l3-ipv4-acl</acl-type>'
139 xml += '<acl-name>{}</acl-name>'.format(self._name)
140 return xml
141
142 def _acl_any(self):
143 xml = '<acl-type>any-acl</acl-type>'
144 xml += '<acl-name>{}</acl-name>'.format(self._name)
145 return xml
146
147 def _install_xml(self):
148 xml = ACL._xml_header('create')
149 if self.is_l2_exception:
150 xml += self._acl_eth()
151 xml += '<aces>{}</aces>'.format(self._ace_l2())
152 elif self.is_l3_l4_exception:
153 xml += self._acl_l4()
154 xml += '<aces>{}</aces>'.format(self._ace_l2_l3_ipv4())
155 else:
156 xml += self._acl_any()
157 xml += '<aces>{}</aces>'.format(self._ace_any())
158
159 xml += ACL._xml_trailer()
160 return xml
161
162 def _remove_xml(self):
163 xml = ACL._xml_header('delete')
164 if self.is_l2_exception:
165 xml += self._acl_eth()
166 elif self.is_l3_l4_exception:
167 xml += self._acl_l4()
168 else:
169 xml += self._acl_any()
170 xml += ACL._xml_trailer()
171 return xml
172
173 def evc_map_ingress_xml(self):
174 """ Individual ACL specific XML for the EVC MAP """
175
176 xml = '<adtn-evc-map-acl:acl-type '
177 fmt = 'xmlns:adtn-ietf-acl="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list">adtn-ietf-acl:{}'\
178 '</adtn-evc-map-acl:acl-type>'
179
180 if self.is_l2_exception:
181 xml += fmt.format('eth-acl')
182
183 elif self.is_l3_l4_exception:
184 xml += fmt.format('mixed-l2-l3-ipv4-acl')
185
186 else:
187 xml += fmt.format('any-acl')
188
189 xml += '<adtn-evc-map-acl:acl-name>{}</adtn-evc-map-acl:acl-name>'.format(self.name)
190 return xml
191
192 @staticmethod
193 def create(flow_entry):
194 acl = ACL(flow_entry)
195
196 # Already created and installed, return that one
197 acls_installed = _acl_list.get(flow_entry.handler.device_id)
198 if acls_installed is not None:
199 entry = acls_installed.get(acl._name)
200 if entry is not None:
201 return entry
202
203 return acl
204
205 @staticmethod
206 def flow_to_name(flow_entry):
207 return ACL_NAME_FORMAT.format(flow_entry.flow_id, ACL.acl_hash(flow_entry))
208
209 @staticmethod
210 def flow_to_ace_name(flow_entry):
211 return ACE_NAME_FORMAT.format(flow_entry.flow_id)
212
213 @staticmethod
214 def acl_hash(flow_entry):
215 from hashlib import md5
216 in_port = flow_entry.in_port or 0
217 eth_type = flow_entry.eth_type or 0
218 ip_protocol = flow_entry.ip_protocol or 0
219 ipv4_dst = flow_entry.ipv4_dst or 0
220 src_port = flow_entry.udp_src or 0
221 dst_port = flow_entry.udp_dst or 0
222 hex_string = md5('{},{},{},{},{},{}'.format(in_port, eth_type, ip_protocol,
223 ipv4_dst, src_port, dst_port)).hexdigest()
224 return hex_string
225
226 @property
227 def valid(self):
228 return self._valid
229
230 @property
231 def installed(self):
232 return self._installed
233
234 @property
235 def status(self):
236 return self._status_message
237
238 @inlineCallbacks
239 def install(self):
240 log.debug('installing-acl', installed=self._installed)
241
242 if not self._installed and self._enabled:
243 if self._handler.device_id not in _acl_list:
244 _acl_list[self._handler.device_id] = {}
245
246 acls_installed = _acl_list[self._handler.device_id]
247 if self._name in acls_installed:
248 # Return OK
249 returnValue(self._enabled)
250
251 try:
252 acl_xml = self._install_xml()
253 log.debug('install-xml', xml=acl_xml, name=self._name)
254
255 results = yield self._handler.netconf_client.edit_config(acl_xml)
256 self._installed = results.ok
257 self._status_message = '' if results.ok else results.error
258
259 if self._installed:
260 acls_installed[self._name] = self
261
262 except Exception as e:
263 log.exception('install-failure', name=self._name, e=e)
264 raise
265
266 returnValue(self._installed and self._enabled)
267
268 @inlineCallbacks
269 def remove(self):
270 log.debug('removing-acl', installed=self._installed)
271
272 if self._installed:
273 acl_xml = self._remove_xml()
274 log.info('remove-xml', xml=acl_xml, name=self._name)
275
276 results = yield self._handler.netconf_client.edit_config(acl_xml)
277 self._installed = not results.ok
278 self._status_message = '' if results.ok else results.error
279
280 if not self._installed:
281 acls_installed = _acl_list.get(self._handler.device_id)
282 if acls_installed is not None and self._name in acls_installed:
283 del acls_installed[self._name]
284
285 returnValue(not self._installed)
286
287 def enable(self):
288 if not self._enabled:
289 self._enabled = False
290 raise NotImplemented("TODO: Implement this")
291
292 def disable(self):
293 if self._enabled:
294 self._enabled = True
295 raise NotImplemented("TODO: Implement this")
296
297 def _decode(self):
298 """
299 Examine the field settings and set ACL up for requested fields
300 """
301 # If EtherType is not None and not IP, this is an L2 exception
302 self._exception = self.is_l2_exception or self.is_l3_l4_exception
303 return True
304
305 # BULK operations
306
307 @staticmethod
308 def enable_all():
309 raise NotImplemented("TODO: Implement this")
310
311 @staticmethod
312 def disable_all():
313 raise NotImplemented("TODO: Implement this")
314
315 @staticmethod
316 def clear_all(device_id):
317 """
318 Clear all acls for this device id from the list
319 :param device_id: id of the device
320 """
321 if device_id in _acl_list:
322 del _acl_list[device_id]
323
324 @staticmethod
325 def remove_all(client, regex_=ACL_NAME_REGEX_ALL):
326 """
327 Remove all matching ACLs from hardware
328 :param client: (ncclient) NETCONF Client to use
329 :param regex_: (String) Regular expression for name matching
330 :return: (deferred)
331 """
332 # Do a 'get' on the evc config an you should get the names
333 get_xml = """
334 <filter>
335 <access-lists xmlns="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list">
336 <acl><acl-type/><acl-name/></acl>
337 </access-lists>
338 </filter>
339 """
340 log.debug('query', xml=get_xml, regex=regex_)
341
342 def request_failed(results, operation):
343 log.error('{}-failed'.format(operation), results=results)
344
345 def delete_complete(results):
346 log.debug('delete-complete', results=results)
347
348 def do_delete(rpc_reply, regexpr):
349 log.debug('query-complete', rpc_reply=rpc_reply)
350
351 if rpc_reply.ok:
352 result_dict = xmltodict.parse(rpc_reply.data_xml)
353 entries = result_dict['data']['access-lists'] if 'access-lists' in result_dict['data'] else {}
354
355 if 'acl' in entries:
356 p = re.compile(regexpr)
357
358 pairs = []
359 if isinstance(entries['acl'], list):
360 pairs = {(entry['acl-type'], entry['acl-name']) for entry in entries['acl']
361 if 'acl-name' in entry and 'acl-type' in entry and p.match(entry['acl-name'])}
362 else:
363 if 'acl' in entries:
364 entry = entries['acl']
365 if 'acl-name' in entry and 'acl-type' in entry and p.match(entry['acl-name']):
366 pairs = [(entry['acl-type'], entry['acl-name'])]
367
368 if len(pairs) > 0:
369 del_xml = '<access-lists xmlns="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list">'
370 for pair in pairs:
371 del_xml += '<acl xc:operation = "delete">'
372 del_xml += '<acl-type>{}</acl-type>'.format(pair[0])
373 del_xml += '<acl-name>{}</acl-name>'.format(pair[1])
374 del_xml += '</acl>'
375 del_xml += '</access-lists>'
376 log.debug('removing', xml=del_xml)
377
378 return client.edit_config(del_xml)
379
380 return succeed('no entries')
381
382 d = client.get(get_xml)
383 d.addCallbacks(do_delete, request_failed, callbackArgs=[regex_], errbackArgs=['get'])
384 d.addCallbacks(delete_complete, request_failed, errbackArgs=['edit-config'])
385 return d