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)
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)
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 != '' """)
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)
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))
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) })
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()}
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
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])
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
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]})
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()
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()
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)
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
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()
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)