#! /usr/bin/env python
# -*- coding: utf-8 -*-
""" Génération de la configuration pour bind9

Copyright (C) Valentin Samir
Licence : GPLv3
"""

import os
import sys
import ssl
import time
import base64
import hashlib
import binascii
import netaddr
if '/usr/scripts' not in sys.path:
    sys.path.append('/usr/scripts')

import lc_ldap.shortcuts
import affich_tools
from gestion.gen_confs import gen_config
from socket import gethostname
from gestion import config
import config.dns

disclamer = """//**************************************************************//
// Ce fichier est genere par les scripts de gen_confs           //
// Les donnees proviennent de la base LDAP et de la conf        //
// presente au debut du script. Il peut être propagé via bcfg2. //
//                                                              //
// NE PAS EDITER                                                //
//                                                              //
//**************************************************************//
"""

def short_name(fullhostname):
    return fullhostname.split(".")[0]

class ResourceRecord(object):
    """Classe standard définissant une ressource DNS"""

    def __init__(self, r_type, name, value, ttl=None):
        """Affecte les valeurs de base de l'enregistrement"""
        self.r_type = r_type
        self.name = name
        self.value = value
        self._ttl = ttl

    def __str__(self):
        """Retourne une chaîne printable dans un fichier bind"""
        if self._ttl:
            return "%s\t%s\tIN\t%s\t%s" % (self.name, self._ttl, self.r_type, self.value)
        else:
            return "%s\tIN\t%s\t%s" % (self.name, self.r_type, self.value)

    def __repr__(self):
        """__repr__ == __str__"""
        return str(self)

class TLSA(ResourceRecord):
    """Enregistrement TLSA pour stocker des certifs dans un enregistrement DNS"""

    def __init__(self, name, port, proto, cert, certtype, reftype, selector=0, compat=True, r_format='pem', ttl=None):
        """            name: nom du domaine du certificat
            port: port où écoute le service utilisant le certificat
            proto: udp ou tcp
            cert: le certificat au format ``format`` (pem ou der) (selector est donc toujours à 0)
            certtype: type d'enregistrement 0 = CA pinning, 1 = cert pinning, 2 = self trusted CA, 3 = self trusted cert
            reftype: 0 = plain cert, 1 = sha256, 2 = sha512
            compat: on génère un enregistement compris même par les serveurs dns n'implémentant pas TLSA
        """
        if not r_format in ['pem', 'der']:
            raise ValueError("format should be pem or der")

        if cert is None and proto == 'tcp' and name[-1] == '.':
            try:
                cert = ssl.get_server_certificate((name[:-1], port), ca_certs='/etc/ssl/certs/ca-certificates.crt')
            except Exception as e:
                raise ValueError("Unable de retrieve cert dynamically: %s" % e)
        elif cert is None:
            raise ValueError("cert can only be retrive if proto is tcp and name fqdn")

        if r_format is not 'der':
            dercert = ssl.PEM_cert_to_DER_cert(cert)
        else:
            dercert = cert

        if not dercert:
            raise ValueError("Impossible de convertir le certificat au format DER %s %s %s\n%s" % (name, port, proto, cert))

        certhex = TLSA.hashCert(reftype, str(dercert))
        if compat:
            super(TLSA, self).__init__(
                'TYPE52',
                '_%s._%s%s' % (port, proto, '.' + name if name else ''),
                "\# %s 0%s0%s0%s%s" % (len(certhex)/2 +3, certtype, selector, reftype, certhex),
                ttl
            )
        else:
            super(TLSA, self).__init__(
                'TLSA',
                '_%s._%s%s' % (port, proto, '.' + name if name else ''),
                "%s %s %s %s"% (certtype, selector, reftype, certhex),
                ttl
            )

    @staticmethod
    def hashCert(reftype, certblob):
        """Retourne un hash d'un certif DER en MAJUSCULES.
            certblob: un certificat au format DER
        """
        if reftype == 0:
            return binascii.b2a_hex(certblob).upper()
        elif reftype == 1:
            hashobj = hashlib.sha256()
            hashobj.update(certblob)
        elif reftype == 2:
            hashobj = hashlib.sha512()
            hashobj.update(certblob)
        else:
            raise ValueError("reftype sould be 0 1 or 2, not %s" % reftype)
        return hashobj.hexdigest().upper()

class SOA(ResourceRecord):
    """Ressource pour une entrée DNS SOA"""
    def __init__(self, master, email, serial, refresh, retry, expire, ttl):
        super(SOA, self).__init__('SOA', '@', '%s. %s. (\n    %s ; numero de serie\n    %s ; refresh (s)\n    %s ; retry (s)\n    %s ; expire (s)\n    %s ; TTL (s)\n    )' % (master, email, serial, refresh, retry, expire, ttl))

