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
29 sys
.path
.insert(0, "bin/python")
33 from samba
import param
34 from samba
.auth
import system_session
35 from samba
.ndr
import (
38 import samba
.getopt
as options
39 from samba
.upgradehelpers
import (
42 from samba
.dsdb
import DS_DOMAIN_FUNCTION_2003
43 from samba
.provision
import (
44 find_provision_key_parameters
,
47 from samba
.provision
.common
import (
50 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
.root_gid
= pnames
.root_gid
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 slist
= shlex
.split(rdata
.to_text())
116 rec
= TXTRecord(slist
, serial
=serial
)
117 elif rdata
.rdtype
== dns
.rdatatype
.SOA
:
118 rec
= SOARecord(rdata
.mname
.to_text(), rdata
.rname
.to_text(),
119 serial
=int(rdata
.serial
),
120 refresh
=int(rdata
.refresh
), retry
=int(rdata
.retry
),
121 expire
=int(rdata
.expire
), minimum
=int(rdata
.minimum
))
127 def import_zone_data(samdb
, logger
, zone
, serial
, domaindn
, forestdn
,
128 dnsdomain
, dnsforest
):
129 """Insert zone data in DNS partitions
131 labels
= dnsdomain
.split('.')
133 domain_root
= dns
.name
.Name(labels
)
134 domain_prefix
= "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (dnsdomain
,
137 tmp
= "_msdcs.%s" % dnsforest
138 labels
= tmp
.split('.')
140 forest_root
= dns
.name
.Name(labels
)
141 dnsmsdcs
= "_msdcs.%s" % dnsforest
142 forest_prefix
= "DC=%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (dnsmsdcs
,
146 at_record
= zone
.get_node(domain_root
)
147 zone
.delete_node(domain_root
)
150 rdset
= at_record
.get_rdataset(dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
)
151 soa_rec
= ndr_pack(convert_dns_rdata(rdset
[0]))
152 at_record
.delete_rdataset(dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
)
155 rdset
= at_record
.get_rdataset(dns
.rdataclass
.IN
, dns
.rdatatype
.NS
)
156 ns_rec
= ndr_pack(convert_dns_rdata(rdset
[0]))
157 at_record
.delete_rdataset(dns
.rdataclass
.IN
, dns
.rdatatype
.NS
)
161 for rdset
in at_record
:
163 rec
= convert_dns_rdata(r
)
164 ip_recs
.append(ndr_pack(rec
))
166 # Add @ record for domain
167 dns_rec
= [soa_rec
, ns_rec
] + ip_recs
168 msg
= ldb
.Message(ldb
.Dn(samdb
, 'DC=@,%s' % domain_prefix
))
169 msg
["objectClass"] = ["top", "dnsNode"]
170 msg
["dnsRecord"] = ldb
.MessageElement(dns_rec
, ldb
.FLAG_MOD_ADD
,
175 logger
.error("Failed to add @ record for domain")
177 logger
.debug("Added @ record for domain")
179 # Add @ record for forest
180 dns_rec
= [soa_rec
, ns_rec
]
181 msg
= ldb
.Message(ldb
.Dn(samdb
, 'DC=@,%s' % forest_prefix
))
182 msg
["objectClass"] = ["top", "dnsNode"]
183 msg
["dnsRecord"] = ldb
.MessageElement(dns_rec
, ldb
.FLAG_MOD_ADD
,
188 logger
.error("Failed to add @ record for forest")
190 logger
.debug("Added @ record for forest")
192 # Add remaining records in domain and forest
193 for node
in zone
.nodes
:
194 name
= node
.relativize(forest_root
).to_text()
195 if name
== node
.to_text():
196 name
= node
.relativize(domain_root
).to_text()
197 dn
= "DC=%s,%s" % (name
, domain_prefix
)
198 fqdn
= "%s.%s" % (name
, dnsdomain
)
200 dn
= "DC=%s,%s" % (name
, forest_prefix
)
201 fqdn
= "%s.%s" % (name
, dnsmsdcs
)
204 for rdataset
in zone
.nodes
[node
]:
205 for rdata
in rdataset
:
206 rec
= convert_dns_rdata(rdata
, serial
)
208 logger
.warn("Unsupported record type (%s) for %s, ignoring" %
209 dns
.rdatatype
.to_text(rdata
.rdatatype
), name
)
211 dns_rec
.append(ndr_pack(rec
))
213 msg
= ldb
.Message(ldb
.Dn(samdb
, dn
))
214 msg
["objectClass"] = ["top", "dnsNode"]
215 msg
["dnsRecord"] = ldb
.MessageElement(dns_rec
, ldb
.FLAG_MOD_ADD
,
220 logger
.error("Failed to add DNS record %s" % (fqdn
))
222 logger
.debug("Added DNS record %s" % (fqdn
))
225 # dnsprovision creates application partitions for AD based DNS mainly if the existing
226 # provision was created using earlier snapshots of samba4 which did not have support
229 if __name__
== '__main__':
231 # Setup command line parser
232 parser
= optparse
.OptionParser("upgradedns [options]")
233 sambaopts
= options
.SambaOptions(parser
)
234 credopts
= options
.CredentialsOptions(parser
)
236 parser
.add_option_group(options
.VersionOptions(parser
))
237 parser
.add_option_group(sambaopts
)
238 parser
.add_option_group(credopts
)
240 parser
.add_option("--dns-backend", type="choice", metavar
="<BIND9_DLZ|SAMBA_INTERNAL>",
241 choices
=["SAMBA_INTERNAL", "BIND9_DLZ"], default
="SAMBA_INTERNAL",
242 help="The DNS server backend, default SAMBA_INTERNAL")
243 parser
.add_option("--migrate", type="choice", metavar
="<yes|no>",
244 choices
=["yes","no"], default
="yes",
245 help="Migrate existing zone data, default yes")
246 parser
.add_option("--verbose", help="Be verbose", action
="store_true")
248 opts
= parser
.parse_args()[0]
250 if opts
.dns_backend
is None:
251 opts
.dns_backend
= 'SAMBA_INTERNAL'
259 logger
= logging
.getLogger("upgradedns")
260 logger
.addHandler(logging
.StreamHandler(sys
.stdout
))
261 logger
.setLevel(logging
.INFO
)
263 logger
.setLevel(logging
.DEBUG
)
265 lp
= sambaopts
.get_loadparm()
266 lp
.load(lp
.configfile
)
267 creds
= credopts
.get_credentials(lp
)
269 logger
.info("Reading domain information")
270 paths
= get_paths(param
, smbconf
=lp
.configfile
)
271 paths
.bind_gid
= find_bind_gid()
272 ldbs
= get_ldbs(paths
, creds
, system_session(), lp
)
273 pnames
= find_provision_key_parameters(ldbs
.sam
, ldbs
.secrets
, ldbs
.idmap
,
274 paths
, lp
.configfile
, lp
)
275 names
= fix_names(pnames
)
277 if names
.domainlevel
< DS_DOMAIN_FUNCTION_2003
:
278 logger
.error("Cannot create AD based DNS for OS level < 2003")
281 domaindn
= names
.domaindn
282 forestdn
= names
.rootdn
284 dnsdomain
= names
.dnsdomain
.lower()
285 dnsforest
= dnsdomain
287 site
= names
.sitename
288 hostname
= names
.hostname
289 dnsname
= '%s.%s' % (hostname
, dnsdomain
)
291 domainsid
= names
.domainsid
292 domainguid
= names
.domainguid
293 ntdsguid
= names
.ntdsguid
295 # Check for DNS accounts and create them if required
297 msg
= ldbs
.sam
.search(base
=domaindn
, scope
=ldb
.SCOPE_DEFAULT
,
298 expression
='(sAMAccountName=DnsAdmins)',
300 dnsadmins_sid
= ndr_unpack(security
.dom_sid
, msg
[0]['objectSid'][0])
302 logger
.info("Adding DNS accounts")
303 add_dns_accounts(ldbs
.sam
, domaindn
)
304 dnsadmins_sid
= get_dnsadmins_sid(ldbs
.sam
, domaindn
)
306 logger
.info("DNS accounts already exist")
308 # Import dns records from zone file
309 if os
.path
.exists(paths
.dns
):
310 logger
.info("Reading records from zone file %s" % paths
.dns
)
312 zone
= dns
.zone
.from_file(paths
.dns
, relativize
=False)
313 rrset
= zone
.get_rdataset("%s." % dnsdomain
, dns
.rdatatype
.SOA
)
314 serial
= int(rrset
[0].serial
)
316 logger
.warn("Error parsing DNS data from '%s' (%s)" % (paths
.dns
, str(e
)))
317 logger
.warn("DNS records will be automatically created")
320 logger
.info("No zone file %s" % paths
.dns
)
321 logger
.warn("DNS records will be automatically created")
324 # Create DNS partitions if missing and fill DNS information
326 expression
= '(|(dnsRoot=DomainDnsZones.%s)(dnsRoot=ForestDnsZones.%s))' % \
327 (dnsdomain
, dnsforest
)
328 msg
= ldbs
.sam
.search(base
=names
.configdn
, scope
=ldb
.SCOPE_DEFAULT
,
329 expression
=expression
, attrs
=['nCName'])
330 ncname
= msg
[0]['nCName'][0]
332 logger
.info("Creating DNS partitions")
334 logger
.info("Looking up IPv4 addresses")
335 hostip
= interface_ips_v4(lp
)
337 hostip
.remove('127.0.0.1')
341 logger
.error("No IPv4 addresses found")
345 logger
.debug("IPv4 addresses: %s" % hostip
)
347 logger
.info("Looking up IPv6 addresses")
348 hostip6
= interface_ips_v6(lp
, linklocal
=False)
353 logger
.debug("IPv6 addresses: %s" % hostip6
)
355 create_dns_partitions(ldbs
.sam
, domainsid
, names
, domaindn
, forestdn
,
358 logger
.info("Populating DNS partitions")
359 fill_dns_data_partitions(ldbs
.sam
, domainsid
, site
, domaindn
, forestdn
,
360 dnsdomain
, dnsforest
, hostname
, hostip
, hostip6
,
361 domainguid
, ntdsguid
, dnsadmins_sid
,
365 logger
.info("Importing records from zone file")
366 import_zone_data(ldbs
.sam
, logger
, zone
, serial
, domaindn
, forestdn
,
367 dnsdomain
, dnsforest
)
369 logger
.info("DNS partitions already exist")
371 # Mark that we are hosting DNS partitions
373 dns_nclist
= [ 'DC=DomainDnsZones,%s' % domaindn
,
374 'DC=ForestDnsZones,%s' % forestdn
]
376 msgs
= ldbs
.sam
.search(base
=names
.serverdn
, scope
=ldb
.SCOPE_DEFAULT
,
377 expression
='(objectclass=nTDSDSa)',
378 attrs
=['hasPartialReplicaNCs',
379 'msDS-hasMasterNCs'])
383 ncs
= msg
.get("msDS-hasMasterNCs")
386 master_nclist
.append(nc
)
389 ncs
= msg
.get("hasPartialReplicaNCs")
392 partial_nclist
.append(nc
)
394 modified_master
= False
395 modified_partial
= False
396 for nc
in dns_nclist
:
397 if nc
not in master_nclist
:
398 master_nclist
.append(nc
)
399 modified_master
= True
400 if nc
in partial_nclist
:
401 partial_nclist
.remove(nc
)
402 modified_partial
= True
404 if modified_master
or modified_partial
:
405 logger
.debug("Updating msDS-hasMasterNCs and hasPartialReplicaNCs attributes")
409 m
["msDS-hasMasterNCs"] = ldb
.MessageElement(master_nclist
,
410 ldb
.FLAG_MOD_REPLACE
,
414 m
["hasPartialReplicaNCs"] = ldb
.MessageElement(partial_nclist
,
415 ldb
.FLAG_MOD_REPLACE
,
416 "hasPartialReplicaNCs")
418 m
["hasPartialReplicaNCs"] = ldb
.MessageElement(ncs
,
420 "hasPartialReplicaNCs")
425 # Special stuff for DLZ backend
426 if opts
.dns_backend
== "BIND9_DLZ":
427 # Check if dns-HOSTNAME account exists and create it if required
429 dn
= 'samAccountName=dns-%s,CN=Principals' % hostname
430 msg
= ldbs
.secrets
.search(expression
='(dn=%s)' % dn
, attrs
=['secret'])
431 dnssecret
= msg
[0]['secret'][0]
433 logger
.info("Adding dns-%s account" % hostname
)
436 msg
= ldbs
.sam
.search(base
=domaindn
, scope
=ldb
.SCOPE_DEFAULT
,
437 expression
='(sAMAccountName=dns-%s)' % (hostname
),
438 attrs
=['clearTextPassword'])
444 dnspass
= samba
.generate_random_password(128, 255)
445 setup_add_ldif(ldbs
.sam
, setup_path("provision_dns_add_samba.ldif"), {
446 "DNSDOMAIN": dnsdomain
,
447 "DOMAINDN": domaindn
,
448 "DNSPASS_B64": b64encode(dnspass
.encode('utf-16-le')),
449 "HOSTNAME" : hostname
,
450 "DNSNAME" : dnsname
}
453 secretsdb_setup_dns(ldbs
.secrets
, names
,
454 paths
.private_dir
, realm
=names
.realm
,
455 dnsdomain
=names
.dnsdomain
,
456 dns_keytab_path
=paths
.dns_keytab
, dnspass
=dnspass
)
458 logger
.info("dns-%s account already exists" % hostname
)
460 # This forces a re-creation of dns directory and all the files within
461 # It's an overkill, but it's easier to re-create a samdb copy, rather
462 # than trying to fix a broken copy.
463 create_dns_dir(logger
, paths
)
465 # Setup a copy of SAM for BIND9
466 create_samdb_copy(ldbs
.sam
, logger
, paths
, names
, domainsid
,
469 create_named_conf(paths
, names
.realm
, dnsdomain
, opts
.dns_backend
)
471 create_named_txt(paths
.namedtxt
, names
.realm
, dnsdomain
, dnsname
,
472 paths
.private_dir
, paths
.dns_keytab
)
473 logger
.info("See %s for an example configuration include file for BIND", paths
.namedconf
)
474 logger
.info("and %s for further documentation required for secure DNS "
475 "updates", paths
.namedtxt
)
476 elif opts
.dns_backend
== "SAMBA_INTERNAL":
477 # Check if dns-HOSTNAME account exists and delete it if required
479 dn_str
= 'samAccountName=dns-%s,CN=Principals' % hostname
480 msg
= ldbs
.secrets
.search(expression
='(dn=%s)' % dn_str
, attrs
=['secret'])
487 ldbs
.secrets
.delete(dn
)
489 logger
.info("Failed to delete %s from secrets.ldb" % dn
)
492 msg
= ldbs
.sam
.search(base
=domaindn
, scope
=ldb
.SCOPE_DEFAULT
,
493 expression
='(sAMAccountName=dns-%s)' % (hostname
),
494 attrs
=['clearTextPassword'])
503 logger
.info("Failed to delete %s from sam.ldb" % dn
)
505 logger
.info("Finished upgrading DNS")