summaryrefslogtreecommitdiffstats
path: root/source4/scripting/bin/samba_upgradedns
diff options
context:
space:
mode:
Diffstat (limited to 'source4/scripting/bin/samba_upgradedns')
-rwxr-xr-xsource4/scripting/bin/samba_upgradedns589
1 files changed, 589 insertions, 0 deletions
diff --git a/source4/scripting/bin/samba_upgradedns b/source4/scripting/bin/samba_upgradedns
new file mode 100755
index 0000000..afc5807
--- /dev/null
+++ b/source4/scripting/bin/samba_upgradedns
@@ -0,0 +1,589 @@
+#!/usr/bin/env python3
+#
+# Unix SMB/CIFS implementation.
+# Copyright (C) Amitay Isaacs <amitay@gmail.com> 2012
+#
+# Upgrade DNS provision from BIND9_FLATFILE to BIND9_DLZ or SAMBA_INTERNAL
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import os
+import errno
+import optparse
+import logging
+import grp
+from base64 import b64encode
+import shlex
+
+sys.path.insert(0, "bin/python")
+
+import ldb
+import samba
+from samba import param
+from samba.auth import system_session
+from samba.ndr import (
+ ndr_pack,
+ ndr_unpack )
+import samba.getopt as options
+from samba.upgradehelpers import (
+ get_paths,
+ get_ldbs )
+from samba.dsdb import DS_DOMAIN_FUNCTION_2003
+from samba.provision import (
+ find_provision_key_parameters,
+ interface_ips_v4,
+ interface_ips_v6 )
+from samba.provision.common import (
+ setup_path,
+ setup_add_ldif,
+ FILL_FULL)
+from samba.provision.sambadns import (
+ ARecord,
+ AAAARecord,
+ CNAMERecord,
+ NSRecord,
+ SOARecord,
+ SRVRecord,
+ TXTRecord,
+ get_dnsadmins_sid,
+ add_dns_accounts,
+ create_dns_partitions,
+ fill_dns_data_partitions,
+ create_dns_dir,
+ secretsdb_setup_dns,
+ create_dns_dir_keytab_link,
+ create_samdb_copy,
+ create_named_conf,
+ create_named_txt )
+from samba.dcerpc import security
+
+import dns.zone, dns.rdatatype
+
+__docformat__ = 'restructuredText'
+
+
+def find_bind_gid():
+ """Find system group id for bind9
+ """
+ for name in ["bind", "named"]:
+ try:
+ return grp.getgrnam(name)[2]
+ except KeyError:
+ pass
+ return None
+
+
+def convert_dns_rdata(rdata, serial=1):
+ """Convert resource records in dnsRecord format
+ """
+ if rdata.rdtype == dns.rdatatype.A:
+ rec = ARecord(rdata.address, serial=serial)
+ elif rdata.rdtype == dns.rdatatype.AAAA:
+ rec = AAAARecord(rdata.address, serial=serial)
+ elif rdata.rdtype == dns.rdatatype.CNAME:
+ rec = CNAMERecord(rdata.target.to_text(), serial=serial)
+ elif rdata.rdtype == dns.rdatatype.NS:
+ rec = NSRecord(rdata.target.to_text(), serial=serial)
+ elif rdata.rdtype == dns.rdatatype.SRV:
+ rec = SRVRecord(rdata.target.to_text(), int(rdata.port),
+ priority=int(rdata.priority), weight=int(rdata.weight),
+ serial=serial)
+ elif rdata.rdtype == dns.rdatatype.TXT:
+ slist = shlex.split(rdata.to_text())
+ rec = TXTRecord(slist, serial=serial)
+ elif rdata.rdtype == dns.rdatatype.SOA:
+ rec = SOARecord(rdata.mname.to_text(), rdata.rname.to_text(),
+ serial=int(rdata.serial),
+ refresh=int(rdata.refresh), retry=int(rdata.retry),
+ expire=int(rdata.expire), minimum=int(rdata.minimum))
+ else:
+ rec = None
+ return rec
+
+
+def import_zone_data(samdb, logger, zone, serial, domaindn, forestdn,
+ dnsdomain, dnsforest):
+ """Insert zone data in DNS partitions
+ """
+ labels = dnsdomain.split('.')
+ labels.append('')
+ domain_root = dns.name.Name(labels)
+ domain_prefix = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (dnsdomain,
+ domaindn)
+
+ tmp = "_msdcs.%s" % dnsforest
+ labels = tmp.split('.')
+ labels.append('')
+ forest_root = dns.name.Name(labels)
+ dnsmsdcs = "_msdcs.%s" % dnsforest
+ forest_prefix = "DC=%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (dnsmsdcs,
+ forestdn)
+
+ # Extract @ record
+ at_record = zone.get_node(domain_root)
+ zone.delete_node(domain_root)
+
+ # SOA record
+ rdset = at_record.get_rdataset(dns.rdataclass.IN, dns.rdatatype.SOA)
+ soa_rec = ndr_pack(convert_dns_rdata(rdset[0]))
+ at_record.delete_rdataset(dns.rdataclass.IN, dns.rdatatype.SOA)
+
+ # NS record
+ rdset = at_record.get_rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
+ ns_rec = ndr_pack(convert_dns_rdata(rdset[0]))
+ at_record.delete_rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
+
+ # A/AAAA records
+ ip_recs = []
+ for rdset in at_record:
+ for r in rdset:
+ rec = convert_dns_rdata(r)
+ ip_recs.append(ndr_pack(rec))
+
+ # Add @ record for domain
+ dns_rec = [soa_rec, ns_rec] + ip_recs
+ msg = ldb.Message(ldb.Dn(samdb, 'DC=@,%s' % domain_prefix))
+ msg["objectClass"] = ["top", "dnsNode"]
+ msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
+ "dnsRecord")
+ try:
+ samdb.add(msg)
+ except Exception:
+ logger.error("Failed to add @ record for domain")
+ raise
+ logger.debug("Added @ record for domain")
+
+ # Add @ record for forest
+ dns_rec = [soa_rec, ns_rec]
+ msg = ldb.Message(ldb.Dn(samdb, 'DC=@,%s' % forest_prefix))
+ msg["objectClass"] = ["top", "dnsNode"]
+ msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
+ "dnsRecord")
+ try:
+ samdb.add(msg)
+ except Exception:
+ logger.error("Failed to add @ record for forest")
+ raise
+ logger.debug("Added @ record for forest")
+
+ # Add remaining records in domain and forest
+ for node in zone.nodes:
+ name = node.relativize(forest_root).to_text()
+ if name == node.to_text():
+ name = node.relativize(domain_root).to_text()
+ dn = "DC=%s,%s" % (name, domain_prefix)
+ fqdn = "%s.%s" % (name, dnsdomain)
+ else:
+ dn = "DC=%s,%s" % (name, forest_prefix)
+ fqdn = "%s.%s" % (name, dnsmsdcs)
+
+ dns_rec = []
+ for rdataset in zone.nodes[node]:
+ for rdata in rdataset:
+ rec = convert_dns_rdata(rdata, serial)
+ if not rec:
+ logger.warn("Unsupported record type (%s) for %s, ignoring" %
+ dns.rdatatype.to_text(rdata.rdatatype), name)
+ else:
+ dns_rec.append(ndr_pack(rec))
+
+ msg = ldb.Message(ldb.Dn(samdb, dn))
+ msg["objectClass"] = ["top", "dnsNode"]
+ msg["dnsRecord"] = ldb.MessageElement(dns_rec, ldb.FLAG_MOD_ADD,
+ "dnsRecord")
+ try:
+ samdb.add(msg)
+ except Exception:
+ logger.error("Failed to add DNS record %s" % (fqdn))
+ raise
+ logger.debug("Added DNS record %s" % (fqdn))
+
+def cleanup_remove_file(file_path):
+ try:
+ os.remove(file_path)
+ except OSError as e:
+ if e.errno not in [errno.EEXIST, errno.ENOENT]:
+ pass
+ else:
+ logger.debug("Could not remove %s: %s" % (file_path, e.strerror))
+
+def cleanup_remove_dir(dir_path):
+ try:
+ for root, dirs, files in os.walk(dir_path, topdown=False):
+ for name in files:
+ os.remove(os.path.join(root, name))
+ for name in dirs:
+ os.rmdir(os.path.join(root, name))
+ os.rmdir(dir_path)
+ except OSError as e:
+ if e.errno not in [errno.EEXIST, errno.ENOENT]:
+ pass
+ else:
+ logger.debug("Could not delete dir %s: %s" % (dir_path, e.strerror))
+
+def cleanup_obsolete_dns_files(paths):
+ cleanup_remove_file(os.path.join(paths.private_dir, "named.conf"))
+ cleanup_remove_file(os.path.join(paths.private_dir, "named.conf.update"))
+ cleanup_remove_file(os.path.join(paths.private_dir, "named.txt"))
+
+ cleanup_remove_dir(os.path.join(paths.private_dir, "dns"))
+
+
+# dnsprovision creates application partitions for AD based DNS mainly if the existing
+# provision was created using earlier snapshots of samba4 which did not have support
+# for DNS partitions
+
+if __name__ == '__main__':
+
+ # Setup command line parser
+ parser = optparse.OptionParser("samba_upgradedns [options]")
+ sambaopts = options.SambaOptions(parser)
+ credopts = options.CredentialsOptions(parser)
+
+ parser.add_option_group(options.VersionOptions(parser))
+ parser.add_option_group(sambaopts)
+ parser.add_option_group(credopts)
+
+ parser.add_option("--dns-backend", type="choice", metavar="<BIND9_DLZ|SAMBA_INTERNAL>",
+ choices=["SAMBA_INTERNAL", "BIND9_DLZ"], default="SAMBA_INTERNAL",
+ help="The DNS server backend, default SAMBA_INTERNAL")
+ parser.add_option("--migrate", type="choice", metavar="<yes|no>",
+ choices=["yes","no"], default="yes",
+ help="Migrate existing zone data, default yes")
+ parser.add_option("--verbose", help="Be verbose", action="store_true")
+
+ opts = parser.parse_args()[0]
+
+ if opts.dns_backend is None:
+ opts.dns_backend = 'SAMBA_INTERNAL'
+
+ if opts.migrate:
+ autofill = False
+ else:
+ autofill = True
+
+ # Set up logger
+ logger = logging.getLogger("upgradedns")
+ logger.addHandler(logging.StreamHandler(sys.stdout))
+ logger.setLevel(logging.INFO)
+ if opts.verbose:
+ logger.setLevel(logging.DEBUG)
+
+ lp = sambaopts.get_loadparm()
+ lp.load(lp.configfile)
+ creds = credopts.get_credentials(lp)
+
+ logger.info("Reading domain information")
+ paths = get_paths(param, smbconf=lp.configfile)
+ paths.bind_gid = find_bind_gid()
+ ldbs = get_ldbs(paths, creds, system_session(), lp)
+ names = find_provision_key_parameters(ldbs.sam, ldbs.secrets, ldbs.idmap,
+ paths, lp.configfile, lp)
+
+ if names.domainlevel < DS_DOMAIN_FUNCTION_2003:
+ logger.error("Cannot create AD based DNS for OS level < 2003")
+ sys.exit(1)
+
+ domaindn = names.domaindn
+ forestdn = names.rootdn
+
+ dnsdomain = names.dnsdomain.lower()
+ dnsforest = dnsdomain
+
+ site = names.sitename
+ hostname = names.hostname
+ dnsname = '%s.%s' % (hostname, dnsdomain)
+
+ domainsid = names.domainsid
+ domainguid = names.domainguid
+ ntdsguid = names.ntdsguid
+
+ # Check for DNS accounts and create them if required
+ try:
+ msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
+ expression='(sAMAccountName=DnsAdmins)',
+ attrs=['objectSid'])
+ dnsadmins_sid = ndr_unpack(security.dom_sid, msg[0]['objectSid'][0])
+ except IndexError:
+ logger.info("Adding DNS accounts")
+ add_dns_accounts(ldbs.sam, domaindn)
+ dnsadmins_sid = get_dnsadmins_sid(ldbs.sam, domaindn)
+ else:
+ logger.info("DNS accounts already exist")
+
+ # Import dns records from zone file
+ if os.path.exists(paths.dns):
+ logger.info("Reading records from zone file %s" % paths.dns)
+ try:
+ zone = dns.zone.from_file(paths.dns, relativize=False)
+ rrset = zone.get_rdataset("%s." % dnsdomain, dns.rdatatype.SOA)
+ serial = int(rrset[0].serial)
+ except Exception as e:
+ logger.warn("Error parsing DNS data from '%s' (%s)" % (paths.dns, str(e)))
+ autofill = True
+ else:
+ logger.info("No zone file %s (normal)" % paths.dns)
+ autofill = True
+
+ # Create DNS partitions if missing and fill DNS information
+ try:
+ expression = '(|(dnsRoot=DomainDnsZones.%s)(dnsRoot=ForestDnsZones.%s))' % \
+ (dnsdomain, dnsforest)
+ msg = ldbs.sam.search(base=names.configdn, scope=ldb.SCOPE_DEFAULT,
+ expression=expression, attrs=['nCName'])
+ ncname = msg[0]['nCName'][0]
+ except IndexError:
+ logger.info("Creating DNS partitions")
+
+ logger.info("Looking up IPv4 addresses")
+ hostip = interface_ips_v4(lp)
+ try:
+ hostip.remove('127.0.0.1')
+ except ValueError:
+ pass
+ if not hostip:
+ logger.error("No IPv4 addresses found")
+ sys.exit(1)
+ else:
+ hostip = hostip[0]
+ logger.debug("IPv4 addresses: %s" % hostip)
+
+ logger.info("Looking up IPv6 addresses")
+ hostip6 = interface_ips_v6(lp)
+ if not hostip6:
+ hostip6 = None
+ else:
+ hostip6 = hostip6[0]
+ logger.debug("IPv6 addresses: %s" % hostip6)
+
+ create_dns_partitions(ldbs.sam, domainsid, names, domaindn, forestdn,
+ dnsadmins_sid, FILL_FULL)
+
+ logger.info("Populating DNS partitions")
+ if autofill:
+ logger.warn("DNS records will be automatically created")
+
+ fill_dns_data_partitions(ldbs.sam, domainsid, site, domaindn, forestdn,
+ dnsdomain, dnsforest, hostname, hostip, hostip6,
+ domainguid, ntdsguid, dnsadmins_sid,
+ autofill=autofill)
+
+ if not autofill:
+ logger.info("Importing records from zone file")
+ import_zone_data(ldbs.sam, logger, zone, serial, domaindn, forestdn,
+ dnsdomain, dnsforest)
+ else:
+ logger.info("DNS partitions already exist")
+
+ # Mark that we are hosting DNS partitions
+ try:
+ dns_nclist = [ 'DC=DomainDnsZones,%s' % domaindn,
+ 'DC=ForestDnsZones,%s' % forestdn ]
+
+ msgs = ldbs.sam.search(base=names.serverdn, scope=ldb.SCOPE_DEFAULT,
+ expression='(objectclass=nTDSDSa)',
+ attrs=['hasPartialReplicaNCs',
+ 'msDS-hasMasterNCs'])
+ msg = msgs[0]
+
+ master_nclist = []
+ ncs = msg.get("msDS-hasMasterNCs")
+ if ncs:
+ for nc in ncs:
+ master_nclist.append(str(nc))
+
+ partial_nclist = []
+ ncs = msg.get("hasPartialReplicaNCs")
+ if ncs:
+ for nc in ncs:
+ partial_nclist.append(str(nc))
+
+ modified_master = False
+ modified_partial = False
+
+ for nc in dns_nclist:
+ if nc not in master_nclist:
+ master_nclist.append(nc)
+ modified_master = True
+ if nc in partial_nclist:
+ partial_nclist.remove(nc)
+ modified_partial = True
+
+ if modified_master or modified_partial:
+ logger.debug("Updating msDS-hasMasterNCs and hasPartialReplicaNCs attributes")
+ m = ldb.Message()
+ m.dn = msg.dn
+ if modified_master:
+ m["msDS-hasMasterNCs"] = ldb.MessageElement(master_nclist,
+ ldb.FLAG_MOD_REPLACE,
+ "msDS-hasMasterNCs")
+ if modified_partial:
+ if partial_nclist:
+ m["hasPartialReplicaNCs"] = ldb.MessageElement(partial_nclist,
+ ldb.FLAG_MOD_REPLACE,
+ "hasPartialReplicaNCs")
+ else:
+ m["hasPartialReplicaNCs"] = ldb.MessageElement(ncs,
+ ldb.FLAG_MOD_DELETE,
+ "hasPartialReplicaNCs")
+ ldbs.sam.modify(m)
+ except Exception:
+ raise
+
+ # Special stuff for DLZ backend
+ if opts.dns_backend == "BIND9_DLZ":
+ config_migration = False
+
+ if (paths.private_dir != paths.binddns_dir and
+ os.path.isfile(os.path.join(paths.private_dir, "named.conf"))):
+ config_migration = True
+
+ # Check if dns-HOSTNAME account exists and create it if required
+ secrets_msgs = ldbs.secrets.search(expression='(samAccountName=dns-%s)' % hostname, attrs=['secret'])
+ msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
+ expression='(sAMAccountName=dns-%s)' % (hostname),
+ attrs=[])
+
+ if len(secrets_msgs) == 0 or len(msg) == 0:
+ logger.info("Adding dns-%s account" % hostname)
+
+ if len(secrets_msgs) == 1:
+ dn = secrets_msgs[0].dn
+ ldbs.secrets.delete(dn)
+
+ if len(msg) == 1:
+ dn = msg[0].dn
+ ldbs.sam.delete(dn)
+
+ dnspass = samba.generate_random_password(128, 255)
+ setup_add_ldif(ldbs.sam, setup_path("provision_dns_add_samba.ldif"), {
+ "DNSDOMAIN": dnsdomain,
+ "DOMAINDN": domaindn,
+ "DNSPASS_B64": b64encode(dnspass.encode('utf-16-le')).decode('utf8'),
+ "HOSTNAME" : hostname,
+ "DNSNAME" : dnsname }
+ )
+
+ res = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
+ expression='(sAMAccountName=dns-%s)' % (hostname),
+ attrs=["msDS-KeyVersionNumber"])
+ if "msDS-KeyVersionNumber" in res[0]:
+ dns_key_version_number = int(res[0]["msDS-KeyVersionNumber"][0])
+ else:
+ dns_key_version_number = None
+
+ secretsdb_setup_dns(ldbs.secrets, names,
+ paths.private_dir, paths.binddns_dir, realm=names.realm,
+ dnsdomain=names.dnsdomain,
+ dns_keytab_path=paths.dns_keytab, dnspass=dnspass,
+ key_version_number=dns_key_version_number)
+
+ else:
+ logger.info("dns-%s account already exists" % hostname)
+
+ if not os.path.exists(paths.binddns_dir):
+ # This directory won't exist if we're restoring from an offline backup.
+ os.mkdir(paths.binddns_dir, 0o770)
+
+ create_dns_dir_keytab_link(logger, paths)
+
+ # This forces a re-creation of dns directory and all the files within
+ # It's an overkill, but it's easier to re-create a samdb copy, rather
+ # than trying to fix a broken copy.
+ create_dns_dir(logger, paths)
+
+ # Setup a copy of SAM for BIND9
+ create_samdb_copy(ldbs.sam, logger, paths, names, domainsid,
+ domainguid)
+
+ create_named_conf(paths, names.realm, dnsdomain, opts.dns_backend, logger)
+
+ create_named_txt(paths.namedtxt, names.realm, dnsdomain, dnsname,
+ paths.binddns_dir, paths.dns_keytab)
+
+ cleanup_obsolete_dns_files(paths)
+
+ if config_migration:
+ logger.info("ATTENTION: The BIND configuration and keytab has been moved to: %s",
+ paths.binddns_dir)
+ logger.info(" Please update your BIND configuration accordingly.")
+ else:
+ logger.info("See %s for an example configuration include file for BIND", paths.namedconf)
+ logger.info("and %s for further documentation required for secure DNS "
+ "updates", paths.namedtxt)
+
+ elif opts.dns_backend == "SAMBA_INTERNAL":
+ # Make sure to remove everything from the bind-dns directory to avoid
+ # possible security issues with the named group having write access
+ # to all AD partitions
+ cleanup_remove_file(os.path.join(paths.binddns_dir, "dns.keytab"))
+ cleanup_remove_file(os.path.join(paths.binddns_dir, "named.conf"))
+ cleanup_remove_file(os.path.join(paths.binddns_dir, "named.conf.update"))
+ cleanup_remove_file(os.path.join(paths.binddns_dir, "named.txt"))
+
+ cleanup_remove_dir(os.path.dirname(paths.dns))
+
+ try:
+ os.chmod(paths.private_dir, 0o700)
+ os.chown(paths.private_dir, -1, 0)
+ except:
+ logger.warn("Failed to restore owner and permissions for %s",
+ (paths.private_dir))
+
+ # Check if dns-HOSTNAME account exists and delete it if required
+ try:
+ dn_str = 'samAccountName=dns-%s,CN=Principals' % hostname
+ msg = ldbs.secrets.search(expression='(dn=%s)' % dn_str, attrs=[])
+ dn = msg[0].dn
+ except IndexError:
+ dn = None
+
+ if dn is not None:
+ try:
+ ldbs.secrets.delete(dn)
+ except Exception:
+ logger.info("Failed to delete %s from secrets.ldb" % dn)
+
+ try:
+ msg = ldbs.sam.search(base=domaindn, scope=ldb.SCOPE_DEFAULT,
+ expression='(sAMAccountName=dns-%s)' % (hostname),
+ attrs=[])
+ dn = msg[0].dn
+ except IndexError:
+ dn = None
+
+ if dn is not None:
+ try:
+ ldbs.sam.delete(dn)
+ except Exception:
+ logger.info("Failed to delete %s from sam.ldb" % dn)
+
+ logger.info("Finished upgrading DNS")
+
+ services = lp.get("server services")
+ for service in services:
+ if service == "dns":
+ if opts.dns_backend.startswith("BIND"):
+ logger.info("You have switched to using %s as your dns backend,"
+ " but still have the internal dns starting. Please"
+ " make sure you add '-dns' to your server services"
+ " line in your smb.conf." % opts.dns_backend)
+ break
+ else:
+ if opts.dns_backend == "SAMBA_INTERNAL":
+ logger.info("You have switched to using %s as your dns backend,"
+ " but you still have samba starting looking for a"
+ " BIND backend. Please remove the -dns from your"
+ " server services line." % opts.dns_backend)