Chip Boling | 8e042f6 | 2019-02-12 16:14:34 -0600 | [diff] [blame] | 1 | # Copyright 2018-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 | import structlog |
| 15 | from twisted.internet.defer import inlineCallbacks, returnValue, TimeoutError |
| 16 | from twisted.internet import reactor |
| 17 | |
| 18 | from voltha.protos.device_pb2 import Image |
| 19 | |
| 20 | from voltha.protos.common_pb2 import OperStatus, ConnectStatus |
| 21 | from voltha.extensions.omci.onu_configuration import OMCCVersion |
| 22 | |
| 23 | from omci_entities import onu_custom_me_entities |
| 24 | from voltha.extensions.omci.omci_me import * |
| 25 | |
| 26 | _STARTUP_RETRY_WAIT = 5 |
| 27 | # abbreviations |
| 28 | OP = EntityOperations |
| 29 | |
| 30 | |
| 31 | class OMCI(object): |
| 32 | """ |
| 33 | OpenOMCI Support |
| 34 | """ |
| 35 | DEFAULT_UNTAGGED_VLAN = 4091 # To be equivalent to BroadCom Defaults |
| 36 | |
| 37 | def __init__(self, handler, omci_agent): |
| 38 | self.log = structlog.get_logger(device_id=handler.device_id) |
| 39 | self._handler = handler |
| 40 | self._openomci_agent = omci_agent |
| 41 | self._enabled = False |
| 42 | self._connected = False |
| 43 | self._deferred = None |
| 44 | self._bridge_initialized = False |
| 45 | self._in_sync_reached = False |
| 46 | self._omcc_version = OMCCVersion.Unknown |
| 47 | self._total_tcont_count = 0 # From ANI-G ME |
| 48 | self._qos_flexibility = 0 # From ONT2_G ME |
| 49 | |
| 50 | self._in_sync_subscription = None |
| 51 | self._connectivity_subscription = None |
| 52 | self._capabilities_subscription = None |
| 53 | |
| 54 | # self._service_downloaded = False |
| 55 | self._mib_downloaded = False |
| 56 | self._mib_download_task = None |
| 57 | self._mib_download_deferred = None |
| 58 | |
| 59 | self._onu_omci_device = omci_agent.add_device(handler.device_id, |
| 60 | handler.adapter_agent, |
| 61 | custom_me_map=onu_custom_me_entities(), |
| 62 | support_classes=handler.adapter.adtran_omci) |
| 63 | |
| 64 | def __str__(self): |
| 65 | return "OMCI" |
| 66 | |
| 67 | @property |
| 68 | def omci_agent(self): |
| 69 | return self._openomci_agent |
| 70 | |
| 71 | @property |
| 72 | def omci_cc(self): |
| 73 | # TODO: Decrement access to Communications channel at this point? What about current PM stuff? |
| 74 | return self.onu_omci_device.omci_cc if self._onu_omci_device is not None else None |
| 75 | |
| 76 | def receive_message(self, msg): |
| 77 | if self.enabled: |
| 78 | # TODO: Have OpenOMCI actually receive the messages |
| 79 | self.omci_cc.receive_message(msg) |
| 80 | |
| 81 | def _start(self): |
| 82 | self._cancel_deferred() |
| 83 | |
| 84 | # Subscriber to events of interest in OpenOMCI |
| 85 | self._subscribe_to_events() |
| 86 | self._onu_omci_device.start() |
| 87 | |
| 88 | device = self._handler.adapter_agent.get_device(self._handler.device_id) |
| 89 | device.reason = 'Performing MIB Upload' |
| 90 | self._handler.adapter_agent.update_device(device) |
| 91 | |
| 92 | if self._onu_omci_device.mib_db_in_sync: |
| 93 | self._deferred = reactor.callLater(0, self._mib_in_sync) |
| 94 | |
| 95 | def _stop(self): |
| 96 | self._cancel_deferred() |
| 97 | |
| 98 | # Unsubscribe to OpenOMCI Events |
| 99 | self._unsubscribe_to_events() |
| 100 | self._onu_omci_device.stop() # Will also cancel any running tasks/state-machines |
| 101 | |
| 102 | self._mib_downloaded = False |
| 103 | self._mib_download_task = None |
| 104 | self._bridge_initialized = False |
| 105 | self._in_sync_reached = False |
| 106 | |
| 107 | def _cancel_deferred(self): |
| 108 | d1, self._deferred = self._deferred, None |
| 109 | d2, self._mib_download_deferred = self._mib_download_deferred, None |
| 110 | |
| 111 | for d in [d1, d2]: |
| 112 | try: |
| 113 | if d is not None and not d.called: |
| 114 | d.cancel() |
| 115 | except: |
| 116 | pass |
| 117 | |
| 118 | def delete(self): |
| 119 | self.enabled = False |
| 120 | |
| 121 | agent, self._openomci_agent = self._openomci_agent, None |
| 122 | device_id = self._handler.device_id |
| 123 | self._onu_omci_device = None |
| 124 | self._handler = None |
| 125 | |
| 126 | if agent is not None: |
| 127 | agent.remove_device(device_id, cleanup=True) |
| 128 | |
| 129 | @property |
| 130 | def enabled(self): |
| 131 | return self._enabled |
| 132 | |
| 133 | @enabled.setter |
| 134 | def enabled(self, value): |
| 135 | if self._enabled != value: |
| 136 | self._enabled = value |
| 137 | |
| 138 | if value: |
| 139 | self._start() |
| 140 | else: |
| 141 | self._stop() |
| 142 | |
| 143 | @property |
| 144 | def connected(self): |
| 145 | return self._connected |
| 146 | |
| 147 | @property |
| 148 | def onu_omci_device(self): |
| 149 | return self._onu_omci_device |
| 150 | |
| 151 | def set_pm_config(self, pm_config): |
| 152 | """ |
| 153 | Set PM interval configuration |
| 154 | |
| 155 | :param pm_config: (OnuPmIntervalMetrics) PM Interval configuration |
| 156 | :return: |
| 157 | """ |
| 158 | self.onu_omci_device.set_pm_config(pm_config) |
| 159 | |
| 160 | def _mib_in_sync(self): |
| 161 | """ |
| 162 | This method is ran whenever the ONU MIB database is in-sync. This is often after |
| 163 | the initial MIB Upload during ONU startup, or after it has gone out-of-sync and |
| 164 | then back in. This second case could be due a reboot of the ONU and a new version |
| 165 | of firmware is running on the ONU hardware. |
| 166 | """ |
| 167 | self.log.info('mib-in-sync') |
| 168 | |
| 169 | device = self._handler.adapter_agent.get_device(self._handler.device_id) |
| 170 | device.oper_status = OperStatus.ACTIVE |
| 171 | device.connect_status = ConnectStatus.REACHABLE |
| 172 | device.reason = '' |
| 173 | self._handler.adapter_agent.update_device(device) |
| 174 | |
| 175 | omci_dev = self._onu_omci_device |
| 176 | config = omci_dev.configuration |
| 177 | |
| 178 | # In Sync, we can register logical ports now. Ideally this could occur on |
| 179 | # the first time we received a successful (no timeout) OMCI Rx response. |
| 180 | try: |
| 181 | device = self._handler.adapter_agent.get_device(self._handler.device_id) |
| 182 | |
| 183 | ani_g = config.ani_g_entities |
| 184 | uni_g = config.uni_g_entities |
| 185 | pon_ports = len(ani_g) if ani_g is not None else 0 |
| 186 | uni_ports = len(uni_g) if uni_g is not None else 0 |
| 187 | |
| 188 | # For the UNI ports below, they are created after the MIB Sync event occurs |
| 189 | # and the onu handler adds the ONU |
| 190 | assert pon_ports == 1, 'Expected one PON/ANI port, got {}'.format(pon_ports) |
| 191 | assert uni_ports == len(self._handler.uni_ports), \ |
| 192 | 'Expected {} UNI port(s), got {}'.format(len(self._handler.uni_ports), uni_ports) |
| 193 | |
| 194 | # serial_number = omci_dev.configuration.serial_number |
| 195 | # self.log.info('serial-number', serial_number=serial_number) |
| 196 | |
| 197 | # Save entity_id of PON ports |
| 198 | self._handler.pon_ports[0].entity_id = ani_g.keys()[0] |
| 199 | |
| 200 | self._total_tcont_count = ani_g.get('total-tcont-count') |
| 201 | self._qos_flexibility = config.qos_configuration_flexibility or 0 |
| 202 | self._omcc_version = config.omcc_version or OMCCVersion.Unknown |
| 203 | |
| 204 | # vendorProductCode = str(config.vendor_product_code or 'unknown').rstrip('\0') |
| 205 | |
| 206 | host_info = omci_dev.query_mib(IpHostConfigData.class_id) |
| 207 | mgmt_mac_address = next((host_info[inst].get('attributes').get('mac_address') |
| 208 | for inst in host_info |
| 209 | if isinstance(inst, int)), 'unknown') |
| 210 | device.mac_address = str(mgmt_mac_address) |
| 211 | device.model = str(config.version or 'unknown').rstrip('\0') |
| 212 | |
| 213 | equipment_id = config.equipment_id or " unknown unknown " |
| 214 | eqpt_boot_version = str(equipment_id).rstrip('\0') |
| 215 | # eqptId = eqpt_boot_version[:10] # ie) BVMDZ10DRA |
| 216 | boot_version = eqpt_boot_version[12:] # ie) CML.D55~ |
| 217 | |
| 218 | images = [Image(name='boot-code', |
| 219 | version=boot_version.rstrip('\0'), |
| 220 | is_active=False, |
| 221 | is_committed=True, |
| 222 | is_valid=True, |
| 223 | install_datetime='Not Available', |
| 224 | hash='Not Available')] + \ |
| 225 | config.software_images |
| 226 | |
| 227 | del (device.images.image[:]) # Clear previous entries |
| 228 | device.images.image.extend(images) |
| 229 | |
| 230 | # Save our device information |
| 231 | self._handler.adapter_agent.update_device(device) |
| 232 | |
| 233 | # Start MIB download TODO: This will be replaced with a MIB Download task soon |
| 234 | self._in_sync_reached = True |
| 235 | |
| 236 | except Exception as e: |
| 237 | self.log.exception('device-info-load', e=e) |
| 238 | self._deferred = reactor.callLater(_STARTUP_RETRY_WAIT, self._mib_in_sync) |
| 239 | |
| 240 | def _subscribe_to_events(self): |
| 241 | from voltha.extensions.omci.onu_device_entry import OnuDeviceEvents, \ |
| 242 | OnuDeviceEntry |
| 243 | from voltha.extensions.omci.omci_cc import OMCI_CC, OmciCCRxEvents |
| 244 | |
| 245 | # OMCI MIB Database sync status |
| 246 | bus = self._onu_omci_device.event_bus |
| 247 | topic = OnuDeviceEntry.event_bus_topic(self._handler.device_id, |
| 248 | OnuDeviceEvents.MibDatabaseSyncEvent) |
| 249 | self._in_sync_subscription = bus.subscribe(topic, self.in_sync_handler) |
| 250 | |
| 251 | # OMCI Capabilities (MEs and Message Types |
| 252 | bus = self._onu_omci_device.event_bus |
| 253 | topic = OnuDeviceEntry.event_bus_topic(self._handler.device_id, |
| 254 | OnuDeviceEvents.OmciCapabilitiesEvent) |
| 255 | self._capabilities_subscription = bus.subscribe(topic, self.capabilities_handler) |
| 256 | |
| 257 | # OMCI-CC Connectivity Events (for reachability/heartbeat) |
| 258 | bus = self._onu_omci_device.omci_cc.event_bus |
| 259 | topic = OMCI_CC.event_bus_topic(self._handler.device_id, |
| 260 | OmciCCRxEvents.Connectivity) |
| 261 | self._connectivity_subscription = bus.subscribe(topic, self.onu_is_reachable) |
| 262 | |
| 263 | # TODO: Watch for any MIB RESET events or detection of an ONU reboot. |
| 264 | # If it occurs, set _service_downloaded and _mib_download to false |
| 265 | # and make sure that we get 'new' capabilities |
| 266 | |
| 267 | def _unsubscribe_to_events(self): |
| 268 | insync, self._in_sync_subscription = self._in_sync_subscription, None |
| 269 | connect, self._connectivity_subscription = self._connectivity_subscription, None |
| 270 | caps, self._capabilities_subscription = self._capabilities_subscription, None |
| 271 | |
| 272 | if insync is not None: |
| 273 | bus = self._onu_omci_device.event_bus |
| 274 | bus.unsubscribe(insync) |
| 275 | |
| 276 | if connect is not None: |
| 277 | bus = self._onu_omci_device.omci_cc.event_bus |
| 278 | bus.unsubscribe(connect) |
| 279 | |
| 280 | if caps is not None: |
| 281 | bus = self._onu_omci_device.event_bus |
| 282 | bus.unsubscribe(caps) |
| 283 | |
| 284 | def in_sync_handler(self, _topic, msg): |
| 285 | if self._in_sync_subscription is not None: |
| 286 | try: |
| 287 | from voltha.extensions.omci.onu_device_entry import IN_SYNC_KEY |
| 288 | |
| 289 | if msg[IN_SYNC_KEY]: |
| 290 | # Start up device_info load from MIB DB |
| 291 | reactor.callLater(0, self._mib_in_sync) |
| 292 | else: |
| 293 | # Cancel any running/scheduled MIB download task |
| 294 | try: |
| 295 | d, self._mib_download_deferred = self._mib_download_deferred, None |
| 296 | d.cancel() |
| 297 | except: |
| 298 | pass |
| 299 | |
| 300 | except Exception as e: |
| 301 | self.log.exception('in-sync', e=e) |
| 302 | |
| 303 | def capabilities_handler(self, _topic, _msg): |
| 304 | """ |
| 305 | This event occurs after an ONU reaches the In-Sync state and the OMCI ME has |
| 306 | been queried for supported ME and message types. |
| 307 | |
| 308 | At this point, we can act upon any download device and/or service Technology |
| 309 | profiles (when they exist). For now, just run our somewhat fixed script |
| 310 | """ |
| 311 | if self._capabilities_subscription is not None: |
| 312 | from adtn_mib_download_task import AdtnMibDownloadTask |
| 313 | self._mib_download_task = None |
| 314 | |
| 315 | def success(_results): |
| 316 | dev = self._handler.adapter_agent.get_device(self._handler.device_id) |
| 317 | dev.reason = '' |
| 318 | self._handler.adapter_agent.update_device(dev) |
| 319 | self._mib_downloaded = True |
| 320 | self._mib_download_task = None |
| 321 | |
| 322 | def failure(reason): |
| 323 | self.log.error('mib-download-failure', reason=reason) |
| 324 | self._mib_download_task = None |
| 325 | dev = self._handler.adapter_agent.get_device(self._handler.device_id) |
| 326 | self._handler.adapter_agent.update_device(dev) |
| 327 | self._mib_download_deferred = reactor.callLater(_STARTUP_RETRY_WAIT, |
| 328 | self.capabilities_handler, |
| 329 | None, None) |
| 330 | if not self._mib_downloaded: |
| 331 | device = self._handler.adapter_agent.get_device(self._handler.device_id) |
| 332 | device.reason = 'Initial MIB Download' |
| 333 | self._handler.adapter_agent.update_device(device) |
| 334 | self._mib_download_task = AdtnMibDownloadTask(self.omci_agent, |
| 335 | self._handler) |
| 336 | if self._mib_download_task is not None: |
| 337 | self._mib_download_deferred = \ |
| 338 | self._onu_omci_device.task_runner.queue_task(self._mib_download_task) |
| 339 | self._mib_download_deferred.addCallbacks(success, failure) |
| 340 | |
| 341 | def onu_is_reachable(self, _topic, msg): |
| 342 | """ |
| 343 | Reach-ability change event |
| 344 | :param _topic: (str) subscription topic, not used |
| 345 | :param msg: (dict) 'connected' key holds True if reachable |
| 346 | """ |
| 347 | from voltha.extensions.omci.omci_cc import CONNECTED_KEY |
| 348 | if self._connectivity_subscription is not None: |
| 349 | try: |
| 350 | connected = msg[CONNECTED_KEY] |
| 351 | |
| 352 | # TODO: For now, only care about the first connect occurrence. |
| 353 | # Later we could use this for a heartbeat, but may want some hysteresis |
| 354 | # Cancel any 'reachable' subscriptions |
| 355 | if connected: |
| 356 | evt_bus = self._onu_omci_device.omci_cc.event_bus |
| 357 | evt_bus.unsubscribe(self._connectivity_subscription) |
| 358 | self._connectivity_subscription = None |
| 359 | self._connected = True |
| 360 | |
| 361 | device = self._handler.adapter_agent.get_device(self._handler.device_id) |
| 362 | device.oper_status = OperStatus.ACTIVE |
| 363 | device.connect_status = ConnectStatus.REACHABLE |
| 364 | self._handler.adapter_agent.update_device(device) |
| 365 | |
| 366 | except Exception as e: |
| 367 | self.log.exception('onu-reachable', e=e) |