Completed
Pull Request — master (#968)
by
unknown
13:00
created

exabgp.bgp.message.update   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 303
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 181
dl 0
loc 303
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 115 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.util import character
14
from exabgp.util import concat_bytes
15
16
from exabgp.protocol.ip import NoNextHop
17
from exabgp.protocol.family import AFI
18
from exabgp.protocol.family import SAFI
19
20
from exabgp.bgp.message.direction import IN
21
from exabgp.bgp.message.direction import OUT
22
from exabgp.bgp.message.message import Message
23
from exabgp.bgp.message.update.eor import EOR
24
25
from exabgp.bgp.message.update.attribute import Attributes
26
from exabgp.bgp.message.update.attribute import Attribute
27
from exabgp.bgp.message.update.attribute import MPRNLRI
28
from exabgp.bgp.message.update.attribute import EMPTY_MPRNLRI
29
from exabgp.bgp.message.update.attribute import MPURNLRI
30
from exabgp.bgp.message.update.attribute import EMPTY_MPURNLRI
31
32
from exabgp.bgp.message.notification import Notify
33
from exabgp.bgp.message.update.nlri import NLRI
34
35
from exabgp.logger import Logger
36
from exabgp.logger import LazyFormat
37
38
# ======================================================================= Update
39
40
# +-----------------------------------------------------+
41
# |   Withdrawn Routes Length (2 octets)                |
42
# +-----------------------------------------------------+
43
# |   Withdrawn Routes (variable)                       |
44
# +-----------------------------------------------------+
45
# |   Total Path Attribute Length (2 octets)            |
46
# +-----------------------------------------------------+
47
# |   Path Attributes (variable)                        |
48
# +-----------------------------------------------------+
49
# |   Network Layer Reachability Information (variable) |
50
# +-----------------------------------------------------+
51
52
# Withdrawn Routes:
53
54
# +---------------------------+
55
# |   Length (1 octet)        |
56
# +---------------------------+
57
# |   Prefix (variable)       |
58
# +---------------------------+
59
60
61
@Message.register
62
class Update (Message):
63
	ID = Message.CODE.UPDATE
64
	TYPE = character(Message.CODE.UPDATE)
65
	EOR = False
66
67
	def __init__ (self, nlris, attributes):
68
		self.nlris = nlris
69
		self.attributes = attributes
70
71
	# message not implemented we should use messages below.
72
73
	def __str__ (self):
74
		return '\n'.join(['%s%s' % (str(self.nlris[n]),str(self.attributes)) for n in range(len(self.nlris))])
75
76
	@staticmethod
77
	def prefix (data):
78
		# This function needs renaming
79
		return concat_bytes(pack('!H',len(data)),data)
80
81
	@staticmethod
82
	def split (data):
83
		length = len(data)
84
85
		len_withdrawn = unpack('!H',data[0:2])[0]
86
		withdrawn = data[2:len_withdrawn+2]
87
88
		if len(withdrawn) != len_withdrawn:
89
			raise Notify(3,1,'invalid withdrawn routes length, not enough data available')
90
91
		start_attributes = len_withdrawn+4
92
		len_attributes = unpack('!H',data[len_withdrawn+2:start_attributes])[0]
93
		start_announced = len_withdrawn+len_attributes+4
94
		attributes = data[start_attributes:start_announced]
95
		announced = data[start_announced:]
96
97
		if len(attributes) != len_attributes:
98
			raise Notify(3,1,'invalid total path attribute length, not enough data available')
99
100
		if 2 + len_withdrawn + 2 + len_attributes + len(announced) != length:
101
			raise Notify(3,1,'error in BGP message length, not enough data for the size announced')
102
103
		return withdrawn,attributes,announced
104
105
	# The routes MUST have the same attributes ...
106
	# XXX: FIXME: calculate size progressively to not have to do it every time
107
	# XXX: FIXME: we could as well track when packed_del, packed_mp_del, etc
108
	# XXX: FIXME: are emptied and therefore when we can save calculations
109
	def messages (self, negotiated, include_withdraw=True):
110
		# sort the nlris
111
112
		nlris = []
113
		mp_nlris = {}
114
115
		for nlri in sorted(self.nlris):
116
			if nlri.family() in negotiated.families:
117
				if nlri.afi == AFI.ipv4 and nlri.safi in [SAFI.unicast, SAFI.multicast] and nlri.nexthop.afi == AFI.ipv4:
118
					nlris.append(nlri)
119
				else:
120
					mp_nlris.setdefault(nlri.family(), {}).setdefault(nlri.action, []).append(nlri)
121
122
		if not nlris and not mp_nlris:
123
			return
124
125
		# If all we have is MP_UNREACH_NLRI, we do not need the default
126
		# attributes. See RFC4760 that states the following:
127
		#
128
		#	An UPDATE message that contains the MP_UNREACH_NLRI is not required
129
		#	to carry any other path attributes.
130
		#
131
		include_defaults = True
132
133
		if mp_nlris and not nlris:
134
135
			for family, actions in mp_nlris.items():
136
				afi, safi = family
137
138
				if safi not in (SAFI.unicast, SAFI.multicast):
139
					break
140
141
				if set(actions.keys()) != {OUT.WITHDRAW}:
142
					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
			Logger().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
			Logger().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
				Logger().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(negotiated, msg_size - len(withdraws + announced + mp_reach)):
214
					if mp_unreach:
215
						yield self._message(Update.prefix(withdraws) + Update.prefix(attr + mp_unreach + mp_reach) + announced)
216
						mp_reach = b''
217
						announced = b''
218
						withdraws = b''
219
					mp_unreach = mpurnlri
220
221
			yield self._message(Update.prefix(withdraws) + Update.prefix(attr + mp_unreach + mp_reach) + announced)  # yield mpr/mpur per family
222
			withdraws = b''
223
			announced = b''
224
225
	# XXX: FIXME: this can raise ValueError. IndexError,TypeError, struct.error (unpack) = check it is well intercepted
226
	@classmethod
227
	def unpack_message (cls, data, negotiated):
228
		logger = Logger()
229
230
		logger.debug(LazyFormat('parsing UPDATE',data),'parser')
231
232
		length = len(data)
233
234
		# This could be speed up massively by changing the order of the IF
235
		if length == 4 and data == b'\x00\x00\x00\x00':
236
			return EOR(AFI.ipv4,SAFI.unicast)  # pylint: disable=E1101
237
		if length == 11 and data.startswith(EOR.NLRI.PREFIX):
238
			return EOR.unpack_message(data,negotiated)
239
240
		withdrawn, _attributes, announced = cls.split(data)
241
242
		if not withdrawn:
243
			logger.debug('withdrawn NLRI none','routes')
244
245
		attributes = Attributes.unpack(_attributes,negotiated)
246
247
		if not announced:
248
			logger.debug('announced NLRI none','routes')
249
250
		# Is the peer going to send us some Path Information with the route (AddPath)
251
		addpath = negotiated.addpath.receive(AFI.ipv4,SAFI.unicast)
252
253
		# empty string for NoNextHop, the packed IP otherwise (without the 3/4 bytes of attributes headers)
254
		nexthop = attributes.get(Attribute.CODE.NEXT_HOP,NoNextHop)
255
		# nexthop = NextHop.unpack(_nexthop.ton())
256
257
		# XXX: NEXTHOP MUST NOT be the IP address of the receiving speaker.
258
259
		nlris = []
260
		while withdrawn:
261
			nlri,left = NLRI.unpack_nlri(AFI.ipv4,SAFI.unicast,withdrawn,IN.WITHDRAWN,addpath)
262
			logger.debug('withdrawn NLRI %s' % nlri,'routes')
263
			withdrawn = left
264
			nlris.append(nlri)
265
266
		while announced:
267
			nlri,left = NLRI.unpack_nlri(AFI.ipv4,SAFI.unicast,announced,IN.ANNOUNCED,addpath)
268
			nlri.nexthop = nexthop
269
			logger.debug('announced NLRI %s' % nlri,'routes')
270
			announced = left
271
			nlris.append(nlri)
272
273
		unreach = attributes.pop(MPURNLRI.ID,None)
274
		reach = attributes.pop(MPRNLRI.ID,None)
275
276
		if unreach is not None:
277
			nlris.extend(unreach.nlris)
278
279
		if reach is not None:
280
			nlris.extend(reach.nlris)
281
282
		if not attributes and not nlris:
283
			# Careful do not use == or != as the comparaison does not work
284
			if unreach is None and reach is None:
285
				return EOR(AFI.ipv4,SAFI.unicast)
286
			if unreach is not None:
287
				return EOR(unreach.afi,unreach.safi)
288
			if reach is not None:
289
				return EOR(reach.afi,reach.safi)
290
			raise RuntimeError('This was not expected')
291
292
		update = Update(nlris, attributes)
293
294
		def parsed (_):
295
			# we need the import in the function as otherwise we have an cyclic loop
296
			# as this function currently uses Update..
297
			from exabgp.reactor.api.response import Response
298
			from exabgp.version import json as json_version
299
			return 'json %s' % Response.JSON(json_version).update(negotiated.neighbor, 'in', update, None, '', '')
300
		logger.debug(LazyFormat('decoded UPDATE', '', parsed), 'parser')
301
302
		return update
303