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 | |
| 17 | import random |
| 18 | import arrow |
| 19 | |
| 20 | import structlog |
| 21 | import xmltodict |
| 22 | from adapters.adtran_common.port import AdtnPort |
| 23 | from twisted.internet import reactor |
| 24 | from twisted.internet.defer import inlineCallbacks, returnValue, succeed, fail |
| 25 | from twisted.python.failure import Failure |
| 26 | from pyvoltha.protos.common_pb2 import OperStatus, AdminState |
| 27 | from pyvoltha.protos.device_pb2 import Port |
| 28 | from pyvoltha.protos.logical_device_pb2 import LogicalPort |
| 29 | from pyvoltha.protos.openflow_13_pb2 import OFPPF_100GB_FD, OFPPF_FIBER, OFPPS_LIVE, ofp_port |
| 30 | |
| 31 | |
| 32 | class NniPort(AdtnPort): |
| 33 | """ |
| 34 | Northbound network port, often Ethernet-based |
| 35 | """ |
| 36 | def __init__(self, parent, **kwargs): |
| 37 | super(NniPort, self).__init__(parent, **kwargs) |
| 38 | |
| 39 | # TODO: Weed out those properties supported by common 'Port' object |
| 40 | |
| 41 | self.log = structlog.get_logger(port_no=kwargs.get('port_no')) |
| 42 | self.log.info('creating') |
| 43 | |
| 44 | # ONOS/SEBA wants 'nni-<port>' for port names, OLT NETCONF wants their |
| 45 | # name (something like hundred-gigabit-ethernet 0/1) which is reported |
| 46 | # when we enumerated the ports |
| 47 | self._physical_port_name = kwargs.get('name', 'nni-{}'.format(self._port_no)) |
| 48 | self._logical_port_name = 'nni-{}'.format(self._port_no) |
| 49 | |
| 50 | self._logical_port = None |
| 51 | |
| 52 | self.sync_tick = 10.0 |
| 53 | |
| 54 | self._stats_tick = 5.0 |
| 55 | self._stats_deferred = None |
| 56 | |
| 57 | # Local cache of NNI configuration |
| 58 | self._ianatype = '<type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:ethernetCsmacd</type>' |
| 59 | |
| 60 | # And optional parameters |
| 61 | # TODO: Currently cannot update admin/oper status, so create this enabled and active |
| 62 | # self._admin_state = kwargs.pop('admin_state', AdminState.UNKNOWN) |
| 63 | # self._oper_status = kwargs.pop('oper_status', OperStatus.UNKNOWN) |
| 64 | self._enabled = True |
| 65 | self._admin_state = AdminState.ENABLED |
| 66 | self._oper_status = OperStatus.ACTIVE |
| 67 | |
| 68 | self._label = self._physical_port_name |
| 69 | self._mac_address = kwargs.pop('mac_address', '00:00:00:00:00:00') |
| 70 | # TODO: Get with JOT and find out how to pull out MAC Address via NETCONF |
| 71 | # TODO: May need to refine capabilities into current, advertised, and peer |
| 72 | |
| 73 | self._ofp_capabilities = kwargs.pop('ofp_capabilities', OFPPF_100GB_FD | OFPPF_FIBER) |
| 74 | self._ofp_state = kwargs.pop('ofp_state', OFPPS_LIVE) |
| 75 | self._current_speed = kwargs.pop('current_speed', OFPPF_100GB_FD) |
| 76 | self._max_speed = kwargs.pop('max_speed', OFPPF_100GB_FD) |
| 77 | self._device_port_no = kwargs.pop('device_port_no', self._port_no) |
| 78 | |
| 79 | # Statistics |
| 80 | self.rx_dropped = 0 |
| 81 | self.rx_error_packets = 0 |
| 82 | self.rx_ucast_packets = 0 |
| 83 | self.rx_bcast_packets = 0 |
| 84 | self.rx_mcast_packets = 0 |
| 85 | self.tx_dropped = 0 |
| 86 | self.rx_ucast_packets = 0 |
| 87 | self.tx_bcast_packets = 0 |
| 88 | self.tx_mcast_packets = 0 |
| 89 | |
| 90 | def __str__(self): |
| 91 | return "NniPort-{}: Admin: {}, Oper: {}, parent: {}".format(self._port_no, |
| 92 | self._admin_state, |
| 93 | self._oper_status, |
| 94 | self._parent) |
| 95 | |
| 96 | def get_port(self): |
| 97 | """ |
| 98 | Get the VOLTHA PORT object for this port |
| 99 | :return: VOLTHA Port object |
| 100 | """ |
| 101 | self.log.debug('get-port-status-update', port=self._port_no, |
| 102 | label=self._label) |
| 103 | if self._port is None: |
| 104 | self._port = Port(port_no=self._port_no, |
| 105 | label=self._label, |
| 106 | type=Port.ETHERNET_NNI, |
| 107 | admin_state=self._admin_state, |
| 108 | oper_status=self._oper_status) |
| 109 | |
| 110 | if self._port.admin_state != self._admin_state or\ |
| 111 | self._port.oper_status != self._oper_status: |
| 112 | |
| 113 | self.log.debug('get-port-status-update', admin_state=self._admin_state, |
| 114 | oper_status=self._oper_status) |
| 115 | self._port.admin_state = self._admin_state |
| 116 | self._port.oper_status = self._oper_status |
| 117 | |
| 118 | return self._port |
| 119 | |
| 120 | @property |
| 121 | def iana_type(self): |
| 122 | return self._ianatype |
| 123 | |
| 124 | def cancel_deferred(self): |
| 125 | super(NniPort, self).cancel_deferred() |
| 126 | |
| 127 | d, self._stats_deferred = self._stats_deferred, None |
| 128 | try: |
| 129 | if d is not None and d.called: |
| 130 | d.cancel() |
| 131 | except: |
| 132 | pass |
| 133 | |
| 134 | def _update_adapter_agent(self): |
| 135 | # adapter_agent add_port also does an update of port status |
| 136 | self.log.debug('update-adapter-agent', admin_state=self._admin_state, |
| 137 | oper_status=self._oper_status) |
| 138 | self.adapter_agent.add_port(self.olt.device_id, self.get_port()) |
| 139 | |
| 140 | def get_logical_port(self): |
| 141 | """ |
| 142 | Get the VOLTHA logical port for this port |
| 143 | :return: VOLTHA logical port or None if not supported |
| 144 | """ |
| 145 | def mac_str_to_tuple(mac): |
| 146 | """ |
| 147 | Convert 'xx:xx:xx:xx:xx:xx' MAC address string to a tuple of integers. |
| 148 | Example: mac_str_to_tuple('00:01:02:03:04:05') == (0, 1, 2, 3, 4, 5) |
| 149 | """ |
| 150 | return tuple(int(d, 16) for d in mac.split(':')) |
| 151 | |
| 152 | if self._logical_port is None: |
| 153 | openflow_port = ofp_port(port_no=self._port_no, |
| 154 | hw_addr=mac_str_to_tuple(self._mac_address), |
| 155 | name=self._logical_port_name, |
| 156 | config=0, |
| 157 | state=self._ofp_state, |
| 158 | curr=self._ofp_capabilities, |
| 159 | advertised=self._ofp_capabilities, |
| 160 | peer=self._ofp_capabilities, |
| 161 | curr_speed=self._current_speed, |
| 162 | max_speed=self._max_speed) |
| 163 | |
| 164 | self._logical_port = LogicalPort(id=self._logical_port_name, |
| 165 | ofp_port=openflow_port, |
| 166 | device_id=self._parent.device_id, |
| 167 | device_port_no=self._device_port_no, |
| 168 | root_port=True) |
| 169 | return self._logical_port |
| 170 | |
| 171 | @inlineCallbacks |
| 172 | def finish_startup(self): |
| 173 | |
| 174 | if self.state != AdtnPort.State.INITIAL: |
| 175 | returnValue('Done') |
| 176 | |
| 177 | self.log.debug('final-startup') |
| 178 | # TODO: Start status polling of NNI interfaces |
| 179 | self.deferred = None # = reactor.callLater(3, self.do_stuff) |
| 180 | |
| 181 | # Begin statistics sync |
| 182 | self._stats_deferred = reactor.callLater(self._stats_tick * 2, self._update_statistics) |
| 183 | |
| 184 | try: |
| 185 | yield self.set_config('enabled', True) |
| 186 | |
| 187 | super(NniPort, self).finish_startup() |
| 188 | |
| 189 | except Exception as e: |
| 190 | self.log.exception('nni-start', e=e) |
| 191 | self._oper_status = OperStatus.UNKNOWN |
| 192 | self._update_adapter_agent() |
| 193 | |
| 194 | returnValue('Enabled') |
| 195 | |
| 196 | def finish_stop(self): |
| 197 | |
| 198 | # NOTE: Leave all NNI ports active (may have inband management) |
| 199 | # TODO: Revisit leaving NNI Ports active on disable |
| 200 | |
| 201 | return self.set_config('enabled', False) |
| 202 | |
| 203 | @inlineCallbacks |
| 204 | def reset(self): |
| 205 | """ |
| 206 | Set the NNI Port to a known good state on initial port startup. Actual |
| 207 | NNI 'Start' is done elsewhere |
| 208 | """ |
| 209 | # if self.state != AdtnPort.State.INITIAL: |
| 210 | # self.log.error('reset-ignored', state=self.state) |
| 211 | # returnValue('Ignored') |
| 212 | |
| 213 | self.log.info('resetting', label=self._label) |
| 214 | |
| 215 | # Always enable our NNI ports |
| 216 | |
| 217 | try: |
| 218 | results = yield self.set_config('enabled', True) |
| 219 | self._admin_state = AdminState.ENABLED |
| 220 | self._enabled = True |
| 221 | returnValue(results) |
| 222 | |
| 223 | except Exception as e: |
| 224 | self.log.exception('reset', e=e) |
| 225 | self._admin_state = AdminState.UNKNOWN |
| 226 | raise |
| 227 | |
| 228 | @inlineCallbacks |
| 229 | def set_config(self, leaf, value): |
| 230 | if isinstance(value, bool): |
| 231 | value = 'true' if value else 'false' |
| 232 | |
| 233 | config = '<interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">' + \ |
| 234 | ' <interface>' + \ |
| 235 | ' <name>{}</name>'.format(self._physical_port_name) + \ |
| 236 | ' {}'.format(self._ianatype) + \ |
| 237 | ' <{}>{}</{}>'.format(leaf, value, leaf) + \ |
| 238 | ' </interface>' + \ |
| 239 | '</interfaces>' |
| 240 | try: |
| 241 | results = yield self._parent.netconf_client.edit_config(config) |
| 242 | returnValue(results) |
| 243 | |
| 244 | except Exception as e: |
| 245 | self.log.exception('set', leaf=leaf, value=value, e=e) |
| 246 | raise |
| 247 | |
| 248 | def get_nni_config(self): |
| 249 | config = '<filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">' + \ |
| 250 | ' <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">' + \ |
| 251 | ' <interface>' + \ |
| 252 | ' <name>{}</name>'.format(self._physical_port_name) + \ |
| 253 | ' <enabled/>' + \ |
| 254 | ' </interface>' + \ |
| 255 | ' </interfaces>' + \ |
| 256 | '</filter>' |
| 257 | return self._parent.netconf_client.get(config) |
| 258 | |
| 259 | def get_nni_statistics(self): |
| 260 | state = '<filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">' + \ |
| 261 | ' <interfaces-state xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">' + \ |
| 262 | ' <interface>' + \ |
| 263 | ' <name>{}</name>'.format(self._physical_port_name) + \ |
| 264 | ' <admin-status/>' + \ |
| 265 | ' <oper-status/>' + \ |
| 266 | ' <statistics/>' + \ |
| 267 | ' </interface>' + \ |
| 268 | ' </interfaces>' + \ |
| 269 | '</filter>' |
| 270 | return self._parent.netconf_client.get(state) |
| 271 | |
| 272 | def sync_hardware(self): |
| 273 | if self.state == AdtnPort.State.RUNNING or self.state == AdtnPort.State.STOPPED: |
| 274 | def read_config(results): |
| 275 | #self.log.debug('read-config', results=results) |
| 276 | try: |
| 277 | result_dict = xmltodict.parse(results.data_xml) |
| 278 | interfaces = result_dict['data']['interfaces'] |
| 279 | if 'if:interface' in interfaces: |
| 280 | entries = interfaces['if:interface'] |
| 281 | else: |
| 282 | entries = interfaces['interface'] |
| 283 | |
| 284 | enabled = entries.get('enabled', |
| 285 | str(not self.enabled).lower()) == 'true' |
| 286 | |
| 287 | if self.enabled == enabled: |
| 288 | return succeed('in-sync') |
| 289 | |
| 290 | self.set_config('enabled', self.enabled) |
| 291 | self._oper_status = OperStatus.ACTIVE |
| 292 | self._update_adapter_agent() |
| 293 | |
| 294 | except Exception as e: |
| 295 | self.log.exception('read-config', e=e) |
| 296 | return fail(Failure()) |
| 297 | |
| 298 | def failure(reason): |
| 299 | self.log.error('hardware-sync-failed', reason=reason) |
| 300 | |
| 301 | def reschedule(_): |
| 302 | delay = self.sync_tick |
| 303 | delay += random.uniform(-delay / 10, delay / 10) |
| 304 | self.sync_deferred = reactor.callLater(delay, self.sync_hardware) |
| 305 | |
| 306 | self.sync_deferred = self.get_nni_config() |
| 307 | self.sync_deferred.addCallbacks(read_config, failure) |
| 308 | self.sync_deferred.addBoth(reschedule) |
| 309 | |
| 310 | def _decode_nni_statistics(self, entry): |
| 311 | # admin_status = entry.get('admin-status') |
| 312 | # oper_status = entry.get('oper-status') |
| 313 | # admin_status = entry.get('admin-status') |
| 314 | # phys_address = entry.get('phys-address') |
| 315 | |
| 316 | stats = entry.get('statistics') |
| 317 | if stats is not None: |
| 318 | self.timestamp = arrow.utcnow().float_timestamp |
| 319 | self.rx_bytes = int(stats.get('in-octets', 0)) |
| 320 | self.rx_ucast_packets = int(stats.get('in-unicast-pkts', 0)) |
| 321 | self.rx_bcast_packets = int(stats.get('in-broadcast-pkts', 0)) |
| 322 | self.rx_mcast_packets = int(stats.get('in-multicast-pkts', 0)) |
| 323 | self.rx_error_packets = int(stats.get('in-errors', 0)) + int(stats.get('in-discards', 0)) |
| 324 | |
| 325 | self.tx_bytes = int(stats.get('out-octets', 0)) |
| 326 | self.tx_ucast_packets = int(stats.get('out-unicast-pkts', 0)) |
| 327 | self.tx_bcast_packets = int(stats.get('out-broadcast-pkts', 0)) |
| 328 | self.tx_mcasy_packets = int(stats.get('out-multicast-pkts', 0)) |
| 329 | self.tx_error_packets = int(stats.get('out-errors', 0)) + int(stats.get('out-discards', 0)) |
| 330 | |
| 331 | self.rx_packets = self.rx_ucast_packets + self.rx_mcast_packets + self.rx_bcast_packets |
| 332 | self.tx_packets = self.tx_ucast_packets + self.tx_mcast_packets + self.tx_bcast_packets |
| 333 | # No support for rx_crc_errors or bip_errors |
| 334 | |
| 335 | def _update_statistics(self): |
| 336 | if self.state == AdtnPort.State.RUNNING: |
| 337 | def read_state(results): |
| 338 | # self.log.debug('read-state', results=results) |
| 339 | try: |
| 340 | result_dict = xmltodict.parse(results.data_xml) |
| 341 | entry = result_dict['data']['interfaces-state']['interface'] |
| 342 | self._decode_nni_statistics(entry) |
| 343 | return succeed('done') |
| 344 | |
| 345 | except Exception as e: |
| 346 | self.log.exception('read-state', e=e) |
| 347 | return fail(Failure()) |
| 348 | |
| 349 | def failure(reason): |
| 350 | self.log.error('update-stats-failed', reason=reason) |
| 351 | |
| 352 | def reschedule(_): |
| 353 | delay = self._stats_tick |
| 354 | delay += random.uniform(-delay / 10, delay / 10) |
| 355 | self._stats_deferred = reactor.callLater(delay, self._update_statistics) |
| 356 | |
| 357 | try: |
| 358 | self._stats_deferred = self.get_nni_statistics() |
| 359 | self._stats_deferred.addCallbacks(read_state, failure) |
| 360 | self._stats_deferred.addBoth(reschedule) |
| 361 | |
| 362 | except Exception as e: |
| 363 | self.log.exception('nni-sync', port=self.name, e=e) |
| 364 | self._stats_deferred = reactor.callLater(self._stats_tick, self._update_statistics) |
| 365 | |
| 366 | |
| 367 | class MockNniPort(NniPort): |
| 368 | """ |
| 369 | A class similar to the 'Port' class in the VOLTHA but for a non-existent (virtual OLT) |
| 370 | |
| 371 | TODO: Merge this with the Port class or cleanup where possible |
| 372 | so we do not duplicate fields/properties/methods |
| 373 | """ |
| 374 | |
| 375 | def __init__(self, parent, **kwargs): |
| 376 | super(MockNniPort, self).__init__(parent, **kwargs) |
| 377 | |
| 378 | def __str__(self): |
| 379 | return "NniPort-mock-{}: Admin: {}, Oper: {}, parent: {}".format(self._port_no, |
| 380 | self._admin_state, |
| 381 | self._oper_status, |
| 382 | self._parent) |
| 383 | |
| 384 | @staticmethod |
| 385 | def get_nni_port_state_results(): |
| 386 | from ncclient.operations.retrieve import GetReply |
| 387 | raw = """ |
| 388 | <?xml version="1.0" encoding="UTF-8"?> |
| 389 | <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" |
| 390 | xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" |
| 391 | message-id="urn:uuid:59e71979-01bb-462f-b17a-b3a45e1889ac"> |
| 392 | <data> |
| 393 | <interfaces-state xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"> |
| 394 | <interface><name>hundred-gigabit-ethernet 0/1</name></interface> |
| 395 | </interfaces-state> |
| 396 | </data> |
| 397 | </rpc-reply> |
| 398 | """ |
| 399 | return GetReply(raw) |
| 400 | |
| 401 | @staticmethod |
| 402 | def get_pon_port_state_results(): |
| 403 | from ncclient.operations.retrieve import GetReply |
| 404 | raw = """ |
| 405 | <?xml version="1.0" encoding="UTF-8"?> |
| 406 | <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" |
| 407 | xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" |
| 408 | message-id="urn:uuid:59e71979-01bb-462f-b17a-b3a45e1889ac"> |
| 409 | <data> |
| 410 | <interfaces-state xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"> |
| 411 | <interface><name>XPON 0/1</name></interface> |
| 412 | <interface><name>XPON 0/2</name></interface> |
| 413 | <interface><name>XPON 0/3</name></interface> |
| 414 | <interface><name>XPON 0/4</name></interface> |
| 415 | <interface><name>XPON 0/5</name></interface> |
| 416 | <interface><name>XPON 0/6</name></interface> |
| 417 | <interface><name>XPON 0/7</name></interface> |
| 418 | <interface><name>XPON 0/8</name></interface> |
| 419 | <interface><name>XPON 0/9</name></interface> |
| 420 | <interface><name>XPON 0/10</name></interface> |
| 421 | <interface><name>XPON 0/11</name></interface> |
| 422 | <interface><name>XPON 0/12</name></interface> |
| 423 | <interface><name>XPON 0/13</name></interface> |
| 424 | <interface><name>XPON 0/14</name></interface> |
| 425 | <interface><name>XPON 0/15</name></interface> |
| 426 | <interface><name>XPON 0/16</name></interface> |
| 427 | </interfaces-state> |
| 428 | </data> |
| 429 | </rpc-reply> |
| 430 | """ |
| 431 | return GetReply(raw) |
| 432 | |
| 433 | def reset(self): |
| 434 | """ |
| 435 | Set the NNI Port to a known good state on initial port startup. Actual |
| 436 | NNI 'Start' is done elsewhere |
| 437 | """ |
| 438 | if self.state != AdtnPort.State.INITIAL: |
| 439 | self.log.error('reset-ignored', state=self.state) |
| 440 | return fail() |
| 441 | |
| 442 | self.log.info('resetting', label=self._label) |
| 443 | |
| 444 | # Always enable our NNI ports |
| 445 | |
| 446 | self._enabled = True |
| 447 | self._admin_state = AdminState.ENABLED |
| 448 | return succeed('Enabled') |
| 449 | |
| 450 | def set_config(self, leaf, value): |
| 451 | |
| 452 | if leaf == 'enabled': |
| 453 | self._enabled = value |
| 454 | else: |
| 455 | raise NotImplemented("Leaf '{}' is not supported".format(leaf)) |
| 456 | |
| 457 | return succeed('Success') |