class A(ResourceRecord):
    """Entrée DNS pour une IPv4"""
    def __init__(self, name, value, ttl=None):
        super(A, self).__init__('A', name, value, ttl)

class DS(ResourceRecord):
    """Entrée DNS pour l'empreinte d'une clef DNSSEC"""
    def __init__(self, name, value, ttl=None):
        super(DS, self).__init__('DS', name, value, ttl)

class PTR(ResourceRecord):
    """Entrée DNS inverse (pour obtenir l'IP à partir du NDD"""
    def __init__(self, name, value, ttl=None):
        super(PTR, self).__init__('PTR', name, value, ttl)

class AAAA(ResourceRecord):
    """Entrée DNS pour une IPv6"""
    def __init__(self, name, value, ttl=None):
        super(AAAA, self).__init__('AAAA', name, value, ttl)

class TXT(ResourceRecord):
    """Entrée DNS pour un champ TXT"""
    def __init__(self, name, value, ttl=None):
        super(TXT, self).__init__('TXT', name, value, ttl)

    def __str__(self):
        """Retourne une chaîne printable dans un fichier bind"""
        if self._ttl:
            return '%s\t%s\tIN\t%s\t"%s"' % (self.name, self._ttl, self.r_type, self.value)
        else:
            return '%s\tIN\t%s\t"%s"' % (self.name, self.r_type, self.value)

class CNAME(ResourceRecord):
    """Entrée DNS pour un alias (toto -> redisdead)"""
    def __init__(self, name, value, ttl=None):
        super(CNAME, self).__init__('CNAME', name, value, ttl)

class DNAME(ResourceRecord):
    """Entrée DNS pour un alias de domaine (crans.eu -> crans.org)"""
    def __init__(self, name, value, ttl=None):
        super(DNAME, self).__init__('DNAME', name, value, ttl)

class MX(ResourceRecord):
    """Entrée DNS pour un serveur mail. crans.org IN MX 5 redisdead.crans.org veut dire
    que redisdead est responsable de recevoir les mails destinés à toto@crans.org avec
    une priorité 5 (plus c'est faible, plus c'est prioritaire.
    """
    def __init__(self, name, priority, value, ttl=None):
        super(MX, self).__init__('MX', name, '%s\t%s' % (priority, value), ttl)

class NS(ResourceRecord):
    """Entrée DNS pour donner les serveurs autoritaires pour un nom de domaine"""
    def __init__(self, name, value, ttl=None):
        super(NS, self).__init__('NS', name, value, ttl)

class SRV(ResourceRecord):
    """Entrée DNS pour les champs SRV"""
    def __init__(self, service, proto, priority, weight, port, target, ttl=None, subdomain=None):
        super(SRV, self).__init__('SRV', '_%s._%s' % (service, proto) + ('.%s' % subdomain if subdomain else ''), '%s\t%s\t%s\t%s' % (priority, weight, port, target), ttl)

class NAPTR(ResourceRecord):
    """Entrée DNS pour les NAPTR"""
    def __init__(self, name, order, preference, flag, service, replace_regexpr, value, ttl=None):
        super(NAPTR, self).__init__('NAPTR', name, '%s\t%s\t"%s"\t"%s"\t"%s"\t%s' % (order, preference, flag, service, replace_regexpr, value), ttl)

class SSHFP(ResourceRecord):
    """Entrée DNS stockant une fingerprint SSH"""
    def __init__(self, name, r_hash, algo, key, ttl=None):
        """Vérifie que hash/algo sont supportés dans la config"""
        if not r_hash in config.sshfp_hash.keys():
            raise ValueError('Hash %s invalid, valid hash are %s' % (r_hash, ', '.join(config.sshfp_hash.keys())))

        if not algo in config.sshfp_algo.keys():
            raise ValueError('Algo %s unknown, valid values are %s' % (algo, ', '.join(config.sshfp_algo.keys())))

        super(SSHFP, self).__init__('SSHFP', name, '%s\t%s\t%s' % (config.sshfp_algo[algo][0], config.sshfp_hash[r_hash], getattr(hashlib, r_hash)(base64.b64decode(key)).hexdigest()), ttl)

