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