Пример #1
0
class DNSZoneRecord(models.Model):
    """
    Zone RRs
    """
    class Meta:
        verbose_name = _("DNS Zone Record")
        verbose_name_plural = _("DNS Zone Records")
        db_table = "dns_dnszonerecord"
        app_label = "dns"

    zone = models.ForeignKey(DNSZone, verbose_name="Zone")
    name = models.CharField(_("Name"), max_length=64, blank=True, null=True)
    ttl = models.IntegerField(_("TTL"), null=True, blank=True)
    type = models.CharField(_("Type"), max_length=16)
    priority = models.IntegerField(_("Priority"), null=True, blank=True)
    content = models.CharField(_("Content"), max_length=256)
    tags = TagsField(_("Tags"), null=True, blank=True)

    def __unicode__(self):
        return u"%s %s" % (self.zone.name,
            " ".join([x
                      for x
                      in (self.name, self.type, self.content)
                      if x
                    ]))

    def get_absolute_url(self):
        """Return link to zone preview

        :return: URL
        :rtype: String
        """
        return site.reverse("dns:dnszone:change", self.zone.id)
Пример #2
0
class VRFGroup(models.Model):
    """
    Group of VRFs with common properties
    """
    class Meta:
        verbose_name = _("VRF Group")
        verbose_name_plural = _("VRF Groups")
        db_table = "ip_vrfgroup"
        app_label = "ip"
        ordering = ["name"]

    name = models.CharField(_("VRF Group"),
                            unique=True,
                            max_length=64,
                            help_text=_("Unique VRF Group name"))
    address_constraint = models.CharField(
        _("Address Constraint"),
        max_length=1,
        choices=[("V", _("Addresses are unique per VRF")),
                 ("G", _("Addresses are unique per VRF Group"))],
        default="V")
    description = models.TextField(_("Description"), blank=True, null=True)
    tags = TagsField(_("Tags"), null=True, blank=True)

    def __unicode__(self):
        return unicode(self.name)

    def get_absolute_url(self):
        return site.reverse("ip:vrfgroup:change", self.id)
Пример #3
0
 def forwards(self):
     # Create temporary tags fields
     db.add_column("sa_managedobjectselector", "tmp_filter_tags",
                   TagsField("Tags", null=True, blank=True))
     # Migrate data
     db.execute("""
         UPDATE sa_managedobjectselector
         SET tmp_filter_tags = string_to_array(regexp_replace(filter_tags, ',$', ''), ',')
         WHERE filter_tags != ''
         """)
Пример #4
0
 def forwards(self):
     # Create temporary tags fields
     for m in self.TAG_MODELS:
         db.add_column(
             m, "tmp_tags", TagsField("Tags", null=True, blank=True))
     # Migrate data
     for m in self.TAG_MODELS:
         db.execute("""
         UPDATE %s
         SET tmp_tags = string_to_array(regexp_replace(tags, ',$', ''), ',')
         WHERE tags != ''
         """ % m)
Пример #5
0
class ASSet(models.Model):
    class Meta:
        verbose_name = "ASSet"
        verbose_name_plural = "ASSets"
        db_table = "peer_asset"
        app_label = "peer"

    name = models.CharField("Name", max_length=32, unique=True)
    project = models.ForeignKey(Project,
                                verbose_name="Project",
                                null=True,
                                blank=True,
                                related_name="asset_set")
    description = models.CharField("Description", max_length=64)
    members = models.TextField("Members", null=True, blank=True)
    rpsl_header = models.TextField("RPSL Header", null=True, blank=True)
    rpsl_footer = models.TextField("RPSL Footer", null=True, blank=True)
    tags = TagsField("Tags", null=True, blank=True)

    def __unicode__(self):
        return self.name

    def get_absolute_url(self):
        return site.reverse("peer:asset:change", self.id)

    @property
    def member_list(self):
        if self.members is None:
            return []
        m = sorted(
            self.members.replace(",", " ").replace("\n", " ").replace(
                "\r", " ").upper().split())
        return m

    @property
    def rpsl(self):
        sep = "remark: %s" % ("-" * 72)
        s = []
        if self.rpsl_header:
            s += self.rpsl_header.split("\n")
        s += ["as-set: %s" % self.name]
        for m in self.member_list:
            s += ["members: %s" % m]
        if self.rpsl_footer:
            s += [sep]
            s += self.rpsl_footer.split("\n")
        return rpsl_format("\n".join(s))
Пример #6
0
class Address(models.Model):
    class Meta:
        verbose_name = _("Address")
        verbose_name_plural = _("Addresses")
        db_table = "ip_address"
        app_label = "ip"
        unique_together = [("vrf", "afi", "address")]

    prefix = models.ForeignKey(Prefix, verbose_name=_("Prefix"))
    vrf = models.ForeignKey(VRF, verbose_name=_("VRF"), default=VRF.get_global)
    afi = models.CharField(_("Address Family"),
                           max_length=1,
                           choices=AFI_CHOICES)
    address = INETField(_("Address"))
    fqdn = models.CharField(_("FQDN"),
                            max_length=255,
                            help_text=_("Full-qualified Domain Name"),
                            validators=[check_fqdn])
    project = models.ForeignKey(Project,
                                verbose_name="Project",
                                on_delete=models.SET_NULL,
                                null=True,
                                blank=True,
                                related_name="address_set")
    mac = MACField("MAC", null=True, blank=True, help_text=_("MAC Address"))
    auto_update_mac = models.BooleanField(
        "Auto Update MAC",
        default=False,
        help_text=_("Set to auto-update MAC field"))
    managed_object = models.ForeignKey(
        ManagedObject,
        verbose_name=_("Managed Object"),
        null=True,
        blank=True,
        related_name="address_set",
        on_delete=models.SET_NULL,
        help_text=_(
            "Set if address belongs to the Managed Object's interface"))
    description = models.TextField(_("Description"), blank=True, null=True)
    tags = TagsField(_("Tags"), null=True, blank=True)
    tt = models.IntegerField(_("TT"),
                             blank=True,
                             null=True,
                             help_text=_("Ticket #"))
    style = models.ForeignKey(Style,
                              verbose_name=_("Style"),
                              blank=True,
                              null=True)
    state = models.ForeignKey(ResourceState,
                              verbose_name=_("State"),
                              default=ResourceState.get_default)
    allocated_till = models.DateField(
        _("Allocated till"),
        null=True,
        blank=True,
        help_text=_("Address temporary allocated till the date"))
    ipv6_transition = models.OneToOneField("self",
                                           related_name="ipv4_transition",
                                           null=True,
                                           blank=True,
                                           limit_choices_to={"afi": "6"},
                                           on_delete=models.SET_NULL)

    csv_ignored_fields = ["prefix"]

    def __unicode__(self):
        return u"%s(%s): %s" % (self.vrf.name, self.afi, self.address)

    def get_absolute_url(self):
        return site.reverse("ip:ipam:vrf_index", self.vrf.id, self.afi,
                            self.prefix.prefix)

    @classmethod
    def get_afi(cls, address):
        return "6" if ":" in address else "4"

    @classmethod
    def get_collision(cls, vrf, address):
        """
        Check VRFGroup restrictions
        :param vrf:
        :param address:
        :return: VRF already containing address or None
        :rtype: VRF or None
        """
        if vrf.vrf_group.address_constraint != "G":
            return None
        afi = cls.get_afi(address)
        try:
            a = Address.objects.get(
                afi=afi,
                address=address,
                vrf__in=vrf.vrf_group.vrf_set.exclude(id=vrf.id))
            return a.vrf
        except Address.DoesNotExist:
            return None

    def save(self, **kwargs):
        """
        Override default save() method to set AFI,
        parent prefix, and check VRF group restrictions
        :param kwargs:
        :return:
        """
        # Check VRF group restrictions
        cv = self.get_collision(self.vrf, self.address)
        if cv:
            # Collision detected
            raise ValidationError("Address already exists in VRF %s" % cv)
        # Detect AFI
        self.afi = self.get_afi(self.address)
        # Set proper prefix
        self.prefix = Prefix.get_parent(self.vrf, self.afi, self.address)
        super(Address, self).save(**kwargs)

    def clean(self):
        """
        Field validation
        :return:
        """
        self.prefix = Prefix.get_parent(self.vrf, self.afi, self.address)
        super(Address, self).clean()
        # Check prefix is of AFI type
        if self.afi == "4":
            check_ipv4(self.address)
        elif self.afi == "6":
            check_ipv6(self.address)

    @property
    def short_description(self):
        """
        First line of description
        """
        if self.description:
            return self.description.split("\n", 1)[0].strip()
        else:
            return ""

    def get_index(self):
        """
        Full-text search
        """
        content = [self.address, self.fqdn]
        card = "Address %s, FQDN %s" % (self.address, self.fqdn)
        if self.mac:
            content += [self.mac]
            card += ", MAC %s" % self.mac
        if self.description:
            content += [self.description]
            card += " (%s)" % self.description
        r = {
            "id": "ip.address:%s" % self.id,
            "title": self.address,
            "content": "\n".join(content),
            "card": card
        }
        if self.tags:
            r["tags"] = self.tags
        return r

    def get_search_info(self, user):
        # @todo: Check user access
        return ("iframe", None, {
            "title":
            "Assigned addresses",
            "url":
            "/ip/ipam/%s/%s/%s/change_address/" %
            (self.vrf.id, self.afi, self.address)
        })
