Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [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 | # |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 16 | import xmltodict |
| 17 | import re |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 18 | import structlog |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 19 | from twisted.internet.defer import inlineCallbacks, returnValue, succeed |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 20 | |
| 21 | log = structlog.get_logger() |
| 22 | |
Chip Boling | 8956b64 | 2018-04-11 13:47:01 -0500 | [diff] [blame^] | 23 | _acl_list = {} # Key -> device-id -> Name: List of encoded EVCs |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 24 | |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 25 | ACL_NAME_FORMAT = 'VOLTHA-ACL-{}-{}' # format(flow_entry.handler.device_id, flow_entry.flow.id) |
| 26 | ACL_NAME_REGEX_ALL = 'VOLTHA-ACL-*' |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 27 | |
| 28 | class ACL(object): |
| 29 | """ |
| 30 | Class to wrap Trap-to-Controller functionality |
| 31 | """ |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 32 | 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 Boling | 5561d55 | 2017-07-07 15:11:26 -0500 | [diff] [blame] | 38 | self._name = ACL.flow_to_name(flow_entry) |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 39 | 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 Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 47 | self._valid = self._decode() |
| 48 | |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 49 | 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 Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 191 | @staticmethod |
| 192 | def create(flow_entry): |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 193 | return ACL(flow_entry) |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 194 | |
| 195 | @staticmethod |
Chip Boling | 5561d55 | 2017-07-07 15:11:26 -0500 | [diff] [blame] | 196 | def flow_to_name(flow_entry): |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 197 | 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 Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 202 | |
| 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 Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 215 | @inlineCallbacks |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 216 | def install(self): |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 217 | log.debug('installing-acl', installed=self._installed) |
| 218 | |
| 219 | if not self._installed and self._enabled: |
Chip Boling | 8956b64 | 2018-04-11 13:47:01 -0500 | [diff] [blame^] | 220 | 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 Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 226 | raise Exception(self._status_message) |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 227 | |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 228 | try: |
| 229 | acl_xml = self._install_xml() |
| 230 | log.debug('install-xml', xml=acl_xml, name=self._name) |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 231 | |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 232 | 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 Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 235 | |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 236 | if self._installed: |
Chip Boling | 8956b64 | 2018-04-11 13:47:01 -0500 | [diff] [blame^] | 237 | acls_installed[self._name] = self |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 238 | |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 239 | 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 Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 246 | def remove(self): |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 247 | log.debug('removing-acl', installed=self._installed) |
| 248 | |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 249 | if self._installed: |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 250 | acl_xml = self._remove_xml() |
| 251 | log.info('remove-xml', xml=acl_xml, name=self._name) |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 252 | |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 253 | 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 Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 256 | |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 257 | if not self._installed: |
Chip Boling | 8956b64 | 2018-04-11 13:47:01 -0500 | [diff] [blame^] | 258 | 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 Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 261 | |
| 262 | returnValue(not self._installed) |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 263 | |
| 264 | def enable(self): |
| 265 | if not self._enabled: |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 266 | self._enabled = False |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 267 | raise NotImplemented("TODO: Implement this") |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 268 | |
| 269 | def disable(self): |
| 270 | if self._enabled: |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 271 | self._enabled = True |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 272 | raise NotImplemented("TODO: Implement this") |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 273 | |
| 274 | def _decode(self): |
| 275 | """ |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 276 | Examine the field settings and set ACL up for requested fields |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 277 | """ |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 278 | # 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 Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 280 | 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 Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 293 | def remove_all(client, regex_=ACL_NAME_REGEX_ALL): |
Chip Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 294 | """ |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 295 | 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 Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 299 | """ |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 300 | # 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 Boling | 7294b25 | 2017-06-15 16:16:55 -0500 | [diff] [blame] | 309 | |
Chip Boling | ab8863d | 2018-03-22 14:50:31 -0500 | [diff] [blame] | 310 | 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 |