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