2014-11-14 14:38:06 +00:00
|
|
|
from django.core.exceptions import ValidationError
|
2014-05-08 16:59:35 +00:00
|
|
|
from django.db import models
|
|
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
|
|
|
|
from orchestra.core import services
|
2014-10-30 16:34:02 +00:00
|
|
|
from orchestra.core.validators import validate_ipv4_address, validate_ipv6_address, validate_ascii
|
2014-09-26 15:05:20 +00:00
|
|
|
from orchestra.utils.python import AttrDict
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
from . import settings, validators, utils
|
|
|
|
|
|
|
|
|
|
|
|
class Domain(models.Model):
|
|
|
|
name = models.CharField(_("name"), max_length=256, unique=True,
|
2014-10-30 16:34:02 +00:00
|
|
|
validators=[validators.validate_domain_name, validators.validate_allowed_domain],
|
2014-10-21 09:27:31 +00:00
|
|
|
help_text=_("Domain or subdomain name."))
|
2014-05-08 16:59:35 +00:00
|
|
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
2014-10-21 09:27:31 +00:00
|
|
|
related_name='domains', blank=True, help_text=_("Automatically selected for subdomains."))
|
2014-11-05 20:22:01 +00:00
|
|
|
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set', editable=False)
|
2014-05-08 16:59:35 +00:00
|
|
|
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial,
|
|
|
|
help_text=_("Serial number"))
|
|
|
|
|
|
|
|
def __unicode__(self):
|
|
|
|
return self.name
|
|
|
|
|
2014-10-20 15:51:24 +00:00
|
|
|
@classmethod
|
|
|
|
def get_top_domain(cls, name):
|
|
|
|
split = name.split('.')
|
|
|
|
top = None
|
|
|
|
for i in range(1, len(split)-1):
|
|
|
|
name = '.'.join(split[i:])
|
|
|
|
domain = Domain.objects.filter(name=name)
|
|
|
|
if domain:
|
|
|
|
top = domain.get()
|
|
|
|
return top
|
|
|
|
|
2014-10-03 14:02:11 +00:00
|
|
|
@property
|
2014-05-08 16:59:35 +00:00
|
|
|
def origin(self):
|
|
|
|
return self.top or self
|
|
|
|
|
2014-10-03 14:02:11 +00:00
|
|
|
@property
|
2014-07-17 16:09:24 +00:00
|
|
|
def is_top(self):
|
2014-10-04 09:29:18 +00:00
|
|
|
# don't cache, don't replace by top_id
|
2014-07-17 16:09:24 +00:00
|
|
|
return not bool(self.top)
|
|
|
|
|
2014-11-05 20:22:01 +00:00
|
|
|
@property
|
|
|
|
def subdomains(self):
|
|
|
|
return Domain.objects.filter(name__regex='\.%s$' % self.name)
|
|
|
|
|
2014-10-23 15:38:46 +00:00
|
|
|
def get_absolute_url(self):
|
|
|
|
return 'http://%s' % self.name
|
|
|
|
|
2014-05-08 16:59:35 +00:00
|
|
|
def get_records(self):
|
2014-10-03 14:02:11 +00:00
|
|
|
""" proxy method, needed for input validation, see helpers.domain_for_validation """
|
2014-05-08 16:59:35 +00:00
|
|
|
return self.records.all()
|
|
|
|
|
2014-10-04 09:29:18 +00:00
|
|
|
def get_subdomains(self):
|
2014-10-03 14:02:11 +00:00
|
|
|
""" proxy method, needed for input validation, see helpers.domain_for_validation """
|
2014-11-10 15:40:51 +00:00
|
|
|
return self.origin.subdomain_set.all().prefetch_related('records')
|
2014-05-08 16:59:35 +00:00
|
|
|
|
2014-10-04 09:29:18 +00:00
|
|
|
def get_top(self):
|
2014-10-20 15:51:24 +00:00
|
|
|
return type(self).get_top_domain(self.name)
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
def render_zone(self):
|
|
|
|
origin = self.origin
|
|
|
|
zone = origin.render_records()
|
2014-11-10 15:40:51 +00:00
|
|
|
for subdomain in origin.get_subdomains():
|
2014-05-08 16:59:35 +00:00
|
|
|
zone += subdomain.render_records()
|
|
|
|
return zone
|
|
|
|
|
|
|
|
def refresh_serial(self):
|
|
|
|
""" Increases the domain serial number by one """
|
|
|
|
serial = utils.generate_zone_serial()
|
|
|
|
if serial <= self.serial:
|
|
|
|
num = int(str(self.serial)[8:]) + 1
|
|
|
|
if num >= 99:
|
|
|
|
raise ValueError('No more serial numbers for today')
|
|
|
|
serial = str(self.serial)[:8] + '%.2d' % num
|
|
|
|
serial = int(serial)
|
|
|
|
self.serial = serial
|
2014-09-30 16:06:42 +00:00
|
|
|
self.save(update_fields=['serial'])
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
def render_records(self):
|
|
|
|
types = {}
|
|
|
|
records = []
|
|
|
|
for record in self.get_records():
|
|
|
|
types[record.type] = True
|
|
|
|
if record.type == record.SOA:
|
|
|
|
# Update serial and insert at 0
|
|
|
|
value = record.value.split()
|
|
|
|
value[2] = str(self.serial)
|
2014-09-26 15:05:20 +00:00
|
|
|
records.insert(0,
|
|
|
|
AttrDict(type=record.SOA, ttl=record.get_ttl(), value=' '.join(value))
|
|
|
|
)
|
2014-05-08 16:59:35 +00:00
|
|
|
else:
|
2014-09-26 15:05:20 +00:00
|
|
|
records.append(
|
|
|
|
AttrDict(type=record.type, ttl=record.get_ttl(), value=record.value)
|
|
|
|
)
|
2014-10-03 14:02:11 +00:00
|
|
|
if self.is_top:
|
2014-05-08 16:59:35 +00:00
|
|
|
if Record.NS not in types:
|
|
|
|
for ns in settings.DOMAINS_DEFAULT_NS:
|
2014-09-26 15:05:20 +00:00
|
|
|
records.append(AttrDict(type=Record.NS, value=ns))
|
2014-05-08 16:59:35 +00:00
|
|
|
if Record.SOA not in types:
|
|
|
|
soa = [
|
|
|
|
"%s." % settings.DOMAINS_DEFAULT_NAME_SERVER,
|
|
|
|
utils.format_hostmaster(settings.DOMAINS_DEFAULT_HOSTMASTER),
|
|
|
|
str(self.serial),
|
|
|
|
settings.DOMAINS_DEFAULT_REFRESH,
|
|
|
|
settings.DOMAINS_DEFAULT_RETRY,
|
|
|
|
settings.DOMAINS_DEFAULT_EXPIRATION,
|
|
|
|
settings.DOMAINS_DEFAULT_MIN_CACHING_TIME
|
|
|
|
]
|
2014-09-26 15:05:20 +00:00
|
|
|
records.insert(0, AttrDict(type=Record.SOA, value=' '.join(soa)))
|
2014-10-20 10:20:18 +00:00
|
|
|
is_a = not types or Record.A in types or Record.AAAA in types
|
|
|
|
if Record.MX not in types and is_a:
|
2014-05-08 16:59:35 +00:00
|
|
|
for mx in settings.DOMAINS_DEFAULT_MX:
|
2014-09-26 15:05:20 +00:00
|
|
|
records.append(AttrDict(type=Record.MX, value=mx))
|
2014-10-20 10:20:18 +00:00
|
|
|
if (Record.A not in types and Record.AAAA not in types) and is_a:
|
2014-09-26 15:05:20 +00:00
|
|
|
records.append(AttrDict(type=Record.A, value=settings.DOMAINS_DEFAULT_A))
|
2014-05-08 16:59:35 +00:00
|
|
|
result = ''
|
2014-09-26 15:05:20 +00:00
|
|
|
for record in records:
|
|
|
|
name = '{name}.{spaces}'.format(
|
|
|
|
name=self.name, spaces=' ' * (37-len(self.name))
|
|
|
|
)
|
|
|
|
ttl = record.get('ttl', settings.DOMAINS_DEFAULT_TTL)
|
|
|
|
ttl = '{spaces}{ttl}'.format(
|
|
|
|
spaces=' ' * (7-len(ttl)), ttl=ttl
|
|
|
|
)
|
|
|
|
type = '{type} {spaces}'.format(
|
|
|
|
type=record.type, spaces=' ' * (7-len(record.type))
|
|
|
|
)
|
|
|
|
result += '{name} {ttl} IN {type} {value}\n'.format(
|
|
|
|
name=name, ttl=ttl, type=type, value=record.value
|
|
|
|
)
|
2014-05-08 16:59:35 +00:00
|
|
|
return result
|
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
""" create top relation """
|
|
|
|
update = False
|
|
|
|
if not self.pk:
|
|
|
|
top = self.get_top()
|
|
|
|
if top:
|
|
|
|
self.top = top
|
2014-10-03 17:37:36 +00:00
|
|
|
self.account_id = self.account_id or top.account_id
|
2014-05-08 16:59:35 +00:00
|
|
|
else:
|
|
|
|
update = True
|
|
|
|
super(Domain, self).save(*args, **kwargs)
|
|
|
|
if update:
|
2014-11-05 20:22:01 +00:00
|
|
|
for domain in self.subdomains.exclude(pk=self.pk):
|
|
|
|
# queryset.update() is not used because we want to trigger backend to delete ex-topdomains
|
2014-05-08 16:59:35 +00:00
|
|
|
domain.top = self
|
2014-09-30 16:06:42 +00:00
|
|
|
domain.save(update_fields=['top'])
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Record(models.Model):
|
|
|
|
""" Represents a domain resource record """
|
|
|
|
MX = 'MX'
|
|
|
|
NS = 'NS'
|
|
|
|
CNAME = 'CNAME'
|
|
|
|
A = 'A'
|
|
|
|
AAAA = 'AAAA'
|
|
|
|
SRV = 'SRV'
|
|
|
|
TXT = 'TXT'
|
|
|
|
SOA = 'SOA'
|
|
|
|
|
|
|
|
TYPE_CHOICES = (
|
|
|
|
(MX, "MX"),
|
|
|
|
(NS, "NS"),
|
|
|
|
(CNAME, "CNAME"),
|
|
|
|
(A, _("A (IPv4 address)")),
|
|
|
|
(AAAA, _("AAAA (IPv6 address)")),
|
|
|
|
(SRV, "SRV"),
|
|
|
|
(TXT, "TXT"),
|
|
|
|
(SOA, "SOA"),
|
|
|
|
)
|
|
|
|
|
|
|
|
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records')
|
2014-09-26 15:05:20 +00:00
|
|
|
ttl = models.CharField(_("TTL"), max_length=8, blank=True,
|
|
|
|
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
|
|
|
|
validators=[validators.validate_zone_interval])
|
|
|
|
type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES)
|
|
|
|
value = models.CharField(_("value"), max_length=256)
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
def __unicode__(self):
|
2014-09-26 15:05:20 +00:00
|
|
|
return "%s %s IN %s %s" % (self.domain, self.get_ttl(), self.type, self.value)
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
""" validates record value based on its type """
|
|
|
|
# validate value
|
2014-10-27 13:29:02 +00:00
|
|
|
self.value = self.value.lower().strip()
|
2014-11-05 20:22:01 +00:00
|
|
|
choices = {
|
2014-05-08 16:59:35 +00:00
|
|
|
self.MX: validators.validate_mx_record,
|
|
|
|
self.NS: validators.validate_zone_label,
|
|
|
|
self.A: validate_ipv4_address,
|
|
|
|
self.AAAA: validate_ipv6_address,
|
|
|
|
self.CNAME: validators.validate_zone_label,
|
|
|
|
self.TXT: validate_ascii,
|
|
|
|
self.SRV: validators.validate_srv_record,
|
|
|
|
self.SOA: validators.validate_soa_record,
|
|
|
|
}
|
2014-11-05 20:22:01 +00:00
|
|
|
try:
|
|
|
|
choices[self.type](self.value)
|
|
|
|
except ValidationError, error:
|
|
|
|
raise ValidationError({'value': error})
|
2014-09-26 15:05:20 +00:00
|
|
|
|
|
|
|
def get_ttl(self):
|
|
|
|
return self.ttl or settings.DOMAINS_DEFAULT_TTL
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
services.register(Domain)
|