blob: 688124aea9f6b85f4da354d9ba78fc8ac4459f39 [file] [log] [blame]
Chip Bolingf5af85d2019-02-12 15:36:17 -06001# Copyright 2017-present Adtran, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import xmltodict
16import re
17import structlog
18from enum import Enum
19from acl import ACL
20from twisted.internet import defer, reactor
21from twisted.internet.defer import inlineCallbacks, returnValue, succeed
22from ncclient.operations.rpc import RPCError
23
24
25log = structlog.get_logger()
26
27# NOTE: For the EVC Map name, the ingress-port number is the VOLTHA port number (not pon-id since
28# it covers NNI ports as well in order to handle the NNI-NNI case. For flows that
29# cover an entire pon, the name will have the ONU ID and GEM ID appended to it upon
30# installation with a period as a separator.
31
32EVC_MAP_NAME_FORMAT = 'VOLTHA-{}-{}' # format(logical-ingress-port-number, flow-id)
33EVC_MAP_NAME_REGEX_ALL = 'VOLTHA-*'
34
35
36class EVCMap(object):
37 """
38 Class to wrap EVC functionality
39 """
40 class EvcConnection(Enum):
41 NO_EVC_CONNECTION = 0
42 EVC = 1
43 DISCARD = 2
44 DEFAULT = NO_EVC_CONNECTION
45
46 @staticmethod
47 def xml(value):
48 # Note we do not have XML for 'EVC' enumeration.
49 if value is None:
50 value = EVCMap.EvcConnection.DEFAULT
51 if value == EVCMap.EvcConnection.DISCARD:
52 return '<no-evc-connection/>'
53 elif value == EVCMap.EvcConnection.DISCARD:
54 return 'discard/'
55 raise ValueError('Invalid EvcConnection enumeration')
56
57 class PriorityOption(Enum):
58 INHERIT_PRIORITY = 0
59 EXPLICIT_PRIORITY = 1
60 DEFAULT = INHERIT_PRIORITY
61
62 @staticmethod
63 def xml(value):
64 if value is None:
65 value = EVCMap.PriorityOption.DEFAULT
66 if value == EVCMap.PriorityOption.INHERIT_PRIORITY:
67 return '<inherit-pri/>'
68 elif value == EVCMap.PriorityOption.EXPLICIT_PRIORITY:
69 return '<explicit-pri/>'
70 raise ValueError('Invalid PriorityOption enumeration')
71
72 def __init__(self, flow, evc, is_ingress_map):
73 self._handler = flow.handler # Same for all Flows attached to this EVC MAP
74 self._flows = {flow.flow_id: flow}
75 self._evc = None
76 self._new_acls = dict() # ACL Name -> ACL Object (To be installed into h/w)
77 self._existing_acls = dict() # ACL Name -> ACL Object (Already in H/w)
78 self._is_ingress_map = is_ingress_map
79 self._pon_id = None
80 self._onu_id = None # Remains None if associated with a multicast flow
81 self._installed = False
82 self._needs_update = False
83 self._status_message = None
84 self._deferred = None
85 self._name = None
86 self._enabled = True
87 self._uni_port = None
88 self._evc_connection = EVCMap.EvcConnection.DEFAULT
89 self._men_priority = EVCMap.PriorityOption.DEFAULT
90 self._men_pri = 0 # If Explicit Priority
91
92 self._c_tag = None
93 self._men_ctag_priority = EVCMap.PriorityOption.DEFAULT
94 self._men_ctag_pri = 0 # If Explicit Priority
95 self._match_ce_vlan_id = None
96 self._match_untagged = False
97 self._match_destination_mac_address = None
98 self._match_l2cp = False
99 self._match_broadcast = False
100 self._match_multicast = False
101 self._match_unicast = False
102 self._match_igmp = False
103
104 from common.tech_profile.tech_profile import DEFAULT_TECH_PROFILE_TABLE_ID
105 self._tech_profile_id = DEFAULT_TECH_PROFILE_TABLE_ID
106 self._gem_ids_and_vid = None # { key -> onu-id, value -> tuple(sorted GEM Port IDs, onu_vid) }
107 self._upstream_bandwidth = None
108 self._shaper_name = None
109
110 # ACL logic
111 self._eth_type = None
112 self._ip_protocol = None
113 self._ipv4_dst = None
114 self._udp_dst = None
115 self._udp_src = None
116
117 try:
118 self._valid = self._decode(evc)
119
120 except Exception as e:
121 log.exception('decode', e=e)
122 self._valid = False
123
124 def __str__(self):
125 return "EVCMap-{}: UNI: {}, hasACL: {}".format(self._name, self._uni_port,
126 self._needs_acl_support)
127
128 @staticmethod
129 def create_ingress_map(flow, evc, dry_run=False):
130 evc_map = EVCMap(flow, evc, True)
131
132 if evc_map._valid and not dry_run:
133 evc.add_evc_map(evc_map)
134 evc_map._evc = evc
135
136 return evc_map
137
138 @staticmethod
139 def create_egress_map(flow, evc, dry_run=False):
140 evc_map = EVCMap(flow, evc, False)
141
142 if evc_map._valid and not dry_run:
143 evc.add_evc_map(evc_map)
144 evc_map._evc = evc
145
146 return evc_map
147
148 @property
149 def valid(self):
150 return self._valid
151
152 @property
153 def installed(self):
154 return self._installed
155
156 @property
157 def needs_update(self):
158 """ True if an parameter/ACL/... needs update or map needs to be reflowed after a failure"""
159 return self._needs_update
160
161 @needs_update.setter
162 def needs_update(self, value):
163 assert not value, 'needs update can only be reset' # Can only reset
164 self._needs_update = False
165
166 @property
167 def name(self):
168 return self._name
169
170 @property
171 def status(self):
172 return self._status_message
173
174 @status.setter
175 def status(self, value):
176 self._status_message = value
177
178 @property
179 def evc(self):
180 return self._evc
181
182 @property
183 def _needs_acl_support(self):
184 if self._ipv4_dst is not None: # In case MCAST downstream has ACL on it
185 return False
186
187 return self._eth_type is not None or self._ip_protocol is not None or\
188 self._udp_dst is not None or self._udp_src is not None
189
190 @property
191 def pon_id(self):
192 return self._pon_id # May be None
193
194 @property
195 def onu_id(self):
196 return self._onu_id # May be None if associated with a multicast flow
197
198 # @property
199 # def onu_ids(self):
200 # return self._gem_ids_and_vid.keys()
201
202 @property
203 def gem_ids_and_vid(self):
204 return self._gem_ids_and_vid.copy()
205
206 @staticmethod
207 def _xml_header(operation=None):
208 return '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"{}><evc-map>'.\
209 format('' if operation is None else ' xc:operation="{}"'.format(operation))
210
211 @staticmethod
212 def _xml_trailer():
213 return '</evc-map></evc-maps>'
214
215 def get_evcmap_name(self, onu_id, gem_id):
216 return'{}.{}.{}.{}'.format(self.name, self.pon_id, onu_id, gem_id)
217
218 def _common_install_xml(self):
219 xml = '<enabled>{}</enabled>'.format('true' if self._enabled else 'false')
220 xml += '<uni>{}</uni>'.format(self._uni_port)
221
222 evc_name = self._evc.name if self._evc is not None else None
223 if evc_name is not None:
224 xml += '<evc>{}</evc>'.format(evc_name)
225 else:
226 xml += EVCMap.EvcConnection.xml(self._evc_connection)
227
228 xml += '<match-untagged>{}</match-untagged>'.format('true'
229 if self._match_untagged
230 else 'false')
231
232 # TODO: The following is not yet supported (and in some cases, not decoded)
233 # self._men_priority = EVCMap.PriorityOption.INHERIT_PRIORITY
234 # self._men_pri = 0 # If Explicit Priority
235 #
236 # self._men_ctag_priority = EVCMap.PriorityOption.INHERIT_PRIORITY
237 # self._men_ctag_pri = 0 # If Explicit Priority
238 #
239 # self._match_ce_vlan_id = None
240 # self._match_untagged = True
241 # self._match_destination_mac_address = None
242 return xml
243
244 def _ingress_install_xml(self, onu_s_gem_ids_and_vid, acl_list, create):
245 from ..onu import Onu
246
247 if len(acl_list):
248 xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"' +\
249 ' xmlns:adtn-evc-map-acl="http://www.adtran.com/ns/yang/adtran-evc-map-access-control-list">'
250 else:
251 xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps">'
252
253 for onu_or_vlan_id, gem_ids_and_vid in onu_s_gem_ids_and_vid.iteritems():
254 first_gem_id = True
255 gem_ids = gem_ids_and_vid[0]
256 vid = gem_ids_and_vid[1]
257 ident = '{}.{}'.format(self._pon_id, onu_or_vlan_id) if vid is None \
258 else onu_or_vlan_id
259
260 for gem_id in gem_ids:
261 xml += '<evc-map{}>'.format('' if not create else ' xc:operation="create"')
262 xml += '<name>{}.{}.{}</name>'.format(self.name, ident, gem_id)
263 xml += '<ce-vlan-id>{}</ce-vlan-id>'.format(Onu.gem_id_to_gvid(gem_id))
264
265 # GEM-IDs are a sorted list (ascending). First gemport handles downstream traffic
266 if first_gem_id and (self._c_tag is not None or vid is not None):
267 first_gem_id = False
268 vlan = vid or self._c_tag
269 xml += '<network-ingress-filter>'
270 xml += '<men-ctag>{}</men-ctag>'.format(vlan) # Added in August 2017 model
271 xml += '</network-ingress-filter>'
272
273 if len(acl_list):
274 xml += '<adtn-evc-map-acl:access-lists>'
275 for acl in acl_list:
276 xml += ' <adtn-evc-map-acl:ingress-acl>'
277 xml += acl.evc_map_ingress_xml()
278 xml += ' </adtn-evc-map-acl:ingress-acl>'
279 xml += '</adtn-evc-map-acl:access-lists>'
280 xml += self._common_install_xml()
281 xml += '</evc-map>'
282 xml += '</evc-maps>'
283 return xml
284
285 def _egress_install_xml(self):
286 xml = EVCMap._xml_header()
287 xml += '<name>{}</name>'.format(self.name)
288 xml += self._common_install_xml()
289 xml += EVCMap._xml_trailer()
290 return xml
291
292 def _ingress_remove_acl_xml(self, onu_s_gem_ids_and_vid, acl):
293 xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"' +\
294 ' xmlns:adtn-evc-map-acl="http://www.adtran.com/ns/yang/adtran-evc-map-access-control-list">'
295 for onu_or_vlan_id, gem_ids_and_vid in onu_s_gem_ids_and_vid.iteritems():
296 first_gem_id = True
297 vid = gem_ids_and_vid[1]
298 ident = '{}.{}'.format(self._pon_id, onu_or_vlan_id) if vid is None \
299 else onu_or_vlan_id
300
301 for gem_id in gem_ids_and_vid[0]:
302 xml += '<evc-map>'
303 xml += '<name>{}.{}.{}</name>'.format(self.name, ident, gem_id)
304 xml += '<adtn-evc-map-acl:access-lists>'
305 xml += ' <adtn-evc-map-acl:ingress-acl xc:operation="delete">'
306 xml += acl.evc_map_ingress_xml()
307 xml += ' </adtn-evc-map-acl:ingress-acl>'
308 xml += '</adtn-evc-map-acl:access-lists>'
309 xml += '</evc-map>'
310 xml += '</evc-maps>'
311 return xml
312
313 @inlineCallbacks
314 def install(self):
315 def gem_ports():
316 ports = []
317 for gems_and_vids in self._gem_ids_and_vid.itervalues():
318 ports.extend(gems_and_vids[0])
319 return ports
320
321 log.debug('install-evc-map', valid=self._valid, gem_ports=gem_ports())
322
323 if self._valid and len(gem_ports()) > 0:
324 # Install ACLs first (if not yet installed)
325 work_acls = self._new_acls.copy()
326 self._new_acls = dict()
327
328 log.debug('install-evc-map-acls', install_acls=len(work_acls))
329 for acl in work_acls.itervalues():
330 try:
331 yield acl.install()
332
333 except Exception as e:
334 log.exception('acl-install-failed', name=self.name, e=e)
335 self._new_acls.update(work_acls)
336 raise
337
338 # Any user-data flows attached to this map ?
339 c_tag = None
340 for flow_id, flow in self._flows.items():
341 c_tag = flow.inner_vid or flow.vlan_id or c_tag
342
343 self._c_tag = c_tag
344
345 # Now EVC-MAP
346 if not self._installed or self._needs_update:
347 log.debug('needs-install-or-update', installed=self._installed, update=self._needs_update)
348 is_installed = self._installed
349 self._installed = True
350 try:
351 self._cancel_deferred()
352
353 log.info('upstream-bandwidth')
354 try:
355 yield self.update_upstream_flow_bandwidth()
356
357 except Exception as e:
358 log.exception('upstream-bandwidth-failed', name=self.name, e=e)
359 raise
360
361 map_xml = self._ingress_install_xml(self._gem_ids_and_vid, work_acls.values(),
362 not is_installed) \
363 if self._is_ingress_map else self._egress_install_xml()
364
365 log.debug('install', xml=map_xml, name=self.name)
366 results = yield self._handler.netconf_client.edit_config(map_xml)
367 self._installed = results.ok
368 self._needs_update = results.ok
369 self._status_message = '' if results.ok else results.error
370
371 if results.ok:
372 self._existing_acls.update(work_acls)
373 else:
374 self._new_acls.update(work_acls)
375
376 except RPCError as rpc_err:
377 if rpc_err.tag == 'data-exists': # Known race due to bulk-flow operation
378 pass
379
380 except Exception as e:
381 log.exception('evc-map-install-failed', name=self.name, e=e)
382 self._installed = is_installed
383 self._new_acls.update(work_acls)
384 raise
385
386 # Install any needed shapers
387 if self._installed:
388 try:
389 yield self.update_downstream_flow_bandwidth()
390
391 except Exception as e:
392 log.exception('shaper-install-failed', name=self.name, e=e)
393 raise
394
395 returnValue(self._installed and self._valid)
396
397 def _ingress_remove_xml(self, onus_gem_ids_and_vid):
398 xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"' + \
399 ' xc:operation="delete">'
400
401 for onu_id, gem_ids_and_vid in onus_gem_ids_and_vid.iteritems():
402 for gem_id in gem_ids_and_vid[0]:
403 xml += '<evc-map>'
404 xml += '<name>{}.{}.{}</name>'.format(self.name, onu_id, gem_id)
405 xml += '</evc-map>'
406 xml += '</evc-maps>'
407 return xml
408
409 def _egress_remove_xml(self):
410 return EVCMap._xml_header('delete') + \
411 '<name>{}</name>'.format(self.name) + EVCMap._xml_trailer()
412
413 def _remove(self):
414 if not self.installed:
415 returnValue('Not installed')
416
417 log.info('removing', evc_map=self)
418
419 def _success(rpc_reply):
420 log.debug('remove-success', rpc_reply=rpc_reply)
421 self._installed = False
422
423 def _failure(failure):
424 log.error('remove-failed', failure=failure)
425 self._installed = False
426
427 def _remove_acls(_):
428 acls, self._new_acls = self._new_acls, dict()
429 existing, self._existing_acls = self._existing_acls, dict()
430 acls.update(existing)
431
432 dl = []
433 for acl in acls.itervalues():
434 dl.append(acl.remove())
435
436 if len(dl) > 0:
437 defer.gatherResults(dl, consumeErrors=True)
438
439 def _remove_shaper(_):
440 if self._shaper_name is not None:
441 self.update_downstream_flow_bandwidth(remove=True)
442
443 map_xml = self._ingress_remove_xml(self._gem_ids_and_vid) if self._is_ingress_map \
444 else self._egress_remove_xml()
445
446 d = self._handler.netconf_client.edit_config(map_xml)
447 d.addCallbacks(_success, _failure)
448 d.addBoth(_remove_acls)
449 d.addBoth(_remove_shaper)
450 return d
451
452 @inlineCallbacks
453 def delete(self, flow):
454 """
455 Remove from hardware and delete/clean-up EVC-MAP Object
456
457 :param flow: (FlowEntry) Specific flow to remove from the MAP or None if all
458 flows should be removed
459 :return:
460 """
461 flows = [flow] if flow is not None else list(self._flows.values())
462 removing_all = len(flows) == len(self._flows)
463
464 log.debug('delete', removing_all=removing_all)
465 if not removing_all:
466 for f in flows:
467 self._remove_flow(f)
468
469 else:
470 if self._evc is not None:
471 self._evc.remove_evc_map(self)
472 self._evc = None
473
474 self._valid = False
475 self._cancel_deferred()
476 try:
477 yield self._remove()
478
479 except Exception as e:
480 log.exception('removal', e=e)
481
482 returnValue('Done')
483
484 def reflow_needed(self):
485 log.debug('reflow-needed', installed=self.installed, needs_update=self.needs_update)
486 reflow = not self.installed or self.needs_update
487
488 if not reflow:
489 pass # TODO: implement retrieve & compare of EVC Map parameters
490
491 return reflow
492
493 @staticmethod
494 def find_matching_ingress_flow(flow, upstream_flow_table):
495 """
496 Look for an existing EVC-MAP that may match this flow. Called when upstream signature
497 for a flow does not make match. This can happen if an ACL flow is added and only an User
498 Data flow exists, or if only an ACL flow exists.
499
500 :param flow: (FlowEntry) flow to add
501 :param upstream_flow_table: (dict of FlowEntry) Existing upstream flows for this device,
502 including the flow we are looking to add
503 :return: (EVCMap) if appropriate one is found, else None
504 """
505 # A User Data flow will have:
506 # signature: <dev>.1.5.2.242
507 # down-sig: <dev>.1.*.2.*
508 # logical-port: 66
509 # is-acl-flow: False
510 #
511 # An ACL flow will have:
512 # signature: <dev>.1.5.[4092 or 4094].None (untagged VLAN == utility VLAN case)
513 # down-sig: <dev>.1.*.[4092 or 4094].*
514 # logical-port: 66
515 # is-acl-flow: True
516 #
517 # Reduce the upstream flow table to only those that match the ingress,
518 # and logical-ports match (and is not this flow) and have a map
519
520 log.debug('find-matching-ingress-flow', logical_port=flow.logical_port, flow=flow.output)
521 candidate_flows = [f for f in upstream_flow_table.itervalues() if
522 f.in_port == flow.in_port and
523 f.logical_port == flow.logical_port and
524 f.output == flow.output and
525 f.evc_map is not None] # This weeds out this flow
526
527 log.debug('find-matching-ingress-flow', candidate_flows=candidate_flows)
528 return candidate_flows[0].evc_map if len(candidate_flows) > 0 else None
529
530 def add_flow(self, flow, evc):
531 """
532 Add a new flow to an existing EVC-MAP. This can be called to add:
533 o an ACL flow to an existing utility EVC, or
534 o an ACL flow to an existing User Data Flow, or
535 o a User Data Flow to an existing ACL flow (and this needs the EVC updated
536 as well.
537
538 Note that the Downstream EVC provided is the one that matches this flow. If
539 this is adding an ACL to and existing User data flow, we DO NOT want to
540 change the EVC Map's EVC
541
542 :param flow: (FlowEntry) New flow
543 :param evc: (EVC) Matching EVC for downstream flow
544 """
545 from flow_entry import FlowEntry
546 # Create temporary EVC-MAP
547 assert flow.flow_direction in FlowEntry.upstream_flow_types, \
548 'Only Upstream flows additions are supported at this time'
549
550 log.debug('add-flow-to-evc', flow=flow, evc=evc)
551
552 tmp_map = EVCMap.create_ingress_map(flow, evc, dry_run=True) \
553 if flow.flow_direction in FlowEntry.upstream_flow_types \
554 else EVCMap.create_egress_map(flow, evc, dry_run=True)
555
556 if tmp_map is None or not tmp_map.valid:
557 return None
558
559 self._flows[flow.flow_id] = flow
560 self._needs_update = True
561
562 # Are there ACLs to add to any existing (or empty) ACLs
563 if len(tmp_map._new_acls) > 0:
564 self._new_acls.update(tmp_map._new_acls) # New ACL flow
565 log.debug('add-acl-flows', map=str(self), new=tmp_map._new_acls)
566
567 # Look up existing EVC for this flow. If it is a service EVC for
568 # Packet In/Out, and this is a regular flow, migrate to the newer EVC
569 if self._evc.service_evc and not evc.service_evc:
570 log.info('new-evc-for-map', old=self._evc.name, new=evc.name)
571 self._evc.remove_evc_map(self)
572 evc.add_evc_map(self)
573 self._evc = evc
574
575 return self
576
577 @inlineCallbacks
578 def _remove_flow(self, flow):
579 """
580 Remove a specific flow from an EVC_MAP. This includes removing any
581 ACL entries associated with the flow and could result in moving the
582 EVC-MAP over to another EVC.
583
584 :param flow: (FlowEntry) Flow to remove
585 """
586 try:
587 del self._flows[flow.flow_id]
588
589 log('remove-flow-to-evc', flow=flow)
590 # Remove any ACLs
591 acl_name = ACL.flow_to_name(flow)
592 acl = None
593
594 # if not yet installed just remove it from list
595 if acl_name in self._new_acls:
596 del self._new_acls[acl_name]
597 else:
598 acl = self._existing_acls[acl_name]
599 if acl is not None:
600 # Remove ACL from EVC-MAP entry
601
602 try:
603 map_xml = self._ingress_remove_acl_xml(self._gem_ids_and_vid, acl)
604 log.debug('remove', xml=map_xml, name=acl.name)
605 results = yield self._handler.netconf_client.edit_config(map_xml)
606 if results.ok:
607 del self._existing_acls[acl.name]
608
609 # Scan EVC to see if it needs to move back to the Utility
610 # or Untagged EVC from a user data EVC
611 if self._evc and not self._evc.service_evc and\
612 len(self._flows) > 0 and\
613 all(f.is_acl_flow for f in self._flows.itervalues()):
614
615 self._evc.remove_evc_map(self)
616 first_flow = self._flows.itervalues().next()
617 self._evc = first_flow.get_utility_evc(True)
618 self._evc.add_evc_map(self)
619 log.debug('moved-acl-flows-to-utility-evc', newevcname=self._evc.name)
620
621 self._needs_update = True
622 self._evc.schedule_install()
623
624 except Exception as e:
625 log.exception('acl-remove-from-evc', e=e)
626
627 # Remove ACL itself
628 try:
629 yield acl.remove()
630
631 except Exception as e:
632 log.exception('acl-remove', e=e)
633
634 except Exception as e:
635 log.exception('remove-failed', e=e)
636
637 @staticmethod
638 def create_evc_map_name(flow):
639 # Note: When actually installed into the OLT, the .onu_id.gem_port is
640 # appended to the name
641 return EVC_MAP_NAME_FORMAT.format(flow.logical_port, flow.flow_id)
642
643 @staticmethod
644 def decode_evc_map_name(name):
645 """
646 Reverse engineer EVC-MAP name parameters. Helpful in quick packet-in
647 processing
648
649 :param name: (str) EVC Map Name
650 :return: (dict) Logical Ingress Port, OpenFlow Flow-ID
651 """
652 items = name.split('-') if name is not None else dict()
653
654 # Note: When actually installed into the OLT, the .onu_id.gem_port is
655 # appended to the name
656 return {'ingress-port': items[1],
657 'flow-id': items[2].split('.')[0]} if len(items) > 2 else dict()
658
659 @inlineCallbacks
660 def update_upstream_flow_bandwidth(self):
661 """
662 Upstream flow bandwidth comes from the flow_entry related to this EVC-MAP
663 and if no bandwidth property is found, allow full bandwidth
664 """
665 # all flows should should be on the same PON
666 flow = self._flows.itervalues().next()
667 is_pon = flow.handler.is_pon_port(flow.in_port)
668
669 if self._is_ingress_map and is_pon:
670 pon_port = flow.handler.get_southbound_port(flow.in_port)
671 if pon_port is None:
672 returnValue('no PON')
673
674 session = self._handler.rest_client
675 # TODO: Refactor with tech profiles
676 tconts = None # pon_port.tconts
677 traffic_descriptors = None # pon_port.traffic_descriptors
678
679 if traffic_descriptors is None or tconts is None:
680 returnValue('no TDs on PON')
681
682 bandwidth = self._upstream_bandwidth or 10000000000
683
684 if self.pon_id is not None and self.onu_id is not None:
685 name = 'tcont-{}-{}-data'.format(self.pon_id, self.onu_id)
686 td = traffic_descriptors.get(name)
687 tcont = tconts.get(name)
688
689 if td is not None and tcont is not None:
690 alloc_id = tcont.alloc_id
691 td.maximum_bandwidth = bandwidth
692 try:
693 results = yield td.add_to_hardware(session)
694 log.debug('td-modify-results', results=results)
695
696 except Exception as _e:
697 pass
698
699 @inlineCallbacks
700 def update_downstream_flow_bandwidth(self, remove=False):
701 """
702 Downstream flow bandwidth is extracted from the related EVC flow_entry
703 bandwidth property. It is written to this EVC-MAP only if it is found
704 """
705 xml = None
706 results = None
707
708 if remove:
709 name, self._shaper_name = self._shaper_name, None
710 if name is not None:
711 xml = self._shaper_remove_xml(name)
712 else:
713 if self._evc is not None and self._evc.flow_entry is not None \
714 and self._evc.flow_entry.bandwidth is not None:
715 self._shaper_name = self._name
716 xml = self._shaper_install_xml(self._shaper_name,
717 self._evc.flow_entry.bandwidth * 1000) # kbps
718 if xml is not None:
719 try:
720 log.info('downstream-bandwidth', xml=xml, name=self.name, remove=remove)
721 results = yield self._handler.netconf_client.edit_config(xml)
722
723 except RPCError as rpc_err:
724 if rpc_err.tag == 'data-exists':
725 pass
726
727 except Exception as e:
728 log.exception('downstream-bandwidth', name=self.name, remove=remove, e=e)
729 raise
730
731 returnValue(results)
732
733 def _shaper_install_xml(self, name, bandwidth):
734 xml = '<adtn-shaper:shapers xmlns:adtn-shaper="http://www.adtran.com/ns/yang/adtran-traffic-shapers" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" nc:operation="merge">'
735 for onu_id, gem_ids_and_vid in self._gem_ids_and_vid.iteritems():
736 for gem_id in gem_ids_and_vid[0]:
737 xml += ' <adtn-shaper:shaper>'
738 xml += ' <adtn-shaper:name>{}.{}.{}</adtn-shaper:name>'.format(name, onu_id, gem_id)
739 xml += ' <adtn-shaper:enabled>true</adtn-shaper:enabled>'
740 xml += ' <adtn-shaper:rate>{}</adtn-shaper:rate>'.format(bandwidth)
741 xml += ' <adtn-shaper-evc-map:evc-map xmlns:adtn-shaper-evc-map="http://www.adtran.com/ns/yang/adtran-traffic-shaper-evc-maps">{}.{}.{}</adtn-shaper-evc-map:evc-map>'.format(self.name, onu_id, gem_id)
742 xml += ' </adtn-shaper:shaper>'
743 xml += '</adtn-shaper:shapers>'
744 return xml
745
746 def _shaper_remove_xml(self, name):
747 xml = '<adtn-shaper:shapers xmlns:adtn-shaper="http://www.adtran.com/ns/yang/adtran-traffic-shapers" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" nc:operation="delete">'
748 for onu_id, gem_ids_and_vid in self._gem_ids_and_vid.iteritems():
749 for gem_id in gem_ids_and_vid[0]:
750 xml += ' <adtn-shaper:shaper >'
751 xml += ' <adtn-shaper:name>{}.{}.{}</adtn-shaper:name>'.format(name, onu_id, gem_id)
752 xml += ' </adtn-shaper:shaper>'
753 xml += '</adtn-shaper:shapers>'
754 return xml
755
756 def _setup_tech_profiles(self):
757 # Set up the TCONT / GEM Ports for this connection (Downstream only of course)
758 # all flows should have same GEM port setup
759 flow = self._flows.itervalues().next()
760 is_pon = flow.handler.is_pon_port(flow.in_port)
761
762 if self._is_ingress_map and is_pon:
763 pon_port = flow.handler.get_southbound_port(flow.in_port)
764
765 if pon_port is None:
766 return
767
768 onu = next((onu for onu in pon_port.onus if onu.logical_port == flow.logical_port), None)
769
770 if onu is None: # TODO: Add multicast support later (self.onu_id == None)
771 return
772
773 self._pon_id = pon_port.pon_id
774 self._onu_id = onu.onu_id
775
776 # Identify or allocate TCONT and GEM Ports. If the ONU has been informed of the
777 # GEM PORTs that belong to it, the tech profiles were already set up by a previous
778 # flows
779 onu_gems = onu.gem_ids(self._tech_profile_id)
780
781 if len(onu_gems) > 0:
782 self._gem_ids_and_vid[onu.onu_id] = (onu_gems, flow.vlan_id)
783 return
784
785 uni_id = self._handler.platform.uni_id_from_uni_port(flow.logical_port)
786 pon_profile = self._handler.tech_profiles[self.pon_id]
787 alloc_id = None
788
789 try:
790 (ofp_port_name, ofp_port_no) = self._handler.get_ofp_port_name(self.pon_id,
791 self.onu_id,
792 flow.logical_port)
793 if ofp_port_name is None:
794 log.error("port-name-not-found")
795 return
796
797 # Check tech profile instance already exists for derived port name
798 tech_profile = pon_profile.get_tech_profile_instance(self._tech_profile_id,
799 ofp_port_name)
800 log.debug('Get-tech-profile-instance-status',
801 tech_profile_instance=tech_profile)
802
803 if tech_profile is None:
804 # create tech profile instance
805 tech_profile = pon_profile.create_tech_profile_instance(self._tech_profile_id,
806 ofp_port_name,
807 self.pon_id)
808 if tech_profile is None:
809 raise Exception('Tech-profile-instance-creation-failed')
810 else:
811 log.debug('Tech-profile-instance-already-exist-for-given port-name',
812 ofp_port_name=ofp_port_name)
813
814 # upstream scheduler
815 us_scheduler = pon_profile.get_us_scheduler(tech_profile)
816
817 # downstream scheduler
818 ds_scheduler = pon_profile.get_ds_scheduler(tech_profile)
819
820 # create Tcont protobuf
821 pb_tconts = pon_profile.get_tconts(tech_profile, us_scheduler, ds_scheduler)
822
823 # create TCONTs & GEM Ports locally
824 for pb_tcont in pb_tconts:
825 from ..xpon.olt_tcont import OltTCont
826 tcont = OltTCont.create(pb_tcont,
827 self.pon_id,
828 self.onu_id,
829 self._tech_profile_id,
830 uni_id,
831 ofp_port_no)
832 if tcont is not None:
833 onu.add_tcont(tcont)
834
835 # Fetch alloc id and gemports from tech profile instance
836 alloc_id = tech_profile.us_scheduler.alloc_id
837
838 onu_gems = [gem.gemport_id for gem in tech_profile.upstream_gem_port_attribute_list]
839
840 for gem in tech_profile.upstream_gem_port_attribute_list:
841 from ..xpon.olt_gem_port import OltGemPort
842 gem_port = OltGemPort.create(self._handler,
843 gem,
844 tech_profile.us_scheduler.alloc_id,
845 self._tech_profile_id,
846 self.pon_id,
847 self.onu_id,
848 uni_id,
849 ofp_port_no)
850 if gem_port is not None:
851 onu.add_gem_port(gem_port)
852
853 self._gem_ids_and_vid = {onu.onu_id: (onu_gems, flow.vlan_id)}
854
855 # Send technology profile information to ONU
856 reactor.callLater(0, self._handler.setup_onu_tech_profile, self._pon_id,
857 self.onu_id, flow.logical_port)
858
859 except BaseException as e:
860 log.exception(exception=e)
861
862 # Update the allocated alloc_id and gem_port_id for the ONU/UNI to KV store
863 pon_intf_onu_id = (self.pon_id, self.onu_id, uni_id)
864 resource_manager = self._handler.resource_mgr.resource_managers[self.pon_id]
865
866 resource_manager.update_alloc_ids_for_onu(pon_intf_onu_id, list([alloc_id]))
867 resource_manager.update_gemport_ids_for_onu(pon_intf_onu_id, onu_gems)
868
869 self._handler.resource_mgr.update_gemports_ponport_to_onu_map_on_kv_store(onu_gems,
870 self.pon_id,
871 self.onu_id,
872 uni_id)
873
874 def _decode(self, evc):
875 from evc import EVC
876 from flow_entry import FlowEntry
877
878 # Only called from initializer, so first flow is only flow
879 flow = self._flows.itervalues().next()
880
881 self._name = EVCMap.create_evc_map_name(flow)
882
883 if evc:
884 self._evc_connection = EVCMap.EvcConnection.EVC
885 else:
886 self._status_message = 'Can only create EVC-MAP if EVC supplied'
887 return False
888
889 is_pon = flow.handler.is_pon_port(flow.in_port)
890 is_uni = flow.handler.is_uni_port(flow.in_port)
891
892 if flow.bandwidth is not None:
893 self._upstream_bandwidth = flow.bandwidth * 1000000
894
895 if is_pon or is_uni:
896 # Preserve CE VLAN tag only if utility VLAN/EVC
897 self._uni_port = flow.handler.get_port_name(flow.in_port)
898 evc.ce_vlan_preservation = evc.ce_vlan_preservation or False
899 else:
900 self._status_message = 'EVC-MAPS without UNI or PON ports are not supported'
901 return False # UNI Ports handled in the EVC Maps
902
903 # ACL logic
904 self._eth_type = flow.eth_type
905
906 if self._eth_type == FlowEntry.EtherType.IPv4:
907 self._ip_protocol = flow.ip_protocol
908 self._ipv4_dst = flow.ipv4_dst
909
910 if self._ip_protocol == FlowEntry.IpProtocol.UDP:
911 self._udp_dst = flow.udp_dst
912 self._udp_src = flow.udp_src
913
914 # If no match of VLAN this may be for untagged traffic or upstream and needs to
915 # match the gem-port vid
916
917 self._setup_tech_profiles()
918
919 # self._match_untagged = flow.vlan_id is None and flow.inner_vid is None
920 self._c_tag = flow.inner_vid or flow.vlan_id
921
922 # If a push of a single VLAN is present with a POP of the VLAN in the EVC's
923 # flow, then this is a traditional EVC flow
924
925 evc.men_to_uni_tag_manipulation = EVC.Men2UniManipulation.POP_OUT_TAG_ONLY
926 evc.switching_method = EVC.SwitchingMethod.DOUBLE_TAGGED \
927 if self._c_tag is not None else EVC.SwitchingMethod.SINGLE_TAGGED
928
929 try:
930 acl = ACL.create(flow)
931 if acl.name not in self._new_acls:
932 self._new_acls[acl.name] = acl
933
934 except Exception as e:
935 log.exception('ACL-decoding', e=e)
936 return False
937
938 return True
939
940 # Bulk operations
941
942 @staticmethod
943 def remove_all(client, regex_=EVC_MAP_NAME_REGEX_ALL):
944 """
945 Remove all matching EVC Maps from hardware
946
947 :param client: (ncclient) NETCONF Client to use
948 :param regex_: (String) Regular expression for name matching
949 :return: (deferred)
950 """
951 # Do a 'get' on the evc-map config an you should get the names
952 get_xml = """
953 <filter>
954 <evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps">
955 <evc-map>
956 <name/>
957 </evc-map>
958 </evc-maps>
959 </filter>
960 """
961 log.debug('query', xml=get_xml, regex=regex_)
962
963 def request_failed(results, operation):
964 log.error('{}-failed'.format(operation), results=results)
965 # No further actions. Periodic poll later on will scrub any old EVC-Maps if needed
966
967 def delete_complete(results):
968 log.debug('delete-complete', results=results)
969
970 def do_delete(rpc_reply, regexpr):
971 log.debug('query-complete', rpc_reply=rpc_reply)
972
973 if rpc_reply.ok:
974 result_dict = xmltodict.parse(rpc_reply.data_xml)
975 entries = result_dict['data']['evc-maps'] if 'evc-maps' in result_dict['data'] else {}
976
977 if 'evc-map' in entries:
978 p = re.compile(regexpr)
979
980 if isinstance(entries['evc-map'], list):
981 names = {entry['name'] for entry in entries['evc-map']
982 if 'name' in entry and p.match(entry['name'])}
983 else:
984 names = set()
985 for item in entries['evc-map'].items():
986 if isinstance(item, tuple) and item[0] == 'name':
987 names.add(item[1])
988 break
989
990 if len(names) > 0:
991 del_xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"' + \
992 ' xc:operation = "delete">'
993 for name in names:
994 del_xml += '<evc-map>'
995 del_xml += '<name>{}</name>'.format(name)
996 del_xml += '</evc-map>'
997 del_xml += '</evc-maps>'
998 log.debug('removing', xml=del_xml)
999
1000 return client.edit_config(del_xml)
1001
1002 return succeed('no entries')
1003
1004 d = client.get(get_xml)
1005 d.addCallbacks(do_delete, request_failed, callbackArgs=[regex_], errbackArgs=['get'])
1006 d.addCallbacks(delete_complete, request_failed, errbackArgs=['edit-config'])
1007 return d
1008
1009 def _cancel_deferred(self):
1010 d, self._deferred = self._deferred, None
1011 try:
1012 if d is not None and not d.called:
1013 d.cancel()
1014 except:
1015 pass