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 [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")
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\n")
106 rpc_server_ip = opts.rpc_server_ip or IPs[0]
108 IP6s = [ip for ip in IPs if ':' in ip]
109 IP4s = [ip for ip in IPs if ':' not in ip]
111 smb_conf = sambaopts.get_loadparm_path()
114 print("IPs: %s" % IPs)
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
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]
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
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]
133 return hosts_soa + hosts_ns
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.
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):
147 settings["lp_ctx"] = lp
148 settings["target_hostname"] = target_hostname
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""
158 (client_finished, client_to_server) = gensec_client.update(server_to_client)
160 print("Successfully obtained Kerberos ticket to DNS/%s as %s" \
161 % (target_hostname, creds.get_username()))
162 return target_hostname
164 # Only raise an exception if they all failed
165 if i == len(rw_dns_servers) - 1:
168 def get_credentials(lp):
169 """# get credentials if we haven't got them already."""
170 from samba import credentials
172 creds = credentials.Credentials()
174 creds.set_machine_account(lp)
175 creds.set_krb_forwardable(credentials.NO_KRB_FORWARDABLE)
176 (tmp_fd, ccachename) = tempfile.mkstemp()
178 if opts.use_file is not None:
181 creds.get_named_ccache(lp, ccachename)
183 # Now confirm we can get a ticket to the DNS server
184 get_krb5_rw_dns_server(creds, sub_vars['DNSDOMAIN'] + '.')
187 except RuntimeError as e:
188 os.unlink(ccachename)
192 class dnsobj(object):
193 """an object to hold a parsed DNS line"""
195 def __init__(self, string_form):
196 list = string_form.split()
198 raise Exception("Invalid DNS entry %r" % string_form)
202 self.existing_port = None
203 self.existing_weight = None
204 self.existing_cname_target = None
213 self.nameservers = []
214 if self.type == 'SRV':
216 raise Exception("Invalid DNS entry %r" % string_form)
219 elif self.type in ['A', 'AAAA']:
220 self.ip = list[2] # usually $IP, which gets replaced
221 elif self.type == 'CNAME':
223 elif self.type == 'NS':
226 raise Exception("Received unexpected DNS reply of type %s: %s" % (self.type, string_form))
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)
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
246 print("Skipping PDC entry (%s) as we are not a PDC" % line)
248 subline = samba.substitute_var(line, sub_vars)
249 if subline == '' or subline[0] == "#":
251 return dnsobj(subline)
254 def hostname_match(h1, h2):
255 """see if two hostnames match."""
258 return h1.lower().rstrip('.') == h2.lower().rstrip('.')
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)
264 if d is not None and d.nameservers != []:
265 resolver.nameservers = d.nameservers
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)
276 def check_dns_name(d):
277 """check that a DNS entry exists."""
278 normalised_name = d.name.rstrip('.') + '.'
280 print("Looking for DNS entry %s as %s" % (d, normalised_name))
282 if opts.use_file is not None:
284 dns_file = open(opts.use_file, "r")
288 for line in dns_file:
290 if line == '' or line[0] == "#":
292 if line.lower() == str(d).lower():
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:
304 print("The DNS entry %s, queried as %s does not exist" % (d, normalised_name))
306 except dns.resolver.NoAnswer:
308 print("The DNS entry %s, queried as %s does not hold this record type" % (d, normalised_name))
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
315 if str(rdata) == str(d.ip):
317 elif d.type == 'CNAME':
318 for i in range(len(ans)):
319 if hostname_match(ans[i].target, d.dest):
322 d.existing_cname_target = str(ans[i].target)
324 for i in range(len(ans)):
325 if hostname_match(ans[i].target, d.dest):
327 elif d.type == 'SRV':
330 print("Checking %s against %s" % (rdata, d))
331 if hostname_match(rdata.target, d.dest):
332 if str(rdata.port) == str(d.port):
335 d.existing_port = str(rdata.port)
336 d.existing_weight = str(rdata.weight)
339 print("Lookup of %s succeeded, but we failed to find a matching DNS entry for %s" % (normalised_name, d))
344 def get_subst_vars(samdb):
345 """get the list of substitution vars."""
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)
359 vars['IF_RWDC'] = "# "
360 vars['IF_RODC'] = "# "
361 vars['IF_PDC'] = "# "
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'] = "# "
372 am_rodc = samdb.am_rodc()
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"])
387 if "options" in res[0]:
388 options = int(res[0]["options"][0])
389 if (options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
396 basedn = str(samdb.get_default_basedn())
397 forestdn = str(samdb.get_root_basedn())
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'] = ""
404 vars['IF_RODNS_DOMAIN'] = ""
406 vars['IF_RWDNS_DOMAIN'] = ""
407 if str(e) == "DC=ForestDnsZones,%s" % forestdn:
408 vars['IF_DNS_FOREST'] = ""
410 vars['IF_RODNS_FOREST'] = ""
412 vars['IF_RWDNS_FOREST'] = ""
417 def call_nsupdate(d, op="add"):
418 """call nsupdate for an entry."""
419 global ccachename, nsupdate_cmd, krb5conf
421 assert(op in ["add", "delete"])
423 if opts.use_file is not None:
425 print("Use File instead of nsupdate for %s (%s)" % (d, op))
428 rfile = open(opts.use_file, 'r+')
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')
441 l = parse_dns_line(line, {})
442 if str(l).lower() == str(d).lower():
446 wfile.write(str(d)+"\n")
447 os.rename(tmpfile, opts.use_file)
448 fcntl.lockf(rfile, fcntl.LOCK_UN)
452 print("Calling nsupdate for %s (%s)" % (d, op))
454 normalised_name = d.name.rstrip('.') + '.'
456 (tmp_fd, tmpfile) = tempfile.mkstemp()
457 f = os.fdopen(tmp_fd, 'w')
459 resolver = get_resolver(d)
461 # Local the zone for this name
462 zone = dns.resolver.zone_for_name(normalised_name,
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.
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)
474 f.write("update %s %s %u A %s\n" % (op, normalised_name, default_ttl, d.ip))
476 f.write("update %s %s %u AAAA %s\n" % (op, normalised_name, default_ttl, d.ip))
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))
485 f.write("update %s %s %u NS %s\n" % (op, normalised_name, default_ttl, d.dest))
491 # Set a bigger MTU size to work around a bug in nsupdate's doio_send()
492 os.environ["SOCKET_WRAPPER_MTU"] = "2000"
496 os.environ["KRB5CCNAME"] = ccachename
498 cmd = nsupdate_cmd[:]
502 env["KRB5_CONFIG"] = krb5conf
504 env["KRB5CCNAME"] = ccachename
505 ret = subprocess.call(cmd, shell=False, env=env)
507 if opts.fail_immediately:
509 print("Failed update with %s" % tmpfile)
511 error_count = error_count + 1
513 print("Failed nsupdate: %d" % ret)
514 except Exception as estr:
515 if opts.fail_immediately:
517 error_count = error_count + 1
519 print("Failed nsupdate: %s : %s" % (str(d), estr))
522 # Let socket_wrapper set the default MTU size
523 os.environ["SOCKET_WRAPPER_MTU"] = "0"
526 def call_samba_tool(d, op="add", zone=None):
527 """call samba-tool dns to update an entry."""
529 assert(op in ["add", "delete"])
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']))
537 print("Calling samba-tool dns for %s (%s)" % (d, op))
539 normalised_name = d.name.rstrip('.') + '.'
541 if normalised_name == (sub_vars['DNSDOMAIN'] + '.'):
543 zone = sub_vars['DNSDOMAIN']
544 elif normalised_name == (sub_vars['DNSFOREST'] + '.'):
546 zone = sub_vars['DNSFOREST']
547 elif normalised_name == ('_msdcs.' + sub_vars['DNSFOREST'] + '.'):
549 zone = '_msdcs.' + sub_vars['DNSFOREST']
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'] + '.'))
554 elif normalised_name.endswith('._msdcs.' + sub_vars['DNSFOREST'] + '.'):
555 zone = '_msdcs.' + sub_vars['DNSFOREST']
557 zone = sub_vars['DNSDOMAIN']
558 len_zone = len(zone)+2
559 short_name = normalised_name[:-len_zone]
561 len_zone = len(zone)+2
562 short_name = normalised_name[:-len_zone]
565 args = [rpc_server_ip, zone, short_name, "A", d.ip]
567 args = [rpc_server_ip, zone, short_name, "AAAA", d.ip]
569 if op == "add" and d.existing_port is not None:
570 print("Not handling modify of existing SRV %s using samba-tool" % d)
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]
579 args = [rpc_server_ip, zone, short_name, "CNAME",
580 d.existing_cname_target.rstrip('.'), d.dest]
583 args = [rpc_server_ip, zone, short_name, "NS", d.dest]
585 if smb_conf and args:
586 args += ["--configfile=" + smb_conf]
592 print("Calling samba-tool dns %s -k no -P %s" % (op, args))
593 ret = cmd._run("dns", op, "-k", "no", "-P", *args)
595 if opts.fail_immediately:
597 error_count = error_count + 1
599 print("Failed 'samba-tool dns' based update of %s" % (str(d)))
600 except Exception as estr:
601 if opts.fail_immediately:
603 error_count = error_count + 1
605 print("Failed 'samba-tool dns' based update: %s : %s" % (str(d), estr))
609 def cached_irpc_wb(lp):
611 if irpc_wb is not None:
613 irpc_wb = winbind.winbind("irpc:winbind_server", lp)
616 def rodc_dns_update(d, t, op):
617 '''a single DNS update via the RODC netlogon call'''
620 assert(op in ["add", "delete"])
623 print("Calling netlogon RODC update for %s" % d)
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
635 w = cached_irpc_wb(lp)
636 dns_names = netlogon.NL_DNS_NAME_INFO_ARRAY()
638 name = netlogon.NL_DNS_NAME_INFO()
640 name.dns_domain_info_type = typemap[t]
643 if d.port is not None:
644 name.port = int(d.port)
646 name.dns_register = True
648 name.dns_register = False
649 dns_names.names = [ name ]
650 site_name = text_type(sub_vars['SITE'])
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
664 print("Called netlogon RODC update for %s" % d)
666 if error_count != 0 and opts.fail_immediately:
670 def call_rodc_update(d, op="add"):
671 '''RODCs need to use the netlogon API for nsupdate'''
674 assert(op in ["add", "delete"])
676 # we expect failure for 3268 if we aren't a GC
677 if d.port is not None and int(d.port) == 3268:
680 # map the DNS request to a netlogon update type
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}'
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)
698 print("Unable to map to netlogon DNS update: %s" % d)
701 # get the list of DNS entries we should have
702 dns_update_list = opts.update_list or lp.private_path('dns_update_list')
704 dns_update_cache = opts.update_cache or lp.private_path('dns_update_cache')
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
715 file = open(dns_update_list, "r")
717 if opts.update_cache:
718 print("The specified update list does not exist")
720 print("The server update list was not found, "
721 "and --update-list was not provided.")
730 samdb = SamDB(url=lp.samdb_url(), session_info=system_session(), lp=lp)
732 # get the substitution dictionary
733 sub_vars = get_subst_vars(samdb)
735 # build up a list of update commands to pass to nsupdate
744 rebuild_cache = False
746 cfile = open(dns_update_cache, 'r+')
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)
755 if line == '' or line[0] == "#":
757 c = parse_dns_line(line, {})
760 if str(c) not in cache_set:
762 cache_set.add(str(c))
764 site_specific_rec = []
766 # read each line, and check that the DNS name exists
770 if '${SITE}' in line:
771 site_specific_rec.append(line)
773 if line == '' or line[0] == "#":
775 d = parse_dns_line(line, sub_vars)
778 if d.type == 'A' and len(IP4s) == 0:
780 if d.type == 'AAAA' and len(IP6s) == 0:
782 if str(d) not in dup_set:
786 # Perform automatic site coverage by default
789 if not am_rodc and auto_coverage:
790 site_names = kcc_utils.uncovered_sites_to_cover(samdb,
791 samdb.server_site_name())
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]
798 for site_line in to_add:
799 d = parse_dns_line(site_line,
801 if d is not None and str(d) not in dup_set:
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
812 for i in range(len(IP4s)-1):
818 for i in range(len(IP6s)-1):
823 # now check if the entries already exist on the DNS server
827 if str(c).lower() == str(d).lower():
833 print("need cache add: %s" % d)
834 if dns_zone_scavenging:
835 update_list.append(d)
837 print("scavenging requires update: %s" % d)
839 update_list.append(d)
841 print("force update: %s" % d)
842 elif not check_dns_name(d):
843 update_list.append(d)
845 print("need update: %s" % d)
850 if str(c).lower() == str(d).lower():
857 print("need cache remove: %s" % c)
858 if not opts.all_names and not check_dns_name(c):
860 delete_list.append(c)
862 print("need delete: %s" % c)
864 if len(delete_list) == 0 and len(update_list) == 0 and not rebuild_cache:
866 print("No DNS updates needed")
870 print("%d DNS updates and %d DNS deletes needed" % (len(update_list), len(delete_list)))
872 use_samba_tool = opts.use_samba_tool
873 use_nsupdate = opts.use_nsupdate
875 if (delete_list or update_list) and not opts.nocreds:
877 creds = get_credentials(lp)
878 except RuntimeError as e:
881 if sub_vars['IF_RWDNS_DOMAIN'] == "# ":
887 print("Failed to get Kerberos credentials, falling back to samba-tool: %s" % e)
888 use_samba_tool = True
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):
895 print("delete (samba-tool): %s" % d)
896 call_samba_tool(d, op="delete", zone=d.zone)
899 if d.name.lower() == domain.lower():
901 print("skip delete (rodc): %s" % d)
903 if not d.type in [ 'A', 'AAAA' ]:
905 print("delete (rodc): %s" % d)
906 call_rodc_update(d, op="delete")
909 print("delete (nsupdate): %s" % d)
910 call_nsupdate(d, op="delete")
913 print("delete (nsupdate): %s" % d)
914 call_nsupdate(d, op="delete")
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):
920 print("update (samba-tool): %s" % d)
921 call_samba_tool(d, zone=d.zone)
924 if d.name.lower() == domain.lower():
926 print("skip (rodc): %s" % d)
928 if not d.type in [ 'A', 'AAAA' ]:
930 print("update (rodc): %s" % d)
934 print("update (nsupdate): %s" % d)
938 print("update(nsupdate): %s" % d)
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')
948 print("Adding %s to %s" % (str(d), file_name))
949 wfile.write(str(d)+"\n")
951 os.rename(tmpfile, dns_update_cache)
952 fcntl.lockf(cfile, fcntl.LOCK_UN)
954 # delete the ccache if we created it
955 if ccachename is not None:
956 os.unlink(ccachename)
959 print("Failed update of %u entries" % error_count)
960 sys.exit(error_count)