Attributes.has()   A
last analyzed

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
attributes.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 unpack
11
12
from exabgp.environment import getenv
13
14
from exabgp.bgp.message.update.attribute.attribute import Attribute
15
from exabgp.bgp.message.update.attribute.attribute import TreatAsWithdraw
16
from exabgp.bgp.message.update.attribute.attribute import Discard
17
from exabgp.bgp.message.update.attribute.generic import GenericAttribute
18
from exabgp.bgp.message.update.attribute.origin import Origin
19
from exabgp.bgp.message.update.attribute.aspath import ASPath
20
from exabgp.bgp.message.update.attribute.localpref import LocalPreference
21
22
# For bagpipe
23
from exabgp.bgp.message.update.attribute.community import Communities
24
25
from exabgp.bgp.message.notification import Notify
26
27
from exabgp.logger import log
28
from exabgp.logger import logfunc
29
from exabgp.logger import lazyattribute
30
31
32
class _NOTHING(object):
33
    def pack(self, _=None):
34
        return b''
35
36
37
NOTHING = _NOTHING()
38
39
40
# =================================================================== Attributes
41
#
42
43
# 0                   1
44
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6
45
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
46
# |  Attr. Flags  |Attr. Type Code|
47
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
48
49
50
class Attributes(dict):
51
    INTERNAL = (
52
        Attribute.CODE.INTERNAL_SPLIT,
53
        Attribute.CODE.INTERNAL_WATCHDOG,
54
        Attribute.CODE.INTERNAL_NAME,
55
        Attribute.CODE.INTERNAL_WITHDRAW,
56
        # Attribute.CODE.INTERNAL_DISCARD,
57
        # Attribute.CODE.INTERNAL_TREAT_AS_WITHDRAW,
58
    )
59
60
    NO_GENERATION = (Attribute.CODE.NEXT_HOP,) + INTERNAL
61
62
    TREAT_AS_WITHDRAW = (
63
        Attribute.CODE.ORIGIN,
64
        Attribute.CODE.AS_PATH,
65
        Attribute.CODE.NEXT_HOP,
66
        Attribute.CODE.MED,
67
        Attribute.CODE.LOCAL_PREF,
68
        Attribute.CODE.LARGE_COMMUNITY,
69
    )
70
71
    DISCARD = (
72
        Attribute.CODE.ATOMIC_AGGREGATE,
73
        Attribute.CODE.AGGREGATOR,
74
    )
75
76
    MANDATORY = (Attribute.CODE.ORIGIN, Attribute.CODE.AS_PATH, Attribute.CODE.LOCAL_PREF)
77
78
    NO_DUPLICATE = (
79
        Attribute.CODE.MP_REACH_NLRI,
80
        Attribute.CODE.MP_UNREACH_NLRI,
81
    )
82
83
    VALID_ZERO = (
84
        Attribute.CODE.ATOMIC_AGGREGATE,
85
        Attribute.CODE.AS_PATH,
86
    )
87
88
    # A cache of parsed attributes
89
    cache = {}
90
91
    # The previously parsed Attributes
92
    cached = None
93
    # previously parsed attribute, from which cached was made of
94
    previous = ''
