samba-tool domain join subdomain: Rework sambadns.py to allow setup of DomainDNSZone...
[nivanova/samba-autobuild/.git] / source4 / scripting / bin / samba_upgradedns
1 #!/usr/bin/env python
2 #
3 # Unix SMB/CIFS implementation.
4 # Copyright (C) Amitay Isaacs <amitay@gmail.com> 2012
5 #
6 # Upgrade DNS provision from BIND9_FLATFILE to BIND9_DLZ or SAMBA_INTERNAL
7 #
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.
12 #
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.
17 #
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/>.
20
21 import sys
22 import os
23 import optparse
24 import logging
25 import grp
26 from base64 import b64encode
27 import shlex
28
29 sys.path.insert(0, "bin/python")
30
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
69
70 samba.ensure_external_module("dns", "dnspython")
71 import dns.zone, dns.rdatatype
72
73 __docformat__ = 'restructuredText'
74
75
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
85
86
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
113
114
115 def import_zone_data(samdb, logger, zone, serial, domaindn, forestdn,
116                      dnsdomain, dnsforest):
117     """Insert zone data in DNS partitions
118     """
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)
124
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)
132
133     # Extract @ record
134     at_record = zone.get_node(domain_root)
135     zone.delete_node(domain_root)
136
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)
141
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)
146
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))
153
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")
166
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")
179
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)
190
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))
200
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))
211
212
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
216
217 if __name__ == '__main__':
218
219     # Setup command line parser
220     parser = optparse.OptionParser("upgradedns [options]")
221     sambaopts = options.SambaOptions(parser)
222     credopts = options.CredentialsOptions(parser)
223
224     parser.add_option_group(options.VersionOptions(parser))
225     parser.add_option_group(sambaopts)
226     parser.add_option_group(credopts)
227
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")
235
236     opts = parser.parse_args()[0]
237
238     if opts.dns_backend is None:
239         opts.dns_backend = 'SAMBA_INTERNAL'
240
241     if opts.migrate:
242         autofill = False
243     else:
244         autofill = True
245
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)
252
253     lp = sambaopts.get_loadparm()
254     lp.load(lp.configfile)
255     creds = credopts.get_credentials(lp)
256
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)
263
264     if names.domainlevel < DS_DOMAIN_FUNCTION_2003:
265         logger.error("Cannot create AD based DNS for OS level < 2003")
266         sys.exit(1)
267
268     domaindn = names.domaindn
269     forestdn = names.rootdn
270
271     dnsdomain = names.dnsdomain.lower()
272     dnsforest = dnsdomain
273
274     site = names.sitename
275     hostname = names.hostname
276     dnsname = '%s.%s' % (hostname, dnsdomain)
277
278     domainsid = names.domainsid
279     domainguid = names.domainguid
280     ntdsguid = names.ntdsguid
281
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")
294
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
310
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")
320
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)
333
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)
341
342         create_dns_partitions(ldbs.sam, domainsid, names, domaindn, forestdn,
343                               dnsadmins_sid, FILL_FULL)
344
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)
350
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")
357
358     # Mark that we are hosting DNS partitions
359     try:
360         dns_nclist = [ 'DC=DomainDnsZones,%s' % domaindn,
361                        'DC=ForestDnsZones,%s' % forestdn ]
362
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]
368
369         master_nclist = []
370         ncs = msg.get("msDS-hasMasterNCs")
371         if ncs:
372             for nc in ncs:
373                 master_nclist.append(nc)
374
375         partial_nclist = []
376         ncs = msg.get("hasPartialReplicaNCs")
377         if ncs:
378             for nc in ncs:
379                 partial_nclist.append(nc)
380
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
390
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
411
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:
420
421             logger.info("Adding dns-%s account" % hostname)
422
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
431
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 }
439                            )
440
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
448
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)
456
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)
461
462         # Setup a copy of SAM for BIND9
463         create_samdb_copy(ldbs.sam, logger, paths, names, domainsid,
464                           domainguid)
465
466         create_named_conf(paths, names.realm, dnsdomain, opts.dns_backend)
467
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
481
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)
487
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
495
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)
501
502     logger.info("Finished upgrading DNS")