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
55 samba.ensure_third_party_module("dns", "dnspython")
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 if opts.all_interfaces:
97 all_interfaces = False
100 IPs = opts.current_ip
102 IPs = samba.interface_ips(lp, all_interfaces)
104 nsupdate_cmd = lp.get('nsupdate command')
106 dns_zone_scavenging = lp.get("dns zone scavenging")
109 print("No IP interfaces - skipping DNS updates")
112 if opts.rpc_server_ip:
113 rpc_server_ip = opts.rpc_server_ip
115 rpc_server_ip = IPs[0]
120 if i.find(':') != -1:
125 smb_conf = sambaopts.get_loadparm_path()
128 print("IPs: %s" % IPs)
130 def get_possible_rw_dns_server(creds, domain):
131 """Get a list of possible read-write DNS servers, starting with
132 the SOA. The SOA is the correct answer, but old Samba domains
133 (4.6 and prior) do not maintain this value, so add NS servers
137 ans_soa = check_one_dns_name(domain, 'SOA')
139 # Actually there is only one
140 for i in range(len(ans_soa)):
141 hostnames.append(str(ans_soa[i].mname).rstrip('.'))
143 # This is not strictly legit, but old Samba domains may have an
144 # unmaintained SOA record, so go for any NS that we can get a
146 ans_ns = check_one_dns_name(domain, 'NS')
148 # Actually there is only one
149 for i in range(len(ans_ns)):
150 hostnames.append(str(ans_ns[i].target).rstrip('.'))
154 def get_krb5_rw_dns_server(creds, domain):
155 """Get a list of read-write DNS servers that we can obtain a ticket
156 for, starting with the SOA. The SOA is the correct answer, but
157 old Samba domains (4.6 and prior) do not maintain this value,
158 so continue with the NS servers as well until we get one that
159 the KDC will issue a ticket to.
162 rw_dns_servers = get_possible_rw_dns_server(creds, domain)
163 # Actually there is only one
164 for i in range(len(rw_dns_servers)):
165 target_hostname = str(rw_dns_servers[i])
167 settings["lp_ctx"] = lp
168 settings["target_hostname"] = target_hostname
170 gensec_client = gensec.Security.start_client(settings)
171 gensec_client.set_credentials(creds)
172 gensec_client.set_target_service("DNS")
173 gensec_client.set_target_hostname(target_hostname)
174 gensec_client.want_feature(gensec.FEATURE_SEAL)
175 gensec_client.start_mech_by_sasl_name("GSSAPI")
176 server_to_client = ""
178 (client_finished, client_to_server) = gensec_client.update(server_to_client)
180 print("Successfully obtained Kerberos ticket to DNS/%s as %s" \
181 % (target_hostname, creds.get_username()))
182 return target_hostname
184 # Only raise an exception if they all failed
185 if i != len(rw_dns_servers) - 1:
189 def get_credentials(lp):
190 """# get credentials if we haven't got them already."""
191 from samba import credentials
193 creds = credentials.Credentials()
195 creds.set_machine_account(lp)
196 creds.set_krb_forwardable(credentials.NO_KRB_FORWARDABLE)
197 (tmp_fd, ccachename) = tempfile.mkstemp()
199 if opts.use_file is not None:
202 creds.get_named_ccache(lp, ccachename)
204 # Now confirm we can get a ticket to the DNS server
205 get_krb5_rw_dns_server(creds, sub_vars['DNSDOMAIN'] + '.')
208 except RuntimeError as e:
209 os.unlink(ccachename)
213 class dnsobj(object):
214 """an object to hold a parsed DNS line"""
216 def __init__(self, string_form):
217 list = string_form.split()
219 raise Exception("Invalid DNS entry %r" % string_form)
223 self.existing_port = None
224 self.existing_weight = None
225 self.existing_cname_target = None
234 self.nameservers = []
235 if self.type == 'SRV':
237 raise Exception("Invalid DNS entry %r" % string_form)
240 elif self.type in ['A', 'AAAA']:
241 self.ip = list[2] # usually $IP, which gets replaced
242 elif self.type == 'CNAME':
244 elif self.type == 'NS':
247 raise Exception("Received unexpected DNS reply of type %s: %s" % (self.type, string_form))
251 return "%s %s %s" % (self.type, self.name, self.ip)
252 if self.type == "AAAA":
253 return "%s %s %s" % (self.type, self.name, self.ip)
254 if self.type == "SRV":
255 return "%s %s %s %s" % (self.type, self.name, self.dest, self.port)
256 if self.type == "CNAME":
257 return "%s %s %s" % (self.type, self.name, self.dest)
258 if self.type == "NS":
259 return "%s %s %s" % (self.type, self.name, self.dest)
262 def parse_dns_line(line, sub_vars):
263 """parse a DNS line from."""
264 if line.startswith("SRV _ldap._tcp.pdc._msdcs.") and not samdb.am_pdc():
265 # We keep this as compat to the dns_update_list of 4.0/4.1
267 print("Skipping PDC entry (%s) as we are not a PDC" % line)
269 subline = samba.substitute_var(line, sub_vars)
270 if subline == '' or subline[0] == "#":
272 return dnsobj(subline)
275 def hostname_match(h1, h2):
276 """see if two hostnames match."""
279 return h1.lower().rstrip('.') == h2.lower().rstrip('.')
281 def get_resolver(d=None):
282 resolv_conf = os.getenv('RESOLV_CONF')
284 resolv_conf = '/etc/resolv.conf'
285 resolver = dns.resolver.Resolver(filename=resolv_conf, configure=True)
287 if d is not None and d.nameservers != []:
288 resolver.nameservers = d.nameservers
292 def check_one_dns_name(name, name_type, d=None):
293 resolver = get_resolver(d)
294 if d is not None and len(d.nameservers) == 0:
295 d.nameservers = resolver.nameservers
297 ans = resolver.query(name, name_type)
300 def check_dns_name(d):
301 """check that a DNS entry exists."""
302 normalised_name = d.name.rstrip('.') + '.'
304 print("Looking for DNS entry %s as %s" % (d, normalised_name))
306 if opts.use_file is not None:
308 dns_file = open(opts.use_file, "r")
312 for line in dns_file:
314 if line == '' or line[0] == "#":
316 if line.lower() == str(d).lower():
321 ans = check_one_dns_name(normalised_name, d.type, d)
322 except dns.exception.Timeout:
323 raise Exception("Timeout while waiting to contact a working DNS server while looking for %s as %s" % (d, normalised_name))
324 except dns.resolver.NoNameservers:
325 raise Exception("Unable to contact a working DNS server while looking for %s as %s" % (d, normalised_name))
326 except dns.resolver.NXDOMAIN:
328 print("The DNS entry %s, queried as %s does not exist" % (d, normalised_name))
330 except dns.resolver.NoAnswer:
332 print("The DNS entry %s, queried as %s does not hold this record type" % (d, normalised_name))
334 except dns.exception.DNSException:
335 raise Exception("Failure while trying to resolve %s as %s" % (d, normalised_name))
336 if d.type in ['A', 'AAAA']:
337 # we need to be sure that our IP is there
339 if str(rdata) == str(d.ip):
341 elif d.type == 'CNAME':
342 for i in range(len(ans)):
343 if hostname_match(ans[i].target, d.dest):
346 d.existing_cname_target = str(ans[i].target)
348 for i in range(len(ans)):
349 if hostname_match(ans[i].target, d.dest):
351 elif d.type == 'SRV':
354 print("Checking %s against %s" % (rdata, d))
355 if hostname_match(rdata.target, d.dest):
356 if str(rdata.port) == str(d.port):
359 d.existing_port = str(rdata.port)
360 d.existing_weight = str(rdata.weight)
363 print("Lookup of %s succeeded, but we failed to find a matching DNS entry for %s" % (normalised_name, d))
368 def get_subst_vars(samdb):
369 """get the list of substitution vars."""
373 vars['DNSDOMAIN'] = samdb.domain_dns_name()
374 vars['DNSFOREST'] = samdb.forest_dns_name()
375 vars['HOSTNAME'] = samdb.host_dns_name()
376 vars['NTDSGUID'] = samdb.get_ntds_GUID()
377 vars['SITE'] = samdb.server_site_name()
378 res = samdb.search(base=samdb.get_default_basedn(), scope=SCOPE_BASE, attrs=["objectGUID"])
379 guid = samdb.schema_format_value("objectGUID", res[0]['objectGUID'][0])
380 vars['DOMAINGUID'] = get_string(guid)
383 vars['IF_RWDC'] = "# "
384 vars['IF_RODC'] = "# "
385 vars['IF_PDC'] = "# "
387 vars['IF_RWGC'] = "# "
388 vars['IF_ROGC'] = "# "
389 vars['IF_DNS_DOMAIN'] = "# "
390 vars['IF_RWDNS_DOMAIN'] = "# "
391 vars['IF_RODNS_DOMAIN'] = "# "
392 vars['IF_DNS_FOREST'] = "# "
393 vars['IF_RWDNS_FOREST'] = "# "
394 vars['IF_R0DNS_FOREST'] = "# "
396 am_rodc = samdb.am_rodc()
405 # check if we "are DNS server"
406 res = samdb.search(base=samdb.get_config_basedn(),
407 expression='(objectguid=%s)' % vars['NTDSGUID'],
408 attrs=["options", "msDS-hasMasterNCs"])
411 if "options" in res[0]:
412 options = int(res[0]["options"][0])
413 if (options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
420 basedn = str(samdb.get_default_basedn())
421 forestdn = str(samdb.get_root_basedn())
423 if "msDS-hasMasterNCs" in res[0]:
424 for e in res[0]["msDS-hasMasterNCs"]:
425 if str(e) == "DC=DomainDnsZones,%s" % basedn:
426 vars['IF_DNS_DOMAIN'] = ""
428 vars['IF_RODNS_DOMAIN'] = ""
430 vars['IF_RWDNS_DOMAIN'] = ""
431 if str(e) == "DC=ForestDnsZones,%s" % forestdn:
432 vars['IF_DNS_FOREST'] = ""
434 vars['IF_RODNS_FOREST'] = ""
436 vars['IF_RWDNS_FOREST'] = ""
441 def call_nsupdate(d, op="add"):
442 """call nsupdate for an entry."""
443 global ccachename, nsupdate_cmd, krb5conf
445 assert(op in ["add", "delete"])
448 print("Calling nsupdate for %s (%s)" % (d, op))
450 if opts.use_file is not None:
452 rfile = open(opts.use_file, 'r+')
455 rfile = open(opts.use_file, 'w+')
456 # Open it for reading again, in case someone else got to it first
457 rfile = open(opts.use_file, 'r+')
458 fcntl.lockf(rfile, fcntl.LOCK_EX)
459 (file_dir, file_name) = os.path.split(opts.use_file)
460 (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
461 wfile = os.fdopen(tmp_fd, 'a')
465 l = parse_dns_line(line, {})
466 if str(l).lower() == str(d).lower():
470 wfile.write(str(d)+"\n")
471 os.rename(tmpfile, opts.use_file)
472 fcntl.lockf(rfile, fcntl.LOCK_UN)
475 normalised_name = d.name.rstrip('.') + '.'
477 (tmp_fd, tmpfile) = tempfile.mkstemp()
478 f = os.fdopen(tmp_fd, 'w')
480 # Getting this line right is really important. When we are under
481 # resolv_wrapper, then we want to use RESOLV_CONF and the
482 # nameserver therein. The issue is that this parameter forces us
483 # to only ever use that server, and not some other server that the
484 # NS record may point to, even as we get a ticket to that other
487 # Therefore we must not set this in production, instead we want
488 # to find the name of a SOA for the zone and use that server.
490 if os.getenv('RESOLV_CONF') and d.nameservers != []:
491 f.write('server %s\n' % d.nameservers[0])
493 resolver = get_resolver(d)
495 # Local the zone for this name
496 zone = dns.resolver.zone_for_name(normalised_name,
499 # Now find the SOA, or if we can't get a ticket to the SOA,
500 # any server with an NS record we can get a ticket for.
502 # Thanks to the Kerberos Credentials cache this is not
503 # expensive inside the loop
504 server = get_krb5_rw_dns_server(creds, zone)
505 f.write('server %s\n' % server)
508 f.write("update %s %s %u A %s\n" % (op, normalised_name, default_ttl, d.ip))
510 f.write("update %s %s %u AAAA %s\n" % (op, normalised_name, default_ttl, d.ip))
512 if op == "add" and d.existing_port is not None:
513 f.write("update delete %s SRV 0 %s %s %s\n" % (normalised_name, d.existing_weight,
514 d.existing_port, d.dest))
515 f.write("update %s %s %u SRV 0 100 %s %s\n" % (op, normalised_name, default_ttl, d.port, d.dest))
516 if d.type == "CNAME":
517 f.write("update %s %s %u CNAME %s\n" % (op, normalised_name, default_ttl, d.dest))
519 f.write("update %s %s %u NS %s\n" % (op, normalised_name, default_ttl, d.dest))
525 # Set a bigger MTU size to work around a bug in nsupdate's doio_send()
526 os.environ["SOCKET_WRAPPER_MTU"] = "2000"
530 os.environ["KRB5CCNAME"] = ccachename
532 cmd = nsupdate_cmd[:]
536 env["KRB5_CONFIG"] = krb5conf
538 env["KRB5CCNAME"] = ccachename
539 ret = subprocess.call(cmd, shell=False, env=env)
541 if opts.fail_immediately:
543 print("Failed update with %s" % tmpfile)
545 error_count = error_count + 1
547 print("Failed nsupdate: %d" % ret)
548 except Exception as estr:
549 if opts.fail_immediately:
551 error_count = error_count + 1
553 print("Failed nsupdate: %s : %s" % (str(d), estr))
556 # Let socket_wrapper set the default MTU size
557 os.environ["SOCKET_WRAPPER_MTU"] = "0"
560 def call_samba_tool(d, op="add", zone=None):
561 """call samba-tool dns to update an entry."""
563 assert(op in ["add", "delete"])
565 if (sub_vars['DNSFOREST'] != sub_vars['DNSDOMAIN']) and \
566 sub_vars['DNSFOREST'].endswith('.' + sub_vars['DNSDOMAIN']):
567 print("Refusing to use samba-tool when forest %s is under domain %s" \
568 % (sub_vars['DNSFOREST'], sub_vars['DNSDOMAIN']))
571 print("Calling samba-tool dns for %s (%s)" % (d, op))
573 normalised_name = d.name.rstrip('.') + '.'
575 if normalised_name == (sub_vars['DNSDOMAIN'] + '.'):
577 zone = sub_vars['DNSDOMAIN']
578 elif normalised_name == (sub_vars['DNSFOREST'] + '.'):
580 zone = sub_vars['DNSFOREST']
581 elif normalised_name == ('_msdcs.' + sub_vars['DNSFOREST'] + '.'):
583 zone = '_msdcs.' + sub_vars['DNSFOREST']
585 if not normalised_name.endswith('.' + sub_vars['DNSDOMAIN'] + '.'):
586 print("Not Calling samba-tool dns for %s (%s), %s not in %s" % (d, op, normalised_name, sub_vars['DNSDOMAIN'] + '.'))
588 elif normalised_name.endswith('._msdcs.' + sub_vars['DNSFOREST'] + '.'):
589 zone = '_msdcs.' + sub_vars['DNSFOREST']
591 zone = sub_vars['DNSDOMAIN']
592 len_zone = len(zone)+2
593 short_name = normalised_name[:-len_zone]
595 len_zone = len(zone)+2
596 short_name = normalised_name[:-len_zone]
599 args = [rpc_server_ip, zone, short_name, "A", d.ip]
601 args = [rpc_server_ip, zone, short_name, "AAAA", d.ip]
603 if op == "add" and d.existing_port is not None:
604 print("Not handling modify of exising SRV %s using samba-tool" % d)
607 args = [rpc_server_ip, zone, short_name, "SRV",
608 "%s %s %s %s" % (d.existing_weight,
609 d.existing_port, "0", "100"),
610 "%s %s %s %s" % (d.dest, d.port, "0", "100")]
612 args = [rpc_server_ip, zone, short_name, "SRV", "%s %s %s %s" % (d.dest, d.port, "0", "100")]
613 if d.type == "CNAME":
614 if d.existing_cname_target is None:
615 args = [rpc_server_ip, zone, short_name, "CNAME", d.dest]
618 args = [rpc_server_ip, zone, short_name, "CNAME",
619 d.existing_cname_target.rstrip('.'), d.dest]
622 args = [rpc_server_ip, zone, short_name, "NS", d.dest]
624 if smb_conf and args:
625 args += ["--configfile=" + smb_conf]
631 print("Calling samba-tool dns %s -k no -P %s" % (op, args))
632 ret = cmd._run("dns", op, "-k", "no", "-P", *args)
634 if opts.fail_immediately:
636 error_count = error_count + 1
638 print("Failed 'samba-tool dns' based update of %s" % (str(d)))
639 except Exception as estr:
640 if opts.fail_immediately:
642 error_count = error_count + 1
644 print("Failed 'samba-tool dns' based update: %s : %s" % (str(d), estr))
647 def rodc_dns_update(d, t, op):
648 '''a single DNS update via the RODC netlogon call'''
651 assert(op in ["add", "delete"])
654 print("Calling netlogon RODC update for %s" % d)
657 netlogon.NlDnsLdapAtSite : netlogon.NlDnsInfoTypeNone,
658 netlogon.NlDnsGcAtSite : netlogon.NlDnsDomainNameAlias,
659 netlogon.NlDnsDsaCname : netlogon.NlDnsDomainNameAlias,
660 netlogon.NlDnsKdcAtSite : netlogon.NlDnsInfoTypeNone,
661 netlogon.NlDnsDcAtSite : netlogon.NlDnsInfoTypeNone,
662 netlogon.NlDnsRfc1510KdcAtSite : netlogon.NlDnsInfoTypeNone,
663 netlogon.NlDnsGenericGcAtSite : netlogon.NlDnsDomainNameAlias
666 w = winbind.winbind("irpc:winbind_server", lp)
667 dns_names = netlogon.NL_DNS_NAME_INFO_ARRAY()
669 name = netlogon.NL_DNS_NAME_INFO()
671 name.dns_domain_info_type = typemap[t]
674 if d.port is not None:
675 name.port = int(d.port)
677 name.dns_register = True
679 name.dns_register = False
680 dns_names.names = [ name ]
681 site_name = sub_vars['SITE'].decode('utf-8')
686 ret_names = w.DsrUpdateReadOnlyServerDnsRecords(site_name, default_ttl, dns_names)
687 if ret_names.names[0].status != 0:
688 print("Failed to set DNS entry: %s (status %u)" % (d, ret_names.names[0].status))
689 error_count = error_count + 1
690 except RuntimeError as reason:
691 print("Error setting DNS entry of type %u: %s: %s" % (t, d, reason))
692 error_count = error_count + 1
694 if error_count != 0 and opts.fail_immediately:
698 def call_rodc_update(d, op="add"):
699 '''RODCs need to use the netlogon API for nsupdate'''
702 assert(op in ["add", "delete"])
704 # we expect failure for 3268 if we aren't a GC
705 if d.port is not None and int(d.port) == 3268:
708 # map the DNS request to a netlogon update type
710 netlogon.NlDnsLdapAtSite : '_ldap._tcp.${SITE}._sites.${DNSDOMAIN}',
711 netlogon.NlDnsGcAtSite : '_ldap._tcp.${SITE}._sites.gc._msdcs.${DNSDOMAIN}',
712 netlogon.NlDnsDsaCname : '${NTDSGUID}._msdcs.${DNSFOREST}',
713 netlogon.NlDnsKdcAtSite : '_kerberos._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
714 netlogon.NlDnsDcAtSite : '_ldap._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
715 netlogon.NlDnsRfc1510KdcAtSite : '_kerberos._tcp.${SITE}._sites.${DNSDOMAIN}',
716 netlogon.NlDnsGenericGcAtSite : '_gc._tcp.${SITE}._sites.${DNSFOREST}'
720 subname = samba.substitute_var(map[t], sub_vars)
721 if subname.lower() == d.name.lower():
722 # found a match - do the update
723 rodc_dns_update(d, t, op)
726 print("Unable to map to netlogon DNS update: %s" % d)
729 # get the list of DNS entries we should have
731 dns_update_list = opts.update_list
733 dns_update_list = lp.private_path('dns_update_list')
735 if opts.update_cache:
736 dns_update_cache = opts.update_cache
738 dns_update_cache = lp.private_path('dns_update_cache')
741 # only change the krb5.conf if we are not in selftest
742 if 'SOCKET_WRAPPER_DIR' not in os.environ:
743 # use our private krb5.conf to avoid problems with the wrong domain
744 # bind9 nsupdate wants the default domain set
745 krb5conf = lp.private_path('krb5.conf')
746 os.environ['KRB5_CONFIG'] = krb5conf
748 file = open(dns_update_list, "r")
753 samdb = SamDB(url=lp.samdb_url(), session_info=system_session(), lp=lp)
755 # get the substitution dictionary
756 sub_vars = get_subst_vars(samdb)
758 # build up a list of update commands to pass to nsupdate
767 rebuild_cache = False
769 cfile = open(dns_update_cache, 'r+')
772 cfile = open(dns_update_cache, 'w+')
773 # Open it for reading again, in case someone else got to it first
774 cfile = open(dns_update_cache, 'r+')
775 fcntl.lockf(cfile, fcntl.LOCK_EX)
778 if line == '' or line[0] == "#":
780 c = parse_dns_line(line, {})
783 if str(c) not in cache_set:
785 cache_set.add(str(c))
787 site_specific_rec = []
789 # read each line, and check that the DNS name exists
793 if '${SITE}' in line:
794 site_specific_rec.append(line)
796 if line == '' or line[0] == "#":
798 d = parse_dns_line(line, sub_vars)
801 if d.type == 'A' and len(IP4s) == 0:
803 if d.type == 'AAAA' and len(IP6s) == 0:
805 if str(d) not in dup_set:
809 # Perform automatic site coverage by default
812 if not am_rodc and auto_coverage:
813 site_names = kcc_utils.uncovered_sites_to_cover(samdb,
814 samdb.server_site_name())
816 # Duplicate all site specific records for the uncovered site
817 for site in site_names:
818 to_add = [samba.substitute_var(line, {'SITE': site})
819 for line in site_specific_rec]
821 for site_line in to_add:
822 d = parse_dns_line(site_line,
824 if d is not None and str(d) not in dup_set:
828 # now expand the entries, if any are A record with ip set to $IP
829 # then replace with multiple entries, one for each interface IP
835 for i in range(len(IP4s)-1):
841 for i in range(len(IP6s)-1):
846 # now check if the entries already exist on the DNS server
850 if str(c).lower() == str(d).lower():
856 print("need cache add: %s" % d)
857 if dns_zone_scavenging:
858 update_list.append(d)
860 print("scavenging requires update: %s" % d)
862 update_list.append(d)
864 print("force update: %s" % d)
865 elif not check_dns_name(d):
866 update_list.append(d)
868 print("need update: %s" % d)
873 if str(c).lower() == str(d).lower():
880 print("need cache remove: %s" % c)
881 if not opts.all_names and not check_dns_name(c):
883 delete_list.append(c)
885 print("need delete: %s" % c)
887 if len(delete_list) == 0 and len(update_list) == 0 and not rebuild_cache:
889 print("No DNS updates needed")
893 print("%d DNS updates and %d DNS deletes needed" % (len(update_list), len(delete_list)))
895 use_samba_tool = opts.use_samba_tool
896 use_nsupdate = opts.use_nsupdate
898 if len(delete_list) != 0 or len(update_list) != 0 and not opts.nocreds:
900 creds = get_credentials(lp)
901 except RuntimeError as e:
904 if sub_vars['IF_RWDNS_DOMAIN'] == "# ":
910 print("Failed to get Kerberos credentials, falling back to samba-tool: %s" % e)
911 use_samba_tool = True
914 # ask nsupdate to delete entries as needed
915 for d in delete_list:
916 if d.rpc or (not use_nsupdate and use_samba_tool):
918 print("update (samba-tool): %s" % d)
919 call_samba_tool(d, op="delete", zone=d.zone)
922 if d.name.lower() == domain.lower():
924 print("skip delete (rodc): %s" % d)
926 if not d.type in [ 'A', 'AAAA' ]:
928 print("delete (rodc): %s" % d)
929 call_rodc_update(d, op="delete")
932 print("delete (nsupdate): %s" % d)
933 call_nsupdate(d, op="delete")
936 print("delete (nsupdate): %s" % d)
937 call_nsupdate(d, op="delete")
939 # ask nsupdate to add entries as needed
940 for d in update_list:
941 if d.rpc or (not use_nsupdate and use_samba_tool):
943 print("update (samba-tool): %s" % d)
944 call_samba_tool(d, zone=d.zone)
947 if d.name.lower() == domain.lower():
949 print("skip (rodc): %s" % d)
951 if not d.type in [ 'A', 'AAAA' ]:
953 print("update (rodc): %s" % d)
957 print("update (nsupdate): %s" % d)
961 print("update(nsupdate): %s" % d)
965 print("Rebuilding cache at %s" % dns_update_cache)
966 (file_dir, file_name) = os.path.split(dns_update_cache)
967 (tmp_fd, tmpfile) = tempfile.mkstemp(dir=file_dir, prefix=file_name, suffix="XXXXXX")
968 wfile = os.fdopen(tmp_fd, 'a')
971 print("Adding %s to %s" % (str(d), file_name))
972 wfile.write(str(d)+"\n")
973 os.rename(tmpfile, dns_update_cache)
974 fcntl.lockf(cfile, fcntl.LOCK_UN)
976 # delete the ccache if we created it
977 if ccachename is not None:
978 os.unlink(ccachename)
981 print("Failed update of %u entries" % error_count)
982 sys.exit(error_count)