samba_dnsupdate: Simplify logic and add more verbose debugging
[samba.git] / source4 / scripting / bin / samba_dnsupdate
1 #!/usr/bin/env python
2 # vim: expandtab
3 #
4 # update our DNS names using TSIG-GSS
5 #
6 # Copyright (C) Andrew Tridgell 2010
7 #
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
22 import os
23 import fcntl
24 import sys
25 import tempfile
26 import subprocess
27
28 # ensure we get messages out immediately, so they get in the samba logs,
29 # and don't get swallowed by a timeout
30 os.environ['PYTHONUNBUFFERED'] = '1'
31
32 # forcing GMT avoids a problem in some timezones with kerberos. Both MIT
33 # heimdal can get mutual authentication errors due to the 24 second difference
34 # between UTC and GMT when using some zone files (eg. the PDT zone from
35 # the US)
36 os.environ["TZ"] = "GMT"
37
38 # Find right directory when running from source tree
39 sys.path.insert(0, "bin/python")
40
41 import samba
42 import optparse
43 from samba import getopt as options
44 from ldb import SCOPE_BASE
45 from samba import dsdb
46 from samba.auth import system_session
47 from samba.samdb import SamDB
48 from samba.dcerpc import netlogon, winbind
49 from samba.netcmd.dns import cmd_dns
50 from samba import gensec
51
52 samba.ensure_third_party_module("dns", "dnspython")
53 import dns.resolver
54 import dns.exception
55
56 default_ttl = 900
57 am_rodc = False
58 error_count = 0
59
60 parser = optparse.OptionParser("samba_dnsupdate")
61 sambaopts = options.SambaOptions(parser)
62 parser.add_option_group(sambaopts)
63 parser.add_option_group(options.VersionOptions(parser))
64 parser.add_option("--verbose", action="store_true")
65 parser.add_option("--use-samba-tool", action="store_true", help="Use samba-tool to make updates over RPC, rather than over DNS")
66 parser.add_option("--use-nsupdate", action="store_true", help="Use nsupdate command to make updates over DNS (default, if kinit successful)")
67 parser.add_option("--all-names", action="store_true")
68 parser.add_option("--all-interfaces", action="store_true")
69 parser.add_option("--current-ip", action="append", help="IP address to update DNS to match (helpful if behind NAT, valid multiple times, defaults to values from interfaces=)")
70 parser.add_option("--rpc-server-ip", type="string", help="IP address of server to use with samba-tool (defaults to first --current-ip)")
71 parser.add_option("--use-file", type="string", help="Use a file, rather than real DNS calls")
72 parser.add_option("--update-list", type="string", help="Add DNS names from the given file")
73 parser.add_option("--update-cache", type="string", help="Cache database of already registered records")
74 parser.add_option("--fail-immediately", action='store_true', help="Exit on first failure")
75 parser.add_option("--no-credentials", dest='nocreds', action='store_true', help="don't try and get credentials")
76 parser.add_option("--no-substitutions", dest='nosubs', action='store_true', help="don't try and expands variables in file specified by --update-list")
77
78 creds = None
79 ccachename = None
80
81 opts, args = parser.parse_args()
82
83 if len(args) != 0:
84     parser.print_usage()
85     sys.exit(1)
86
87 lp = sambaopts.get_loadparm()
88
89 domain = lp.get("realm")
90 host = lp.get("netbios name")
91 if opts.all_interfaces:
92     all_interfaces = True
93 else:
94     all_interfaces = False
95
96 if opts.current_ip:
97     IPs = opts.current_ip
98 else:
99     IPs = samba.interface_ips(lp, all_interfaces)
100
101 nsupdate_cmd = lp.get('nsupdate command')
102
103 if len(IPs) == 0:
104     print "No IP interfaces - skipping DNS updates"
105     sys.exit(0)
106
107 if opts.rpc_server_ip:
108     rpc_server_ip = opts.rpc_server_ip
109 else:
110     rpc_server_ip = IPs[0]
111
112 IP6s = []
113 IP4s = []
114 for i in IPs:
115     if i.find(':') != -1:
116         IP6s.append(i)
117     else:
118         IP4s.append(i)
119
120
121 if opts.verbose:
122     print "IPs: %s" % IPs
123
124
125 def get_credentials(lp):
126     """# get credentials if we haven't got them already."""
127     from samba import credentials
128     global ccachename
129     creds = credentials.Credentials()
130     creds.guess(lp)
131     creds.set_machine_account(lp)
132     creds.set_krb_forwardable(credentials.NO_KRB_FORWARDABLE)
133     (tmp_fd, ccachename) = tempfile.mkstemp()
134     try:
135         creds.get_named_ccache(lp, ccachename)
136
137         if opts.use_file is not None:
138             return
139
140         # Now confirm we can get a ticket to a DNS server
141         ans = check_one_dns_name(sub_vars['DNSDOMAIN'] + '.', 'NS')
142         for i in range(len(ans)):
143             target_hostname = str(ans[i].target).rstrip('.')
144             settings = {}
145             settings["lp_ctx"] = lp
146             settings["target_hostname"] = target_hostname
147
148             gensec_client = gensec.Security.start_client(settings)
149             gensec_client.set_credentials(creds)
150             gensec_client.set_target_service("DNS")
151             gensec_client.set_target_hostname(target_hostname)
152             gensec_client.want_feature(gensec.FEATURE_SEAL)
153             gensec_client.start_mech_by_sasl_name("GSSAPI")
154             server_to_client = ""
155             try:
156                 (client_finished, client_to_server) = gensec_client.update(server_to_client)
157                 if opts.verbose:
158                     print "Successfully obtained Kerberos ticket to DNS/%s as %s" \
159                             % (target_hostname, creds.get_username())
160                 return
161             except RuntimeError:
162                 # Only raise an exception if they all failed
163                 if i != len(ans) - 1:
164                     pass
165                 raise
166
167     except RuntimeError as e:
168         os.unlink(ccachename)
169         raise e
170
171
172 class dnsobj(object):
173     """an object to hold a parsed DNS line"""
174
175     def __init__(self, string_form):
176         list = string_form.split()
177         if len(list) < 3:
178             raise Exception("Invalid DNS entry %r" % string_form)
179         self.dest = None
180         self.port = None
181         self.ip = None
182         self.existing_port = None
183         self.existing_weight = None
184         self.existing_cname_target = None
185         self.type = list[0]
186         self.name = list[1]
187         self.nameservers = []
188         if self.type == 'SRV':
189             if len(list) < 4:
190                 raise Exception("Invalid DNS entry %r" % string_form)
191             self.dest = list[2]
192             self.port = list[3]
193         elif self.type in ['A', 'AAAA']:
194             self.ip   = list[2] # usually $IP, which gets replaced
195         elif self.type == 'CNAME':
196             self.dest = list[2]
197         elif self.type == 'NS':
198             self.dest = list[2]
199         else:
200             raise Exception("Received unexpected DNS reply of type %s: %s" % (self.type, string_form))
201
202     def __str__(self):
203         if self.type == "A":
204             return "%s %s %s" % (self.type, self.name, self.ip)
205         if self.type == "AAAA":
206             return "%s %s %s" % (self.type, self.name, self.ip)
207         if self.type == "SRV":
208             return "%s %s %s %s" % (self.type, self.name, self.dest, self.port)
209         if self.type == "CNAME":
210             return "%s %s %s" % (self.type, self.name, self.dest)
211         if self.type == "NS":
212             return "%s %s %s" % (self.type, self.name, self.dest)
213
214
215 def parse_dns_line(line, sub_vars):
216     """parse a DNS line from."""
217     if line.startswith("SRV _ldap._tcp.pdc._msdcs.") and not samdb.am_pdc():
218         # We keep this as compat to the dns_update_list of 4.0/4.1
219         if opts.verbose:
220             print "Skipping PDC entry (%s) as we are not a PDC" % line
221         return None
222     subline = samba.substitute_var(line, sub_vars)
223     if subline == '' or subline[0] == "#":
224         return None
225     return dnsobj(subline)
226
227
228 def hostname_match(h1, h2):
229     """see if two hostnames match."""
230     h1 = str(h1)
231     h2 = str(h2)
232     return h1.lower().rstrip('.') == h2.lower().rstrip('.')
233
234 def check_one_dns_name(name, name_type, d=None):
235     resolv_conf = os.getenv('RESOLV_WRAPPER_CONF')
236     if not resolv_conf:
237         resolv_conf = '/etc/resolv.conf'
238     resolver = dns.resolver.Resolver(filename=resolv_conf, configure=True)
239
240     if d is not None and d.nameservers != []:
241         resolver.nameservers = d.nameservers
242     elif d is not None:
243         d.nameservers = resolver.nameservers
244
245     ans = resolver.query(name, name_type)
246     return ans
247
248 def check_dns_name(d):
249     """check that a DNS entry exists."""
250     normalised_name = d.name.rstrip('.') + '.'
251     if opts.verbose:
252         print "Looking for DNS entry %s as %s" % (d, normalised_name)
253
254     if opts.use_file is not None:
255         try:
256             dns_file = open(opts.use_file, "r")
257         except IOError:
258             return False
259
260         for line in dns_file:
261             line = line.strip()
262             if line == '' or line[0] == "#":
263                 continue
264             if line.lower() == str(d).lower():
265                 return True
266         return False
267
268     try:
269         ans = check_one_dns_name(normalised_name, d.type, d)
270     except dns.exception.DNSException:
271         if opts.verbose:
272             print "Failed to find DNS entry %s" % d
273         return False
274     if d.type in ['A', 'AAAA']:
275         # we need to be sure that our IP is there
276         for rdata in ans:
277             if str(rdata) == str(d.ip):
278                 return True
279     elif d.type == 'CNAME':
280         for i in range(len(ans)):
281             if hostname_match(ans[i].target, d.dest):
282                 return True
283             else:
284                 d.existing_cname_target = str(ans[i].target)
285     elif d.type == 'NS':
286         for i in range(len(ans)):
287             if hostname_match(ans[i].target, d.dest):
288                 return True
289     elif d.type == 'SRV':
290         for rdata in ans:
291             if opts.verbose:
292                 print "Checking %s against %s" % (rdata, d)
293             if hostname_match(rdata.target, d.dest):
294                 if str(rdata.port) == str(d.port):
295                     return True
296                 else:
297                     d.existing_port     = str(rdata.port)
298                     d.existing_weight = str(rdata.weight)
299
300     if opts.verbose:
301         print "Failed to find matching DNS entry %s" % d
302
303     return False
304
305
306 def get_subst_vars(samdb):
307     """get the list of substitution vars."""
308     global lp, am_rodc
309     vars = {}
310
311     vars['DNSDOMAIN'] = samdb.domain_dns_name()
312     vars['DNSFOREST'] = samdb.forest_dns_name()
313     vars['HOSTNAME']  = samdb.host_dns_name()
314     vars['NTDSGUID']  = samdb.get_ntds_GUID()
315     vars['SITE']      = samdb.server_site_name()
316     res = samdb.search(base=samdb.get_default_basedn(), scope=SCOPE_BASE, attrs=["objectGUID"])
317     guid = samdb.schema_format_value("objectGUID", res[0]['objectGUID'][0])
318     vars['DOMAINGUID'] = guid
319
320     vars['IF_DC'] = ""
321     vars['IF_RWDC'] = "# "
322     vars['IF_RODC'] = "# "
323     vars['IF_PDC'] = "# "
324     vars['IF_GC'] = "# "
325     vars['IF_RWGC'] = "# "
326     vars['IF_ROGC'] = "# "
327     vars['IF_DNS_DOMAIN'] = "# "
328     vars['IF_RWDNS_DOMAIN'] = "# "
329     vars['IF_RODNS_DOMAIN'] = "# "
330     vars['IF_DNS_FOREST'] = "# "
331     vars['IF_RWDNS_FOREST'] = "# "
332     vars['IF_R0DNS_FOREST'] = "# "
333
334     am_rodc = samdb.am_rodc()
335     if am_rodc:
336         vars['IF_RODC'] = ""
337     else:
338         vars['IF_RWDC'] = ""
339
340     if samdb.am_pdc():
341         vars['IF_PDC'] = ""
342
343     # check if we "are DNS server"
344     res = samdb.search(base=samdb.get_config_basedn(),
345                    expression='(objectguid=%s)' % vars['NTDSGUID'],
346                    attrs=["options", "msDS-hasMasterNCs"])
347
348     if len(res) == 1:
349         if "options" in res[0]:
350             options = int(res[0]["options"][0])
351             if (options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
352                 vars['IF_GC'] = ""
353                 if am_rodc:
354                     vars['IF_ROGC'] = ""
355                 else:
356                     vars['IF_RWGC'] = ""
357
358         basedn = str(samdb.get_default_basedn())
359         forestdn = str(samdb.get_root_basedn())
360
361         if "msDS-hasMasterNCs" in res[0]:
362             for e in res[0]["msDS-hasMasterNCs"]:
363                 if str(e) == "DC=DomainDnsZones,%s" % basedn:
364                     vars['IF_DNS_DOMAIN'] = ""
365                     if am_rodc:
366                         vars['IF_RODNS_DOMAIN'] = ""
367                     else:
368                         vars['IF_RWDNS_DOMAIN'] = ""
369                 if str(e) == "DC=ForestDnsZones,%s" % forestdn:
370                     vars['IF_DNS_FOREST'] = ""
371                     if am_rodc:
372                         vars['IF_RODNS_FOREST'] = ""
373                     else:
374                         vars['IF_RWDNS_FOREST'] = ""
375
376     return vars
377
378
379 def call_nsupdate(d, op="add"):
380     """call nsupdate for an entry."""
381     global ccachename, nsupdate_cmd, krb5conf
382
383     assert(op in ["add", "delete"])
384
385     if opts.verbose:
386         print "Calling nsupdate for %s (%s)" % (d, op)
387
388     if opts.use_file is not None:
389         try:
390             rfile = open(opts.use_file, 'r+')
391         except IOError:
392             # Perhaps create it
393             rfile = open(opts.use_file, 'w+')
394             # Open it for reading again, in case someone else got to it first
395             rfile = open(opts.use_file, 'r+')
396         fcntl.lockf(rfile, fcntl.LOCK_EX)
397         (file_dir, file_name) = os.path.split(opts.use_file)
398         (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
399         wfile = os.fdopen(tmp_fd, 'a')
400         rfile.seek(0)
401         for line in rfile:
402             if op == "delete":
403                 l = parse_dns_line(line, {})
404                 if str(l).lower() == str(d).lower():
405                     continue
406             wfile.write(line)
407         if op == "add":
408             wfile.write(str(d)+"\n")
409         os.rename(tmpfile, opts.use_file)
410         fcntl.lockf(rfile, fcntl.LOCK_UN)
411         return
412
413     normalised_name = d.name.rstrip('.') + '.'
414
415     (tmp_fd, tmpfile) = tempfile.mkstemp()
416     f = os.fdopen(tmp_fd, 'w')
417     if d.nameservers != []:
418         f.write('server %s\n' % d.nameservers[0])
419     if d.type == "A":
420         f.write("update %s %s %u A %s\n" % (op, normalised_name, default_ttl, d.ip))
421     if d.type == "AAAA":
422         f.write("update %s %s %u AAAA %s\n" % (op, normalised_name, default_ttl, d.ip))
423     if d.type == "SRV":
424         if op == "add" and d.existing_port is not None:
425             f.write("update delete %s SRV 0 %s %s %s\n" % (normalised_name, d.existing_weight,
426                                                            d.existing_port, d.dest))
427         f.write("update %s %s %u SRV 0 100 %s %s\n" % (op, normalised_name, default_ttl, d.port, d.dest))
428     if d.type == "CNAME":
429         f.write("update %s %s %u CNAME %s\n" % (op, normalised_name, default_ttl, d.dest))
430     if d.type == "NS":
431         f.write("update %s %s %u NS %s\n" % (op, normalised_name, default_ttl, d.dest))
432     if opts.verbose:
433         f.write("show\n")
434     f.write("send\n")
435     f.close()
436
437     # Set a bigger MTU size to work around a bug in nsupdate's doio_send()
438     os.environ["SOCKET_WRAPPER_MTU"] = "2000"
439
440     global error_count
441     if ccachename:
442         os.environ["KRB5CCNAME"] = ccachename
443     try:
444         cmd = nsupdate_cmd[:]
445         cmd.append(tmpfile)
446         env = os.environ
447         if krb5conf:
448             env["KRB5_CONFIG"] = krb5conf
449         if ccachename:
450             env["KRB5CCNAME"] = ccachename
451         ret = subprocess.call(cmd, shell=False, env=env)
452         if ret != 0:
453             if opts.fail_immediately:
454                 if opts.verbose:
455                     print("Failed update with %s" % tmpfile)
456                 sys.exit(1)
457             error_count = error_count + 1
458             if opts.verbose:
459                 print("Failed nsupdate: %d" % ret)
460     except Exception, estr:
461         if opts.fail_immediately:
462             sys.exit(1)
463         error_count = error_count + 1
464         if opts.verbose:
465             print("Failed nsupdate: %s : %s" % (str(d), estr))
466     os.unlink(tmpfile)
467
468     # Let socket_wrapper set the default MTU size
469     os.environ["SOCKET_WRAPPER_MTU"] = "0"
470
471
472 def call_samba_tool(d, op="add"):
473     """call samba-tool dns to update an entry."""
474
475     assert(op in ["add", "delete"])
476
477     if (sub_vars['DNSFOREST'] != sub_vars['DNSDOMAIN']) and \
478        sub_vars['DNSFOREST'].endswith('.' + sub_vars['DNSDOMAIN']):
479         print "Refusing to use samba-tool when forest %s is under domain %s" \
480             % (sub_vars['DNSFOREST'], sub_vars['DNSDOMAIN'])
481
482     if opts.verbose:
483         print "Calling samba-tool dns for %s (%s)" % (d, op)
484
485     normalised_name = d.name.rstrip('.') + '.'
486     if normalised_name == (sub_vars['DNSDOMAIN'] + '.'):
487         short_name = '@'
488         zone = sub_vars['DNSDOMAIN']
489     elif normalised_name == (sub_vars['DNSFOREST'] + '.'):
490         short_name = '@'
491         zone = sub_vars['DNSFOREST']
492     elif normalised_name == ('_msdcs.' + sub_vars['DNSFOREST'] + '.'):
493         short_name = '@'
494         zone = '_msdcs.' + sub_vars['DNSFOREST']
495     else:
496         if not normalised_name.endswith('.' + sub_vars['DNSDOMAIN'] + '.'):
497             print "Not Calling samba-tool dns for %s (%s), %s not in %s" % (d, op, normalised_name, sub_vars['DNSDOMAIN'] + '.')
498             return False
499         elif normalised_name.endswith('._msdcs.' + sub_vars['DNSFOREST'] + '.'):
500             zone = '_msdcs.' + sub_vars['DNSFOREST']
501         else:
502             zone = sub_vars['DNSDOMAIN']
503         len_zone = len(zone)+2
504         short_name = normalised_name[:-len_zone]
505
506     if d.type == "A":
507         args = [rpc_server_ip, zone, short_name, "A", d.ip]
508     if d.type == "AAAA":
509         args = [rpc_server_ip, zone, short_name, "AAAA", d.ip]
510     if d.type == "SRV":
511         if op == "add" and d.existing_port is not None:
512             print "Not handling modify of exising SRV %s using samba-tool" % d
513             return False
514             op = "update"
515             args = [rpc_server_ip, zone, short_name, "SRV",
516                     "%s %s %s %s" % (d.existing_weight,
517                                      d.existing_port, "0", "100"),
518                     "%s %s %s %s" % (d.dest, d.port, "0", "100")]
519         else:
520             args = [rpc_server_ip, zone, short_name, "SRV", "%s %s %s %s" % (d.dest, d.port, "0", "100")]
521     if d.type == "CNAME":
522         if d.existing_cname_target is None:
523             args = [rpc_server_ip, zone, short_name, "CNAME", d.dest]
524         else:
525             op = "update"
526             args = [rpc_server_ip, zone, short_name, "CNAME",
527                     d.existing_cname_target.rstrip('.'), d.dest]
528
529     if d.type == "NS":
530         args = [rpc_server_ip, zone, short_name, "NS", d.dest]
531
532     global error_count
533     try:
534         cmd = cmd_dns()
535         if opts.verbose:
536             print "Calling samba-tool dns %s -k no -P %s" % (op, args)
537         cmd._run("dns", op, "-k", "no", "-P", *args)
538     except Exception, estr:
539         raise
540         if opts.fail_immediately:
541             sys.exit(1)
542         error_count = error_count + 1
543         if opts.verbose:
544             print("Failed 'samba-tool dns' based update: %s : %s" % (str(d), estr))
545
546 def rodc_dns_update(d, t, op):
547     '''a single DNS update via the RODC netlogon call'''
548     global sub_vars
549
550     assert(op in ["add", "delete"])
551
552     if opts.verbose:
553         print "Calling netlogon RODC update for %s" % d
554
555     typemap = {
556         netlogon.NlDnsLdapAtSite       : netlogon.NlDnsInfoTypeNone,
557         netlogon.NlDnsGcAtSite         : netlogon.NlDnsDomainNameAlias,
558         netlogon.NlDnsDsaCname         : netlogon.NlDnsDomainNameAlias,
559         netlogon.NlDnsKdcAtSite        : netlogon.NlDnsInfoTypeNone,
560         netlogon.NlDnsDcAtSite         : netlogon.NlDnsInfoTypeNone,
561         netlogon.NlDnsRfc1510KdcAtSite : netlogon.NlDnsInfoTypeNone,
562         netlogon.NlDnsGenericGcAtSite  : netlogon.NlDnsDomainNameAlias
563         }
564
565     w = winbind.winbind("irpc:winbind_server", lp)
566     dns_names = netlogon.NL_DNS_NAME_INFO_ARRAY()
567     dns_names.count = 1
568     name = netlogon.NL_DNS_NAME_INFO()
569     name.type = t
570     name.dns_domain_info_type = typemap[t]
571     name.priority = 0
572     name.weight   = 0
573     if d.port is not None:
574         name.port = int(d.port)
575     if op == "add":
576         name.dns_register = True
577     else:
578         name.dns_register = False
579     dns_names.names = [ name ]
580     site_name = sub_vars['SITE'].decode('utf-8')
581
582     global error_count
583
584     try:
585         ret_names = w.DsrUpdateReadOnlyServerDnsRecords(site_name, default_ttl, dns_names)
586         if ret_names.names[0].status != 0:
587             print("Failed to set DNS entry: %s (status %u)" % (d, ret_names.names[0].status))
588             error_count = error_count + 1
589     except RuntimeError, reason:
590         print("Error setting DNS entry of type %u: %s: %s" % (t, d, reason))
591         error_count = error_count + 1
592
593     if error_count != 0 and opts.fail_immediately:
594         sys.exit(1)
595
596
597 def call_rodc_update(d, op="add"):
598     '''RODCs need to use the netlogon API for nsupdate'''
599     global lp, sub_vars
600
601     assert(op in ["add", "delete"])
602
603     # we expect failure for 3268 if we aren't a GC
604     if d.port is not None and int(d.port) == 3268:
605         return
606
607     # map the DNS request to a netlogon update type
608     map = {
609         netlogon.NlDnsLdapAtSite       : '_ldap._tcp.${SITE}._sites.${DNSDOMAIN}',
610         netlogon.NlDnsGcAtSite         : '_ldap._tcp.${SITE}._sites.gc._msdcs.${DNSDOMAIN}',
611         netlogon.NlDnsDsaCname         : '${NTDSGUID}._msdcs.${DNSFOREST}',
612         netlogon.NlDnsKdcAtSite        : '_kerberos._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
613         netlogon.NlDnsDcAtSite         : '_ldap._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
614         netlogon.NlDnsRfc1510KdcAtSite : '_kerberos._tcp.${SITE}._sites.${DNSDOMAIN}',
615         netlogon.NlDnsGenericGcAtSite  : '_gc._tcp.${SITE}._sites.${DNSFOREST}'
616         }
617
618     for t in map:
619         subname = samba.substitute_var(map[t], sub_vars)
620         if subname.lower() == d.name.lower():
621             # found a match - do the update
622             rodc_dns_update(d, t, op)
623             return
624     if opts.verbose:
625         print("Unable to map to netlogon DNS update: %s" % d)
626
627
628 # get the list of DNS entries we should have
629 if opts.update_list:
630     dns_update_list = opts.update_list
631 else:
632     dns_update_list = lp.private_path('dns_update_list')
633
634 if opts.update_cache:
635     dns_update_cache = opts.update_cache
636 else:
637     dns_update_cache = lp.private_path('dns_update_cache')
638
639 # use our private krb5.conf to avoid problems with the wrong domain
640 # bind9 nsupdate wants the default domain set
641 krb5conf = lp.private_path('krb5.conf')
642 os.environ['KRB5_CONFIG'] = krb5conf
643
644 file = open(dns_update_list, "r")
645
646 if opts.nosubs:
647     sub_vars = {}
648 else:
649     samdb = SamDB(url=lp.samdb_url(), session_info=system_session(), lp=lp)
650
651     # get the substitution dictionary
652     sub_vars = get_subst_vars(samdb)
653
654 # build up a list of update commands to pass to nsupdate
655 update_list = []
656 dns_list = []
657 cache_list = []
658 delete_list = []
659
660 dup_set = set()
661 cache_set = set()
662
663 rebuild_cache = False
664 try:
665     cfile = open(dns_update_cache, 'r+')
666 except IOError:
667     # Perhaps create it
668     cfile = open(dns_update_cache, 'w+')
669     # Open it for reading again, in case someone else got to it first
670     cfile = open(dns_update_cache, 'r+')
671 fcntl.lockf(cfile, fcntl.LOCK_EX)
672 for line in cfile:
673     line = line.strip()
674     if line == '' or line[0] == "#":
675         continue
676     c = parse_dns_line(line, {})
677     if c is None:
678         continue
679     if str(c) not in cache_set:
680         cache_list.append(c)
681         cache_set.add(str(c))
682
683 # read each line, and check that the DNS name exists
684 for line in file:
685     line = line.strip()
686     if line == '' or line[0] == "#":
687         continue
688     d = parse_dns_line(line, sub_vars)
689     if d is None:
690         continue
691     if d.type == 'A' and len(IP4s) == 0:
692         continue
693     if d.type == 'AAAA' and len(IP6s) == 0:
694         continue
695     if str(d) not in dup_set:
696         dns_list.append(d)
697         dup_set.add(str(d))
698
699 # now expand the entries, if any are A record with ip set to $IP
700 # then replace with multiple entries, one for each interface IP
701 for d in dns_list:
702     if d.ip != "$IP":
703         continue
704     if d.type == 'A':
705         d.ip = IP4s[0]
706         for i in range(len(IP4s)-1):
707             d2 = dnsobj(str(d))
708             d2.ip = IP4s[i+1]
709             dns_list.append(d2)
710     if d.type == 'AAAA':
711         d.ip = IP6s[0]
712         for i in range(len(IP6s)-1):
713             d2 = dnsobj(str(d))
714             d2.ip = IP6s[i+1]
715             dns_list.append(d2)
716
717 # now check if the entries already exist on the DNS server
718 for d in dns_list:
719     found = False
720     for c in cache_list:
721         if str(c).lower() == str(d).lower():
722             found = True
723             break
724     if not found:
725         rebuild_cache = True
726     if opts.all_names:
727         update_list.append(d)
728         if opts.verbose:
729             print "force update: %s" % d
730     elif not check_dns_name(d):
731         update_list.append(d)
732         if opts.verbose:
733             print "need update: %s" % d
734
735
736 for c in cache_list:
737     found = False
738     for d in dns_list:
739         if str(c).lower() == str(d).lower():
740             found = True
741             break
742     if found:
743         continue
744     rebuild_cache = True
745     if not opts.all_names and not check_dns_name(c):
746         continue
747     delete_list.append(c)
748     if opts.verbose:
749         print "need delete: %s" % c
750
751 if len(delete_list) == 0 and len(update_list) == 0 and not rebuild_cache:
752     if opts.verbose:
753         print "No DNS updates needed"
754     sys.exit(0)
755 else:
756     if opts.verbose:
757         print "%d DNS updates and %d DNS deletes needed" % (len(update_list), len(delete_list))
758
759 use_samba_tool = opts.use_samba_tool
760 use_nsupdate = opts.use_nsupdate
761 # get our krb5 creds
762 if len(delete_list) != 0 or len(update_list) != 0 and not opts.nocreds:
763     try:
764         creds = get_credentials(lp)
765     except RuntimeError as e:
766         ccachename = None
767
768         if sub_vars['IF_RWDNS_DOMAIN'] == "# ":
769             raise
770
771         if use_nsupdate:
772             raise
773
774         print "Failed to get Kerberos credentials, falling back to samba-tool: %s" % e
775         use_samba_tool = True
776
777
778 # ask nsupdate to delete entries as needed
779 for d in delete_list:
780     if not use_nsupdate and use_samba_tool:
781         call_samba_tool(d, op="delete")
782
783     elif am_rodc:
784         if d.name.lower() == domain.lower():
785             if opts.verbose:
786                 print "skip delete (rodc): %s" % d
787             continue
788         if not d.type in [ 'A', 'AAAA' ]:
789             if opts.verbose:
790                 print "delete (rodc): %s" % d
791             call_rodc_update(d, op="delete")
792         else:
793             if opts.verbose:
794                 print "delete (nsupdate): %s" % d
795             call_nsupdate(d, op="delete")
796     else:
797         if opts.verbose:
798             print "delete (nsupdate): %s" % d
799         call_nsupdate(d, op="delete")
800
801 # ask nsupdate to add entries as needed
802 for d in update_list:
803     if not use_nsupdate and use_samba_tool:
804         call_samba_tool(d)
805
806     elif am_rodc:
807         if d.name.lower() == domain.lower():
808             if opts.verbose:
809                 print "skip (rodc): %s" % d
810             continue
811         if not d.type in [ 'A', 'AAAA' ]:
812             if opts.verbose:
813                 print "update (rodc): %s" % d
814             call_rodc_update(d)
815         else:
816             if opts.verbose:
817                 print "update (nsupdate): %s" % d
818             call_nsupdate(d)
819     else:
820         if opts.verbose:
821             print "update(nsupdate): %s" % d
822         call_nsupdate(d)
823
824 if rebuild_cache:
825     (file_dir, file_name) = os.path.split(dns_update_cache)
826     (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
827     wfile = os.fdopen(tmp_fd, 'a')
828     for d in dns_list:
829         wfile.write(str(d)+"\n")
830     os.rename(tmpfile, dns_update_cache)
831 fcntl.lockf(cfile, fcntl.LOCK_UN)
832
833 # delete the ccache if we created it
834 if ccachename is not None:
835     os.unlink(ccachename)
836
837 if error_count != 0:
838     print("Failed update of %u entries" % error_count)
839 sys.exit(error_count)