Пример #7
0
class DNSZone(models.Model):
    """
    DNS Zone
    """
    class Meta:
        verbose_name = _("DNS Zone")
        verbose_name_plural = _("DNS Zones")
        ordering = ["name"]
        db_table = "dns_dnszone"
        app_label = "dns"

    name = models.CharField(_("Domain"), max_length=256, unique=True)
    description = models.CharField(_("Description"),
                                   null=True,
                                   blank=True,
                                   max_length=64)
    project = models.ForeignKey(Project,
                                verbose_name="Project",
                                null=True,
                                blank=True,
                                related_name="dnszone_set")
    # @todo: Rename to is_provisioned
    is_auto_generated = models.BooleanField(_("Auto generated?"))
    serial = models.IntegerField(_("Serial"), default=0)
    profile = models.ForeignKey(DNSZoneProfile, verbose_name=_("Profile"))
    notification_group = models.ForeignKey(
        NotificationGroup,
        verbose_name=_("Notification Group"),
        null=True,
        blank=True,
        help_text=_("Notification group to use when zone changed"))
    paid_till = models.DateField(_("Paid Till"), null=True, blank=True)
    tags = TagsField(_("Tags"), null=True, blank=True)

    # Managers
    objects = models.Manager()
    forward_zones = ForwardZoneManager()
    reverse_zones = ReverseZoneManager()
    zone = GridVCSField("dnszone")

    def __unicode__(self):
        return self.name

    def get_absolute_url(self):
        """Return link to zone preview

        :return: URL
        :rtype: String
        """
        return site.reverse("dns:dnszone:change", self.id)

    @property
    def type(self):
        """
        Zone type. One of:

        * R4 - IPv4 reverse
        * R6 - IPv6 reverse
        * F - forward zone

        :return: Zone type
        :rtype: String
        """
        nl = self.name.lower()
        if nl.endswith(".in-addr.arpa"):
            return "R4"  # IPv4 reverse
        elif nl.endswith(".ip6.int") or nl.endswith(".ip6.arpa"):
            return "R6"  # IPv6 reverse
        else:
            return "F"  # Forward

    rx_rzone = re.compile(r"^(\d+)\.(\d+)\.(\d+)\.in-addr.arpa$")

    @property
    def reverse_prefix(self):
        """
        Appropriative prefix for reverse zone

        :return: IPv4 or IPv6 prefix
        :rtype: String
        """
        if self.type == "R4":
            # Get IPv4 prefix covering reverse zone
            n = self.name.lower()
            if n.endswith(".in-addr.arpa"):
                r = n[:-13].split(".")
                r.reverse()
                l = 4 - len(r)
                r += ["0"] * l
                ml = 32 - 8 * l
                return ".".join(r) + "/%d" % ml
        elif self.type == "R6":
            # Get IPv6 prefix covering reverse zone
            n = self.name.lower()
            if n.endswith(".ip6.int"):
                n = n[:-8]
            elif n.endswith(".ip6.arpa"):
                n = n[:-9]
            else:
                raise Exception("Invalid IPv6 zone suffix")
            p = n.split(".")
            p.reverse()
            l = len(p)
            if l % 4:
                p += [u"0"] * (4 - l % 4)
            r = ""
            for i, c in enumerate(p):
                if i and i % 4 == 0:
                    r += ":"
                r += c
            if len(p) != 32:
                r += "::"
            prefix = r + "/%d" % (l * 4)
            return IPv6(prefix).normalized.prefix

    @property
    def next_serial(self):
        """
        Next zone serial number. Next serial is greater
        than current one. Serial is built using current data
        to follow common practive.

        :return: Zone serial number
        :rtype: int
        """
        T = time.gmtime()
        base = T[0] * 10000 + T[1] * 100 + T[2]
        s_base = self.serial // 100
        if s_base < base:
            return base * 100  # New day
        else:
            return self.serial + 1  # May cause future lap

    def set_next_serial(self):
        old_serial = self.serial
        self.serial = self.next_serial
        logger.info("Zone %s serial change: %s -> %s", self.name, old_serial,
                    self.serial)
        # self.save()
        # Hack to not send post_save signal
        DNSZone.objects.filter(id=self.id).update(serial=self.serial)

    @property
    def records(self):
        """
        All zone records. Zone records returned as list of tuples
        (name, type, content), where type is RR type.

        :return: Zone records
        :trype: List of tuples
        """

        # @todo: deprecated
        def f(name, type, content, ttl, prio):
            name = name[:-lnsuffix]  # Strip domain from name
            if type == "CNAME" and content.endswith(nsuffix):
                # Strip domain from content
                content = content[:-lnsuffix]
            if prio:
                content = "%s %s" % (prio, content)
            return name, type, content

        suffix = self.name + "."
        nsuffix = "." + suffix
        lnsuffix = len(nsuffix)
        return [
            f(a, b, c, d, e) for a, b, c, d, e in self.get_records()
            if b != "SOA"
        ]

    def zonedata(self, ns):
        """
        Return zone data formatted for given nameserver.

        :param ns: DNS Server
        :type ns: DNSServer
        :return: Zone data
        :rtype: str
        """
        # @todo: deprecated
        return ns.generator_class().get_zone(self)

    @property
    def distribution_list(self):
        """List of DNSServers to distribute zone

        :return: List of DNSServers
        :rtype: List of DNSServer instances
        """
        return self.profile.masters.filter(provisioning__isnull=False)

    @property
    def children(self):
        """List of next-level nested zones"""
        l = len(self.name)
        s = ".%s" % self.name
        return [
            z for z in DNSZone.objects.filter(name__iendswith=s)
            if "." not in z.name[:-l - 1]
        ]

    @classmethod
    def get_ns_name(cls, ns):
        """Add missed '.' to the end of NS name, if given as FQDN"""
        name = ns.name.strip()
        if not is_ipv4(name) and not name.endswith("."):
            return name + "."
        else:
            return name

    @property
    def ns_list(self):
        """
        Sorted list of zone NSes. NSes are properly formatted and have '.'
        at the end.

        :return: List of zone NSes
        :rtype: List of string
        """
        return sorted(
            self.get_ns_name(ns) for ns in self.profile.authoritative_servers)

    @property
    def rpsl(self):
        """
        RPSL for reverse zone. RPSL contains domain: and nserver:
        attributes

        :return: RPSL
        :rtype: String
        """
        if self.type == "F":
            return ""
        # Do not generate RPSL for private reverse zones
        if self.name.lower().endswith(".10.in-addr.arpa"):
            return ""
        n1, n2, n = self.name.lower().split(".", 2)
        if "16.172.in-addr.arpa" <= n <= "31.172.in-addr.arpa":
            return ""
        n1, n = self.name.lower().split(".", 1)
        if n == "168.192.in-addr.arpa":
            return ""
        s = ["domain: %s" % self.name
             ] + ["nserver: %s" % ns for ns in self.ns_list]
        return rpsl_format("\n".join(s))

    def to_idna(self, n):
        if isinstance(n, unicode):
            return n.lower().encode("idna")
        elif isinstance(n, basestring):
            return unicode(n, "utf-8").lower().encode("idna")
        else:
            return n

    def get_soa(self):
        """
        SOA record
        :return:
        """
        def dotted(s):
            if not s.endswith("."):
                return s + "."
            else:
                return s

        return [
            (dotted(self.to_idna(self.name)), "SOA", "%s %s %d %d %d %d %d" %
             (dotted(self.profile.zone_soa), dotted(self.profile.zone_contact),
              self.serial, self.profile.zone_refresh, self.profile.zone_retry,
              self.profile.zone_expire, self.profile.zone_ttl),
             self.profile.zone_ttl, None)
        ]

    def get_ipam_a(self):
        """
        Fetch A/AAAA records from IPAM
        :return: (name, type, content, ttl, prio)
        """
        ttl = self.profile.zone_ttl
        # @todo: Filter by VRF
        r = []
        l = len(self.name) + 1
        q = (Q(fqdn__iexact=self.name) | Q(fqdn__iendswith=".%s" % self.name))
        for z in DNSZone.objects.filter(name__iendswith=".%s" %
                                        self.name).values_list("name",
                                                               flat=True):
            q &= ~(Q(fqdn__iexact=z) | Q(fqdn__iendswith=".%s" % z))
        return [(fqdn[:-l], "A" if afi == "4" else "AAAA", address, ttl, None)
                for afi, fqdn, address in Address.objects.filter(
                    q).values_list("afi", "fqdn", "address")]

    def get_ipam_ptr4(self):
        """
        Fetch IPv4 PTR records from IPAM
        :return: (name, type, content, ttl, prio)
        """
        def ptr(a):
            """
            Convert address to full PTR record
            """
            x = a.split(".")
            x.reverse()
            return "%s.in-addr.arpa" % (".".join(x))

        ttl = self.profile.zone_ttl
        l = len(self.name) + 1
        return [(ptr(a.address)[:-l], "PTR", a.fqdn + ".", ttl, None)
                for a in Address.objects.filter(afi="4").extra(
                    where=["address << %s"], params=[self.reverse_prefix])]

    def get_ipam_ptr6(self):
        """
        Fetch IPv6 PTR records from IPAM
        :return: (name, type, content, ttl, prio)
        :return:
        """
        ttl = self.profile.zone_ttl
        origin_length = (len(self.name) - 8 + 1) // 2
        return [(IPv6(a.address).ptr(origin_length), "PTR", a.fqdn + ".", ttl,
                 None) for a in Address.objects.filter(afi="6").extra(
                     where=["address << %s"], params=[self.reverse_prefix])]

    def get_missed_ns_a(self):
        """
        Returns missed A record for NS'es
        :param records:
        :return:
        """
        suffix = ".%s." % self.name
        ttl = self.profile.zone_ttl
        # Create missed A records for NSses from zone
        # Find in-zone NSes
        in_zone_nses = {}
        for ns in self.profile.authoritative_servers:
            if not ns.ip:
                continue
            ns_name = self.get_ns_name(ns)
            # NS server from zone
            if (ns_name.endswith(suffix)
                    and "." not in ns_name[:-len(suffix)]):
                in_zone_nses[ns_name[:-len(suffix)]] = ns.ip
        # Find missed in-zone NSes
        return [(name, "A", in_zone_nses[name], ttl, None)
                for name in in_zone_nses
                if not (name in in_zone_nses and type in ("A", "IN A"))]

    def get_ns(self):
        ttl = self.profile.zone_ttl
        # Zone NSes
        records = [("", "NS", n, ttl, None) for n in self.ns_list]
        # Add nested NS records if nesessary
        suffix = ".%s." % self.name
        l = len(self.name)
        for z in self.children:
            nested_nses = []
            for ns in z.profile.authoritative_servers:
                ns_name = self.get_ns_name(ns)
                records += [(z.name[:-l - 1], "NS", ns_name, ttl, None)]
                # Zone delegated to NS from the child zone
                if (ns_name.endswith(suffix)
                        and "." in ns_name[:-len(suffix)]):
                    r = (ns_name[:-len(suffix)], ns.ip)
                    if r not in nested_nses:
                        nested_nses += [r]
            if nested_nses:  # Create A records for nested NSes
                for name, ip in nested_nses:
                    records += [(name, "A", ip, ttl, None)]
        return records

    def get_rr(self):
        """
        Get RRs from database
        :return:
        """
        ttl = self.profile.zone_ttl
        return [(r.name, r.type, r.content, r.ttl if r.ttl else ttl,
                 r.priority)
                for r in self.dnszonerecord_set.exclude(name__contains="/")]

    def get_classless_delegation(self):
        """
        Classless reverse zone delegation
        :return:
        """
        records = []
        ttl = self.profile.zone_ttl
        # Range delegations
        for r in AddressRange.objects.filter(action="D").extra(
                where=["from_address << %s", "to_address << %s"],
                params=[self.reverse_prefix, self.reverse_prefix]):
            nses = [ns.strip() for ns in r.reverse_nses.split(",")]
            for a in r.addresses:
                n = a.address.split(".")[-1]
                records += [(n, "CNAME", "%s.%s/32" % (n, n), ttl, None)]
                for ns in nses:
                    if not ns.endswith("."):
                        ns += "."
                    records += [("%s/32" % n, "NS", ns, ttl, None)]
        # Subnet delegation macro
        delegations = defaultdict(list)
        for d in [
                r for r in self.dnszonerecord_set.filter(type="NS",
                                                         name__contains="/")
        ]:
            delegations[d.name] += [d.content]
        # Perform classless reverse zone delegation
        for d in delegations:
            nses = delegations[d]
            net, mask = [int(x) for x in d.split("/")]
            if net < 0 or net > 255 or mask <= 24 or mask > 32:
                continue  # Invalid record
            for ns in nses:
                ns = str(ns)
                if not ns.endswith("."):
                    ns += "."
                records += [(d, "NS", ns, ttl, None)]
            m = mask - 24
            bitmask = ((1 << m) - 1) << (8 - m)
            if net & bitmask != net:
                continue  # Invalid network
            records += [(str(i), "CNAME", "%d.%s" % (i, d), ttl, None)
                        for i in range(net, net + (1 << (8 - m)))]
        return records

    def get_records(self):
        def cmp_fwd(x, y):
            sn = self.name + "."
            return cmp((None if x[0] == sn else x[0], x[1], x[2], x[3], x[4]),
                       (None if y[0] == sn else y[0], y[1], y[2], y[3], y[4]))

        def cmp_ptr(x, y):
            """
            Compare two RR tuples. PTR records are compared as integer,
            other records - as strings.
            """
            x1, x2, _, _, _ = x
            y1, y2, _, _, _ = y
            if x2 == "NS" and y2 != "NS":
                return -1
            if x2 != "NS" and y2 == "NS":
                return 1
            if x2 == y2 == "PTR":
                try:
                    return cmp(int(x1), int(y1))
                except ValueError:
                    pass
            return cmp(x, y)

        def fr(r):
            name, type, content, ttl, prio = r
            if not name.endswith("."):
                if name:
                    name += ".%s." % self.name
                else:
                    name = self.name + "."
            name = self.to_idna(name)
            if (type in ("NS", "MX", "CNAME")):
                if content:
                    if not content.endswith("."):
                        content += ".%s." % self.name
                else:
                    content = self.name + "."
                content = self.to_idna(content)
            return name, type, content, ttl, prio

        records = []
        records += self.get_rr()
        records += self.get_ns()
        if self.type == "F":
            records += self.get_ipam_a()
            records += self.get_missed_ns_a()
            order_by = cmp_fwd
        elif self.type == "R4":
            records += self.get_ipam_ptr4()
            records += self.get_classless_delegation()
            order_by = cmp_ptr
        elif self.type == "R6":
            records += self.get_ipam_ptr6()
            order_by = cmp_ptr
        else:
            raise ValueError("Invalid zone type")
        records = (self.get_soa() + sorted(set(fr(r)
                                               for r in records), order_by))
        return records

    def get_zone_text(self):
        """
        BIND-style zone text for configuration management
        :return:
        """
        zf = ZoneFile(zone=self.name, records=self.get_records())
        return zf.get_text()

    @classmethod
    def get_zone(cls, name):
        """
        Resolve name to zone object
        :return:
        """
        def get_closest(n):
            """
            Return closest matching zone
            """
            while n:
                try:
                    return DNSZone.objects.get(name=n)
                except DNSZone.DoesNotExist:
                    pass
                n = ".".join(n.split(".")[1:])
            return None

        if not name:
            return None
        if is_ipv4(name):
            # IPv4 zone
            n = name.split(".")
            n.reverse()
            return get_closest("%s.in-addr.arpa" % (".".join(n[1:])))
        elif is_ipv6(name):
            # IPv6 zone
            d = IPv6(name).digits
            d.reverse()
            c = ".".join(d)
            return (get_closest("%s.ip6.arpa" % c)
                    or get_closest("%s.ip6.int" % c))
        else:
            return get_closest(name)

    @classmethod
    def touch(cls, name):
        """
        Mark zone as dirty
        :param cls:
        :param name:
        :return:
        """
        z = cls.get_zone(name)
        if z and z.is_auto_generated:
            z._touch()

    def _touch(self, is_new=False):
        logger.debug("Touching zone %s", self.name)
        SyncCache.expire_object(self)

    def ensure_sync(self):
        ss = set(s.sync for s in self.profile.authoritative_servers if s.sync)
        SyncCache.ensure_syncs(self, ss)

    @property
    def channels(self):
        return sorted(
            set("dns/zone/%s" % c for c in self.profile.masters.filter(
                sync_channel__isnull=False).values_list("sync_channel",
                                                        flat=True)))

    def get_notification_groups(self):
        """
        Get a list of notification groups to notify
        about zone changes
        :return:
        """
        if self.notification_group:
            return [self.notification_group]
        if self.profile.notification_group:
            return [self.profile.notification_group]
        ng = SystemNotification.get_notification_group("dns.change")
        if ng:
            return [ng]
        else:
            return []

    def refresh_zone(self):
        """
        Compare zone state with stored one.
        Increase serial and store new version on change
        :return: True if zone has been changed
        """
        logger.debug("Refreshing zone %s", self.name)
        # Stored version
        cz = self.zone.read()
        # Generated version
        nz = self.get_zone_text()
        if cz == nz:
            logger.debug("Zone not changed: %s", self.name)
            return False  # Not changed
        # Step serial
        self.set_next_serial()
        # Generate new zone again
        # Because serial has been changed
        zt = self.get_zone_text()
        self.zone.write(zt)
        # Set change notifications
        groups = self.get_notification_groups()
        if groups:
            ctx = {"name": self.name}
            if cz:
                revs = self.zone.get_revisions()[-2:]
                stpl = "dns.zone.change"
                ctx["diff"] = self.zone.diff(revs[0], revs[1])
            else:
                stpl = "dns.zone.new"
                ctx["data"] = zt
            try:
                t = SystemTemplate.objects.get(name=stpl)
            except SystemTemplate.DoesNotExist:
                return True
            subject = t.render_subject(**ctx)
            body = t.render_body(**ctx)
            for g in groups:
                g.notify(subject, body)
        return True

    def get_sync_data(self):
        """
        Returns sync daemon configuration
        {
            records: [5-tuple]
        }
        """
        self.refresh_zone()
        return {"records": self.get_records()}
