various: Remove references to about to be deleted thirdparty/dnspython
[samba.git] / source4 / scripting / bin / samba_dnsupdate
1 #!/usr/bin/env python
2 # vim: expandtab
3 #
4 # update our DNS names using TSIG-GSS
5 #
6 # Copyright (C) Andrew Tridgell 2010
7 #
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
22 import os
23 import fcntl
24 import sys
25 import tempfile
26 import subprocess
27
28 # ensure we get messages out immediately, so they get in the samba logs,
29 # and don't get swallowed by a timeout
30 os.environ['PYTHONUNBUFFERED'] = '1'
31
32 # forcing GMT avoids a problem in some timezones with kerberos. Both MIT
33 # heimdal can get mutual authentication errors due to the 24 second difference
34 # between UTC and GMT when using some zone files (eg. the PDT zone from
35 # the US)
36 os.environ["TZ"] = "GMT"
37
38 # Find right directory when running from source tree
39 sys.path.insert(0, "bin/python")
40
41 import samba
42 import optparse
43 from samba import getopt as options
44 from ldb import SCOPE_BASE
45 from samba import dsdb
46 from samba.auth import system_session
47 from samba.samdb import SamDB
48 from samba.dcerpc import netlogon, winbind
49 from samba.netcmd.dns import cmd_dns
50 from samba import gensec
51 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     # Getting this line right is really important.  When we are under
480     # resolv_wrapper, then we want to use RESOLV_CONF and the
481     # nameserver therein. The issue is that this parameter forces us
482     # to only ever use that server, and not some other server that the
483     # NS record may point to, even as we get a ticket to that other
484     # server.
485     #
486     # Therefore we must not set this in production, instead we want
487     # to find the name of a SOA for the zone and use that server.
488
489     if os.getenv('RESOLV_CONF') and d.nameservers != []:
490         f.write('server %s\n' % d.nameservers[0])
491     else:
492         resolver = get_resolver(d)
493
494         # Local the zone for this name
495         zone = dns.resolver.zone_for_name(normalised_name,
496                                           resolver=resolver)
497
498         # Now find the SOA, or if we can't get a ticket to the SOA,
499         # any server with an NS record we can get a ticket for.
500         #
501         # Thanks to the Kerberos Credentials cache this is not
502         # expensive inside the loop
503         server = get_krb5_rw_dns_server(creds, zone)
504         f.write('server %s\n' % server)
505
506     if d.type == "A":
507         f.write("update %s %s %u A %s\n" % (op, normalised_name, default_ttl, d.ip))
508     if d.type == "AAAA":
509         f.write("update %s %s %u AAAA %s\n" % (op, normalised_name, default_ttl, d.ip))
510     if d.type == "SRV":
511         if op == "add" and d.existing_port is not None:
512             f.write("update delete %s SRV 0 %s %s %s\n" % (normalised_name, d.existing_weight,
513                                                            d.existing_port, d.dest))
514         f.write("update %s %s %u SRV 0 100 %s %s\n" % (op, normalised_name, default_ttl, d.port, d.dest))
515     if d.type == "CNAME":
516         f.write("update %s %s %u CNAME %s\n" % (op, normalised_name, default_ttl, d.dest))
517     if d.type == "NS":
518         f.write("update %s %s %u NS %s\n" % (op, normalised_name, default_ttl, d.dest))
519     if opts.verbose:
520         f.write("show\n")
521     f.write("send\n")
522     f.close()
523
524     # Set a bigger MTU size to work around a bug in nsupdate's doio_send()
525     os.environ["SOCKET_WRAPPER_MTU"] = "2000"
526
527     global error_count
528     if ccachename:
529         os.environ["KRB5CCNAME"] = ccachename
530     try:
531         cmd = nsupdate_cmd[:]
532         cmd.append(tmpfile)
533         env = os.environ
534         if krb5conf:
535             env["KRB5_CONFIG"] = krb5conf
536         if ccachename:
537             env["KRB5CCNAME"] = ccachename
538         ret = subprocess.call(cmd, shell=False, env=env)
539         if ret != 0:
540             if opts.fail_immediately:
541                 if opts.verbose:
542                     print("Failed update with %s" % tmpfile)
543                 sys.exit(1)
544             error_count = error_count + 1
545             if opts.verbose:
546                 print("Failed nsupdate: %d" % ret)
547     except Exception as estr:
548         if opts.fail_immediately:
549             sys.exit(1)
550         error_count = error_count + 1
551         if opts.verbose:
552             print("Failed nsupdate: %s : %s" % (str(d), estr))
553     os.unlink(tmpfile)
554
555     # Let socket_wrapper set the default MTU size
556     os.environ["SOCKET_WRAPPER_MTU"] = "0"
557
558
559 def call_samba_tool(d, op="add", zone=None):
560     """call samba-tool dns to update an entry."""
561
562     assert(op in ["add", "delete"])
563
564     if (sub_vars['DNSFOREST'] != sub_vars['DNSDOMAIN']) and \
565        sub_vars['DNSFOREST'].endswith('.' + sub_vars['DNSDOMAIN']):
566         print("Refusing to use samba-tool when forest %s is under domain %s" \
567             % (sub_vars['DNSFOREST'], sub_vars['DNSDOMAIN']))
568
569     if opts.verbose:
570         print("Calling samba-tool dns for %s (%s)" % (d, op))
571
572     normalised_name = d.name.rstrip('.') + '.'
573     if zone is None:
574         if normalised_name == (sub_vars['DNSDOMAIN'] + '.'):
575             short_name = '@'
576             zone = sub_vars['DNSDOMAIN']
577         elif normalised_name == (sub_vars['DNSFOREST'] + '.'):
578             short_name = '@'
579             zone = sub_vars['DNSFOREST']
580         elif normalised_name == ('_msdcs.' + sub_vars['DNSFOREST'] + '.'):
581             short_name = '@'
582             zone = '_msdcs.' + sub_vars['DNSFOREST']
583         else:
584             if not normalised_name.endswith('.' + sub_vars['DNSDOMAIN'] + '.'):
585                 print("Not Calling samba-tool dns for %s (%s), %s not in %s" % (d, op, normalised_name, sub_vars['DNSDOMAIN'] + '.'))
586                 return False
587             elif normalised_name.endswith('._msdcs.' + sub_vars['DNSFOREST'] + '.'):
588                 zone = '_msdcs.' + sub_vars['DNSFOREST']
589             else:
590                 zone = sub_vars['DNSDOMAIN']
591             len_zone = len(zone)+2
592             short_name = normalised_name[:-len_zone]
593     else:
594         len_zone = len(zone)+2
595         short_name = normalised_name[:-len_zone]
596
597     if d.type == "A":
598         args = [rpc_server_ip, zone, short_name, "A", d.ip]
599     if d.type == "AAAA":
600         args = [rpc_server_ip, zone, short_name, "AAAA", d.ip]
601     if d.type == "SRV":
602         if op == "add" and d.existing_port is not None:
603             print("Not handling modify of exising SRV %s using samba-tool" % d)
604             return False
605             op = "update"
606             args = [rpc_server_ip, zone, short_name, "SRV",
607                     "%s %s %s %s" % (d.existing_weight,
608                                      d.existing_port, "0", "100"),
609                     "%s %s %s %s" % (d.dest, d.port, "0", "100")]
610         else:
611             args = [rpc_server_ip, zone, short_name, "SRV", "%s %s %s %s" % (d.dest, d.port, "0", "100")]
612     if d.type == "CNAME":
613         if d.existing_cname_target is None:
614             args = [rpc_server_ip, zone, short_name, "CNAME", d.dest]
615         else:
616             op = "update"
617             args = [rpc_server_ip, zone, short_name, "CNAME",
618                     d.existing_cname_target.rstrip('.'), d.dest]
619
620     if d.type == "NS":
621         args = [rpc_server_ip, zone, short_name, "NS", d.dest]
622
623     if smb_conf and args:
624         args += ["--configfile=" + smb_conf]
625
626     global error_count
627     try:
628         cmd = cmd_dns()
629         if opts.verbose:
630             print("Calling samba-tool dns %s -k no -P %s" % (op, args))
631         ret = cmd._run("dns", op, "-k", "no", "-P", *args)
632         if ret == -1:
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 of %s" % (str(d)))
638     except Exception as estr:
639         if opts.fail_immediately:
640             sys.exit(1)
641         error_count = error_count + 1
642         if opts.verbose:
643             print("Failed 'samba-tool dns' based update: %s : %s" % (str(d), estr))
644         raise
645
646 def rodc_dns_update(d, t, op):
647     '''a single DNS update via the RODC netlogon call'''
648     global sub_vars
649
650     assert(op in ["add", "delete"])
651
652     if opts.verbose:
653         print("Calling netlogon RODC update for %s" % d)
654
655     typemap = {
656         netlogon.NlDnsLdapAtSite       : netlogon.NlDnsInfoTypeNone,
657         netlogon.NlDnsGcAtSite         : netlogon.NlDnsDomainNameAlias,
658         netlogon.NlDnsDsaCname         : netlogon.NlDnsDomainNameAlias,
659         netlogon.NlDnsKdcAtSite        : netlogon.NlDnsInfoTypeNone,
660         netlogon.NlDnsDcAtSite         : netlogon.NlDnsInfoTypeNone,
661         netlogon.NlDnsRfc1510KdcAtSite : netlogon.NlDnsInfoTypeNone,
662         netlogon.NlDnsGenericGcAtSite  : netlogon.NlDnsDomainNameAlias
663         }
664
665     w = winbind.winbind("irpc:winbind_server", lp)
666     dns_names = netlogon.NL_DNS_NAME_INFO_ARRAY()
667     dns_names.count = 1
668     name = netlogon.NL_DNS_NAME_INFO()
669     name.type = t
670     name.dns_domain_info_type = typemap[t]
671     name.priority = 0
672     name.weight   = 0
673     if d.port is not None:
674         name.port = int(d.port)
675     if op == "add":
676         name.dns_register = True
677     else:
678         name.dns_register = False
679     dns_names.names = [ name ]
680     site_name = text_type(sub_vars['SITE'])
681
682     global error_count
683
684     try:
685         ret_names = w.DsrUpdateReadOnlyServerDnsRecords(site_name, default_ttl, dns_names)
686         if ret_names.names[0].status != 0:
687             print("Failed to set DNS entry: %s (status %u)" % (d, ret_names.names[0].status))
688             error_count = error_count + 1
689     except RuntimeError as reason:
690         print("Error setting DNS entry of type %u: %s: %s" % (t, d, reason))
691         error_count = error_count + 1
692
693     if error_count != 0 and opts.fail_immediately:
694         sys.exit(1)
695
696
697 def call_rodc_update(d, op="add"):
698     '''RODCs need to use the netlogon API for nsupdate'''
699     global lp, sub_vars
700
701     assert(op in ["add", "delete"])
702
703     # we expect failure for 3268 if we aren't a GC
704     if d.port is not None and int(d.port) == 3268:
705         return
706
707     # map the DNS request to a netlogon update type
708     map = {
709         netlogon.NlDnsLdapAtSite       : '_ldap._tcp.${SITE}._sites.${DNSDOMAIN}',
710         netlogon.NlDnsGcAtSite         : '_ldap._tcp.${SITE}._sites.gc._msdcs.${DNSDOMAIN}',
711         netlogon.NlDnsDsaCname         : '${NTDSGUID}._msdcs.${DNSFOREST}',
712         netlogon.NlDnsKdcAtSite        : '_kerberos._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
713         netlogon.NlDnsDcAtSite         : '_ldap._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
714         netlogon.NlDnsRfc1510KdcAtSite : '_kerberos._tcp.${SITE}._sites.${DNSDOMAIN}',
715         netlogon.NlDnsGenericGcAtSite  : '_gc._tcp.${SITE}._sites.${DNSFOREST}'
716         }
717
718     for t in map:
719         subname = samba.substitute_var(map[t], sub_vars)
720         if subname.lower() == d.name.lower():
721             # found a match - do the update
722             rodc_dns_update(d, t, op)
723             return
724     if opts.verbose:
725         print("Unable to map to netlogon DNS update: %s" % d)
726
727
728 # get the list of DNS entries we should have
729 if opts.update_list:
730     dns_update_list = opts.update_list
731 else:
732     dns_update_list = lp.private_path('dns_update_list')
733
734 if opts.update_cache:
735     dns_update_cache = opts.update_cache
736 else:
737     dns_update_cache = lp.private_path('dns_update_cache')
738
739 krb5conf = None
740 # only change the krb5.conf if we are not in selftest
741 if 'SOCKET_WRAPPER_DIR' not in os.environ:
742     # use our private krb5.conf to avoid problems with the wrong domain
743     # bind9 nsupdate wants the default domain set
744     krb5conf = lp.private_path('krb5.conf')
745     os.environ['KRB5_CONFIG'] = krb5conf
746
747 file = open(dns_update_list, "r")
748
749 if opts.nosubs:
750     sub_vars = {}
751 else:
752     samdb = SamDB(url=lp.samdb_url(), session_info=system_session(), lp=lp)
753
754     # get the substitution dictionary
755     sub_vars = get_subst_vars(samdb)
756
757 # build up a list of update commands to pass to nsupdate
758 update_list = []
759 dns_list = []
760 cache_list = []
761 delete_list = []
762
763 dup_set = set()
764 cache_set = set()
765
766 rebuild_cache = False
767 try:
768     cfile = open(dns_update_cache, 'r+')
769 except IOError:
770     # Perhaps create it
771     cfile = open(dns_update_cache, 'w+')
772     # Open it for reading again, in case someone else got to it first
773     cfile = open(dns_update_cache, 'r+')
774 fcntl.lockf(cfile, fcntl.LOCK_EX)
775 for line in cfile:
776     line = line.strip()
777     if line == '' or line[0] == "#":
778         continue
779     c = parse_dns_line(line, {})
780     if c is None:
781         continue
782     if str(c) not in cache_set:
783         cache_list.append(c)
784         cache_set.add(str(c))
785
786 site_specific_rec = []
787
788 # read each line, and check that the DNS name exists
789 for line in file:
790     line = line.strip()
791
792     if '${SITE}' in line:
793         site_specific_rec.append(line)
794
795     if line == '' or line[0] == "#":
796         continue
797     d = parse_dns_line(line, sub_vars)
798     if d is None:
799         continue
800     if d.type == 'A' and len(IP4s) == 0:
801         continue
802     if d.type == 'AAAA' and len(IP6s) == 0:
803         continue
804     if str(d) not in dup_set:
805         dns_list.append(d)
806         dup_set.add(str(d))
807
808 # Perform automatic site coverage by default
809 auto_coverage = True
810
811 if not am_rodc and auto_coverage:
812     site_names = kcc_utils.uncovered_sites_to_cover(samdb,
813                                                     samdb.server_site_name())
814
815     # Duplicate all site specific records for the uncovered site
816     for site in site_names:
817         to_add = [samba.substitute_var(line, {'SITE': site})
818                   for line in site_specific_rec]
819
820         for site_line in to_add:
821             d = parse_dns_line(site_line,
822                                sub_vars=sub_vars)
823             if d is not None and str(d) not in dup_set:
824                 dns_list.append(d)
825                 dup_set.add(str(d))
826
827 # now expand the entries, if any are A record with ip set to $IP
828 # then replace with multiple entries, one for each interface IP
829 for d in dns_list:
830     if d.ip != "$IP":
831         continue
832     if d.type == 'A':
833         d.ip = IP4s[0]
834         for i in range(len(IP4s)-1):
835             d2 = dnsobj(str(d))
836             d2.ip = IP4s[i+1]
837             dns_list.append(d2)
838     if d.type == 'AAAA':
839         d.ip = IP6s[0]
840         for i in range(len(IP6s)-1):
841             d2 = dnsobj(str(d))
842             d2.ip = IP6s[i+1]
843             dns_list.append(d2)
844
845 # now check if the entries already exist on the DNS server
846 for d in dns_list:
847     found = False
848     for c in cache_list:
849         if str(c).lower() == str(d).lower():
850             found = True
851             break
852     if not found:
853         rebuild_cache = True
854         if opts.verbose:
855             print("need cache add: %s" % d)
856     if dns_zone_scavenging:
857         update_list.append(d)
858         if opts.verbose:
859             print("scavenging requires update: %s" % d)
860     elif opts.all_names:
861         update_list.append(d)
862         if opts.verbose:
863             print("force update: %s" % d)
864     elif not check_dns_name(d):
865         update_list.append(d)
866         if opts.verbose:
867             print("need update: %s" % d)
868
869 for c in cache_list:
870     found = False
871     for d in dns_list:
872         if str(c).lower() == str(d).lower():
873             found = True
874             break
875     if found:
876         continue
877     rebuild_cache = True
878     if opts.verbose:
879         print("need cache remove: %s" % c)
880     if not opts.all_names and not check_dns_name(c):
881         continue
882     delete_list.append(c)
883     if opts.verbose:
884         print("need delete: %s" % c)
885
886 if len(delete_list) == 0 and len(update_list) == 0 and not rebuild_cache:
887     if opts.verbose:
888         print("No DNS updates needed")
889     sys.exit(0)
890 else:
891     if opts.verbose:
892         print("%d DNS updates and %d DNS deletes needed" % (len(update_list), len(delete_list)))
893
894 use_samba_tool = opts.use_samba_tool
895 use_nsupdate = opts.use_nsupdate
896 # get our krb5 creds
897 if len(delete_list) != 0 or len(update_list) != 0 and not opts.nocreds:
898     try:
899         creds = get_credentials(lp)
900     except RuntimeError as e:
901         ccachename = None
902
903         if sub_vars['IF_RWDNS_DOMAIN'] == "# ":
904             raise
905
906         if use_nsupdate:
907             raise
908
909         print("Failed to get Kerberos credentials, falling back to samba-tool: %s" % e)
910         use_samba_tool = True
911
912
913 # ask nsupdate to delete entries as needed
914 for d in delete_list:
915     if d.rpc or (not use_nsupdate and use_samba_tool):
916         if opts.verbose:
917             print("update (samba-tool): %s" % d)
918         call_samba_tool(d, op="delete", zone=d.zone)
919
920     elif am_rodc:
921         if d.name.lower() == domain.lower():
922             if opts.verbose:
923                 print("skip delete (rodc): %s" % d)
924             continue
925         if not d.type in [ 'A', 'AAAA' ]:
926             if opts.verbose:
927                 print("delete (rodc): %s" % d)
928             call_rodc_update(d, op="delete")
929         else:
930             if opts.verbose:
931                 print("delete (nsupdate): %s" % d)
932             call_nsupdate(d, op="delete")
933     else:
934         if opts.verbose:
935             print("delete (nsupdate): %s" % d)
936         call_nsupdate(d, op="delete")
937
938 # ask nsupdate to add entries as needed
939 for d in update_list:
940     if d.rpc or (not use_nsupdate and use_samba_tool):
941         if opts.verbose:
942             print("update (samba-tool): %s" % d)
943         call_samba_tool(d, zone=d.zone)
944
945     elif am_rodc:
946         if d.name.lower() == domain.lower():
947             if opts.verbose:
948                 print("skip (rodc): %s" % d)
949             continue
950         if not d.type in [ 'A', 'AAAA' ]:
951             if opts.verbose:
952                 print("update (rodc): %s" % d)
953             call_rodc_update(d)
954         else:
955             if opts.verbose:
956                 print("update (nsupdate): %s" % d)
957             call_nsupdate(d)
958     else:
959         if opts.verbose:
960             print("update(nsupdate): %s" % d)
961         call_nsupdate(d)
962
963 if rebuild_cache:
964     print("Rebuilding cache at %s" % dns_update_cache)
965     (file_dir, file_name) = os.path.split(dns_update_cache)
966     (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
967     wfile = os.fdopen(tmp_fd, 'a')
968     for d in dns_list:
969         if opts.verbose:
970             print("Adding %s to %s" % (str(d), file_name))
971         wfile.write(str(d)+"\n")
972     os.rename(tmpfile, dns_update_cache)
973 fcntl.lockf(cfile, fcntl.LOCK_UN)
974
975 # delete the ccache if we created it
976 if ccachename is not None:
977     os.unlink(ccachename)
978
979 if error_count != 0:
980     print("Failed update of %u entries" % error_count)
981 sys.exit(error_count)