address.py 18.9 KB
Newer Older
Dmitry Volodin's avatar
Dmitry Volodin committed
1
2
3
# ----------------------------------------------------------------------
# Address check
# ----------------------------------------------------------------------
Dmitry Volodin's avatar
Dmitry Volodin committed
4
# Copyright (C) 2007-2020 The NOC Project
Dmitry Volodin's avatar
Dmitry Volodin committed
5
6
7
8
9
# See LICENSE for details
# ----------------------------------------------------------------------

# Python modules
from collections import namedtuple, defaultdict
Dmitry Volodin's avatar
Dmitry Volodin committed
10

Dmitry Volodin's avatar
Dmitry Volodin committed
11
12
13
# NOC modules
from noc.services.discovery.jobs.base import DiscoveryCheck
from noc.ip.models.vrf import VRF
14
from noc.ip.models.prefix import Prefix
Dmitry Volodin's avatar
Dmitry Volodin committed
15
16
17
from noc.ip.models.address import Address
from noc.core.perf import metrics
from noc.core.handler import get_handler
kk's avatar
kk committed
18
from noc.core.validators import is_fqdn
19
from noc.core.ip import IP, PrefixDB
Dmitry Volodin's avatar
Dmitry Volodin committed
20
21


Dmitry Volodin's avatar
Dmitry Volodin committed
22
23
24
25
DiscoveredAddress = namedtuple(
    "DiscoveredAddress",
    ["vpn_id", "address", "profile", "description", "source", "subinterface", "mac", "fqdn"],
)
Dmitry Volodin's avatar
Dmitry Volodin committed
26
27
28
29
30
31
32
33

GLOBAL_VRF = "0:0"
SRC_MANAGEMENT = "m"
SRC_INTERFACE = "i"
SRC_DHCP = "d"
SRC_NEIGHBOR = "n"
SRC_MANUAL = "M"

Dmitry Volodin's avatar
Dmitry Volodin committed
34
PREF_VALUE = {SRC_NEIGHBOR: 0, SRC_DHCP: 1, SRC_MANAGEMENT: 2, SRC_INTERFACE: 3, SRC_MANUAL: 4}
Dmitry Volodin's avatar
Dmitry Volodin committed
35
36
37
38
39
40
41
42

LOCAL_SRC = {SRC_MANAGEMENT, SRC_INTERFACE}


class AddressCheck(DiscoveryCheck):
    name = "address"

    def handler(self):
43
        self.propagated_prefixes = set()
Dmitry Volodin's avatar
Dmitry Volodin committed
44
45
46
47
48
49
        addresses = self.get_addresses()
        self.sync_addresses(addresses)

    def get_addresses(self):
        """
        Discover addresses
50
        :return: dict of (vpn_id, address) => DiscoveredAddress
Dmitry Volodin's avatar
Dmitry Volodin committed
51
        """
52
        # vpn_id, address => DiscoveredAddress
Dmitry Volodin's avatar
Dmitry Volodin committed
53
54
55
        addresses = {}
        # Apply interface addresses
        if self.object.object_profile.enable_box_discovery_address_interface:
Dmitry Volodin's avatar
Dmitry Volodin committed
56
            addresses = self.apply_addresses(addresses, self.get_interface_addresses())
Dmitry Volodin's avatar
Dmitry Volodin committed
57
58
        # Apply management addresses
        if self.object.object_profile.enable_box_discovery_address_management:
Dmitry Volodin's avatar
Dmitry Volodin committed
59
            addresses = self.apply_addresses(addresses, self.get_management_addresses())
Dmitry Volodin's avatar
Dmitry Volodin committed
60
61
        # Apply DHCP leases
        if self.object.object_profile.enable_box_discovery_address_dhcp:
Dmitry Volodin's avatar
Dmitry Volodin committed
62
            addresses = self.apply_addresses(addresses, self.get_dhcp_addresses())
Dmitry Volodin's avatar
Dmitry Volodin committed
63
        # Apply neighbor addresses
64
        if self.object.object_profile.enable_box_discovery_address_neighbor:
Dmitry Volodin's avatar
Dmitry Volodin committed
65
            addresses = self.apply_addresses(addresses, self.get_neighbor_addresses())