95
96
    representation = {
97
        # key:  (how, default, name, text_presentation, json_presentation),
98
        Attribute.CODE.ORIGIN: ('string', '', 'origin', '%s', '%s'),
99
        Attribute.CODE.AS_PATH: (
100
            'multiple',
101
            '',
102
            ('as-path', 'as-set', 'confederation-path', 'confederation-set'),
103
            '%s',
104
            '%s',
105
        ),
106
        Attribute.CODE.NEXT_HOP: ('string', '', 'next-hop', '%s', '%s'),
107
        Attribute.CODE.MED: ('integer', '', 'med', '%s', '%s'),
108
        Attribute.CODE.LOCAL_PREF: ('integer', '', 'local-preference', '%s', '%s'),
109
        Attribute.CODE.ATOMIC_AGGREGATE: ('boolean', '', 'atomic-aggregate', '%s', '%s'),
110
        Attribute.CODE.AGGREGATOR: ('string', '', 'aggregator', '( %s )', '%s'),
111
        Attribute.CODE.AS4_AGGREGATOR: ('string', '', 'aggregator', '( %s )', '%s'),
112
        Attribute.CODE.COMMUNITY: ('list', '', 'community', '%s', '%s'),
113
        Attribute.CODE.LARGE_COMMUNITY: ('list', '', 'large-community', '%s', '%s'),
114
        Attribute.CODE.ORIGINATOR_ID: ('inet', '', 'originator-id', '%s', '%s'),
115
        Attribute.CODE.CLUSTER_LIST: ('list', '', 'cluster-list', '%s', '%s'),
116
        Attribute.CODE.EXTENDED_COMMUNITY: ('list', '', 'extended-community', '%s', '%s'),
117
        Attribute.CODE.PMSI_TUNNEL: ('string', '', 'pmsi', '%s', '%s'),
118
        Attribute.CODE.AIGP: ('integer', '', 'aigp', '%s', '%s'),
119
        Attribute.CODE.BGP_LS: ('list', '', 'bgp-ls', '%s', '%s'),
120
        Attribute.CODE.BGP_PREFIX_SID: ('list', '', 'bgp-prefix-sid', '%s', '%s'),
121
        Attribute.CODE.INTERNAL_NAME: ('string', '', 'name', '%s', '%s'),
122
        Attribute.CODE.INTERNAL_DISCARD: ('string', '', 'error', '%s', '%s'),
123
        Attribute.CODE.INTERNAL_TREAT_AS_WITHDRAW: ('string', '', 'error', '%s', '%s'),
124
    }
125
126
    def _generate_text(self):
127
        for code in sorted(self.keys()):
128
            # XXX: FIXME: really we should have a INTERNAL attribute in the classes
129
            if code in Attributes.NO_GENERATION:
130
                continue
131
132
            attribute = self[code]
133
134
            if code not in self.representation:
135
                yield ' attribute [ 0x%02X 0x%02X %s ]' % (code, attribute.FLAG, str(attribute))
136
                continue
137
138
            if attribute.GENERIC:
139
                yield ' attribute [ 0x%02X 0x%02X %s ]' % (code, attribute.FLAG, str(attribute))
140
                continue
141
142
            how, _, name, presentation, _ = self.representation[code]
143
            if how == 'boolean':
144
                yield ' %s' % name
145
            elif how == 'list':
146
                yield ' %s %s' % (name, presentation % str(attribute))
147
            elif how == 'multiple':
148
                yield ' %s %s' % (name[0], presentation % str(attribute))
149
            else:
150
                yield ' %s %s' % (name, presentation % str(attribute))
151
152
    def _generate_json(self):
153
        for code in sorted(self.keys()):
154
            # remove the next-hop from the attribute as it is define with the NLRI
155
            if code in Attributes.NO_GENERATION:
156
                continue
157
158
            attribute = self[code]
159
160
            if code not in self.representation:
161
                yield '"attribute-0x%02X-0x%02X": "%s"' % (code, attribute.FLAG, str(attribute))
162
                continue
163
164
            how, _, name, _, presentation = self.representation[code]
165
            if how == 'boolean':
166
                yield '"%s": %s' % (name, 'true' if self.has(code) else 'false')
167
            elif how == 'string':
168
                yield '"%s": "%s"' % (name, presentation % str(attribute))
169
            elif how == 'list':
170
                yield '"%s": %s' % (name, presentation % attribute.json())
171
            elif how == 'multiple':
172
                for n in name:
173
                    value = attribute.json(n)
174
                    if value:
175
                        yield '"%s": %s' % (n, presentation % value)
176
            elif how == 'inet':
177
                yield '"%s": "%s"' % (name, presentation % str(attribute))
178
            # Should never be ran
179
            else:
180
                yield '"%s": %s' % (name, presentation % str(attribute))
181
182
    def __init__(self):
183
        dict.__init__(self)
184
        # cached representation of the object
