4 # update our DNS names using TSIG-GSS
6 # Copyright (C) Andrew Tridgell 2010
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.
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.
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/>.
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'
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
36 os.environ["TZ"] = "GMT"
38 # Find right directory when running from source tree
39 sys.path.insert(0, "bin/python")
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
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")
84 opts, args = parser.parse_args()
90 lp = sambaopts.get_loadparm()
92 domain = lp.get("realm")
93 host = lp.get("netbios name")
94 all_interfaces = opts.all_interfaces
96 IPs = opts.current_ip or samba.interface_ips(lp, bool(all_interfaces)) or []
98 nsupdate_cmd = lp.get('nsupdate command')
99 dns_zone_scavenging = lp.get("dns zone scavenging")
102 print("No IP interfaces - skipping DNS updates")
105 rpc_server_ip = opts.rpc_server_ip or IPs[0]
107 IP6s = [ip for ip in IPs if ':' in ip]
108 IP4s = [ip for ip in IPs if ':' not in ip]
110 smb_conf = sambaopts.get_loadparm_path()
113 print("IPs: %s" % IPs)
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
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]
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
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]
132 return hosts_soa + hosts_ns
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.
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):
146 settings["lp_ctx"] = lp
147 settings["target_hostname"] = target_hostname
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""
157 (client_finished, client_to_server) = gensec_client.update(server_to_client)
159 print("Successfully obtained Kerberos ticket to DNS/%s as %s" \
160 % (target_hostname, creds.get_username()))
161 return target_hostname
163 # Only raise an exception if they all failed
164 if i == len(rw_dns_servers) - 1:
167 def get_credentials(lp):
168 """# get credentials if we haven't got them already."""
169 from samba import credentials
171 creds = credentials.Credentials()
173 creds.set_machine_account(lp)
174 creds.set_krb_forwardable(credentials.NO_KRB_FORWARDABLE)
175 (tmp_fd, ccachename) = tempfile.mkstemp()
177 if opts.use_file is not None:
180 creds.get_named_ccache(lp, ccachename)
182 # Now confirm we can get a ticket to the DNS server
183 get_krb5_rw_dns_server(creds, sub_vars['DNSDOMAIN'] + '.')
186 except RuntimeError as e:
187 os.unlink(ccachename)
191 class dnsobj(object):
192 """an object to hold a parsed DNS line"""
194 def __init__(self, string_form):
195 list = string_form.split()
197 raise Exception("Invalid DNS entry %r" % string_form)
201 self.existing_port = None
202 self.existing_weight = None
203 self.existing_cname_target = None
212 self.nameservers = []
213 if self.type == 'SRV':
215 raise Exception("Invalid DNS entry %r" % string_form)
218 elif self.type in ['A', 'AAAA']:
219 self.ip = list[2] # usually $IP, which gets replaced
220 elif self.type == 'CNAME':
222 elif self.type == 'NS':
225 raise Exception("Received unexpected DNS reply of type %s: %s" % (self.type, string_form))
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)
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
245 print("Skipping PDC entry (%s) as we are not a PDC" % line)
247 subline = samba.substitute_var(line, sub_vars)
248 if subline == '' or subline[0] == "#":
250 return dnsobj(subline)
253 def hostname_match(h1, h2):
254 """see if two hostnames match."""
257 return h1.lower().rstrip('.') == h2.lower().rstrip('.')
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)
263 if d is not None and d.nameservers != []:
264 resolver.nameservers = d.nameservers
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)
275 def check_dns_name(d):
276 """check that a DNS entry exists."""
277 normalised_name = d.name.rstrip('.') + '.'
279 print("Looking for DNS entry %s as %s" % (d, normalised_name))
281 if opts.use_file is not None:
283 dns_file = open(opts.use_file, "r")
287 for line in dns_file:
289 if line == '' or line[0] == "#":
291 if line.lower() == str(d).lower():
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:
303 print("The DNS entry %s, queried as %s does not exist" % (d, normalised_name))
305 except dns.resolver.NoAnswer:
307 print("The DNS entry %s, queried as %s does not hold this record type" % (d, normalised_name))
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
314 if str(rdata) == str(d.ip):
316 elif d.type == 'CNAME':
317 for i in range(len(ans)):
318 if hostname_match(ans[i].target, d.dest):
321 d.existing_cname_target = str(ans[i].target)
323 for i in range(len(ans)):
324 if hostname_match(ans[i].target, d.dest):
326 elif d.type == 'SRV':
329 print("Checking %s against %s" % (rdata, d))
330 if hostname_match(rdata.target, d.dest):
331 if str(rdata.port) == str(d.port):
334 d.existing_port = str(rdata.port)
335 d.existing_weight = str(rdata.weight)
338 print("Lookup of %s succeeded, but we failed to find a matching DNS entry for %s" % (normalised_name, d))
343 def get_subst_vars(samdb):
344 """get the list of substitution vars."""
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)
358 vars['IF_RWDC'] = "# "
359 vars['IF_RODC'] = "# "
360 vars['IF_PDC'] = "# "
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'] = "# "
371 am_rodc = samdb.am_rodc()
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"])
386 if "options" in res[0]:
387 options = int(res[0]["options"][0])
388 if (options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
395 basedn = str(samdb.get_default_basedn())
396 forestdn = str(samdb.get_root_basedn())
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'] = ""
403 vars['IF_RODNS_DOMAIN'] = ""
405 vars['IF_RWDNS_DOMAIN'] = ""
406 if str(e) == "DC=ForestDnsZones,%s" % forestdn:
407 vars['IF_DNS_FOREST'] = ""
409 vars['IF_RODNS_FOREST'] = ""
411 vars['IF_RWDNS_FOREST'] = ""
416 def call_nsupdate(d, op="add"):
417 """call nsupdate for an entry."""
418 global ccachename, nsupdate_cmd, krb5conf
420 assert(op in ["add", "delete"])
422 if opts.use_file is not None:
424 print("Use File instead of nsupdate for %s (%s)" % (d, op))
427 rfile = open(opts.use_file, 'r+')
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')
440 l = parse_dns_line(line, {})
441 if str(l).lower() == str(d).lower():
445 wfile.write(str(d)+"\n")
446 os.rename(tmpfile, opts.use_file)
447 fcntl.lockf(rfile, fcntl.LOCK_UN)
451 print("Calling nsupdate for %s (%s)" % (d, op))
453 normalised_name = d.name.rstrip('.') + '.'
455 (tmp_fd, tmpfile) = tempfile.mkstemp()
456 f = os.fdopen(tmp_fd, 'w')
458 resolver = get_resolver(d)
460 # Local the zone for this name
461 zone = dns.resolver.zone_for_name(normalised_name,
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.
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)
473 f.write("update %s %s %u A %s\n" % (op, normalised_name, default_ttl, d.ip))
475 f.write("update %s %s %u AAAA %s\n" % (op, normalised_name, default_ttl, d.ip))
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))
484 f.write("update %s %s %u NS %s\n" % (op, normalised_name, default_ttl, d.dest))
490 # Set a bigger MTU size to work around a bug in nsupdate's doio_send()
491 os.environ["SOCKET_WRAPPER_MTU"] = "2000"
495 os.environ["KRB5CCNAME"] = ccachename
497 cmd = nsupdate_cmd[:]
501 env["KRB5_CONFIG"] = krb5conf
503 env["KRB5CCNAME"] = ccachename
504 ret = subprocess.call(cmd, shell=False, env=env)
506 if opts.fail_immediately:
508 print("Failed update with %s" % tmpfile)
510 error_count = error_count + 1
512 print("Failed nsupdate: %d" % ret)
513 except Exception as estr:
514 if opts.fail_immediately:
516 error_count = error_count + 1
518 print("Failed nsupdate: %s : %s" % (str(d), estr))
521 # Let socket_wrapper set the default MTU size
522 os.environ["SOCKET_WRAPPER_MTU"] = "0"
525 def call_samba_tool(d, op="add", zone=None):
526 """call samba-tool dns to update an entry."""
528 assert(op in ["add", "delete"])
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']))
536 print("Calling samba-tool dns for %s (%s)" % (d, op))
538 normalised_name = d.name.rstrip('.') + '.'
540 if normalised_name == (sub_vars['DNSDOMAIN'] + '.'):
542 zone = sub_vars['DNSDOMAIN']
543 elif normalised_name == (sub_vars['DNSFOREST'] + '.'):
545 zone = sub_vars['DNSFOREST']
546 elif normalised_name == ('_msdcs.' + sub_vars['DNSFOREST'] + '.'):
548 zone = '_msdcs.' + sub_vars['DNSFOREST']
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'] + '.'))
553 elif normalised_name.endswith('._msdcs.' + sub_vars['DNSFOREST'] + '.'):
554 zone = '_msdcs.' + sub_vars['DNSFOREST']
556 zone = sub_vars['DNSDOMAIN']
557 len_zone = len(zone)+2
558 short_name = normalised_name[:-len_zone]
560 len_zone = len(zone)+2
561 short_name = normalised_name[:-len_zone]
564 args = [rpc_server_ip, zone, short_name, "A", d.ip]
566 args = [rpc_server_ip, zone, short_name, "AAAA", d.ip]
568 if op == "add" and d.existing_port is not None:
569 print("Not handling modify of exising SRV %s using samba-tool" % d)
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")]
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]
583 args = [rpc_server_ip, zone, short_name, "CNAME",
584 d.existing_cname_target.rstrip('.'), d.dest]
587 args = [rpc_server_ip, zone, short_name, "NS", d.dest]
589 if smb_conf and args:
590 args += ["--configfile=" + smb_conf]
596 print("Calling samba-tool dns %s -k no -P %s" % (op, args))
597 ret = cmd._run("dns", op, "-k", "no", "-P", *args)
599 if opts.fail_immediately:
601 error_count = error_count + 1
603 print("Failed 'samba-tool dns' based update of %s" % (str(d)))
604 except Exception as estr:
605 if opts.fail_immediately:
607 error_count = error_count + 1
609 print("Failed 'samba-tool dns' based update: %s : %s" % (str(d), estr))
613 def cached_irpc_wb(lp):
615 if irpc_wb is not None:
617 irpc_wb = winbind.winbind("irpc:winbind_server", lp)
620 def rodc_dns_update(d, t, op):
621 '''a single DNS update via the RODC netlogon call'''
624 assert(op in ["add", "delete"])
627 print("Calling netlogon RODC update for %s" % d)
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
639 w = cached_irpc_wb(lp)
640 dns_names = netlogon.NL_DNS_NAME_INFO_ARRAY()
642 name = netlogon.NL_DNS_NAME_INFO()
644 name.dns_domain_info_type = typemap[t]
647 if d.port is not None:
648 name.port = int(d.port)
650 name.dns_register = True
652 name.dns_register = False
653 dns_names.names = [ name ]
654 site_name = text_type(sub_vars['SITE'])
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
668 print("Called netlogon RODC update for %s" % d)
670 if error_count != 0 and opts.fail_immediately:
674 def call_rodc_update(d, op="add"):
675 '''RODCs need to use the netlogon API for nsupdate'''
678 assert(op in ["add", "delete"])
680 # we expect failure for 3268 if we aren't a GC
681 if d.port is not None and int(d.port) == 3268:
684 # map the DNS request to a netlogon update type
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}'
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)
702 print("Unable to map to netlogon DNS update: %s" % d)
705 # get the list of DNS entries we should have
706 dns_update_list = opts.update_list or lp.private_path('dns_update_list')
708 dns_update_cache = opts.update_cache or lp.private_path('dns_update_cache')
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
718 file = open(dns_update_list, "r")
723 samdb = SamDB(url=lp.samdb_url(), session_info=system_session(), lp=lp)
725 # get the substitution dictionary
726 sub_vars = get_subst_vars(samdb)
728 # build up a list of update commands to pass to nsupdate
737 rebuild_cache = False
739 cfile = open(dns_update_cache, 'r+')
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)
748 if line == '' or line[0] == "#":
750 c = parse_dns_line(line, {})
753 if str(c) not in cache_set:
755 cache_set.add(str(c))
757 site_specific_rec = []
759 # read each line, and check that the DNS name exists
763 if '${SITE}' in line:
764 site_specific_rec.append(line)
766 if line == '' or line[0] == "#":
768 d = parse_dns_line(line, sub_vars)
771 if d.type == 'A' and len(IP4s) == 0:
773 if d.type == 'AAAA' and len(IP6s) == 0:
775 if str(d) not in dup_set:
779 # Perform automatic site coverage by default
782 if not am_rodc and auto_coverage:
783 site_names = kcc_utils.uncovered_sites_to_cover(samdb,
784 samdb.server_site_name())
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]
791 for site_line in to_add:
792 d = parse_dns_line(site_line,
794 if d is not None and str(d) not in dup_set:
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
805 for i in range(len(IP4s)-1):
811 for i in range(len(IP6s)-1):
816 # now check if the entries already exist on the DNS server
820 if str(c).lower() == str(d).lower():
826 print("need cache add: %s" % d)
827 if dns_zone_scavenging:
828 update_list.append(d)
830 print("scavenging requires update: %s" % d)
832 update_list.append(d)
834 print("force update: %s" % d)
835 elif not check_dns_name(d):
836 update_list.append(d)
838 print("need update: %s" % d)
843 if str(c).lower() == str(d).lower():
850 print("need cache remove: %s" % c)
851 if not opts.all_names and not check_dns_name(c):
853 delete_list.append(c)
855 print("need delete: %s" % c)
857 if len(delete_list) == 0 and len(update_list) == 0 and not rebuild_cache:
859 print("No DNS updates needed")
863 print("%d DNS updates and %d DNS deletes needed" % (len(update_list), len(delete_list)))
865 use_samba_tool = opts.use_samba_tool
866 use_nsupdate = opts.use_nsupdate
868 if delete_list or update_list and not opts.nocreds:
870 creds = get_credentials(lp)
871 except RuntimeError as e:
874 if sub_vars['IF_RWDNS_DOMAIN'] == "# ":
880 print("Failed to get Kerberos credentials, falling back to samba-tool: %s" % e)
881 use_samba_tool = True
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):
888 print("update (samba-tool): %s" % d)
889 call_samba_tool(d, op="delete", zone=d.zone)
892 if d.name.lower() == domain.lower():
894 print("skip delete (rodc): %s" % d)
896 if not d.type in [ 'A', 'AAAA' ]:
898 print("delete (rodc): %s" % d)
899 call_rodc_update(d, op="delete")
902 print("delete (nsupdate): %s" % d)
903 call_nsupdate(d, op="delete")
906 print("delete (nsupdate): %s" % d)
907 call_nsupdate(d, op="delete")
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):
913 print("update (samba-tool): %s" % d)
914 call_samba_tool(d, zone=d.zone)
917 if d.name.lower() == domain.lower():
919 print("skip (rodc): %s" % d)
921 if not d.type in [ 'A', 'AAAA' ]:
923 print("update (rodc): %s" % d)
927 print("update (nsupdate): %s" % d)
931 print("update(nsupdate): %s" % d)
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')
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)
946 # delete the ccache if we created it
947 if ccachename is not None:
948 os.unlink(ccachename)
951 print("Failed update of %u entries" % error_count)
952 sys.exit(error_count)