class ZoneBase(list):
    """Classe abstraite décrivant une zone.

    Elle surcharge une liste, car l'ensemble des enregistrements de cette
    zone sera contenu en elle-même."""
    def __init__(self, zone_name):
        """Affecte un nom de zone"""
        super(ZoneBase, self).__init__()
        self.zone_name = zone_name
        self.ttl = 3600

    def __repr__(self):
        return "<%s %s>" % (self.__class__.__name__, self.zone_name)

    def __str__(self):
        """Version enregistrable en fichier d'une zone."""
        _ret = "%s\n$ORIGIN %s.\n$TTL %s\n" % (disclamer.replace('//', ';'), self.zone_name, self.ttl)
        for rr in self:
            _ret += "%s\n" % rr
        return _ret

    def add(self, rr):
        """Ajout d'un enregistrement DNS"""
        if isinstance(rr, ResourceRecord):
            self.append(rr)
        else:
            raise ValueError("You can only add ResourceRecords to a Zone")

    def write(self, path):
        """Pour dumper le tout dans le fichier idoine."""
        with open(path, 'w') as f:
            f.write("%s" % self)

class ZoneClone(ZoneBase):
    """Zone clone d'une autre zone."""
    def __init__(self, zone_name, zone_clone, soa):
        """La zone clone possède, outre son nom, un pointeur vers
        la zone qu'elle duplique.

        Le SOA est fourni manuellement, et la première entrée de la zone clonée
        est ignorée. (c'est a priori le SOA de celle-ci)
        """
        super(ZoneClone, self).__init__(zone_name)
        self.zone_clone = zone_clone
        self.ttl = zone_clone.ttl

        # On met un SOA custom.
        self.add(soa)

        # On ajoute un DNAME, qui indique que la zone est un clone.
        self.add(DNAME('', "%s." % self.zone_clone.zone_name))

        # Et on extrait les données nécessaires de la zone clônée
        # à savoir, celles de l'apex (la base du domaine, qui elle
        # n'est pas clônée, seuls les sous-domaines le sont)
        for rr in self.zone_clone[1:]:
            # Si pas de nom ou si le nom est @, on duplique bêtement l'enregistrement
            if rr.name in ['', '@']:
                self.add(rr)

            # Si le nom de domaine concerné est celui de la zone clonée, pareil, on
            # "duplique", en créant un enregistrement idoine.
            if rr.name in ["%s." % self.zone_clone.zone_name]:
                self.add(ResourceRecord(rr.r_type, "%s." % self.zone_name, rr.value))


