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 (
51 from samba
.provision
.sambadns
import (
61 create_dns_partitions
,
62 fill_dns_data_partitions
,
68 from samba
.dcerpc
import security
70 samba
.ensure_external_module("dns", "dnspython")
71 import dns
.zone
, dns
.rdatatype
73 __docformat__
= 'restructuredText'
77 """Find system group id for bind9
79 for name
in ["bind", "named"]:
81 return grp
.getgrnam(name
)[2]
87 def convert_dns_rdata(rdata
, serial
=1):
88 """Convert resource records in dnsRecord format
90 if rdata
.rdtype
== dns
.rdatatype
.A
:
91 rec
= ARecord(rdata
.address
, serial
=serial
)
92 elif rdata
.rdtype
== dns
.rdatatype
.AAAA
:
93 rec
= AAAARecord(rdata
.address
, serial
=serial
)
94 elif rdata
.rdtype
== dns
.rdatatype
.CNAME
:
95 rec
= CNameRecord(rdata
.target
.to_text(), serial
=serial
)
96 elif rdata
.rdtype
== dns
.rdatatype
.NS
:
97 rec
= NSRecord(rdata
.target
.to_text(), serial
=serial
)
98 elif rdata
.rdtype
== dns
.rdatatype
.SRV
:
99 rec
= SRVRecord(rdata
.target
.to_text(), int(rdata
.port
),
100 priority
=int(rdata
.priority
), weight
=int(rdata
.weight
),
102 elif rdata
.rdtype
== dns
.rdatatype
.TXT
:
103 slist
= shlex
.split(rdata
.to_text())
104 rec
= TXTRecord(slist
, serial
=serial
)
105 elif rdata
.rdtype
== dns
.rdatatype
.SOA
:
106 rec
= SOARecord(rdata
.mname
.to_text(), rdata
.rname
.to_text(),
107 serial
=int(rdata
.serial
),
108 refresh
=int(rdata
.refresh
), retry
=int(rdata
.retry
),
109 expire
=int(rdata
.expire
), minimum
=int(rdata
.minimum
))
115 def import_zone_data(samdb
, logger
, zone
, serial
, domaindn
, forestdn
,
116 dnsdomain
, dnsforest
):
117 """Insert zone data in DNS partitions
119 labels
= dnsdomain
.split('.')
121 domain_root
= dns
.name
.Name(labels
)
122 domain_prefix
= "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (dnsdomain
,
125 tmp
= "_msdcs.%s" % dnsforest
126 labels
= tmp
.split('.')
128 forest_root
= dns
.name
.Name(labels
)
129 dnsmsdcs
= "_msdcs.%s" % dnsforest
130 forest_prefix
= "DC=%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (dnsmsdcs
,
134 at_record
= zone
.get_node(domain_root
)
135 zone
.delete_node(domain_root
)
138 rdset
= at_record
.get_rdataset(dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
)
139 soa_rec
= ndr_pack(convert_dns_rdata(rdset
[0]))
140 at_record
.delete_rdataset(dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
)
143 rdset
= at_record
.get_rdataset(dns
.rdataclass
.IN
, dns
.rdatatype
.NS
)
144 ns_rec
= ndr_pack(convert_dns_rdata(rdset
[0]))
145 at_record
.delete_rdataset(dns
.rdataclass
.IN
, dns
.rdatatype
.NS
)
149 for rdset
in at_record
:
151 rec
= convert_dns_rdata(r
)
152 ip_recs
.append(ndr_pack(rec
))
154 # Add @ record for domain
155 dns_rec
= [soa_rec
, ns_rec
] + ip_recs
156 msg
= ldb
.Message(ldb
.Dn(samdb
, 'DC=@,%s' % domain_prefix
))
157 msg
["objectClass"] = ["top", "dnsNode"]
158 msg
["dnsRecord"] = ldb
.MessageElement(dns_rec
, ldb
.FLAG_MOD_ADD
,
163 logger
.error("Failed to add @ record for domain")
165 logger
.debug("Added @ record for domain")
167 # Add @ record for forest
168 dns_rec
= [soa_rec
, ns_rec
]
169 msg
= ldb
.Message(ldb
.Dn(samdb
, 'DC=@,%s' % forest_prefix
))
170 msg
["objectClass"] = ["top", "dnsNode"]
171 msg
["dnsRecord"] = ldb
.MessageElement(dns_rec
, ldb
.FLAG_MOD_ADD
,
176 logger
.error("Failed to add @ record for forest")
178 logger
.debug("Added @ record for forest")
180 # Add remaining records in domain and forest
181 for node
in zone
.nodes
:
182 name
= node
.relativize(forest_root
).to_text()
183 if name
== node
.to_text():
184 name
= node
.relativize(domain_root
).to_text()
185 dn
= "DC=%s,%s" % (name
, domain_prefix
)
186 fqdn
= "%s.%s" % (name
, dnsdomain
)
188 dn
= "DC=%s,%s" % (name
, forest_prefix
)
189 fqdn
= "%s.%s" % (name
, dnsmsdcs
)
192 for rdataset
in zone
.nodes
[node
]:
193 for rdata
in rdataset
:
194 rec
= convert_dns_rdata(rdata
, serial
)
196 logger
.warn("Unsupported record type (%s) for %s, ignoring" %
197 dns
.rdatatype
.to_text(rdata
.rdatatype
), name
)
199 dns_rec
.append(ndr_pack(rec
))
201 msg
= ldb
.Message(ldb
.Dn(samdb
, dn
))
202 msg
["objectClass"] = ["top", "dnsNode"]
203 msg
["dnsRecord"] = ldb
.MessageElement(dns_rec
, ldb
.FLAG_MOD_ADD
,
208 logger
.error("Failed to add DNS record %s" % (fqdn
))
210 logger
.debug("Added DNS record %s" % (fqdn
))
213 # dnsprovision creates application partitions for AD based DNS mainly if the existing
214 # provision was created using earlier snapshots of samba4 which did not have support
217 if __name__
== '__main__':
219 # Setup command line parser
220 parser
= optparse
.OptionParser("upgradedns [options]")
221 sambaopts
= options
.SambaOptions(parser
)
222 credopts
= options
.CredentialsOptions(parser
)
224 parser
.add_option_group(options
.VersionOptions(parser
))
225 parser
.add_option_group(sambaopts
)
226 parser
.add_option_group(credopts
)
228 parser
.add_option("--dns-backend", type="choice", metavar
="<BIND9_DLZ|SAMBA_INTERNAL>",
229 choices
=["SAMBA_INTERNAL", "BIND9_DLZ"], default
="SAMBA_INTERNAL",
230 help="The DNS server backend, default SAMBA_INTERNAL")
231 parser
.add_option("--migrate", type="choice", metavar
="<yes|no>",
232 choices
=["yes","no"], default
="yes",
233 help="Migrate existing zone data, default yes")
234 parser
.add_option("--verbose", help="Be verbose", action
="store_true")
236 opts
= parser
.parse_args()[0]
238 if opts
.dns_backend
is None:
239 opts
.dns_backend
= 'SAMBA_INTERNAL'
247 logger
= logging
.getLogger("upgradedns")
248 logger
.addHandler(logging
.StreamHandler(sys
.stdout
))
249 logger
.setLevel(logging
.INFO
)
251 logger
.setLevel(logging
.DEBUG
)
253 lp
= sambaopts
.get_loadparm()
254 lp
.load(lp
.configfile
)
255 creds
= credopts
.get_credentials(lp
)
257 logger
.info("Reading domain information")
258 paths
= get_paths(param
, smbconf
=lp
.configfile
)
259 paths
.bind_gid
= find_bind_gid()
260 ldbs
= get_ldbs(paths
, creds
, system_session(), lp
)
261 names
= find_provision_key_parameters(ldbs
.sam
, ldbs
.secrets
, ldbs
.idmap
,
262 paths
, lp
.configfile
, lp
)
264 if names
.domainlevel
< DS_DOMAIN_FUNCTION_2003
:
265 logger
.error("Cannot create AD based DNS for OS level < 2003")
268 domaindn
= names
.domaindn
269 forestdn
= names
.rootdn
271 dnsdomain
= names
.dnsdomain
.lower()
272 dnsforest
= dnsdomain
274 site
= names
.sitename
275 hostname
= names
.hostname
276 dnsname
= '%s.%s' % (hostname
, dnsdomain
)
278 domainsid
= names
.domainsid
279 domainguid
= names
.domainguid
280 ntdsguid
= names
.ntdsguid
282 # Check for DNS accounts and create them if required
284 msg
= ldbs
.sam
.search(base
=domaindn
, scope
=ldb
.SCOPE_DEFAULT
,
285 expression
='(sAMAccountName=DnsAdmins)',
287 dnsadmins_sid
= ndr_unpack(security
.dom_sid
, msg
[0]['objectSid'][0])
289 logger
.info("Adding DNS accounts")
290 add_dns_accounts(ldbs
.sam
, domaindn
)
291 dnsadmins_sid
= get_dnsadmins_sid(ldbs
.sam
, domaindn
)
293 logger
.info("DNS accounts already exist")
295 # Import dns records from zone file
296 if os
.path
.exists(paths
.dns
):
297 logger
.info("Reading records from zone file %s" % paths
.dns
)
299 zone
= dns
.zone
.from_file(paths
.dns
, relativize
=False)
300 rrset
= zone
.get_rdataset("%s." % dnsdomain
, dns
.rdatatype
.SOA
)
301 serial
= int(rrset
[0].serial
)
303 logger
.warn("Error parsing DNS data from '%s' (%s)" % (paths
.dns
, str(e
)))
304 logger
.warn("DNS records will be automatically created")
307 logger
.info("No zone file %s" % paths
.dns
)
308 logger
.warn("DNS records will be automatically created")
311 # Create DNS partitions if missing and fill DNS information
313 expression
= '(|(dnsRoot=DomainDnsZones.%s)(dnsRoot=ForestDnsZones.%s))' % \
314 (dnsdomain
, dnsforest
)
315 msg
= ldbs
.sam
.search(base
=names
.configdn
, scope
=ldb
.SCOPE_DEFAULT
,
316 expression
=expression
, attrs
=['nCName'])
317 ncname
= msg
[0]['nCName'][0]
319 logger
.info("Creating DNS partitions")
321 logger
.info("Looking up IPv4 addresses")
322 hostip
= interface_ips_v4(lp
)
324 hostip
.remove('127.0.0.1')
328 logger
.error("No IPv4 addresses found")
332 logger
.debug("IPv4 addresses: %s" % hostip
)
334 logger
.info("Looking up IPv6 addresses")
335 hostip6
= interface_ips_v6(lp
)
340 logger
.debug("IPv6 addresses: %s" % hostip6
)
342 create_dns_partitions(ldbs
.sam
, domainsid
, names
, domaindn
, forestdn
,
343 dnsadmins_sid
, FILL_FULL
)
345 logger
.info("Populating DNS partitions")
346 fill_dns_data_partitions(ldbs
.sam
, domainsid
, site
, domaindn
, forestdn
,
347 dnsdomain
, dnsforest
, hostname
, hostip
, hostip6
,
348 domainguid
, ntdsguid
, dnsadmins_sid
,
352 logger
.info("Importing records from zone file")
353 import_zone_data(ldbs
.sam
, logger
, zone
, serial
, domaindn
, forestdn
,
354 dnsdomain
, dnsforest
)
356 logger
.info("DNS partitions already exist")
358 # Mark that we are hosting DNS partitions
360 dns_nclist
= [ 'DC=DomainDnsZones,%s' % domaindn
,
361 'DC=ForestDnsZones,%s' % forestdn
]
363 msgs
= ldbs
.sam
.search(base
=names
.serverdn
, scope
=ldb
.SCOPE_DEFAULT
,
364 expression
='(objectclass=nTDSDSa)',
365 attrs
=['hasPartialReplicaNCs',
366 'msDS-hasMasterNCs'])
370 ncs
= msg
.get("msDS-hasMasterNCs")
373 master_nclist
.append(nc
)
376 ncs
= msg
.get("hasPartialReplicaNCs")
379 partial_nclist
.append(nc
)
381 modified_master
= False
382 modified_partial
= False
383 for nc
in dns_nclist
:
384 if nc
not in master_nclist
:
385 master_nclist
.append(nc
)
386 modified_master
= True
387 if nc
in partial_nclist
:
388 partial_nclist
.remove(nc
)
389 modified_partial
= True
391 if modified_master
or modified_partial
:
392 logger
.debug("Updating msDS-hasMasterNCs and hasPartialReplicaNCs attributes")
396 m
["msDS-hasMasterNCs"] = ldb
.MessageElement(master_nclist
,
397 ldb
.FLAG_MOD_REPLACE
,
401 m
["hasPartialReplicaNCs"] = ldb
.MessageElement(partial_nclist
,
402 ldb
.FLAG_MOD_REPLACE
,
403 "hasPartialReplicaNCs")
405 m
["hasPartialReplicaNCs"] = ldb
.MessageElement(ncs
,
407 "hasPartialReplicaNCs")
412 # Special stuff for DLZ backend
413 if opts
.dns_backend
== "BIND9_DLZ":
414 # Check if dns-HOSTNAME account exists and create it if required
416 dn
= 'samAccountName=dns-%s,CN=Principals' % hostname
417 msg
= ldbs
.secrets
.search(expression
='(dn=%s)' % dn
, attrs
=['secret'])
418 dnssecret
= msg
[0]['secret'][0]
421 logger
.info("Adding dns-%s account" % hostname
)
424 msg
= ldbs
.sam
.search(base
=domaindn
, scope
=ldb
.SCOPE_DEFAULT
,
425 expression
='(sAMAccountName=dns-%s)' % (hostname
),
432 dnspass
= samba
.generate_random_password(128, 255)
433 setup_add_ldif(ldbs
.sam
, setup_path("provision_dns_add_samba.ldif"), {
434 "DNSDOMAIN": dnsdomain
,
435 "DOMAINDN": domaindn
,
436 "DNSPASS_B64": b64encode(dnspass
.encode('utf-16-le')),
437 "HOSTNAME" : hostname
,
438 "DNSNAME" : dnsname
}
441 res
= ldbs
.sam
.search(base
=domaindn
, scope
=ldb
.SCOPE_DEFAULT
,
442 expression
='(sAMAccountName=dns-%s)' % (hostname
),
443 attrs
=["msDS-KeyVersionNumber"])
444 if "msDS-KeyVersionNumber" in res
[0]:
445 dns_key_version_number
= int(res
[0]["msDS-KeyVersionNumber"][0])
447 dns_key_version_number
= None
449 secretsdb_setup_dns(ldbs
.secrets
, names
,
450 paths
.private_dir
, realm
=names
.realm
,
451 dnsdomain
=names
.dnsdomain
,
452 dns_keytab_path
=paths
.dns_keytab
, dnspass
=dnspass
,
453 key_version_number
=dns_key_version_number
)
455 logger
.info("dns-%s account already exists" % hostname
)
457 # This forces a re-creation of dns directory and all the files within
458 # It's an overkill, but it's easier to re-create a samdb copy, rather
459 # than trying to fix a broken copy.
460 create_dns_dir(logger
, paths
)
462 # Setup a copy of SAM for BIND9
463 create_samdb_copy(ldbs
.sam
, logger
, paths
, names
, domainsid
,
466 create_named_conf(paths
, names
.realm
, dnsdomain
, opts
.dns_backend
, logger
)
468 create_named_txt(paths
.namedtxt
, names
.realm
, dnsdomain
, dnsname
,
469 paths
.private_dir
, paths
.dns_keytab
)
470 logger
.info("See %s for an example configuration include file for BIND", paths
.namedconf
)
471 logger
.info("and %s for further documentation required for secure DNS "
472 "updates", paths
.namedtxt
)
473 elif opts
.dns_backend
== "SAMBA_INTERNAL":
474 # Check if dns-HOSTNAME account exists and delete it if required
476 dn_str
= 'samAccountName=dns-%s,CN=Principals' % hostname
477 msg
= ldbs
.secrets
.search(expression
='(dn=%s)' % dn_str
, attrs
=[])
484 ldbs
.secrets
.delete(dn
)
486 logger
.info("Failed to delete %s from secrets.ldb" % dn
)
489 msg
= ldbs
.sam
.search(base
=domaindn
, scope
=ldb
.SCOPE_DEFAULT
,
490 expression
='(sAMAccountName=dns-%s)' % (hostname
),
500 logger
.info("Failed to delete %s from sam.ldb" % dn
)
502 logger
.info("Finished upgrading DNS")
504 services
= lp
.get("server services")
505 for service
in services
:
507 if opts
.dns_backend
.startswith("BIND"):
508 logger
.info("You have switched to using %s as your dns backend,"
509 " but still have the internal dns starting. Please"
510 " make sure you add '-dns' to your server services"
511 " line in your smb.conf." % opts
.dns_backend
)
514 if opts
.dns_backend
== "SAMBA_INTERNAL":
515 logger
.info("You have switched to using %s as your dns backend,"
516 " but you still have samba starting looking for a"
517 " BIND backend. Please remove the -dns from your"
518 " server services line." % opts
.dns_backend
)