#! /usr/bin/env python # -*- coding: utf-8 -*- """ Génération de la configuration pour bind9 Copyright (C) Valentin Samir Licence : GPLv3 """ import sys import ssl import time import base64 import hashlib import binascii import netaddr 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): def __init__(self, type, name, value, ttl=None): self._type=type self._name=name self._value=value self._ttl=ttl def __str__(self): if self._ttl: return "%s\t%s\tIN\t%s\t%s" % (self._name, self._ttl, self._type, self._value) else: return "%s\tIN\t%s\t%s" % (self._name, self._type, self._value) def __repr__(self): return str(self) class TLSA(ResourceRecord): def __init__(self, name, port, proto, cert, certtype, reftype, compat=True, 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 pem (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 """ selector = 0 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") dercert = ssl.PEM_cert_to_DER_cert(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, 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): """ 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): 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): def __init__(self, name, value, ttl=None): super(A, self).__init__('A', name, value, ttl) class DS(ResourceRecord): def __init__(self, name, value, ttl=None): super(DS, self).__init__('DS', name, value, ttl) class PTR(ResourceRecord): def __init__(self, name, value, ttl=None): super(PTR, self).__init__('PTR', name, value, ttl) class AAAA(ResourceRecord): def __init__(self, name, value, ttl=None): super(AAAA, self).__init__('AAAA', name, value, ttl) class TXT(ResourceRecord): def __init__(self, name, value, ttl=None): super(TXT, self).__init__('TXT', name, value, ttl) class CNAME(ResourceRecord): def __init__(self, name, value, ttl=None): super(CNAME, self).__init__('CNAME', name, value, ttl) class DNAME(ResourceRecord): def __init__(self, name, value, ttl=None): super(DNAME, self).__init__('DNAME', name, value, ttl) class MX(ResourceRecord): def __init__(self, name, priority, value, ttl=None): super(MX, self).__init__('MX', name, '%s\t%s' % (priority, value), ttl) class NS(ResourceRecord): def __init__(self, name, value, ttl=None): super(NS, self).__init__('NS', name, value, ttl) class SPF(ResourceRecord): def __init__(self, name, value, ttl=None): super(SPF, self).__init__('SPF', name, value, ttl) class SRV(ResourceRecord): def __init__(self, service, proto, priority, weight, port, target, ttl=None): super(SRV, self).__init__('SRV', '_%s._%s' % (service, proto), '%s\t%s\t%s\t%s' % (priority, weight, port, target), ttl) class NAPTR(ResourceRecord): 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): def __init__(self, name, hash, algo, key, ttl=None): if not hash in config.sshfp_hash.keys(): raise ValueError('Hash %s invalid, valid hash are %s' % (hash, ', '.join(config.sshfp_host.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[hash], getattr(hashlib, hash)(base64.b64decode(key)).hexdigest()), ttl) class ZoneBase(object): def __init__(self, zone_name): self._rrlist=[] self.zone_name = zone_name def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.zone_name) def __str__(self): ret="%s\n$ORIGIN %s.\n$TTL %s\n" % (disclamer.replace('//', ';'), self.zone_name, self.ttl) for rr in self._rrlist: ret+="%s\n" % rr return ret def add(self, rr): if isinstance(rr, ResourceRecord): self._rrlist.append(rr) else: raise ValueError("You can only add ResourceRecords to a Zone") def extend(self, rr_list): for rr in rr_list: self.add(rr) def write(self, path): with open(path, 'w') as f: f.write("%s" % self) class ZoneClone(ZoneBase): def __init__(self, zone_name, zone_clone, soa): super(ZoneClone, self).__init__(zone_name) self.zone_clone = zone_clone self.ttl = zone_clone.ttl self.add(soa) self.add(DNAME('', "%s." % self.zone_clone.zone_name)) for rr in self.zone_clone._rrlist[1:]: if rr._name in ['', '@']: self.add(rr) if rr._name in ["%s." % self.zone_clone.zone_name]: self.add(ResourceRecord(rr._type, "%s." % self.zone_name, rr._value)) class Zone(ZoneBase): def __init__(self, zone_name, ttl, soa, ns_list, ipv6=True, ipv4=True, 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): for zone in self.subzones: if str(hostname).endswith(".%s" % zone): return True return False def get_name(self, hostname): # le hostname fini bien par la zone courante, et il n'appartient pas à une sous-zone 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 add_delegation(zone, server): zone = self.het_name(zone) if zone: self.add(NS('@', '%s.' % server)) def add_a_record(self, nom, machine): if self.ipv4: for ip in machine.get('ipHostNumber', []): self.add(A(nom, ip)) if self.ipv6: if nom == '@': self.add(A("v4", ip)) else: self.add(A("%s.v4" % nom, ip)) def add_aaaa_record(self, nom, machine): if self.ipv6: for ip in machine.get('ip6HostNumber', []): if machine.get('dnsIpv6', [True])[0]: self.add(AAAA(nom, ip)) if self.ipv4: if nom == '@': self.add(AAAA("v6", ip)) else: self.add(AAAA("%s.v6" % nom, ip)) def add_sshfp_record(self, nom, machine): for sshkey in machine.get('sshFingerprint', []): try: algo_txt, key = str(sshkey).split()[:2] algo=config.sshfs_ralgo[algo_txt][1] for hash in config.sshfp_hash.keys(): self.add(SSHFP(nom, hash, algo, key)) if self.ipv4 and self.ipv6: if nom == '@': self.add(SSHFP("v4", hash, algo, key)) self.add(SSHFP("v6", hash, algo, key)) else: self.add(SSHFP("%s.v4" % nom, hash, algo, key)) self.add(SSHFP("%s.v6" % nom, 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_machine(self, machine): for host in machine['host']: 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) if machine['host']: for alias in machine.get('hostAlias', []): 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, to_zone = str(machine['host'][0]).split('.', 1) 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_zone == self.zone_name: self.add(CNAME(alias, "%s" % to_nom)) if self.ipv4 and self.ipv6: self.add(CNAME("%s.v4" % alias, "%s.v4" % to_nom)) self.add(CNAME("%s.v6" % alias, "%s.v6" % to_nom)) else: self.add(CNAME(alias, "%s." % machine['host'][0])) class ZoneReverse(Zone): def __init__(self, net, ttl, soa, ns_list): 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.""" n = netaddr.IPNetwork(net) a = netaddr.IPAddress(ip if ip else n.ip) rev_dns_a = a.reverse_dns.split('.')[:-1] assert a in n if n.version == 4: if n.prefixlen == 8: return ('.'.join(rev_dns_a[3:]), 3) elif n.prefixlen == 16: return ('.'.join(rev_dns_a[2:]), 2) elif n.prefixlen == 24: return ('.'.join(rev_dns_a[1:]), 1) else: raise ValueError("Bad network %s" % n) elif n.version == 6: return ('.'.join(rev_dns_a[(128-n.prefixlen)/4:]), (128-n.prefixlen)/4) @staticmethod def network_to_arpanets(nets): """ retourne une liste de reseaux ne contenant que des préfixes de taille 32, 24, 16 ou 8 en ipv4 et laisse inchangé les réseaux ipv6. """ if not isinstance(nets, list): nets = [nets] subnets = [] for net in nets: if not isinstance(net, netaddr.IPNetwork): net = netaddr.IPNetwork(net) 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)) elif net.version == 6: subnets.append(net) return subnets def add_machine(self, 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]) if zone != self.zone_name: continue if attr != 'ip6HostNumber' or machine.get('dnsIpv6', [True])[0]: # Hack pour envoyer le reverse vers l'adresse .v6 dans le cas où dnsIpv6 = False self.add(PTR(nom, '%s.' % machine['host'][0])) else: rev_nom, rev_zone = str(machine['host'][0]).split('.', 1) self.add(PTR(nom, '%s.v6.%s.' % (rev_nom, rev_zone))) except AssertionError: pass class dns(gen_config) : ######################################PARTIE DE CONFIGURATION ### Fichiers à écrire # Répertoire d'écriture des fichiers de zone DNS_DIR = '/etc/bind/generated/' # Avec un / à la fin DNSSEC_DIR = '/etc/bind/signed/' # Avec un / à la fin # Fichier de définition des zones pour le maître DNS_CONF = DNS_DIR + 'zones_crans' # Fichier de définition des zones pour les esclaves géré par BCfg2 DNS_CONF_BCFG2 = "/var/lib/bcfg2/Cfg/etc/bind/generated/zones_crans/zones_crans" ### 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('@',10, 'redisdead.crans.org.'), MX('@',20, 'ovh.crans.org.'), MX('@',25, 'freebox.crans.org.'), ] 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'), ] } NATPRs = { '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','565 8 2 498f6cd5bcf291aae4129700a7569fa6e9a86821185bd655f0b9efc6a3bf547e'), DS('ferme','35156 8 2 b63a1443b3d7434429e879e046bc8ba89056cdcb4b9c3566853e64fd521895b8'), DS('wifi','41320 8 2 024799c1d53f1e827f03d17bc96709b85ee1c05d77eb0ebeadcfbe207ee776a4'), DS('tv','30910 8 2 3317f684081867ab94402804fbb3cd187e29655cc7f34cb92c938183fe0b71f5'), ], } 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): xmpp_cert = ssl.get_server_certificate(('xmpp.crans.org', 443), ca_certs='/etc/ssl/certs/ca-certificates.crt') self.EXTRAS = { 'crans.org' : [ TLSA('crans.org.', 443, 'tcp', None, 3, 2), TLSA('www.crans.org.', 443, 'tcp', None, 3, 2), TLSA('cas.crans.org.', 443, 'tcp', None, 3, 2), TLSA('wiki.crans.org.', 443, 'tcp', None, 3, 2), TLSA('perso.crans.org.', 443, 'tcp', None, 3, 2), TLSA('intranet.crans.org.', 443, 'tcp', None, 3, 2), TLSA('intranet2.crans.org.', 443, 'tcp', None, 3, 2), TLSA('webmail.crans.org.', 443, 'tcp', None, 3, 2), TLSA('horde.crans.org.', 443, 'tcp', None, 3, 2), TLSA('roundcube.crans.org.', 443, 'tcp', None, 3, 2), TLSA('sogo.crans.org.', 443, 'tcp', None, 3, 2), TLSA('git.crans.org.', 443, 'tcp', None, 3, 2), TLSA('nagios.crans.org.', 443, 'tcp', None, 3, 2), TLSA('pad.crans.org.', 443, 'tcp', None, 3, 2), TLSA('news.crans.org.', 443, 'tcp', None, 3, 2), TLSA('asterisk.crans.org.', 5061, 'tcp', None, 3, 2), TLSA('smtp.crans.org.', 465, 'tcp', None, 3, 2), TLSA('imap.crans.org.', 993, 'tcp', None, 3, 2), TLSA('xmpp', 5222, 'tcp', xmpp_cert, 3, 2), TLSA('xmpp', 5269, 'tcp', xmpp_cert, 3, 2), TLSA('xmpp', 443, 'tcp', xmpp_cert, 3, 2), TLSA('jabber', 443, 'tcp', xmpp_cert, 3, 2), ], 'wifi.crans.org' : [ TLSA('wifi.crans.org.', 443, 'tcp', None, 3, 2), ], } super(dns, self).__init__(*args, **kwargs) def gen_soa(self, ns_list, serial, ttl): return SOA(ns_list[0], 'root.crans.org', serial, 21600, 3600, 1209600, ttl) def populate_zones(self, zones, machines): self.anim.iter=len(zones.values()) for zone in zones.values(): zone.extend(self.MXs) for rr_type in [self.SRVs, self.NATPRs, self.DSs, self.EXTRAS]: 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): for zone in zones_ldap: 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): for net in ZoneReverse.network_to_arpanets(zones_reverse_v4 + zones_reverse_v6): 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={}): for zone_clone, zones_alias in config.dns.zone_alias.items(): for zone in zones_alias: zones[zone]=ZoneClone(zone, zones[zone_clone], self.gen_soa(ns_list, serial, ttl)) for rr_type in [self.SRVs, self.NATPRs, self.DSs]: 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): zones = {} self.gen_zones_ldap(ttl, ns_list, serial, zones) self.gen_zones_reverse(ttl, ns_list, serial, zones) 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): 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]) 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(self.DNS_DIR + 'db.' + zone.zone_name) self.anim.reinit() print affich_tools.OK return zones def gen_master(self): # 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(self.DNS_DIR + 'db.' + zone.zone_name) if zone.zone_name in config.dns.zones_dnssec: zone_path = self.DNSSEC_DIR + 'db.' + zone.zone_name else: zone_path = self.DNS_DIR + 'db.' + zone.zone_name f.write(zone_template % {'zone_name' : zone.zone_name, 'zone_path' : zone_path}) def gen_slave(self): 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 = self.DNSSEC_DIR + 'db.' + zone.zone_name else: zone_path = self.DNS_DIR + 'db.' + 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)." c = dns() c.gen_slave() elif hostname == short_name(config.dns.DNSs[0]): print "Serveur maître :" c = dns() zones = c.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() p=subprocess.Popen(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE) ret=p.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 map(lambda fullhostname : short_name(fullhostname),config.dns.DNSs[1:]): print "Ce serveur est esclave! Lancez ce script sur %s, puis lancez bcfg2 ici" % bcfg2_main else: print "Ce serveur ne correspond à rien pour la configuration DNS."