ascension

Migrate DNS zones to the GNU Name System
Log | Files | Refs | README | LICENSE

ascension.py (30698B)


      1 #!/usr/bin/env python3
      2 # This file is part of Ascension.
      3 # Copyright (C) 2019 GNUnet e.V.
      4 #
      5 # Ascension is free software: you can redistribute it and/or modify it
      6 # under the terms of the GNU Affero General Public License as published
      7 # by the Free Software Foundation, either version 3 of the License,
      8 # or (at your option) any later version.
      9 #
     10 # Ascension is distributed in the hope that it will be useful, but
     11 # WITHOUT ANY WARRANTY; without even the implied warranty of
     12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     13 # Affero General Public License for more details.
     14 #
     15 # You should have received a copy of the GNU Affero General Public License
     16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 #
     18 # SPDX-License-Identifier: AGPL3.0-or-later
     19 #
     20 # Author rexxnor
     21 """
     22 Usage:
     23     ascension <domain> [-d] [-p] [-s] [--minimum-ttl=<ttl>] [--dry-run]
     24     ascension <domain> <port> [-d] [-p] [-s] [--minimum-ttl=<ttl>]
     25     ascension <domain> -n <transferns> [-d] [-p] [-s] [--minimum-ttl=<ttl>] [--dry-run]
     26     ascension <domain> -n <transferns> <port> [-d] [-p] [-s] [--minimum-ttl=<ttl>] [--dry-run]
     27     ascension -p | --public
     28     ascension -s | --debug
     29     ascension -s | --standalone
     30     ascension -h | --help
     31     ascension -v | --version
     32 
     33 Options:
     34     <domain>              Domain to migrate
     35     <port>                Port for zone transfer
     36     <transferns>          DNS Server that does the zone transfer
     37     --minimum-ttl=<ttl>   Minimum TTL for records to migrate [default: 3600]
     38     --dry-run             Only try if a zone transfer is allowed
     39     -p --public           Make records public on the DHT
     40     -s --standalone       Run ascension once
     41     -d --debug            Enable debugging
     42     -h --help         Show this screen.
     43     -v --version      Show version.
     44 """
     45 
     46 # imports
     47 import logging
     48 import queue
     49 import re
     50 import socket
     51 import sys
     52 import time
     53 import subprocess as sp
     54 import threading
     55 import dns.query
     56 import dns.resolver
     57 import dns.zone
     58 import docopt
     59 
     60 # GLOBALS
     61 GNUNET_ZONE_CREATION_COMMAND = 'gnunet-identity'
     62 GNUNET_NAMESTORE_COMMAND = 'gnunet-namestore'
     63 GNUNET_GNS_COMMAND = 'gnunet-gns'
     64 GNUNET_ARM_COMMAND = 'gnunet-arm'
     65 # This is the list of record types Ascension (and GNS) currently
     66 # explicitly supports.  Record types we encounter that are not
     67 # in this list and not in the OBSOLETE_RECORD_TYPES list will
     68 # create a warning (information loss during migration).
     69 SUPPORTED_RECORD_TYPES = [
     70     "A", "AAAA", "NS", "MX", "SRV", "TXT", "CNAME",
     71 ]
     72 # Record types that exist in DNS but that won't ever exist in GNS
     73 # as they are not needed anymore (so we should not create a warning
     74 # if we drop one of these).
     75 OBSOLETE_RECORD_TYPES = [
     76     "PTR",
     77     "SIG", "KEY",
     78     "RRSIG", "NSEC", "DNSKEY", "NSEC3", "NSEC3PARAM", "CDNSKEY",
     79     "TKEY", "TSIG",
     80     "TA", "DLV",
     81 ]
     82 
     83 class Ascender():
     84     """
     85     Class that provides migration for any given domain
     86     """
     87     def __init__(self,
     88                  domain: str,
     89                  transferns: str,
     90                  port: str,
     91                  flags: str,
     92                  minimum: str) -> None:
     93         self.domain = domain
     94         if domain[-1] == '.':
     95             self.domain = self.domain[:-1]
     96         self.port = int(port)
     97         self.transferns = transferns
     98         self.soa = None
     99         self.tld = self.domain.split(".")[::-1][0]
    100         self.zone = None
    101         self.zonegenerator = None
    102         self.flags = flags
    103         self.minimum = int(minimum)
    104         self.subzonedict = dict()
    105 
    106     def bootstrap_zone(self) -> None:
    107         """
    108         Creates the zone in gnunet
    109         """
    110         try:
    111             ret = sp.run([GNUNET_ZONE_CREATION_COMMAND,
    112                           '-C', self.domain])
    113             logging.info("executed command: %s", " ".join(ret.args))
    114         except sp.CalledProcessError:
    115             logging.info("Zone %s already exists!", self.domain)
    116 
    117     def get_dns_zone_serial(self,
    118                             domain: str,
    119                             resolver=None) -> int:
    120         """
    121         Gets the current serial for a given zone
    122         :param domain: Domain to query for in DNS
    123         :param resolver: Nameserver to query in DNS, defaults to None
    124         :returns: Serial of the zones SOA record
    125         """
    126         # Makes domains better resolvable
    127         domain = domain + "."
    128         # SOA is different if taken directly from SOA record
    129         # compared to AXFR/IXFR - changed to respect this
    130         try:
    131             soa_answer = dns.resolver.query(domain, 'SOA')
    132             master_answer = dns.resolver.query(soa_answer[0].mname, 'A')
    133         except dns.resolver.NoAnswer:
    134             logging.warning("The domain '%s' is not publicly resolvable.",
    135                             domain)
    136         except dns.resolver.NXDOMAIN:
    137             logging.warning("The domain '%s' is not publicly resolvable.",
    138                             domain)
    139         except Exception:
    140             logging.warning("The domain '%s' is not publicly resolvable.",
    141                             domain)
    142 
    143         try:
    144             if resolver:
    145                 zone = dns.zone.from_xfr(dns.query.xfr(
    146                     resolver, domain, port=self.port))
    147             else:
    148                 zone = dns.zone.from_xfr(dns.query.xfr(
    149                     master_answer[0].address, domain,
    150                     port=self.port))
    151         except dns.resolver.NoAnswer:
    152             logging.critical("Nameserver for '%s' did not answer.", domain)
    153             return None
    154         except dns.exception.FormError:
    155             logging.critical("Domain '%s' does not allow xfr requests.",
    156                              domain)
    157             return None
    158         except Exception:
    159             logging.error("Unexpected error while transfering domain '%s'",
    160                           domain)
    161             return None
    162 
    163         for soa_record in zone.iterate_rdatas(rdtype=dns.rdatatype.SOA):
    164             if not self.transferns:
    165                 mname = soa_record[2].mname
    166                 if self.domain not in mname:
    167                     self.transferns = str(soa_record[2].mname) + "." + domain
    168                 else:
    169                     self.transferns = str(soa_record[2].mname)
    170             return int(soa_record[2].serial)
    171 
    172     def add_records_to_gns(self) -> None:
    173         """
    174         Extracts records from zone and adds them to GNS
    175         :raises AttributeError: When getting incomplete data
    176         """
    177         logging.info("Starting to add records into GNS...")
    178 
    179         # Defining FIFO Queue
    180         taskqueue = queue.Queue(maxsize=5)
    181 
    182         # Defining worker
    183         def worker():
    184             while True:
    185                 # define recordline
    186                 recordline = list()
    187                 label = ""
    188                 domain = None
    189 
    190                 labelrecords = taskqueue.get()
    191                 # break if taskqueue is empty
    192                 if labelrecords is None:
    193                     break
    194 
    195                 # execute thing to run on item
    196                 label, listofrdatasets = labelrecords
    197                 subzones = label.split('.')
    198                 domain = self.domain
    199 
    200                 if len(subzones) > 1:
    201                     label = subzones[0]
    202                     subdomains = ".".join(subzones[1:])
    203                     subzone = "%s.%s" % (subdomains, domain)
    204                     fqdn = "%s.%s.%s" % (label, subdomains, domain)
    205                     if fqdn in self.subzonedict.keys():
    206                         label = "@"
    207                         domain = fqdn
    208                     elif subzone in self.subzonedict.keys():
    209                         domain = subzone
    210 
    211                 for rdataset in listofrdatasets:
    212                     for record in rdataset:
    213                         rdtype = dns.rdatatype.to_text(record.rdtype)
    214                         if rdtype not in SUPPORTED_RECORD_TYPES:
    215                             continue
    216 
    217                         try:
    218                             if rdataset.ttl <= self.minimum:
    219                                 ttl = self.minimum
    220                             else:
    221                                 ttl = rdataset.ttl
    222                         except AttributeError:
    223                             ttl = self.minimum
    224 
    225                         value = str(record)
    226 
    227                         # ignore NS for itself here
    228                         if label == '@' and rdtype == 'NS':
    229                             logging.info("ignoring NS record for itself")
    230 
    231                         # modify value to fit gns syntax
    232                         rdtype, value, label = \
    233                             self.transform_to_gns_format(record,
    234                                                         rdtype,
    235                                                         domain,
    236                                                         label)
    237                         # skip record if value is none
    238                         if value is None:
    239                             continue
    240 
    241                         if isinstance(value, list):
    242                             for element in value:
    243                                 # build recordline
    244                                 recordline.append("-R")
    245                                 recordline.append('%d %s %s %s' %
    246                                                   (int(ttl),
    247                                                    rdtype,
    248                                                    self.flags,
    249                                                    element))
    250                         else:
    251                             # build recordline
    252                             recordline.append("-R")
    253 
    254                             # TODO possible regression here; maybe use a separate
    255                             # list to pass those arguments to prevent quoting
    256                             # issues in the future.
    257                             recordline.append('%d %s %s %s' %
    258                                               (int(ttl),
    259                                                rdtype,
    260                                                self.flags,
    261                                                value))
    262 
    263                 # add recordline to gns and filter out empty lines
    264                 if len(recordline) > 1:
    265                     self.add_recordline_to_gns(recordline,
    266                                                domain,
    267                                                label)
    268 
    269                 taskqueue.task_done()
    270         # End of worker
    271 
    272 
    273         # Check if a delegated zone is available in GNS as per NS record
    274         nsrecords = self.zone.iterate_rdatasets(dns.rdatatype.NS)
    275 
    276         # This is broken if your NS is for ns.foo.YOURZONE as you add
    277         # the PKEY to YOURZONE instead of to the foo.YOURZONE subzone.
    278         # alice NS IN  ns.alice
    279         # bob   NS IN ns.bob
    280         # carol NS IN ns.alice
    281         # => carol GNS2DNS GNS ns.alice@$IP
    282         # dave.foo NS IN gns--pkey--$KEY.bob
    283         # => dave.foo PKEY GNS $KEY
    284         # foo.bar A IN 1.2.3.4
    285         # => bar PKEY GNS $NEWKEY     + mapping: bar => $NEWKEY
    286         # => foo[.bar] A GNS 1.2.3.4
    287         #gnspkey = list(filter(lambda record: for rec in record[2]: if str(rec).startswith('gns--pkey--'): return true; return false, nsrecords))
    288         illegalchars = ["I", "L", "O", "U", "i", "l", "o", "u"]
    289         for nsrecord in nsrecords:
    290             name = str(nsrecord[0])
    291             values = nsrecord[1]
    292             ttl = values.ttl
    293 
    294             gnspkeys = list(filter(lambda record:
    295                                    str(record).startswith('gns--pkey--'),
    296                                    values))
    297 
    298             num_gnspkeys = len(gnspkeys)
    299             if not num_gnspkeys:
    300                 # skip empty values
    301                 continue
    302             if num_gnspkeys > 1:
    303                 logging.critical("Detected ambiguous PKEY records for label \
    304                                   %s (not generating PKEY record)", name)
    305                 continue
    306 
    307             gnspkey = str(gnspkeys[0])
    308             # FIXME: drop all NS records under this name later!
    309             # => new map, if entry present during NS processing, skip!
    310             if not any(illegal in gnspkey for illegal in illegalchars):
    311                 self.add_pkey_record_to_zone(gnspkey[11:],
    312                                              self.domain,
    313                                              name,
    314                                              ttl)
    315 
    316         # Unify all records under same label into a record set
    317         customrdataset = dict()
    318         for name, rdset in self.zone.iterate_rdatasets():
    319             # build lookup table for later GNS2DNS records
    320             name = str(name) # Name could be str or DNS.name.Name
    321             if customrdataset.get(name) is None:
    322                 work = list()
    323                 work.append(rdset)
    324                 customrdataset[name] = work
    325             else:
    326                 customrdataset[name].append(rdset)
    327 
    328         for label, value in customrdataset.items():
    329             if value is None:
    330                 continue
    331 
    332             subzones = label.split('.')
    333             label = subzones[0]
    334             subdomain = ".".join(subzones[1:])
    335             zonename = "%s.%s" % (subdomain, self.domain)
    336 
    337             try:
    338                 if value.ttl <= self.minimum:
    339                     ttl = self.minimum
    340                 else:
    341                     ttl = value.ttl
    342             except AttributeError:
    343                 ttl = self.minimum
    344 
    345             if len(subzones) > 1:
    346                 if self.subzonedict.get(zonename):
    347                     continue
    348                 else:
    349                     self.subzonedict[zonename] = (False, ttl)
    350 
    351         self.create_zone_hierarchy()
    352 
    353         # Create one thread
    354         thread = threading.Thread(target=worker)
    355         thread.start()
    356 
    357         # add records
    358         for label, value in customrdataset.items():
    359             if value is None:
    360                 continue
    361             taskqueue.put((label, value))
    362 
    363         # Block until all tasks are done
    364         taskqueue.join()
    365 
    366         # Stop workers and threads
    367         taskqueue.put(None)
    368         thread.join(timeout=10)
    369         if thread.is_alive():
    370             logging.critical("thread join timed out, still running")
    371 
    372         # Add soa record to GNS once completed (updates the previous one)
    373         self.add_soa_record_to_gns(self.soa)
    374 
    375         logging.info("All records have been added!")
    376 
    377     @staticmethod
    378     def add_recordline_to_gns(recordline: list,
    379                               zonename: str,
    380                               label: str) -> None:
    381         """
    382         Replaces records in zone or adds them if not
    383         :param recordline: records to replace as list in form
    384         ['-R', 'TTL TYPE FLAGS VALUE']
    385         :param zonename: zonename of zone to add records to
    386         :param label: label under which to add the records
    387         """
    388         logging.info("trying to add %d records with name %s",
    389                      len(recordline)/2, label)
    390 
    391         ret = sp.run([GNUNET_NAMESTORE_COMMAND,
    392                       '-z', zonename,
    393                       '-n', str(label),
    394                       ] + recordline)
    395 
    396         if ret.returncode != 0:
    397             logging.warning("failed adding record with name %s",
    398                             ' '.join(ret.args))
    399         else:
    400             logging.info("successfully added record with command %s",
    401                          ' '.join(ret.args))
    402 
    403     def resolve_glue(self,
    404                      authorityname: str) -> list:
    405         """
    406         Resolves IP Adresses within zone
    407         :param authorityname:
    408         """
    409         try:
    410             rdsets = self.zone[authorityname].rdatasets
    411         except KeyError:
    412             return []
    413         value = []
    414         for rdataset in rdsets:
    415             if rdataset.rdtype in [dns.rdatatype.A, dns.rdatatype.AAAA]:
    416                 for rdata in rdataset:
    417                     value.append("%s.%s@%s" % (authorityname,
    418                                                self.domain,
    419                                                str(rdata)))
    420         return value
    421 
    422     def transform_to_gns_format(self,
    423                                 record: dns.rdata.Rdata,
    424                                 rdtype: dns.rdata.Rdata,
    425                                 zonename: str,
    426                                 label: str) -> tuple:
    427         """
    428         Transforms value of record to GNS compatible format
    429         :param record: record to transform
    430         :param rdtype: record value to transform
    431         :param zonename: name of the zone to add to
    432         :param label: label under which the record is stored
    433         :returns: a tuple consisting of the new rdtype, the label and value
    434         """
    435         value = str(record)
    436         if label is None:
    437             label = '@'
    438         if rdtype == 'SOA':
    439             zonetuple = str(value).split(' ')
    440             authns, owner, serial, refresh, retry, expiry, irefresh = zonetuple
    441             if authns[-1] == '.':
    442                 authns = authns[:-1]
    443             if owner[-1] == '.':
    444                 owner = owner[:-1]
    445             # hacky and might cause bugs
    446             authns += self.tld
    447             owner += self.tld
    448             value = "rname=%s.%s mname=%s.%s %d,%d,%d,%d,%d" % (
    449                 authns, zonename, owner, zonename,
    450                 int(serial), int(refresh), int(retry),
    451                 int(expiry), int(irefresh)
    452             )
    453         elif rdtype in ['CNAME']:
    454             if value[-1] == ".":
    455                 value = value[:-1]
    456             else:
    457                 value = "%s.%s" % (value, self.domain)
    458         elif rdtype == 'NS':
    459             nameserver = str(record.target)
    460             if nameserver[-1] == ".":
    461                 nameserver = nameserver[:-1]
    462             if value[-1] == ".":
    463                 # FQDN provided
    464                 if value.endswith(".%s." % zonename):
    465                     # in bailiwick
    466                     value = self.resolve_glue(record.target)
    467                 else:
    468                      # out of bailiwick
    469                     if label.startswith("@"):
    470                         value = '%s@%s' % (zonename, nameserver)
    471                     else:
    472                         value = '%s.%s@%s' % (str(label),
    473                                               zonename,
    474                                               nameserver)
    475             else:
    476                 # Name is relative to zone, must be in bailiwick
    477                 value = self.resolve_glue(record.target)
    478                 if not value:
    479                     if label.startswith("@"):
    480                         value = '%s@%s.%s' % (self.domain,
    481                                               record.target,
    482                                               self.domain)
    483                     else:
    484                         value = '%s.%s@%s.%s' % (str(label), self.domain,
    485                                                  record.target, self.domain)
    486 
    487             logging.info("transformed %s record to GNS2DNS format", rdtype)
    488             rdtype = 'GNS2DNS'
    489         elif rdtype == 'MX':
    490             priority, mailserver = str(value).split(' ')
    491             if mailserver[-1] == ".":
    492                 mailserver = mailserver[:-1]
    493             mailserver = '%s.%s' % (mailserver, zonename)
    494             value = '%s,%s' % (priority, mailserver)
    495             logging.info("transformed %s record to GNS format", rdtype)
    496         elif rdtype == 'SRV':
    497             # this is the number for a SRV record
    498             rdtype = 'BOX'
    499             srv = 33
    500 
    501             # tearing the record apart
    502             try:
    503                 srvrecord = str(label).split('.')
    504                 proto = srvrecord[1]
    505             except IndexError:
    506                 logging.warning("could not parse SRV label %s", label)
    507                 return (rdtype, None, None)
    508             priority, weight, destport, target = value.split(' ')
    509 
    510             try:
    511                 protostring = proto.strip('_')
    512                 protonum = socket.getprotobyname(protostring)
    513             except OSError:
    514                 logging.warning("invalid protocol: %s", protostring)
    515                 return (rdtype, None, None)
    516 
    517             if target[:-1] == ".":
    518                 value = '%s %s %s %s %s %s %s' % (
    519                     destport, protonum, srv, priority, weight, destport,
    520                     "%s" % target
    521                 )
    522             else:
    523                 value = '%s %s %s %s %s %s %s' % (
    524                     destport, protonum, srv, priority, weight, destport,
    525                     "%s.%s" % (target, zonename)
    526                 )
    527 
    528             label = target
    529         else:
    530             logging.info("Did not transform record of type: %s", rdtype)
    531         return (rdtype, value, label)
    532 
    533     def get_gns_zone_serial(self) -> int:
    534         """
    535         Fetches the zones serial from GNS
    536         :returns: serial of the SOA record in GNS
    537         """
    538         try:
    539             serial = sp.check_output([GNUNET_GNS_COMMAND,
    540                                       '-t', 'SOA',
    541                                       '-u', '%s' % self.domain,])
    542             serial = serial.decode()
    543         except sp.CalledProcessError:
    544             serial = ""
    545             soa_serial = 0
    546         soapattern = re.compile(r'.+\s(\d+),\d+,\d+,\d+,\d+', re.M)
    547         if re.findall(soapattern, serial):
    548             soa_serial = re.findall(soapattern, serial)[0]
    549         else:
    550             soa_serial = 0
    551         return int(soa_serial)
    552 
    553     @staticmethod
    554     def get_zone_soa(zone) -> dns.rdatatype.SOA:
    555         """
    556         Fetches soa record from zone a given zone
    557         :param zone: A dnspython zone
    558         :returns: SOA record of given zone
    559         """
    560         soa = None
    561         for soarecord in zone.iterate_rdatas(rdtype=dns.rdatatype.SOA):
    562             if str(soarecord[0]) == '@':
    563                 soa = soarecord
    564         return soa
    565 
    566     def add_soa_record_to_gns(self, record) -> None:
    567         """
    568         Adds a SOA record to GNS
    569         :param record: The record to add
    570         """
    571         label, ttl, rdata = record
    572         zonetuple = str(rdata).split(' ')
    573         authns, owner, serial, refresh, retry, expiry, irefresh = zonetuple
    574         if authns[-1] == '.':
    575             authns = authns[:-1]
    576         else:
    577             authns = "%s.%s" % (authns, self.domain)
    578         if owner[-1] == '.':
    579             owner = owner[:-1]
    580         else:
    581             owner = "%s.%s" % (owner, self.domain)
    582 
    583         value = "rname=%s mname=%s %s,%s,%s,%s,%s" % (authns,
    584                                                       owner,
    585                                                       serial,
    586                                                       refresh,
    587                                                       retry,
    588                                                       expiry,
    589                                                       irefresh)
    590         recordval = '%s %s %s %s' % (ttl, "SOA", self.flags, str(value))
    591         recordline = ['-R', recordval]
    592         self.add_recordline_to_gns(recordline, self.domain, str(label))
    593 
    594     @staticmethod
    595     def create_zone_and_get_pkey(zonestring: str) -> str:
    596         """
    597         Creates the zone in zonestring and returns pkey
    598         :param zonestring: The label name of the zone
    599         :returns: gnunet pkey of the zone
    600         """
    601         try:
    602             ret = sp.run([GNUNET_ZONE_CREATION_COMMAND,
    603                           '-C', zonestring,
    604                           '-V'],
    605                          stdout=sp.PIPE,
    606                          stderr=sp.DEVNULL,
    607                          check=True)
    608             logging.info("executed command: %s", " ".join(ret.args))
    609             pkey_zone = ret.stdout.decode().strip()
    610         except sp.CalledProcessError:
    611             ret = sp.run([GNUNET_ZONE_CREATION_COMMAND,
    612                           '-dq',
    613                           '-e', zonestring],
    614                          stdout=sp.PIPE)
    615             logging.info("executed command: %s", " ".join(ret.args))
    616             pkey_zone = ret.stdout.decode().strip()
    617         return pkey_zone
    618 
    619     @staticmethod
    620     def add_pkey_record_to_zone(pkey: str,
    621                                 domain: str,
    622                                 label: str,
    623                                 ttl: str) -> None:
    624         """
    625         Adds the pkey of the subzone to the parent zone
    626         :param pkey: the public key of the child zone
    627         :param domain: the name of the parent zone
    628         :param label: the label under which to add the pkey
    629         :param ttl: the time to live the record should have
    630         """
    631         #lookup = sp.run(GNUNET_NAMESTORE_COMMAND,
    632         #                '-z', domain,
    633         #                '-n', label,
    634         #                '-t', 'PKEY')
    635         #if not 'Got result:' in lookup.stdout:
    636         debug = " ".join([GNUNET_NAMESTORE_COMMAND,
    637                           '-z', domain,
    638                           '-a', '-n', label,
    639                           '-t', 'PKEY',
    640                           '-V', pkey,
    641                           '-e', "%ss" % ttl])
    642         ret = sp.run([GNUNET_NAMESTORE_COMMAND,
    643                       '-z', domain,
    644                       '-a', '-n', label,
    645                       '-t', 'PKEY',
    646                       '-V', pkey,
    647                       '-e', "%ss" % ttl])
    648         logging.info("executed command: %s", debug)
    649         if ret.returncode != 0:
    650             logging.warning("failed to add PKEY record %s to %s",
    651                             label, domain)
    652         #logging.warning("PKEY record %s already exists in %s", label, domain)
    653 
    654     def create_zone_hierarchy(self) -> None:
    655         """
    656         Creates the zone hierarchy in GNS for label
    657         """
    658         domain = self.domain
    659 
    660         # Build Dictionary from GNS identities
    661         ids = sp.run([GNUNET_ZONE_CREATION_COMMAND, '-d'], stdout=sp.PIPE)
    662         domainlist = ''.join(col for col in ids.stdout.decode()).split('\n')
    663         altdomainlist = [e for e in domainlist if domain + " " in e]
    664         for zone in altdomainlist:
    665             zonename, _, pkey = zone.split(" ")
    666             self.subzonedict[zonename] = (pkey, self.minimum)
    667 
    668         zonelist = self.subzonedict.items()
    669         sortedlist = sorted(zonelist, key=lambda s: len(str(s).split('.')))
    670         for zone, pkeyttltuple in sortedlist:
    671             pkey, ttl = pkeyttltuple
    672             if not pkey:
    673                 domain = ".".join(zone.split('.')[1::])
    674                 label = zone.split('.')[0]
    675                 pkey = self.create_zone_and_get_pkey(zone)
    676                 logging.info("adding zone %s with %s pkey into %s", zone, pkey, domain)
    677                 self.add_pkey_record_to_zone(pkey, domain, label, pkeyttltuple[1])
    678                 self.subzonedict[zone] = (pkey, ttl)
    679 
    680 def main():
    681     """
    682     Initializes object and handles arguments
    683     """
    684     # argument parsing from docstring definition
    685     args = docopt.docopt(__doc__, version='Ascension 0.6.1')
    686 
    687     # argument parsing
    688     debug = args['--debug']
    689     domain = args.get('<domain>', None)
    690     transferns = args['<transferns>'] if args['<transferns>'] else None
    691     port = args['<port>'] if args['<port>'] else "53"
    692     flags = "p" if args.get('--public') else "n"
    693     standalone = bool(args.get('--standalone'))
    694     dryrun = bool(args.get('--dry-run'))
    695     minimum = args['--minimum-ttl']
    696 
    697     # Change logging severity to debug
    698     if debug:
    699         logging.basicConfig(level=logging.DEBUG)
    700 
    701     # Initialize class instance
    702     ascender = Ascender(domain, transferns, port, flags, minimum)
    703 
    704     # Do dry run before GNUnet check
    705     if dryrun:
    706         dns_zone_serial = ascender.get_dns_zone_serial(ascender.domain,
    707                                                        ascender.transferns)
    708         if dns_zone_serial is None:
    709             return 1
    710         else:
    711             return 0
    712 
    713     # Checks if GNUnet services are running
    714     try:
    715         sp.check_output([GNUNET_ARM_COMMAND, '-I'], timeout=1)
    716     except sp.TimeoutExpired:
    717         logging.critical('GNUnet services are not running!')
    718         sys.exit(1)
    719 
    720     # Set to defaults to use before we get a SOA for the first time
    721     retry = 300
    722     refresh = 300
    723 
    724     # Main loop for actual daemon
    725     while True:
    726         gns_zone_serial = ascender.get_gns_zone_serial()
    727         if gns_zone_serial:
    728             ascender.zonegenerator = dns.query.xfr(ascender.transferns,
    729                                                    ascender.domain,
    730                                                    rdtype=dns.rdatatype.IXFR,
    731                                                    serial=gns_zone_serial,
    732                                                    port=ascender.port)
    733         else:
    734             ascender.zonegenerator = dns.query.xfr(ascender.transferns,
    735                                                    ascender.domain,
    736                                                    port=ascender.port)
    737         dns_zone_serial = ascender.get_dns_zone_serial(ascender.domain,
    738                                                        ascender.transferns)
    739 
    740         if not dns_zone_serial:
    741             logging.error("Could not get DNS zone serial")
    742             if standalone:
    743                 return 1
    744             time.sleep(retry)
    745             continue
    746         if not gns_zone_serial:
    747             logging.info("GNS zone does not exist yet, performing full transfer.")
    748             print("GNS zone does not exist yet, performing full transfer.")
    749             ascender.bootstrap_zone()
    750         elif gns_zone_serial == dns_zone_serial:
    751             logging.info("GNS zone is up to date.")
    752             print("GNS zone is up to date.")
    753             if standalone:
    754                 return 0
    755             time.sleep(refresh)
    756             continue
    757         elif gns_zone_serial > dns_zone_serial:
    758             logging.critical("SOA serial in GNS is bigger than SOA serial in DNS?")
    759             logging.critical("GNS zone: %s, DNS zone: %s", gns_zone_serial, dns_zone_serial)
    760             if standalone:
    761                 return 1
    762             time.sleep(retry)
    763             continue
    764         else:
    765             logging.info("GNS zone is out of date, performing incremental transfer.")
    766             print("GNS zone is out of date, performing incremental transfer.")
    767 
    768         try:
    769             ascender.zone = dns.zone.from_xfr(ascender.zonegenerator,
    770                                               check_origin=False)
    771             ascender.soa = ascender.get_zone_soa(ascender.zone)
    772             refresh = int(str(ascender.soa[2]).split(" ")[3])
    773             retry = int(str(ascender.soa[2]).split(" ")[4])
    774         except dns.zone.BadZone:
    775             logging.critical("Malformed DNS Zone '%s'", ascender.domain)
    776             if standalone:
    777                 return 2
    778             time.sleep(retry)
    779             continue
    780 
    781         ascender.add_records_to_gns()
    782         logging.info("Finished migration of the zone %s", ascender.domain)
    783 
    784 if __name__ == '__main__':
    785     main()