Completed
Push — master ( 8d9de4...6fdc5a )
by Thomas
10:31
created

exabgp.reactor.peer.Peer.me()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
# encoding: utf-8
2
"""
3
peer.py
4
5
Created by Thomas Mangin on 2009-08-25.
6
Copyright (c) 2009-2017 Exa Networks. All rights reserved.
7
License: 3-clause BSD. (See the COPYRIGHT file)
8
"""
9
10
import time
11
from collections import defaultdict
12
13
# import traceback
14
from exabgp.vendoring import six
15
from exabgp.util import ordinal
16
from exabgp.bgp.timer import ReceiveTimer
17
from exabgp.bgp.message import Message
18
from exabgp.bgp.fsm import FSM
19
from exabgp.bgp.message.open.capability import Capability
20
from exabgp.bgp.message.open.capability import REFRESH
21
from exabgp.bgp.message import NOP
22
from exabgp.bgp.message import Update
23
from exabgp.bgp.message.refresh import RouteRefresh
24
from exabgp.bgp.message import Notification
25
from exabgp.bgp.message import Notify
26
from exabgp.reactor.protocol import Protocol
27
from exabgp.reactor.delay import Delay
28
from exabgp.reactor.keepalive import KA
29
from exabgp.reactor.network.error import NetworkError
30
from exabgp.reactor.api.processes import ProcessError
31
32
from exabgp.rib.change import Change
33
34
from exabgp.configuration.environment import environment
35
from exabgp.logger import Logger
36
from exabgp.logger import FakeLogger
37
from exabgp.logger import LazyFormat
38
39
from exabgp.util.trace import trace
40
41
from exabgp.util.panic import NO_PANIC
42
from exabgp.util.panic import FOOTER
43
44
45
class ACTION(object):
46
    CLOSE = 0x01  # finished, no need to restart the peer
47
    LATER = 0x02  # re-run at the next reactor round
48
    NOW = 0x03  # re-run immediatlely
49
    ALL = [CLOSE, LATER, NOW]
50
51
52
class SEND(object):
53
    DONE = 0x01
54
    NORMAL = 0x02
55
    REFRESH = 0x04
56
    ALL = [DONE, NORMAL, REFRESH]
57
58
59
# As we can not know if this is our first start or not, this flag is used to
60
# always make the program act like it was recovering from a failure
61
# If set to FALSE, no EOR and OPEN Flags set for Restart will be set in the
62
# OPEN Graceful Restart Capability
63
FORCE_GRACEFUL = True
64
65
66
class Interrupted(Exception):
67
    pass
68
69
70
class Stop(Exception):
71
    pass
72
73
74
# ======================================================================== Peer
75
# Present a File like interface to socket.socket
76
77
78
class Peer(object):
79
    def __init__(self, neighbor, reactor):
80
        try:
81
            self.logger = Logger()
82
            # We only to try to connect via TCP once
83
            self.once = environment.settings().tcp.once
84
            self.bind = True if environment.settings().tcp.bind else False
85
        except RuntimeError:
86
            self.logger = FakeLogger()
87
            self.once = False
88
            self.bind = True
89
90
        now = time.time()
91
92
        self.reactor = reactor
93
        self.neighbor = neighbor
94
        # The next restart neighbor definition
95
        self._neighbor = None
96
97
        self.proto = None
98
        self.fsm = FSM(self, FSM.IDLE)
99
        self.stats = {
100
            'fsm': self.fsm,
101
            'creation': now,
102
            'reset': now,
103
            'complete': 0,
104
        }
105
        self.generator = None
106
107
        # The peer should restart after a stop
108
        self._restart = True
109
        # The peer was restarted (to know what kind of open to send for graceful restart)
110
        self._restarted = FORCE_GRACEFUL
111
112
        # We want to remove routes which are not in the configuration anymore after a signal to reload
113
        self._reconfigure = True
114
        # We want to send all the known routes
115
        self._resend_routes = SEND.DONE
116
117
        # We have been asked to teardown the session with this code
118
        self._teardown = None
119
120
        self._delay = Delay()