Dmitry Volodin's avatar
Dmitry Volodin committed
66
67
68
69
70
71
72
73
        return addresses

    def sync_addresses(self, addresses):
        """
        Apply addresses to database
        :param addresses:
        :return:
        """
74
        # vpn_id -> [address, ]
Dmitry Volodin's avatar
Dmitry Volodin committed
75
        vrf_addresses = defaultdict(list)
76
77
78
        for vpn_id, a in addresses:
            vrf_addresses[vpn_id] += [a]
        # build vpn_id -> VRF mapping
Dmitry Volodin's avatar
Dmitry Volodin committed
79
80
        self.logger.debug("Building VRF map")
        vrfs = {}
81
82
        for vpn_id in vrf_addresses:
            vrf = VRF.get_by_vpn_id(vpn_id)
Dmitry Volodin's avatar
Dmitry Volodin committed
83
            if vrf:
84
85
86
                vrfs[vpn_id] = vrf
        missed_vpn_id = set(vrf_addresses) - set(vrfs)
        if missed_vpn_id:
Dmitry Volodin's avatar
Dmitry Volodin committed
87
88
89
            self.logger.info(
                "VPN ID are missed in VRF database and to be ignored: %s", ", ".join(missed_vpn_id)
            )
Dmitry Volodin's avatar
Dmitry Volodin committed
90
91
        #
        self.logger.debug("Getting addresses to synchronize")
92
93
        for vpn_id in vrfs:
            vrf = vrfs[vpn_id]
Dmitry Volodin's avatar
Dmitry Volodin committed
94
            seen = set()
95
            for a in Address.objects.filter(vrf=vrf, address__in=vrf_addresses[vpn_id]):
96
                norm_address = IP.expand(a.address)
Dmitry Volodin's avatar
Dmitry Volodin committed
97
                # Confirmed address, apply changes and touch
98
                address = addresses[vpn_id, norm_address]
Dmitry Volodin's avatar
Dmitry Volodin committed
99
                self.apply_address_changes(a, address)
100
                seen.add(norm_address)
101
            for a in set(vrf_addresses[vpn_id]) - seen:
Dmitry Volodin's avatar
Dmitry Volodin committed
102
                # New address, create
103
                self.create_address(addresses[vpn_id, a])
Dmitry Volodin's avatar
Dmitry Volodin committed
104
105
106
        # Detaching hanging addresses
        self.logger.debug("Checking for hanging addresses")
        for a in Address.objects.filter(managed_object=self.object):
107
108
            norm_address = IP.expand(a.address)
            address = addresses.get((a.vrf.vpn_id, norm_address))
109
            if not address or address.source not in LOCAL_SRC:
Dmitry Volodin's avatar
Dmitry Volodin committed
110
111
112
113
114
115
116
117
                self.logger.info("Detaching %s:%s", a.vrf.name, a.address)
                a.managed_object = None
                a.save()

    @staticmethod
    def apply_addresses(addresses, discovered_addresses):
        """
        Apply list of discovered addresses to addresses dict
118
        :param addresses: dict of (vpn_id, address) => DiscoveredAddress
Dmitry Volodin's avatar
Dmitry Volodin committed
119
120
121
122
        :param discovered_addresses: List of [DiscoveredAddress]
        :returns: Resulted addresses
        """
        for address in discovered_addresses:
123
124
            norm_address = IP.expand(address.address)
            old = addresses.get((address.vpn_id, norm_address))
Dmitry Volodin's avatar
Dmitry Volodin committed
125
126
127
            if old:
                if AddressCheck.is_preferred(old.source, address.source):
                    # New address is preferable, replace
128
                    addresses[address.vpn_id, norm_address] = address
Dmitry Volodin's avatar
Dmitry Volodin committed
129
130
            else:
                # Not seen yet
131
                addresses[address.vpn_id, norm_address] = address
Dmitry Volodin's avatar
Dmitry Volodin committed
132
133
134
        return addresses

    def is_enabled(self):
Dmitry Volodin's avatar
Dmitry Volodin committed
135
        enabled = super().is_enabled()
Dmitry Volodin's avatar
Dmitry Volodin committed
136
137
        if not enabled:
            return False
138
        return self.is_enabled_for_object(self.object)
Dmitry Volodin's avatar
Dmitry Volodin committed
139
140
141
142
143
144
145
146

    def get_interface_addresses(self):
        """
        Get addresses from interface discovery artifact
        :return:
        """
        self.logger.debug("Getting interface addresses")
        if not self.object.object_profile.address_profile_interface:
