#!/usr/bin/env python # # update our DNS names using TSIG-GSS # # Copyright (C) Andrew Tridgell 2010 # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import fcntl import sys import tempfile # ensure we get messages out immediately, so they get in the samba logs, # and don't get swallowed by a timeout os.putenv('PYTHONUNBUFFERED', '1') # Find right directory when running from source tree sys.path.insert(0, "bin/python") import samba import optparse from samba import getopt as options from ldb import SCOPE_BASE from samba.auth import system_session from samba.samdb import SamDB samba.ensure_external_module("dns", "dnspython") import dns.resolver as resolver default_ttl = 900 parser = optparse.OptionParser("samba_dnsupdate") sambaopts = options.SambaOptions(parser) parser.add_option_group(sambaopts) parser.add_option_group(options.VersionOptions(parser)) parser.add_option("--verbose", action="store_true") parser.add_option("--all-interfaces", action="store_true") parser.add_option("--use-file", type="string", help="Use a file, rather than real DNS calls") creds = None ccachename = None opts, args = parser.parse_args() if len(args) != 0: parser.print_usage() sys.exit(1) lp = sambaopts.get_loadparm() domain = lp.get("realm") host = lp.get("netbios name") if opts.all_interfaces: all_interfaces = True else: all_interfaces = False IPs = samba.interface_ips(lp, all_interfaces) nsupdate_cmd = lp.get('nsupdate command') if len(IPs) == 0: print "No IP interfaces - skipping DNS updates" sys.exit(0) ######################################################## # get credentials if we haven't got them already def get_credentials(lp): from samba.credentials import Credentials global ccachename, creds if creds is not None: return creds = Credentials() creds.guess(lp) creds.set_machine_account(lp) (tmp_fd, ccachename) = tempfile.mkstemp() creds.get_named_ccache(lp, ccachename) ############################################# # an object to hold a parsed DNS line class dnsobj(object): def __init__(self, string_form): list = string_form.split() self.dest = None self.port = None self.ip = None self.existing_port = None self.existing_weight = None self.type = list[0] self.name = list[1] if self.type == 'SRV': self.dest = list[2] self.port = list[3] elif self.type == 'A': self.ip = list[2] # usually $IP, which gets replaced elif self.type == 'CNAME': self.dest = list[2] else: print "Received unexpected DNS reply of type %s" % self.type raise def __str__(self): if d.type == "A": return "%s %s %s" % (self.type, self.name, self.ip) if d.type == "SRV": return "%s %s %s %s" % (self.type, self.name, self.dest, self.port) if d.type == "CNAME": return "%s %s %s" % (self.type, self.name, self.dest) ################################################ # parse a DNS line from def parse_dns_line(line, sub_vars): subline = samba.substitute_var(line, sub_vars) d = dnsobj(subline) return d ############################################ # see if two hostnames match def hostname_match(h1, h2): h1 = str(h1) h2 = str(h2) return h1.lower().rstrip('.') == h2.lower().rstrip('.') ############################################ # check that a DNS entry exists def check_dns_name(d): normalised_name = d.name.rstrip('.') + '.' if opts.verbose: print "Looking for DNS entry %s as %s" % (d, normalised_name) if opts.use_file is not None: try: dns_file = open(opts.use_file, "r") except IOError: return False for line in dns_file: line = line.strip() if line == '' or line[0] == "#": continue if line.lower() == str(d).lower(): return True return False try: ans = resolver.query(normalised_name, d.type) except resolver.NXDOMAIN: return False if d.type == 'A': # we need to be sure that our IP is there for rdata in ans: if str(rdata) == str(d.ip): return True if d.type == 'CNAME': for i in range(len(ans)): if hostname_match(ans[i].target, d.dest): return True if d.type == 'SRV': for rdata in ans: if opts.verbose: print "Checking %s against %s" % (rdata, d) if hostname_match(rdata.target, d.dest): if str(rdata.port) == str(d.port): return True else: d.existing_port = str(rdata.port) d.existing_weight = str(rdata.weight) if opts.verbose: print "Failed to find DNS entry %s" % d return False ########################################### # get the list of substitution vars def get_subst_vars(): global lp vars = {} samdb = SamDB(url=lp.get("sam database"), session_info=system_session(), lp=lp) vars['DNSDOMAIN'] = lp.get('realm').lower() vars['HOSTNAME'] = lp.get('netbios name').lower() + "." + vars['DNSDOMAIN'] vars['NTDSGUID'] = samdb.get_ntds_GUID() vars['SITE'] = samdb.server_site_name() res = samdb.search(base=None, scope=SCOPE_BASE, attrs=["objectGUID"]) guid = samdb.schema_format_value("objectGUID", res[0]['objectGUID'][0]) vars['DOMAINGUID'] = guid return vars ############################################ # call nsupdate for an entry def call_nsupdate(d): global ccachename, nsupdate_cmd if opts.verbose: print "Calling nsupdate for %s" % d if opts.use_file is not None: wfile = open(opts.use_file, 'a') fcntl.lockf(wfile, fcntl.LOCK_EX) wfile.write(str(d)+"\n") fcntl.lockf(wfile, fcntl.LOCK_UN) return (tmp_fd, tmpfile) = tempfile.mkstemp() f = os.fdopen(tmp_fd, 'w') if d.type == "A": f.write("update add %s %u A %s\n" % (d.name, default_ttl, d.ip)) if d.type == "SRV": if d.existing_port is not None: f.write("update delete %s SRV 0 %s %s %s\n" % (d.name, d.existing_weight, d.existing_port, d.dest)) f.write("update add %s %u SRV 0 100 %s %s\n" % (d.name, default_ttl, d.port, d.dest)) if d.type == "CNAME": f.write("update add %s %u CNAME %s\n" % (d.name, default_ttl, d.dest)) if opts.verbose: f.write("show\n") f.write("send\n") f.close() os.putenv("KRB5CCNAME", ccachename) os.system("%s %s" % (nsupdate_cmd, tmpfile)) os.unlink(tmpfile) # get the list of DNS entries we should have dns_update_list = lp.private_path('dns_update_list') file = open(dns_update_list, "r") # get the substitution dictionary sub_vars = get_subst_vars() # build up a list of update commands to pass to nsupdate update_list = [] dns_list = [] # read each line, and check that the DNS name exists for line in file: line = line.strip() if line == '' or line[0] == "#": continue d = parse_dns_line(line, sub_vars) dns_list.append(d) # now expand the entries, if any are A record with ip set to $IP # then replace with multiple entries, one for each interface IP for d in dns_list: if d.type == 'A' and d.ip == "$IP": d.ip = IPs[0] for i in range(len(IPs)-1): d2 = d d2.ip = IPs[i+1] dns_list.append(d2) # now check if the entries already exist on the DNS server for d in dns_list: if not check_dns_name(d): update_list.append(d) if len(update_list) == 0: if opts.verbose: print "No DNS updates needed" sys.exit(0) # get our krb5 creds get_credentials(lp) # ask nsupdate to add entries as needed for d in update_list: call_nsupdate(d) # delete the ccache if we created it if ccachename is not None: os.unlink(ccachename)