3 # Unix SMB/CIFS implementation.
4 # Copyright (C) Amitay Isaacs <amitay@gmail.com> 2012
6 # Upgrade DNS provision from BIND9_FLATFILE to BIND9_DLZ or SAMBA_INTERNAL
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/>.
27 from base64 import b64encode
30 sys.path.insert(0, "bin/python")
34 from samba import param
35 from samba.auth import system_session
36 from samba.ndr import (
39 import samba.getopt as options
40 from samba.upgradehelpers import (
43 from samba.dsdb import DS_DOMAIN_FUNCTION_2003
44 from samba.provision import (
45 find_provision_key_parameters,
48 from samba.provision.common import (
52 from samba.provision.sambadns import (
62 create_dns_partitions,
63 fill_dns_data_partitions,
69 from samba.dcerpc import security
71 samba.ensure_third_party_module("dns", "dnspython")
72 import dns.zone, dns.rdatatype
74 __docformat__ = 'restructuredText'
78 """Find system group id for bind9
80 for name in ["bind", "named"]:
82 return grp.getgrnam(name)[2]
88 def convert_dns_rdata(rdata, serial=1):
89 """Convert resource records in dnsRecord format
91 if rdata.rdtype == dns.rdatatype.A:
92 rec = ARecord(rdata.address, serial=serial)
93 elif rdata.rdtype == dns.rdatatype.AAAA:
94 rec = AAAARecord(rdata.address, serial=serial)
95 elif rdata.rdtype == dns.rdatatype.CNAME:
96 rec = CNameRecord(rdata.target.to_text(), serial=serial)
97 elif rdata.rdtype == dns.rdatatype.NS:
98 rec = NSRecord(rdata.target.to_text(), serial=serial)
99 elif rdata.rdtype == dns.rdatatype.SRV:
100 rec = SRVRecord(rdata.target.to_text(), int(rdata.port),
101 priority=int(rdata.priority), weight=int(rdata.weight),
103 elif rdata.rdtype == dns.rdatatype.TXT:
104 slist = shlex.split(rdata.to_text())
105 rec = TXTRecord(slist, serial=serial)
106 elif rdata.rdtype == dns.rdatatype.SOA:
107 rec = SOARecord(rdata.mname.to_text(), rdata.rname.to_text(),
108 serial=int(rdata.serial),
109 refresh=int(rdata.refresh), retry=int(rdata.retry),
110 expire=int(rdata.expire), minimum=int(rdata.minimum))
116 def import_zone_data(samdb, logger, zone, serial, domaindn, forestdn,
117 dnsdomain, dnsforest):
118 """Insert zone data in DNS partitions
120 labels = dnsdomain.split('.')
122 domain_root = dns.name.Name(labels)
123 domain_prefix = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (dnsdomain,
126 tmp = "_msdcs.%s" % dnsforest
127 labels = tmp.split('.')
129 forest_root = dns.name.Name(labels)
130 dnsmsdcs = "_msdcs.%s" % dnsforest
131 forest_prefix = "DC=%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (dnsmsdcs,
135 at_record = zone.get_node(domain_root)
136 zone.delete_node(domain_root)
139 rdset = at_record.get_rdataset(dns.rdataclass.IN, dns.rdatatype.SOA)
140 soa_rec = ndr_pack(convert_dns_rdata(rdset[0]))
141 at_record.delete_rdataset(dns.rdataclass.IN, dns.rdatatype.SOA)
144 rdset = at_record.get_rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
145 ns_rec = ndr_pack(convert_dns_rdata(rdset[0]))
146 at_record.delete_rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
150 for rdset in at_record:
152 rec = convert_dns_rdata(r)
153 ip_recs.append(ndr_pack(rec))
155 # Add @ record for domain
156 dns_rec = [soa_rec, ns_rec] + ip_recs
157 msg = ldb.Message(ldb.Dn(samdb, 'DC=@,%s' % domain_prefix))
158 msg["objectClass"] = ["top", "dnsNode"]
159 msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
164 logger.error("Failed to add @ record for domain")
166 logger.debug("Added @ record for domain")
168 # Add @ record for forest
169 dns_rec = [soa_rec, ns_rec]
170 msg = ldb.Message(ldb.Dn(samdb, 'DC=@,%s' % forest_prefix))
171 msg["objectClass"] = ["top", "dnsNode"]
172 msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
177 logger.error("Failed to add @ record for forest")
179 logger.debug("Added @ record for forest")
181 # Add remaining records in domain and forest
182 for node in zone.nodes:
183 name = node.relativize(forest_root).to_text()
184 if name == node.to_text():
185 name = node.relativize(domain_root).to_text()
186 dn = "DC=%s,%s" % (name, domain_prefix)
187 fqdn = "%s.%s" % (name, dnsdomain)
189 dn = "DC=%s,%s" % (name, forest_prefix)
190 fqdn = "%s.%s" % (name, dnsmsdcs)
193 for rdataset in zone.nodes[node]:
194 for rdata in rdataset:
195 rec = convert_dns_rdata(rdata, serial)
197 logger.warn("Unsupported record type (%s) for %s, ignoring" %
198 dns.rdatatype.to_text(rdata.rdatatype), name)
200 dns_rec.append(ndr_pack(rec))
202 msg = ldb.Message(ldb.Dn(samdb, dn))
203 msg["objectClass"] = ["top", "dnsNode"]
204 msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
209 logger.error("Failed to add DNS record %s" % (fqdn))
211 logger.debug("Added DNS record %s" % (fqdn))
213 def cleanup_remove_file(file_path):
217 if e.errno not in [errno.EEXIST, errno.ENOENT]:
220 logger.debug("Could not remove %s: %s" % (file_path, e.strerror))
222 def cleanup_remove_dir(dir_path):
224 for root, dirs, files in os.walk(dir_path, topdown=False):
226 os.remove(os.path.join(root, name))
228 os.rmdir(os.path.join(root, name))
231 if e.errno not in [errno.EEXIST, errno.ENOENT]:
234 logger.debug("Could not delete dir %s: %s" % (dir_path, e.strerror))
236 def cleanup_obsolete_dns_files(paths):
237 cleanup_remove_file(os.path.join(paths.private_dir, "named.conf"))
238 cleanup_remove_file(os.path.join(paths.private_dir, "named.conf.update"))
239 cleanup_remove_file(os.path.join(paths.private_dir, "named.txt"))
241 cleanup_remove_dir(os.path.join(paths.private_dir, "dns"))
244 # dnsprovision creates application partitions for AD based DNS mainly if the existing
245 # provision was created using earlier snapshots of samba4 which did not have support
248 if __name__ == '__main__':
250 # Setup command line parser
251 parser = optparse.OptionParser("upgradedns [options]")
252 sambaopts = options.SambaOptions(parser)
253 credopts = options.CredentialsOptions(parser)
255 parser.add_option_group(options.VersionOptions(parser))
256 parser.add_option_group(sambaopts)
257 parser.add_option_group(credopts)
259 parser.add_option("--dns-backend", type="choice", metavar="<BIND9_DLZ|SAMBA_INTERNAL>",
260 choices=["SAMBA_INTERNAL", "BIND9_DLZ"], default="SAMBA_INTERNAL",
261 help="The DNS server backend, default SAMBA_INTERNAL")
262 parser.add_option("--migrate", type="choice", metavar="<yes|no>",
263 choices=["yes","no"], default="yes",
264 help="Migrate existing zone data, default yes")
265 parser.add_option("--verbose", help="Be verbose", action="store_true")
267 opts = parser.parse_args()[0]
269 if opts.dns_backend is None:
270 opts.dns_backend = 'SAMBA_INTERNAL'
278 logger = logging.getLogger("upgradedns")
279 logger.addHandler(logging.StreamHandler(sys.stdout))
280 logger.setLevel(logging.INFO)
282 logger.setLevel(logging.DEBUG)
284 lp = sambaopts.get_loadparm()
285 lp.load(lp.configfile)
286 creds = credopts.get_credentials(lp)
288 logger.info("Reading domain information")
289 paths = get_paths(param, smbconf=lp.configfile)
290 paths.bind_gid = find_bind_gid()
291 ldbs = get_ldbs(paths, creds, system_session(), lp)
292 names = find_provision_key_parameters(ldbs.sam, ldbs.secrets, ldbs.idmap,
293 paths, lp.configfile, lp)
295 if names.domainlevel < DS_DOMAIN_FUNCTION_2003:
296 logger.error("Cannot create AD based DNS for OS level < 2003")
299 domaindn = names.domaindn
300 forestdn = names.rootdn
302 dnsdomain = names.dnsdomain.lower()
303 dnsforest = dnsdomain
305 site = names.sitename
306 hostname = names.hostname
307 dnsname = '%s.%s' % (hostname, dnsdomain)
309 domainsid = names.domainsid
310 domainguid = names.domainguid
311 ntdsguid = names.ntdsguid
313 # Check for DNS accounts and create them if required
315 msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
316 expression='(sAMAccountName=DnsAdmins)',
318 dnsadmins_sid = ndr_unpack(security.dom_sid, msg[0]['objectSid'][0])
320 logger.info("Adding DNS accounts")
321 add_dns_accounts(ldbs.sam, domaindn)
322 dnsadmins_sid = get_dnsadmins_sid(ldbs.sam, domaindn)
324 logger.info("DNS accounts already exist")
326 # Import dns records from zone file
327 if os.path.exists(paths.dns):
328 logger.info("Reading records from zone file %s" % paths.dns)
330 zone = dns.zone.from_file(paths.dns, relativize=False)
331 rrset = zone.get_rdataset("%s." % dnsdomain, dns.rdatatype.SOA)
332 serial = int(rrset[0].serial)
333 except Exception as e:
334 logger.warn("Error parsing DNS data from '%s' (%s)" % (paths.dns, str(e)))
335 logger.warn("DNS records will be automatically created")
338 logger.info("No zone file %s" % paths.dns)
339 logger.warn("DNS records will be automatically created")
342 # Create DNS partitions if missing and fill DNS information
344 expression = '(|(dnsRoot=DomainDnsZones.%s)(dnsRoot=ForestDnsZones.%s))' % \
345 (dnsdomain, dnsforest)
346 msg = ldbs.sam.search(base=names.configdn, scope=ldb.SCOPE_DEFAULT,
347 expression=expression, attrs=['nCName'])
348 ncname = msg[0]['nCName'][0]
350 logger.info("Creating DNS partitions")
352 logger.info("Looking up IPv4 addresses")
353 hostip = interface_ips_v4(lp)
355 hostip.remove('127.0.0.1')
359 logger.error("No IPv4 addresses found")
363 logger.debug("IPv4 addresses: %s" % hostip)
365 logger.info("Looking up IPv6 addresses")
366 hostip6 = interface_ips_v6(lp)
371 logger.debug("IPv6 addresses: %s" % hostip6)
373 create_dns_partitions(ldbs.sam, domainsid, names, domaindn, forestdn,
374 dnsadmins_sid, FILL_FULL)
376 logger.info("Populating DNS partitions")
377 fill_dns_data_partitions(ldbs.sam, domainsid, site, domaindn, forestdn,
378 dnsdomain, dnsforest, hostname, hostip, hostip6,
379 domainguid, ntdsguid, dnsadmins_sid,
383 logger.info("Importing records from zone file")
384 import_zone_data(ldbs.sam, logger, zone, serial, domaindn, forestdn,
385 dnsdomain, dnsforest)
387 logger.info("DNS partitions already exist")
389 # Mark that we are hosting DNS partitions
391 dns_nclist = [ 'DC=DomainDnsZones,%s' % domaindn,
392 'DC=ForestDnsZones,%s' % forestdn ]
394 msgs = ldbs.sam.search(base=names.serverdn, scope=ldb.SCOPE_DEFAULT,
395 expression='(objectclass=nTDSDSa)',
396 attrs=['hasPartialReplicaNCs',
397 'msDS-hasMasterNCs'])
401 ncs = msg.get("msDS-hasMasterNCs")
404 master_nclist.append(nc)
407 ncs = msg.get("hasPartialReplicaNCs")
410 partial_nclist.append(nc)
412 modified_master = False
413 modified_partial = False
414 for nc in dns_nclist:
415 if nc not in master_nclist:
416 master_nclist.append(nc)
417 modified_master = True
418 if nc in partial_nclist:
419 partial_nclist.remove(nc)
420 modified_partial = True
422 if modified_master or modified_partial:
423 logger.debug("Updating msDS-hasMasterNCs and hasPartialReplicaNCs attributes")
427 m["msDS-hasMasterNCs"] = ldb.MessageElement(master_nclist,
428 ldb.FLAG_MOD_REPLACE,
432 m["hasPartialReplicaNCs"] = ldb.MessageElement(partial_nclist,
433 ldb.FLAG_MOD_REPLACE,
434 "hasPartialReplicaNCs")
436 m["hasPartialReplicaNCs"] = ldb.MessageElement(ncs,
438 "hasPartialReplicaNCs")
443 # Special stuff for DLZ backend
444 if opts.dns_backend == "BIND9_DLZ":
445 config_migration = False
447 if (paths.private_dir != paths.binddns_dir and
448 os.path.isfile(os.path.join(paths.private_dir, "named.conf"))):
449 config_migration = True
451 # Check if dns-HOSTNAME account exists and create it if required
452 secrets_msgs = ldbs.secrets.search(expression='(samAccountName=dns-%s)' % hostname, attrs=['secret'])
453 msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
454 expression='(sAMAccountName=dns-%s)' % (hostname),
457 if len(secrets_msgs) == 0 or len(msg) == 0:
458 logger.info("Adding dns-%s account" % hostname)
460 if len(secrets_msgs) == 1:
461 dn = secrets_msgs[0].dn
462 ldbs.secrets.delete(dn)
468 dnspass = samba.generate_random_password(128, 255)
469 setup_add_ldif(ldbs.sam, setup_path("provision_dns_add_samba.ldif"), {
470 "DNSDOMAIN": dnsdomain,
471 "DOMAINDN": domaindn,
472 "DNSPASS_B64": b64encode(dnspass.encode('utf-16-le')).decode('utf8'),
473 "HOSTNAME" : hostname,
474 "DNSNAME" : dnsname }
477 res = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
478 expression='(sAMAccountName=dns-%s)' % (hostname),
479 attrs=["msDS-KeyVersionNumber"])
480 if "msDS-KeyVersionNumber" in res[0]:
481 dns_key_version_number = int(res[0]["msDS-KeyVersionNumber"][0])
483 dns_key_version_number = None
485 secretsdb_setup_dns(ldbs.secrets, names,
486 paths.private_dir, paths.binddns_dir, realm=names.realm,
487 dnsdomain=names.dnsdomain,
488 dns_keytab_path=paths.dns_keytab, dnspass=dnspass,
489 key_version_number=dns_key_version_number)
492 logger.info("dns-%s account already exists" % hostname)
494 private_dns_keytab_path = os.path.join(paths.private_dir, paths.dns_keytab)
495 bind_dns_keytab_path = os.path.join(paths.binddns_dir, paths.dns_keytab)
497 if os.path.isfile(private_dns_keytab_path):
498 if os.path.isfile(bind_dns_keytab_path):
500 os.unlink(bind_dns_keytab_path)
502 logger.error("Failed to remove %s: %s" %
503 (bind_dns_keytab_path, e.strerror))
505 # link the dns.keytab to the bind-dns directory
507 os.link(private_dns_keytab_path, bind_dns_keytab_path)
509 logger.error("Failed to create link %s -> %s: %s" %
510 (private_dns_keytab_path, bind_dns_keytab_path, e.strerror))
512 # chown the dns.keytab in the bind-dns directory
513 if paths.bind_gid is not None:
515 os.chmod(paths.binddns_dir, 0o770)
516 os.chown(paths.binddns_dir, -1, paths.bind_gid)
518 if not os.environ.has_key('SAMBA_SELFTEST'):
519 logger.info("Failed to chown %s to bind gid %u",
520 paths.binddns_dir, paths.bind_gid)
522 os.chmod(bind_dns_keytab_path, 0640)
523 os.chown(bind_dns_keytab_path, -1, paths.bind_gid)
525 if not os.environ.has_key('SAMBA_SELFTEST'):
526 logger.info("Failed to chown %s to bind gid %u",
527 bind_dns_keytab_path, paths.bind_gid)
530 # This forces a re-creation of dns directory and all the files within
531 # It's an overkill, but it's easier to re-create a samdb copy, rather
532 # than trying to fix a broken copy.
533 create_dns_dir(logger, paths)
535 # Setup a copy of SAM for BIND9
536 create_samdb_copy(ldbs.sam, logger, paths, names, domainsid,
539 create_named_conf(paths, names.realm, dnsdomain, opts.dns_backend, logger)
541 create_named_txt(paths.namedtxt, names.realm, dnsdomain, dnsname,
542 paths.binddns_dir, paths.dns_keytab)
544 cleanup_obsolete_dns_files(paths)
547 logger.info("ATTENTION: The BIND configuration and keytab has been moved to: %s",
549 logger.info(" Please update your BIND configuration accordingly.")
551 logger.info("See %s for an example configuration include file for BIND", paths.namedconf)
552 logger.info("and %s for further documentation required for secure DNS "
553 "updates", paths.namedtxt)
555 elif opts.dns_backend == "SAMBA_INTERNAL":
556 # Make sure to remove everything from the bind-dns directory to avoid
557 # possible security issues with the named group having write access
559 cleanup_remove_file(os.path.join(paths.binddns_dir, "dns.keytab"))
560 cleanup_remove_file(os.path.join(paths.binddns_dir, "named.conf"))
561 cleanup_remove_file(os.path.join(paths.binddns_dir, "named.conf.update"))
562 cleanup_remove_file(os.path.join(paths.binddns_dir, "named.txt"))
564 cleanup_remove_dir(os.path.dirname(paths.dns))
567 os.chmod(paths.private_dir, 0o700)
568 os.chown(paths.private_dir, -1, 0)
570 logger.warn("Failed to restore owner and permissions for %s",
573 # Check if dns-HOSTNAME account exists and delete it if required
575 dn_str = 'samAccountName=dns-%s,CN=Principals' % hostname
576 msg = ldbs.secrets.search(expression='(dn=%s)' % dn_str, attrs=[])
583 ldbs.secrets.delete(dn)
585 logger.info("Failed to delete %s from secrets.ldb" % dn)
588 msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
589 expression='(sAMAccountName=dns-%s)' % (hostname),
599 logger.info("Failed to delete %s from sam.ldb" % dn)
601 logger.info("Finished upgrading DNS")
603 services = lp.get("server services")
604 for service in services:
606 if opts.dns_backend.startswith("BIND"):
607 logger.info("You have switched to using %s as your dns backend,"
608 " but still have the internal dns starting. Please"
609 " make sure you add '-dns' to your server services"
610 " line in your smb.conf." % opts.dns_backend)
613 if opts.dns_backend == "SAMBA_INTERNAL":
614 logger.info("You have switched to using %s as your dns backend,"
615 " but you still have samba starting looking for a"
616 " BIND backend. Please remove the -dns from your"
617 " server services line." % opts.dns_backend)