Dmitry Volodin's avatar
Dmitry Volodin committed
147
148
149
            self.logger.info(
                "Default interface address profile is not set. Skipping interface address discovery"
            )
Dmitry Volodin's avatar
Dmitry Volodin committed
150
151
152
153
154
155
156
            return []
        addresses = self.get_artefact("interface_prefix")
        if not addresses:
            self.logger.info("No interface_prefix artefact, skipping interface addresses")
            return []
        return [
            DiscoveredAddress(
Dmitry Volodin's avatar
Fix    
Dmitry Volodin committed
157
                vpn_id=a.get("vpn_id", GLOBAL_VRF) or GLOBAL_VRF,
Dmitry Volodin's avatar
Dmitry Volodin committed
158
159
160
161
162
                address=a["address"].rsplit("/", 1)[0],
                profile=self.object.object_profile.address_profile_interface,
                source=SRC_INTERFACE,
                description=a["description"],
                subinterface=a["subinterface"],
163
                mac=a["mac"],
Dmitry Volodin's avatar
Dmitry Volodin committed
164
165
166
                fqdn=None,
            )
            for a in addresses
Dmitry Volodin's avatar
Dmitry Volodin committed
167
168
169
170
171
172
173
174
        ]

    def get_management_addresses(self):
        """
        Get addresses from ManagedObject management
        :return:
        """
        if not self.object.object_profile.address_profile_management:
Dmitry Volodin's avatar
Dmitry Volodin committed
175
176
177
            self.logger.info(
                "Default management address profile is not set. Skipping interface address discovery"
            )
Dmitry Volodin's avatar
Dmitry Volodin committed
178
179
180
181
182
183
            return []
        self.logger.debug("Getting management addresses")
        addresses = []
        if self.object.address:
            addresses = [
                DiscoveredAddress(
184
                    vpn_id=self.object.vrf.vpn_id if self.object.vrf else GLOBAL_VRF,
Dmitry Volodin's avatar
Dmitry Volodin committed
185
186
187
188
189
                    address=self.object.address,
                    profile=self.object.object_profile.address_profile_management,
                    source=SRC_MANAGEMENT,
                    description="Management address",
                    subinterface=None,
190
                    mac=None,
Dmitry Volodin's avatar
Dmitry Volodin committed
191
                    fqdn=self.object.get_full_fqdn(),
Dmitry Volodin's avatar
Dmitry Volodin committed
192
193
194
195
196
197
198
199
200
                )
            ]
        return addresses

    def get_dhcp_addresses(self):
        """
        Return addresses from DHCP leases
        :return:
        """
Dmitry Volodin's avatar
Dmitry Volodin committed
201

202
203
        def get_vpn_id(ip):
            try:
204
                return vpn_db[IP.prefix(ip)]
205
206
207
208
209
210
            except KeyError:
                pass
            if self.object.vrf:
                return self.object.vrf.vpn_id
            return GLOBAL_VRF

211
        if not self.object.object_profile.address_profile_dhcp:
Dmitry Volodin's avatar
Dmitry Volodin committed
212
213
214
            self.logger.info(
                "Default DHCP address profile is not set. Skipping DHCP address discovery"
            )
215
            return []
Dmitry Volodin's avatar
Dmitry Volodin committed
216
217
218
219
        # @todo: Check DHCP server capability
        if "get_dhcp_binding" not in self.object.scripts:
            self.logger.info("No get_dhcp_binding script, skipping neighbor discovery")
            return []
220
221
222
223
224
225
226
        # Build network -> vpn mappings
        addresses = self.get_artefact("interface_prefix")
        if not addresses:
            self.logger.info("No interface_prefix artefact, skipping interface addresses")
            return []
        vpn_db = PrefixDB()
        for a in addresses:
227
            vpn_db[IP.prefix(a["address"]).first] = a["vpn_id"]
228
        #