Пример #8
0
class Peer(models.Model):
    """
    BGP Peering session
    """
    class Meta:
        verbose_name = "Peer"
        verbose_name_plural = "Peers"
        db_table = "peer_peer"
        app_label = "peer"

    peer_group = models.ForeignKey(PeerGroup, verbose_name="Peer Group")
    project = models.ForeignKey(Project,
                                verbose_name="Project",
                                null=True,
                                blank=True,
                                related_name="peer_set")
    peering_point = models.ForeignKey(PeeringPoint,
                                      verbose_name="Peering Point")
    local_asn = models.ForeignKey(AS, verbose_name="Local AS")
    local_ip = INETField("Local IP")
    local_backup_ip = INETField("Local Backup IP", null=True, blank=True)
    remote_asn = models.IntegerField("Remote AS")
    remote_ip = INETField("Remote IP")
    remote_backup_ip = INETField("Remote Backup IP", null=True, blank=True)
    status = models.CharField("Status",
                              max_length=1,
                              default="A",
                              choices=[("P", "Planned"), ("A", "Active"),
                                       ("S", "Shutdown")])
    import_filter = models.CharField("Import filter", max_length=64)
    # Override PeerGroup.local_pref
    local_pref = models.IntegerField("Local Pref", null=True, blank=True)
    # Override PeerGroup.import_med
    import_med = models.IntegerField("Import MED", blank=True, null=True)
    # Override PeerGroup.export_med
    export_med = models.IntegerField("Export MED", blank=True, null=True)
    export_filter = models.CharField("Export filter", max_length=64)
    description = models.CharField("Description",
                                   max_length=64,
                                   null=True,
                                   blank=True)
    # Peer remark to be shown in RPSL
    rpsl_remark = models.CharField("RPSL Remark",
                                   max_length=64,
                                   null=True,
                                   blank=True)
    tt = models.IntegerField("TT", blank=True, null=True)
    # In addition to PeerGroup.communities
    #and PeeringPoint.communities
    communities = models.CharField("Import Communities",
                                   max_length=128,
                                   blank=True,
                                   null=True)
    max_prefixes = models.IntegerField("Max. Prefixes", default=100)
    import_filter_name = models.CharField("Import Filter Name",
                                          max_length=64,
                                          blank=True,
                                          null=True)
    export_filter_name = models.CharField("Export Filter Name",
                                          max_length=64,
                                          blank=True,
                                          null=True)
    tags = TagsField("Tags", null=True, blank=True)

    def __unicode__(self):
        return u" %s (%s@%s)" % (self.remote_asn, self.remote_ip,
                                 self.peering_point.hostname)

    def get_absolute_url(self):
        return site.reverse("peer:peer:change", self.id)

    def save(self):
        if (self.import_filter_name is not None
                and not self.import_filter_name.strip()):
            self.import_filter_name = None
        if (self.export_filter_name is not None
                and not self.export_filter_name.strip()):
            self.export_filter_name = None
        super(Peer, self).save()
        self.peering_point.sync_cm_prefix_list()

    @property
    def tt_url(self):
        return tt_url(self)

    @property
    def all_communities(self):
        r = {}
        for cl in [
                self.peering_point.communities, self.peer_group.communities,
                self.communities
        ]:
            if cl is None:
                continue
            for c in cl.replace(",", " ").split():
                r[c] = None
        c = sorted(r.keys())
        return " ".join(c)

    @property
    def rpsl(self):
        s = "import: from AS%d" % self.remote_asn
        s += " at %s" % self.peering_point.hostname
        actions = []
        local_pref = self.effective_local_pref
        if local_pref:
            # Select pref meaning
            if config.getboolean("peer", "rpsl_inverse_pref_style"):
                pref = 65535 - local_pref  # RPSL style
            else:
                pref = local_pref
            actions += ["pref=%d;" % pref]
        import_med = self.effective_import_med
        if import_med:
            actions += ["med=%d;" % import_med]
        if actions:
            s += " action " + " ".join(actions)
        s += " accept %s\n" % self.import_filter
        actions = []
        export_med = self.effective_export_med
        if export_med:
            actions += ["med=%d;" % export_med]
        s += "export: to AS%s at %s" % (self.remote_asn,
                                        self.peering_point.hostname)
        if actions:
            s += " action " + " ".join(actions)
        s += " announce %s" % self.export_filter
        return s

    @property
    def effective_max_prefixes(self):
        if self.max_prefixes:
            return self.max_prefixes
        if self.peer_group.max_prefixes:
            return self.peer_group.max_prefixes
        return 0

    @property
    def effective_local_pref(self):
        """
        Effective localpref: Peer specific or PeerGroup inherited
        """
        if self.local_pref is not None:
            return self.local_pref
        return self.peer_group.local_pref

    @property
    def effective_import_med(self):
        """
        Effective import med: Peer specific or PeerGroup inherited
        """
        if self.import_med is not None:
            return self.import_med
        return self.peer_group.import_med

    @property
    def effective_export_med(self):
        """
        Effective export med: Peer specific or PeerGroup inherited
        """
        if self.export_med is not None:
            return self.export_med
        return self.peer_group.export_med

    @classmethod
    def get_peer(cls, address):
        """
        Get peer by address

        :param address: Remote address
        :type address: Str
        :returns: Peer instance or None
        """
        data = list(Peer.objects.filter().extra(
            where=["host(remote_ip)=%s OR host(remote_backup_ip)=%s"],
            params=[address, address]))
        if data:
            return data[0]
        else:
            return None
Пример #9
0
class AS(models.Model):
    class Meta:
        verbose_name = "AS"
        verbose_name_plural = "ASes"
        db_table = "peer_as"
        app_label = "peer"

    asn = models.IntegerField("ASN", unique=True)
    # as-name RPSL Field
    as_name = models.CharField("AS Name", max_length=64, null=True, blank=True)
    project = models.ForeignKey(Project,
                                verbose_name="Project",
                                null=True,
                                blank=True,
                                related_name="as_set")
    # RPSL descr field
    description = models.CharField("Description", max_length=64)
    organisation = models.ForeignKey(Organisation, verbose_name="Organisation")
    administrative_contacts = models.ManyToManyField(
        Person,
        verbose_name="admin-c",
        related_name="as_administrative_contacts",
        null=True,
        blank=True)
    tech_contacts = models.ManyToManyField(Person,
                                           verbose_name="tech-c",
                                           related_name="as_tech_contacts",
                                           null=True,
                                           blank=True)
    maintainers = models.ManyToManyField(Maintainer,
                                         verbose_name="Maintainers",
                                         related_name="as_maintainers",
                                         null=True,
                                         blank=True)
    routes_maintainers = models.ManyToManyField(
        Maintainer,
        verbose_name="Routes Maintainers",
        related_name="as_route_maintainers",
        null=True,
        blank=True)
    # remarks: will be prepended automatically
    header_remarks = models.TextField("Header Remarks", null=True, blank=True)
    # remarks: will be prepended automatically
    footer_remarks = models.TextField("Footer Remarks", null=True, blank=True)
    rir = models.ForeignKey(RIR, verbose_name="RIR")  # source:
    tags = TagsField("Tags", null=True, blank=True)

    def __unicode__(self):
        return u"AS%d (%s)" % (self.asn, self.description)

    def get_absolute_url(self):
        return site.reverse("peer:as:change", self.id)

    @classmethod
    def default_as(cls):
        try:
            return AS.objects.get(asn=0)
        except AS.DoesNotExist:
            # Try to create AS0
            rir = RIR.objects.all()[0]
            org = Organisation.objects.all()[0]
            a = AS(asn=0,
                   as_name="Default",
                   description="Default AS, do not delete",
                   rir=rir,
                   organisation=org)
            a.save()
            return a

    @property
    def rpsl(self):
        sep = "remarks: %s" % ("-" * 72)
        s = []
        s += ["aut-num: AS%s" % self.asn]
        if self.as_name:
            s += ["as-name: %s" % self.as_name]
        if self.description:
            s += ["descr: %s" % x for x in self.description.split("\n")]
        s += ["org: %s" % self.organisation.organisation]
        # Add header remarks
        if self.header_remarks:
            s += ["remarks: %s" % x for x in self.header_remarks.split("\n")]
        # Find AS peers
        pg = {
        }  # Peer Group -> AS -> peering_point -> [(import, export, localpref, import_med, export_med, remark)]
        for peer in self.peer_set.filter(status="A"):
            if peer.peer_group not in pg:
                pg[peer.peer_group] = {}
            if peer.remote_asn not in pg[peer.peer_group]:
                pg[peer.peer_group][peer.remote_asn] = {}
            if peer.peering_point not in pg[peer.peer_group][peer.remote_asn]:
                pg[peer.peer_group][peer.remote_asn][peer.peering_point] = []
            to_skip = False
            e_import_med = peer.effective_import_med
            e_export_med = peer.effective_export_med
            for R in pg[peer.peer_group][peer.remote_asn][peer.peering_point]:
                p_import, p_export, localpref, import_med, export_med, remark = R
                if (peer.import_filter == p_import
                        and peer.export_filter == p_export
                        and e_import_med == import_med
                        and e_export_med == export_med):
                    to_skip = True
                    break
            if not to_skip:
                pg[peer.peer_group][peer.remote_asn][peer.peering_point] +=\
                    [(peer.import_filter, peer.export_filter,
                      peer.effective_local_pref, e_import_med, e_export_med,
                      peer.rpsl_remark)]
        # Build RPSL
        inverse_pref = config.getboolean("peer", "rpsl_inverse_pref_style")
        for peer_group in pg:
            s += [sep]
            s += [
                "remarks: -- %s" % x
                for x in peer_group.description.split("\n")
            ]
            s += [sep]
            for asn in sorted(pg[peer_group]):
                add_at = len(pg[peer_group][asn]) != 1
                for pp in pg[peer_group][asn]:
                    for R in pg[peer_group][asn][pp]:
                        import_filter, export_filter, localpref, import_med,\
                        export_med, remark = R
                        # Prepend import and export with remark when given
                        if remark:
                            s += ["remarks: # %s" % remark]
                        # Build import statement
                        i_s = "import: from AS%d" % asn
                        if add_at:
                            i_s += " at %s" % pp.hostname
                        actions = []
                        if localpref:
                            pref = (65535 -
                                    localpref) if inverse_pref else localpref
                            actions += ["pref=%d;" % pref]
                        if import_med:
                            actions += ["med=%d;" % import_med]
                        if actions:
                            i_s += " action " + " ".join(actions)
                        i_s += " accept %s" % import_filter
                        s += [i_s]
                        # Build export statement
                        e_s = "export: to AS%d" % asn
                        if add_at:
                            e_s += " at %s" % pp.hostname
                        if export_med:
                            e_s += " action med=%d;" % export_med
                        e_s += " announce %s" % export_filter
                        s += [e_s]
        # Add contacts
        for c in self.administrative_contacts.order_by("nic_hdl"):
            s += ["admin-c: %s" % c.nic_hdl]
        for c in self.tech_contacts.order_by("nic_hdl"):
            s += ["tech-c: %s" % c.nic_hdl]
        # Add maintainers
        for m in self.maintainers.all():
            s += ["mnt-by: %s" % m.maintainer]
        for m in self.routes_maintainers.all():
            s += ["mnt-routes: %s" % m.maintainer]
        # Add footer remarks
        if self.footer_remarks:
            s += ["remarks: %s" % x for x in self.footer_remarks.split("\n")]
        return rpsl_format("\n".join(s))

    @property
    def dot(self):
        from noc.peer.models import Peer

        s = ["graph {"]
        all_peers = Peer.objects.filter(local_asn__exact=self)
        uplinks = {}
        peers = {}
        downlinks = {}
        for p in all_peers:
            if p.import_filter == "ANY" and p.export_filter != "ANY":
                uplinks[p.remote_asn] = p
            elif p.export_filter == "ANY":
                downlinks[p.remote_asn] = p
            else:
                peers[p.remote_asn] = p
        asn = "AS%d" % self.asn
        for subgraph, peers in [("uplinks", uplinks.values()),
                                ("peers", peers.values()),
                                ("downlinks", downlinks.values())]:
            s += ["subgraph %s {" % subgraph]
            for p in peers:
                attrs = [
                    "taillabel=\" %s\"" % p.import_filter,
                    "headlabel=\" %s\"" % p.export_filter
                ]
                if p.import_filter == "ANY":
                    attrs += ["arrowtail=open"]
                if p.export_filter == "ANY":
                    attrs += ["arrothead=open"]
                s += [
                    "    %s -- AS%d [%s];" %
                    (asn, p.remote_asn, ",".join(attrs))
                ]
            s += ["}"]
        s += ["}"]
        return "\n".join(s)

    def update_rir_db(self):
        return self.rir.update_rir_db(self.rpsl, self.maintainers.all()[0])