121
        self.recv_timer = None
122
123
    def id(self):
124
        return 'peer-%s' % self.neighbor.uid
125
126
    def _reset(self, message='', error=''):
127
        self.fsm.change(FSM.IDLE)
128
        self.stats = {
129
            'fsm': self.fsm,
130
            'creation': self.stats['creation'],
131
            'reset': time.time(),
132
            'complete': 0,
133
        }
134
        if self.proto:
135
            try:
136
                message = u"peer reset, message [{0}] error[{1}]".format(message, error)
137
            except UnicodeDecodeError as msg_err:
138
                message = u"peer reset, message [{0}] error[{1}]".format(message, msg_err)
139
            self.proto.close(message)
140
        self._delay.increase()
141
142
        self.proto = None
143
144
        if not self._restart or self.neighbor.generated:
145
            self.generator = False
146
            return
147
148
        self.generator = None
149
        self._teardown = None
150
        self.neighbor.rib.reset()
151
152
        # If we are restarting, and the neighbor definition is different, update the neighbor
153
        if self._neighbor:
154
            self.neighbor = self._neighbor
155
            self._neighbor = None
156
157
    def _stop(self, message):
158
        self.generator = None
159
        if self.proto:
160
            self.proto.close('stop, message [%s]' % message)
161
            self.proto = None
162
163
    # logging
164
165
    def me(self, message):
166
        return "peer %s ASN %-7s %s" % (self.neighbor.peer_address, self.neighbor.peer_as, message)
167
168
    # control
169
170
    def stop(self):
171
        self._teardown = 3
172
        self._restart = False
173
        self._restarted = False
174
        self._delay.reset()
175
        self.fsm.change(FSM.IDLE)
176
        self.stats = {
177
            'fsm': self.fsm,
178
            'creation': self.stats['creation'],
179
            'reset': time.time(),
180
        }
181
        self.neighbor.rib.uncache()
182
183
    def remove(self):
184
        self._stop("removed")
185
        self.stop()
186
187
    def shutdown(self):
188
        self._stop("shutting down")
189
        self.stop()
190
191
    def resend(self):
192
        self._resend_routes = SEND.NORMAL
193
        self._delay.reset()
194
195
    def reestablish(self, restart_neighbor=None):
196
        # we want to tear down the session and re-establish it
197
        self._teardown = 3
198
        self._restart = True
199
        self._restarted = True
200
        self._resend_routes = SEND.NORMAL
201
        self._neighbor = restart_neighbor
202
        self._delay.reset()
203
204
    def reconfigure(self, restart_neighbor=None):
205
        # we want to update the route which were in the configuration file
206
        self._reconfigure = True
207
        self._neighbor = restart_neighbor
208
        self._resend_routes = SEND.NORMAL
209
        self._neighbor = restart_neighbor
210
211
    def teardown(self, code, restart=True):
212
        self._restart = restart
213
        self._teardown = code
214
        self._delay.reset()
215
216
    def socket(self):
217
        if self.proto:
218
            return self.proto.fd()
219
        return -1
220
221
    def handle_connection(self, connection):
222
        self.logger.debug("state machine for the peer is %s" % self.fsm.name(), self.id())
223
224
        # if the other side fails, we go back to idle
225
        if self.fsm == FSM.ESTABLISHED:
226
            self.logger.debug('we already have a peer in state established for %s' % connection.name(), self.id())
227
            return connection.notification(6, 7, 'could not accept the connection, already established')
228
229
        # 6.8 The convention is to compare the BGP Identifiers of the peers
230
        # involved in the collision and to retain only the connection initiated
231
        # by the BGP speaker with the higher-valued BGP Identifier.
232
        # FSM.IDLE , FSM.ACTIVE , FSM.CONNECT , FSM.OPENSENT , FSM.OPENCONFIRM , FSM.ESTABLISHED
233
234
        if self.fsm == FSM.OPENCONFIRM:
235
            # We cheat: we are not really reading the OPEN, we use the data we have instead
236
            # it does not matter as the open message will be the same anyway
