upgradedns: Rename to less generic name samba_upgradedns.
[metze/samba/wip.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
28 sys.path.insert(0, 'bin/python')
29
30 import ldb
31 import samba
32 from samba import param
33 from samba.auth import system_session
34 from samba.ndr import (
35     ndr_pack,
36     ndr_unpack )
37 import samba.getopt as options
38 from samba.upgradehelpers import (
39     get_paths,
40     get_ldbs )
41 from samba.dsdb import DS_DOMAIN_FUNCTION_2003
42 from samba.provision import (
43     find_provision_key_parameters,
44     interface_ips_v4,
45     interface_ips_v6 )
46 from samba.provision.common import (
47     setup_path,
48     setup_add_ldif )
49 from samba.provision.sambadns import (
50     ARecord,
51     AAAARecord,
52     CNameRecord,
53     NSRecord,
54     SOARecord,
55     SRVRecord,
56     TXTRecord,
57     get_dnsadmins_sid,
58     get_domainguid,
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
68
69 samba.ensure_external_module("dns", "dnspython")
70 import dns.zone, dns.rdatatype
71
72 __docformat__ = 'restructuredText'
73
74
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
84
85
86 def fix_names(pnames):
87     """Convert elements to strings from MessageElement
88     """
89     names = pnames
90     names.rootdn = pnames.rootdn[0]
91     names.domaindn = pnames.domaindn[0]
92     names.configdn = pnames.configdn[0]
93     names.schemadn = pnames.schemadn[0]
94     names.wheel_gid = pnames.wheel_gid[0]
95     names.serverdn = str(pnames.serverdn)
96     return names
97
98
99 def convert_dns_rdata(rdata, serial=1):
100     """Convert resource records in dnsRecord format
101     """
102     if rdata.rdtype == dns.rdatatype.A:
103         rec = ARecord(rdata.address, serial=serial)
104     elif rdata.rdtype == dns.rdatatype.AAAA:
105         rec = AAAARecord(rdata.address, serial=serial)
106     elif rdata.rdtype == dns.rdatatype.CNAME:
107         rec = CNameRecord(rdata.target.to_text(), serial=serial)
108     elif rdata.rdtype == dns.rdatatype.NS:
109         rec = NSRecord(rdata.target.to_text(), serial=serial)
110     elif rdata.rdtype == dns.rdatatype.SRV:
111         rec = SRVRecord(rdata.target.to_text(), int(rdata.port),
112                         priority=int(rdata.priority), weight=int(rdata.weight),
113                         serial=serial)
114     elif rdata.rdtype == dns.rdatatype.TXT:
115         rec = TXTRecord(rdata.to_text(relativize=False), serial=serial)
116     elif rdata.rdtype == dns.rdatatype.SOA:
117         rec = SOARecord(rdata.mname.to_text(), rdata.rname.to_text(),
118                         serial=int(rdata.serial),
119                         refresh=int(rdata.refresh), retry=int(rdata.retry),
120                         expire=int(rdata.expire), minimum=int(rdata.minimum))
121     else:
122         rec = None
123     return rec
124
125
126 def import_zone_data(samdb, logger, zone, serial, domaindn, forestdn,
127                      dnsdomain, dnsforest):
128     """Insert zone data in DNS partitions
129     """
130     labels = dnsdomain.split('.')
131     labels.append('')
132     domain_root = dns.name.Name(labels)
133     domain_prefix = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (dnsdomain,
134                                                                     domaindn)
135
136     tmp = "_msdcs.%s" % dnsforest
137     labels = tmp.split('.')
138     labels.append('')
139     forest_root = dns.name.Name(labels)
140     dnsmsdcs = "_msdcs.%s" % dnsforest
141     forest_prefix = "DC=%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (dnsmsdcs,
142                                                                     forestdn)
143
144     # Extract @ record
145     at_record = zone.get_node(domain_root)
146     zone.delete_node(domain_root)
147
148     # SOA record
149     rdset = at_record.get_rdataset(dns.rdataclass.IN, dns.rdatatype.SOA)
150     soa_rec = ndr_pack(convert_dns_rdata(rdset[0]))
151     at_record.delete_rdataset(dns.rdataclass.IN, dns.rdatatype.SOA)
152
153     # NS record
154     rdset = at_record.get_rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
155     ns_rec = ndr_pack(convert_dns_rdata(rdset[0]))
156     at_record.delete_rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
157
158     # A/AAAA records
159     ip_recs = []
160     for rdset in at_record:
161         for r in rdset:
162             rec = convert_dns_rdata(r)
163             ip_recs.append(ndr_pack(rec))
164
165     # Add @ record for domain
166     dns_rec = [soa_rec, ns_rec] + ip_recs
167     msg = ldb.Message(ldb.Dn(samdb, 'DC=@,%s' % domain_prefix))
168     msg["objectClass"] = ["top", "dnsNode"]
169     msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
170                                           "dnsRecord")
171     try:
172         samdb.add(msg)
173     except Exception:
174         logger.error("Failed to add @ record for domain")
175         raise
176     logger.debug("Added @ record for domain")
177
178     # Add @ record for forest
179     dns_rec = [soa_rec, ns_rec]
180     msg = ldb.Message(ldb.Dn(samdb, 'DC=@,%s' % forest_prefix))
181     msg["objectClass"] = ["top", "dnsNode"]
182     msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
183                                           "dnsRecord")
184     try:
185         samdb.add(msg)
186     except Exception:
187         logger.error("Failed to add @ record for forest")
188         raise
189     logger.debug("Added @ record for forest")
190
191     # Add remaining records in domain and forest
192     for node in zone.nodes:
193         name = node.relativize(forest_root).to_text()
194         if name == node.to_text():
195             name = node.relativize(domain_root).to_text()
196             dn = "DC=%s,%s" % (name, domain_prefix)
197             fqdn = "%s.%s" % (name, dnsdomain)
198         else:
199             dn = "DC=%s,%s" % (name, forest_prefix)
200             fqdn = "%s.%s" % (name, dnsmsdcs)
201
202         dns_rec = []
203         for rdataset in zone.nodes[node]:
204             for rdata in rdataset:
205                 rec = convert_dns_rdata(rdata, serial)
206                 if not rec:
207                     logger.warn("Unsupported record type (%s) for %s, ignoring" %
208                                 dns.rdatatype.to_text(rdata.rdatatype), name)
209                 else:
210                     dns_rec.append(ndr_pack(rec))
211
212         msg = ldb.Message(ldb.Dn(samdb, dn))
213         msg["objectClass"] = ["top", "dnsNode"]
214         msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
215                                               "dnsRecord")
216         try:
217             samdb.add(msg)
218         except Exception:
219             logger.error("Failed to add DNS record %s" % (fqdn))
220             raise
221         logger.debug("Added DNS record %s" % (fqdn))
222
223
224 # dnsprovision creates application partitions for AD based DNS mainly if the existing
225 # provision was created using earlier snapshots of samba4 which did not have support
226 # for DNS partitions
227
228 if __name__ == '__main__':
229
230     # Setup command line parser
231     parser = optparse.OptionParser("upgradedns [options]")
232     sambaopts = options.SambaOptions(parser)
233     credopts = options.CredentialsOptions(parser)
234
235     parser.add_option_group(options.VersionOptions(parser))
236     parser.add_option_group(sambaopts)
237     parser.add_option_group(credopts)
238
239     parser.add_option("--dns-backend", type="choice", metavar="<BIND9_DLZ|SAMBA_INTERNAL>",
240                       choices=["SAMBA_INTERNAL", "BIND9_DLZ"], default="BIND9_DLZ",
241                       help="The DNS server backend, default BIND9_DLZ")
242     parser.add_option("--migrate", type="choice", metavar="<yes|no>",
243                       choices=["yes","no"], default="yes",
244                       help="Migrate existing zone data, default yes")
245     parser.add_option("--verbose", help="Be verbose", action="store_true")
246
247     opts = parser.parse_args()[0]
248
249     if opts.dns_backend is None:
250         opts.dns_backend = 'DLZ_BIND9'
251
252     if opts.migrate:
253         autofill = False
254     else:
255         autofill = True
256
257     # Set up logger
258     logger = logging.getLogger("upgradedns")
259     logger.addHandler(logging.StreamHandler(sys.stdout))
260     logger.setLevel(logging.INFO)
261     if opts.verbose:
262         logger.setLevel(logging.DEBUG)
263
264     lp = sambaopts.get_loadparm()
265     lp.load(lp.configfile)
266     creds = credopts.get_credentials(lp)
267
268     logger.info("Reading domain information")
269     paths = get_paths(param, smbconf=lp.configfile)
270     paths.bind_gid = find_bind_gid()
271     ldbs = get_ldbs(paths, creds, system_session(), lp)
272     pnames = find_provision_key_parameters(ldbs.sam, ldbs.secrets, ldbs.idmap,
273                                            paths, lp.configfile, lp)
274     names = fix_names(pnames)
275
276     if names.domainlevel < DS_DOMAIN_FUNCTION_2003:
277         logger.error("Cannot create AD based DNS for OS level < 2003")
278         sys.exit(1)
279
280     logger.info("Looking up IPv4 addresses")
281     hostip = interface_ips_v4(lp)
282     try:
283         hostip.remove('127.0.0.1')
284     except ValueError:
285         pass
286     if not hostip:
287         logger.error("No IPv4 addresses found")
288         sys.exit(1)
289     else:
290         hostip = hostip[0]
291         logger.debug("IPv4 addresses: %s" % hostip)
292
293     logger.info("Looking up IPv6 addresses")
294     hostip6 = interface_ips_v6(lp, linklocal=False)
295     if not hostip6:
296         hostip6 = None
297     else:
298         hostip6 = hostip6[0]
299         logger.debug("IPv6 addresses: %s" % hostip6)
300
301     domaindn = names.domaindn
302     forestdn = names.rootdn
303
304     dnsdomain = names.dnsdomain.lower()
305     dnsforest = dnsdomain
306
307     site = names.sitename
308     hostname = names.hostname
309     dnsname = '%s.%s' % (hostname, dnsdomain)
310
311     domainsid = names.domainsid
312     domainguid = names.domainguid
313     ntdsguid = names.ntdsguid
314
315     # Check for DNS accounts and create them if required
316     try:
317         msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
318                               expression='(sAMAccountName=DnsAdmins)',
319                               attrs=['objectSid'])
320         dnsadmins_sid = ndr_unpack(security.dom_sid, msg[0]['objectSid'][0])
321     except Exception, e:
322         logger.info("Adding DNS accounts")
323         add_dns_accounts(ldbs.sam, domaindn)
324         dnsadmins_sid = get_dnsadmins_sid(ldbs.sam, domaindn)
325
326     # Import dns records from zone file
327     if os.path.exists(paths.dns):
328         logger.info("Reading records from zone file %s" % paths.dns)
329         try:
330             zone = dns.zone.from_file(paths.dns, relativize=False)
331             rrset = zone.get_rdataset("%s." % dnsdomain, dns.rdatatype.SOA)
332             serial = int(rrset[0].serial)
333         except Exception, e:
334             logger.warn("Error parsing DNS data from '%s' (%s)" % (paths.dns, str(e)))
335             logger.warn("DNS records will be automatically created")
336             autofill = True
337     else:
338         logger.info("No zone file %s" % paths.dns)
339         logger.warn("DNS records will be automatically created")
340         autofill = True
341
342     # Fill DNS information
343     logger.info("Creating DNS partitions")
344     create_dns_partitions(ldbs.sam, domainsid, names, domaindn, forestdn,
345                           dnsadmins_sid)
346
347     logger.info("Populating DNS partitions")
348     fill_dns_data_partitions(ldbs.sam, domainsid, site, domaindn, forestdn,
349                              dnsdomain, dnsforest, hostname, hostip, hostip6,
350                              domainguid, ntdsguid, dnsadmins_sid,
351                              autofill=autofill)
352
353     if not autofill:
354         logger.info("Importing records from zone file")
355         import_zone_data(ldbs.sam, logger, zone, serial, domaindn, forestdn,
356                          dnsdomain, dnsforest)
357
358     if opts.dns_backend == "BIND9_DLZ":
359         create_dns_dir(logger, paths)
360
361         # Check if dns-HOSTNAME account exists and create it if required
362         try:
363             dn = 'samAccountName=dns-%s,CN=Principals' % hostname
364             msg = ldbs.secrets.search(expression='(dn=%s)' % dn, attrs=['secret'])
365             dnssecret = msg[0]['secret'][0]
366         except Exception:
367             logger.info("Creating DNS account for BIND9")
368
369             try:
370                 msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
371                                       expression='(sAMAccountName=dns-%s)' % (hostname),
372                                       attrs=['clearTextPassword'])
373                 dn = msg[0].dn
374                 ldbs.sam.delete(dn)
375             except Exception:
376                 pass
377
378             dnspass = samba.generate_random_password(128, 255)
379             setup_add_ldif(ldbs.sam, setup_path("provision_dns_add_samba.ldif"), {
380                            "DNSDOMAIN": dnsdomain,
381                            "DOMAINDN": domaindn,
382                            "DNSPASS_B64": b64encode(dnspass.encode('utf-16-le')),
383                            "HOSTNAME" : hostname,
384                            "DNSNAME" : dnsname }
385                            )
386
387             secretsdb_setup_dns(ldbs.secrets, names,
388                                 paths.private_dir, realm=names.realm,
389                                 dnsdomain=names.dnsdomain,
390                                 dns_keytab_path=paths.dns_keytab, dnspass=dnspass)
391
392         # Setup a copy of SAM for BIND9
393         create_samdb_copy(ldbs.sam, logger, paths, names, domainsid,
394                           domainguid)
395
396         create_named_conf(paths, names.realm, dnsdomain, opts.dns_backend)
397
398         create_named_txt(paths.namedtxt, names.realm, dnsdomain, dnsname,
399                          paths.private_dir, paths.dns_keytab)
400         logger.info("See %s for an example configuration include file for BIND", paths.namedconf)
401         logger.info("and %s for further documentation required for secure DNS "
402                     "updates", paths.namedtxt)
403
404     logger.info("Finished upgrading DNS")