Пример #10
0
class Prefix(models.Model):
    """
    Allocated prefix
    """
    class Meta:
        verbose_name = _("Prefix")
        verbose_name_plural = _("Prefixes")
        db_table = "ip_prefix"
        app_label = "ip"
        unique_together = [("vrf", "afi", "prefix")]

    parent = models.ForeignKey("self",
                               related_name="children_set",
                               verbose_name=_("Parent"),
                               null=True,
                               blank=True)
    vrf = models.ForeignKey(VRF, verbose_name=_("VRF"), default=VRF.get_global)
    afi = models.CharField(_("Address Family"),
                           max_length=1,
                           choices=AFI_CHOICES)
    prefix = CIDRField(_("Prefix"))
    asn = models.ForeignKey(
        AS,
        verbose_name=_("AS"),
        help_text=_("Autonomous system granted with prefix"),
        default=AS.default_as)
    project = models.ForeignKey(Project,
                                verbose_name="Project",
                                on_delete=models.SET_NULL,
                                null=True,
                                blank=True,
                                related_name="prefix_set")
    vc = models.ForeignKey(VC,
                           verbose_name=_("VC"),
                           null=True,
                           blank=True,
                           on_delete=models.SET_NULL,
                           help_text=_("VC bound to prefix"))
    description = models.TextField(_("Description"), blank=True, null=True)
    tags = TagsField("Tags", null=True, blank=True)
    tt = models.IntegerField("TT",
                             blank=True,
                             null=True,
                             help_text=_("Ticket #"))
    style = models.ForeignKey(Style,
                              verbose_name=_("Style"),
                              blank=True,
                              null=True)
    state = models.ForeignKey(ResourceState,
                              verbose_name=_("State"),
                              default=ResourceState.get_default)
    allocated_till = models.DateField(
        _("Allocated till"),
        null=True,
        blank=True,
        help_text=_("Prefix temporary allocated till the date"))
    ipv6_transition = models.OneToOneField("self",
                                           related_name="ipv4_transition",
                                           null=True,
                                           blank=True,
                                           limit_choices_to={"afi": "6"},
                                           on_delete=models.SET_NULL)
    enable_ip_discovery = models.CharField(_("Enable IP Discovery"),
                                           max_length=1,
                                           choices=[("I", "Inherit"),
                                                    ("E", "Enable"),
                                                    ("D", "Disable")],
                                           default="I",
                                           blank=False,
                                           null=False)

    csv_ignored_fields = ["parent"]

    def __unicode__(self):
        return u"%s(%s): %s" % (self.vrf.name, self.afi, self.prefix)

    def get_absolute_url(self):
        return site.reverse("ip:ipam:vrf_index", self.vrf.id, self.afi,
                            self.prefix)

    @property
    def has_transition(self):
        """
        Check prefix has ipv4/ipv6 transition
        :return:
        """
        if self.afi == "4":
            return bool(self.ipv6_transition)
        else:
            try:
                self.ipv4_transition
                return True
            except Prefix.DoesNotExist:
                return False

    def clear_transition(self):
        if self.has_transition:
            if self.afi == "4":
                self.ipv6_transition = None
                self.save()
            else:
                self.ipv4_transition.ipv6_transition = None
                self.ipv4_transition.save()

    @classmethod
    def get_parent(cls, vrf, afi, prefix):
        """
        Get nearest closing prefix
        """
        r = list(
            Prefix.objects.raw(
                """
                SELECT id, prefix
                FROM ip_prefix
                WHERE
                        vrf_id=%s
                    AND afi=%s
                    AND prefix >> %s
                ORDER BY masklen(prefix) DESC
                LIMIT 1
                """, [vrf.id, str(afi), str(prefix)]))
        if not r:
            return None
        return r[0]

    @property
    def is_root(self):
        """
        Returns true if the prefix is a root of VRF
        """
        return (self.afi == "4"
                and self.prefix == "0.0.0.0/0") or (self.afi == "6"
                                                    and self.prefix == "::/0")

    def clean(self):
        """
        Field validation
        """
        super(Prefix, self).clean()
        # Check prefix is of AFI type
        if self.afi == "4":
            check_ipv4_prefix(self.prefix)
        elif self.afi == "6":
            check_ipv6_prefix(self.prefix)
        # Check root prefix have no parent
        if self.is_root and self.parent:
            raise ValidationError("Root prefix cannot have parent")

    def save(self, **kwargs):
        """
        Save prefix
        """
        # Set defaults
        self.afi = "6" if ":" in self.prefix else "4"
        if not self.vrf:
            self.vrf = VRF.get_global()
        if not self.asn:
            self.asn = AS.default_as()
        if not self.is_root:
            # Set proper parent
            self.parent = Prefix.get_parent(self.vrf, self.afi, self.prefix)
        super(Prefix, self).save(**kwargs)
        # Rebuild tree if necessary
        # Reconnect children children prefixes
        c = connection.cursor()
        c.execute(
            """
            UPDATE %s
            SET    parent_id=%%s
            WHERE
                    vrf_id=%%s
                AND afi=%%s
                AND prefix << %%s
                AND parent_id=%%s
        """ % Prefix._meta.db_table, [
                self.id, self.vrf.id, self.afi, self.prefix,
                self.parent.id if self.parent else None
            ])
        # Reconnect children addresses
        c.execute(
            """
            UPDATE %s
            SET prefix_id=%%s
            WHERE
                    prefix_id=%%s
                AND address << %%s
                """ % Address._meta.db_table,
            [self.id, self.parent.id if self.parent else None, self.prefix])

    def delete(self, *args, **kwargs):
        """
        Delete prefix
        """
        if self.is_root:
            raise ValidationError("Cannot delete root prefix")
        # Reconnect children prefixes
        self.children_set.update(parent=self.parent)
        # Reconnect children addresses
        self.address_set.update(prefix=self.parent)
        # Unlink dual-stack allocations
        self.clear_transition()
        # Remove bookmarks
        self.prefixbookmark_set.all().delete()
        # Finally delete
        super(Prefix, self).delete(*args, **kwargs)

    def delete_recursive(self):
        """
        Delete prefix and all descendancies
        """
        # Unlink dual-stack allocations
        self.clear_transition()
        # Recursive delete
        # Get nested prefixes
        ids = Prefix.objects.filter(vrf=self.vrf, afi=self.afi).extra(
            where=["prefix <<= %s"],
            params=[self.prefix]).values_list("id", flat=True)
        #
        zones = set()
        for a in Address.objects.filter(prefix__in=ids):
            zones.add(a.address)
            zones.add(a.fqdn)
        # Delete nested addresses
        Address.objects.filter(prefix__in=ids).delete()
        # Delete nested prefixes
        Prefix.objects.filter(id__in=ids).delete()
        # Delete permissions
        PrefixAccess.objects.filter(vrf=self.vrf, afi=self.afi).extra(
            where=["prefix <<= %s"], params=[self.prefix])
        # Touch dns zones
        for z in zones:
            DNSZone.touch(z)

    @property
    def maintainers(self):
        """
        List of persons having write access
        @todo: PostgreSQL-independent implementation
        """
        return User.objects.raw(
            """
            SELECT id,username,first_name,last_name
            FROM %s u
            WHERE
                is_active=TRUE
                AND
                    (is_superuser=TRUE
                    OR
                    EXISTS(SELECT id
                           FROM %s a
                           WHERE
                                    user_id=u.id
                                AND vrf_id=%%s
                                AND afi=%%s
                                AND prefix>>=%%s
                                AND can_change=TRUE
                           ))
            ORDER BY username""" %
            (User._meta.db_table, PrefixAccess._meta.db_table),
            [self.vrf.id, self.afi, self.prefix])

    ##
    ## First line of description
    ##
    @property
    def short_description(self):
        if self.description:
            return self.description.split("\n", 1)[0].strip()
        else:
            return ""

    ##
    ## Netmask for IPv4
    ##
    @property
    def netmask(self):
        if self.afi == "4":
            return IPv4(self.prefix).netmask.address
        else:
            return None

    ##
    ## Broadcast for IPv4
    ##
    @property
    def broadcast(self):
        if self.afi == "4":
            return IPv4(self.prefix).last.address
        else:
            return None

    ##
    ## Cisco wildcard for IPv4
    ##
    @property
    def wildcard(self):
        if self.afi == "4":
            return IPv4(self.prefix).wildcard.address
        else:
            return ""

    ##
    ## IPv4 prefix size
    ##
    @property
    def size(self):
        if self.afi == "4":
            return IPv4(self.prefix).size
        else:
            return None

    ##
    ## Return True if user has view access
    ##
    def can_view(self, user):
        return PrefixAccess.user_can_view(user, self.vrf, self.afi,
                                          self.prefix)

    ##
    ## Return True if user has change access
    ##
    def can_change(self, user):
        return PrefixAccess.user_can_change(user, self.vrf, self.afi,
                                            self.prefix)

    ##
    ## Check the user has bookmark on prefix
    ##
    def has_bookmark(self, user):
        try:
            PrefixBookmark.objects.get(user=user, prefix=self)
            return True
        except PrefixBookmark.DoesNotExist:
            return False

    ##
    ## Toggle user bookmark. Returns new bookmark state
    ##
    def toggle_bookmark(self, user):
        b, created = PrefixBookmark.objects.get_or_create(user=user,
                                                          prefix=self)
        if created:
            return True
        else:
            b.delete()
            return False

    def get_index(self):
        """
        Full-text search
        """
        content = [self.prefix]
        card = "Prefix %s" % self.prefix
        if self.description:
            content += [self.description]
            card += " (%s)" % self.description
        r = {
            "id": "ip.prefix:%s" % self.id,
            "title": self.prefix,
            "content": "\n".join(content),
            "card": card
        }
        if self.tags:
            r["tags"] = self.tags
        return r

    def get_search_info(self, user):
        # @todo: Check user access
        return ("iframe", None, {
            "title":
            "Assigned addresses",
            "url":
            "/ip/ipam/%s/%s/%s/" % (self.vrf.id, self.afi, self.prefix)
        })

    ##
    ## All prefix-related address ranges
    ##
    @property
    def address_ranges(self):
        return list(
            AddressRange.objects.raw(
                """
            SELECT *
            FROM ip_addressrange
            WHERE
                    vrf_id=%s
                AND afi=%s
                AND is_active=TRUE
                AND
                    (
                            from_address << %s
                        OR  to_address << %s
                        OR  %s BETWEEN from_address AND to_address
                    )
            ORDER BY from_address, to_address
            """,
                [self.vrf.id, self.afi, self.prefix, self.prefix, self.prefix
                 ]))

    @property
    def ippools(self):
        """
        All nested IP Pools
        """
        return list(
            IPPool.objects.raw(
                """
            SELECT *
            FROM ip_ippool i
            WHERE
                  vrf_id = %s
              AND afi = %s
              AND from_address << %s
              AND to_address << %s
              AND NOT EXISTS (
                SELECT id
                FROM ip_prefix p
                WHERE
                      vrf_id = i.vrf_id
                  AND afi = i.afi
                  AND prefix << %s
                  AND
                    (
                      from_address << p.prefix
                      OR to_address << p.prefix
                    )
              )
            ORDER BY from_address
        """, [self.vrf.id, self.afi, self.prefix, self.prefix, self.prefix]))

    def rebase(self, vrf, new_prefix):
        """
        Rebase prefix to a new location
        :param vrf:
        :param new_prefix:
        :return:
        """
        b = IP.prefix(self.prefix)
        nb = IP.prefix(new_prefix)
        # Rebase prefix and all nested prefixes
        # Parents are left untouched
        for p in Prefix.objects.filter(vrf=self.vrf, afi=self.afi).extra(
                where=["prefix <<= %s"], params=[self.prefix]):
            np = IP.prefix(p.prefix).rebase(b, nb).prefix
            # Prefix.objects.filter(pk=p.pk).update(prefix=np, vrf=vrf)
            p.prefix = np
            p.vrf = vrf
            p.save()  # Raise events
        # Rebase addresses
        # Parents are left untouched
        for a in Address.objects.filter(vrf=self.vrf, afi=self.afi).extra(
                where=["address <<= %s"], params=[self.prefix]):
            na = IP.prefix(a.address).rebase(b, nb).address
            # Address.objects.filter(pk=a.pk).update(address=na, vrf=vrf)
            a.address = na
            a.vrf = vrf
            a.save()  # Raise events
        # Rebase permissions
        # move all permissions to the nested blocks
        for pa in PrefixAccess.objects.filter(vrf=self.vrf).extra(
                where=["prefix <<= %s"], params=[self.prefix]):
            np = IP.prefix(pa.prefix).rebase(b, nb).prefix
            PrefixAccess.objects.filter(pk=pa.pk).update(prefix=np, vrf=vrf)
        # create permissions for covered blocks
        for pa in PrefixAccess.objects.filter(vrf=self.vrf).extra(
                where=["prefix >> %s"], params=[self.prefix]):
            PrefixAccess(user=pa.user,
                         vrf=vrf,
                         afi=pa.afi,
                         prefix=new_prefix,
                         can_view=pa.can_view,
                         can_change=pa.can_change).save()
        # @todo: Rebase bookmarks
        # Return rebased prefix
        return Prefix.objects.get(pk=self.pk)  # Updated object

    @property
    def nested_prefix_set(self):
        """
        Queryset returning all nested prefixes inside the prefix
        """
        return Prefix.objects.filter(vrf=self.vrf, afi=self.afi).extra(
            where=["prefix <<= %s"], params=[self.prefix])

    @property
    def nested_address_set(self):
        """
        Queryset returning all nested addresses inside the prefix
        """
        return Address.objects.filter(vrf=self.vrf, afi=self.afi).extra(
            where=["address <<= %s"], params=[self.prefix])

    def iter_free(self):
        """
        Generator returning all available free prefixes inside
        :return:
        """
        for fp in IP.prefix(self.prefix).iter_free(
            [p.prefix for p in self.children_set.all()]):
            yield str(fp)

    @property
    def effective_ip_discovery(self):
        if self.enable_ip_discovery == "I":
            if self.parent:
                return self.parent.effective_ip_discovery
            else:
                return "E"
        else:
            return self.enable_ip_discovery

    @property
    def usage(self):
        if self.afi == "4":
            size = IPv4(self.prefix).size
            if not size:
                return 100.0
            n_ips = Address.objects.filter(prefix=self).count()
            n_pfx = sum(
                IPv4(p).size for p in Prefix.objects.filter(parent=self).only(
                    "prefix").values_list("prefix", flat=True))
            if n_ips:
                if size > 2:  # Not /31 or /32
                    size -= 2  # Exclude broadcast and network
            return float(n_ips + n_pfx) * 100.0 / float(size)
        else:
            return None

    @property
    def usage_percent(self):
        u = self.usage
        if u is None:
            return ""
        else:
            return "%.2f%%" % u
