dsdb:schema: use NUMERIC_CMP in place of uint32_cmp
[Samba.git] / source4 / scripting / bin / samba_upgradedns
blobafc580779b7bdbba8e9e86a863e3247c4ca61ef4
1 #!/usr/bin/env python3
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 errno
24 import optparse
25 import logging
26 import grp
27 from base64 import b64encode
28 import shlex
30 sys.path.insert(0, "bin/python")
32 import ldb
33 import samba
34 from samba import param
35 from samba.auth import system_session
36 from samba.ndr import (
37     ndr_pack,
38     ndr_unpack )
39 import samba.getopt as options
40 from samba.upgradehelpers import (
41     get_paths,
42     get_ldbs )
43 from samba.dsdb import DS_DOMAIN_FUNCTION_2003
44 from samba.provision import (
45     find_provision_key_parameters,
46     interface_ips_v4,
47     interface_ips_v6 )
48 from samba.provision.common import (
49     setup_path,
50     setup_add_ldif,
51     FILL_FULL)
52 from samba.provision.sambadns import (
53     ARecord,
54     AAAARecord,
55     CNAMERecord,
56     NSRecord,
57     SOARecord,
58     SRVRecord,
59     TXTRecord,
60     get_dnsadmins_sid,
61     add_dns_accounts,
62     create_dns_partitions,
63     fill_dns_data_partitions,
64     create_dns_dir,
65     secretsdb_setup_dns,
66     create_dns_dir_keytab_link,
67     create_samdb_copy,
68     create_named_conf,
69     create_named_txt )
70 from samba.dcerpc import security
72 import dns.zone, dns.rdatatype
74 __docformat__ = 'restructuredText'
77 def find_bind_gid():
78     """Find system group id for bind9
79     """
80     for name in ["bind", "named"]:
81         try:
82             return grp.getgrnam(name)[2]
83         except KeyError:
84             pass
85     return None
88 def convert_dns_rdata(rdata, serial=1):
89     """Convert resource records in dnsRecord format
90     """
91     if rdata.rdtype == dns.rdatatype.A:
92         rec = ARecord(rdata.address, serial=serial)
93     elif rdata.rdtype == dns.rdatatype.AAAA:
94         rec = AAAARecord(rdata.address, serial=serial)
95     elif rdata.rdtype == dns.rdatatype.CNAME:
96         rec = CNAMERecord(rdata.target.to_text(), serial=serial)
97     elif rdata.rdtype == dns.rdatatype.NS:
98         rec = NSRecord(rdata.target.to_text(), serial=serial)
99     elif rdata.rdtype == dns.rdatatype.SRV:
100         rec = SRVRecord(rdata.target.to_text(), int(rdata.port),
101                         priority=int(rdata.priority), weight=int(rdata.weight),
102                         serial=serial)
103     elif rdata.rdtype == dns.rdatatype.TXT:
104         slist = shlex.split(rdata.to_text())
105         rec = TXTRecord(slist, serial=serial)
106     elif rdata.rdtype == dns.rdatatype.SOA:
107         rec = SOARecord(rdata.mname.to_text(), rdata.rname.to_text(),
108                         serial=int(rdata.serial),
109                         refresh=int(rdata.refresh), retry=int(rdata.retry),
110                         expire=int(rdata.expire), minimum=int(rdata.minimum))
111     else:
112         rec = None
113     return rec
116 def import_zone_data(samdb, logger, zone, serial, domaindn, forestdn,
117                      dnsdomain, dnsforest):
118     """Insert zone data in DNS partitions
119     """
120     labels = dnsdomain.split('.')
121     labels.append('')
122     domain_root = dns.name.Name(labels)
123     domain_prefix = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (dnsdomain,
124                                                                     domaindn)
126     tmp = "_msdcs.%s" % dnsforest
127     labels = tmp.split('.')
128     labels.append('')
129     forest_root = dns.name.Name(labels)
130     dnsmsdcs = "_msdcs.%s" % dnsforest
131     forest_prefix = "DC=%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (dnsmsdcs,
132                                                                     forestdn)
134     # Extract @ record
135     at_record = zone.get_node(domain_root)
136     zone.delete_node(domain_root)
138     # SOA record
139     rdset = at_record.get_rdataset(dns.rdataclass.IN, dns.rdatatype.SOA)
140     soa_rec = ndr_pack(convert_dns_rdata(rdset[0]))
141     at_record.delete_rdataset(dns.rdataclass.IN, dns.rdatatype.SOA)
143     # NS record
144     rdset = at_record.get_rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
145     ns_rec = ndr_pack(convert_dns_rdata(rdset[0]))
146     at_record.delete_rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
148     # A/AAAA records
149     ip_recs = []
150     for rdset in at_record:
151         for r in rdset:
152             rec = convert_dns_rdata(r)
153             ip_recs.append(ndr_pack(rec))
155     # Add @ record for domain
156     dns_rec = [soa_rec, ns_rec] + ip_recs
157     msg = ldb.Message(ldb.Dn(samdb, 'DC=@,%s' % domain_prefix))
158     msg["objectClass"] = ["top", "dnsNode"]
159     msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
160                                           "dnsRecord")
161     try:
162         samdb.add(msg)
163     except Exception:
164         logger.error("Failed to add @ record for domain")
165         raise
166     logger.debug("Added @ record for domain")
168     # Add @ record for forest
169     dns_rec = [soa_rec, ns_rec]
170     msg = ldb.Message(ldb.Dn(samdb, 'DC=@,%s' % forest_prefix))
171     msg["objectClass"] = ["top", "dnsNode"]
172     msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
173                                           "dnsRecord")
174     try:
175         samdb.add(msg)
176     except Exception:
177         logger.error("Failed to add @ record for forest")
178         raise
179     logger.debug("Added @ record for forest")
181     # Add remaining records in domain and forest
182     for node in zone.nodes:
183         name = node.relativize(forest_root).to_text()
184         if name == node.to_text():
185             name = node.relativize(domain_root).to_text()
186             dn = "DC=%s,%s" % (name, domain_prefix)
187             fqdn = "%s.%s" % (name, dnsdomain)
188         else:
189             dn = "DC=%s,%s" % (name, forest_prefix)
190             fqdn = "%s.%s" % (name, dnsmsdcs)
192         dns_rec = []
193         for rdataset in zone.nodes[node]:
194             for rdata in rdataset:
195                 rec = convert_dns_rdata(rdata, serial)
196                 if not rec:
197                     logger.warn("Unsupported record type (%s) for %s, ignoring" %
198                                 dns.rdatatype.to_text(rdata.rdatatype), name)
199                 else:
200                     dns_rec.append(ndr_pack(rec))
202         msg = ldb.Message(ldb.Dn(samdb, dn))
203         msg["objectClass"] = ["top", "dnsNode"]
204         msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
205                                               "dnsRecord")
206         try:
207             samdb.add(msg)
208         except Exception:
209             logger.error("Failed to add DNS record %s" % (fqdn))
210             raise
211         logger.debug("Added DNS record %s" % (fqdn))
213 def cleanup_remove_file(file_path):
214     try:
215         os.remove(file_path)
216     except OSError as e:
217         if e.errno not in [errno.EEXIST, errno.ENOENT]:
218             pass
219         else:
220             logger.debug("Could not remove %s: %s" % (file_path, e.strerror))
222 def cleanup_remove_dir(dir_path):
223     try:
224         for root, dirs, files in os.walk(dir_path, topdown=False):
225             for name in files:
226                 os.remove(os.path.join(root, name))
227             for name in dirs:
228                 os.rmdir(os.path.join(root, name))
229         os.rmdir(dir_path)
230     except OSError as e:
231         if e.errno not in [errno.EEXIST, errno.ENOENT]:
232             pass
233         else:
234             logger.debug("Could not delete dir %s: %s" % (dir_path, e.strerror))
236 def cleanup_obsolete_dns_files(paths):
237     cleanup_remove_file(os.path.join(paths.private_dir, "named.conf"))
238     cleanup_remove_file(os.path.join(paths.private_dir, "named.conf.update"))
239     cleanup_remove_file(os.path.join(paths.private_dir, "named.txt"))
241     cleanup_remove_dir(os.path.join(paths.private_dir, "dns"))
244 # dnsprovision creates application partitions for AD based DNS mainly if the existing
245 # provision was created using earlier snapshots of samba4 which did not have support
246 # for DNS partitions
248 if __name__ == '__main__':
250     # Setup command line parser
251     parser = optparse.OptionParser("samba_upgradedns [options]")
252     sambaopts = options.SambaOptions(parser)
253     credopts = options.CredentialsOptions(parser)
255     parser.add_option_group(options.VersionOptions(parser))
256     parser.add_option_group(sambaopts)
257     parser.add_option_group(credopts)
259     parser.add_option("--dns-backend", type="choice", metavar="<BIND9_DLZ|SAMBA_INTERNAL>",
260                       choices=["SAMBA_INTERNAL", "BIND9_DLZ"], default="SAMBA_INTERNAL",
261                       help="The DNS server backend, default SAMBA_INTERNAL")
262     parser.add_option("--migrate", type="choice", metavar="<yes|no>",
263                       choices=["yes","no"], default="yes",
264                       help="Migrate existing zone data, default yes")
265     parser.add_option("--verbose", help="Be verbose", action="store_true")
267     opts = parser.parse_args()[0]
269     if opts.dns_backend is None:
270         opts.dns_backend = 'SAMBA_INTERNAL'
272     if opts.migrate:
273         autofill = False
274     else:
275         autofill = True
277     # Set up logger
278     logger = logging.getLogger("upgradedns")
279     logger.addHandler(logging.StreamHandler(sys.stdout))
280     logger.setLevel(logging.INFO)
281     if opts.verbose:
282         logger.setLevel(logging.DEBUG)
284     lp = sambaopts.get_loadparm()
285     lp.load(lp.configfile)
286     creds = credopts.get_credentials(lp)
288     logger.info("Reading domain information")
289     paths = get_paths(param, smbconf=lp.configfile)
290     paths.bind_gid = find_bind_gid()
291     ldbs = get_ldbs(paths, creds, system_session(), lp)
292     names = find_provision_key_parameters(ldbs.sam, ldbs.secrets, ldbs.idmap,
293                                            paths, lp.configfile, lp)
295     if names.domainlevel < DS_DOMAIN_FUNCTION_2003:
296         logger.error("Cannot create AD based DNS for OS level < 2003")
297         sys.exit(1)
299     domaindn = names.domaindn
300     forestdn = names.rootdn
302     dnsdomain = names.dnsdomain.lower()
303     dnsforest = dnsdomain
305     site = names.sitename
306     hostname = names.hostname
307     dnsname = '%s.%s' % (hostname, dnsdomain)
309     domainsid = names.domainsid
310     domainguid = names.domainguid
311     ntdsguid = names.ntdsguid
313     # Check for DNS accounts and create them if required
314     try:
315         msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
316                               expression='(sAMAccountName=DnsAdmins)',
317                               attrs=['objectSid'])
318         dnsadmins_sid = ndr_unpack(security.dom_sid, msg[0]['objectSid'][0])
319     except IndexError:
320         logger.info("Adding DNS accounts")
321         add_dns_accounts(ldbs.sam, domaindn)
322         dnsadmins_sid = get_dnsadmins_sid(ldbs.sam, domaindn)
323     else:
324         logger.info("DNS accounts already exist")
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 as e:
334             logger.warn("Error parsing DNS data from '%s' (%s)" % (paths.dns, str(e)))
335             autofill = True
336     else:
337         logger.info("No zone file %s (normal)" % paths.dns)
338         autofill = True
340     # Create DNS partitions if missing and fill DNS information
341     try:
342         expression = '(|(dnsRoot=DomainDnsZones.%s)(dnsRoot=ForestDnsZones.%s))' % \
343                      (dnsdomain, dnsforest)
344         msg = ldbs.sam.search(base=names.configdn, scope=ldb.SCOPE_DEFAULT,
345                               expression=expression, attrs=['nCName'])
346         ncname = msg[0]['nCName'][0]
347     except IndexError:
348         logger.info("Creating DNS partitions")
350         logger.info("Looking up IPv4 addresses")
351         hostip = interface_ips_v4(lp)
352         try:
353             hostip.remove('127.0.0.1')
354         except ValueError:
355             pass
356         if not hostip:
357             logger.error("No IPv4 addresses found")
358             sys.exit(1)
359         else:
360             hostip = hostip[0]
361             logger.debug("IPv4 addresses: %s" % hostip)
363         logger.info("Looking up IPv6 addresses")
364         hostip6 = interface_ips_v6(lp)
365         if not hostip6:
366             hostip6 = None
367         else:
368             hostip6 = hostip6[0]
369         logger.debug("IPv6 addresses: %s" % hostip6)
371         create_dns_partitions(ldbs.sam, domainsid, names, domaindn, forestdn,
372                               dnsadmins_sid, FILL_FULL)
374         logger.info("Populating DNS partitions")
375         if autofill:
376             logger.warn("DNS records will be automatically created")
378         fill_dns_data_partitions(ldbs.sam, domainsid, site, domaindn, forestdn,
379                              dnsdomain, dnsforest, hostname, hostip, hostip6,
380                              domainguid, ntdsguid, dnsadmins_sid,
381                              autofill=autofill)
383         if not autofill:
384             logger.info("Importing records from zone file")
385             import_zone_data(ldbs.sam, logger, zone, serial, domaindn, forestdn,
386                              dnsdomain, dnsforest)
387     else:
388         logger.info("DNS partitions already exist")
390     # Mark that we are hosting DNS partitions
391     try:
392         dns_nclist = [ 'DC=DomainDnsZones,%s' % domaindn,
393                        'DC=ForestDnsZones,%s' % forestdn ]
395         msgs = ldbs.sam.search(base=names.serverdn, scope=ldb.SCOPE_DEFAULT,
396                                expression='(objectclass=nTDSDSa)',
397                                attrs=['hasPartialReplicaNCs',
398                                       'msDS-hasMasterNCs'])
399         msg = msgs[0]
401         master_nclist = []
402         ncs = msg.get("msDS-hasMasterNCs")
403         if ncs:
404             for nc in ncs:
405                 master_nclist.append(str(nc))
407         partial_nclist = []
408         ncs = msg.get("hasPartialReplicaNCs")
409         if ncs:
410             for nc in ncs:
411                 partial_nclist.append(str(nc))
413         modified_master = False
414         modified_partial = False
416         for nc in dns_nclist:
417             if nc not in master_nclist:
418                 master_nclist.append(nc)
419                 modified_master = True
420             if nc in partial_nclist:
421                 partial_nclist.remove(nc)
422                 modified_partial = True
424         if modified_master or modified_partial:
425             logger.debug("Updating msDS-hasMasterNCs and hasPartialReplicaNCs attributes")
426             m = ldb.Message()
427             m.dn = msg.dn
428             if modified_master:
429                 m["msDS-hasMasterNCs"] = ldb.MessageElement(master_nclist,
430                                                             ldb.FLAG_MOD_REPLACE,
431                                                             "msDS-hasMasterNCs")
432             if modified_partial:
433                 if partial_nclist:
434                     m["hasPartialReplicaNCs"] = ldb.MessageElement(partial_nclist,
435                                                                    ldb.FLAG_MOD_REPLACE,
436                                                                    "hasPartialReplicaNCs")
437                 else:
438                     m["hasPartialReplicaNCs"] = ldb.MessageElement(ncs,
439                                                                    ldb.FLAG_MOD_DELETE,
440                                                                    "hasPartialReplicaNCs")
441             ldbs.sam.modify(m)
442     except Exception:
443         raise
445     # Special stuff for DLZ backend
446     if opts.dns_backend == "BIND9_DLZ":
447         config_migration = False
449         if (paths.private_dir != paths.binddns_dir and
450             os.path.isfile(os.path.join(paths.private_dir, "named.conf"))):
451             config_migration = True
453         # Check if dns-HOSTNAME account exists and create it if required
454         secrets_msgs = ldbs.secrets.search(expression='(samAccountName=dns-%s)' % hostname, attrs=['secret'])
455         msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
456                               expression='(sAMAccountName=dns-%s)' % (hostname),
457                               attrs=[])
459         if len(secrets_msgs) == 0 or len(msg) == 0:
460             logger.info("Adding dns-%s account" % hostname)
462             if len(secrets_msgs) == 1:
463                 dn = secrets_msgs[0].dn
464                 ldbs.secrets.delete(dn)
466             if len(msg) == 1:
467                 dn = msg[0].dn
468                 ldbs.sam.delete(dn)
470             dnspass = samba.generate_random_password(128, 255)
471             setup_add_ldif(ldbs.sam, setup_path("provision_dns_add_samba.ldif"), {
472                     "DNSDOMAIN": dnsdomain,
473                     "DOMAINDN": domaindn,
474                     "DNSPASS_B64": b64encode(dnspass.encode('utf-16-le')).decode('utf8'),
475                     "HOSTNAME" : hostname,
476                     "DNSNAME" : dnsname }
477                            )
479             res = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
480                                   expression='(sAMAccountName=dns-%s)' % (hostname),
481                                   attrs=["msDS-KeyVersionNumber"])
482             if "msDS-KeyVersionNumber" in res[0]:
483                 dns_key_version_number = int(res[0]["msDS-KeyVersionNumber"][0])
484             else:
485                 dns_key_version_number = None
487             secretsdb_setup_dns(ldbs.secrets, names,
488                                 paths.private_dir, paths.binddns_dir, realm=names.realm,
489                                 dnsdomain=names.dnsdomain,
490                                 dns_keytab_path=paths.dns_keytab, dnspass=dnspass,
491                                 key_version_number=dns_key_version_number)
493         else:
494             logger.info("dns-%s account already exists" % hostname)
496         if not os.path.exists(paths.binddns_dir):
497             # This directory won't exist if we're restoring from an offline backup.
498             os.mkdir(paths.binddns_dir, 0o770)
500         create_dns_dir_keytab_link(logger, paths)
502         # This forces a re-creation of dns directory and all the files within
503         # It's an overkill, but it's easier to re-create a samdb copy, rather
504         # than trying to fix a broken copy.
505         create_dns_dir(logger, paths)
507         # Setup a copy of SAM for BIND9
508         create_samdb_copy(ldbs.sam, logger, paths, names, domainsid,
509                           domainguid)
511         create_named_conf(paths, names.realm, dnsdomain, opts.dns_backend, logger)
513         create_named_txt(paths.namedtxt, names.realm, dnsdomain, dnsname,
514                          paths.binddns_dir, paths.dns_keytab)
516         cleanup_obsolete_dns_files(paths)
518         if config_migration:
519             logger.info("ATTENTION: The BIND configuration and keytab has been moved to: %s",
520                         paths.binddns_dir)
521             logger.info("           Please update your BIND configuration accordingly.")
522         else:
523             logger.info("See %s for an example configuration include file for BIND", paths.namedconf)
524             logger.info("and %s for further documentation required for secure DNS "
525                         "updates", paths.namedtxt)
527     elif opts.dns_backend == "SAMBA_INTERNAL":
528         # Make sure to remove everything from the bind-dns directory to avoid
529         # possible security issues with the named group having write access
530         # to all AD partitions
531         cleanup_remove_file(os.path.join(paths.binddns_dir, "dns.keytab"))
532         cleanup_remove_file(os.path.join(paths.binddns_dir, "named.conf"))
533         cleanup_remove_file(os.path.join(paths.binddns_dir, "named.conf.update"))
534         cleanup_remove_file(os.path.join(paths.binddns_dir, "named.txt"))
536         cleanup_remove_dir(os.path.dirname(paths.dns))
538         try:
539             os.chmod(paths.private_dir, 0o700)
540             os.chown(paths.private_dir, -1, 0)
541         except:
542             logger.warn("Failed to restore owner and permissions for %s",
543                         (paths.private_dir))
545         # Check if dns-HOSTNAME account exists and delete it if required
546         try:
547             dn_str = 'samAccountName=dns-%s,CN=Principals' % hostname
548             msg = ldbs.secrets.search(expression='(dn=%s)' % dn_str, attrs=[])
549             dn = msg[0].dn
550         except IndexError:
551             dn = None
553         if dn is not None:
554             try:
555                 ldbs.secrets.delete(dn)
556             except Exception:
557                 logger.info("Failed to delete %s from secrets.ldb" % dn)
559         try:
560             msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
561                                   expression='(sAMAccountName=dns-%s)' % (hostname),
562                                   attrs=[])
563             dn = msg[0].dn
564         except IndexError:
565             dn = None
567         if dn is not None:
568             try:
569                 ldbs.sam.delete(dn)
570             except Exception:
571                 logger.info("Failed to delete %s from sam.ldb" % dn)
573     logger.info("Finished upgrading DNS")
575     services = lp.get("server services")
576     for service in services:
577         if service == "dns":
578             if opts.dns_backend.startswith("BIND"):
579                 logger.info("You have switched to using %s as your dns backend,"
580                         " but still have the internal dns starting. Please"
581                         " make sure you add '-dns' to your server services"
582                         " line in your smb.conf." % opts.dns_backend)
583             break
584     else:
585         if opts.dns_backend == "SAMBA_INTERNAL":
586             logger.info("You have switched to using %s as your dns backend,"
587                     " but you still have samba starting looking for a"
588                     " BIND backend. Please remove the -dns from your"
589                     " server services line." % opts.dns_backend)