exabgp.bgp.message.update   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 309
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 185
dl 0
loc 309
rs 3.6
c 0
b 0
f 0
wmc 60

6 Methods

Rating   Name   Duplication   Size   Complexity  
A Update.prefix() 0 4 1
A Update.__init__() 0 3 1
A Update.split() 0 23 4
F Update.messages() 0 123 36
A Update.__str__() 0 2 1
F Update.unpack_message() 0 77 17

How to fix   Complexity   

Complexity

Complex classes like exabgp.bgp.message.update often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# encoding: utf-8
2
"""
3
update/__init__.py
4
5
Created by Thomas Mangin on 2009-11-05.
6
Copyright (c) 2009-2017 Exa Networks. All rights reserved.
7
License: 3-clause BSD. (See the COPYRIGHT file)
8
"""
9
10
from struct import pack
11
from struct import unpack
12
13
from exabgp.protocol.ip import NoNextHop
14
from exabgp.protocol.family import AFI
15
from exabgp.protocol.family import SAFI
16
17
from exabgp.bgp.message.direction import IN
18
from exabgp.bgp.message.direction import OUT
19
from exabgp.bgp.message.message import Message
20
from exabgp.bgp.message.update.eor import EOR
21
22
from exabgp.bgp.message.update.attribute import Attributes
23
from exabgp.bgp.message.update.attribute import Attribute
24
from exabgp.bgp.message.update.attribute import MPRNLRI
25
from exabgp.bgp.message.update.attribute import EMPTY_MPRNLRI
26
from exabgp.bgp.message.update.attribute import MPURNLRI
27
from exabgp.bgp.message.update.attribute import EMPTY_MPURNLRI
28
29
from exabgp.bgp.message.notification import Notify
30
from exabgp.bgp.message.update.nlri import NLRI
31
32
from exabgp.logger import log
33
from exabgp.logger import logfunc
34
from exabgp.logger import lazyformat
35
36
# ======================================================================= Update
37
38
# +-----------------------------------------------------+
39
# |   Withdrawn Routes Length (2 octets)                |
40
# +-----------------------------------------------------+
41
# |   Withdrawn Routes (variable)                       |
42
# +-----------------------------------------------------+
43
# |   Total Path Attribute Length (2 octets)            |
44
# +-----------------------------------------------------+
45
# |   Path Attributes (variable)                        |
46
# +-----------------------------------------------------+
47
# |   Network Layer Reachability Information (variable) |
48
# +-----------------------------------------------------+
49
50
# Withdrawn Routes:
51
52
# +---------------------------+
53
# |   Length (1 octet)        |
54
# +---------------------------+
55
# |   Prefix (variable)       |
56
# +---------------------------+
57
58
59
@Message.register
60
class Update(Message):
61
    ID = Message.CODE.UPDATE
62
    TYPE = bytes([Message.CODE.UPDATE])
63
    EOR = False
64
65
    def __init__(self, nlris, attributes):
66
        self.nlris = nlris
67
        self.attributes = attributes
68
69
    # message not implemented we should use messages below.
70
71
    def __str__(self):
72
        return '\n'.join(['%s%s' % (str(self.nlris[n]), str(self.attributes)) for n in range(len(self.nlris))])
73
74
    @staticmethod
75
    def prefix(data):
76
        # This function needs renaming
77
        return pack('!H', len(data)) + data
78
79
    @staticmethod
80
    def split(data):
81
        length = len(data)
82
83
        len_withdrawn = unpack('!H', data[0:2])[0]
84
        withdrawn = data[2 : len_withdrawn + 2]
85
86
        if len(withdrawn) != len_withdrawn:
87
            raise Notify(3, 1, 'invalid withdrawn routes length, not enough data available')
88
89
        start_attributes = len_withdrawn + 4
90
        len_attributes = unpack('!H', data[len_withdrawn + 2 : start_attributes])[0]
91
        start_announced = len_withdrawn + len_attributes + 4
92
        attributes = data[start_attributes:start_announced]
93
        announced = data[start_announced:]
94
95
        if len(attributes) != len_attributes:
96
            raise Notify(3, 1, 'invalid total path attribute length, not enough data available')
97
98
        if 2 + len_withdrawn + 2 + len_attributes + len(announced) != length:
99
            raise Notify(3, 1, 'error in BGP message length, not enough data for the size announced')
100
101
        return withdrawn, attributes, announced
