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 convert_dns_rdata(rdata
, serial
=1):
87 """Convert resource records in dnsRecord format
89 if rdata
.rdtype
== dns
.rdatatype
.A
:
90 rec
= ARecord(rdata
.address
, serial
=serial
)
91 elif rdata
.rdtype
== dns
.rdatatype
.AAAA
:
92 rec
= AAAARecord(rdata
.address
, serial
=serial
)
93 elif rdata
.rdtype
== dns
.rdatatype
.CNAME
:
94 rec
= CNameRecord(rdata
.target
.to_text(), serial
=serial
)
95 elif rdata
.rdtype
== dns
.rdatatype
.NS
:
96 rec
= NSRecord(rdata
.target
.to_text(), serial
=serial
)
97 elif rdata
.rdtype
== dns
.rdatatype
.SRV
:
98 rec
= SRVRecord(rdata
.target
.to_text(), int(rdata
.port
),
99 priority
=int(rdata
.priority
), weight
=int(rdata
.weight
),
101 elif rdata
.rdtype
== dns
.rdatatype
.TXT
:
102 slist
= shlex
.split(rdata
.to_text())
103 rec
= TXTRecord(slist
, serial
=serial
)
104 elif rdata
.rdtype
== dns
.rdatatype
.SOA
:
105 rec
= SOARecord(rdata
.mname
.to_text(), rdata
.rname
.to_text(),
106 serial
=int(rdata
.serial
),
107 refresh
=int(rdata
.refresh
), retry
=int(rdata
.retry
),
108 expire
=int(rdata
.expire
), minimum
=int(rdata
.minimum
))
114 def import_zone_data(samdb
, logger
, zone
, serial
, domaindn
, forestdn
,
115 dnsdomain
, dnsforest
):
116 """Insert zone data in DNS partitions
118 labels
= dnsdomain
.split('.')
120 domain_root
= dns
.name
.Name(labels
)
121 domain_prefix
= "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (dnsdomain
,
124 tmp
= "_msdcs.%s" % dnsforest
125 labels
= tmp
.split('.')
127 forest_root
= dns
.name
.Name(labels
)
128 dnsmsdcs
= "_msdcs.%s" % dnsforest
129 forest_prefix
= "DC=%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (dnsmsdcs
,
133 at_record
= zone
.get_node(domain_root
)
134 zone
.delete_node(domain_root
)
137 rdset
= at_record
.get_rdataset(dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
)
138 soa_rec
= ndr_pack(convert_dns_rdata(rdset
[0]))
139 at_record
.delete_rdataset(dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
)
142 rdset
= at_record
.get_rdataset(dns
.rdataclass
.IN
, dns
.rdatatype
.NS
)
143 ns_rec
= ndr_pack(convert_dns_rdata(rdset
[0]))
144 at_record
.delete_rdataset(dns
.rdataclass
.IN
, dns
.rdatatype
.NS
)
148 for rdset
in at_record
:
150 rec
= convert_dns_rdata(r
)
151 ip_recs
.append(ndr_pack(rec
))
153 # Add @ record for domain
154 dns_rec
= [soa_rec
, ns_rec
] + ip_recs
155 msg
= ldb
.Message(ldb
.Dn(samdb
, 'DC=@,%s' % domain_prefix
))
156 msg
["objectClass"] = ["top", "dnsNode"]
157 msg
["dnsRecord"] = ldb
.MessageElement(dns_rec
, ldb
.FLAG_MOD_ADD
,
162 logger
.error("Failed to add @ record for domain")
164 logger
.debug("Added @ record for domain")
166 # Add @ record for forest
167 dns_rec
= [soa_rec
, ns_rec
]
168 msg
= ldb
.Message(ldb
.Dn(samdb
, 'DC=@,%s' % forest_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 forest")
177 logger
.debug("Added @ record for forest")
179 # Add remaining records in domain and forest
180 for node
in zone
.nodes
:
181 name
= node
.relativize(forest_root
).to_text()
182 if name
== node
.to_text():
183 name
= node
.relativize(domain_root
).to_text()
184 dn
= "DC=%s,%s" % (name
, domain_prefix
)
185 fqdn
= "%s.%s" % (name
, dnsdomain
)
187 dn
= "DC=%s,%s" % (name
, forest_prefix
)
188 fqdn
= "%s.%s" % (name
, dnsmsdcs
)
191 for rdataset
in zone
.nodes
[node
]:
192 for rdata
in rdataset
:
193 rec
= convert_dns_rdata(rdata
, serial
)
195 logger
.warn("Unsupported record type (%s) for %s, ignoring" %
196 dns
.rdatatype
.to_text(rdata
.rdatatype
), name
)
198 dns_rec
.append(ndr_pack(rec
))
200 msg
= ldb
.Message(ldb
.Dn(samdb
, dn
))
201 msg
["objectClass"] = ["top", "dnsNode"]
202 msg
["dnsRecord"] = ldb
.MessageElement(dns_rec
, ldb
.FLAG_MOD_ADD
,
207 logger
.error("Failed to add DNS record %s" % (fqdn
))
209 logger
.debug("Added DNS record %s" % (fqdn
))
212 # dnsprovision creates application partitions for AD based DNS mainly if the existing
213 # provision was created using earlier snapshots of samba4 which did not have support
216 if __name__
== '__main__':
218 # Setup command line parser
219 parser
= optparse
.OptionParser("upgradedns [options]")
220 sambaopts
= options
.SambaOptions(parser
)
221 credopts
= options
.CredentialsOptions(parser
)
223 parser
.add_option_group(options
.VersionOptions(parser
))
224 parser
.add_option_group(sambaopts
)
225 parser
.add_option_group(credopts
)
227 parser
.add_option("--dns-backend", type="choice", metavar
="<BIND9_DLZ|SAMBA_INTERNAL>",
228 choices
=["SAMBA_INTERNAL", "BIND9_DLZ"], default
="SAMBA_INTERNAL",
229 help="The DNS server backend, default SAMBA_INTERNAL")
230 parser
.add_option("--migrate", type="choice", metavar
="<yes|no>",
231 choices
=["yes","no"], default
="yes",
232 help="Migrate existing zone data, default yes")
233 parser
.add_option("--verbose", help="Be verbose", action
="store_true")
235 opts
= parser
.parse_args()[0]
237 if opts
.dns_backend
is None:
238 opts
.dns_backend
= 'SAMBA_INTERNAL'
246 logger
= logging
.getLogger("upgradedns")
247 logger
.addHandler(logging
.StreamHandler(sys
.stdout
))
248 logger
.setLevel(logging
.INFO
)
250 logger
.setLevel(logging
.DEBUG
)
252 lp
= sambaopts
.get_loadparm()
253 lp
.load(lp
.configfile
)
254 creds
= credopts
.get_credentials(lp
)
256 logger
.info("Reading domain information")
257 paths
= get_paths(param
, smbconf
=lp
.configfile
)
258 paths
.bind_gid
= find_bind_gid()
259 ldbs
= get_ldbs(paths
, creds
, system_session(), lp
)
260 names
= find_provision_key_parameters(ldbs
.sam
, ldbs
.secrets
, ldbs
.idmap
,
261 paths
, lp
.configfile
, lp
)
263 if names
.domainlevel
< DS_DOMAIN_FUNCTION_2003
:
264 logger
.error("Cannot create AD based DNS for OS level < 2003")
267 domaindn
= names
.domaindn
268 forestdn
= names
.rootdn
270 dnsdomain
= names
.dnsdomain
.lower()
271 dnsforest
= dnsdomain
273 site
= names
.sitename
274 hostname
= names
.hostname
275 dnsname
= '%s.%s' % (hostname
, dnsdomain
)
277 domainsid
= names
.domainsid
278 domainguid
= names
.domainguid
279 ntdsguid
= names
.ntdsguid
281 # Check for DNS accounts and create them if required
283 msg
= ldbs
.sam
.search(base
=domaindn
, scope
=ldb
.SCOPE_DEFAULT
,
284 expression
='(sAMAccountName=DnsAdmins)',
286 dnsadmins_sid
= ndr_unpack(security
.dom_sid
, msg
[0]['objectSid'][0])
288 logger
.info("Adding DNS accounts")
289 add_dns_accounts(ldbs
.sam
, domaindn
)
290 dnsadmins_sid
= get_dnsadmins_sid(ldbs
.sam
, domaindn
)
292 logger
.info("DNS accounts already exist")
294 # Import dns records from zone file
295 if os
.path
.exists(paths
.dns
):
296 logger
.info("Reading records from zone file %s" % paths
.dns
)
298 zone
= dns
.zone
.from_file(paths
.dns
, relativize
=False)
299 rrset
= zone
.get_rdataset("%s." % dnsdomain
, dns
.rdatatype
.SOA
)
300 serial
= int(rrset
[0].serial
)
302 logger
.warn("Error parsing DNS data from '%s' (%s)" % (paths
.dns
, str(e
)))
303 logger
.warn("DNS records will be automatically created")
306 logger
.info("No zone file %s" % paths
.dns
)
307 logger
.warn("DNS records will be automatically created")
310 # Create DNS partitions if missing and fill DNS information
312 expression
= '(|(dnsRoot=DomainDnsZones.%s)(dnsRoot=ForestDnsZones.%s))' % \
313 (dnsdomain
, dnsforest
)
314 msg
= ldbs
.sam
.search(base
=names
.configdn
, scope
=ldb
.SCOPE_DEFAULT
,
315 expression
=expression
, attrs
=['nCName'])
316 ncname
= msg
[0]['nCName'][0]
318 logger
.info("Creating DNS partitions")
320 logger
.info("Looking up IPv4 addresses")
321 hostip
= interface_ips_v4(lp
)
323 hostip
.remove('127.0.0.1')
327 logger
.error("No IPv4 addresses found")
331 logger
.debug("IPv4 addresses: %s" % hostip
)
333 logger
.info("Looking up IPv6 addresses")
334 hostip6
= interface_ips_v6(lp
)
339 logger
.debug("IPv6 addresses: %s" % hostip6
)
341 create_dns_partitions(ldbs
.sam
, domainsid
, names
, domaindn
, forestdn
,
344 logger
.info("Populating DNS partitions")
345 fill_dns_data_partitions(ldbs
.sam
, domainsid
, site
, domaindn
, forestdn
,
346 dnsdomain
, dnsforest
, hostname
, hostip
, hostip6
,
347 domainguid
, ntdsguid
, dnsadmins_sid
,
351 logger
.info("Importing records from zone file")
352 import_zone_data(ldbs
.sam
, logger
, zone
, serial
, domaindn
, forestdn
,
353 dnsdomain
, dnsforest
)
355 logger
.info("DNS partitions already exist")
357 # Mark that we are hosting DNS partitions
359 dns_nclist
= [ 'DC=DomainDnsZones,%s' % domaindn
,
360 'DC=ForestDnsZones,%s' % forestdn
]
362 msgs
= ldbs
.sam
.search(base
=names
.serverdn
, scope
=ldb
.SCOPE_DEFAULT
,
363 expression
='(objectclass=nTDSDSa)',
364 attrs
=['hasPartialReplicaNCs',
365 'msDS-hasMasterNCs'])
369 ncs
= msg
.get("msDS-hasMasterNCs")
372 master_nclist
.append(nc
)
375 ncs
= msg
.get("hasPartialReplicaNCs")
378 partial_nclist
.append(nc
)
380 modified_master
= False
381 modified_partial
= False
382 for nc
in dns_nclist
:
383 if nc
not in master_nclist
:
384 master_nclist
.append(nc
)
385 modified_master
= True
386 if nc
in partial_nclist
:
387 partial_nclist
.remove(nc
)
388 modified_partial
= True
390 if modified_master
or modified_partial
:
391 logger
.debug("Updating msDS-hasMasterNCs and hasPartialReplicaNCs attributes")
395 m
["msDS-hasMasterNCs"] = ldb
.MessageElement(master_nclist
,
396 ldb
.FLAG_MOD_REPLACE
,
400 m
["hasPartialReplicaNCs"] = ldb
.MessageElement(partial_nclist
,
401 ldb
.FLAG_MOD_REPLACE
,
402 "hasPartialReplicaNCs")
404 m
["hasPartialReplicaNCs"] = ldb
.MessageElement(ncs
,
406 "hasPartialReplicaNCs")
411 # Special stuff for DLZ backend
412 if opts
.dns_backend
== "BIND9_DLZ":
413 # Check if dns-HOSTNAME account exists and create it if required
415 dn
= 'samAccountName=dns-%s,CN=Principals' % hostname
416 msg
= ldbs
.secrets
.search(expression
='(dn=%s)' % dn
, attrs
=['secret'])
417 dnssecret
= msg
[0]['secret'][0]
420 logger
.info("Adding dns-%s account" % hostname
)
423 msg
= ldbs
.sam
.search(base
=domaindn
, scope
=ldb
.SCOPE_DEFAULT
,
424 expression
='(sAMAccountName=dns-%s)' % (hostname
),
431 dnspass
= samba
.generate_random_password(128, 255)
432 setup_add_ldif(ldbs
.sam
, setup_path("provision_dns_add_samba.ldif"), {
433 "DNSDOMAIN": dnsdomain
,
434 "DOMAINDN": domaindn
,
435 "DNSPASS_B64": b64encode(dnspass
.encode('utf-16-le')),
436 "HOSTNAME" : hostname
,
437 "DNSNAME" : dnsname
}
440 res
= ldbs
.sam
.search(base
=domaindn
, scope
=ldb
.SCOPE_DEFAULT
,
441 expression
='(sAMAccountName=dns-%s)' % (hostname
),
442 attrs
=["msDS-KeyVersionNumber"])
443 if "msDS-KeyVersionNumber" in res
[0]:
444 dns_key_version_number
= int(res
[0]["msDS-KeyVersionNumber"][0])
446 dns_key_version_number
= None
448 secretsdb_setup_dns(ldbs
.secrets
, names
,
449 paths
.private_dir
, realm
=names
.realm
,
450 dnsdomain
=names
.dnsdomain
,
451 dns_keytab_path
=paths
.dns_keytab
, dnspass
=dnspass
,
452 key_version_number
=dns_key_version_number
)
454 logger
.info("dns-%s account already exists" % hostname
)
456 # This forces a re-creation of dns directory and all the files within
457 # It's an overkill, but it's easier to re-create a samdb copy, rather
458 # than trying to fix a broken copy.
459 create_dns_dir(logger
, paths
)
461 # Setup a copy of SAM for BIND9
462 create_samdb_copy(ldbs
.sam
, logger
, paths
, names
, domainsid
,
465 create_named_conf(paths
, names
.realm
, dnsdomain
, opts
.dns_backend
)
467 create_named_txt(paths
.namedtxt
, names
.realm
, dnsdomain
, dnsname
,
468 paths
.private_dir
, paths
.dns_keytab
)
469 logger
.info("See %s for an example configuration include file for BIND", paths
.namedconf
)
470 logger
.info("and %s for further documentation required for secure DNS "
471 "updates", paths
.namedtxt
)
472 elif opts
.dns_backend
== "SAMBA_INTERNAL":
473 # Check if dns-HOSTNAME account exists and delete it if required
475 dn_str
= 'samAccountName=dns-%s,CN=Principals' % hostname
476 msg
= ldbs
.secrets
.search(expression
='(dn=%s)' % dn_str
, attrs
=[])
483 ldbs
.secrets
.delete(dn
)
485 logger
.info("Failed to delete %s from secrets.ldb" % dn
)
488 msg
= ldbs
.sam
.search(base
=domaindn
, scope
=ldb
.SCOPE_DEFAULT
,
489 expression
='(sAMAccountName=dns-%s)' % (hostname
),
499 logger
.info("Failed to delete %s from sam.ldb" % dn
)
501 logger
.info("Finished upgrading DNS")