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