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