74f10427b4e6c082e904600c2c9546f2221e7e5a
[gd/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.verbose:
447         print("Calling nsupdate for %s (%s)" % (d, op))
448
449     if opts.use_file is not None:
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     normalised_name = d.name.rstrip('.') + '.'
475
476     (tmp_fd, tmpfile) = tempfile.mkstemp()
477     f = os.fdopen(tmp_fd, 'w')
478
479     resolver = get_resolver(d)
480
481     # Local the zone for this name
482     zone = dns.resolver.zone_for_name(normalised_name,
483                                       resolver=resolver)
484
485     # Now find the SOA, or if we can't get a ticket to the SOA,
486     # any server with an NS record we can get a ticket for.
487     #
488     # Thanks to the Kerberos Credentials cache this is not
489     # expensive inside the loop
490     server = get_krb5_rw_dns_server(creds, zone)
491     f.write('server %s\n' % server)
492
493     if d.type == "A":
494         f.write("update %s %s %u A %s\n" % (op, normalised_name, default_ttl, d.ip))
495     if d.type == "AAAA":
496         f.write("update %s %s %u AAAA %s\n" % (op, normalised_name, default_ttl, d.ip))
497     if d.type == "SRV":
498         if op == "add" and d.existing_port is not None:
499             f.write("update delete %s SRV 0 %s %s %s\n" % (normalised_name, d.existing_weight,
500                                                            d.existing_port, d.dest))
501         f.write("update %s %s %u SRV 0 100 %s %s\n" % (op, normalised_name, default_ttl, d.port, d.dest))
502     if d.type == "CNAME":
503         f.write("update %s %s %u CNAME %s\n" % (op, normalised_name, default_ttl, d.dest))
504     if d.type == "NS":
505         f.write("update %s %s %u NS %s\n" % (op, normalised_name, default_ttl, d.dest))
506     if opts.verbose:
507         f.write("show\n")
508     f.write("send\n")
509     f.close()
510
511     # Set a bigger MTU size to work around a bug in nsupdate's doio_send()
512     os.environ["SOCKET_WRAPPER_MTU"] = "2000"
513
514     global error_count
515     if ccachename:
516         os.environ["KRB5CCNAME"] = ccachename
517     try:
518         cmd = nsupdate_cmd[:]
519         cmd.append(tmpfile)
520         env = os.environ
521         if krb5conf:
522             env["KRB5_CONFIG"] = krb5conf
523         if ccachename:
524             env["KRB5CCNAME"] = ccachename
525         ret = subprocess.call(cmd, shell=False, env=env)
526         if ret != 0:
527             if opts.fail_immediately:
528                 if opts.verbose:
529                     print("Failed update with %s" % tmpfile)
530                 sys.exit(1)
531             error_count = error_count + 1
532             if opts.verbose:
533                 print("Failed nsupdate: %d" % ret)
534     except Exception as estr:
535         if opts.fail_immediately:
536             sys.exit(1)
537         error_count = error_count + 1
538         if opts.verbose:
539             print("Failed nsupdate: %s : %s" % (str(d), estr))
540     os.unlink(tmpfile)
541
542     # Let socket_wrapper set the default MTU size
543     os.environ["SOCKET_WRAPPER_MTU"] = "0"
544
545
546 def call_samba_tool(d, op="add", zone=None):
547     """call samba-tool dns to update an entry."""
548
549     assert(op in ["add", "delete"])
550
551     if (sub_vars['DNSFOREST'] != sub_vars['DNSDOMAIN']) and \
552        sub_vars['DNSFOREST'].endswith('.' + sub_vars['DNSDOMAIN']):
553         print("Refusing to use samba-tool when forest %s is under domain %s" \
554             % (sub_vars['DNSFOREST'], sub_vars['DNSDOMAIN']))
555
556     if opts.verbose:
557         print("Calling samba-tool dns for %s (%s)" % (d, op))
558
559     normalised_name = d.name.rstrip('.') + '.'
560     if zone is None:
561         if normalised_name == (sub_vars['DNSDOMAIN'] + '.'):
562             short_name = '@'
563             zone = sub_vars['DNSDOMAIN']
564         elif normalised_name == (sub_vars['DNSFOREST'] + '.'):
565             short_name = '@'
566             zone = sub_vars['DNSFOREST']
567         elif normalised_name == ('_msdcs.' + sub_vars['DNSFOREST'] + '.'):
568             short_name = '@'
569             zone = '_msdcs.' + sub_vars['DNSFOREST']
570         else:
571             if not normalised_name.endswith('.' + sub_vars['DNSDOMAIN'] + '.'):
572                 print("Not Calling samba-tool dns for %s (%s), %s not in %s" % (d, op, normalised_name, sub_vars['DNSDOMAIN'] + '.'))
573                 return False
574             elif normalised_name.endswith('._msdcs.' + sub_vars['DNSFOREST'] + '.'):
575                 zone = '_msdcs.' + sub_vars['DNSFOREST']
576             else:
577                 zone = sub_vars['DNSDOMAIN']
578             len_zone = len(zone)+2
579             short_name = normalised_name[:-len_zone]
580     else:
581         len_zone = len(zone)+2
582         short_name = normalised_name[:-len_zone]
583
584     if d.type == "A":
585         args = [rpc_server_ip, zone, short_name, "A", d.ip]
586     if d.type == "AAAA":
587         args = [rpc_server_ip, zone, short_name, "AAAA", d.ip]
588     if d.type == "SRV":
589         if op == "add" and d.existing_port is not None:
590             print("Not handling modify of exising SRV %s using samba-tool" % d)
591             return False
592             op = "update"
593             args = [rpc_server_ip, zone, short_name, "SRV",
594                     "%s %s %s %s" % (d.existing_weight,
595                                      d.existing_port, "0", "100"),
596                     "%s %s %s %s" % (d.dest, d.port, "0", "100")]
597         else:
598             args = [rpc_server_ip, zone, short_name, "SRV", "%s %s %s %s" % (d.dest, d.port, "0", "100")]
599     if d.type == "CNAME":
600         if d.existing_cname_target is None:
601             args = [rpc_server_ip, zone, short_name, "CNAME", d.dest]
602         else:
603             op = "update"
604             args = [rpc_server_ip, zone, short_name, "CNAME",
605                     d.existing_cname_target.rstrip('.'), d.dest]
606
607     if d.type == "NS":
608         args = [rpc_server_ip, zone, short_name, "NS", d.dest]
609
610     if smb_conf and args:
611         args += ["--configfile=" + smb_conf]
612
613     global error_count
614     try:
615         cmd = cmd_dns()
616         if opts.verbose:
617             print("Calling samba-tool dns %s -k no -P %s" % (op, args))
618         ret = cmd._run("dns", op, "-k", "no", "-P", *args)
619         if ret == -1:
620             if opts.fail_immediately:
621                 sys.exit(1)
622             error_count = error_count + 1
623             if opts.verbose:
624                 print("Failed 'samba-tool dns' based update of %s" % (str(d)))
625     except Exception as estr:
626         if opts.fail_immediately:
627             sys.exit(1)
628         error_count = error_count + 1
629         if opts.verbose:
630             print("Failed 'samba-tool dns' based update: %s : %s" % (str(d), estr))
631         raise
632
633 def rodc_dns_update(d, t, op):
634     '''a single DNS update via the RODC netlogon call'''
635     global sub_vars
636
637     assert(op in ["add", "delete"])
638
639     if opts.verbose:
640         print("Calling netlogon RODC update for %s" % d)
641
642     typemap = {
643         netlogon.NlDnsLdapAtSite       : netlogon.NlDnsInfoTypeNone,
644         netlogon.NlDnsGcAtSite         : netlogon.NlDnsDomainNameAlias,
645         netlogon.NlDnsDsaCname         : netlogon.NlDnsDomainNameAlias,
646         netlogon.NlDnsKdcAtSite        : netlogon.NlDnsInfoTypeNone,
647         netlogon.NlDnsDcAtSite         : netlogon.NlDnsInfoTypeNone,
648         netlogon.NlDnsRfc1510KdcAtSite : netlogon.NlDnsInfoTypeNone,
649         netlogon.NlDnsGenericGcAtSite  : netlogon.NlDnsDomainNameAlias
650         }
651
652     w = winbind.winbind("irpc:winbind_server", lp)
653     dns_names = netlogon.NL_DNS_NAME_INFO_ARRAY()
654     dns_names.count = 1
655     name = netlogon.NL_DNS_NAME_INFO()
656     name.type = t
657     name.dns_domain_info_type = typemap[t]
658     name.priority = 0
659     name.weight   = 0
660     if d.port is not None:
661         name.port = int(d.port)
662     if op == "add":
663         name.dns_register = True
664     else:
665         name.dns_register = False
666     dns_names.names = [ name ]
667     site_name = text_type(sub_vars['SITE'])
668
669     global error_count
670
671     try:
672         ret_names = w.DsrUpdateReadOnlyServerDnsRecords(site_name, default_ttl, dns_names)
673         if ret_names.names[0].status != 0:
674             print("Failed to set DNS entry: %s (status %u)" % (d, ret_names.names[0].status))
675             error_count = error_count + 1
676     except RuntimeError as reason:
677         print("Error setting DNS entry of type %u: %s: %s" % (t, d, reason))
678         error_count = error_count + 1
679
680     if error_count != 0 and opts.fail_immediately:
681         sys.exit(1)
682
683
684 def call_rodc_update(d, op="add"):
685     '''RODCs need to use the netlogon API for nsupdate'''
686     global lp, sub_vars
687
688     assert(op in ["add", "delete"])
689
690     # we expect failure for 3268 if we aren't a GC
691     if d.port is not None and int(d.port) == 3268:
692         return
693
694     # map the DNS request to a netlogon update type
695     map = {
696         netlogon.NlDnsLdapAtSite       : '_ldap._tcp.${SITE}._sites.${DNSDOMAIN}',
697         netlogon.NlDnsGcAtSite         : '_ldap._tcp.${SITE}._sites.gc._msdcs.${DNSDOMAIN}',
698         netlogon.NlDnsDsaCname         : '${NTDSGUID}._msdcs.${DNSFOREST}',
699         netlogon.NlDnsKdcAtSite        : '_kerberos._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
700         netlogon.NlDnsDcAtSite         : '_ldap._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
701         netlogon.NlDnsRfc1510KdcAtSite : '_kerberos._tcp.${SITE}._sites.${DNSDOMAIN}',
702         netlogon.NlDnsGenericGcAtSite  : '_gc._tcp.${SITE}._sites.${DNSFOREST}'
703         }
704
705     for t in map:
706         subname = samba.substitute_var(map[t], sub_vars)
707         if subname.lower() == d.name.lower():
708             # found a match - do the update
709             rodc_dns_update(d, t, op)
710             return
711     if opts.verbose:
712         print("Unable to map to netlogon DNS update: %s" % d)
713
714
715 # get the list of DNS entries we should have
716 if opts.update_list:
717     dns_update_list = opts.update_list
718 else:
719     dns_update_list = lp.private_path('dns_update_list')
720
721 if opts.update_cache:
722     dns_update_cache = opts.update_cache
723 else:
724     dns_update_cache = lp.private_path('dns_update_cache')
725
726 krb5conf = None
727 # only change the krb5.conf if we are not in selftest
728 if 'SOCKET_WRAPPER_DIR' not in os.environ:
729     # use our private krb5.conf to avoid problems with the wrong domain
730     # bind9 nsupdate wants the default domain set
731     krb5conf = lp.private_path('krb5.conf')
732     os.environ['KRB5_CONFIG'] = krb5conf
733
734 file = open(dns_update_list, "r")
735
736 if opts.nosubs:
737     sub_vars = {}
738 else:
739     samdb = SamDB(url=lp.samdb_url(), session_info=system_session(), lp=lp)
740
741     # get the substitution dictionary
742     sub_vars = get_subst_vars(samdb)
743
744 # build up a list of update commands to pass to nsupdate
745 update_list = []
746 dns_list = []
747 cache_list = []
748 delete_list = []
749
750 dup_set = set()
751 cache_set = set()
752
753 rebuild_cache = False
754 try:
755     cfile = open(dns_update_cache, 'r+')
756 except IOError:
757     # Perhaps create it
758     cfile = open(dns_update_cache, 'w+')
759     # Open it for reading again, in case someone else got to it first
760     cfile = open(dns_update_cache, 'r+')
761 fcntl.lockf(cfile, fcntl.LOCK_EX)
762 for line in cfile:
763     line = line.strip()
764     if line == '' or line[0] == "#":
765         continue
766     c = parse_dns_line(line, {})
767     if c is None:
768         continue
769     if str(c) not in cache_set:
770         cache_list.append(c)
771         cache_set.add(str(c))
772
773 site_specific_rec = []
774
775 # read each line, and check that the DNS name exists
776 for line in file:
777     line = line.strip()
778
779     if '${SITE}' in line:
780         site_specific_rec.append(line)
781
782     if line == '' or line[0] == "#":
783         continue
784     d = parse_dns_line(line, sub_vars)
785     if d is None:
786         continue
787     if d.type == 'A' and len(IP4s) == 0:
788         continue
789     if d.type == 'AAAA' and len(IP6s) == 0:
790         continue
791     if str(d) not in dup_set:
792         dns_list.append(d)
793         dup_set.add(str(d))
794
795 # Perform automatic site coverage by default
796 auto_coverage = True
797
798 if not am_rodc and auto_coverage:
799     site_names = kcc_utils.uncovered_sites_to_cover(samdb,
800                                                     samdb.server_site_name())
801
802     # Duplicate all site specific records for the uncovered site
803     for site in site_names:
804         to_add = [samba.substitute_var(line, {'SITE': site})
805                   for line in site_specific_rec]
806
807         for site_line in to_add:
808             d = parse_dns_line(site_line,
809                                sub_vars=sub_vars)
810             if d is not None and str(d) not in dup_set:
811                 dns_list.append(d)
812                 dup_set.add(str(d))
813
814 # now expand the entries, if any are A record with ip set to $IP
815 # then replace with multiple entries, one for each interface IP
816 for d in dns_list:
817     if d.ip != "$IP":
818         continue
819     if d.type == 'A':
820         d.ip = IP4s[0]
821         for i in range(len(IP4s)-1):
822             d2 = dnsobj(str(d))
823             d2.ip = IP4s[i+1]
824             dns_list.append(d2)
825     if d.type == 'AAAA':
826         d.ip = IP6s[0]
827         for i in range(len(IP6s)-1):
828             d2 = dnsobj(str(d))
829             d2.ip = IP6s[i+1]
830             dns_list.append(d2)
831
832 # now check if the entries already exist on the DNS server
833 for d in dns_list:
834     found = False
835     for c in cache_list:
836         if str(c).lower() == str(d).lower():
837             found = True
838             break
839     if not found:
840         rebuild_cache = True
841         if opts.verbose:
842             print("need cache add: %s" % d)
843     if dns_zone_scavenging:
844         update_list.append(d)
845         if opts.verbose:
846             print("scavenging requires update: %s" % d)
847     elif opts.all_names:
848         update_list.append(d)
849         if opts.verbose:
850             print("force update: %s" % d)
851     elif not check_dns_name(d):
852         update_list.append(d)
853         if opts.verbose:
854             print("need update: %s" % d)
855
856 for c in cache_list:
857     found = False
858     for d in dns_list:
859         if str(c).lower() == str(d).lower():
860             found = True
861             break
862     if found:
863         continue
864     rebuild_cache = True
865     if opts.verbose:
866         print("need cache remove: %s" % c)
867     if not opts.all_names and not check_dns_name(c):
868         continue
869     delete_list.append(c)
870     if opts.verbose:
871         print("need delete: %s" % c)
872
873 if len(delete_list) == 0 and len(update_list) == 0 and not rebuild_cache:
874     if opts.verbose:
875         print("No DNS updates needed")
876     sys.exit(0)
877 else:
878     if opts.verbose:
879         print("%d DNS updates and %d DNS deletes needed" % (len(update_list), len(delete_list)))
880
881 use_samba_tool = opts.use_samba_tool
882 use_nsupdate = opts.use_nsupdate
883 # get our krb5 creds
884 if len(delete_list) != 0 or len(update_list) != 0 and not opts.nocreds:
885     try:
886         creds = get_credentials(lp)
887     except RuntimeError as e:
888         ccachename = None
889
890         if sub_vars['IF_RWDNS_DOMAIN'] == "# ":
891             raise
892
893         if use_nsupdate:
894             raise
895
896         print("Failed to get Kerberos credentials, falling back to samba-tool: %s" % e)
897         use_samba_tool = True
898
899
900 # ask nsupdate to delete entries as needed
901 for d in delete_list:
902     if d.rpc or (not use_nsupdate and use_samba_tool):
903         if opts.verbose:
904             print("update (samba-tool): %s" % d)
905         call_samba_tool(d, op="delete", zone=d.zone)
906
907     elif am_rodc:
908         if d.name.lower() == domain.lower():
909             if opts.verbose:
910                 print("skip delete (rodc): %s" % d)
911             continue
912         if not d.type in [ 'A', 'AAAA' ]:
913             if opts.verbose:
914                 print("delete (rodc): %s" % d)
915             call_rodc_update(d, op="delete")
916         else:
917             if opts.verbose:
918                 print("delete (nsupdate): %s" % d)
919             call_nsupdate(d, op="delete")
920     else:
921         if opts.verbose:
922             print("delete (nsupdate): %s" % d)
923         call_nsupdate(d, op="delete")
924
925 # ask nsupdate to add entries as needed
926 for d in update_list:
927     if d.rpc or (not use_nsupdate and use_samba_tool):
928         if opts.verbose:
929             print("update (samba-tool): %s" % d)
930         call_samba_tool(d, zone=d.zone)
931
932     elif am_rodc:
933         if d.name.lower() == domain.lower():
934             if opts.verbose:
935                 print("skip (rodc): %s" % d)
936             continue
937         if not d.type in [ 'A', 'AAAA' ]:
938             if opts.verbose:
939                 print("update (rodc): %s" % d)
940             call_rodc_update(d)
941         else:
942             if opts.verbose:
943                 print("update (nsupdate): %s" % d)
944             call_nsupdate(d)
945     else:
946         if opts.verbose:
947             print("update(nsupdate): %s" % d)
948         call_nsupdate(d)
949
950 if rebuild_cache:
951     print("Rebuilding cache at %s" % dns_update_cache)
952     (file_dir, file_name) = os.path.split(dns_update_cache)
953     (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
954     wfile = os.fdopen(tmp_fd, 'a')
955     for d in dns_list:
956         if opts.verbose:
957             print("Adding %s to %s" % (str(d), file_name))
958         wfile.write(str(d)+"\n")
959     os.rename(tmpfile, dns_update_cache)
960 fcntl.lockf(cfile, fcntl.LOCK_UN)
961
962 # delete the ccache if we created it
963 if ccachename is not None:
964     os.unlink(ccachename)
965
966 if error_count != 0:
967     print("Failed update of %u entries" % error_count)
968 sys.exit(error_count)