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