Source code for dnsmanager.models

from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator, \
    RegexValidator
from django.db import models
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel

# Custom validator to check a domain name
domain_name_validator = RegexValidator(
    regex=r'(?:[a-zA-Z0-9][a-zA-Z0-9-]{0,62}(?<!-)\.)*(?:[a-zA-Z0-9][a-zA-Z0-9-]{0,62}(?<!-))',
    message='Not a valid domain name',
)


[docs]class Zone(models.Model): """ A DNS Zone of a domain name contains all its records. A DNS zone is represented by a zone text file that starts with the special DNS record type Start of Authority (SOA) and contains all records for the resources described within the zone. This format is defined in RFC 1034 and RFC 1035. Please read <https://en.wikipedia.org/wiki/DNS_zone> for more details. In Django DNSManager, zone text file can be generated by going to </dns/slug/>, "slug" being the value of the slug field in this object. """ name = models.CharField( unique=True, max_length=253, validators=[domain_name_validator], verbose_name=_("name"), ) slug = models.SlugField( unique=True, max_length=253, verbose_name=_("slug"), help_text=_("This zone will be accessible at /dns/{slug}/."), ) def __str__(self): """ String for autocompletion results and zone admin column """ return self.name
[docs] def save(self, *args, **kwargs): """ Default value for slug """ if not self.slug: self.slug = slugify(self.name) super().save(*args, **kwargs)
class Meta: verbose_name = _("zone") verbose_name_plural = _("zones") ordering = ['name']
[docs]class Record(PolymorphicModel): """ A generic DNS record of a zone. This object should never be created directly as it is a polymorphic parent to all record types, but records can be retreived by requesting this object. As Django DNSManager uses Django Polymorphic, a request of a record object will not return a record object but the polymorphic child which contains additionnal fields. """ DNS_CLASSES = [ ('IN', _("IN (Internet)")), ('CS', _("CS (CSNET, obsolete)")), ('CH', _("CH (CHAOS)")), ('HS', _("HS (Hesiod)")), ] zone = models.ForeignKey( Zone, on_delete=models.CASCADE, verbose_name=_("zone"), help_text=_("This record will be applied on that zone."), ) name = models.CharField( max_length=253, verbose_name=_("name"), help_text=_( "The domain name for which this record is valid, ending in a dot."), blank=True, ) dns_class = models.CharField( max_length=2, choices=DNS_CLASSES, default='IN', verbose_name=_("class"), help_text=_("You shouldn't need anything else than IN."), ) ttl = models.PositiveIntegerField( null=True, default=3600, verbose_name=_("Time To Live"), help_text=_("Limits the lifetime of this record."), )
[docs] def save(self, *args, **kwargs): """ Clean model on save Without that the model does not get validated """ self.full_clean() return super().save(*args, **kwargs)
class Meta: verbose_name = _("record") verbose_name_plural = _("records") ordering = ['zone', 'name']
[docs]class AddressRecord(Record): """ A Ipv4 Address record (abbreviated A) maps a hostname to a IPv4 address. This format is defined in RFC 1035. """ address = models.GenericIPAddressField( protocol='IPv4', verbose_name=_("IPv4 address"), ) def __str__(self): return f"{self.name} {self.ttl} {self.dns_class} A {self.address}" class Meta: verbose_name = _("A record") verbose_name_plural = _("A records")
[docs]class Ipv6AddressRecord(Record): """ A Ipv6 Address record, or quad-A (abbreviated AAAA), maps a hostname to a IPv6 address. This format is defined in RFC 3596. Please read <https://en.wikipedia.org/wiki/IPv6_address#Domain_Name_System> for more details. """ address = models.GenericIPAddressField( protocol='IPv6', verbose_name=_("IPv6 address"), ) def __str__(self): return f"{self.name} {self.ttl} {self.dns_class} AAAA {self.address}" class Meta: verbose_name = _("AAAA record") verbose_name_plural = _("AAAA records")
[docs]class CanonicalNameRecord(Record): """ A Canonical name record (abbreviated CNAME) aliases one name to another. This format is defined in RFC 1035. Please read <https://en.wikipedia.org/wiki/CNAME_record> for more details. """ c_name = models.CharField( max_length=253, verbose_name=_("canonical name"), help_text=_("This domain name will alias to this canonical name."), ) def __str__(self): return f"{self.name} {self.ttl} {self.dns_class} CNAME {self.c_name}" class Meta: verbose_name = _("CNAME record") verbose_name_plural = _("CNAME records") ordering = ['c_name']
[docs]class CertificationAuthorityAuthorizationRecord(Record): """ A Certification Authority Authorization record (abbreviated CAA) constraints acceptable CAs for a host or domain. This format is defined in RFC 6844. """ TAGS = [ ('issue', _("issue")), ('issuewild', _("issue wildcard")), ('iodef', _("Incident object description exchange format")), ] flags = models.PositiveIntegerField( validators=[ MinValueValidator(0), MaxValueValidator(255), ], verbose_name=_("flags"), ) tag = models.CharField( max_length=255, choices=TAGS, verbose_name=_("tag"), ) value = models.CharField( max_length=511, verbose_name=_("value") ) def __str__(self): return (f"{self.name} {self.ttl} {self.dns_class} CAA {self.flags} " f"{self.tag} {self.value!r}") class Meta: verbose_name = _("CAA record") verbose_name_plural = _("CAA records") ordering = ['flags']
[docs]class DelegationNameRecord(Record): """ A Delegation name record (abbreviated DNAME), aliases a domain to the entire subtree of another domain. This format is defined in RFC 6672. Please read <https://en.wikipedia.org/wiki/CNAME_record#DNAME_record> for more details. """ d_name = models.CharField( max_length=253, validators=[domain_name_validator], verbose_name=_("delegation domain name"), help_text=_("This domain name will alias to the entire subtree of " "that delegation domain."), ) def __str__(self): return f"{self.name} {self.ttl} {self.dns_class} DNAME {self.d_name}" class Meta: verbose_name = _("DNAME record") verbose_name_plural = _("DNAME records") ordering = ['d_name']
[docs]class MailExchangeRecord(Record): """ A Mail Exchange record (abbreviated MX) maps a domain name to a list of message transfer agents for that domain. This format is defined in RFC 1035 and 7505. """ preference = models.PositiveIntegerField( validators=[ MinValueValidator(0), MaxValueValidator(65535), ], verbose_name=_("preference"), ) exchange = models.CharField( max_length=253, validators=[domain_name_validator], verbose_name=_("exchange server"), default="@", ) def __str__(self): return (f"{self.name} {self.ttl} {self.dns_class} MX {self.preference} " f"{self.exchange}") class Meta: verbose_name = _("MX record") verbose_name_plural = _("MX records") ordering = ['preference']
[docs]class NameServerRecord(Record): """ A Name Server record (abbreviated NS) delegates a DNS zone to use the given authoritative name servers. This format is defined in RFC 1035. """ nsdname = models.CharField( max_length=253, validators=[domain_name_validator], verbose_name=_("name server"), default="@", ) def __str__(self): return f"{self.name} {self.ttl} {self.dns_class} NS {self.nsdname}" class Meta: verbose_name = _("NS record") verbose_name_plural = _("NS records") ordering = ['nsdname']
[docs]class PointerRecord(Record): """ A Pointer Resource record (abbreviated PTR) points a name to a canonical name. Unlike a CNAME, DNS processing stops and just the name is returned. It is useful for implementing reverse DNS lookups. This format is defined in RFC 1035. """ ptrdname = models.CharField( max_length=253, validators=[domain_name_validator], verbose_name=_("pointer domain name"), ) def __str__(self): return f"{self.name} {self.ttl} {self.dns_class} PTR {self.ptrdname}" class Meta: verbose_name = _("PTR record") verbose_name_plural = _("PTR records") ordering = ['ptrdname']
[docs]class SshFingerprintRecord(Record): """ A SSH Fingerprint record (abbreviated SSHFP) indicates the SSH public host key fingerprint of a host. This format is defined in RFC 4255 and 6594. """ ALGORITHMS = [ (1, "RSA"), (2, "DSA"), (3, "ECDSA"), (4, "Ed25519"), ] TYPES = [ (1, "SHA-1"), (2, "SHA-256") ] algorithm = models.PositiveIntegerField( choices=ALGORITHMS, validators=[ MinValueValidator(1), MaxValueValidator(4), ], verbose_name=_("algorithm"), ) type = models.PositiveIntegerField( choices=TYPES, validators=[ MinValueValidator(1), MaxValueValidator(2), ], verbose_name=_("type"), ) fingerprint = models.CharField( max_length=64, verbose_name=_("fingerprint"), ) def __str__(self): return (f"{self.name} {self.ttl} {self.dns_class} SSHFP " f"{self.algorithm} {self.type} {self.fingerprint}") class Meta: verbose_name = _("SSHFP record") verbose_name_plural = _("SSHFP records") ordering = ['algorithm']
[docs]class StartOfAuthorityRecord(Record): """ A Start Of Authority record (abbreviated SOA) contains administrative information about the zone. Every zone must have a SOA record to conform to the standard. This format is defined in RFC 1035. Please read <https://en.wikipedia.org/wiki/SOA_record> for more details. """ mname = models.CharField( max_length=253, validators=[domain_name_validator], verbose_name=_("master name server"), help_text=_("Primary master name server for this zone."), ) rname = models.EmailField( verbose_name=_("responsible email"), help_text=_("Email address of the administrator responsible for this " "zone."), ) # TODO: automatic serial based on date # e.g. 2017031405 (5th change from 14th march 2017) serial = models.BigIntegerField( verbose_name=_("serial number"), help_text=_("A slave name server will initiate a zone transfer if " "this serial is incremented."), ) refresh = models.BigIntegerField( verbose_name=_("refresh"), help_text=_("Number of seconds after which secondary name servers " "should query the master to detect zone changes."), default=86400, ) retry = models.BigIntegerField( verbose_name=_("retry"), help_text=_("Number of seconds after which secondary name servers " "should retry to request the serial number from the " "master if the master does not respond."), default=7200, ) expire = models.BigIntegerField( verbose_name=_("expire"), help_text=_("Number of seconds after which secondary name servers " "should stop answering request for this zone if the " "master does not respond."), default=3600000, ) minimum = models.BigIntegerField( verbose_name=_("minimum"), help_text=_("Time to live for purposes of negative caching."), default=172800, )
[docs] def clean(self): super().clean() # Validate that retry < refresh if self.retry >= self.refresh: raise ValidationError({ 'retry': _("Retry should be lower that refresh.") }) # Validate that expire > refresh + retry if self.expire <= self.refresh + self.retry: raise ValidationError({ 'expire': _("Expire should be greater that the sum of refresh " "and retry.") })
[docs] def email_to_rname(self): """ Convert email format to domain name format e.g. root@example.org to root.example.org """ rname = self.rname.split('@') return rname[0].replace('.', '\\.') + '.' + rname[1]
def __str__(self): rname = self.email_to_rname() return (f"{self.name} {self.ttl} {self.dns_class} SOA {self.mname} " f"{rname}. {self.serial} {self.refresh} {self.retry} " f"{self.expire} {self.minimum}") class Meta: verbose_name = _("SOA record") verbose_name_plural = _("SOA records") ordering = ['mname']
[docs]class ServiceRecord(Record): """ A Service record (abbreviated SRV) indicates the presence of a service. It is a generalized service record instead of protocol-specific records such as MX. This format is defined in RFC 2782. Please read <https://en.wikipedia.org/wiki/SRV_record> for more details. """ service = models.CharField( max_length=253, verbose_name=_("service"), help_text=_("The symbolic name of the desired service."), ) protocol = models.CharField( max_length=253, verbose_name=_("protocol"), help_text=_("The transport protocol of the desired service, usually " "either TCP or UDP."), ) priority = models.PositiveIntegerField( validators=[ MinValueValidator(0), MaxValueValidator(65535), ], verbose_name=_("priority"), help_text=_("The priority of the target host, lower value means more " "preferred."), ) weight = models.PositiveIntegerField( validators=[ MinValueValidator(0), MaxValueValidator(65535), ], verbose_name=_("weight"), help_text=_("A relative weight for records with the same priority, " "higher value means higher chance of getting picked."), ) port = models.PositiveIntegerField( validators=[ MinValueValidator(1), MaxValueValidator(65535), ], verbose_name=_("port"), ) target = models.CharField( max_length=253, validators=[domain_name_validator], verbose_name=_("target"), help_text=_("The canonical hostname of the machine providing the " "service, ending in a dot."), ) def __str__(self): return (f"_{self.service}.{self.protocol}.{self.name} {self.ttl} {self.dns_class} SRV {self.priority} " f"{self.weight} {self.port} {self.target}") class Meta: verbose_name = _("SRV record") verbose_name_plural = _("SRV records") ordering = ['priority', 'target']
[docs]class TextRecord(Record): """ A Text record (abbreviated TXT) indicates arbitrary human-readable text. This format is defined in RFC 1035 and 1464. """ data = models.TextField() def __str__(self): # TODO: Make sure that data is split every 255 characters return f"{self.name} {self.ttl} {self.dns_class} TXT {self.data!r}" class Meta: verbose_name = _("TXT record") verbose_name_plural = _("TXT records")
# Aliases A = AddressRecord AAAA = Ipv6AddressRecord CAA = CertificationAuthorityAuthorizationRecord CNAME = CanonicalNameRecord DNAME = DelegationNameRecord MX = MailExchangeRecord NS = NameServerRecord PTR = PointerRecord SOA = StartOfAuthorityRecord SRV = ServiceRecord SSHFP = SshFingerprintRecord TXT = TextRecord