scripting: Fill the ProvisionNames hash with strings, not ldb.MessageElement or Dn
[Samba/gbeck.git] / source4 / scripting / bin / samba_upgradedns
blob3c30090e0fd38cd03a6f53d0cf05f4efca9344cf
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 from samba.provision.sambadns import (
51 ARecord,
52 AAAARecord,
53 CNameRecord,
54 NSRecord,
55 SOARecord,
56 SRVRecord,
57 TXTRecord,
58 get_dnsadmins_sid,
59 add_dns_accounts,
60 create_dns_partitions,
61 fill_dns_data_partitions,
62 create_dns_dir,
63 secretsdb_setup_dns,
64 create_samdb_copy,
65 create_named_conf,
66 create_named_txt )
67 from samba.dcerpc import security
69 samba.ensure_external_module("dns", "dnspython")
70 import dns.zone, dns.rdatatype
72 __docformat__ = 'restructuredText'
75 def find_bind_gid():
76 """Find system group id for bind9
77 """
78 for name in ["bind", "named"]:
79 try:
80 return grp.getgrnam(name)[2]
81 except KeyError:
82 pass
83 return None
86 def convert_dns_rdata(rdata, serial=1):
87 """Convert resource records in dnsRecord format
88 """
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),
100 serial=serial)
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))
109 else:
110 rec = None
111 return rec
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('.')
119 labels.append('')
120 domain_root = dns.name.Name(labels)
121 domain_prefix = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (dnsdomain,
122 domaindn)
124 tmp = "_msdcs.%s" % dnsforest
125 labels = tmp.split('.')
126 labels.append('')
127 forest_root = dns.name.Name(labels)
128 dnsmsdcs = "_msdcs.%s" % dnsforest
129 forest_prefix = "DC=%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (dnsmsdcs,
130 forestdn)
132 # Extract @ record
133 at_record = zone.get_node(domain_root)
134 zone.delete_node(domain_root)
136 # SOA record
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)
141 # NS record
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)
146 # A/AAAA records
147 ip_recs = []
148 for rdset in at_record:
149 for r in rdset:
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,
158 "dnsRecord")
159 try:
160 samdb.add(msg)
161 except Exception:
162 logger.error("Failed to add @ record for domain")
163 raise
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,
171 "dnsRecord")
172 try:
173 samdb.add(msg)
174 except Exception:
175 logger.error("Failed to add @ record for forest")
176 raise
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)
186 else:
187 dn = "DC=%s,%s" % (name, forest_prefix)
188 fqdn = "%s.%s" % (name, dnsmsdcs)
190 dns_rec = []
191 for rdataset in zone.nodes[node]:
192 for rdata in rdataset:
193 rec = convert_dns_rdata(rdata, serial)
194 if not rec:
195 logger.warn("Unsupported record type (%s) for %s, ignoring" %
196 dns.rdatatype.to_text(rdata.rdatatype), name)
197 else:
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,
203 "dnsRecord")
204 try:
205 samdb.add(msg)
206 except Exception:
207 logger.error("Failed to add DNS record %s" % (fqdn))
208 raise
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
214 # for DNS partitions
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'
240 if opts.migrate:
241 autofill = False
242 else:
243 autofill = True
245 # Set up logger
246 logger = logging.getLogger("upgradedns")
247 logger.addHandler(logging.StreamHandler(sys.stdout))
248 logger.setLevel(logging.INFO)
249 if opts.verbose:
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")
265 sys.exit(1)
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
282 try:
283 msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
284 expression='(sAMAccountName=DnsAdmins)',
285 attrs=['objectSid'])
286 dnsadmins_sid = ndr_unpack(security.dom_sid, msg[0]['objectSid'][0])
287 except Exception, e:
288 logger.info("Adding DNS accounts")
289 add_dns_accounts(ldbs.sam, domaindn)
290 dnsadmins_sid = get_dnsadmins_sid(ldbs.sam, domaindn)
291 else:
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)
297 try:
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)
301 except Exception, e:
302 logger.warn("Error parsing DNS data from '%s' (%s)" % (paths.dns, str(e)))
303 logger.warn("DNS records will be automatically created")
304 autofill = True
305 else:
306 logger.info("No zone file %s" % paths.dns)
307 logger.warn("DNS records will be automatically created")
308 autofill = True
310 # Create DNS partitions if missing and fill DNS information
311 try:
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]
317 except Exception, e:
318 logger.info("Creating DNS partitions")
320 logger.info("Looking up IPv4 addresses")
321 hostip = interface_ips_v4(lp)
322 try:
323 hostip.remove('127.0.0.1')
324 except ValueError:
325 pass
326 if not hostip:
327 logger.error("No IPv4 addresses found")
328 sys.exit(1)
329 else:
330 hostip = hostip[0]
331 logger.debug("IPv4 addresses: %s" % hostip)
333 logger.info("Looking up IPv6 addresses")
334 hostip6 = interface_ips_v6(lp, linklocal=False)
335 if not hostip6:
336 hostip6 = None
337 else:
338 hostip6 = hostip6[0]
339 logger.debug("IPv6 addresses: %s" % hostip6)
341 create_dns_partitions(ldbs.sam, domainsid, names, domaindn, forestdn,
342 dnsadmins_sid)
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,
348 autofill=autofill)
350 if not autofill:
351 logger.info("Importing records from zone file")
352 import_zone_data(ldbs.sam, logger, zone, serial, domaindn, forestdn,
353 dnsdomain, dnsforest)
354 else:
355 logger.info("DNS partitions already exist")
357 # Mark that we are hosting DNS partitions
358 try:
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'])
366 msg = msgs[0]
368 master_nclist = []
369 ncs = msg.get("msDS-hasMasterNCs")
370 if ncs:
371 for nc in ncs:
372 master_nclist.append(nc)
374 partial_nclist = []
375 ncs = msg.get("hasPartialReplicaNCs")
376 if ncs:
377 for nc in ncs:
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")
392 m = ldb.Message()
393 m.dn = msg.dn
394 if modified_master:
395 m["msDS-hasMasterNCs"] = ldb.MessageElement(master_nclist,
396 ldb.FLAG_MOD_REPLACE,
397 "msDS-hasMasterNCs")
398 if modified_partial:
399 if partial_nclist:
400 m["hasPartialReplicaNCs"] = ldb.MessageElement(partial_nclist,
401 ldb.FLAG_MOD_REPLACE,
402 "hasPartialReplicaNCs")
403 else:
404 m["hasPartialReplicaNCs"] = ldb.MessageElement(ncs,
405 ldb.FLAG_MOD_DELETE,
406 "hasPartialReplicaNCs")
407 ldbs.sam.modify(m)
408 except Exception:
409 raise
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
414 try:
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]
418 except Exception:
419 logger.info("Adding dns-%s account" % hostname)
421 try:
422 msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
423 expression='(sAMAccountName=dns-%s)' % (hostname),
424 attrs=['clearTextPassword'])
425 dn = msg[0].dn
426 ldbs.sam.delete(dn)
427 except Exception:
428 pass
430 dnspass = samba.generate_random_password(128, 255)
431 setup_add_ldif(ldbs.sam, setup_path("provision_dns_add_samba.ldif"), {
432 "DNSDOMAIN": dnsdomain,
433 "DOMAINDN": domaindn,
434 "DNSPASS_B64": b64encode(dnspass.encode('utf-16-le')),
435 "HOSTNAME" : hostname,
436 "DNSNAME" : dnsname }
439 secretsdb_setup_dns(ldbs.secrets, names,
440 paths.private_dir, realm=names.realm,
441 dnsdomain=names.dnsdomain,
442 dns_keytab_path=paths.dns_keytab, dnspass=dnspass)
443 else:
444 logger.info("dns-%s account already exists" % hostname)
446 # This forces a re-creation of dns directory and all the files within
447 # It's an overkill, but it's easier to re-create a samdb copy, rather
448 # than trying to fix a broken copy.
449 create_dns_dir(logger, paths)
451 # Setup a copy of SAM for BIND9
452 create_samdb_copy(ldbs.sam, logger, paths, names, domainsid,
453 domainguid)
455 create_named_conf(paths, names.realm, dnsdomain, opts.dns_backend)
457 create_named_txt(paths.namedtxt, names.realm, dnsdomain, dnsname,
458 paths.private_dir, paths.dns_keytab)
459 logger.info("See %s for an example configuration include file for BIND", paths.namedconf)
460 logger.info("and %s for further documentation required for secure DNS "
461 "updates", paths.namedtxt)
462 elif opts.dns_backend == "SAMBA_INTERNAL":
463 # Check if dns-HOSTNAME account exists and delete it if required
464 try:
465 dn_str = 'samAccountName=dns-%s,CN=Principals' % hostname
466 msg = ldbs.secrets.search(expression='(dn=%s)' % dn_str, attrs=['secret'])
467 dn = msg[0].dn
468 except Exception:
469 dn = None
471 if dn is not None:
472 try:
473 ldbs.secrets.delete(dn)
474 except Exception:
475 logger.info("Failed to delete %s from secrets.ldb" % dn)
477 try:
478 msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
479 expression='(sAMAccountName=dns-%s)' % (hostname),
480 attrs=['clearTextPassword'])
481 dn = msg[0].dn
482 except Exception:
483 dn = None
485 if dn is not None:
486 try:
487 ldbs.sam.delete(dn)
488 except Exception:
489 logger.info("Failed to delete %s from sam.ldb" % dn)
491 logger.info("Finished upgrading DNS")