fix a comment
[invirt/packages/invirt-dns.git] / invirt-dns
index 3d95965..32b18c2 100755 (executable)
@@ -3,14 +3,17 @@ from twisted.internet import reactor
 from twisted.names import server
 from twisted.names import dns
 from twisted.names import common
 from twisted.names import server
 from twisted.names import dns
 from twisted.names import common
+from twisted.names import authority
 from twisted.internet import defer
 from twisted.python import failure
 
 from twisted.internet import defer
 from twisted.python import failure
 
+from invirt.common import InvirtConfigError
 from invirt.config import structs as config
 import invirt.database
 import psycopg2
 import sqlalchemy
 import time
 from invirt.config import structs as config
 import invirt.database
 import psycopg2
 import sqlalchemy
 import time
+import re
 
 class DatabaseAuthority(common.ResolverBase):
     """An Authority that is loaded from a file."""
 
 class DatabaseAuthority(common.ResolverBase):
     """An Authority that is loaded from a file."""
@@ -57,30 +60,20 @@ class DatabaseAuthority(common.ResolverBase):
         ttl = 900
         name = name.lower()
 
         ttl = 900
         name = name.lower()
 
-        # XXX hack for the transition to two separate dev/prod clusters
-        if 'dev.xvm.mit.edu' in self.domains and name.endswith('prod.xvm.mit.edu'):
-            # import time, sys
-            # print time.localtime(), 'handling prod request', name
-            # sys.stdout.flush()
-
-            # Point the client in the right direction for prod requests.
-            authority = dns.RRHeader('prod.xvm.mit.edu', dns.NS, dns.IN, 3600,
-                    dns.Record_NS(name='ns1.prod.xvm.mit.edu', ttl=3600), auth=True)
-            additional = dns.RRHeader('ns1.prod.xvm.mit.edu', dns.A, dns.IN, 3600,
-                    dns.Record_A(address='18.181.0.221', ttl=3600), auth=True)
-            return defer.succeed(([], [authority], [additional]))
-
         if name in self.domains:
             domain = name
         else:
         if name in self.domains:
             domain = name
         else:
-            # Look for the longest-matching domain.  (This works because domain
-            # will remain bound after breaking out of the loop.)
+            # Look for the longest-matching domain.
             best_domain = ''
             for domain in self.domains:
                 if name.endswith('.'+domain) and len(domain) > len(best_domain):
                     best_domain = domain
             if best_domain == '':
             best_domain = ''
             for domain in self.domains:
                 if name.endswith('.'+domain) and len(domain) > len(best_domain):
                     best_domain = domain
             if best_domain == '':
-                return defer.fail(failure.Failure(dns.DomainError(name)))
+                if name.endswith('.in-addr.arpa'):
+                    # Act authoritative for the IP address for reverse resolution requests
+                    best_domain = name
+                else:
+                    return defer.fail(failure.Failure(dns.DomainError(name)))
             domain = best_domain
         results = []
         authority = []
             domain = best_domain
         results = []
         authority = []
@@ -90,7 +83,7 @@ class DatabaseAuthority(common.ResolverBase):
 
         if cls == dns.IN:
             host = name[:-len(domain)-1]
 
         if cls == dns.IN:
             host = name[:-len(domain)-1]