class Zone(ZoneBase):
    """Une zone standard"""
    def __init__(self, zone_name, ttl, soa, ns_list, ipv6=True, ipv4=True, other_zones=None):
        """Héritage, plus quelques propriétés en plus

        On définit ici si la zone comporte des ipv4/ipv6,
        ainsi que des données utiles pour le comportement de celles-ci.

        other_zones contient la liste de sous-zones "indépendantes".
        (exemple avec wifi.crans.org qui est une sous-zone de crans.org)"""

        if other_zones is None:
            other_zones = []
        super(Zone, self).__init__(zone_name)
        self.ttl = ttl
        self.ipv4 = ipv4
        self.ipv6 = ipv6
        self.other_zones = other_zones
        self.subzones = [z for z in self.other_zones if z != self.zone_name and z.endswith(self.zone_name)]

        self.add(soa)
        for ns in ns_list:
            self.add(NS('@', '%s.' % ns))

    def name_in_subzone(self, hostname):
        """Teste si le nom qu'on observe est dans une
        sous-zone (toto.wifi.crans.org. est dans wifi.crans.org., et non
        dans crans.org..
        """
        for zone in self.subzones:
            if str(hostname).endswith(".%s" % zone):
                return True
        return False

    def get_name(self, hostname):
        """Retourne la base du nom d'un hôte. Teste si celui-ci appartient bien
        à la zone courante et s'il n'est pas lié à une sous-zone.

        Si tout est bon, le nom peut valoir "", auquel cas, l'entrée concerne le domaine
        courant, donc @.

        Dans le cas où ce nom ne devrait pas être là, on retourne None.
        """
        if str(hostname) == self.zone_name or str(hostname).endswith(".%s" % self.zone_name) and not self.name_in_subzone(hostname):
            ret = str(hostname)[0:-len(self.zone_name)-1]
            if ret == "":
                return "@"
            else:
                return ret
        else:
            return None

    def get_name_vi(self, nom, i):
        """Kludge foireux pour retourner toto.v4.crans.org à partir
        de toto.crans.org (sous-zones v4/v6)."""
        if not i in [4, 6]:
            raise ValueError("i should be 4 or 6")
        if nom == '@':
            return 'v%s' % i
        # On considère que le "vrai" nom est la partie avant le premier .
        elif '.' in nom:
            nom_1, nom_2 = nom.split('.', 1)
            return "%s.v%s.%s" % (nom_1, i, nom_2)
        else:
            return "%s.v%s" % (nom, i)

    def add_delegation(self, zone, server):
        """Lorsqu'on veut offrir une délégation DNS à une machine
        pour un nom de domaine"""
        zone = self.get_name(zone)
        if zone:
            self.add(NS('@', '%s.' % server))

    def add_a_record(self, nom, machine):
        """Ajout d'une entrée A."""
        # Fait-on de l'IPv4 dans cette zone ?
        if self.ipv4:
            for ip in machine.get('ipHostNumber', []):
                self.add(A(nom, ip))
                # Fait-on aussi de l'IPv6 ?
                if self.ipv6:
                    # Bon bah alors on ajoute nom.v4.crans.org en plus.
                    self.add(A(self.get_name_vi(nom, 4), ip))

    def add_aaaa_record(self, nom, machine):
        """Ajout d'une entrée AAAA (for the AAAAAAAAwesome)."""
        # Fait-on de l'IPv6 dans cette zone ?
        if self.ipv6:
            for ip in machine.get('ip6HostNumber', []):
                # Si dnsIpv6 est à True dans la base LDAP, on ajoute l'entrée.
                # On l'ajoute quand même si la zone ne fait pas d'IPv4, parce que
                # ça semble assez dommage d'avoir une machine qui a une IPv6, pas
                # d'IPv4, et pas d'entrée DNS pour la contacter, non mais oh.
                dnsipv6 = machine.get('dnsIpv6', [True])[0]
                if dnsipv6 or not self.ipv4:
                    self.add(AAAA(nom, ip))
                # Si on fait aussi de l'IPv4...
                if self.ipv4:
                    self.add(AAAA(self.get_name_vi(nom, 6), ip))

    def add_sshfp_record(self, nom, machine):
        """Ajoute une fingerprint SSH"""
        for sshkey in machine.get('sshFingerprint', []):
            try:
                algo_txt, key = str(sshkey).split()[:2]
                algo = config.sshfs_ralgo[algo_txt][1]
                for r_hash in config.sshfp_hash.keys():
                    self.add(SSHFP(nom, r_hash, algo, key))
                    if self.ipv4:
                        self.add(SSHFP(self.get_name_vi(nom, 4), r_hash, algo, key))
                    if self.ipv6:
                        self.add(SSHFP(self.get_name_vi(nom, 6), r_hash, algo, key))
            # KeyError is l'algo dans ldap n'est pas connu
            # TypeError si la clef n'est pas bien en base64
            except (KeyError, TypeError):
                pass

    def add_tlsa_record(self, cert):
        """Ajout d'un certif dans le DNS"""
        if 'TLSACert' in cert['objectClass']:
            for host in cert['hostCert']:
                nom = self.get_name(host)
                if nom is None: continue
                for port in cert['portTCPin']:
                    self.add(TLSA(nom, port, 'tcp', cert['certificat'][0], cert['certificatUsage'][0], cert['matchingType'][0], cert['selector'][0], r_format='der'))
                for port in cert['portUDPin']:
                    self.add(TLSA(nom, port, 'udp', cert['certificat'][0], cert['certificatUsage'][0], cert['matchingType'][0], cert['selector'][0], r_format='der'))

    def add_machine(self, machine):
        """Ajout d'une machine, à savoir chaînage d'ajout
        d'IP, d'IPv6, de fingerprint et de TLSA, pour chaque
        entrée "host" dans la base LDAP."""
        for host in machine['host']:
            # Le nom peut être None (machine appartenant à une sous-zone, ou à une autre zone)
            nom = self.get_name(host)
            if nom is None:
                continue

            self.add_a_record(nom, machine)
            self.add_aaaa_record(nom, machine)
            self.add_sshfp_record(nom, machine)
            for cert in machine.certificats():
                self.add_tlsa_record(cert)

        # Si la machine a bien un nom en "host", on lui ajoute aussi
        # les aliases, sous forme de CNAME vers le premier nom.
        if machine['host']:
            for alias in machine.get('hostAlias', []):
                # Si l'alias pointe dans une autre zone, on passe. (ça sera fait quand on refera le add_machine
                # en toutnant dans la sous-zone
                if str(alias) in self.other_zones and str(alias) != self.zone_name:
                    continue

                alias = self.get_name(alias)
                if alias is None:
                    continue

                to_nom = self.get_name(machine['host'][0])

                # Si l'alias est sur le nom de la zone, il faut ajouter
                # des entrées standard.
                if alias in ['@', '%s.' % self.zone_name]:
                    self.add_a_record(alias, machine)
                    self.add_aaaa_record(alias, machine)
                    self.add_sshfp_record(alias, machine)
                elif to_nom:
                    self.add(CNAME(alias, "%s" % to_nom))
                    if self.ipv4 and self.ipv6:
                        self.add(CNAME(self.get_name_vi(alias, 6), self.get_name_vi(to_nom, 6)))
                        self.add(CNAME(self.get_name_vi(alias, 4), self.get_name_vi(to_nom, 4)))
                # Ne devrait pas arriver.
                else:
                    self.add(CNAME(alias, "%s." % machine['host'][0]))


