blob: af11e9bcaaf1fe8b2dd6851f7681d395d773f1f1 [file] [log] [blame]
Chip Bolingf5af85d2019-02-12 15:36:17 -06001#
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
17import random
18import arrow
19
20import structlog
21import xmltodict
22from adapters.adtran_common.port import AdtnPort
23from twisted.internet import reactor
24from twisted.internet.defer import inlineCallbacks, returnValue, succeed, fail
25from twisted.python.failure import Failure
26from pyvoltha.protos.common_pb2 import OperStatus, AdminState
27from pyvoltha.protos.device_pb2 import Port
28from pyvoltha.protos.logical_device_pb2 import LogicalPort
29from pyvoltha.protos.openflow_13_pb2 import OFPPF_100GB_FD, OFPPF_FIBER, OFPPS_LIVE, ofp_port
30
31
32class 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
367class 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')