Completed
Push — master ( 81e1b1...1458cc )
by Thomas
11:22
created

exabgp.bgp.message.update   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 308
Duplicated Lines 0 %

Importance

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