237
            local_id = self.neighbor.router_id.pack()
238
            remote_id = self.proto.negotiated.received_open.router_id.pack()
239
240
            if remote_id < local_id:
241
                self.logger.debug(
242
                    'closing incoming connection as we have an outgoing connection with higher router-id for %s'
243
                    % connection.name(),
244
                    self.id(),
245
                )
246
                return connection.notification(
247
                    6,
248
                    7,
249
                    'could not accept the connection, as another connection is already in open-confirm and will go through',
250
                )
251
252
        # accept the connection
253
        if self.proto:
254
            self.logger.debug(
255
                'closing outgoing connection as we have another incoming on with higher router-id for %s'
256
                % connection.name(),
257
                self.id(),
258
            )
259
            self.proto.close('closing outgoing connection as we have another incoming on with higher router-id')
260
261
        self.proto = Protocol(self).accept(connection)
262
        self.generator = None
263
        # Let's make sure we do some work with this connection
264
        self._delay.reset()
265
        return None
266
267
    def established(self):
268
        return self.fsm == FSM.ESTABLISHED
269
270
    def negotiated_families(self):
271
        if self.proto:
272
            families = ["%s/%s" % (x[0], x[1]) for x in self.proto.negotiated.families]
273
        else:
274
            families = ["%s/%s" % (x[0], x[1]) for x in self.neighbor.families()]
275
276
        if len(families) > 1:
277
            return "[ %s ]" % " ".join(families)
278
        elif len(families) == 1:
279
            return families[0]
280
281
        return ''
282
283
    def _connect(self):
284
        proto = Protocol(self)
285
        connected = False
286
        try:
287
            for connected in proto.connect():
288
                if connected:
289
                    break
290
                if self._teardown:
291
                    raise Stop()
292
                # we want to come back as soon as possible
293
                yield ACTION.LATER
294
            self.proto = proto
295
        except Stop:
296
            # Connection failed
297
            if not connected and self.proto:
298
                self.proto.close('connection to %s:%d failed' % (self.neighbor.peer_address, self.neighbor.connect))
299
300
            # A connection arrived before we could establish !
301
            if not connected or self.proto:
302
                yield ACTION.NOW
303
                raise Interrupted()
304
305
    def _send_open(self):
306
        message = Message.CODE.NOP
307
        for message in self.proto.new_open():
308
            if ordinal(message.TYPE) == Message.CODE.NOP:
309
                yield ACTION.NOW
310
        yield message
311
312
    def _read_open(self):
313
        wait = environment.settings().bgp.openwait
314
        opentimer = ReceiveTimer(
315
            self.proto.connection.session, wait, 1, 1, 'waited for open too long, we do not like stuck in active'
316
        )
317
        # Only yield if we have not the open, otherwise the reactor can run the other connection
318
        # which would be bad as we need to do the collission check without going to the other peer
319
        for message in self.proto.read_open(self.neighbor.peer_address.top()):
320
            opentimer.check_ka(message)
321
            # XXX: FIXME: change the whole code to use the ord and not the chr version
322
            # Only yield if we have not the open, otherwise the reactor can run the other connection
323
            # which would be bad as we need to do the collission check
324
            if ordinal(message.TYPE) == Message.CODE.NOP:
325
                # If a peer does not reply to OPEN message, or not enough bytes
326
                # yielding ACTION.NOW can cause ExaBGP to busy spin trying to
327
                # read from peer. See GH #723 .
328
                yield ACTION.LATER
329
        yield message
0 ignored issues
show
introduced by
The variable message does not seem to be defined in case the for loop on line 319 is not entered. Are you sure this can never be the case?
Loading history...
330
331
    def _send_ka(self):
332
        for message in self.proto.new_keepalive('OPENCONFIRM'):
333
            yield ACTION.NOW
334
335
    def _read_ka(self):
336
        # Start keeping keepalive timer
337
        for message in self.proto.read_keepalive():
338
            self.recv_timer.check_ka_timer(message)
339
            yield ACTION.NOW
340
341
    def _establish(self):
342
        # try to establish the outgoing connection