Dmitry Volodin's avatar
Dmitry Volodin committed
229
230
231
232
        self.logger.debug("Getting DHCP addresses")
        leases = self.object.scripts.get_dhcp_binding()
        r = [
            DiscoveredAddress(
233
                vpn_id=get_vpn_id(a["ip"]),
Dmitry Volodin's avatar
Dmitry Volodin committed
234
235
236
237
238
                address=a["ip"],
                profile=self.object.object_profile.address_profile_dhcp,
                source=SRC_DHCP,
                description=None,
                subinterface=None,
239
                mac=a.get("mac"),
Dmitry Volodin's avatar
Dmitry Volodin committed
240
241
242
                fqdn=None,
            )
            for a in leases
Dmitry Volodin's avatar
Dmitry Volodin committed
243
244
245
246
247
248
249
250
        ]
        return r

    def get_neighbor_addresses(self):
        """
        Return addresses from ARP/IPv6 ND
        :return:
        """
251
        if not self.object.object_profile.address_profile_neighbor:
Dmitry Volodin's avatar
Dmitry Volodin committed
252
253
254
            self.logger.info(
                "Default neighbor address profile is not set. Skipping neighbor address discovery"
            )
255
            return []
Dmitry Volodin's avatar
Dmitry Volodin committed
256
257
258
259
260
261
        if "get_ip_discovery" not in self.object.scripts:
            self.logger.info("No get_ip_discovery script, skipping neighbor discovery")
            return []
        self.logger.debug("Getting neighbor addresses")
        neighbors = self.object.scripts.get_ip_discovery()
        r = []
262
263
        for vpn in neighbors:
            for a in vpn["addresses"]:
Dmitry Volodin's avatar
Dmitry Volodin committed
264
265
                r += [
                    DiscoveredAddress(
266
                        vpn_id=vpn.get("vpn_id", GLOBAL_VRF) or GLOBAL_VRF,
Dmitry Volodin's avatar
Dmitry Volodin committed
267
                        address=a["ip"],
268
                        profile=self.object.object_profile.address_profile_neighbor,
Dmitry Volodin's avatar
Dmitry Volodin committed
269
270
271
                        source=SRC_NEIGHBOR,
                        description=None,
                        subinterface=None,
272
                        mac=a.get("mac"),
Dmitry Volodin's avatar
Dmitry Volodin committed
273
                        fqdn=None,
Dmitry Volodin's avatar
Dmitry Volodin committed
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
                    )
                ]
        return r

    @staticmethod
    def is_preferred(old_method, new_method):
        """
        Check which method is preferable

        Preference order: interface, management, neighbor
        :param old_method:
        :param new_method:
        :return:
        """
        return PREF_VALUE[old_method] <= PREF_VALUE[new_method]

    def create_address(self, address):
        """
        Create new address
        :param address: DiscoveredAddress instance
        :return:
        """
296
297
        if self.is_ignored_address(address):
            return
298
        vrf = VRF.get_by_vpn_id(address.vpn_id)
299
300
        self.ensure_afi(vrf, address)
        if not self.has_address_permission(vrf, address):
Dmitry Volodin's avatar
Dmitry Volodin committed
301
            self.logger.debug(
302
                "Do not creating vpn_id=%s address=%s: Disabled by policy",
Dmitry Volodin's avatar
Dmitry Volodin committed
303
304
                address.vpn_id,
                address.address,
Dmitry Volodin's avatar
Dmitry Volodin committed
305
306
307
308
            )
            metrics["address_creation_denied"] += 1
            return
        a = Address(
309
            vrf=vrf,
Dmitry Volodin's avatar
Dmitry Volodin committed
310
311
            address=address.address,
            name=self.get_address_name(address),
312
            fqdn=address.fqdn,
Dmitry Volodin's avatar
Dmitry Volodin committed
313
314
315
            profile=address.profile,
            description=address.description,
            source=address.source,
Dmitry Volodin's avatar
Dmitry Volodin committed
316
            mac=address.mac,
Dmitry Volodin's avatar
Dmitry Volodin committed
317
318
319
320
321
322
        )
        if address.source in LOCAL_SRC:
            a.managed_object = self.object
            a.subinterface = address.subinterface
        self.logger.info(
            "Creating address %s (%s): name=%s fqdn=%s mac=%s profile=%s source=%s",
Dmitry Volodin's avatar
Dmitry Volodin committed
323
324
325
326
327
328
329
            a.address,
            a.vrf.name,
            a.name,
            a.fqdn,
            a.mac,
            a.profile.name,
            a.source,
Dmitry Volodin's avatar
Dmitry Volodin committed
330
331
        )
        a.save()
