django-orchestra/orchestra/contrib/domains/backends.py

223 lines
8 KiB
Python
Raw Normal View History

2014-10-17 20:03:41 +00:00
import re
import socket
import textwrap
2014-05-08 16:59:35 +00:00
from django.utils.translation import ugettext_lazy as _
2014-05-08 16:59:35 +00:00
from orchestra.contrib.orchestration import ServiceController
2015-04-07 15:14:49 +00:00
from orchestra.contrib.orchestration import Operation
2015-04-20 14:23:10 +00:00
from orchestra.utils.python import OrderedSet
2014-05-08 16:59:35 +00:00
from . import settings
from .models import Record, Domain
2014-05-08 16:59:35 +00:00
2016-03-08 10:16:49 +00:00
class Bind9MasterDomainController(ServiceController):
2015-04-23 19:46:23 +00:00
"""
Bind9 zone and config generation.
It auto-discovers slave Bind9 servers based on your routing configuration and NS servers.
2015-04-23 19:46:23 +00:00
"""
2015-04-24 11:39:20 +00:00
CONF_PATH = settings.DOMAINS_MASTERS_PATH
2015-04-23 19:46:23 +00:00
2014-05-08 16:59:35 +00:00
verbose_name = _("Bind9 master domain")
model = 'domains.Domain'
related_models = (
('domains.Record', 'domain__origin'),
('domains.Domain', 'origin'),
)
2016-01-28 11:18:28 +00:00
ignore_fields = ('serial',)
2015-04-24 11:39:20 +00:00
doc_settings = (settings,
('DOMAINS_MASTERS_PATH',)
2015-04-24 11:39:20 +00:00
)
2014-05-08 16:59:35 +00:00
@classmethod
def is_main(cls, obj):
""" work around Domain.top self relationship """
2016-03-08 10:16:49 +00:00
if super(Bind9MasterDomainController, cls).is_main(obj):
2014-05-08 16:59:35 +00:00
return not obj.top
def save(self, domain):
context = self.get_context(domain)
domain.refresh_serial()
self.update_zone(domain, context)
self.update_conf(context)
def update_zone(self, domain, context):
2014-05-08 16:59:35 +00:00
context['zone'] = ';; %(banner)s\n' % context
context['zone'] += domain.render_zone()
self.append(textwrap.dedent("""\
# Generate %(name)s zone file
cat << 'EOF' > %(zone_path)s.tmp
%(zone)s
EOF
2014-10-17 20:03:41 +00:00
diff -N -I'^\s*;;' %(zone_path)s %(zone_path)s.tmp || UPDATED=1
2015-03-11 20:01:08 +00:00
# Because bind reload will not display any fucking error
named-checkzone -k fail -n fail %(name)s %(zone_path)s.tmp
mv %(zone_path)s.tmp %(zone_path)s\
""") % context
)
2014-05-08 16:59:35 +00:00
def update_conf(self, context):
self.append(textwrap.dedent("""
# Update bind config file for %(name)s
read -r -d '' conf << 'EOF' || true
%(conf)s
EOF
2015-04-07 15:14:49 +00:00
sed '/zone "%(name)s".*/,/^\s*};\s*$/!d' %(conf_path)s | diff -B -I"^\s*//" - <(echo "${conf}") || {
2014-10-20 15:51:24 +00:00
sed -i -e '/zone\s\s*"%(name)s".*/,/^\s*};/d' \\
2014-10-27 15:15:22 +00:00
-e 'N; /^\s*\\n\s*$/d; P; D' %(conf_path)s
2015-04-07 15:14:49 +00:00
echo "${conf}" >> %(conf_path)s
2014-10-17 13:41:08 +00:00
UPDATED=1
}""") % context
)
2014-10-17 20:03:41 +00:00
self.append(textwrap.dedent("""\
# Delete ex-top-domains that are now subdomains
2014-10-20 15:51:24 +00:00
sed -i -e '/zone\s\s*".*\.%(name)s".*/,/^\s*};\s*$/d' \\
-e 'N; /^\s*\\n\s*$/d; P; D' %(conf_path)s""") % context
)
2014-10-17 20:03:41 +00:00
if 'zone_path' in context:
context['zone_subdomains_path'] = re.sub(r'^(.*/)', r'\1*.', context['zone_path'])
2016-06-17 10:00:04 +00:00
self.append('rm -f -- %(zone_subdomains_path)s' % context)
2014-05-08 16:59:35 +00:00
def delete(self, domain):
context = self.get_context(domain)
self.append('# Delete zone file for %(name)s' % context)
2016-06-17 10:00:04 +00:00
self.append('rm -f -- %(zone_path)s;' % context)
2014-05-08 16:59:35 +00:00
self.delete_conf(context)
def delete_conf(self, context):
2014-10-17 13:09:56 +00:00
if context['name'][0] in ('*', '_'):
# These can never be top level domains
return
self.append(textwrap.dedent("""
# Delete config for %(name)s
2014-10-20 15:51:24 +00:00
sed -e '/zone\s\s*"%(name)s".*/,/^\s*};\s*$/d' \\
-e 'N; /^\s*\\n\s*$/d; P; D' %(conf_path)s > %(conf_path)s.tmp""") % context
)
2014-10-17 13:57:34 +00:00
self.append('diff -B -I"^\s*//" %(conf_path)s.tmp %(conf_path)s || UPDATED=1' % context)
2014-05-08 16:59:35 +00:00
self.append('mv %(conf_path)s.tmp %(conf_path)s' % context)
def commit(self):
""" reload bind if needed """
self.append(textwrap.dedent("""
# Apply changes
if [[ $UPDATED == 1 ]]; then
service bind9 reload
fi""")
)
2014-05-08 16:59:35 +00:00
def get_servers(self, domain, backend):
2015-03-04 21:06:16 +00:00
""" Get related server IPs from registered backend routes """
2015-04-05 10:46:24 +00:00
from orchestra.contrib.orchestration.manager import router
2015-04-07 15:14:49 +00:00
operation = Operation(backend, domain, Operation.SAVE)
servers = []
for route in router.objects.get_for_operation(operation):
2015-05-06 15:32:22 +00:00
servers.append(route.host.get_ip())
return servers
2015-05-05 19:42:55 +00:00
def get_masters_ips(self, domain):
ips = list(settings.DOMAINS_MASTERS)
if not ips:
2016-03-08 10:16:49 +00:00
ips += self.get_servers(domain, Bind9MasterDomainController)
return OrderedSet(sorted(ips))
2014-10-04 09:29:18 +00:00
def get_slaves(self, domain):
ips = []
2015-05-05 19:42:55 +00:00
masters_ips = self.get_masters_ips(domain)
records = domain.get_records()
# Slaves from NS
2015-05-05 19:42:55 +00:00
for record in records.by_type(Record.NS):
hostname = record.value.rstrip('.')
# First try with a DNS query, a more reliable source
try:
addr = socket.gethostbyname(hostname)
except socket.gaierror:
2015-05-05 19:42:55 +00:00
# check if hostname is declared
try:
2015-05-05 19:42:55 +00:00
domain = Domain.objects.get(name=hostname)
except Domain.DoesNotExist:
continue
else:
2015-05-05 19:42:55 +00:00
# default to domain A record address
addr = records.by_type(Record.A)[0].value
if addr not in masters_ips:
ips.append(addr)
# Slaves from internal networks
if not settings.DOMAINS_MASTERS:
2016-03-08 10:16:49 +00:00
for server in self.get_servers(domain, Bind9SlaveDomainController):
ips.append(server)
return OrderedSet(sorted(ips))
2014-10-04 09:29:18 +00:00
2014-05-08 16:59:35 +00:00
def get_context(self, domain):
2014-10-27 13:29:02 +00:00
slaves = self.get_slaves(domain)
2014-05-08 16:59:35 +00:00
context = {
'name': domain.name,
'zone_path': settings.DOMAINS_ZONE_PATH % {'name': domain.name},
2014-10-03 14:02:11 +00:00
'subdomains': domain.subdomains.all(),
2014-05-08 16:59:35 +00:00
'banner': self.get_banner(),
2014-10-27 13:29:02 +00:00
'slaves': '; '.join(slaves) or 'none',
'also_notify': '; '.join(slaves) + ';' if slaves else '',
'conf_path': self.CONF_PATH,
2015-04-05 18:02:36 +00:00
}
context['conf'] = textwrap.dedent("""\
2015-04-05 18:02:36 +00:00
zone "%(name)s" {
// %(banner)s
type master;
file "%(zone_path)s";
allow-transfer { %(slaves)s; };
also-notify { %(also_notify)s };
notify yes;
};""") % context
return context
2014-05-08 16:59:35 +00:00
2016-03-08 10:16:49 +00:00
class Bind9SlaveDomainController(Bind9MasterDomainController):
2015-04-23 19:46:23 +00:00
"""
Generate the configuartion for slave servers
It auto-discover the master server based on your routing configuration or you can use
DOMAINS_MASTERS to explicitly configure the master.
2015-04-24 11:39:20 +00:00
"""
CONF_PATH = settings.DOMAINS_SLAVES_PATH
2015-04-23 19:46:23 +00:00
2014-05-08 16:59:35 +00:00
verbose_name = _("Bind9 slave domain")
2014-09-24 20:09:41 +00:00
related_models = (
('domains.Domain', 'origin'),
)
2015-04-24 11:39:20 +00:00
doc_settings = (settings,
('DOMAINS_MASTERS', 'DOMAINS_SLAVES_PATH')
)
2014-05-08 16:59:35 +00:00
def save(self, domain):
context = self.get_context(domain)
self.update_conf(context)
def delete(self, domain):
context = self.get_context(domain)
self.delete_conf(context)
2014-10-04 09:29:18 +00:00
2014-05-08 16:59:35 +00:00
def commit(self):
self.append(textwrap.dedent("""
# Apply changes
2015-05-12 12:38:40 +00:00
if [[ $UPDATED == 1 ]]; then
# Async restart, ideally after master
2015-05-12 12:38:40 +00:00
nohup bash -c 'sleep 1 && service bind9 reload' &> /dev/null &
fi""")
2015-05-12 12:38:40 +00:00
)
2014-05-08 16:59:35 +00:00
def get_context(self, domain):
context = {
'name': domain.name,
'banner': self.get_banner(),
'subdomains': domain.subdomains.all(),
2015-05-05 19:42:55 +00:00
'masters': '; '.join(self.get_masters_ips(domain)) or 'none',
'conf_path': self.CONF_PATH,
2015-04-05 18:02:36 +00:00
}
context['conf'] = textwrap.dedent("""\
2015-04-05 18:02:36 +00:00
zone "%(name)s" {
// %(banner)s
type slave;
file "%(name)s";
masters { %(masters)s; };
allow-notify { %(masters)s; };
};""") % context
return context