185
        self._str = ''
186
        self._idx = ''
187
        self._json = ''
188
        # The parsed attributes have no mp routes and/or those are last
189
        self.cacheable = True
190
191
        # XXX: FIXME: surely not the best place for this
192
        Attribute.caching = getenv().cache.attributes
193
194
    def has(self, k):
195
        return k in self
196
197
    def add(self, attribute, _=None):
198
        # we return None as attribute if the unpack code must not generate them
199
        if attribute is None:
200
            return
201
        if attribute.ID in self:
202
            return
203
204
        self._str = ''
205
        self._json = ''
206
207
        self[attribute.ID] = attribute
208
209
    # This is as when we generate flow spec we can have multiple keywords
210
    # which are all adding information in the extended-community
211
    def add_and_merge(self, attribute):
212
        if attribute.ID not in self:
213
            self.add(attribute)
214
            return
215
216
        if attribute.ID == Attribute.CODE.EXTENDED_COMMUNITY:
217
            for community in attribute.communities:
218
                self[attribute.ID].add(community)
219
220
    def remove(self, attrid):
221
        self.pop(attrid)
222
223
    def watchdog(self):
224
        return self.pop(Attribute.CODE.INTERNAL_WATCHDOG, None)
225
226
    def withdraw(self):
227
        return self.pop(Attribute.CODE.INTERNAL_WITHDRAW, None) is not None
228
229
    def pack(self, negotiated, with_default=True):
230
        local_asn = negotiated.local_as
231
        peer_asn = negotiated.peer_as
232
233
        message = b''
234
235
        default = {
236
            Attribute.CODE.ORIGIN: lambda left, right: Origin(Origin.IGP),
237
            Attribute.CODE.AS_PATH: lambda left, right: ASPath([], []) if left == right else ASPath([local_asn,], []),
238
            Attribute.CODE.LOCAL_PREF: lambda left, right: LocalPreference(100) if left == right else NOTHING,
239
        }
240
241
        skip = {
242
            Attribute.CODE.NEXT_HOP: lambda left, right, nh: nh.ipv4() is not True,
243
            Attribute.CODE.LOCAL_PREF: lambda left, right, nh: left != right,
244
        }
245
246
        keys = list(self)
247
        alls = set(keys + list(default) if with_default else [])
248
249
        for code in sorted(alls):
250
            if code in Attributes.INTERNAL:
251
                continue
252
253
            if code not in keys and code in default:
254
                message += default[code](local_asn, peer_asn).pack(negotiated)
255
                continue
256
257
            attribute = self[code]
258
259
            if code in skip and skip[code](local_asn, peer_asn, attribute):
260
                continue
261
262
            message += attribute.pack(negotiated)
263
264
        return message
265
266
    def json(self):
267
        if not self._json:
268
            self._json = ', '.join(self._generate_json())
269
        return self._json
270
271
    def __repr__(self):
272
        if not self._str:
273
            self._str = ''.join(self._generate_text())
274
        return self._str
275
276
    def index(self):
277
        # XXX: something a little bit smaller memory wise ?
278
        if not self._idx:
279
            idx = ''.join(self._generate_text())
280
            nexthop = str(self.get(Attribute.CODE.NEXT_HOP, ''))
281
            self._idx = '%s next-hop %s' % (idx, nexthop) if nexthop else idx
282
        return self._idx
283
284
    @classmethod
285
    def unpack(cls, data, negotiated):
286
        if cls.cached and data == cls.previous:
287
            return cls.cached
288
289
        attributes = cls().parse(data, negotiated)
290
291
        if Attribute.CODE.INTERNAL_TREAT_AS_WITHDRAW in attributes:
292
            return attributes
293
294
        if Attribute.CODE.AS_PATH in attributes and Attribute.CODE.AS4_PATH in attributes:
295
            attributes.merge_attributes()
296
297
        if Attribute.CODE.MP_REACH_NLRI not in attributes and Attribute.CODE.MP_UNREACH_NLRI not in attributes:
298
            cls.previous = data
299
            cls.cached = attributes