Пример #11
0
class VRF(models.Model):
    """
    VRF
    """
    class Meta:
        verbose_name = _("VRF")
        verbose_name_plural = _("VRFs")
        db_table = "ip_vrf"
        app_label = "ip"
        ordering = ["name"]

    name = models.CharField(
        _("VRF"),
        unique=True,
        max_length=64,
        help_text=_("Unique VRF Name"))
    vrf_group = models.ForeignKey(
        VRFGroup, verbose_name=_("VRF Group"))
    rd = models.CharField(
        _("RD"),
        unique=True,
        max_length=21,
        validators=[check_rd],
        help_text=_("Route Distinguisher in form of ASN:N or IP:N"))
    afi_ipv4 = models.BooleanField(
        _("IPv4"),
        default=True,
        help_text=_("Enable IPv4 Address Family"))
    afi_ipv6 = models.BooleanField(
        _("IPv6"),
        default=False,
        help_text=_("Enable IPv6 Address Family"))
    project = models.ForeignKey(
        Project, verbose_name="Project",
        null=True, blank=True, related_name="vrf_set")
    description = models.TextField(
        _("Description"), blank=True, null=True)
    tt = models.IntegerField(
        _("TT"),
        blank=True,
        null=True,
        help_text=_("Ticket #"))
    tags = TagsField(_("Tags"), null=True, blank=True)
    style = models.ForeignKey(
        Style,
        verbose_name=_("Style"),
        blank=True,
        null=True)
    state = models.ForeignKey(
        ResourceState,
        verbose_name=_("State"),
        default=ResourceState.get_default)
    allocated_till = models.DateField(
        _("Allocated till"),
        null=True,
        blank=True,
        help_text=_("VRF temporary allocated till the date"))

    def __unicode__(self):
        if self.rd == "0:0":
            return u"global"
        else:
            return self.name

    def get_absolute_url(self):
        return site.reverse("ip:vrf:change", self.id)

    @classmethod
    def get_global(cls):
        """
        Returns VRF 0:0
        """
        return VRF.objects.get(rd="0:0")

    @classmethod
    def generate_rd(cls, name):
        """
        Generate unique rd for given name
        """
        return "0:%d" % struct.unpack(
            "I", hashlib.sha1(name).digest()[:4])

    def save(self, **kwargs):
        """
        Create root entries for all enabled AFIs
        """
        # Generate unique rd, if empty
        if not self.rd:
            self.rd = self.generate_rd(self.name)
        # Save VRF
        super(VRF, self).save(**kwargs)
        if self.afi_ipv4:
            # Create IPv4 root, if not exists
            Prefix.objects.get_or_create(
                vrf=self, afi="4", prefix="0.0.0.0/0",
                defaults={
                    "asn": AS.default_as(),
                    "description": "IPv4 Root"
                })
        if self.afi_ipv6:
            # Create IPv6 root, if not exists
            Prefix.objects.get_or_create(
                vrf=self, afi="6", prefix="::/0",
                defaults={
                    "asn": AS.default_as(),
                    "description": "IPv6 Root"})

    def get_index(self):
        """
        Full-text search
        """
        content = [self.name, str(self.rd)]
        card = "VRF %s. RD %s" % (self.name, self.rd)
        if self.description:
            content += [self.description]
            card += " (%s)" % self.description
        r = {
            "id": "ip.vrf:%s" % self.id,
            "title": self.name,
            "content": "\n".join(content),
            "card": card
        }
        if self.tags:
            r["tags"] = self.tags
        return r

    def get_search_info(self, user):
        return ("ip.vrf", "history", {"args": [self.id]})
Пример #12
0
class AddressRange(models.Model):
    class Meta:
        verbose_name = _("Address Range")
        verbose_name = _("Address Ranges")
        db_table = "ip_addressrange"
        app_label = "ip"
        unique_together = [("vrf", "afi", "from_address", "to_address")]

    name = models.CharField(_("Name"), max_length=64, unique=True)
    is_active = models.BooleanField(_("Is Active"), default=True)
    vrf = models.ForeignKey(VRF, verbose_name=_("VRF"))
    afi = models.CharField(
        _("Address Family"),
        max_length=1,
        choices=AFI_CHOICES)
    from_address = CIDRField(_("From Address"))
    to_address = CIDRField(_("To address"))
    description = models.TextField(_("Description"), blank=True, null=True)
    is_locked = models.BooleanField(
        _("Is Locked"),
        default=False,
        help_text=_("Check to deny address creation or editing within the range"))
    action = models.CharField(
        _("Action"),
        max_length=1,
        choices=[
            ("N", _("Do nothing")),
            ("G", _("Generate FQDNs")),
            ("D", _("Partial reverse zone delegation"))
        ],
        default="N")
    fqdn_template = models.CharField(
        _("FQDN Template"),
        max_length=255,
        null=True, blank=True,
        help_text=_("Template to generate FQDNs when 'Action' set to 'Generate FQDNs'"))
    reverse_nses = models.CharField(
        _("Reverse NSes"),
        max_length=255,
        null=True, blank=True,
        help_text=_("Comma-separated list of NSes to partial reverse zone delegation when 'Action' set to 'Partial reverse zone delegation"))
    tags = TagsField(_("Tags"), null=True, blank=True)
    tt = models.IntegerField(
        "TT",
        blank=True, null=True,
        help_text=_("Ticket #"))
    allocated_till = models.DateField(
        _("Allocated till"),
        null=True, blank=True,
        help_text=_("VRF temporary allocated till the date"))

    def __unicode__(self):
        return u"%s (IPv%s): %s -- %s" % (
        self.vrf.name, self.afi, self.from_address, self.to_address)

    def clean(self):
        """
        Field validation
        """
        super(AddressRange, self).clean()
        # Check prefix is of AFI type
        if self.afi == "4":
            check_ipv4(self.from_address)
            check_ipv4(self.to_address)
        elif self.afi == "6":
            check_ipv6(self.from_address)
            check_ipv6(self.to_address)

    def get_absolute_url(self):
        return site.reverse("ip:addressrange:change", self.id)

    ##
    ## Save instance
    ##
    def save(self, **kwargs):
        def generate_fqdns():
            # Prepare FQDN template
            t = Template(self.fqdn_template)
            # Sync FQDNs
            sn = 0
            for ip in self.addresses:
                # Generage FQDN
                vars = {
                    "afi": self.afi,
                    "vrf": self.vrf,
                    "range": self,
                    "n": sn
                }
                sn += 1
                if self.afi == "4":
                    i = ip.address.split(".")
                    vars["ip"] = i  # ip.0 .. ip.3
                    # ip1, ip2, ip3, ip4 for backward compatibility
                    for n, i in enumerate(i):
                        vars["ip%d" % (n + 1)] = i
                elif self.afi == "6":
                    vars["ip"] = ip.digits  # ip.0 .. ip.31
                fqdn = t.render(Context(vars))
                description = "Generated by address range '%s'" % self.name
                # Create or update address record when necessary
                a, created = Address.objects.get_or_create(
                    vrf=self.vrf, afi=self.afi, address=ip.address)
                if created:
                    a.fqdn = fqdn
                    a.description = description
                    a.save()
                elif a.fqdn != fqdn or a.description != a.description:
                    a.fqdn = fqdn
                    a.description = description
                    a.save()

        created = self.id is None
        if not created:
            # Get old values
            old = AddressRange.objects.get(id=self.id)
        super(AddressRange, self).save(**kwargs)
        if created:
            # New
            if self.action == "G":
                generate_fqdns()
        else:
            # Changed
            if old.action == "G" and self.action != "G":
                # Drop all auto-generated IPs
                Address.objects.filter(vrf=self.vrf, afi=self.afi,
                                       address__gte=self.from_address,
                                       address__lte=self.to_address).delete()
            elif old.action != "G" and self.action == "G":
                # Generate IPs
                generate_fqdns()
            elif self.action == "G":
                # Check for boundaries change
                if IP.prefix(old.from_address) < IP.prefix(self.from_address):
                    # Lower boundary raised up. Clean up addresses falled out of range
                    Address.objects.filter(
                        vrf=self.vrf, afi=self.afi,
                        address__gte=old.from_address,
                        address__lt=self.to_address).delete()
                if IP.prefix(old.to_address) > IP.prefix(self.to_address):
                    # Upper boundary is lowered. Clean up addressess falled out of range
                    Address.objects.filter(
                        vrf=self.vrf, afi=self.afi,
                                           address__gt=self.to_address,
                                           address__lte=old.to_address).delete()
                    # Finally recheck FQDNs
                generate_fqdns()

    @property
    def short_description(self):
        """
        First line of description
        """
        if self.description:
            return self.description.split("\n", 1)[0].strip()
        else:
            return ""

    @property
    def addresses(self):
        """
        Generator returning all addresses in range
        """
        return IP.prefix(self.from_address).iter_address(
            until=IP.prefix(self.to_address))

    ##
    ## Returns a list of overlapping ranges
    ##
    @classmethod
    def get_overlapping_ranges(cls, vrf, afi, from_address, to_address):
        return AddressRange.objects.raw("""
            SELECT *
            FROM ip_addressrange
            WHERE
                    vrf_id=%(vrf)s
                AND afi=%(afi)s
                AND is_active
                AND (
                        from_address BETWEEN %(from_address)s AND %(to_address)s
                    OR  to_address BETWEEN %(from_address)s AND %(to_address)s
                    OR  %(from_address)s BETWEEN from_address AND to_address
                    OR  %(to_address)s BETWEEN from_address AND to_address
                )
        """, {
            "vrf": vrf.id,
            "afi": afi,
            "from_address": from_address,
            "to_address": to_address
        })

    ##
    ## Returns a queryset with overlapped ranges
    ##
    @property
    def overlapping_ranges(self):
        return self.get_overlapping_ranges(
            self.vrf, self.afi, self.from_address, self.to_address)

    @classmethod
    def address_is_locked(cls, vrf, afi, address):
        """
        Check wrether address is locked by any range
        """
        return AddressRange.objects.filter(
            vrf=vrf, afi=afi, is_locked=True,
            is_active=True,
            from_address__lte=address,
            to_address__gte=address).exists()
Пример #13
0
class Activator(models.Model):
    """
    Activator
    """
    class Meta:
        verbose_name = _("Activator")
        verbose_name_plural = _("Activators")
        db_table = "sa_activator"
        app_label = "sa"
        ordering = ["name"]

    name = models.CharField(_("Name"), max_length=32, unique=True)
    shard = models.ForeignKey(Shard, verbose_name=_("Shard"))
    is_active = models.BooleanField(_("Is Active"), default=True)
    prefix_table = models.ForeignKey(PrefixTable,
                                     verbose_name=_("Prefix Table"))
    auth = models.CharField(_("Auth String"), max_length=64)
    min_sessions = models.IntegerField(_("Min Scripts"), default=0)
    min_members = models.IntegerField(_("Min Members"), default=0)
    tags = TagsField(_("Tags"), null=True, blank=True)

    def __unicode__(self):
        return self.name

    def get_absolute_url(self):
        return site.reverse("sa:activator:change", self.id)

    @classmethod
    def check_ip_access(cls, ip):
        """
        Check IP belongs to any activator

        :param ip: IP address
        :type ip: String
        :rtype: bool
        """
        return Activator.objects.filter(is_active=True).extra(
            tables=["main_prefixtable", "main_prefixtableprefix"],
            where=[
                "main_prefixtable.id=main_prefixtableprefix.table_id",
                "sa_activator.prefix_table_id=main_prefixtable.id",
                "%s::inet <<= main_prefixtableprefix.prefix"
            ],
            params=[ip]).exists()

    @property
    def capabilities(self):
        """
        Get current activator pool capabilities in form of dict or None
        """
        c = ActivatorCapabilitiesCache.objects.filter(
            activator_id=self.id).first()
        if c is None:
            return {"members": 0, "max_scripts": 0}
        else:
            return {"members": c.members, "max_scripts": c.max_scripts}

    def update_capabilities(self, members, max_scripts):
        """
        Update activator pool capabilities

        :param members: Active members in pool. Pool considered inactive when
                        members == 0
        :param max_scripts: Maximum amount of concurrent scripts in pool
        """
        c = ActivatorCapabilitiesCache.objects.filter(
            activator_id=self.id).first()
        if c:
            c.members = members
            c.max_scripts = max_scripts
            c.save()
        else:
            c = ActivatorCapabilitiesCache(activator_id=self.id,
                                           members=members,
                                           max_scripts=max_scripts)
            c.save()
        return c

    def get_capabilities(self):
        return ActivatorCapabilitiesCache.objects.filter(
            activator_id=self.id).first()
