Chip Boling | f5af85d | 2019-02-12 15:36:17 -0600 | [diff] [blame] | 1 | # |
| 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 | # |
| 16 | import xmltodict |
| 17 | import re |
| 18 | import structlog |
| 19 | from twisted.internet.defer import inlineCallbacks, returnValue, succeed |
| 20 | |
| 21 | log = structlog.get_logger() |
| 22 | |
| 23 | _acl_list = {} # Key -> device-id -> Name: List of encoded EVCs |
| 24 | |
| 25 | ACL_NAME_FORMAT = 'VOLTHA-ACL-{}-{}' # format(flow_entry.flow_id, flow-entry-hash) |
| 26 | ACL_NAME_REGEX_ALL = 'VOLTHA-ACL-*' |
| 27 | ACE_NAME_FORMAT = 'VOLTHA-ACE-{}' # format(flow_entry.flow_id) |
| 28 | |
| 29 | |
| 30 | class 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 |