343
        self.fsm.change(FSM.ACTIVE)
344
345
        if not self.proto:
346
            for action in self._connect():
347
                if action in ACTION.ALL:
348
                    yield action
349
        self.fsm.change(FSM.CONNECT)
350
351
        # normal sending of OPEN first ...
352
        if self.neighbor.local_as:
353
            for sent_open in self._send_open():
354
                if sent_open in ACTION.ALL:
355
                    yield sent_open
356
            self.proto.negotiated.sent(sent_open)
0 ignored issues
show
introduced by
The variable sent_open does not seem to be defined in case the for loop on line 353 is not entered. Are you sure this can never be the case?
Loading history...
357
            self.fsm.change(FSM.OPENSENT)
358
359
        # read the peer's open
360
        for received_open in self._read_open():
361
            if received_open in ACTION.ALL:
362
                yield received_open
363
        self.proto.negotiated.received(received_open)
0 ignored issues
show
introduced by
The variable received_open does not seem to be defined in case the for loop on line 360 is not entered. Are you sure this can never be the case?
Loading history...
364
365
        self.proto.connection.msg_size = self.proto.negotiated.msg_size
366
367
        # if we mirror the ASN, we need to read first and send second
368
        if not self.neighbor.local_as:
369
            for sent_open in self._send_open():
370
                if sent_open in ACTION.ALL:
371
                    yield sent_open
372
            self.proto.negotiated.sent(sent_open)
373
            self.fsm.change(FSM.OPENSENT)
374
375
        self.proto.validate_open()
376
        self.fsm.change(FSM.OPENCONFIRM)
377
378
        self.recv_timer = ReceiveTimer(self.proto.connection.session, self.proto.negotiated.holdtime, 4, 0)
379
        for action in self._send_ka():
380
            yield action
381
        for action in self._read_ka():
382
            yield action
383
        self.fsm.change(FSM.ESTABLISHED)
384
        self.stats['complete'] = time.time()
385
386
        # let the caller know that we were sucesfull
387
        yield ACTION.NOW
388
389
    def _main(self):
390
        """yield True if we want to come back to it asap, None if nothing urgent, and False if stopped"""
391
        if self._teardown:
392
            raise Notify(6, 3)
393
394
        self.neighbor.rib.incoming.clear()
395
396
        include_withdraw = False
397
398
        # Announce to the process BGP is up
399
        self.logger.notice('connected to %s with %s' % (self.id(), self.proto.connection.name()), 'reactor')
400
        self.stats['up'] = self.stats.get('up', 0) + 1
401
        if self.neighbor.api['neighbor-changes']:
402
            try:
403
                self.reactor.processes.up(self.neighbor)
404
            except ProcessError:
405
                # Can not find any better error code than 6,0 !
406
                # XXX: We can not restart the program so this will come back again and again - FIX
407
                # XXX: In the main loop we do exit on this kind of error
408
                raise Notify(6, 0, 'ExaBGP Internal error, sorry.')
409
410
        send_eor = not self.neighbor.manual_eor
411
        new_routes = None
412
        self._resend_routes = SEND.NORMAL
413
        send_families = []
414
415
        # Every last asm message should be re-announced on restart
416
        for family in self.neighbor.asm:
417
            if family in self.neighbor.families():
418
                self.neighbor.messages.appendleft(self.neighbor.asm[family])
419
420
        operational = None
421
        refresh = None
422
        command_eor = None
423
        number = 0
424
        refresh_enhanced = True if self.proto.negotiated.refresh == REFRESH.ENHANCED else False
425
426
        send_ka = KA(self.proto.connection.session, self.proto)
427
428
        while not self._teardown:
429
            for message in self.proto.read_message():
430
                self.recv_timer.check_ka(message)
431
432
                if send_ka() is not False:
433
                    # we need and will send a keepalive
434
                    while send_ka() is None:
435
                        yield ACTION.NOW
436
437
                # Received update
438
                if message.TYPE == Update.TYPE:
439
                    number += 1
440
                    self.logger.debug('<< UPDATE #%d' % number, self.id())