102
103
    # The routes MUST have the same attributes ...
104
    # XXX: FIXME: calculate size progressively to not have to do it every time
105
    # XXX: FIXME: we could as well track when packed_del, packed_mp_del, etc
106
    # XXX: FIXME: are emptied and therefore when we can save calculations
107
    def messages(self, negotiated, include_withdraw=True):
108
        # sort the nlris
109
110
        nlris = []
111
        mp_nlris = {}
112
113
        for nlri in sorted(self.nlris):
114
            if nlri.family() in negotiated.families:
115
                if (
116
                    nlri.afi == AFI.ipv4
117
                    and nlri.safi in [SAFI.unicast, SAFI.multicast]
118
                    and nlri.nexthop.afi == AFI.ipv4
119
                ):
120
                    nlris.append(nlri)
121
                else:
122
                    mp_nlris.setdefault(nlri.family(), {}).setdefault(nlri.action, []).append(nlri)
123
124
        if not nlris and not mp_nlris:
125
            return
126
127
        # If all we have is MP_UNREACH_NLRI, we do not need the default
128
        # attributes. See RFC4760 that states the following:
129
        #
130
        #   An UPDATE message that contains the MP_UNREACH_NLRI is not required
131
        #   to carry any other path attributes.
132
        #
133
        include_defaults = True
134
135
        if mp_nlris and not nlris:
136
            for family, actions in mp_nlris.items():
137
                afi, safi = family
138
                if safi not in (SAFI.unicast, SAFI.multicast):
139
                    break
140
                if set(actions.keys()) != {OUT.WITHDRAW}:
141
                    break
142
            # no break
143
            else:
144
                include_defaults = False
145
146
        attr = self.attributes.pack(negotiated, include_defaults)
147
148
        # Withdraws/NLRIS (IPv4 unicast and multicast)
149
        msg_size = negotiated.msg_size - 19 - 2 - 2 - len(attr)  # 2 bytes for each of the two prefix() header
150
151
        if msg_size < 0:
152
            # raise Notify(6,0,'attributes size is so large we can not even pack one NLRI')
153
            log.critical('attributes size is so large we can not even pack one NLRI', 'parser')
154
            return
155
156
        if msg_size == 0 and (nlris or mp_nlris):
157
            # raise Notify(6,0,'attributes size is so large we can not even pack one NLRI')
158
            log.critical('attributes size is so large we can not even pack one NLRI', 'parser')
159
            return
160
161
        withdraws = b''
162
        announced = b''
163
        for nlri in nlris:
164
            packed = nlri.pack(negotiated)
165
            if len(announced + withdraws + packed) <= msg_size:
166
                if nlri.action == OUT.ANNOUNCE:
167
                    announced += packed
168
                elif include_withdraw:
169
                    withdraws += packed
170
                continue
171
172
            if not withdraws and not announced:
173
                # raise Notify(6,0,'attributes size is so large we can not even pack one NLRI')
174
                log.critical('attributes size is so large we can not even pack one NLRI', 'parser')
175
                return
176
177
            if announced:
178
                yield self._message(Update.prefix(withdraws) + Update.prefix(attr) + announced)
179
            else:
180
                yield self._message(Update.prefix(withdraws) + Update.prefix(b'') + announced)
181
182
            if nlri.action == OUT.ANNOUNCE:
183
                announced = packed
184
                withdraws = b''
185
            elif include_withdraw:
186
                withdraws = packed
187
                announced = b''
188
            else:
189
                withdraws = b''
190
                announced = b''
191
192
        if announced or withdraws:
193
            if announced:
194
                yield self._message(Update.prefix(withdraws) + Update.prefix(attr) + announced)
195
            else:
196
                yield self._message(Update.prefix(withdraws) + Update.prefix(b'') + announced)
197
198
        for family in mp_nlris.keys():
199
            afi, safi = family
200
            mp_reach = b''
201
            mp_unreach = b''
202
            mp_announce = MPRNLRI(afi, safi, mp_nlris[family].get(OUT.ANNOUNCE, []))
203
            mp_withdraw = MPURNLRI(afi, safi, mp_nlris[family].get(OUT.WITHDRAW, []))
204
205
            for mprnlri in mp_announce.packed_attributes(negotiated, msg_size - len(withdraws + announced)):
206
                if mp_reach:
207
                    yield self._message(Update.prefix(withdraws) + Update.prefix(attr + mp_reach) + announced)