Пример #14
0
class ManagedObjectSelector(models.Model):
    class Meta:
        verbose_name = _("Managed Object Selector")
        verbose_name_plural = _("Managed Object Selectors")
        db_table = "sa_managedobjectselector"
        app_label = "sa"
        ordering = ["name"]

    name = models.CharField(_("Name"), max_length=64, unique=True)
    description = models.TextField(_("Description"), blank=True, null=True)
    is_enabled = models.BooleanField(_("Is Enabled"), default=True)
    filter_id = models.IntegerField(_("Filter by ID"), null=True, blank=True)
    filter_name = models.CharField(_("Filter by Name (REGEXP)"),
                                   max_length=256,
                                   null=True,
                                   blank=True,
                                   validators=[check_re])
    filter_managed = models.NullBooleanField(_("Filter by Is Managed"),
                                             null=True,
                                             blank=True,
                                             default=True)
    filter_profile = models.CharField(_("Filter by Profile"),
                                      max_length=64,
                                      null=True,
                                      blank=True,
                                      choices=profile_registry.choices)
    filter_object_profile = models.ForeignKey(
        ManagedObjectProfile,
        verbose_name=_("Filter by Object's Profile"),
        null=True,
        blank=True)
    filter_address = models.CharField(_("Filter by Address (REGEXP)"),
                                      max_length=256,
                                      null=True,
                                      blank=True,
                                      validators=[check_re])
    filter_prefix = models.ForeignKey(PrefixTable,
                                      verbose_name=_("Filter by Prefix Table"),
                                      null=True,
                                      blank=True)
    filter_shard = models.ForeignKey(Shard,
                                     verbose_name=_("Filter by Shard"),
                                     null=True,
                                     blank=True)
    filter_administrative_domain = models.ForeignKey(
        AdministrativeDomain,
        verbose_name=_("Filter by Administrative Domain"),
        null=True,
        blank=True)
    filter_activator = models.ForeignKey(Activator,
                                         verbose_name=_("Filter by Activator"),
                                         null=True,
                                         blank=True)
    filter_vrf = models.ForeignKey("ip.VRF",
                                   verbose_name=_("Filter by VRF"),
                                   null=True,
                                   blank=True)
    filter_vc_domain = models.ForeignKey("vc.VCDomain",
                                         verbose_name=_("Filter by VC Domain"),
                                         null=True,
                                         blank=True)
    filter_termination_group = models.ForeignKey(
        TerminationGroup,
        verbose_name=_("Filter by termination group"),
        null=True,
        blank=True,
        related_name="selector_termination_group_set")
    filter_service_terminator = models.ForeignKey(
        TerminationGroup,
        verbose_name=_("Filter by service terminator"),
        null=True,
        blank=True,
        related_name="selector_service_terminator_set")
    filter_user = models.CharField(_("Filter by User (REGEXP)"),
                                   max_length=256,
                                   null=True,
                                   blank=True)
    filter_remote_path = models.CharField(_("Filter by Remote Path (REGEXP)"),
                                          max_length=256,
                                          null=True,
                                          blank=True,
                                          validators=[check_re])
    filter_description = models.CharField(_("Filter by Description (REGEXP)"),
                                          max_length=256,
                                          null=True,
                                          blank=True,
                                          validators=[check_re])
    filter_tags = TagsField(_("Filter By Tags"), null=True, blank=True)
    source_combine_method = models.CharField(_("Source Combine Method"),
                                             max_length=1,
                                             default="O",
                                             choices=[("A", "AND"),
                                                      ("O", "OR")])
    sources = models.ManyToManyField("self",
                                     verbose_name=_("Sources"),
                                     symmetrical=False,
                                     null=True,
                                     blank=True,
                                     related_name="sources_set")

    def __unicode__(self):
        return self.name

    @property
    def Q(self):
        """
        Returns Q object which can be applied to
        ManagedObject.objects.filter
        """
        # Exclude NOC internal objects
        q = ~Q(profile_name__startswith="NOC.")
        # Exclude objects being wiped
        q &= ~Q(name__startswith="wiping-")
        # Filter by is_managed
        if self.filter_managed is not None:
            q &= Q(is_managed=self.filter_managed)
        # Filter by ID
        if self.filter_id:
            q &= Q(id=self.filter_id)
        # Filter by name (regex)
        if self.filter_name:
            q &= Q(name__regex=self.filter_name)
        # Filter by profile
        if self.filter_profile:
            q &= Q(profile_name=self.filter_profile)
        # Filter by object's profile
        if self.filter_object_profile:
            q &= Q(object_profile=self.filter_object_profile)
        # Filter by address (regex)
        if self.filter_address:
            q &= Q(address__regex=self.filter_address)
        # Filter by prefix table
        if self.filter_prefix:
            q &= SQL("""
                EXISTS (
                    SELECT * FROM main_prefixtableprefix p
                    WHERE   table_id=%d
                        AND address::inet <<= p.prefix)""" %
                     self.filter_prefix.id)
        # Filter by shard
        if self.filter_shard:
            q &= Q(activator__shard=self.filter_shard)
        # Filter by administrative domain
        if self.filter_administrative_domain:
            q &= Q(administrative_domain=self.filter_administrative_domain)
        # Filter by activator
        if self.filter_activator:
            q &= Q(activator=self.filter_activator)
        # Filter by VRF
        if self.filter_vrf:
            q &= Q(vrf=self.filter_vrf)
        # Filter by VC domain
        if self.filter_vc_domain:
            q &= Q(vc_domain=self.filter_vc_domain)
        # Filter by termination group
        if self.filter_termination_group:
            q &= Q(termination_group=self.filter_termination_group)
        # Filter by termination group
        if self.filter_service_terminator:
            q &= Q(service_terminator=self.filter_service_terminator)
        # Filter by username
        if self.filter_user:
            q &= Q(user__regex=self.filter_user)
        # Filter by remote path
        if self.filter_remote_path:
            q &= Q(remote_path__regex=self.filter_remote_path)
        # Filter by description
        if self.filter_description:
            q &= Q(description__regex=self.filter_description)
        # Restrict to tags when necessary
        if self.filter_tags:
            q &= QTags(self.filter_tags)
        # Restrict to attributes when necessary
        # @todo: optimize with SQL
        m_ids = None
        for s in self.managedobjectselectorbyattribute_set.all():
            ids = ManagedObjectAttribute.objects.filter(
                key__regex=s.key_re,
                value__regex=s.value_re).values_list("managed_object",
                                                     flat=True)
            if m_ids is None:
                m_ids = set(ids)
            else:
                m_ids &= set(ids)
        if m_ids is not None:
            q &= Q(id__in=m_ids)
        # Restrict to sources
        if self.sources.count():
            if self.source_combine_method == "A":
                # AND
                for s in self.sources.all():
                    q &= s.Q
            else:
                # OR
                ql = list(self.sources.all())
                q = ql.pop(0).Q
                for qo in ql:
                    q |= qo.Q
        return q

    EXPR_MAP = [
        # Field, var, op
        ["filter_id", "id", "=="],
        ["filter_name", "name", "~"],
        ["filter_profile", "profile", "=="],
        ["filter_object_profile", "object_profile", "=="],
        ["filter_address", "address", "~"],
        ["filter_prefix", "address", "IN"],
        ["filter_shard", "shard", "=="],
        ["filter_administrative_domain", "administrative_domain", "=="],
        ["filter_activator", "activator", "=="],
        ["filter_vrf", "vrf", "=="],
        ["filter_vc_domain", "vc_domain", "=="],
        ["filter_termination_group", "termination_group", "=="],
        ["filter_service_terminator", "serivce_terminator", "=="],
        ["filter_user", "user", "=="],
        ["filter_remote_path", "remote_path", "~"],
        ["filter_description", "description", "~"],
        ["filter_tags", "tags", "CONTAINS"]
    ]

    @property
    def expr(self):
        """
        Return selector as text expression
        """
        def q(s):
            if isinstance(s, six.integer_types):
                return str(s)
            elif isinstance(s, (list, tuple)):
                s = [q(x) for x in s]
                return u"[%s]" % ", ".join(s)
            else:
                return u"\"%s\"" % unicode(s).replace("\\", "\\\\").replace(
                    "'", "\\'")

        expr = []
        # Filter by is_managed
        if self.filter_managed is not None:
            if self.filter_managed:
                expr += [u"IS MANAGED"]
            else:
                expr += [u"IS NOT MANAGED"]
        # Apply filters
        for f, n, op in self.EXPR_MAP:
            v = getattr(self, f)
            if v:
                expr += [u"%s %s %s" % (n, op, q(v))]
        # Apply attributes filters
        for s in self.managedobjectselectorbyattribute_set.all():
            expr += [u"attr(%s) ~ %s" % (q(s.key_re), q(s.value_re))]

        expr = [u" AND ".join(expr)]
        # Restrict to sources
        if self.sources.count():
            for s in self.sources.all():
                expr += [s.expr]
            op = u" AND " if self.source_combine_method == "A" else u" OR "
            expr = [op.join(u"(%s)" % x for x in expr)]
        return expr[0]

    ##
    ## Returns queryset containing managed objects
    ##
    @property
    def managed_objects(self):
        return ManagedObject.objects.filter(self.Q)

    ##
    ## Check Managed Object matches selector
    ##
    def match(self, managed_object):
        return self.managed_objects.filter(id=managed_object.id).exists()

    def __contains__(self, managed_object):
        """
        "managed_object in selector"
        :param managed_object:
        :return:
        """
        return self.match(managed_object)

    def scripts_profiles(self, scripts):
        """
        Returns a list of profile names supporting scripts
        :param scripts: List of script names
        :return: List of profile names
        """
        sp = set()
        for p in profile_registry.classes:
            skip = False
            for s in scripts:
                if s not in profile_registry[p].scripts:
                    skip = True
                    continue
            if not skip:
                sp.add(p)
        return list(sp)

    ##
    ## Returns queryset containing managed objects supporting scripts
    ##
    def objects_with_scripts(self, scripts):
        return self.managed_objects.filter(
            profile_name__in=self.scripts_profiles(scripts))

    def objects_for_user(self, user, scripts=None):
        """
        Returns queryset containing selector objects accessible to user,
        optionally restricted to ones having scripts
        :param user: User
        :param scripts: optional list of scripts
        :return:
        """
        q = UserAccess.Q(user)
        if scripts:
            q &= Q(profile_name__in=self.scripts_profiles(scripts))
        return self.managed_objects.filter(q)

    @classmethod
    def resolve_expression(cls, s):
        """
        Resolve expression to a list of object.

        Expression must be string or list.
        Elements must be one of:
        * string starting with @ - treated as selector name
        * string containing numbers - treated as object's id
        * string - managed object name.
        * string - IPv4 or IPv6 address - management address

        Raises ManagedObject.DoesNotExists if object is not found.
        Raises ManagedObjectSelector.DoesNotExists if selector is not found
        :param cls:
        :param s:
        :return:
        """
        if type(s) in (int, long, str, unicode):
            s = [s]
        if type(s) != list:
            raise ValueError("list required")
        objects = set()
        for so in s:
            if not isinstance(so, basestring):
                so = str(so)
            if so.startswith("@"):
                # Selector expression: @<selector name>
                o = ManagedObjectSelector.objects.get(name=so[1:])
                objects |= set(o.managed_objects)
            else:
                # Search by name
                q = Q(name=so)
                if is_int(so):
                    # Search by id
                    q |= Q(id=int(so))
                if is_ipv4(so) or is_ipv6(so):
                    q |= Q(address=so)
                o = ManagedObject.objects.get(q)
                objects.add(o)
        return list(objects)