441
442
                    for nlri in message.nlris:
443
                        self.neighbor.rib.incoming.update_cache(Change(nlri, message.attributes))
444
                        self.logger.debug(LazyFormat('   UPDATE #%d nlri ' % number, nlri, str), self.id())
445
446
                elif message.TYPE == RouteRefresh.TYPE:
447
                    if message.reserved == RouteRefresh.request:
448
                        self._resend_routes = SEND.REFRESH
449
                        send_families.append((message.afi, message.safi))
450
451
                # SEND OPERATIONAL
452
                if self.neighbor.operational:
453
                    if not operational:
454
                        new_operational = self.neighbor.messages.popleft() if self.neighbor.messages else None
455
                        if new_operational:
456
                            operational = self.proto.new_operational(new_operational, self.proto.negotiated)
457
458
                    if operational:
459
                        try:
460
                            six.next(operational)
461
                        except StopIteration:
462
                            operational = None
463
                # make sure that if some operational message are received via the API
464
                # that we do not eat memory for nothing
465
                elif self.neighbor.messages:
466
                    self.neighbor.messages.popleft()
467
468
                # SEND REFRESH
469
                if self.neighbor.route_refresh:
470
                    if not refresh:
471
                        new_refresh = self.neighbor.refresh.popleft() if self.neighbor.refresh else None
472
                        if new_refresh:
473
                            refresh = self.proto.new_refresh(new_refresh)
474
475
                    if refresh:
476
                        try:
477
                            six.next(refresh)
478
                        except StopIteration:
479
                            refresh = None
480
481
                # Take the routes already sent to that peer and resend them
482
                if self._reconfigure:
483
                    self._reconfigure = False
484
485
                    # we are here following a configuration change
486
                    if self._neighbor:
487
                        # see what changed in the configuration
488
                        self.neighbor.rib.outgoing.replace(self._neighbor.backup_changes, self._neighbor.changes)
489
                        # do not keep the previous routes in memory as they are not useful anymore
490
                        self._neighbor.backup_changes = []
491
492
                # Take the routes already sent to that peer and resend them
493
                if self._resend_routes != SEND.DONE:
494
                    enhanced = True if refresh_enhanced and self._resend_routes == SEND.REFRESH else False
495
                    self._resend_routes = SEND.DONE
496
                    self.neighbor.rib.outgoing.resend(send_families, enhanced)
497
                    send_families = []
498
499
                # Need to send update
500
                if not new_routes and self.neighbor.rib.outgoing.pending():
501
                    # XXX: in proto really. hum to think about ?
502
                    new_routes = self.proto.new_update(include_withdraw)
503
504
                if new_routes:
505
                    count = 1 if self.neighbor.rate_limit > 0 else 25
506
                    try:
507
                        for _ in range(count):
508
                            # This can raise a NetworkError
509
                            six.next(new_routes)
510
                    except StopIteration:
511
                        new_routes = None
512
                        include_withdraw = True
513
514
                elif send_eor:
515
                    send_eor = False
516
                    for _ in self.proto.new_eors():
517
                        yield ACTION.NOW
518
                    self.logger.debug('>> EOR(s)', self.id())
519
520
                # SEND MANUAL KEEPALIVE (only if we have no more routes to send)
521
                elif not command_eor and self.neighbor.eor:
522
                    new_eor = self.neighbor.eor.popleft()
523
                    command_eor = self.proto.new_eors(new_eor.afi, new_eor.safi)
524
525
                if command_eor:
526
                    try:
527
                        six.next(command_eor)
528
                    except StopIteration:
529
                        command_eor = None
530
531
                if new_routes or message.TYPE != NOP.TYPE:
532
                    yield ACTION.NOW
533
                elif self.neighbor.messages or operational:
534
                    yield ACTION.NOW
535
                elif self.neighbor.eor or command_eor:
536
                    yield ACTION.NOW
537
                else:
538
                    yield ACTION.LATER
539
540
                # read_message will loop until new message arrives with NOP
541
                if self._teardown:
542
                    break
543
544
        # If graceful restart, silent shutdown