-            if not host: # Request for the domain itself.
+            if not host and type != dns.PTR: # Request for the domain itself.
                 if type in (dns.A, dns.ALL_RECORDS):
                     record = dns.Record_A(config.dns.nameservers[0].ip, ttl)
                     results.append(dns.RRHeader(name, dns.A, dns.IN, 
                 if type in (dns.A, dns.ALL_RECORDS):
                     record = dns.Record_A(config.dns.nameservers[0].ip, ttl)
                     results.append(dns.RRHeader(name, dns.A, dns.IN, 
@@ -103,26 +96,41 @@ class DatabaseAuthority(common.ResolverBase):
                     results.append(dns.RRHeader(domain, dns.SOA, dns.IN,
                                                 ttl, self.soa, auth=True))
             else: # Request for a subdomain.
                     results.append(dns.RRHeader(domain, dns.SOA, dns.IN,
                                                 ttl, self.soa, auth=True))
             else: # Request for a subdomain.
-                if 'passup' in dir(config.dns) and host in config.dns.passup:
-                    record = dns.Record_CNAME('%s.%s' % (host, config.dns.parent), ttl)
-                    return defer.succeed((
-                        [dns.RRHeader(name, dns.CNAME, dns.IN, ttl, record, auth=True)],
-                        [], []))
+                if name.endswith(".in-addr.arpa"): # Reverse resolution here
+                    if type in (dns.PTR, dns.ALL_RECORDS):
+                        ip = '.'.join(reversed(name.split('.')[:-2]))
+                        value = invirt.database.NIC.query.filter_by(ip=ip).first()
+                        if value and value.hostname:
+                            hostname = value.hostname
+                            if '.' not in hostname:
+                                hostname = hostname + "." + config.dns.domains[0]
+                            record = dns.Record_PTR(hostname, ttl)
+                            results.append(dns.RRHeader(name, dns.PTR, dns.IN,
+                                                        ttl, record, auth=True))
+                        else: # IP address doesn't point to an active host
+                            return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
+                    # FIXME: Should only return success with no records if the name actually exists
+                else: # Forward resolution here
+                    value = invirt.database.NIC.query.filter_by(hostname=host).first()
+                    if value:
+                        ip = value.ip
+                    else:
+                        value = invirt.database.Machine.query().filter_by(name=host).first()
+                        if value:
+                            ip = value.nics[0].ip
+                        else:
+                            return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
+                
+                    if ip is None:
+                        return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
 
 
-                value = invirt.database.Machine.query().filter_by(name=host).first()
-                if value is None or not value.nics:
-                    return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
-                ip = value.nics[0].ip
-                if ip is None:  #Deactivated?
-                    return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
-
-                if type in (dns.A, dns.ALL_RECORDS):
-                    record = dns.Record_A(ip, ttl)
-                    results.append(dns.RRHeader(name, dns.A, dns.IN, 
-                                                ttl, record, auth=True))
-                elif type == dns.SOA:
-                    results.append(dns.RRHeader(domain, dns.SOA, dns.IN,
-                                                ttl, self.soa, auth=True))
+                    if type in (dns.A, dns.ALL_RECORDS):
+                        record = dns.Record_A(ip, ttl)
+                        results.append(dns.RRHeader(name, dns.A, dns.IN, 
+                                                    ttl, record, auth=True))
+                    elif type == dns.SOA:
+                        results.append(dns.RRHeader(domain, dns.SOA, dns.IN,
+                                                    ttl, self.soa, auth=True))
             if len(results) == 0:
                 authority = []
                 additional = []
             if len(results) == 0:
                 authority = []
                 additional = []
@@ -131,11 +139,74 @@ class DatabaseAuthority(common.ResolverBase):
             #Doesn't exist
             return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
 
             #Doesn't exist
             return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
 
+class QuotingBindAuthority(authority.BindAuthority):
+    """
+    A BindAuthority that (almost) deals with quoting correctly
+    
+    This will catch double quotes as marking the start or end of a
+    quoted phrase, unless the double quote is escaped by a backslash
+    """
+    # Match either a quoted or unquoted string literal followed by
+    # whitespace or the end of line.  This yields two groups, one of
+    # which has a match, and the other of which is None, depending on
+    # whether the string literal was quoted or unquoted; this is what
+    # necessitates the subsequent filtering out of groups that are
+    # None.
+    string_pat = \
+            re.compile(r'"((?:[^"\\]|\\.)*)"|((?:[^\\\s]|\\.)+)(?:\s+|\s*$)')
+
+    # For interpreting escapes.
+    escape_pat = re.compile(r'\\(.)')
+
+    def collapseContinuations(self, lines):
+        L = []
+        state = 0
+        for line in lines:
+            if state == 0:
+                if line.find('(') == -1:
+                    L.append(line)
+                else:
+                    L.append(line[:line.find('(')])
+                    state = 1
+            else:
+                if line.find(')') != -1:
+                    L[-1] += ' ' + line[:line.find(')')]
+                    state = 0
+                else:
+                    L[-1] += ' ' + line
+        lines = L
+        L = []
+
+        for line in lines:
+            in_quote = False
+            split_line = []
+            for m in self.string_pat.finditer(line):
+                [x] = [x for x in m.groups() if x is not None]
+                split_line.append(self.escape_pat.sub(r'\1', x))
+            L.append(split_line)
+        return filter(None, L)
+
 if '__main__' == __name__:
 if '__main__' == __name__:
-    resolver = DatabaseAuthority()
+    resolvers = []
+    try:
+        for zone in config.dns.zone_files:
+            for origin in config.dns.domains:
+                r = QuotingBindAuthority(zone)
+                # This sucks, but if I want a generic zone file, I have to
+                # reload the information by hand
+                r.origin = origin
+                lines = open(zone).readlines()
+                lines = r.collapseContinuations(r.stripComments(lines))
+                r.parseLines(lines)
+                
+                resolvers.append(r)
+    except InvirtConfigError:
+        # Don't care if zone_files isn't defined
+        pass
+    resolvers.append(DatabaseAuthority())
 
     verbosity = 0
 
     verbosity = 0
-    f = server.DNSServerFactory(authorities=[resolver], verbose=verbosity)
+    f = server.DNSServerFactory(authorities=resolvers, verbose=verbosity)
     p = dns.DNSDatagramProtocol(f)
     f.noisy = p.noisy = verbosity
     
     p = dns.DNSDatagramProtocol(f)
     f.noisy = p.noisy = verbosity