Пример #15
0
class VC(models.Model):
    """
    Virtual circuit
    """
    class Meta:
        verbose_name = "VC"
        verbose_name_plural = "VCs"
        unique_together = [("vc_domain", "l1", "l2"), ("vc_domain", "name")]
        db_table = "vc_vc"
        app_label = "vc"
        ordering = ["vc_domain", "l1", "l2"]

    vc_domain = models.ForeignKey(VCDomain, verbose_name="VC Domain")
    name = models.CharField("Name", max_length=64)
    state = models.ForeignKey(ResourceState,
                              verbose_name="State",
                              default=ResourceState.get_default)
    project = models.ForeignKey(Project,
                                verbose_name="Project",
                                on_delete=models.SET_NULL,
                                null=True,
                                blank=True,
                                related_name="vc_set")
    l1 = models.IntegerField("Label 1")
    l2 = models.IntegerField("Label 2", default=0)
    description = models.CharField("Description",
                                   max_length=256,
                                   null=True,
                                   blank=True)
    style = models.ForeignKey(Style,
                              verbose_name="Style",
                              blank=True,
                              null=True)
    tags = TagsField("Tags", null=True, blank=True)

    def __unicode__(self):
        s = u"%s %d" % (self.vc_domain, self.l1)
        if self.l2:
            s += u"/%d" % self.l2
        s += u": %s" % self.name
        return s

    def get_absolute_url(self):
        return site.reverse("vc:vc:change", self.id)

    @classmethod
    def convert_name(cls, name):
        name = rx_vc_underline.sub("_", name)
        name = rx_vc_empty.sub("", name)
        return name

    def save(self):
        """
        Enforce additional checks
        """
        if (self.l1 < self.vc_domain.type.label1_min
                or self.l1 > self.vc_domain.type.label1_max):
            raise InvalidLabelException("Invalid value for L1")
        if self.vc_domain.type.min_labels > 1 and self.l2 is None:
            raise MissedLabelException("L2 required")
        if (self.vc_domain.type.min_labels > 1
                and not (self.vc_domain.type.label2_min <= self.l2 <=
                         self.vc_domain.type.label2_max)):
            raise InvalidLabelException("Invalid value for L2")
        # Format name
        if self.name:
            self.name = self.convert_name(self.name)
        else:
            self.name = "VC_%04d" % self.l1
        super(VC, self).save()

    def get_index(self):
        """
        Full-text search
        """
        content = [self.name, str(self.l1)]
        card = "VC %s. Tag %s" % (self.name, self.l1)
        if self.description:
            content += [self.description]
            card += " (%s)" % self.description
        if self.l2:
            content += [str(self.l2)]
        r = {
            "id": "vc.vc:%s" % self.id,
            "title": self.name,
            "content": "\n".join(content),
            "card": card
        }
        if self.tags:
            r["tags"] = self.tags
        return r

    def get_search_info(self, user):
        return ("vc.vc", "history", {"args": [self.id]})

    def get_bridge_subinterfaces(self):
        """
        Returns a list of SubInterface instances belonging to VC
        """
        from noc.inv.models.interface import Interface
        from noc.inv.models.subinterface import SubInterface

        r = []
        si_q = MEQ(untagged_vlan=self.l1) | MEQ(tagged_vlans=self.l1)
        # VC Domain's objects
        objects = set(
            self.vc_domain.managedobject_set.values_list("id", flat=True))
        for si in SubInterface.objects.filter(
                managed_object__in=objects, enabled_afi="BRIDGE").filter(si_q):
            if (si.interface.vc_domain is None
                    or si.interface.vc_domain.id == self.vc_domain.id):
                r += [si]
        # Explicit interfaces
        for i in Interface.objects.filter(vc_domain=self.vc_domain.id):
            for si in SubInterface.objects.filter(
                    interface=i.id, enabled_afi="BRIDGE").filter(si_q):
                r += [si]
        return r
Пример #16
0
class CommandSnippet(models.Model):
    """
    Command snippet
    """
    class Meta:
        verbose_name = _("Command Snippet")
        verbose_name_plural = _("Command Snippets")
        db_table = "sa_commandsnippet"
        app_label = "sa"
        ordering = ["name"]

    name = models.CharField(_("Name"), max_length=128, unique=True)
    description = models.TextField(_("Description"))
    snippet = models.TextField(_("Snippet"),
                               help_text=_("Code snippet template"))
    change_configuration = models.BooleanField(_("Change configuration"),
                                               default=False)
    selector = models.ForeignKey(ManagedObjectSelector,
                                 verbose_name=_("Object Selector"))
    is_enabled = models.BooleanField(_("Is Enabled?"), default=True)
    timeout = models.IntegerField(_("Timeout (sec)"), default=60)
    require_confirmation = models.BooleanField(_("Require Confirmation"),
                                               default=False)
    ignore_cli_errors = models.BooleanField(_("Ignore CLI errors"),
                                            default=False)
    # Restrict access to snippet if set
    # effective permission name will be sa:runsnippet:<permission_name>
    permission_name = models.CharField(_("Permission Name"),
                                       max_length=64,
                                       null=True,
                                       blank=True)
    display_in_menu = models.BooleanField(_("Show in menu"), default=False)
    #
    tags = TagsField(_("Tags"), null=True, blank=True)

    def __unicode__(self):
        return self.name

    def get_absolute_url(self):
        return site.reverse("sa:commandsnippet:change", self.id)

    rx_var = re.compile(r"{{\s*([^|}]+?)\s*(?:\|.+?)?}}", re.MULTILINE)
    rx_vartag = re.compile(
        r"\{%\s*var\s+(?P<name>\S+)\s+(?P<type>\S+)(?P<rest>.*)\s*%\}",
        re.MULTILINE)

    @property
    def vars(self):
        """
        Variables used in snippet. Returns dict
        name -> {type: , required: }
        """
        vars = {}
        # Search for {{ var }}
        for v in self.rx_var.findall(self.snippet):
            if "." in v:
                v = v.split(".", 1)[0]
            if v != "object":
                vars[v] = {"type": "str", "required": True, "label": v}
        # Search for {% var <name> <type> %}
        for match in self.rx_vartag.finditer(self.snippet):
            name, type, rest = match.groups()
            vars[name] = {"type": type, "required": True, "label": name}
            if rest:
                for a in shlex.split(rest.strip()):
                    k, v = a.split("=", 1)
                    if k == "label":
                        vars[name][k] = v
        return vars

    def expand(self, data):
        """
        Expand snippet with variables
        """
        return Template(self.snippet).render(Context(data))

    @property
    def effective_permission_name(self):
        if self.permission_name:
            return "sa:runsnippet:" + self.permission_name
        else:
            return "sa:runsnippet:default"

    def save(self, *args, **kwargs):
        super(CommandSnippet, self).save(*args, **kwargs)
        # Create permission if required
        if self.permission_name:
            try:
                Permission.objects.get(name=self.effective_permission_name)
            except Permission.DoesNotExist:
                Permission(name=self.effective_permission_name).save()