class ZoneReverse(Zone):
    """Zone inverse, listant des PTR (toto.crans.org IN PTR 138.231...)"""
    def __init__(self, net, ttl, soa, ns_list):
        """Initialise une zone reverse.
        net est un truc de la forme fe80::/64, ou 138.231.136.0/24
        En v4, il faut que net soit un /32, un /24, un /16 ou un /8
        En gros, il faut que network_to_arpanets retourne une liste à un élément."""

        # Comme dit, liste à un élément.
        if len(ZoneReverse.network_to_arpanets(net)) != 1:
            raise ValueError("%s n'est pas un réseau valide pour une zone de reverse dns" % net)

        self.net = net
        zone_name = ZoneReverse.reverse(net)[0]
        if '.' in net:
            ipv6 = False
            ipv4 = True
        elif ':' in net:
            ipv6 = True
            ipv4 = False
        else:
            raise ValueError("net should be an ipv4 ou ipv6 network")

        super(ZoneReverse, self).__init__(zone_name, ttl, soa, ns_list, ipv6=ipv6, ipv4=ipv4)

    @staticmethod
    def reverse(net, ip=None):
        """Renvoie la zone DNS inverse correspondant au réseau et à
        l'adresse donnés, ainsi que le nombre d'éléments de l'ip a
        mettre dans le fichier de zone si elle est fournie, n'importe
        quoi sinon."""
        # Initialise la plage d'IP à partir de net
        _network = netaddr.IPNetwork(net)
        # Prend la première adresse ip de la plage, sauf si une est fournie
        _address = netaddr.IPAddress(ip if ip else _network.ip)
        # retourne le reverse splitté. (un reverse ressemble à 0.136.231.138.in-addr.arpa.)
        rev_dns_a = _address.reverse_dns.split('.')[:-1]

        # Si la config est foireuse (donc si on a fourni une IP hors de la plage, ça
        # va planter ici.
        assert _address in _network

        # En v4, le reverse étant de la forme 0.136.231.138.in-addr.arpa., soit
        # on a un /8, soit un /16, soit un /24.
        if _network.version == 4:
            if _network.prefixlen == 8:
                return ('.'.join(rev_dns_a[3:]), 3)
            elif _network.prefixlen == 16:
                return ('.'.join(rev_dns_a[2:]), 2)
            elif _network.prefixlen == 24:
                return ('.'.join(rev_dns_a[1:]), 1)
            else:
                raise ValueError("Bad network %s" % _network)
        # En v6 c'est plus calme.
        # Le reverse a cette tronche : 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f.ip6.arpa.
        # Du coup c'est aussi fin qu'on le souhaite.
        elif _network.version == 6:
            return ('.'.join(rev_dns_a[(128 - _network.prefixlen)/4:]), (128 - _network.prefixlen)/4)


    @staticmethod
    def network_to_arpanets(nets):
        """Dans reverse(net, ip), on a constaté qu'en v4, on ne pouvait définir
        que des plages reverse en /24, /16 ou /8. Cette fonction vise à retourner
        une liste des plages en tenant compte de ce critère (donc de taille
        32/24/16/8)

        Ne touche à rien pour l'IPv6.
        """
        if not isinstance(nets, list):
            nets = [nets]
        subnets = []
        for net in nets:
            if not isinstance(net, netaddr.IPNetwork):
                net = netaddr.IPNetwork(net)
            # Si on est en v4, on fragmente les subnets
            # dans les tailles qui vont bien.
            if net.version == 4:
                if net.prefixlen > 24:
                    subnets.extend(net.subnet(32))
                elif net.prefixlen > 16:
                    subnets.extend(net.subnet(24))
                elif net.prefixlen > 8:
                    subnets.extend(net.subnet(16))
                else:
                    subnets.extend(net.subnet(8))
            # En v6 c'est tout pété.
            elif net.version == 6:
                subnets.append(net)
        return subnets

    def add_machine(self, machine):
        """Ajout d'un reverse pour une machine."""
        if machine['host']:
            if self.ipv4:
                attr = 'ipHostNumber'
            elif self.ipv6:
                attr = 'ip6HostNumber'
            else:
                raise ValueError("A reverse zone should be ipv6 or ipv6")

            for ip in machine[attr]:
                try:
                    zone, length = ZoneReverse.reverse(self.net, str(ip))
                    nom = '.'.join(ip.value.reverse_dns.split('.')[:length])

                    # La zone retournée n'est pas le nom de la zone. A priori
                    # on aurait dû tomber en AssertionError.
                    if zone != self.zone_name:
                        continue

                    if attr != 'ip6HostNumber' or machine.get('dnsIpv6', [True])[0]:
                        self.add(PTR(nom, '%s.' % machine['host'][0]))
                    # Gros kludge pour ajouter le reverse vers le .v6 quand on est sur
                    # une reverse v6 et que dnsIpv6 est faux.
                    else:
                        rev_nom, rev_zone = str(machine['host'][0]).split('.', 1)
                        self.add(PTR(nom, '%s.v6.%s.' % (rev_nom, rev_zone)))
                except AssertionError:
                    # L'ip n'est pas dans la zone reverse, donc on continue silencieusement.
                    pass