545
        if self.neighbor.graceful_restart and self.proto.negotiated.sent_open.capabilities.announced(
546
            Capability.CODE.GRACEFUL_RESTART
547
        ):
548
            self.logger.error('closing the session without notification', self.id())
549
            self.proto.close('graceful restarted negotiated, closing without sending any notification')
550
            raise NetworkError('closing')
551
552
        # notify our peer of the shutdown
553
        raise Notify(6, self._teardown)
554
555
    def _run(self):
556
        """yield True if we want the reactor to give us back the hand with the same peer loop, None if we do not have any more work to do"""
557
        try:
558
            for action in self._establish():
559
                yield action
560
561
            for action in self._main():
562
                yield action
563
564
        # CONNECTION FAILURE
565
        except NetworkError as network:
566
            # we tried to connect once, it failed and it was not a manual request, we stop
567
            if self.once and not self._teardown:
568
                self.logger.debug('only one attempt to connect is allowed, stopping the peer', self.id())
569
                self.stop()
570
571
            self._reset('closing connection', network)
572
            return
573
574
        # NOTIFY THE PEER OF AN ERROR
575
        except Notify as notify:
576
            if self.proto:
577
                try:
578
                    generator = self.proto.new_notification(notify)
579
                    try:
580
                        while True:
581
                            six.next(generator)
582
                            yield ACTION.NOW
583
                    except StopIteration:
584
                        pass
585
                except (NetworkError, ProcessError):
586
                    self.logger.error('Notification not sent', self.id())
587
                self._reset('notification sent (%d,%d)' % (notify.code, notify.subcode), notify)
588
            else:
589
                self._reset()
590
            return
591
592
        # THE PEER NOTIFIED US OF AN ERROR
593
        except Notification as notification:
594
            # we tried to connect once, it failed and it was not a manual request, we stop
595
            if self.once and not self._teardown:
596
                self.logger.debug('only one attempt to connect is allowed, stopping the peer', self.id())
597
                self.stop()
598
599
            self._reset('notification received (%d,%d)' % (notification.code, notification.subcode), notification)
600
            return
601
602
        # RECEIVED a Message TYPE we did not expect
603
        except Message as message:
604
            self._reset('unexpected message received', message)
605
            return
606
607
        # PROBLEM WRITING TO OUR FORKED PROCESSES
608
        except ProcessError as process:
609
            self._reset('process problem', process)
610
            return
611
612
        # ....
613
        except Interrupted as interruption:
614
            self._reset('connection received before we could fully establish one')
615
            return
616
617
        # UNHANDLED PROBLEMS
618
        except Exception as exc:
619
            # Those messages can not be filtered in purpose
620
            self.logger.debug('\n'.join([NO_PANIC, '', '', str(type(exc)), str(exc), trace(), FOOTER]), 'reactor')
621
            self._reset()
622
            return
623
624
    # loop
625
626
    def run(self):
627
        if self.reactor.processes.broken(self.neighbor):
628
            # XXX: we should perhaps try to restart the process ??
629
            self.logger.error('ExaBGP lost the helper process for this peer - stopping', 'process')
630
            if self.reactor.processes.terminate_on_error:
631
                self.reactor.api_shutdown()
632
            else:
633
                self.stop()
634
            return True
635
636
        if self.generator:
637
            try:
638
                # This generator only stops when it raises
639
                # otherwise return one of the ACTION
640
                return six.next(self.generator)
641
            except StopIteration:
642
                # Trying to run a closed loop, no point continuing
643
                self.generator = None
644
                if self._restart:
645
                    return ACTION.LATER
646
                return ACTION.CLOSE
647
648
        elif self.generator is None:
649
            if self.fsm in [FSM.OPENCONFIRM, FSM.ESTABLISHED]:
650
                self.logger.debug('stopping, other connection is established', self.id())
651
                self.generator = False
652
                return ACTION.LATER
653
            if self._delay.backoff():
654
                return ACTION.LATER
655
            if self._restart:
656
                self.logger.debug('initialising connection to %s' % self.id(), 'reactor')