Пример #17
0
class ManagedObject(models.Model):
    """
    Managed Object
    """

    class Meta:
        verbose_name = _("Managed Object")
        verbose_name_plural = _("Managed Objects")
        db_table = "sa_managedobject"
        app_label = "sa"
        ordering = ["name"]

    name = models.CharField(_("Name"), max_length=64, unique=True)
    is_managed = models.BooleanField(_("Is Managed?"), default=True)
    administrative_domain = models.ForeignKey(AdministrativeDomain,
            verbose_name=_("Administrative Domain"))
    activator = models.ForeignKey(Activator,
            verbose_name=_("Activator"),
            limit_choices_to={"is_active": True})
    collector = models.ForeignKey(Collector,
            verbose_name=_("Collector"),
            limit_choices_to={"is_active": True}, null=True, blank=True)
    profile_name = models.CharField(_("SA Profile"),
            max_length=128, choices=profile_registry.choices)
    object_profile = models.ForeignKey(ManagedObjectProfile,
        verbose_name=_("Object Profile"))
    description = models.CharField(_("Description"),
            max_length=256, null=True, blank=True)
    # Access
    auth_profile = models.ForeignKey(
        AuthProfile, verbose_name="Auth Profile", null=True, blank=True)
    scheme = models.IntegerField(_("Scheme"), choices=scheme_choices)
    address = models.CharField(_("Address"), max_length=64)
    port = models.IntegerField(_("Port"), blank=True, null=True)
    user = models.CharField(_("User"), max_length=32, blank=True, null=True)
    password = models.CharField(_("Password"),
            max_length=32, blank=True, null=True)
    super_password = models.CharField(_("Super Password"),
            max_length=32, blank=True, null=True)
    remote_path = models.CharField(_("Path"),
            max_length=256, blank=True, null=True)
    trap_source_ip = INETField(_("Trap Source IP"), null=True,
            blank=True, default=None)
    trap_community = models.CharField(_("Trap Community"),
            blank=True, null=True, max_length=64)
    snmp_ro = models.CharField(_("RO Community"),
            blank=True, null=True, max_length=64)
    snmp_rw = models.CharField(_("RW Community"),
            blank=True, null=True, max_length=64)
    #
    vc_domain = models.ForeignKey(
        "vc.VCDomain", verbose_name="VC Domain", null=True, blank=True)
    # CM
    config = GridVCSField("config", mirror=CONFIG_MIRROR)
    # Default VRF
    vrf = models.ForeignKey("ip.VRF", verbose_name=_("VRF"),
                            blank=True, null=True)
    # For service terminators
    # Name of service termination group (i.e. BRAS, SBC)
    termination_group = models.ForeignKey(
        TerminationGroup, verbose_name=_("Termination Group"),
        blank=True, null=True,
        related_name="termination_set"
    )
    # For access switches -- L3 terminator
    service_terminator = models.ForeignKey(
        TerminationGroup, verbose_name=_("Service termination"),
        blank=True, null=True,
        related_name="access_set"
    )
    # Stencils
    shape = models.CharField(_("Shape"), blank=True, null=True,
        choices=stencil_registry.choices, max_length=128)
    # pyRules
    config_filter_rule = models.ForeignKey(PyRule,
            verbose_name="Config Filter pyRule",
            limit_choices_to={"interface": "IConfigFilter"},
            null=True, blank=True,
            on_delete=models.SET_NULL,
            related_name="managed_object_config_filter_rule_set")
    config_diff_filter_rule = models.ForeignKey(PyRule,
            verbose_name=_("Config Diff Filter Rule"),
            limit_choices_to={"interface": "IConfigDiffFilter"},
            null=True, blank=True,
            on_delete=models.SET_NULL,
            related_name="managed_object_config_diff_rule_set")
    config_validation_rule = models.ForeignKey(PyRule,
            verbose_name="Config Validation pyRule",
            limit_choices_to={"interface": "IConfigValidator"},
            null=True, blank=True,
            on_delete=models.SET_NULL,
            related_name="managed_object_config_validation_rule_set")
    max_scripts = models.IntegerField(_("Max. Scripts"),
            null=True, blank=True,
            help_text=_("Concurrent script session limits"))
    #
    tags = TagsField(_("Tags"), null=True, blank=True)

    # Use special filter for profile
    profile_name.existing_choices_filter = True

    # Event ids
    EV_CONFIG_CHANGED = "config_changed"  # Object's config changed
    EV_ALARM_RISEN = "alarm_risen"  # New alarm risen
    EV_ALARM_REOPENED = "alarm_reopened"  # Alarm has been reopen
    EV_ALARM_CLEARED = "alarm_cleared"  # Alarm cleared
    EV_ALARM_COMMENTED = "alarm_commented"  # Alarm commented
    EV_NEW = "new"  # New object created
    EV_DELETED = "deleted"  # Object deleted
    EV_VERSION_CHANGED = "version_changed"  # Version changed
    EV_INTERFACE_CHANGED = "interface_changed"  # Interface configuration changed
    EV_SCRIPT_FAILED = "script_failed"  # Script error
    EV_CONFIG_POLICY_VIOLATION = "config_policy_violation"  # Policy violations found

    PROFILE_LINK = "object_profile"

    ## object.scripts. ...
    class ScriptsProxy(object):
        class CallWrapper(object):
            def __init__(self, obj, name):
                self.name = name
                self.object = obj

            def __call__(self, **kwargs):
                task = ReduceTask.create_task(
                    [self.object],
                    reduce_object_script, {},
                    self.name, kwargs, None
                )
                return task.get_result(block=True)

        def __init__(self, obj):
            self._object = obj
            self._cache = {}

        def __getattr__(self, name):
            if name in self._cache:
                return self._cache[name]
            if name not in self._object.profile.scripts:
                raise AttributeError(name)
            cw = ManagedObject.ScriptsProxy.CallWrapper(self._object, name)
            self._cache[name] = cw
            return cw

    class ActionsProxy(object):
        class CallWrapper(object):
            def __init__(self, obj, name, action):
                self.name = name
                self.object = obj
                self.action = action

            def __call__(self, **kwargs):
                return self.action.execute(self.object, **kwargs)

        def __init__(self, obj):
            self._object = obj
            self._cache = {}

        def __getattr__(self, name):
            if name in self._cache:
                return self._cache[name]
            a = Action.objects.filter(name=name).first()
            if not a:
                raise AttributeError(name)
            cw = ManagedObject.ActionsProxy.CallWrapper(self._object, name, a)
            self._cache[name] = cw
            return cw

    def __init__(self, *args, **kwargs):
        super(ManagedObject, self).__init__(*args, **kwargs)
        self.scripts = ManagedObject.ScriptsProxy(self)
        self.actions = ManagedObject.ActionsProxy(self)

    def __unicode__(self):
        return self.name

    def get_absolute_url(self):
        return site.reverse("sa:managedobject:change", self.id)

    @property
    def profile(self):
        """
        Get object's profile instance. Instances are cached. Same profile's
        instance will be returned for all .profile invocations for
        given managed objet

        :rtype: Profile instance
        """
        try:
            return self._cached_profile
        except AttributeError:
            self._cached_profile = profile_registry[self.profile_name]()
            return self._cached_profile

    @classmethod
    def user_objects(cls, user):
        """
        Get objects available to user

        :param user: User
        :type user: User instance
        :rtype: Queryset instance
        """
        return cls.objects.filter(UserAccess.Q(user))

    def has_access(self, user):
        """
        Check user has access to object

        :param user: User
        :type user: User instance
        :rtype: Bool
        """
        if user.is_superuser:
            return True
        return self.user_objects(user).filter(id=self.id).exists()

    @property
    def granted_users(self):
        """
        Get list of user granted access to object

        :rtype: List of User instancies
        """
        return [u for u in User.objects.filter(is_active=True)
                if ManagedObject.objects.filter(UserAccess.Q(u) &
                                                Q(id=self.id)).exists()]

    @property
    def granted_groups(self):
        """
        Get list of groups granted access to object

        :rtype: List of Group instancies
        """
        return [g for g in Group.objects.filter()
                if ManagedObject.objects.filter(GroupAccess.Q(g) &
                                                Q(id=self.id)).exists()]

    def save(self):
        """
        Overload model's save()
        """
        # Get previous version
        if self.id:
            old = ManagedObject.objects.get(id=self.id)
        else:
            old = None
        # Save
        super(ManagedObject, self).save()
        # IPAM sync
        if self.object_profile.sync_ipam:
            self.sync_ipam()
        # Notify changes
        if ((old is None and self.trap_source_ip) or
            (old and self.trap_source_ip != old.trap_source_ip) or
            (old and self.activator.id != old.activator.id)):
            self.sae_refresh_event_filter()
        # Notify new object
        if old is None:
            SelectorCache.rebuild_for_object(self)
            self.event(self.EV_NEW, {"object": self})
        if not self.collector or not self.trap_source_ip:
            # Remove from object mappings
            ObjectMap.delete_map(self)
        else:
            # Add to object mappings
            ObjectMap.update_map(
                self, self.collector, self.trap_source_ip)

    def delete(self, *args, **kwargs):
        # Deny to delete "SAE" object
        if self.name == "SAE":
            raise IntegrityError("Cannot delete SAE object")
        super(ManagedObject, self).delete(*args, **kwargs)

    def sync_ipam(self):
        """
        Synchronize FQDN and address with IPAM
        """
        from noc.ip.models.address import Address
        from noc.ip.models.vrf import VRF
        # Generate FQDN from template
        fqdn = self.object_profile.get_fqdn(self)
        # Get existing IPAM record
        vrf = self.vrf if self.vrf else VRF.get_global()
        try:
            a = Address.objects.get(vrf=vrf, address=self.address)
        except Address.DoesNotExist:
            # Create new address
            Address(
                vrf=vrf,
                address=self.address,
                fqdn=fqdn,
                managed_object=self
            ).save()
            return
        # Update existing address
        if (a.managed_object != self or
            a.address != self.address or a.fqdn != fqdn):
            a.managed_object = self
            a.address = self.address
            a.fqdn = fqdn
            a.save()

    def get_index(self):
        """
        Get FTS index
        """
        card = "Managed object %s (%s)" % (self.name, self.address)
        content = [
            self.name,
            self.address,
        ]
        if self.trap_source_ip:
            content += [self.trap_source_ip]
        platform = self.platform
        if platform:
            content += [platform]
            card += " [%s]" % platform
        version = self.get_attr("version")
        if version:
            content += [version]
            card += " version %s" % version
        if self.description:
            content += [self.description]
        config = self.config.read()
        if config:
            content += [config]
        r = {
            "id": "sa.managedobject:%s" % self.id,
            "title": self.name,
            "content": "\n".join(content),
            "card": card
        }
        if self.tags:
            r["tags"] = self.tags
        return r

    def get_search_info(self, user):
        if self.has_access(user):
            return ("sa.managedobject", "history", {"args": [self.id]})
        else:
            return None

    ##
    ## Returns True if Managed Object presents in more than one networks
    ## @todo: Rewrite
    ##
    @property
    def is_router(self):
        return self.address_set.count() > 1

    ##
    ## Return attribute as string
    ##
    def get_attr(self, name, default=None):
        try:
            return self.managedobjectattribute_set.get(key=name).value
        except ManagedObjectAttribute.DoesNotExist:
            return default

    ##
    ## Return attribute as bool
    ##
    def get_attr_bool(self, name, default=False):
        v = self.get_attr(name)
        if v is None:
            return default
        if v.lower() in ["t", "true", "y", "yes", "1"]:
            return True
        else:
            return False

    ##
    ## Return attribute as integer
    ##
    def get_attr_int(self, name, default=0):
        v = self.get_attr(name)
        if v is None:
            return default
        try:
            return int(v)
        except:
            return default

    ##
    ## Set attribute
    ##
    def set_attr(self, name, value):
        value = unicode(value)
        try:
            v = self.managedobjectattribute_set.get(key=name)
            v.value = value
        except ManagedObjectAttribute.DoesNotExist:
            v = ManagedObjectAttribute(managed_object=self,
                                       key=name, value=value)
        v.save()

    @property
    def platform(self):
        """
        Return "vendor model" string from attributes
        """
        x = [self.get_attr("vendor"), self.get_attr("platform")]
        x = [a for a in x if a]
        if x:
            return " ".join(x)
        else:
            return None

    def is_ignored_interface(self, interface):
        interface = self.profile.convert_interface_name(interface)
        rx = self.get_attr("ignored_interfaces")
        if rx:
            return re.match(rx, interface) is not None
        return False

    def sae_refresh_event_filter(self):
        """
        Refresh event filters for all activators serving object
        """
        def reduce_notify(task):
            mt = task.maptask_set.all()[0]
            if mt.status == "C":
                return mt.script_result
            return False

        ReduceTask.create_task(
            "SAE",
            reduce_notify, {},
            "notify", {
                "event": "refresh_event_filter",
                "object_id": self.id},
            1
        )

    def get_status(self):
        return ObjectStatus.get_status(self)

    def set_status(self, status):
        ObjectStatus.set_status(self, status)

    def get_inventory(self):
        """
        Retuns a list of inventory Objects managed by
        this managed object
        """
        from noc.inv.models.object import Object
        return list(Object.objects.filter(
            data__management__managed_object=self.id))

    def run_discovery(self, delta=0):
        op = self.object_profile
        for name in get_active_discovery_methods():
            cfg = "enable_%s" % name
            if getattr(op, cfg):
                refresh_schedule(
                    "inv.discovery", name, self.id, delta=delta)
                delta += 1

    def event(self, event_id, data=None, delay=None, tag=None):
        """
        Process object-related event
        :param event_id: ManagedObject.EV_*
        :param data: Event context to render
        :param delay: Notification delay in seconds
        :param tag: Notification tag
        """
        # Get cached selectors
        selectors = SelectorCache.get_object_selectors(self)
        # Find notification groups
        groups = set()
        for o in ObjectNotification.objects.filter(**{
            event_id: True,
            "selector__in": selectors}):
            groups.add(o.notification_group)
        if not groups:
            return  # Nothing to notify
        # Render message
        subject, body = ObjectNotification.render_message(event_id, data)
        # Send notification
        if not tag and event_id in (
                self.EV_ALARM_CLEARED,
                self.EV_ALARM_COMMENTED,
                self.EV_ALARM_REOPENED,
                self.EV_ALARM_RISEN) and "alarm" in data:
            tag = "alarm:%s" % data["alarm"].id
        NotificationGroup.group_notify(
            groups, subject=subject, body=body, delay=delay, tag=tag)
        # Schedule FTS reindex
        if event_id in (
            self.EV_CONFIG_CHANGED, self.EV_VERSION_CHANGED):
            FTSQueue.schedule_update(self)

    def save_config(self, data):
        if isinstance(data, list):
            # Convert list to plain text
            r = []
            for d in sorted(data, lambda x, y: cmp(x["name"], y["name"])):
                r += ["==[ %s ]========================================\n%s" % (d["name"], d["config"])]
            data = "\n".join(r)
        # Pass data through config filter, if given
        if self.config_filter_rule:
            data = self.config_filter_rule(
                managed_object=self, config=data)
        # Pass data through the validation filter, if given
        # @todo: Remove
        if self.config_validation_rule:
            warnings = self.config_validation_rule(
                managed_object=self, config=data)
            if warnings:
                # There are some warnings. Notify responsible persons
                self.event(
                    self.EV_CONFIG_POLICY_VIOLATION,
                    {
                        "object": self,
                        "warnings": warnings
                    }
                )
        # Calculate diff
        old_data = self.config.read()
        is_new = not bool(old_data)
        diff = None
        if not is_new:
            # Calculate diff
            if self.config_diff_filter_rule:
                # Pass through filters
                old_data = self.config_diff_filter_rule(
                    managed_object=self, config=old_data)
                new_data = self.config_diff_filter_rule(
                    managed_object=self, config=data)
                if not old_data and not new_data:
                    logger.error("[%s] broken config_diff_filter: Returns empty result", self.name)
            else:
                new_data = data
            if old_data == new_data:
                return  # Nothing changed
            diff = "".join(difflib.unified_diff(
                old_data.splitlines(True),
                new_data.splitlines(True),
                fromfile=os.path.join("a", self.name.encode("utf8")),
                tofile=os.path.join("b", self.name.encode("utf8"))
            ))
        # Notify changes
        self.event(
            self.EV_CONFIG_CHANGED,
            {
                "object": self,
                "is_new": is_new,
                "config": data,
                "diff": diff
            }
        )
        # Save config
        self.config.write(data)
        # Run config validation
        from noc.cm.engine import Engine
        engine = Engine(self)
        try:
            engine.check()
        except:
            logger.error("Failed to validate config for %s", self.name)
            error_report()

    @property
    def credentials(self):
        """
        Get effective credentials
        """
        if self.auth_profile:
            return Credentials(
                user=self.auth_profile.user,
                password=self.auth_profile.password,
                super_password=self.auth_profile.super_password,
                snmp_ro=self.auth_profile.snmp_ro or self.snmp_ro,
                snmp_rw=self.auth_profile.snmp_rw or self.snmp_rw
            )
        else:
            return Credentials(
                user=self.user,
                password=self.password,
                super_password=self.super_password,
                snmp_ro=self.snmp_ro,
                snmp_rw=self.snmp_rw
            )

    @property
    def scripts_limit(self):
        ol = self.max_scripts or None
        pl = self.profile.max_scripts
        if not ol:
            return pl
        if pl:
            return min(ol, pl)
        else:
            return ol

    def get_probe_config(self, config):
        # Get via solutions
        try:
            return get_probe_config(self, config)
        except ValueError:
            pass
        if config == "address":
            return self.address
        elif config == "snmp__ro":
            s = self.credentials.snmp_ro
            if not s:
                raise ValueError("No SNMP RO community")
            else:
                return s
        elif config == "caps":
            if not hasattr(self, "_caps"):
                self._caps = self.get_caps()
            return self._caps
        elif config == "managed_object":
            return self
        elif config == "profile":
            return self.profile_name
        raise ValueError("Invalid config parameter '%s'" % config)

    def iter_recursive_objects(self):
        """
        Generator yilding all recursive objects
        for effective PM settings
        """
        from noc.inv.models.interface import Interface
        for i in Interface.objects.filter(managed_object=self.id):
            yield i

    def get_caps(self):
        """
        Returns a dict of effective object capabilities
        """
        caps = ObjectCapabilities.objects.filter(object=self).first()
        if not caps:
            return {}
        r = {}
        for c in caps.caps:
            v = c.local_value if c.local_value is not None else c.discovered_value
            if v is None:
                continue
            r[c.capability.name] = v
        return r

    def update_caps(self, caps, local=False):
        """
        Update existing capabilities with a new ones.
        :param caps: dict of caps name -> caps value
        """
        def get_cap(name):
            if name in ccache:
                return ccache[name]
            c = Capability.objects.filter(name=name).first()
            ccache[name] = c
            return c

        to_save = False
        ocaps = ObjectCapabilities.objects.filter(object=self).first()
        if not ocaps:
            ocaps = ObjectCapabilities(object=self)
            to_save = True
        # Index existing capabilities
        cn = {}
        ccache = {}
        for c in ocaps.caps:
            cn[c.capability.name] = c
        # Add missed capabilities
        for mc in set(caps) - set(cn):
            c = get_cap(mc)
            if c:
                cn[mc] = CapsItem(
                    capability=c,
                    discovered_value=None, local_value=None
                )
                to_save = True
        nc = []
        for c in sorted(cn):
            cc = cn[c]
            if c in caps:
                if local:
                    if cc.local_value != caps[c]:
                        logger.info("[%s] Setting local capability %s = %s",
                                    self.name, c, caps[c])
                        cc.local_value = caps[c]
                        to_save = True
                else:
                    if cc.discovered_value != caps[c]:
                        logger.info("[%s] Setting discovered capability %s = %s",
                                    self.name, c, caps[c])
                        cc.discovered_value = caps[c]
                        to_save = True
            nc += [cc]
        # Remove deleted capabilities
        ocaps.caps = [
            c for c in nc
            if (c.discovered_value is not None or
                c.local_value is not None)
        ]
        if to_save:
            ocaps.save()  # forces probe rebuild

    def disable_discovery(self):
        """
        Disable all discovery methods related with managed object
        """

    def apply_discovery(self):
        """
        Apply effective discovery settings
        """
        methods = []
        for name in get_active_discovery_methods():
            cfg = "enable_%s" % name
            if getattr(self.object_profile, cfg):
                methods += [cfg]
        # @todo: Create tasks

    @property
    def version(self):
        """
        Returns filled Version object
        """
        if not hasattr(self, "_c_version"):
            self._c_version = Version(
                profile=self.profile_name,
                vendor=self.get_attr("vendor"),
                platform=self.get_attr("platform"),
                version=self.get_attr("version")
            )
        return self._c_version

    def get_parser(self):
        """
        Return parser instance or None.
        Depends on version_discovery
        """
        v = self.version
        cls = self.profile.get_parser(v.vendor, v.platform, v.version)
        if cls:
            return get_solution(cls)(self)
        else:
            return get_solution("noc.cm.parsers.base.BaseParser")(self)