Add usable stream name defines
[Samba.git] / source4 / scripting / bin / samba_upgradedns
blob4d497704936f086b57ec422cf31af37e5d666eae
1 #!/usr/bin/env python
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/>.
21 import sys
22 import os
23 import optparse
24 import logging
25 import grp
26 from base64 import b64encode
27 import shlex
29 sys.path.insert(0, "bin/python")
31 import ldb
32 import samba
33 from samba import param
34 from samba.auth import system_session
35 from samba.ndr import (
36 ndr_pack,
37 ndr_unpack )
38 import samba.getopt as options
39 from samba.upgradehelpers import (
40 get_paths,
41 get_ldbs )
42 from samba.dsdb import DS_DOMAIN_FUNCTION_2003
43 from samba.provision import (
44 find_provision_key_parameters,
45 interface_ips_v4,
46 interface_ips_v6 )
47 from samba.provision.common import (
48 setup_path,
49 setup_add_ldif,
50 FILL_FULL)
51 from samba.provision.sambadns import (
52 ARecord,
53 AAAARecord,
54 CNameRecord,
55 NSRecord,
56 SOARecord,
57 SRVRecord,
58 TXTRecord,
59 get_dnsadmins_sid,
60 add_dns_accounts,
61 create_dns_partitions,
62 fill_dns_data_partitions,
63 create_dns_dir,
64 secretsdb_setup_dns,
65 create_samdb_copy,
66 create_named_conf,
67 create_named_txt )
68 from samba.dcerpc import security
70 samba.ensure_external_module("dns", "dnspython")
71 import dns.zone, dns.rdatatype
73 __docformat__ = 'restructuredText'
76 def find_bind_gid():
77 """Find system group id for bind9
78 """
79 for name in ["bind", "named"]:
80 try:
81 return grp.getgrnam(name)[2]
82 except KeyError:
83 pass
84 return None
87 def convert_dns_rdata(rdata, serial=1):
88 """Convert resource records in dnsRecord format
89 """
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),
101 serial=serial)
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))
110 else:
111 rec = None
112 return rec
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('.')
120 labels.append('')
121 domain_root = dns.name.Name(labels)
122 domain_prefix = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (dnsdomain,
123 domaindn)
125 tmp = "_msdcs.%s" % dnsforest
126 labels = tmp.split('.')
127 labels.append('')
128 forest_root = dns.name.Name(labels)
129 dnsmsdcs = "_msdcs.%s" % dnsforest
130 forest_prefix = "DC=%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (dnsmsdcs,
131 forestdn)
133 # Extract @ record
134 at_record = zone.get_node(domain_root)
135 zone.delete_node(domain_root)
137 # SOA record
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)
142 # NS record
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)
147 # A/AAAA records
148 ip_recs = []
149 for rdset in at_record:
150 for r in rdset:
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,
159 "dnsRecord")
160 try:
161 samdb.add(msg)
162 except Exception:
163 logger.error("Failed to add @ record for domain")
164 raise
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,
172 "dnsRecord")
173 try:
174 samdb.add(msg)
175 except Exception:
176 logger.error("Failed to add @ record for forest")
177 raise
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)
187 else:
188 dn = "DC=%s,%s" % (name, forest_prefix)
189 fqdn = "%s.%s" % (name, dnsmsdcs)
191 dns_rec = []
192 for rdataset in zone.nodes[node]:
193 for rdata in rdataset:
194 rec = convert_dns_rdata(rdata, serial)
195 if not rec:
196 logger.warn("Unsupported record type (%s) for %s, ignoring" %
197 dns.rdatatype.to_text(rdata.rdatatype), name)
198 else:
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,
204 "dnsRecord")
205 try:
206 samdb.add(msg)
207 except Exception:
208 logger.error("Failed to add DNS record %s" % (fqdn))
209 raise
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
215 # for DNS partitions
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'
241 if opts.migrate:
242 autofill = False
243 else:
244 autofill = True
246 # Set up logger
247 logger = logging.getLogger("upgradedns")
248 logger.addHandler(logging.StreamHandler(sys.stdout))
249 logger.setLevel(logging.INFO)
250 if opts.verbose:
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")
266 sys.exit(1)
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
283 try:
284 msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
285 expression='(sAMAccountName=DnsAdmins)',
286 attrs=['objectSid'])
287 dnsadmins_sid = ndr_unpack(security.dom_sid, msg[0]['objectSid'][0])
288 except IndexError:
289 logger.info("Adding DNS accounts")
290 add_dns_accounts(ldbs.sam, domaindn)
291 dnsadmins_sid = get_dnsadmins_sid(ldbs.sam, domaindn)
292 else:
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)
298 try:
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)
302 except Exception, e:
303 logger.warn("Error parsing DNS data from '%s' (%s)" % (paths.dns, str(e)))
304 logger.warn("DNS records will be automatically created")
305 autofill = True
306 else:
307 logger.info("No zone file %s" % paths.dns)
308 logger.warn("DNS records will be automatically created")
309 autofill = True
311 # Create DNS partitions if missing and fill DNS information
312 try:
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]
318 except IndexError:
319 logger.info("Creating DNS partitions")
321 logger.info("Looking up IPv4 addresses")
322 hostip = interface_ips_v4(lp)
323 try:
324 hostip.remove('127.0.0.1')
325 except ValueError:
326 pass
327 if not hostip:
328 logger.error("No IPv4 addresses found")
329 sys.exit(1)
330 else:
331 hostip = hostip[0]
332 logger.debug("IPv4 addresses: %s" % hostip)
334 logger.info("Looking up IPv6 addresses")
335 hostip6 = interface_ips_v6(lp)
336 if not hostip6:
337 hostip6 = None
338 else:
339 hostip6 = hostip6[0]
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,
349 autofill=autofill)
351 if not autofill:
352 logger.info("Importing records from zone file")
353 import_zone_data(ldbs.sam, logger, zone, serial, domaindn, forestdn,
354 dnsdomain, dnsforest)
355 else:
356 logger.info("DNS partitions already exist")
358 # Mark that we are hosting DNS partitions
359 try:
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'])
367 msg = msgs[0]
369 master_nclist = []
370 ncs = msg.get("msDS-hasMasterNCs")
371 if ncs:
372 for nc in ncs:
373 master_nclist.append(nc)
375 partial_nclist = []
376 ncs = msg.get("hasPartialReplicaNCs")
377 if ncs:
378 for nc in ncs:
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")
393 m = ldb.Message()
394 m.dn = msg.dn
395 if modified_master:
396 m["msDS-hasMasterNCs"] = ldb.MessageElement(master_nclist,
397 ldb.FLAG_MOD_REPLACE,
398 "msDS-hasMasterNCs")
399 if modified_partial:
400 if partial_nclist:
401 m["hasPartialReplicaNCs"] = ldb.MessageElement(partial_nclist,
402 ldb.FLAG_MOD_REPLACE,
403 "hasPartialReplicaNCs")
404 else:
405 m["hasPartialReplicaNCs"] = ldb.MessageElement(ncs,
406 ldb.FLAG_MOD_DELETE,
407 "hasPartialReplicaNCs")
408 ldbs.sam.modify(m)
409 except Exception:
410 raise
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
415 try:
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]
419 except IndexError:
421 logger.info("Adding dns-%s account" % hostname)
423 try:
424 msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
425 expression='(sAMAccountName=dns-%s)' % (hostname),
426 attrs=[])
427 dn = msg[0].dn
428 ldbs.sam.delete(dn)
429 except IndexError:
430 pass
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])
446 else:
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)
454 else:
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,
464 domainguid)
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
475 try:
476 dn_str = 'samAccountName=dns-%s,CN=Principals' % hostname
477 msg = ldbs.secrets.search(expression='(dn=%s)' % dn_str, attrs=[])
478 dn = msg[0].dn
479 except IndexError:
480 dn = None
482 if dn is not None:
483 try:
484 ldbs.secrets.delete(dn)
485 except Exception:
486 logger.info("Failed to delete %s from secrets.ldb" % dn)
488 try:
489 msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
490 expression='(sAMAccountName=dns-%s)' % (hostname),
491 attrs=[])
492 dn = msg[0].dn
493 except IndexError:
494 dn = None
496 if dn is not None:
497 try:
498 ldbs.sam.delete(dn)
499 except Exception:
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:
506 if service == "dns":
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)
512 break
513 else:
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)