657
                self.generator = self._run()
658
                return ACTION.LATER  # make sure we go through a clean loop
659
            return ACTION.CLOSE
660
661
    def cli_data(self):
662
        def tri(value):
663
            if value is None:
664
                return None
665
            return True if value else False
666
667
        peer = defaultdict(lambda: None)
668
669
        have_peer = self.proto is not None
670
        have_open = self.proto and self.proto.negotiated.received_open
671
672
        if have_peer:
673
            peer.update(
674
                {'multi-session': self.proto.negotiated.multisession, 'operational': self.proto.negotiated.operational,}
675
            )
676
677
        if have_open:
678
            capa = self.proto.negotiated.received_open.capabilities
679
            peer.update(
680
                {
681
                    'router-id': self.proto.negotiated.sent_open.router_id,
682
                    'peer-id': self.proto.negotiated.received_open.router_id,
683
                    'hold-time': self.proto.negotiated.received_open.hold_time,
684
                    'asn4': self.proto.negotiated.asn4,
685
                    'route-refresh': capa.announced(Capability.CODE.ROUTE_REFRESH),
686
                    'multi-session': capa.announced(Capability.CODE.MULTISESSION)
687
                    or capa.announced(Capability.CODE.MULTISESSION_CISCO),
688
                    'add-path': capa.announced(Capability.CODE.ADD_PATH),
689
                    'extended-message': capa.announced(Capability.CODE.EXTENDED_MESSAGE),
690
                    'graceful-restart': capa.announced(Capability.CODE.GRACEFUL_RESTART),
691
                }
692
            )
693
694
        capabilities = {
695
            'asn4': (tri(self.neighbor.asn4), tri(peer['asn4'])),
696
            'route-refresh': (tri(self.neighbor.route_refresh), tri(peer['route-refresh'])),
697
            'multi-session': (tri(self.neighbor.multisession), tri(peer['multi-session'])),
698
            'operational': (tri(self.neighbor.operational), tri(peer['operational'])),
699
            'add-path': (tri(self.neighbor.add_path), tri(peer['add-path'])),
700
            'extended-message': (tri(self.neighbor.extended_message), tri(peer['extended-message'])),
701
            'graceful-restart': (tri(self.neighbor.graceful_restart), tri(peer['graceful-restart'])),
702
        }
703
704
        families = {}
705
        for family in self.neighbor.families():
706
            if have_open:
707
                common = True if family in self.proto.negotiated.families else False
708
                addpath = self.proto.negotiated.addpath.send(*family) and self.proto.negotiated.addpath.receive(*family)
709
            else:
710
                common = None
711
                addpath = None if family in self.neighbor.addpaths() else False
712
            families[family] = (True, common, addpath)
713
714
        messages = {}
715
        total_sent = 0
716
        total_rcvd = 0
717
        for message in ('open', 'notification', 'keepalive', 'update', 'refresh'):
718
            sent = self.stats.get('send-%s' % message, 0)
719
            rcvd = self.stats.get('receive-%s' % message, 0)
720
            total_sent += sent
721
            total_rcvd += rcvd
722
            messages[message] = (sent, rcvd)
723
        messages['total'] = (total_sent, total_rcvd)
724
725
        return {
726
            'down': int(self.stats['reset'] - self.stats['creation']),
727
            'duration': int(time.time() - self.stats['complete']) if self.stats['complete'] else 0,
728
            'local-address': str(self.neighbor.local_address),
729
            'peer-address': str(self.neighbor.peer_address),
730
            'local-as': int(self.neighbor.local_as),
731
            'peer-as': int(self.neighbor.peer_as),
732
            'local-id': str(self.neighbor.router_id),
733
            'peer-id': None if peer['peer-id'] is None else str(peer['router-id']),
734
            'local-hold': int(self.neighbor.hold_time),
735
            'peer-hold': None if peer['hold-time'] is None else int(peer['hold-time']),
736
            'state': self.fsm.name(),
737
            'capabilities': capabilities,
738
            'families': families,
739
            'messages': messages,
740
        }
741