300
        else:
301
            cls.previous = ''
302
            cls.cached = None
303
304
        return attributes
305
306
    @staticmethod
307
    def flag_attribute_content(data):
308
        flag = Attribute.Flag(data[0])
309
        attr = Attribute.CODE(data[1])
310
311
        if flag & Attribute.Flag.EXTENDED_LENGTH:
312
            length = unpack('!H', data[2:4])[0]
313
            return flag, attr, data[4 : length + 4]
314
        else:
315
            length = data[2]
316
            return flag, attr, data[3 : length + 3]
317
318
    def parse(self, data, negotiated):
319
        if not data:
320
            return self
321
322
        try:
323
            # We do not care if the attribute are transitive or not as we do not redistribute
324
            flag = Attribute.Flag(data[0])
325
            aid = Attribute.CODE(data[1])
326
        except IndexError:
327
            self.add(TreatAsWithdraw())
328
            return self
329
330
        try:
331
            offset = 3
332
            length = data[2]
333
334
            if flag & Attribute.Flag.EXTENDED_LENGTH:
335
                offset = 4
336
                length = (length << 8) + data[3]
337
        except IndexError:
338
            self.add(TreatAsWithdraw(aid))
339
            return self
340
341
        data = data[offset:]
342
        left = data[length:]
343
        attribute = data[:length]
344
345
        logfunc.debug(lazyattribute(flag, aid, length, data[:length]), 'parser')
346
347
        # remove the PARTIAL bit before comparaison if the attribute is optional
348
        if aid in Attribute.attributes_optional:
349
            flag &= Attribute.Flag.MASK_PARTIAL & 0xFF
350
            # flag &= ~Attribute.Flag.PARTIAL & 0xFF  # cleaner than above (python use signed integer for ~)
351
352
        if aid in self:
353
            if aid in self.NO_DUPLICATE:
354
                raise Notify(3, 1, 'multiple attribute for %s' % str(Attribute.CODE(attribute.ID)))
355
356
            log.debug(
357
                'duplicate attribute %s (flag 0x%02X, aid 0x%02X) skipping'
358
                % (Attribute.CODE.names.get(aid, 'unset'), flag, aid),
359
                'parser',
360
            )
361
            return self.parse(left, negotiated)
362
363
        # handle the attribute if we know it
364
        if Attribute.registered(aid, flag):
365
            if length == 0 and aid not in self.VALID_ZERO:
366
                self.add(TreatAsWithdraw(aid))
367
                return self.parse(left, negotiated)
368
369
            try:
370
                decoded = Attribute.unpack(aid, flag, attribute, negotiated)
371
            except IndexError as exc:
372
                if aid in self.TREAT_AS_WITHDRAW:
373
                    decoded = TreatAsWithdraw(aid)
374
                else:
375
                    raise exc
376
            except Notify as exc:
377
                if aid in self.TREAT_AS_WITHDRAW:
378
                    decoded = TreatAsWithdraw()
379
                elif aid in self.DISCARD:
380
                    decoded = Discard()
381
                else:
382
                    raise exc
383
            self.add(decoded)
384
            return self.parse(left, negotiated)
385
386
        # XXX: FIXME: we could use a fallback function here like capability
387
388
        # if we know the attribute but the flag is not what the RFC says.
389
        if aid in Attribute.attributes_known:
390
            if aid in self.TREAT_AS_WITHDRAW:
391
                log.debug(
392
                    'invalid flag for attribute %s (flag 0x%02X, aid 0x%02X) treat as withdraw'
393
                    % (Attribute.CODE.names.get(aid, 'unset'), flag, aid),
394
                    'parser',
395
                )
396
                self.add(TreatAsWithdraw())
397
            if aid in self.DISCARD:
398
                log.debug(
399
                    'invalid flag for attribute %s (flag 0x%02X, aid 0x%02X) discard'
400
                    % (Attribute.CODE.names.get(aid, 'unset'), flag, aid),
401
                    'parser',
402
                )
403
                return self.parse(left, negotiated)
