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