332
        self.fire_seen(a)
Dmitry Volodin's avatar
Dmitry Volodin committed
333
334
335
336
337
338
339
340
341
        metrics["address_created"] += 1

    def apply_address_changes(self, address, discovered_address):
        """
        Apply address changes and send signals
        :param address: Address instance
        :param discovered_address: DiscoveredAddress instance
        :return:
        """
342
343
        if self.is_ignored_address(discovered_address):
            return
Dmitry Volodin's avatar
Dmitry Volodin committed
344
345
346
347
348
349
350
351
352
353
354
355
        if self.is_preferred(address.source, discovered_address.source):
            changes = []
            if address.source != discovered_address.source:
                changes += ["source: %s -> %s" % (address.source, discovered_address.source)]
                address.source = discovered_address.source
            if discovered_address.source in LOCAL_SRC:
                # Check name
                name = self.get_address_name(discovered_address)
                if name != address.name:
                    changes += ["name: %s -> %s" % (address.name, name)]
                    address.name = name
                # Check fqdn
356
357
358
                if discovered_address.fqdn != address.fqdn and discovered_address.fqdn:
                    changes += ["fqdn: %s -> %s" % (address.fqdn, discovered_address.fqdn)]
                    address.fqdn = discovered_address.fqdn
Dmitry Volodin's avatar
Dmitry Volodin committed
359
360
                # @todo: Change profile
                # Change managed object
Dmitry Volodin's avatar
Dmitry Volodin committed
361
362
                if discovered_address.source in LOCAL_SRC and (
                    not address.managed_object or address.managed_object.id != self.object.id
Dmitry Volodin's avatar
Dmitry Volodin committed
363
                ):
Dmitry Volodin's avatar
Dmitry Volodin committed
364
                    changes += ["object: %s -> %s" % (address.managed_object, self.object)]
Dmitry Volodin's avatar
Dmitry Volodin committed
365
366
367
                    address.managed_object = self.object
                # Change subinterface
                if (
Dmitry Volodin's avatar
Dmitry Volodin committed
368
369
                    discovered_address.source == SRC_INTERFACE
                    and address.subinterface != discovered_address.subinterface
Dmitry Volodin's avatar
Dmitry Volodin committed
370
                ):
Dmitry Volodin's avatar
Dmitry Volodin committed
371
372
373
374
                    changes += [
                        "subinterface: %s -> %s"
                        % (address.subinterface, discovered_address.subinterface)
                    ]
Dmitry Volodin's avatar
Dmitry Volodin committed
375
376
377
378
379
380
381
382
                    address.subinterface = discovered_address.subinterface
            if discovered_address.mac and address.mac != discovered_address.mac:
                address.mac = discovered_address.mac
                changes += ["mac: %s -> %s" % (address.mac, discovered_address.mac)]
            if changes:
                self.logger.info(
                    "Changing %s (%s): %s",
                    address.address,
383
                    discovered_address.vpn_id,
Dmitry Volodin's avatar
Dmitry Volodin committed
384
                    ", ".join(changes),
Dmitry Volodin's avatar
Dmitry Volodin committed
385
386
387
388
389
                )
                address.save()
                metrics["address_updated"] += 1
        else:
            self.logger.debug(
390
391
                "Do not updating vpn_id=%s address=%s. Source level too low",
                discovered_address.vpn_id,
Dmitry Volodin's avatar
Dmitry Volodin committed
392
                discovered_address.address,
Dmitry Volodin's avatar
Dmitry Volodin committed
393
394
            )
            metrics["address_update_denied"] += 1
395
        self.fire_seen(address)
Dmitry Volodin's avatar
Dmitry Volodin committed
396

397
    def has_address_permission(self, vrf, address):
Dmitry Volodin's avatar
Dmitry Volodin committed
398
399
        """
        Check discovery has permission to manipulate address
400
        :param vrf: VRF instance
Dmitry Volodin's avatar
Dmitry Volodin committed
401
402
403
        :param address: DiscoveredAddress instance
        :return:
        """
Dmitry Volodin's avatar
Dmitry Volodin committed
404
        parent = Prefix.get_parent(vrf, "6" if ":" in address.address else "4", address.address)
405
406
407
        if parent:
            return parent.effective_address_discovery == "E"
        return False