404
            # XXX: Check if we are missing any
405
            log.debug(
406
                'invalid flag for attribute %s (flag 0x%02X, aid 0x%02X) unspecified (should not happen)'
407
                % (Attribute.CODE.names.get(aid, 'unset'), flag, aid),
408
                'parser',
409
            )
410
            return self.parse(left, negotiated)
411
412
        # it is an unknown transitive attribute we need to pass on
413
        if flag & Attribute.Flag.TRANSITIVE:
414
            log.debug('unknown transitive attribute (flag 0x%02X, aid 0x%02X)' % (flag, aid), 'parser')
415
            try:
416
                decoded = GenericAttribute(aid, flag | Attribute.Flag.PARTIAL, attribute)
417
            except IndexError:
418
                decoded = TreatAsWithdraw(aid)
419
            self.add(decoded, attribute)
420
            return self.parse(left, negotiated)
421
422
        # it is an unknown non-transitive attribute we can ignore.
423
        log.debug('ignoring unknown non-transitive attribute (flag 0x%02X, aid 0x%02X)' % (flag, aid), 'parser')
424
        return self.parse(left, negotiated)
425
426
    def merge_attributes(self):
427
        as2path = self[Attribute.CODE.AS_PATH]
428
        as4path = self[Attribute.CODE.AS4_PATH]
429
        self.remove(Attribute.CODE.AS_PATH)
430
        self.remove(Attribute.CODE.AS4_PATH)
431
432
        # this key is unique as index length is a two header, plus a number of ASN of size 2 or 4
433
        # so adding the: make the length odd and unique
434
        key = "%s:%s" % (as2path.index, as4path.index)
435
436
        # found a cache copy
437
        cached = Attribute.cache.get(Attribute.CODE.AS_PATH, {}).get(key, None)
438
        if cached:
439
            self.add(cached, key)
440
            return
441
442
        # as_seq = []
443
        # as_set = []
444
445
        len2 = len(as2path.as_seq)
446
        len4 = len(as4path.as_seq)
447
448
        # RFC 4893 section 4.2.3
449
        if len2 < len4:
450
            as_seq = as2path.as_seq
451
        else:
452
            as_seq = as2path.as_seq[:-len4]
453
            as_seq.extend(as4path.as_seq)
454
455
        len2 = len(as2path.as_set)
456
        len4 = len(as4path.as_set)
457
458
        if len2 < len4:
459
            as_set = as4path.as_set
460
        else:
461
            as_set = as2path.as_set[:-len4]
462
            as_set.extend(as4path.as_set)
463
464
        aspath = ASPath(as_seq, as_set)
465
        self.add(aspath, key)
466
467
    def __hash__(self):
468
        # FIXME: two routes with distinct nh but other attributes equal
469
        # will hash to the same value until repr represents the nh (??)
470
        return hash(repr(self))
471
472
    def __eq__(self, other):
473
        return self.sameValuesAs(other)
474
475
    # BaGPipe code ..
476
477
    # test that sets of attributes exactly match
478
    # can't rely on __eq__ for this, because __eq__ relies on Attribute.__eq__ which does not look at attributes values
479
480
    def sameValuesAs(self, other):
481
        # we sort based on packed values since the items do not
482
        # necessarily implement __cmp__
483
        def pack_(x):
484
            return x.pack()
485
486
        try:
487
            for key in set(self.keys()).union(set(other.keys())):
488
                if key == Attribute.CODE.MP_REACH_NLRI or key == Attribute.CODE.MP_UNREACH_NLRI:
489
                    continue
490
491
                sval = self[key]
492
                oval = other[key]
493
494
                # In the case where the attribute is Communities or
495
                # extended communities, we want to compare values independently of their order
496
                if isinstance(sval, Communities):
497
                    if not isinstance(oval, Communities):
498
                        return False
499
500
                    sval = sorted(sval, key=pack_)
501
                    oval = sorted(oval, key=pack_)
502
503
                if sval != oval:
504
                    return False
505
            return True
506
        except KeyError:
507
            return False
508