class dns(gen_config):
    """Classe de configuration du DNS (les services, generate, toussa)"""
    ######################################PARTIE DE CONFIGURATION

    ### Fichiers à écrire
    # Répertoire d'écriture des fichiers de zone
    DNS_DIR = config.dns.DNS_DIR
    DNSSEC_DIR = config.dns.DNSSEC_DIR
    # Fichier de définition des zones pour le maître
    DNS_CONF = config.dns.DNS_CONF

    # Fichier de définition des zones pour les esclaves géré par BCfg2
    DNS_CONF_BCFG2 = config.dns.DNS_CONF_BCFG2

    ### Liste DNS
    # Le premier doit être le  maitre

    ### Liste des délégations de zone
    # Pour les demandes de ces zones, le DNS dira d'aller voir les serveurs listés ici
    # Pour les noms des serveurs on met le nom sans point à la fin
    # { nom_de_zone : [ ns1, ns2, ...]
    DELEG = {}

    ### Serveurs de mail
    # format : [ priorité serveur , .... ]
    MXs = [MX('@', config.dns.MXs[_mx].get('prio', 25), "%s." %_mx) for _mx in config.dns.MXs]

    SRVs = {
        'crans.org': [
            SRV('jabber', 'tcp', 5, 0, 5269, 'xmpp'),
            SRV('xmpp-server', 'tcp', 5, 0, 5269, 'xmpp'),
            SRV('xmpp-client', 'tcp', 5, 0, 5222, 'xmpp'),
            SRV('sip', 'udp', 5, 0, 5060, 'asterisk'),
            SRV('sip', 'tcp', 5, 0, 5060, 'asterisk'),
            SRV('sips', 'tcp', 5, 0, 5061, 'asterisk'),
            SRV('stun', 'udp', 5, 0, 3478, 'asterisk'),

            # Quelques ancien utilisent le server XMPP avec des addresses de la forme
            # login@jabber.crans.org, aussi les clients XMPP et autres serveurs de la
            # fédération veulent ces ResourceRecord
            SRV('jabber', 'tcp', 5, 0, 5269, 'xmpp', subdomain="jabber"),
            SRV('xmpp-server', 'tcp', 5, 0, 5269, 'xmpp', subdomain="jabber"),
            SRV('xmpp-client', 'tcp', 5, 0, 5222, 'xmpp', subdomain="jabber"),
        ],
    }

    SPFs = {
        'crans.org': [
            TXT('@', 'v=spf1 mx ~all'),
        ],
    }

    NON_CLONABLE_SPFs = {
        'crans.org': [
            TXT(short_name(_mx), 'v=spf1 mx:crans.org ~all') for _mx in config.dns.MXs
        ],
    }


    NAPTRs = {
        'crans.org' : [
            NAPTR('@', 5, 100, "S", "SIPS+D2T", "", '_sips._tcp.crans.org.', ttl=86400),
            NAPTR('@', 10, 100, "S", "SIP+D2U", "", '_sip._udp.crans.org.', ttl=86400),
            NAPTR('@', 15, 100, "S", "SIP+D2T", "", '_sip._tcp.crans.org.', ttl=86400),
        ],
    }

    # DS à publier dans zone parentes : { parent : [ zone. TTL IN DS key_id algo_id 1 hash ] }
    # ex : { 'crans.eu' :  ['wifi.crans.eu.             86400   IN      DS      33131 8 1 3B573B0E2712D8A8B1B0C3'] }
    # /!\ Il faut faire attention au rollback des keys, il faudrait faire quelque chose d'automatique avec opendnssec
    DSs = {
        'crans.org': [
            DS('adm', '64649 8 2 9c45f0fef063672d96c983d5a3813a08a649c72d357f41ddece73ae8872d60cf'),
            DS('wifi', '5531  8 2 daf30a647566234edc1617546fd74abbbaf965b17389248f72fc66a33d6f5063'),
            DS('tv', '18199 8 2 d3cc2f5f81b830cbb8894ffd32c236e968edd3b0c0305112b6eb970aa763418e'),
        ],
    }


    hostname = short_name(gethostname())
    serial = int(time.time()) + 1000000000
    TTL = 3600

    if hostname == short_name(config.dns.DNSs[0]):
        restart_cmd = '/usr/sbin/ods-signer sign --all && /etc/init.d/bind9 reload'
    else:
        restart_cmd = '/etc/init.d/bind9 reload'

    def __init__(self, *args, **kwargs):
        """Surcharge pour affecter EXTRAS"""
        self.EXTRAS = {}
        self.anim = None
        super(dns, self).__init__(*args, **kwargs)

    def gen_soa(self, ns_list, serial, ttl):
        """Génère l'enregistrement SOA pour le domaine"""
        return SOA(ns_list[0], 'root.crans.org', serial, 21600, 3600, 1209600, ttl)

    def populate_zones(self, zones, machines):
        """On peuple les fichiers de zones"""
        self.anim.iter = len(zones.values())
        for zone in zones.values():
            # On met les mêmes MX pour toutes les zones.
            zone.extend(self.MXs)
            # Les RR définis ici sont ajoutés aux zones idoines, de façon à se simplifier la vie.
            for rr_type in [self.SRVs, self.NAPTRs, self.DSs, self.EXTRAS, self.SPFs, self.NON_CLONABLE_SPFs]:
                if zone.zone_name in rr_type.keys():
                    zone.extend(rr_type[zone.zone_name])
            for m in machines:
                zone.add_machine(m)
            self.anim.cycle()
        return zones

    def gen_zones_ldap(self, ttl, ns_list, serial, zones={}, zones_ldap=config.dns.zones_ldap):
        """On génère la liste des zones ldap, à partir de config.dns. C'est un peu ici que tout commence.
        Le dico zones passé en argument est modifié en place."""
        for zone in zones_ldap:
            # On crée la zone et on l'ajoute au dico.
            zones[zone] = Zone(zone, ttl, self.gen_soa(ns_list, serial, ttl), ns_list, other_zones=config.dns.zones_direct)
        return zones

    def gen_zones_reverse(self, ttl, ns_list, serial, zones={},
                          zones_reverse_v4=config.dns.zones_reverse,
                          zones_reverse_v6=config.dns.zones_reverse_v6):
        """Deuxième gros morceau, les reverses, pareil, on peuple depuis config.dns, et on crée toutes les zones
        idoines. Pareil, ici, le dico zones est modifié en place"""
        for net in ZoneReverse.network_to_arpanets(zones_reverse_v4 + zones_reverse_v6):
            # On crée la zone et on l'ajoute au dico.
            zones[str(net)] = ZoneReverse(str(net), ttl, self.gen_soa(ns_list, serial, ttl), ns_list)
        return zones

    def gen_zones_clone(self, ttl, ns_list, serial, zones={}):
        """Les clônes, à savoir crans.eu et cie, dico zones modifié en place."""
        for zone_clone, zones_alias in config.dns.zone_alias.iteritems():
            for zone in zones_alias:
                # On crée la zone et on l'ajoute au dico.
                zones[zone] = ZoneClone(zone, zones[zone_clone], self.gen_soa(ns_list, serial, ttl))
                # Et on ajoute les enregistrements concernant la zone clône (pas la clônée, ça
                # a déjà été fait à l'init) à la main.
                for rr_type in [self.SRVs, self.NAPTRs, self.DSs, self.SPFs]:
                    if zones[zone].zone_name in rr_type.keys():
                        zones[zone].extend(rr_type[zones[zone].zone_name])
        return zones

    def gen_zones(self, ttl, serial, ns_list, populate=True):
        """On chaîne les différents gen_zones_*"""
        zones = {}
        self.gen_zones_ldap(ttl, ns_list, serial, zones)
        self.gen_zones_reverse(ttl, ns_list, serial, zones)

        # Si populate, on remplit les zones avec les enregistrements \o/
        if populate:
            conn = lc_ldap.shortcuts.lc_ldap_admin()
            machines = conn.search(u"mid=*", sizelimit=10000)
            machines.extend(conn.machinesMulticast())
            self.populate_zones(zones, machines)

        # Doit être fait après populate_zones lorsque l'on a l'intention d'écrire les fichiers de zone
        # En effet, la génération de la zone clone dépend du contenue de la zone originale
        self.gen_zones_clone(ttl, ns_list, serial, zones)
        return zones

    def gen_tv(self, populate=True):
        """Génération de la TV, un peu à part."""
        self.anim = affich_tools.anim('\tgénération de la zone tv')
        zones = {}
        serial = self.serial
        self.gen_zones_reverse(self.TTL, config.dns.DNSs, serial, zones, zones_reverse_v4=config.NETs['multicast'], zones_reverse_v6=[])
        self.gen_zones_ldap(self.TTL, config.dns.DNSs, serial, zones, zones_ldap=[config.dns.zone_tv])

        # Pareil, si on doit peupler on ajoute ce qu'il faut niveau machines.
        if populate:
            conn = lc_ldap.shortcuts.lc_ldap_admin()
            machines = conn.machinesMulticast()
            machines.extend(conn.search(u'(|(host=%s)(host=*.%s)(hostAlias=%s)(hostAlias=*.%s))' % ((config.dns.zone_tv,)*4)))
            self.populate_zones(zones, machines)

            for zone in zones.values():
                zone.write(os.path.join(self.DNS_DIR, 'db.%s' % (zone.zone_name,)))

        self.anim.reinit()
        print affich_tools.OK
        return zones

    def gen_master(self):
        """Pour le serveur maître.

        Appelle gen_zones puis écrit les fichiers."""
        # Syntaxe utilisée dans le fichier DNS_CONF pour définir une zone sur le maître
        zone_template = """
zone "%(zone_name)s" {
    type master;
    file "%(zone_path)s";
};
"""
        zones = self.gen_zones(self.TTL, self.serial, config.dns.DNSs)
        with open(self.DNS_CONF, 'w') as f:
            f.write(disclamer)
            for zone in zones.values():
                zone.write(os.path.join(self.DNS_DIR, 'db.%s' % (zone.zone_name,)))
                if zone.zone_name in config.dns.zones_dnssec:
                    zone_path = os.path.join(self.DNSSEC_DIR, 'db.%s' % (zone.zone_name,))
                else:
                    zone_path = os.path.join(self.DNS_DIR, 'db.%s' % (zone.zone_name,))
                f.write(zone_template % {'zone_name' : zone.zone_name, 'zone_path' : zone_path})

    def gen_slave(self):
        """Pour les slaves, fait l'écriture de la conf dans bcfg2, mais on ne peuple rien !
        On ne fait qu'écrire le fichier zone_crans."""
        zone_template = """
zone "%(zone_name)s" {
    type slave;
    file "%(zone_path)s";
    masters { %(master_ip)s; };
};
"""
        zones = self.gen_zones(self.TTL, self.serial, config.dns.DNSs, populate=False)
        with open(self.DNS_CONF_BCFG2, 'w') as f:
            f.write(disclamer)
            for zone in zones.values():
                if zone.zone_name in config.dns.zones_dnssec:
                    zone_path = os.path.join(self.DNSSEC_DIR, 'db.%s' % (zone.zone_name,))
                else:
                    zone_path = os.path.join(self.DNS_DIR, 'db.%s' % (zone.zone_name,))
                f.write(zone_template % {'zone_name' : zone.zone_name, 'zone_path' : zone_path, 'master_ip' : config.dns.master})

    def _gen(self):
        self.gen_master()

    def __str__(self):
        return "DNS"


