dns: update tool changed for scavenging
[kai/samba-autobuild/.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 from samba.kcc import kcc_utils
52 import ldb
53
54 samba.ensure_third_party_module("dns", "dnspython")
55 import dns.resolver
56 import dns.exception
57
58 default_ttl = 900
59 am_rodc = False
60 error_count = 0
61
62 parser = optparse.OptionParser("samba_dnsupdate")
63 sambaopts = options.SambaOptions(parser)
64 parser.add_option_group(sambaopts)
65 parser.add_option_group(options.VersionOptions(parser))
66 parser.add_option("--verbose", action="store_true")
67 parser.add_option("--use-samba-tool", action="store_true", help="Use samba-tool to make updates over RPC, rather than over DNS")
68 parser.add_option("--use-nsupdate", action="store_true", help="Use nsupdate command to make updates over DNS (default, if kinit successful)")
69 parser.add_option("--all-names", action="store_true")
70 parser.add_option("--all-interfaces", action="store_true")
71 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=)")
72 parser.add_option("--rpc-server-ip", type="string", help="IP address of server to use with samba-tool (defaults to first --current-ip)")
73 parser.add_option("--use-file", type="string", help="Use a file, rather than real DNS calls")
74 parser.add_option("--update-list", type="string", help="Add DNS names from the given file")
75 parser.add_option("--update-cache", type="string", help="Cache database of already registered records")
76 parser.add_option("--fail-immediately", action='store_true', help="Exit on first failure")
77 parser.add_option("--no-credentials", dest='nocreds', action='store_true', help="don't try and get credentials")
78 parser.add_option("--no-substitutions", dest='nosubs', action='store_true', help="don't try and expands variables in file specified by --update-list")
79
80 creds = None
81 ccachename = None
82
83 opts, args = parser.parse_args()
84
85 if len(args) != 0:
86     parser.print_usage()
87     sys.exit(1)
88
89 lp = sambaopts.get_loadparm()
90
91 domain = lp.get("realm")
92 host = lp.get("netbios name")
93 if opts.all_interfaces:
94     all_interfaces = True
95 else:
96     all_interfaces = False
97
98 if opts.current_ip:
99     IPs = opts.current_ip
100 else:
101     IPs = samba.interface_ips(lp, all_interfaces)
102
103 nsupdate_cmd = lp.get('nsupdate command')
104
105 if len(IPs) == 0:
106     print "No IP interfaces - skipping DNS updates"
107     sys.exit(0)
108
109 if opts.rpc_server_ip:
110     rpc_server_ip = opts.rpc_server_ip
111 else:
112     rpc_server_ip = IPs[0]
113
114 IP6s = []
115 IP4s = []
116 for i in IPs:
117     if i.find(':') != -1:
118         IP6s.append(i)
119     else:
120         IP4s.append(i)
121
122
123 if opts.verbose:
124     print "IPs: %s" % IPs
125
126 def get_possible_rw_dns_server(creds, domain):
127     """Get a list of possible read-write DNS servers, starting with
128        the SOA.  The SOA is the correct answer, but old Samba domains
129        (4.6 and prior) do not maintain this value, so add NS servers
130        as well"""
131
132     hostnames = []
133     ans_soa = check_one_dns_name(domain, 'SOA')
134
135     # Actually there is only one
136     for i in range(len(ans_soa)):
137         hostnames.append(str(ans_soa[i].mname).rstrip('.'))
138
139     # This is not strictly legit, but old Samba domains may have an
140     # unmaintained SOA record, so go for any NS that we can get a
141     # ticket to.
142     ans_ns = check_one_dns_name(domain, 'NS')
143
144     # Actually there is only one
145     for i in range(len(ans_ns)):
146         hostnames.append(str(ans_ns[i].target).rstrip('.'))
147
148     return hostnames
149
150 def get_krb5_rw_dns_server(creds, domain):
151     """Get a list of read-write DNS servers that we can obtain a ticket
152        for, starting with the SOA.  The SOA is the correct answer, but
153        old Samba domains (4.6 and prior) do not maintain this value,
154        so continue with the NS servers as well until we get one that
155        the KDC will issue a ticket to.
156     """
157
158     rw_dns_servers = get_possible_rw_dns_server(creds, domain)
159     # Actually there is only one
160     for i in range(len(rw_dns_servers)):
161         target_hostname = str(rw_dns_servers[i])
162         settings = {}
163         settings["lp_ctx"] = lp
164         settings["target_hostname"] = target_hostname
165
166         gensec_client = gensec.Security.start_client(settings)
167         gensec_client.set_credentials(creds)
168         gensec_client.set_target_service("DNS")
169         gensec_client.set_target_hostname(target_hostname)
170         gensec_client.want_feature(gensec.FEATURE_SEAL)
171         gensec_client.start_mech_by_sasl_name("GSSAPI")
172         server_to_client = ""
173         try:
174             (client_finished, client_to_server) = gensec_client.update(server_to_client)
175             if opts.verbose:
176                 print "Successfully obtained Kerberos ticket to DNS/%s as %s" \
177                     % (target_hostname, creds.get_username())
178             return target_hostname
179         except RuntimeError:
180             # Only raise an exception if they all failed
181             if i != len(rw_dns_servers) - 1:
182                 pass
183             raise
184
185 def get_credentials(lp):
186     """# get credentials if we haven't got them already."""
187     from samba import credentials
188     global ccachename
189     creds = credentials.Credentials()
190     creds.guess(lp)
191     creds.set_machine_account(lp)
192     creds.set_krb_forwardable(credentials.NO_KRB_FORWARDABLE)
193     (tmp_fd, ccachename) = tempfile.mkstemp()
194     try:
195         creds.get_named_ccache(lp, ccachename)
196
197         if opts.use_file is not None:
198             return
199
200         # Now confirm we can get a ticket to the DNS server
201         get_krb5_rw_dns_server(creds, sub_vars['DNSDOMAIN'] + '.')
202         return creds
203
204     except RuntimeError as e:
205         os.unlink(ccachename)
206         raise e
207
208
209 class dnsobj(object):
210     """an object to hold a parsed DNS line"""
211
212     def __init__(self, string_form):
213         list = string_form.split()
214         if len(list) < 3:
215             raise Exception("Invalid DNS entry %r" % string_form)
216         self.dest = None
217         self.port = None
218         self.ip = None
219         self.existing_port = None
220         self.existing_weight = None
221         self.existing_cname_target = None
222         self.rpc = False
223         self.zone = None
224         if list[0] == "RPC":
225             self.rpc = True
226             self.zone = list[1]
227             list = list[2:]
228         self.type = list[0]
229         self.name = list[1]
230         self.nameservers = []
231         if self.type == 'SRV':
232             if len(list) < 4:
233                 raise Exception("Invalid DNS entry %r" % string_form)
234             self.dest = list[2]
235             self.port = list[3]
236         elif self.type in ['A', 'AAAA']:
237             self.ip   = list[2] # usually $IP, which gets replaced
238         elif self.type == 'CNAME':
239             self.dest = list[2]
240         elif self.type == 'NS':
241             self.dest = list[2]
242         else:
243             raise Exception("Received unexpected DNS reply of type %s: %s" % (self.type, string_form))
244
245     def __str__(self):
246         if self.type == "A":
247             return "%s %s %s" % (self.type, self.name, self.ip)
248         if self.type == "AAAA":
249             return "%s %s %s" % (self.type, self.name, self.ip)
250         if self.type == "SRV":
251             return "%s %s %s %s" % (self.type, self.name, self.dest, self.port)
252         if self.type == "CNAME":
253             return "%s %s %s" % (self.type, self.name, self.dest)
254         if self.type == "NS":
255             return "%s %s %s" % (self.type, self.name, self.dest)
256
257
258 def parse_dns_line(line, sub_vars):
259     """parse a DNS line from."""
260     if line.startswith("SRV _ldap._tcp.pdc._msdcs.") and not samdb.am_pdc():
261         # We keep this as compat to the dns_update_list of 4.0/4.1
262         if opts.verbose:
263             print "Skipping PDC entry (%s) as we are not a PDC" % line
264         return None
265     subline = samba.substitute_var(line, sub_vars)
266     if subline == '' or subline[0] == "#":
267         return None
268     return dnsobj(subline)
269
270
271 def hostname_match(h1, h2):
272     """see if two hostnames match."""
273     h1 = str(h1)
274     h2 = str(h2)
275     return h1.lower().rstrip('.') == h2.lower().rstrip('.')
276
277 def get_resolver(d=None):
278     resolv_conf = os.getenv('RESOLV_CONF')
279     if not resolv_conf:
280         resolv_conf = '/etc/resolv.conf'
281     resolver = dns.resolver.Resolver(filename=resolv_conf, configure=True)
282
283     if d is not None and d.nameservers != []:
284         resolver.nameservers = d.nameservers
285
286     return resolver
287
288 def check_one_dns_name(name, name_type, d=None):
289     resolver = get_resolver(d)
290     if d is not None and len(d.nameservers) == 0:
291         d.nameservers = resolver.nameservers
292
293     ans = resolver.query(name, name_type)
294     return ans
295
296 def check_dns_name(d):
297     """check that a DNS entry exists."""
298     normalised_name = d.name.rstrip('.') + '.'
299     if opts.verbose:
300         print "Looking for DNS entry %s as %s" % (d, normalised_name)
301
302     if opts.use_file is not None:
303         try:
304             dns_file = open(opts.use_file, "r")
305         except IOError:
306             return False
307
308         for line in dns_file:
309             line = line.strip()
310             if line == '' or line[0] == "#":
311                 continue
312             if line.lower() == str(d).lower():
313                 return True
314         return False
315
316     try:
317         ans = check_one_dns_name(normalised_name, d.type, d)
318     except dns.exception.Timeout:
319         raise Exception("Timeout while waiting to contact a working DNS server while looking for %s as %s" % (d, normalised_name))
320     except dns.resolver.NoNameservers:
321         raise Exception("Unable to contact a working DNS server while looking for %s as %s" % (d, normalised_name))
322     except dns.resolver.NXDOMAIN:
323         if opts.verbose:
324             print "The DNS entry %s, queried as %s does not exist" % (d, normalised_name)
325         return False
326     except dns.resolver.NoAnswer:
327         if opts.verbose:
328             print "The DNS entry %s, queried as %s does not hold this record type" % (d, normalised_name)
329         return False
330     except dns.exception.DNSException:
331         raise Exception("Failure while trying to resolve %s as %s" % (d, normalised_name))
332     if d.type in ['A', 'AAAA']:
333         # we need to be sure that our IP is there
334         for rdata in ans:
335             if str(rdata) == str(d.ip):
336                 return True
337     elif d.type == 'CNAME':
338         for i in range(len(ans)):
339             if hostname_match(ans[i].target, d.dest):
340                 return True
341             else:
342                 d.existing_cname_target = str(ans[i].target)
343     elif d.type == 'NS':
344         for i in range(len(ans)):
345             if hostname_match(ans[i].target, d.dest):
346                 return True
347     elif d.type == 'SRV':
348         for rdata in ans:
349             if opts.verbose:
350                 print "Checking %s against %s" % (rdata, d)
351             if hostname_match(rdata.target, d.dest):
352                 if str(rdata.port) == str(d.port):
353                     return True
354                 else:
355                     d.existing_port     = str(rdata.port)
356                     d.existing_weight = str(rdata.weight)
357
358     if opts.verbose:
359         print "Lookup of %s succeeded, but we failed to find a matching DNS entry for %s" % (normalised_name, d)
360
361     return False
362
363
364 def get_subst_vars(samdb):
365     """get the list of substitution vars."""
366     global lp, am_rodc
367     vars = {}
368
369     vars['DNSDOMAIN'] = samdb.domain_dns_name()
370     vars['DNSFOREST'] = samdb.forest_dns_name()
371     vars['HOSTNAME']  = samdb.host_dns_name()
372     vars['NTDSGUID']  = samdb.get_ntds_GUID()
373     vars['SITE']      = samdb.server_site_name()
374     res = samdb.search(base=samdb.get_default_basedn(), scope=SCOPE_BASE, attrs=["objectGUID"])
375     guid = samdb.schema_format_value("objectGUID", res[0]['objectGUID'][0])
376     vars['DOMAINGUID'] = guid
377
378     vars['IF_DC'] = ""
379     vars['IF_RWDC'] = "# "
380     vars['IF_RODC'] = "# "
381     vars['IF_PDC'] = "# "
382     vars['IF_GC'] = "# "
383     vars['IF_RWGC'] = "# "
384     vars['IF_ROGC'] = "# "
385     vars['IF_DNS_DOMAIN'] = "# "
386     vars['IF_RWDNS_DOMAIN'] = "# "
387     vars['IF_RODNS_DOMAIN'] = "# "
388     vars['IF_DNS_FOREST'] = "# "
389     vars['IF_RWDNS_FOREST'] = "# "
390     vars['IF_R0DNS_FOREST'] = "# "
391
392     am_rodc = samdb.am_rodc()
393     if am_rodc:
394         vars['IF_RODC'] = ""
395     else:
396         vars['IF_RWDC'] = ""
397
398     if samdb.am_pdc():
399         vars['IF_PDC'] = ""
400
401     # check if we "are DNS server"
402     res = samdb.search(base=samdb.get_config_basedn(),
403                    expression='(objectguid=%s)' % vars['NTDSGUID'],
404                    attrs=["options", "msDS-hasMasterNCs"])
405
406     if len(res) == 1:
407         if "options" in res[0]:
408             options = int(res[0]["options"][0])
409             if (options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
410                 vars['IF_GC'] = ""
411                 if am_rodc:
412                     vars['IF_ROGC'] = ""
413                 else:
414                     vars['IF_RWGC'] = ""
415
416         basedn = str(samdb.get_default_basedn())
417         forestdn = str(samdb.get_root_basedn())
418
419         if "msDS-hasMasterNCs" in res[0]:
420             for e in res[0]["msDS-hasMasterNCs"]:
421                 if str(e) == "DC=DomainDnsZones,%s" % basedn:
422                     vars['IF_DNS_DOMAIN'] = ""
423                     if am_rodc:
424                         vars['IF_RODNS_DOMAIN'] = ""
425                     else:
426                         vars['IF_RWDNS_DOMAIN'] = ""
427                 if str(e) == "DC=ForestDnsZones,%s" % forestdn:
428                     vars['IF_DNS_FOREST'] = ""
429                     if am_rodc:
430                         vars['IF_RODNS_FOREST'] = ""
431                     else:
432                         vars['IF_RWDNS_FOREST'] = ""
433
434     return vars
435
436
437 def call_nsupdate(d, op="add"):
438     """call nsupdate for an entry."""
439     global ccachename, nsupdate_cmd, krb5conf
440
441     assert(op in ["add", "delete"])
442
443     if opts.verbose:
444         print "Calling nsupdate for %s (%s)" % (d, op)
445
446     if opts.use_file is not None:
447         try:
448             rfile = open(opts.use_file, 'r+')
449         except IOError:
450             # Perhaps create it
451             rfile = open(opts.use_file, 'w+')
452             # Open it for reading again, in case someone else got to it first
453             rfile = open(opts.use_file, 'r+')
454         fcntl.lockf(rfile, fcntl.LOCK_EX)
455         (file_dir, file_name) = os.path.split(opts.use_file)
456         (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
457         wfile = os.fdopen(tmp_fd, 'a')
458         rfile.seek(0)
459         for line in rfile:
460             if op == "delete":
461                 l = parse_dns_line(line, {})
462                 if str(l).lower() == str(d).lower():
463                     continue
464             wfile.write(line)
465         if op == "add":
466             wfile.write(str(d)+"\n")
467         os.rename(tmpfile, opts.use_file)
468         fcntl.lockf(rfile, fcntl.LOCK_UN)
469         return
470
471     normalised_name = d.name.rstrip('.') + '.'
472
473     (tmp_fd, tmpfile) = tempfile.mkstemp()
474     f = os.fdopen(tmp_fd, 'w')
475
476     # Getting this line right is really important.  When we are under
477     # resolv_wrapper, then we want to use RESOLV_CONF and the
478     # nameserver therein. The issue is that this parameter forces us
479     # to only ever use that server, and not some other server that the
480     # NS record may point to, even as we get a ticket to that other
481     # server.
482     #
483     # Therefore we must not set this in production, instead we want
484     # to find the name of a SOA for the zone and use that server.
485
486     if os.getenv('RESOLV_CONF') and d.nameservers != []:
487         f.write('server %s\n' % d.nameservers[0])
488     else:
489         resolver = get_resolver(d)
490
491         # Local the zone for this name
492         zone = dns.resolver.zone_for_name(normalised_name,
493                                           resolver=resolver)
494
495         # Now find the SOA, or if we can't get a ticket to the SOA,
496         # any server with an NS record we can get a ticket for.
497         #
498         # Thanks to the Kerberos Credentials cache this is not
499         # expensive inside the loop
500         server = get_krb5_rw_dns_server(creds, zone)
501         f.write('server %s\n' % server)
502
503     if d.type == "A":
504         f.write("update %s %s %u A %s\n" % (op, normalised_name, default_ttl, d.ip))
505     if d.type == "AAAA":
506         f.write("update %s %s %u AAAA %s\n" % (op, normalised_name, default_ttl, d.ip))
507     if d.type == "SRV":
508         if op == "add" and d.existing_port is not None:
509             f.write("update delete %s SRV 0 %s %s %s\n" % (normalised_name, d.existing_weight,
510                                                            d.existing_port, d.dest))
511         f.write("update %s %s %u SRV 0 100 %s %s\n" % (op, normalised_name, default_ttl, d.port, d.dest))
512     if d.type == "CNAME":
513         f.write("update %s %s %u CNAME %s\n" % (op, normalised_name, default_ttl, d.dest))
514     if d.type == "NS":
515         f.write("update %s %s %u NS %s\n" % (op, normalised_name, default_ttl, d.dest))
516     if opts.verbose:
517         f.write("show\n")
518     f.write("send\n")
519     f.close()
520
521     # Set a bigger MTU size to work around a bug in nsupdate's doio_send()
522     os.environ["SOCKET_WRAPPER_MTU"] = "2000"
523
524     global error_count
525     if ccachename:
526         os.environ["KRB5CCNAME"] = ccachename
527     try:
528         cmd = nsupdate_cmd[:]
529         cmd.append(tmpfile)
530         env = os.environ
531         if krb5conf:
532             env["KRB5_CONFIG"] = krb5conf
533         if ccachename:
534             env["KRB5CCNAME"] = ccachename
535         ret = subprocess.call(cmd, shell=False, env=env)
536         if ret != 0:
537             if opts.fail_immediately:
538                 if opts.verbose:
539                     print("Failed update with %s" % tmpfile)
540                 sys.exit(1)
541             error_count = error_count + 1
542             if opts.verbose:
543                 print("Failed nsupdate: %d" % ret)
544     except Exception as estr:
545         if opts.fail_immediately:
546             sys.exit(1)
547         error_count = error_count + 1
548         if opts.verbose:
549             print("Failed nsupdate: %s : %s" % (str(d), estr))
550     os.unlink(tmpfile)
551
552     # Let socket_wrapper set the default MTU size
553     os.environ["SOCKET_WRAPPER_MTU"] = "0"
554
555
556 def call_samba_tool(d, op="add", zone=None):
557     """call samba-tool dns to update an entry."""
558
559     assert(op in ["add", "delete"])
560
561     if (sub_vars['DNSFOREST'] != sub_vars['DNSDOMAIN']) and \
562        sub_vars['DNSFOREST'].endswith('.' + sub_vars['DNSDOMAIN']):
563         print "Refusing to use samba-tool when forest %s is under domain %s" \
564             % (sub_vars['DNSFOREST'], sub_vars['DNSDOMAIN'])
565
566     if opts.verbose:
567         print "Calling samba-tool dns for %s (%s)" % (d, op)
568
569     normalised_name = d.name.rstrip('.') + '.'
570     if zone is None:
571         if normalised_name == (sub_vars['DNSDOMAIN'] + '.'):
572             short_name = '@'
573             zone = sub_vars['DNSDOMAIN']
574         elif normalised_name == (sub_vars['DNSFOREST'] + '.'):
575             short_name = '@'
576             zone = sub_vars['DNSFOREST']
577         elif normalised_name == ('_msdcs.' + sub_vars['DNSFOREST'] + '.'):
578             short_name = '@'
579             zone = '_msdcs.' + sub_vars['DNSFOREST']
580         else:
581             if not normalised_name.endswith('.' + sub_vars['DNSDOMAIN'] + '.'):
582                 print "Not Calling samba-tool dns for %s (%s), %s not in %s" % (d, op, normalised_name, sub_vars['DNSDOMAIN'] + '.')
583                 return False
584             elif normalised_name.endswith('._msdcs.' + sub_vars['DNSFOREST'] + '.'):
585                 zone = '_msdcs.' + sub_vars['DNSFOREST']
586             else:
587                 zone = sub_vars['DNSDOMAIN']
588             len_zone = len(zone)+2
589             short_name = normalised_name[:-len_zone]
590     else:
591         len_zone = len(zone)+2
592         short_name = normalised_name[:-len_zone]
593
594     if d.type == "A":
595         args = [rpc_server_ip, zone, short_name, "A", d.ip]
596     if d.type == "AAAA":
597         args = [rpc_server_ip, zone, short_name, "AAAA", d.ip]
598     if d.type == "SRV":
599         if op == "add" and d.existing_port is not None:
600             print "Not handling modify of exising SRV %s using samba-tool" % d
601             return False
602             op = "update"
603             args = [rpc_server_ip, zone, short_name, "SRV",
604                     "%s %s %s %s" % (d.existing_weight,
605                                      d.existing_port, "0", "100"),
606                     "%s %s %s %s" % (d.dest, d.port, "0", "100")]
607         else:
608             args = [rpc_server_ip, zone, short_name, "SRV", "%s %s %s %s" % (d.dest, d.port, "0", "100")]
609     if d.type == "CNAME":
610         if d.existing_cname_target is None:
611             args = [rpc_server_ip, zone, short_name, "CNAME", d.dest]
612         else:
613             op = "update"
614             args = [rpc_server_ip, zone, short_name, "CNAME",
615                     d.existing_cname_target.rstrip('.'), d.dest]
616
617     if d.type == "NS":
618         args = [rpc_server_ip, zone, short_name, "NS", d.dest]
619
620     global error_count
621     try:
622         cmd = cmd_dns()
623         if opts.verbose:
624             print "Calling samba-tool dns %s -k no -P %s" % (op, args)
625         ret = cmd._run("dns", op, "-k", "no", "-P", *args)
626         if ret == -1:
627             if opts.fail_immediately:
628                 sys.exit(1)
629             error_count = error_count + 1
630             if opts.verbose:
631                 print("Failed 'samba-tool dns' based update of %s" % (str(d)))
632     except Exception as estr:
633         if opts.fail_immediately:
634             sys.exit(1)
635         error_count = error_count + 1
636         if opts.verbose:
637             print("Failed 'samba-tool dns' based update: %s : %s" % (str(d), estr))
638         raise
639
640 def rodc_dns_update(d, t, op):
641     '''a single DNS update via the RODC netlogon call'''
642     global sub_vars
643
644     assert(op in ["add", "delete"])
645
646     if opts.verbose:
647         print "Calling netlogon RODC update for %s" % d
648
649     typemap = {
650         netlogon.NlDnsLdapAtSite       : netlogon.NlDnsInfoTypeNone,
651         netlogon.NlDnsGcAtSite         : netlogon.NlDnsDomainNameAlias,
652         netlogon.NlDnsDsaCname         : netlogon.NlDnsDomainNameAlias,
653         netlogon.NlDnsKdcAtSite        : netlogon.NlDnsInfoTypeNone,
654         netlogon.NlDnsDcAtSite         : netlogon.NlDnsInfoTypeNone,
655         netlogon.NlDnsRfc1510KdcAtSite : netlogon.NlDnsInfoTypeNone,
656         netlogon.NlDnsGenericGcAtSite  : netlogon.NlDnsDomainNameAlias
657         }
658
659     w = winbind.winbind("irpc:winbind_server", lp)
660     dns_names = netlogon.NL_DNS_NAME_INFO_ARRAY()
661     dns_names.count = 1
662     name = netlogon.NL_DNS_NAME_INFO()
663     name.type = t
664     name.dns_domain_info_type = typemap[t]
665     name.priority = 0
666     name.weight   = 0
667     if d.port is not None:
668         name.port = int(d.port)
669     if op == "add":
670         name.dns_register = True
671     else:
672         name.dns_register = False
673     dns_names.names = [ name ]
674     site_name = sub_vars['SITE'].decode('utf-8')
675
676     global error_count
677
678     try:
679         ret_names = w.DsrUpdateReadOnlyServerDnsRecords(site_name, default_ttl, dns_names)
680         if ret_names.names[0].status != 0:
681             print("Failed to set DNS entry: %s (status %u)" % (d, ret_names.names[0].status))
682             error_count = error_count + 1
683     except RuntimeError as reason:
684         print("Error setting DNS entry of type %u: %s: %s" % (t, d, reason))
685         error_count = error_count + 1
686
687     if error_count != 0 and opts.fail_immediately:
688         sys.exit(1)
689
690
691 def call_rodc_update(d, op="add"):
692     '''RODCs need to use the netlogon API for nsupdate'''
693     global lp, sub_vars
694
695     assert(op in ["add", "delete"])
696
697     # we expect failure for 3268 if we aren't a GC
698     if d.port is not None and int(d.port) == 3268:
699         return
700
701     # map the DNS request to a netlogon update type
702     map = {
703         netlogon.NlDnsLdapAtSite       : '_ldap._tcp.${SITE}._sites.${DNSDOMAIN}',
704         netlogon.NlDnsGcAtSite         : '_ldap._tcp.${SITE}._sites.gc._msdcs.${DNSDOMAIN}',
705         netlogon.NlDnsDsaCname         : '${NTDSGUID}._msdcs.${DNSFOREST}',
706         netlogon.NlDnsKdcAtSite        : '_kerberos._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
707         netlogon.NlDnsDcAtSite         : '_ldap._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
708         netlogon.NlDnsRfc1510KdcAtSite : '_kerberos._tcp.${SITE}._sites.${DNSDOMAIN}',
709         netlogon.NlDnsGenericGcAtSite  : '_gc._tcp.${SITE}._sites.${DNSFOREST}'
710         }
711
712     for t in map:
713         subname = samba.substitute_var(map[t], sub_vars)
714         if subname.lower() == d.name.lower():
715             # found a match - do the update
716             rodc_dns_update(d, t, op)
717             return
718     if opts.verbose:
719         print("Unable to map to netlogon DNS update: %s" % d)
720
721
722 # get the list of DNS entries we should have
723 if opts.update_list:
724     dns_update_list = opts.update_list
725 else:
726     dns_update_list = lp.private_path('dns_update_list')
727
728 if opts.update_cache:
729     dns_update_cache = opts.update_cache
730 else:
731     dns_update_cache = lp.private_path('dns_update_cache')
732
733 krb5conf = None
734 # only change the krb5.conf if we are not in selftest
735 if 'SOCKET_WRAPPER_DIR' not in os.environ:
736     # use our private krb5.conf to avoid problems with the wrong domain
737     # bind9 nsupdate wants the default domain set
738     krb5conf = lp.private_path('krb5.conf')
739     os.environ['KRB5_CONFIG'] = krb5conf
740
741 file = open(dns_update_list, "r")
742
743 if opts.nosubs:
744     sub_vars = {}
745 else:
746     samdb = SamDB(url=lp.samdb_url(), session_info=system_session(), lp=lp)
747
748     # get the substitution dictionary
749     sub_vars = get_subst_vars(samdb)
750
751 # build up a list of update commands to pass to nsupdate
752 update_list = []
753 dns_list = []
754 cache_list = []
755 delete_list = []
756
757 dup_set = set()
758 cache_set = set()
759
760 rebuild_cache = False
761 try:
762     cfile = open(dns_update_cache, 'r+')
763 except IOError:
764     # Perhaps create it
765     cfile = open(dns_update_cache, 'w+')
766     # Open it for reading again, in case someone else got to it first
767     cfile = open(dns_update_cache, 'r+')
768 fcntl.lockf(cfile, fcntl.LOCK_EX)
769 for line in cfile:
770     line = line.strip()
771     if line == '' or line[0] == "#":
772         continue
773     c = parse_dns_line(line, {})
774     if c is None:
775         continue
776     if str(c) not in cache_set:
777         cache_list.append(c)
778         cache_set.add(str(c))
779
780 site_specific_rec = []
781
782 # read each line, and check that the DNS name exists
783 for line in file:
784     line = line.strip()
785
786     if '${SITE}' in line:
787         site_specific_rec.append(line)
788
789     if line == '' or line[0] == "#":
790         continue
791     d = parse_dns_line(line, sub_vars)
792     if d is None:
793         continue
794     if d.type == 'A' and len(IP4s) == 0:
795         continue
796     if d.type == 'AAAA' and len(IP6s) == 0:
797         continue
798     if str(d) not in dup_set:
799         dns_list.append(d)
800         dup_set.add(str(d))
801
802 # Perform automatic site coverage by default
803 auto_coverage = True
804
805 if not am_rodc and auto_coverage:
806     site_names = kcc_utils.uncovered_sites_to_cover(samdb,
807                                                     samdb.server_site_name())
808
809     # Duplicate all site specific records for the uncovered site
810     for site in site_names:
811         to_add = [samba.substitute_var(line, {'SITE': site})
812                   for line in site_specific_rec]
813
814         for site_line in to_add:
815             d = parse_dns_line(site_line,
816                                sub_vars=sub_vars)
817             if d is not None and str(d) not in dup_set:
818                 dns_list.append(d)
819                 dup_set.add(str(d))
820
821 # now expand the entries, if any are A record with ip set to $IP
822 # then replace with multiple entries, one for each interface IP
823 for d in dns_list:
824     if d.ip != "$IP":
825         continue
826     if d.type == 'A':
827         d.ip = IP4s[0]
828         for i in range(len(IP4s)-1):
829             d2 = dnsobj(str(d))
830             d2.ip = IP4s[i+1]
831             dns_list.append(d2)
832     if d.type == 'AAAA':
833         d.ip = IP6s[0]
834         for i in range(len(IP6s)-1):
835             d2 = dnsobj(str(d))
836             d2.ip = IP6s[i+1]
837             dns_list.append(d2)
838
839 # now check if the entries already exist on the DNS server
840 for d in dns_list:
841     found = False
842     for c in cache_list:
843         if str(c).lower() == str(d).lower():
844             found = True
845             break
846     if not found:
847         rebuild_cache = True
848         if opts.verbose:
849             print "need cache add: %s" % d
850     update_list.append(d)
851
852 for c in cache_list:
853     found = False
854     for d in dns_list:
855         if str(c).lower() == str(d).lower():
856             found = True
857             break
858     if found:
859         continue
860     rebuild_cache = True
861     if opts.verbose:
862         print "need cache remove: %s" % c
863     if not opts.all_names and not check_dns_name(c):
864         continue
865     delete_list.append(c)
866     if opts.verbose:
867         print "need delete: %s" % c
868
869 if len(delete_list) == 0 and len(update_list) == 0 and not rebuild_cache:
870     if opts.verbose:
871         print "No DNS updates needed"
872     sys.exit(0)
873 else:
874     if opts.verbose:
875         print "%d DNS updates and %d DNS deletes needed" % (len(update_list), len(delete_list))
876
877 use_samba_tool = opts.use_samba_tool
878 use_nsupdate = opts.use_nsupdate
879 # get our krb5 creds
880 if len(delete_list) != 0 or len(update_list) != 0 and not opts.nocreds:
881     try:
882         creds = get_credentials(lp)
883     except RuntimeError as e:
884         ccachename = None
885
886         if sub_vars['IF_RWDNS_DOMAIN'] == "# ":
887             raise
888
889         if use_nsupdate:
890             raise
891
892         print "Failed to get Kerberos credentials, falling back to samba-tool: %s" % e
893         use_samba_tool = True
894
895
896 # ask nsupdate to delete entries as needed
897 for d in delete_list:
898     if d.rpc or (not use_nsupdate and use_samba_tool):
899         if opts.verbose:
900             print "update (samba-tool): %s" % d
901         call_samba_tool(d, op="delete", zone=d.zone)
902
903     elif am_rodc:
904         if d.name.lower() == domain.lower():
905             if opts.verbose:
906                 print "skip delete (rodc): %s" % d
907             continue
908         if not d.type in [ 'A', 'AAAA' ]:
909             if opts.verbose:
910                 print "delete (rodc): %s" % d
911             call_rodc_update(d, op="delete")
912         else:
913             if opts.verbose:
914                 print "delete (nsupdate): %s" % d
915             call_nsupdate(d, op="delete")
916     else:
917         if opts.verbose:
918             print "delete (nsupdate): %s" % d
919         call_nsupdate(d, op="delete")
920
921 # ask nsupdate to add entries as needed
922 for d in update_list:
923     if d.rpc or (not use_nsupdate and use_samba_tool):
924         if opts.verbose:
925             print "update (samba-tool): %s" % d
926         call_samba_tool(d, zone=d.zone)
927
928     elif am_rodc:
929         if d.name.lower() == domain.lower():
930             if opts.verbose:
931                 print "skip (rodc): %s" % d
932             continue
933         if not d.type in [ 'A', 'AAAA' ]:
934             if opts.verbose:
935                 print "update (rodc): %s" % d
936             call_rodc_update(d)
937         else:
938             if opts.verbose:
939                 print "update (nsupdate): %s" % d
940             call_nsupdate(d)
941     else:
942         if opts.verbose:
943             print "update(nsupdate): %s" % d
944         call_nsupdate(d)
945
946 if rebuild_cache:
947     print "Rebuilding cache at %s" % dns_update_cache
948     (file_dir, file_name) = os.path.split(dns_update_cache)
949     (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
950     wfile = os.fdopen(tmp_fd, 'a')
951     for d in dns_list:
952         if opts.verbose:
953             print "Adding %s to %s" % (str(d), file_name)
954         wfile.write(str(d)+"\n")
955     os.rename(tmpfile, dns_update_cache)
956 fcntl.lockf(cfile, fcntl.LOCK_UN)
957
958 # delete the ccache if we created it
959 if ccachename is not None:
960     os.unlink(ccachename)
961
962 if error_count != 0:
963     print("Failed update of %u entries" % error_count)
964 sys.exit(error_count)