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/>.
26 from base64 import b64encode
28 sys.path.insert(0, 'bin/python')
32 from samba import param
33 from samba.auth import system_session
34 from samba.ndr import (
37 import samba.getopt as options
38 from samba.upgradehelpers import (
41 from samba.dsdb import DS_DOMAIN_FUNCTION_2003
42 from samba.provision import (
43 find_provision_key_parameters,
46 from samba.provision.common import (
49 from samba.provision.sambadns import (
60 create_dns_partitions,
61 fill_dns_data_partitions,
67 from samba.dcerpc import security
69 samba.ensure_external_module("dns", "dnspython")
70 import dns.zone, dns.rdatatype
72 __docformat__ = 'restructuredText'
76 """Find system group id for bind9
78 for name in ["bind", "named"]:
80 return grp.getgrnam(name)[2]
86 def fix_names(pnames):
87 """Convert elements to strings from MessageElement
90 names.rootdn = pnames.rootdn[0]
91 names.domaindn = pnames.domaindn[0]
92 names.configdn = pnames.configdn[0]
93 names.schemadn = pnames.schemadn[0]
94 names.wheel_gid = pnames.wheel_gid[0]
95 names.serverdn = str(pnames.serverdn)
99 def convert_dns_rdata(rdata, serial=1):
100 """Convert resource records in dnsRecord format
102 if rdata.rdtype == dns.rdatatype.A:
103 rec = ARecord(rdata.address, serial=serial)
104 elif rdata.rdtype == dns.rdatatype.AAAA:
105 rec = AAAARecord(rdata.address, serial=serial)
106 elif rdata.rdtype == dns.rdatatype.CNAME:
107 rec = CNameRecord(rdata.target.to_text(), serial=serial)
108 elif rdata.rdtype == dns.rdatatype.NS:
109 rec = NSRecord(rdata.target.to_text(), serial=serial)
110 elif rdata.rdtype == dns.rdatatype.SRV:
111 rec = SRVRecord(rdata.target.to_text(), int(rdata.port),
112 priority=int(rdata.priority), weight=int(rdata.weight),
114 elif rdata.rdtype == dns.rdatatype.TXT:
115 rec = TXTRecord(rdata.to_text(relativize=False), serial=serial)
116 elif rdata.rdtype == dns.rdatatype.SOA:
117 rec = SOARecord(rdata.mname.to_text(), rdata.rname.to_text(),
118 serial=int(rdata.serial),
119 refresh=int(rdata.refresh), retry=int(rdata.retry),
120 expire=int(rdata.expire), minimum=int(rdata.minimum))
126 def import_zone_data(samdb, logger, zone, serial, domaindn, forestdn,
127 dnsdomain, dnsforest):
128 """Insert zone data in DNS partitions
130 labels = dnsdomain.split('.')
132 domain_root = dns.name.Name(labels)
133 domain_prefix = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (dnsdomain,
136 tmp = "_msdcs.%s" % dnsforest
137 labels = tmp.split('.')
139 forest_root = dns.name.Name(labels)
140 dnsmsdcs = "_msdcs.%s" % dnsforest
141 forest_prefix = "DC=%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (dnsmsdcs,
145 at_record = zone.get_node(domain_root)
146 zone.delete_node(domain_root)
149 rdset = at_record.get_rdataset(dns.rdataclass.IN, dns.rdatatype.SOA)
150 soa_rec = ndr_pack(convert_dns_rdata(rdset[0]))
151 at_record.delete_rdataset(dns.rdataclass.IN, dns.rdatatype.SOA)
154 rdset = at_record.get_rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
155 ns_rec = ndr_pack(convert_dns_rdata(rdset[0]))
156 at_record.delete_rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
160 for rdset in at_record:
162 rec = convert_dns_rdata(r)
163 ip_recs.append(ndr_pack(rec))
165 # Add @ record for domain
166 dns_rec = [soa_rec, ns_rec] + ip_recs
167 msg = ldb.Message(ldb.Dn(samdb, 'DC=@,%s' % domain_prefix))
168 msg["objectClass"] = ["top", "dnsNode"]
169 msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
174 logger.error("Failed to add @ record for domain")
176 logger.debug("Added @ record for domain")
178 # Add @ record for forest
179 dns_rec = [soa_rec, ns_rec]
180 msg = ldb.Message(ldb.Dn(samdb, 'DC=@,%s' % forest_prefix))
181 msg["objectClass"] = ["top", "dnsNode"]
182 msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
187 logger.error("Failed to add @ record for forest")
189 logger.debug("Added @ record for forest")
191 # Add remaining records in domain and forest
192 for node in zone.nodes:
193 name = node.relativize(forest_root).to_text()
194 if name == node.to_text():
195 name = node.relativize(domain_root).to_text()
196 dn = "DC=%s,%s" % (name, domain_prefix)
197 fqdn = "%s.%s" % (name, dnsdomain)
199 dn = "DC=%s,%s" % (name, forest_prefix)
200 fqdn = "%s.%s" % (name, dnsmsdcs)
203 for rdataset in zone.nodes[node]:
204 for rdata in rdataset:
205 rec = convert_dns_rdata(rdata, serial)
207 logger.warn("Unsupported record type (%s) for %s, ignoring" %
208 dns.rdatatype.to_text(rdata.rdatatype), name)
210 dns_rec.append(ndr_pack(rec))
212 msg = ldb.Message(ldb.Dn(samdb, dn))
213 msg["objectClass"] = ["top", "dnsNode"]
214 msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
219 logger.error("Failed to add DNS record %s" % (fqdn))
221 logger.debug("Added DNS record %s" % (fqdn))
224 # dnsprovision creates application partitions for AD based DNS mainly if the existing
225 # provision was created using earlier snapshots of samba4 which did not have support
228 if __name__ == '__main__':
230 # Setup command line parser
231 parser = optparse.OptionParser("upgradedns [options]")
232 sambaopts = options.SambaOptions(parser)
233 credopts = options.CredentialsOptions(parser)
235 parser.add_option_group(options.VersionOptions(parser))
236 parser.add_option_group(sambaopts)
237 parser.add_option_group(credopts)
239 parser.add_option("--dns-backend", type="choice", metavar="<BIND9_DLZ|SAMBA_INTERNAL>",
240 choices=["SAMBA_INTERNAL", "BIND9_DLZ"], default="BIND9_DLZ",
241 help="The DNS server backend, default BIND9_DLZ")
242 parser.add_option("--migrate", type="choice", metavar="<yes|no>",
243 choices=["yes","no"], default="yes",
244 help="Migrate existing zone data, default yes")
245 parser.add_option("--verbose", help="Be verbose", action="store_true")
247 opts = parser.parse_args()[0]
249 if opts.dns_backend is None:
250 opts.dns_backend = 'DLZ_BIND9'
258 logger = logging.getLogger("upgradedns")
259 logger.addHandler(logging.StreamHandler(sys.stdout))
260 logger.setLevel(logging.INFO)
262 logger.setLevel(logging.DEBUG)
264 lp = sambaopts.get_loadparm()
265 lp.load(lp.configfile)
266 creds = credopts.get_credentials(lp)
268 logger.info("Reading domain information")
269 paths = get_paths(param, smbconf=lp.configfile)
270 paths.bind_gid = find_bind_gid()
271 ldbs = get_ldbs(paths, creds, system_session(), lp)
272 pnames = find_provision_key_parameters(ldbs.sam, ldbs.secrets, ldbs.idmap,
273 paths, lp.configfile, lp)
274 names = fix_names(pnames)
276 if names.domainlevel < DS_DOMAIN_FUNCTION_2003:
277 logger.error("Cannot create AD based DNS for OS level < 2003")
280 logger.info("Looking up IPv4 addresses")
281 hostip = interface_ips_v4(lp)
283 hostip.remove('127.0.0.1')
287 logger.error("No IPv4 addresses found")
291 logger.debug("IPv4 addresses: %s" % hostip)
293 logger.info("Looking up IPv6 addresses")
294 hostip6 = interface_ips_v6(lp, linklocal=False)
299 logger.debug("IPv6 addresses: %s" % hostip6)
301 domaindn = names.domaindn
302 forestdn = names.rootdn
304 dnsdomain = names.dnsdomain.lower()
305 dnsforest = dnsdomain
307 site = names.sitename
308 hostname = names.hostname
309 dnsname = '%s.%s' % (hostname, dnsdomain)
311 domainsid = names.domainsid
312 domainguid = names.domainguid
313 ntdsguid = names.ntdsguid
315 # Check for DNS accounts and create them if required
317 msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
318 expression='(sAMAccountName=DnsAdmins)',
320 dnsadmins_sid = ndr_unpack(security.dom_sid, msg[0]['objectSid'][0])
322 logger.info("Adding DNS accounts")
323 add_dns_accounts(ldbs.sam, domaindn)
324 dnsadmins_sid = get_dnsadmins_sid(ldbs.sam, domaindn)
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)
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 # Fill DNS information
343 logger.info("Creating DNS partitions")
344 create_dns_partitions(ldbs.sam, domainsid, names, domaindn, forestdn,
347 logger.info("Populating DNS partitions")
348 fill_dns_data_partitions(ldbs.sam, domainsid, site, domaindn, forestdn,
349 dnsdomain, dnsforest, hostname, hostip, hostip6,
350 domainguid, ntdsguid, dnsadmins_sid,
354 logger.info("Importing records from zone file")
355 import_zone_data(ldbs.sam, logger, zone, serial, domaindn, forestdn,
356 dnsdomain, dnsforest)
358 if opts.dns_backend == "BIND9_DLZ":
359 create_dns_dir(logger, paths)
361 # Check if dns-HOSTNAME account exists and create it if required
363 dn = 'samAccountName=dns-%s,CN=Principals' % hostname
364 msg = ldbs.secrets.search(expression='(dn=%s)' % dn, attrs=['secret'])
365 dnssecret = msg[0]['secret'][0]
367 logger.info("Creating DNS account for BIND9")
370 msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
371 expression='(sAMAccountName=dns-%s)' % (hostname),
372 attrs=['clearTextPassword'])
378 dnspass = samba.generate_random_password(128, 255)
379 setup_add_ldif(ldbs.sam, setup_path("provision_dns_add_samba.ldif"), {
380 "DNSDOMAIN": dnsdomain,
381 "DOMAINDN": domaindn,
382 "DNSPASS_B64": b64encode(dnspass.encode('utf-16-le')),
383 "HOSTNAME" : hostname,
384 "DNSNAME" : dnsname }
387 secretsdb_setup_dns(ldbs.secrets, names,
388 paths.private_dir, realm=names.realm,
389 dnsdomain=names.dnsdomain,
390 dns_keytab_path=paths.dns_keytab, dnspass=dnspass)
392 # Setup a copy of SAM for BIND9
393 create_samdb_copy(ldbs.sam, logger, paths, names, domainsid,
396 create_named_conf(paths, names.realm, dnsdomain, opts.dns_backend)
398 create_named_txt(paths.namedtxt, names.realm, dnsdomain, dnsname,
399 paths.private_dir, paths.dns_keytab)
400 logger.info("See %s for an example configuration include file for BIND", paths.namedconf)
401 logger.info("and %s for further documentation required for secure DNS "
402 "updates", paths.namedtxt)
404 logger.info("Finished upgrading DNS")