if __name__ == '__main__':
    HOSTNAME = short_name(gethostname())
    if HOSTNAME == short_name(config.bcfg2_main):
        print "Reconfiguration du fichier de BCfg2 pour configurer le bind d'un serveur en esclave (pensez à lancer bcfg2 sur les esclaves)."
        CONFIG = dns()
        CONFIG.gen_slave()
    elif HOSTNAME == short_name(config.dns.DNSs[0]):
        print "Serveur maître :"
        CONFIG = dns()
        ZONES = CONFIG.gen_tv()
        import subprocess
        for ZONE in ZONES.values():
            if ZONE.zone_name in config.dns.zones_dnssec:
                ARGS = ("/usr/sbin/ods-signer sign %s" % ZONE.zone_name).split()
                PROCESS = subprocess.Popen(ARGS, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                RET = PROCESS.communicate()
                print RET[0].strip()
                if RET[1].strip():
                    print RET[1].strip()
        print "Ce serveur est également serveur maitre pour les autres zones dns, mais leur reconfiguration se fait par generate."
    elif HOSTNAME in [short_name(FULLHOSTNAME) for FULLHOSTNAME in config.dns.DNSs[1:]]:
        print "Ce serveur est esclave! Lancez ce script sur %s, puis lancez bcfg2 ici" % (config.bcfg2_main,)
    else:
        print "Ce serveur ne correspond à rien pour la configuration DNS."