Dmitry Volodin's avatar
Dmitry Volodin committed
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431

    def get_address_name(self, address):
        """
        Render address name
        :param address: DiscoveredAddress instance
        :return: Rendered name
        """
        if address.profile.name_template:
            name = address.profile.name_template.render_subject(
                **self.get_template_context(address)
            )
            return self.strip(name)
        return address.address

    def get_address_fqdn(self, address):
        """
        Render address name
        :param address: DiscoveredAddress instance
        :return: Rendered name or None
        """
        if address.profile.fqdn_template:
            fqdn = address.profile.fqdn_template.render_subject(
                **self.get_template_context(address)
            )
432
433
434
435
            fqdn = self.strip(fqdn)
            if is_fqdn(fqdn):
                return fqdn
            self.logger.error(
Dmitry Volodin's avatar
Dmitry Volodin committed
436
                "Address %s renders to invalid FQDN '%s'. " "Ignoring FQDN", address.address, fqdn
437
            )
Dmitry Volodin's avatar
Dmitry Volodin committed
438
439
440
441
442
443
444
        return None

    @staticmethod
    def strip(s):
        return s.replace("\n", "").strip()

    def get_template_context(self, address):
Dmitry Volodin's avatar
Dmitry Volodin committed
445
        return {"address": address, "get_handler": get_handler, "object": self.object}
446
447
448
449

    @staticmethod
    def is_enabled_for_object(object):
        return (
Dmitry Volodin's avatar
Dmitry Volodin committed
450
451
452
453
            object.object_profile.enable_box_discovery_address_interface
            or object.object_profile.enable_box_discovery_address_management
            or object.object_profile.enable_box_discovery_address_dhcp
            or object.object_profile.enable_box_discovery_address_neighbor
454
        )
455
456
457
458
459
460
461
462
463
464

    def ensure_afi(self, vrf, address):
        """
        Ensure VRF has appropriate AFI enabled
        :param vrf: VRF instance
        :param address: DiscoveredAddress instance
        :return:
        """
        if ":" in address.address:
            # IPv6
Dmitry Volodin's avatar
Dmitry Volodin committed
465
            if not vrf.afi_ipv6:
466
                self.logger.info("[%s|%s] Enabling IPv6 AFI", vrf.name, vrf.vpn_id)
Dmitry Volodin's avatar
Dmitry Volodin committed
467
                vrf.afi_ipv6 = True
468
469
470
                vrf.save()
        else:
            # IPv4
Dmitry Volodin's avatar
Dmitry Volodin committed
471
            if not vrf.afi_ipv4:
472
                self.logger.info("[%s|%s] Enabling IPv4 AFI", vrf.name, vrf.vpn_id)
Dmitry Volodin's avatar
Dmitry Volodin committed
473
                vrf.afi_ipv4 = True
474
                vrf.save()
475
476
477
478
479
480
481
482

    def is_ignored_address(self, address):
        """
        Check address should be ignored
        :param address: DiscoveredAddress instance
        :return: boolean
        """
        return (
Dmitry Volodin's avatar
Dmitry Volodin committed
483
484
485
486
487
            address.mac == "FF:FF:FF:FF:FF:FF"
            or address.address.startswith("127.")
            or address.address.startswith("169.254.")
            or address.address == "::1"
            or address.address.startswith("fe80:")
488
        )
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516

    def fire_seen(self, address):
        """
        Fire `seen` event and process `seen_propagation_policy`
        :param address:
        :return:
        """
        address.fire_event("seen")
        if address.profile.seen_propagation_policy == "D":
            return  # Disabled
        self.propagate_seen(address.prefix)

    def propagate_seen(self, prefix):
        """
        Propagate `seen` through prefix hierarchy
        :param prefix:
        :return:
        """
        if prefix.id in self.propagated_prefixes:
            return  # Already processed
        self.propagated_prefixes.add(prefix.id)
        if prefix.profile.seen_propagation_policy == "D":
            return  # Disabled
        # Fire seen
        prefix.fire_event("seen")
        if prefix.profile.seen_propagation_policy == "E":
            return  # Do not propagate upwards
        # Propagate upwards
Dmitry Volodin's avatar
Fix    
Dmitry Volodin committed
517
        if prefix.parent and prefix.parent.profile.seen_propagation_policy != "D":
518
            self.propagate_seen(prefix.parent)