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
54 samba.ensure_third_party_module("dns", "dnspython")
62 parser = optparse.OptionParser("samba_dnsupdate")
63 sambaopts = options.SambaOptions(parser)
64 parser.add_option_group(sambaopts)
65 parser.add_option_group(options.VersionOptions(parser))
66 parser.add_option("--verbose", action="store_true")
67 parser.add_option("--use-samba-tool", action="store_true", help="Use samba-tool to make updates over RPC, rather than over DNS")
68 parser.add_option("--use-nsupdate", action="store_true", help="Use nsupdate command to make updates over DNS (default, if kinit successful)")
69 parser.add_option("--all-names", action="store_true")
70 parser.add_option("--all-interfaces", action="store_true")
71 parser.add_option("--current-ip", action="append", help="IP address to update DNS to match (helpful if behind NAT, valid multiple times, defaults to values from interfaces=)")
72 parser.add_option("--rpc-server-ip", type="string", help="IP address of server to use with samba-tool (defaults to first --current-ip)")
73 parser.add_option("--use-file", type="string", help="Use a file, rather than real DNS calls")
74 parser.add_option("--update-list", type="string", help="Add DNS names from the given file")
75 parser.add_option("--update-cache", type="string", help="Cache database of already registered records")
76 parser.add_option("--fail-immediately", action='store_true', help="Exit on first failure")
77 parser.add_option("--no-credentials", dest='nocreds', action='store_true', help="don't try and get credentials")
78 parser.add_option("--no-substitutions", dest='nosubs', action='store_true', help="don't try and expands variables in file specified by --update-list")
83 opts, args = parser.parse_args()
89 lp = sambaopts.get_loadparm()
91 domain = lp.get("realm")
92 host = lp.get("netbios name")
93 if opts.all_interfaces:
96 all_interfaces = False
101 IPs = samba.interface_ips(lp, all_interfaces)
103 nsupdate_cmd = lp.get('nsupdate command')
105 dns_zone_scavenging = lp.get("dns zone scavenging")
108 print("No IP interfaces - skipping DNS updates")
111 if opts.rpc_server_ip:
112 rpc_server_ip = opts.rpc_server_ip
114 rpc_server_ip = IPs[0]
119 if i.find(':') != -1:
126 print("IPs: %s" % IPs)
128 def get_possible_rw_dns_server(creds, domain):
129 """Get a list of possible read-write DNS servers, starting with
130 the SOA. The SOA is the correct answer, but old Samba domains
131 (4.6 and prior) do not maintain this value, so add NS servers
135 ans_soa = check_one_dns_name(domain, 'SOA')
137 # Actually there is only one
138 for i in range(len(ans_soa)):
139 hostnames.append(str(ans_soa[i].mname).rstrip('.'))
141 # This is not strictly legit, but old Samba domains may have an
142 # unmaintained SOA record, so go for any NS that we can get a
144 ans_ns = check_one_dns_name(domain, 'NS')
146 # Actually there is only one
147 for i in range(len(ans_ns)):
148 hostnames.append(str(ans_ns[i].target).rstrip('.'))
152 def get_krb5_rw_dns_server(creds, domain):
153 """Get a list of read-write DNS servers that we can obtain a ticket
154 for, starting with the SOA. The SOA is the correct answer, but
155 old Samba domains (4.6 and prior) do not maintain this value,
156 so continue with the NS servers as well until we get one that
157 the KDC will issue a ticket to.
160 rw_dns_servers = get_possible_rw_dns_server(creds, domain)
161 # Actually there is only one
162 for i in range(len(rw_dns_servers)):
163 target_hostname = str(rw_dns_servers[i])
165 settings["lp_ctx"] = lp
166 settings["target_hostname"] = target_hostname
168 gensec_client = gensec.Security.start_client(settings)
169 gensec_client.set_credentials(creds)
170 gensec_client.set_target_service("DNS")
171 gensec_client.set_target_hostname(target_hostname)
172 gensec_client.want_feature(gensec.FEATURE_SEAL)
173 gensec_client.start_mech_by_sasl_name("GSSAPI")
174 server_to_client = ""
176 (client_finished, client_to_server) = gensec_client.update(server_to_client)
178 print("Successfully obtained Kerberos ticket to DNS/%s as %s" \
179 % (target_hostname, creds.get_username()))
180 return target_hostname
182 # Only raise an exception if they all failed
183 if i != len(rw_dns_servers) - 1:
187 def get_credentials(lp):
188 """# get credentials if we haven't got them already."""
189 from samba import credentials
191 creds = credentials.Credentials()
193 creds.set_machine_account(lp)
194 creds.set_krb_forwardable(credentials.NO_KRB_FORWARDABLE)
195 (tmp_fd, ccachename) = tempfile.mkstemp()
197 creds.get_named_ccache(lp, ccachename)
199 if opts.use_file is not None:
202 # Now confirm we can get a ticket to the DNS server
203 get_krb5_rw_dns_server(creds, sub_vars['DNSDOMAIN'] + '.')
206 except RuntimeError as e:
207 os.unlink(ccachename)
211 class dnsobj(object):
212 """an object to hold a parsed DNS line"""
214 def __init__(self, string_form):
215 list = string_form.split()
217 raise Exception("Invalid DNS entry %r" % string_form)
221 self.existing_port = None
222 self.existing_weight = None
223 self.existing_cname_target = None
232 self.nameservers = []
233 if self.type == 'SRV':
235 raise Exception("Invalid DNS entry %r" % string_form)
238 elif self.type in ['A', 'AAAA']:
239 self.ip = list[2] # usually $IP, which gets replaced
240 elif self.type == 'CNAME':
242 elif self.type == 'NS':
245 raise Exception("Received unexpected DNS reply of type %s: %s" % (self.type, string_form))
249 return "%s %s %s" % (self.type, self.name, self.ip)
250 if self.type == "AAAA":
251 return "%s %s %s" % (self.type, self.name, self.ip)
252 if self.type == "SRV":
253 return "%s %s %s %s" % (self.type, self.name, self.dest, self.port)
254 if self.type == "CNAME":
255 return "%s %s %s" % (self.type, self.name, self.dest)
256 if self.type == "NS":
257 return "%s %s %s" % (self.type, self.name, self.dest)
260 def parse_dns_line(line, sub_vars):
261 """parse a DNS line from."""
262 if line.startswith("SRV _ldap._tcp.pdc._msdcs.") and not samdb.am_pdc():
263 # We keep this as compat to the dns_update_list of 4.0/4.1
265 print("Skipping PDC entry (%s) as we are not a PDC" % line)
267 subline = samba.substitute_var(line, sub_vars)
268 if subline == '' or subline[0] == "#":
270 return dnsobj(subline)
273 def hostname_match(h1, h2):
274 """see if two hostnames match."""
277 return h1.lower().rstrip('.') == h2.lower().rstrip('.')
279 def get_resolver(d=None):
280 resolv_conf = os.getenv('RESOLV_CONF')
282 resolv_conf = '/etc/resolv.conf'
283 resolver = dns.resolver.Resolver(filename=resolv_conf, configure=True)
285 if d is not None and d.nameservers != []:
286 resolver.nameservers = d.nameservers
290 def check_one_dns_name(name, name_type, d=None):
291 resolver = get_resolver(d)
292 if d is not None and len(d.nameservers) == 0:
293 d.nameservers = resolver.nameservers
295 ans = resolver.query(name, name_type)
298 def check_dns_name(d):
299 """check that a DNS entry exists."""
300 normalised_name = d.name.rstrip('.') + '.'
302 print("Looking for DNS entry %s as %s" % (d, normalised_name))
304 if opts.use_file is not None:
306 dns_file = open(opts.use_file, "r")
310 for line in dns_file:
312 if line == '' or line[0] == "#":
314 if line.lower() == str(d).lower():
319 ans = check_one_dns_name(normalised_name, d.type, d)
320 except dns.exception.Timeout:
321 raise Exception("Timeout while waiting to contact a working DNS server while looking for %s as %s" % (d, normalised_name))
322 except dns.resolver.NoNameservers:
323 raise Exception("Unable to contact a working DNS server while looking for %s as %s" % (d, normalised_name))
324 except dns.resolver.NXDOMAIN:
326 print("The DNS entry %s, queried as %s does not exist" % (d, normalised_name))
328 except dns.resolver.NoAnswer:
330 print("The DNS entry %s, queried as %s does not hold this record type" % (d, normalised_name))
332 except dns.exception.DNSException:
333 raise Exception("Failure while trying to resolve %s as %s" % (d, normalised_name))
334 if d.type in ['A', 'AAAA']:
335 # we need to be sure that our IP is there
337 if str(rdata) == str(d.ip):
339 elif d.type == 'CNAME':
340 for i in range(len(ans)):
341 if hostname_match(ans[i].target, d.dest):
344 d.existing_cname_target = str(ans[i].target)
346 for i in range(len(ans)):
347 if hostname_match(ans[i].target, d.dest):
349 elif d.type == 'SRV':
352 print("Checking %s against %s" % (rdata, d))
353 if hostname_match(rdata.target, d.dest):
354 if str(rdata.port) == str(d.port):
357 d.existing_port = str(rdata.port)
358 d.existing_weight = str(rdata.weight)
361 print("Lookup of %s succeeded, but we failed to find a matching DNS entry for %s" % (normalised_name, d))
366 def get_subst_vars(samdb):
367 """get the list of substitution vars."""
371 vars['DNSDOMAIN'] = samdb.domain_dns_name()
372 vars['DNSFOREST'] = samdb.forest_dns_name()
373 vars['HOSTNAME'] = samdb.host_dns_name()
374 vars['NTDSGUID'] = samdb.get_ntds_GUID()
375 vars['SITE'] = samdb.server_site_name()
376 res = samdb.search(base=samdb.get_default_basedn(), scope=SCOPE_BASE, attrs=["objectGUID"])
377 guid = samdb.schema_format_value("objectGUID", res[0]['objectGUID'][0])
378 vars['DOMAINGUID'] = guid
381 vars['IF_RWDC'] = "# "
382 vars['IF_RODC'] = "# "
383 vars['IF_PDC'] = "# "
385 vars['IF_RWGC'] = "# "
386 vars['IF_ROGC'] = "# "
387 vars['IF_DNS_DOMAIN'] = "# "
388 vars['IF_RWDNS_DOMAIN'] = "# "
389 vars['IF_RODNS_DOMAIN'] = "# "
390 vars['IF_DNS_FOREST'] = "# "
391 vars['IF_RWDNS_FOREST'] = "# "
392 vars['IF_R0DNS_FOREST'] = "# "
394 am_rodc = samdb.am_rodc()
403 # check if we "are DNS server"
404 res = samdb.search(base=samdb.get_config_basedn(),
405 expression='(objectguid=%s)' % vars['NTDSGUID'],
406 attrs=["options", "msDS-hasMasterNCs"])
409 if "options" in res[0]:
410 options = int(res[0]["options"][0])
411 if (options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
418 basedn = str(samdb.get_default_basedn())
419 forestdn = str(samdb.get_root_basedn())
421 if "msDS-hasMasterNCs" in res[0]:
422 for e in res[0]["msDS-hasMasterNCs"]:
423 if str(e) == "DC=DomainDnsZones,%s" % basedn:
424 vars['IF_DNS_DOMAIN'] = ""
426 vars['IF_RODNS_DOMAIN'] = ""
428 vars['IF_RWDNS_DOMAIN'] = ""
429 if str(e) == "DC=ForestDnsZones,%s" % forestdn:
430 vars['IF_DNS_FOREST'] = ""
432 vars['IF_RODNS_FOREST'] = ""
434 vars['IF_RWDNS_FOREST'] = ""
439 def call_nsupdate(d, op="add"):
440 """call nsupdate for an entry."""
441 global ccachename, nsupdate_cmd, krb5conf
443 assert(op in ["add", "delete"])
446 print("Calling nsupdate for %s (%s)" % (d, op))
448 if opts.use_file is not None:
450 rfile = open(opts.use_file, 'r+')
453 rfile = open(opts.use_file, 'w+')
454 # Open it for reading again, in case someone else got to it first
455 rfile = open(opts.use_file, 'r+')
456 fcntl.lockf(rfile, fcntl.LOCK_EX)
457 (file_dir, file_name) = os.path.split(opts.use_file)
458 (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
459 wfile = os.fdopen(tmp_fd, 'a')
463 l = parse_dns_line(line, {})
464 if str(l).lower() == str(d).lower():
468 wfile.write(str(d)+"\n")
469 os.rename(tmpfile, opts.use_file)
470 fcntl.lockf(rfile, fcntl.LOCK_UN)
473 normalised_name = d.name.rstrip('.') + '.'
475 (tmp_fd, tmpfile) = tempfile.mkstemp()
476 f = os.fdopen(tmp_fd, 'w')
478 # Getting this line right is really important. When we are under
479 # resolv_wrapper, then we want to use RESOLV_CONF and the
480 # nameserver therein. The issue is that this parameter forces us
481 # to only ever use that server, and not some other server that the
482 # NS record may point to, even as we get a ticket to that other
485 # Therefore we must not set this in production, instead we want
486 # to find the name of a SOA for the zone and use that server.
488 if os.getenv('RESOLV_CONF') and d.nameservers != []:
489 f.write('server %s\n' % d.nameservers[0])
491 resolver = get_resolver(d)
493 # Local the zone for this name
494 zone = dns.resolver.zone_for_name(normalised_name,
497 # Now find the SOA, or if we can't get a ticket to the SOA,
498 # any server with an NS record we can get a ticket for.
500 # Thanks to the Kerberos Credentials cache this is not
501 # expensive inside the loop
502 server = get_krb5_rw_dns_server(creds, zone)
503 f.write('server %s\n' % server)
506 f.write("update %s %s %u A %s\n" % (op, normalised_name, default_ttl, d.ip))
508 f.write("update %s %s %u AAAA %s\n" % (op, normalised_name, default_ttl, d.ip))
510 if op == "add" and d.existing_port is not None:
511 f.write("update delete %s SRV 0 %s %s %s\n" % (normalised_name, d.existing_weight,
512 d.existing_port, d.dest))
513 f.write("update %s %s %u SRV 0 100 %s %s\n" % (op, normalised_name, default_ttl, d.port, d.dest))
514 if d.type == "CNAME":
515 f.write("update %s %s %u CNAME %s\n" % (op, normalised_name, default_ttl, d.dest))
517 f.write("update %s %s %u NS %s\n" % (op, normalised_name, default_ttl, d.dest))
523 # Set a bigger MTU size to work around a bug in nsupdate's doio_send()
524 os.environ["SOCKET_WRAPPER_MTU"] = "2000"
528 os.environ["KRB5CCNAME"] = ccachename
530 cmd = nsupdate_cmd[:]
534 env["KRB5_CONFIG"] = krb5conf
536 env["KRB5CCNAME"] = ccachename
537 ret = subprocess.call(cmd, shell=False, env=env)
539 if opts.fail_immediately:
541 print("Failed update with %s" % tmpfile)
543 error_count = error_count + 1
545 print("Failed nsupdate: %d" % ret)
546 except Exception as estr:
547 if opts.fail_immediately:
549 error_count = error_count + 1
551 print("Failed nsupdate: %s : %s" % (str(d), estr))
554 # Let socket_wrapper set the default MTU size
555 os.environ["SOCKET_WRAPPER_MTU"] = "0"
558 def call_samba_tool(d, op="add", zone=None):
559 """call samba-tool dns to update an entry."""
561 assert(op in ["add", "delete"])
563 if (sub_vars['DNSFOREST'] != sub_vars['DNSDOMAIN']) and \
564 sub_vars['DNSFOREST'].endswith('.' + sub_vars['DNSDOMAIN']):
565 print("Refusing to use samba-tool when forest %s is under domain %s" \
566 % (sub_vars['DNSFOREST'], sub_vars['DNSDOMAIN']))
569 print("Calling samba-tool dns for %s (%s)" % (d, op))
571 normalised_name = d.name.rstrip('.') + '.'
573 if normalised_name == (sub_vars['DNSDOMAIN'] + '.'):
575 zone = sub_vars['DNSDOMAIN']
576 elif normalised_name == (sub_vars['DNSFOREST'] + '.'):
578 zone = sub_vars['DNSFOREST']
579 elif normalised_name == ('_msdcs.' + sub_vars['DNSFOREST'] + '.'):
581 zone = '_msdcs.' + sub_vars['DNSFOREST']
583 if not normalised_name.endswith('.' + sub_vars['DNSDOMAIN'] + '.'):
584 print("Not Calling samba-tool dns for %s (%s), %s not in %s" % (d, op, normalised_name, sub_vars['DNSDOMAIN'] + '.'))
586 elif normalised_name.endswith('._msdcs.' + sub_vars['DNSFOREST'] + '.'):
587 zone = '_msdcs.' + sub_vars['DNSFOREST']
589 zone = sub_vars['DNSDOMAIN']
590 len_zone = len(zone)+2
591 short_name = normalised_name[:-len_zone]
593 len_zone = len(zone)+2
594 short_name = normalised_name[:-len_zone]
597 args = [rpc_server_ip, zone, short_name, "A", d.ip]
599 args = [rpc_server_ip, zone, short_name, "AAAA", d.ip]
601 if op == "add" and d.existing_port is not None:
602 print("Not handling modify of exising SRV %s using samba-tool" % d)
605 args = [rpc_server_ip, zone, short_name, "SRV",
606 "%s %s %s %s" % (d.existing_weight,
607 d.existing_port, "0", "100"),
608 "%s %s %s %s" % (d.dest, d.port, "0", "100")]
610 args = [rpc_server_ip, zone, short_name, "SRV", "%s %s %s %s" % (d.dest, d.port, "0", "100")]
611 if d.type == "CNAME":
612 if d.existing_cname_target is None:
613 args = [rpc_server_ip, zone, short_name, "CNAME", d.dest]
616 args = [rpc_server_ip, zone, short_name, "CNAME",
617 d.existing_cname_target.rstrip('.'), d.dest]
620 args = [rpc_server_ip, zone, short_name, "NS", d.dest]
626 print("Calling samba-tool dns %s -k no -P %s" % (op, args))
627 ret = cmd._run("dns", op, "-k", "no", "-P", *args)
629 if opts.fail_immediately:
631 error_count = error_count + 1
633 print("Failed 'samba-tool dns' based update of %s" % (str(d)))
634 except Exception as estr:
635 if opts.fail_immediately:
637 error_count = error_count + 1
639 print("Failed 'samba-tool dns' based update: %s : %s" % (str(d), estr))
642 def rodc_dns_update(d, t, op):
643 '''a single DNS update via the RODC netlogon call'''
646 assert(op in ["add", "delete"])
649 print("Calling netlogon RODC update for %s" % d)
652 netlogon.NlDnsLdapAtSite : netlogon.NlDnsInfoTypeNone,
653 netlogon.NlDnsGcAtSite : netlogon.NlDnsDomainNameAlias,
654 netlogon.NlDnsDsaCname : netlogon.NlDnsDomainNameAlias,
655 netlogon.NlDnsKdcAtSite : netlogon.NlDnsInfoTypeNone,
656 netlogon.NlDnsDcAtSite : netlogon.NlDnsInfoTypeNone,
657 netlogon.NlDnsRfc1510KdcAtSite : netlogon.NlDnsInfoTypeNone,
658 netlogon.NlDnsGenericGcAtSite : netlogon.NlDnsDomainNameAlias
661 w = winbind.winbind("irpc:winbind_server", lp)
662 dns_names = netlogon.NL_DNS_NAME_INFO_ARRAY()
664 name = netlogon.NL_DNS_NAME_INFO()
666 name.dns_domain_info_type = typemap[t]
669 if d.port is not None:
670 name.port = int(d.port)
672 name.dns_register = True
674 name.dns_register = False
675 dns_names.names = [ name ]
676 site_name = sub_vars['SITE'].decode('utf-8')
681 ret_names = w.DsrUpdateReadOnlyServerDnsRecords(site_name, default_ttl, dns_names)
682 if ret_names.names[0].status != 0:
683 print("Failed to set DNS entry: %s (status %u)" % (d, ret_names.names[0].status))
684 error_count = error_count + 1
685 except RuntimeError as reason:
686 print("Error setting DNS entry of type %u: %s: %s" % (t, d, reason))
687 error_count = error_count + 1
689 if error_count != 0 and opts.fail_immediately:
693 def call_rodc_update(d, op="add"):
694 '''RODCs need to use the netlogon API for nsupdate'''
697 assert(op in ["add", "delete"])
699 # we expect failure for 3268 if we aren't a GC
700 if d.port is not None and int(d.port) == 3268:
703 # map the DNS request to a netlogon update type
705 netlogon.NlDnsLdapAtSite : '_ldap._tcp.${SITE}._sites.${DNSDOMAIN}',
706 netlogon.NlDnsGcAtSite : '_ldap._tcp.${SITE}._sites.gc._msdcs.${DNSDOMAIN}',
707 netlogon.NlDnsDsaCname : '${NTDSGUID}._msdcs.${DNSFOREST}',
708 netlogon.NlDnsKdcAtSite : '_kerberos._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
709 netlogon.NlDnsDcAtSite : '_ldap._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
710 netlogon.NlDnsRfc1510KdcAtSite : '_kerberos._tcp.${SITE}._sites.${DNSDOMAIN}',
711 netlogon.NlDnsGenericGcAtSite : '_gc._tcp.${SITE}._sites.${DNSFOREST}'
715 subname = samba.substitute_var(map[t], sub_vars)
716 if subname.lower() == d.name.lower():
717 # found a match - do the update
718 rodc_dns_update(d, t, op)
721 print("Unable to map to netlogon DNS update: %s" % d)
724 # get the list of DNS entries we should have
726 dns_update_list = opts.update_list
728 dns_update_list = lp.private_path('dns_update_list')
730 if opts.update_cache:
731 dns_update_cache = opts.update_cache
733 dns_update_cache = lp.private_path('dns_update_cache')
736 # only change the krb5.conf if we are not in selftest
737 if 'SOCKET_WRAPPER_DIR' not in os.environ:
738 # use our private krb5.conf to avoid problems with the wrong domain
739 # bind9 nsupdate wants the default domain set
740 krb5conf = lp.private_path('krb5.conf')
741 os.environ['KRB5_CONFIG'] = krb5conf
743 file = open(dns_update_list, "r")
748 samdb = SamDB(url=lp.samdb_url(), session_info=system_session(), lp=lp)
750 # get the substitution dictionary
751 sub_vars = get_subst_vars(samdb)
753 # build up a list of update commands to pass to nsupdate
762 rebuild_cache = False
764 cfile = open(dns_update_cache, 'r+')
767 cfile = open(dns_update_cache, 'w+')
768 # Open it for reading again, in case someone else got to it first
769 cfile = open(dns_update_cache, 'r+')
770 fcntl.lockf(cfile, fcntl.LOCK_EX)
773 if line == '' or line[0] == "#":
775 c = parse_dns_line(line, {})
778 if str(c) not in cache_set:
780 cache_set.add(str(c))
782 site_specific_rec = []
784 # read each line, and check that the DNS name exists
788 if '${SITE}' in line:
789 site_specific_rec.append(line)
791 if line == '' or line[0] == "#":
793 d = parse_dns_line(line, sub_vars)
796 if d.type == 'A' and len(IP4s) == 0:
798 if d.type == 'AAAA' and len(IP6s) == 0:
800 if str(d) not in dup_set:
804 # Perform automatic site coverage by default
807 if not am_rodc and auto_coverage:
808 site_names = kcc_utils.uncovered_sites_to_cover(samdb,
809 samdb.server_site_name())
811 # Duplicate all site specific records for the uncovered site
812 for site in site_names:
813 to_add = [samba.substitute_var(line, {'SITE': site})
814 for line in site_specific_rec]
816 for site_line in to_add:
817 d = parse_dns_line(site_line,
819 if d is not None and str(d) not in dup_set:
823 # now expand the entries, if any are A record with ip set to $IP
824 # then replace with multiple entries, one for each interface IP
830 for i in range(len(IP4s)-1):
836 for i in range(len(IP6s)-1):
841 # now check if the entries already exist on the DNS server
845 if str(c).lower() == str(d).lower():
851 print("need cache add: %s" % d)
852 if dns_zone_scavenging:
853 update_list.append(d)
855 print("scavenging requires update: %s" % d)
857 update_list.append(d)
859 print("force update: %s" % d)
860 elif not check_dns_name(d):
861 update_list.append(d)
863 print("need update: %s" % d)
868 if str(c).lower() == str(d).lower():
875 print("need cache remove: %s" % c)
876 if not opts.all_names and not check_dns_name(c):
878 delete_list.append(c)
880 print("need delete: %s" % c)
882 if len(delete_list) == 0 and len(update_list) == 0 and not rebuild_cache:
884 print("No DNS updates needed")
888 print("%d DNS updates and %d DNS deletes needed" % (len(update_list), len(delete_list)))
890 use_samba_tool = opts.use_samba_tool
891 use_nsupdate = opts.use_nsupdate
893 if len(delete_list) != 0 or len(update_list) != 0 and not opts.nocreds:
895 creds = get_credentials(lp)
896 except RuntimeError as e:
899 if sub_vars['IF_RWDNS_DOMAIN'] == "# ":
905 print("Failed to get Kerberos credentials, falling back to samba-tool: %s" % e)
906 use_samba_tool = True
909 # ask nsupdate to delete entries as needed
910 for d in delete_list:
911 if d.rpc or (not use_nsupdate and use_samba_tool):
913 print("update (samba-tool): %s" % d)
914 call_samba_tool(d, op="delete", zone=d.zone)
917 if d.name.lower() == domain.lower():
919 print("skip delete (rodc): %s" % d)
921 if not d.type in [ 'A', 'AAAA' ]:
923 print("delete (rodc): %s" % d)
924 call_rodc_update(d, op="delete")
927 print("delete (nsupdate): %s" % d)
928 call_nsupdate(d, op="delete")
931 print("delete (nsupdate): %s" % d)
932 call_nsupdate(d, op="delete")
934 # ask nsupdate to add entries as needed
935 for d in update_list:
936 if d.rpc or (not use_nsupdate and use_samba_tool):
938 print("update (samba-tool): %s" % d)
939 call_samba_tool(d, zone=d.zone)
942 if d.name.lower() == domain.lower():
944 print("skip (rodc): %s" % d)
946 if not d.type in [ 'A', 'AAAA' ]:
948 print("update (rodc): %s" % d)
952 print("update (nsupdate): %s" % d)
956 print("update(nsupdate): %s" % d)
960 print("Rebuilding cache at %s" % dns_update_cache)
961 (file_dir, file_name) = os.path.split(dns_update_cache)
962 (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
963 wfile = os.fdopen(tmp_fd, 'a')
966 print("Adding %s to %s" % (str(d), file_name))
967 wfile.write(str(d)+"\n")
968 os.rename(tmpfile, dns_update_cache)
969 fcntl.lockf(cfile, fcntl.LOCK_UN)
971 # delete the ccache if we created it
972 if ccachename is not None:
973 os.unlink(ccachename)
976 print("Failed update of %u entries" % error_count)
977 sys.exit(error_count)