208
                    announced = b''
209
                    withdraws = b''
210
                mp_reach = mprnlri
211
212
            if include_withdraw:
213
                for mpurnlri in mp_withdraw.packed_attributes(
214
                    negotiated, msg_size - len(withdraws + announced + mp_reach)
215
                ):
216
                    if mp_unreach:
217
                        yield self._message(
218
                            Update.prefix(withdraws) + Update.prefix(attr + mp_unreach + mp_reach) + announced
219
                        )
220
                        mp_reach = b''
221
                        announced = b''
222
                        withdraws = b''
223
                    mp_unreach = mpurnlri
224
225
            yield self._message(
226
                Update.prefix(withdraws) + Update.prefix(attr + mp_unreach + mp_reach) + announced
227
            )  # yield mpr/mpur per family
228
            withdraws = b''
229
            announced = b''
230
231
    # XXX: FIXME: this can raise ValueError. IndexError,TypeError, struct.error (unpack) = check it is well intercepted
232
    @classmethod
233
    def unpack_message(cls, data, negotiated):
234
        logfunc.debug(lazyformat('parsing UPDATE', data), 'parser')
235
236
        length = len(data)
237
238
        # This could be speed up massively by changing the order of the IF
239
        if length == 4 and data == b'\x00\x00\x00\x00':
240
            return EOR(AFI.ipv4, SAFI.unicast)  # pylint: disable=E1101
241
        if length == 11 and data.startswith(EOR.NLRI.PREFIX):
242
            return EOR.unpack_message(data, negotiated)
243
244
        withdrawn, _attributes, announced = cls.split(data)
245
246
        if not withdrawn:
247
            log.debug('withdrawn NLRI none', 'routes')
248
249
        attributes = Attributes.unpack(_attributes, negotiated)
250
251
        if not announced:
252
            log.debug('announced NLRI none', 'routes')
253
254
        # Is the peer going to send us some Path Information with the route (AddPath)
255
        addpath = negotiated.addpath.receive(AFI.ipv4, SAFI.unicast)
256
257
        # empty string for NoNextHop, the packed IP otherwise (without the 3/4 bytes of attributes headers)
258
        nexthop = attributes.get(Attribute.CODE.NEXT_HOP, NoNextHop)
259
        # nexthop = NextHop.unpack(_nexthop.ton())
260
261
        # XXX: NEXTHOP MUST NOT be the IP address of the receiving speaker.
262
263
        nlris = []
264
        while withdrawn:
265
            nlri, left = NLRI.unpack_nlri(AFI.ipv4, SAFI.unicast, withdrawn, IN.WITHDRAWN, addpath)
266
            log.debug('withdrawn NLRI %s' % nlri, 'routes')
267
            withdrawn = left
268
            nlris.append(nlri)
269
270
        while announced:
271
            nlri, left = NLRI.unpack_nlri(AFI.ipv4, SAFI.unicast, announced, IN.ANNOUNCED, addpath)
272
            nlri.nexthop = nexthop
273
            log.debug('announced NLRI %s' % nlri, 'routes')
274
            announced = left
275
            nlris.append(nlri)
276
277
        unreach = attributes.pop(MPURNLRI.ID, None)
278
        reach = attributes.pop(MPRNLRI.ID, None)
279
280
        if unreach is not None:
281
            nlris.extend(unreach.nlris)
282
283
        if reach is not None:
284
            nlris.extend(reach.nlris)
285
286
        if not attributes and not nlris:
287
            # Careful do not use == or != as the comparaison does not work
288
            if unreach is None and reach is None:
289
                return EOR(AFI.ipv4, SAFI.unicast)
290
            if unreach is not None:
291
                return EOR(unreach.afi, unreach.safi)
292
            if reach is not None:
293
                return EOR(reach.afi, reach.safi)
294
            raise RuntimeError('This was not expected')
295
296
        update = Update(nlris, attributes)
297
298
        def parsed(_):
299
            # we need the import in the function as otherwise we have an cyclic loop
300
            # as this function currently uses Update..
301
            from exabgp.reactor.api.response import Response
302
            from exabgp.version import json as json_version
303
304
            return 'json %s' % Response.JSON(json_version).update(negotiated.neighbor, 'in', update, None, '', '')
305
306
        logfunc.debug(lazyformat('decoded UPDATE', '', parsed), 'parser')
307
308
        return update
309