From 6d301ad1c9ff0f1ccd4f97bd5f234b10707a15bf Mon Sep 17 00:00:00 2001 From: Andrew Bartlett Date: Mon, 17 Aug 2015 15:33:31 +1200 Subject: [PATCH] samba-tool: Add new command 'samba-tool drs clone-dc-database' This command makes a clone of an existing AD Domain, but does not join the domain. This allows us to test if the join would work without adding objects to the target DC. The server password will need to be reset for the clone to be any use, see the source4/scripting/devel/chgtdcpass (Based on patches written with Garming Sam) Andrew Bartlett Signed-off-by: Andrew Bartlett Signed-off-by: Garming Sam Reviewed-by: Garming Sam --- python/samba/join.py | 142 ++++++++++++------ python/samba/netcmd/drs.py | 41 +++++ python/samba/tests/__init__.py | 2 +- python/samba/tests/blackbox/samba_tool_drs.py | 34 ++++- 4 files changed, 166 insertions(+), 53 deletions(-) diff --git a/python/samba/join.py b/python/samba/join.py index c3561452765..0f7dde237d1 100644 --- a/python/samba/join.py +++ b/python/samba/join.py @@ -54,12 +54,13 @@ class dc_join(object): def __init__(ctx, logger=None, server=None, creds=None, lp=None, site=None, netbios_name=None, targetdir=None, domain=None, machinepass=None, use_ntvfs=False, dns_backend=None, - promote_existing=False): + promote_existing=False, clone_only=False): + ctx.clone_only=clone_only + ctx.logger = logger ctx.creds = creds ctx.lp = lp ctx.site = site - ctx.netbios_name = netbios_name ctx.targetdir = targetdir ctx.use_ntvfs = use_ntvfs @@ -89,8 +90,6 @@ class dc_join(object): raise DCJoinException(estr) - ctx.myname = netbios_name - ctx.samname = "%s$" % ctx.myname ctx.base_dn = str(ctx.samdb.get_default_basedn()) ctx.root_dn = str(ctx.samdb.get_root_basedn()) ctx.schema_dn = str(ctx.samdb.get_schema_basedn()) @@ -110,17 +109,34 @@ class dc_join(object): else: ctx.acct_pass = samba.generate_random_password(32, 40) - # work out the DNs of all the objects we will be adding - ctx.server_dn = "CN=%s,CN=Servers,CN=%s,CN=Sites,%s" % (ctx.myname, ctx.site, ctx.config_dn) - ctx.ntds_dn = "CN=NTDS Settings,%s" % ctx.server_dn - topology_base = "CN=Topology,CN=Domain System Volume,CN=DFSR-GlobalSettings,CN=System,%s" % ctx.base_dn - if ctx.dn_exists(topology_base): - ctx.topology_dn = "CN=%s,%s" % (ctx.myname, topology_base) + ctx.dnsdomain = ctx.samdb.domain_dns_name() + if clone_only: + # As we don't want to create or delete these DNs, we set them to None + ctx.server_dn = None + ctx.ntds_dn = None + ctx.acct_dn = None + ctx.myname = ctx.server.split('.')[0] + ctx.ntds_guid = None else: - ctx.topology_dn = None + # work out the DNs of all the objects we will be adding + ctx.myname = netbios_name + ctx.samname = "%s$" % ctx.myname + ctx.server_dn = "CN=%s,CN=Servers,CN=%s,CN=Sites,%s" % (ctx.myname, ctx.site, ctx.config_dn) + ctx.ntds_dn = "CN=NTDS Settings,%s" % ctx.server_dn + ctx.acct_dn = "CN=%s,OU=Domain Controllers,%s" % (ctx.myname, ctx.base_dn) + ctx.dnshostname = "%s.%s" % (ctx.myname.lower(), ctx.dnsdomain) + ctx.dnsforest = ctx.samdb.forest_dns_name() + + topology_base = "CN=Topology,CN=Domain System Volume,CN=DFSR-GlobalSettings,CN=System,%s" % ctx.base_dn + if ctx.dn_exists(topology_base): + ctx.topology_dn = "CN=%s,%s" % (ctx.myname, topology_base) + else: + ctx.topology_dn = None + + ctx.SPNs = [ "HOST/%s" % ctx.myname, + "HOST/%s" % ctx.dnshostname, + "GC/%s/%s" % (ctx.dnshostname, ctx.dnsforest) ] - ctx.dnsdomain = ctx.samdb.domain_dns_name() - ctx.dnsforest = ctx.samdb.forest_dns_name() ctx.domaindns_zone = 'DC=DomainDnsZones,%s' % ctx.base_dn ctx.forestdns_zone = 'DC=ForestDnsZones,%s' % ctx.root_dn @@ -137,18 +153,10 @@ class dc_join(object): else: ctx.dns_backend = dns_backend - ctx.dnshostname = "%s.%s" % (ctx.myname.lower(), ctx.dnsdomain) - ctx.realm = ctx.dnsdomain - ctx.acct_dn = "CN=%s,OU=Domain Controllers,%s" % (ctx.myname, ctx.base_dn) - ctx.tmp_samdb = None - ctx.SPNs = [ "HOST/%s" % ctx.myname, - "HOST/%s" % ctx.dnshostname, - "GC/%s/%s" % (ctx.dnshostname, ctx.dnsforest) ] - # these elements are optional ctx.never_reveal_sid = None ctx.reveal_sid = None @@ -538,28 +546,30 @@ class dc_join(object): if ctx.krbtgt_dn: ctx.add_krbtgt_account() - print "Adding %s" % ctx.server_dn - rec = { - "dn": ctx.server_dn, - "objectclass" : "server", - # windows uses 50000000 decimal for systemFlags. A windows hex/decimal mixup bug? - "systemFlags" : str(samba.dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME | - samba.dsdb.SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE | - samba.dsdb.SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE), - # windows seems to add the dnsHostName later - "dnsHostName" : ctx.dnshostname} - - if ctx.acct_dn: - rec["serverReference"] = ctx.acct_dn + if ctx.server_dn: + print "Adding %s" % ctx.server_dn + rec = { + "dn": ctx.server_dn, + "objectclass" : "server", + # windows uses 50000000 decimal for systemFlags. A windows hex/decimal mixup bug? + "systemFlags" : str(samba.dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME | + samba.dsdb.SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE | + samba.dsdb.SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE), + # windows seems to add the dnsHostName later + "dnsHostName" : ctx.dnshostname} + + if ctx.acct_dn: + rec["serverReference"] = ctx.acct_dn - ctx.samdb.add(rec) + ctx.samdb.add(rec) if ctx.subdomain: # the rest is done after replication ctx.ntds_guid = None return - ctx.join_add_ntdsdsa() + if ctx.ntds_dn: + ctx.join_add_ntdsdsa() if ctx.connection_dn is not None: print "Adding %s" % ctx.connection_dn @@ -876,15 +886,17 @@ class dc_join(object): """Finalise the join, mark us synchronised and setup secrets db.""" # FIXME we shouldn't do this in all cases + # If for some reasons we joined in another site than the one of # DC we just replicated from then we don't need to send the updatereplicateref # as replication between sites is time based and on the initiative of the # requesting DC - ctx.logger.info("Sending DsReplicaUpdateRefs for all the replicated partitions") - for nc in ctx.nc_list: - ctx.send_DsReplicaUpdateRefs(nc) + if not ctx.clone_only: + ctx.logger.info("Sending DsReplicaUpdateRefs for all the replicated partitions") + for nc in ctx.nc_list: + ctx.send_DsReplicaUpdateRefs(nc) - if ctx.RODC: + if not ctx.clone_only and ctx.RODC: print "Setting RODC invocationId" ctx.local_samdb.set_invocation_id(str(ctx.invocation_id)) ctx.local_samdb.set_opaque_integer("domainFunctionality", @@ -914,11 +926,18 @@ class dc_join(object): m = ldb.Message() m.dn = ldb.Dn(ctx.local_samdb, '@ROOTDSE') m["isSynchronized"] = ldb.MessageElement("TRUE", ldb.FLAG_MOD_REPLACE, "isSynchronized") - m["dsServiceName"] = ldb.MessageElement("" % str(ctx.ntds_guid), + + # We want to appear to be the server we just cloned + if ctx.clone_only: + guid = ctx.samdb.get_ntds_GUID() + else: + guid = ctx.ntds_guid + + m["dsServiceName"] = ldb.MessageElement("" % str(guid), ldb.FLAG_MOD_REPLACE, "dsServiceName") ctx.local_samdb.modify(m) - if ctx.subdomain: + if ctx.clone_only or ctx.subdomain: return secrets_ldb = Ldb(ctx.paths.secrets, session_info=system_session(), lp=ctx.lp) @@ -1077,23 +1096,26 @@ class dc_join(object): ctx.full_nc_list += [ctx.domaindns_zone] ctx.full_nc_list += [ctx.forestdns_zone] - if ctx.promote_existing: - ctx.promote_possible() - else: - ctx.cleanup_old_join() + if not ctx.clone_only: + if ctx.promote_existing: + ctx.promote_possible() + else: + ctx.cleanup_old_join() try: - ctx.join_add_objects() + if not ctx.clone_only: + ctx.join_add_objects() ctx.join_provision() ctx.join_replicate() - if ctx.subdomain: + if (not ctx.clone_only and ctx.subdomain): ctx.join_add_objects2() ctx.join_provision_own_domain() ctx.join_setup_trusts() ctx.join_finalise() except: print "Join failed - cleaning up" - ctx.cleanup_old_join() + if not ctx.clone_only: + ctx.cleanup_old_join() raise @@ -1183,6 +1205,28 @@ def join_DC(logger=None, server=None, creds=None, lp=None, site=None, netbios_na ctx.do_join() logger.info("Joined domain %s (SID %s) as a DC" % (ctx.domain_name, ctx.domsid)) +def join_clone(logger=None, server=None, creds=None, lp=None, + targetdir=None, domain=None): + """Join as a DC.""" + ctx = dc_join(logger, server, creds, lp, site=None, netbios_name=None, targetdir=targetdir, domain=domain, + machinepass=None, use_ntvfs=False, dns_backend="NONE", promote_existing=False, clone_only=True) + + lp.set("workgroup", ctx.domain_name) + logger.info("workgroup is %s" % ctx.domain_name) + + lp.set("realm", ctx.realm) + logger.info("realm is %s" % ctx.realm) + + ctx.replica_flags = (drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_INIT_SYNC | + drsuapi.DRSUAPI_DRS_PER_SYNC | + drsuapi.DRSUAPI_DRS_FULL_SYNC_IN_PROGRESS | + drsuapi.DRSUAPI_DRS_NEVER_SYNCED) + ctx.domain_replica_flags = ctx.replica_flags + + ctx.do_join() + logger.info("Cloned domain %s (SID %s)" % (ctx.domain_name, ctx.domsid)) + def join_subdomain(logger=None, server=None, creds=None, lp=None, site=None, netbios_name=None, targetdir=None, parent_domain=None, dnsdomain=None, netbios_domain=None, machinepass=None, adminpass=None, use_ntvfs=False, diff --git a/python/samba/netcmd/drs.py b/python/samba/netcmd/drs.py index e8e9ec879aa..f1d4970aecb 100644 --- a/python/samba/netcmd/drs.py +++ b/python/samba/netcmd/drs.py @@ -20,6 +20,7 @@ import samba.getopt as options import ldb +import logging from samba.auth import system_session from samba.netcmd import ( @@ -32,6 +33,7 @@ from samba.samdb import SamDB from samba import drs_utils, nttime2string, dsdb from samba.dcerpc import drsuapi, misc import common +from samba.join import join_clone def drsuapi_connect(ctx): '''make a DRSUAPI connection to the server''' @@ -513,6 +515,44 @@ class cmd_drs_options(Command): self.message("New DSA options: " + ", ".join(cur_opts)) +class cmd_drs_clone_dc_database(Command): + """Replicate an initial clone of domain, but DO NOT JOIN it.""" + + synopsis = "%prog [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("--server", help="DC to join", type=str), + Option("--targetdir", help="where to store provision", type=str), + Option("--quiet", help="Be quiet", action="store_true"), + Option("--verbose", help="Be verbose", action="store_true") + ] + + takes_args = ["domain"] + + def run(self, domain, sambaopts=None, credopts=None, + versionopts=None, server=None, targetdir=None, + quiet=False, verbose=False): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + logger = self.get_logger() + if verbose: + logger.setLevel(logging.DEBUG) + elif quiet: + logger.setLevel(logging.WARNING) + else: + logger.setLevel(logging.INFO) + + join_clone(logger=logger, server=server, creds=creds, lp=lp, domain=domain, + targetdir=targetdir) + + class cmd_drs(SuperCommand): """Directory Replication Services (DRS) management.""" @@ -522,3 +562,4 @@ class cmd_drs(SuperCommand): subcommands["replicate"] = cmd_drs_replicate() subcommands["showrepl"] = cmd_drs_showrepl() subcommands["options"] = cmd_drs_options() + subcommands["clone-dc-database"] = cmd_drs_clone_dc_database() diff --git a/python/samba/tests/__init__.py b/python/samba/tests/__init__.py index b53c4ea027f..87b69435e37 100644 --- a/python/samba/tests/__init__.py +++ b/python/samba/tests/__init__.py @@ -253,7 +253,7 @@ class BlackboxProcessError(Exception): return "Command '%s'; exit status %d; stdout: '%s'; stderr: '%s'" % (self.cmd, self.returncode, self.stdout, self.stderr) -class BlackboxTestCase(TestCase): +class BlackboxTestCase(TestCaseInTempDir): """Base test case for blackbox tests.""" def _make_cmdline(self, line): diff --git a/python/samba/tests/blackbox/samba_tool_drs.py b/python/samba/tests/blackbox/samba_tool_drs.py index 9b7106ff03f..0bfd65cac5c 100644 --- a/python/samba/tests/blackbox/samba_tool_drs.py +++ b/python/samba/tests/blackbox/samba_tool_drs.py @@ -18,7 +18,8 @@ """Blackbox tests for samba-tool drs.""" import samba.tests - +import shutil +import os class SambaToolDrsTests(samba.tests.BlackboxTestCase): """Blackbox test case for samba-tool drs.""" @@ -33,10 +34,10 @@ class SambaToolDrsTests(samba.tests.BlackboxTestCase): self.cmdline_creds = "-U%s/%s%%%s" % (creds.get_domain(), creds.get_username(), creds.get_password()) - def _get_rootDSE(self, dc): + def _get_rootDSE(self, dc, ldap_only=True): samdb = samba.tests.connect_samdb(dc, lp=self.get_loadparm(), credentials=self.get_credentials(), - ldap_only=True) + ldap_only=ldap_only) return samdb.search(base="", scope=samba.tests.ldb.SCOPE_BASE)[0] def test_samba_tool_bind(self): @@ -100,3 +101,30 @@ class SambaToolDrsTests(samba.tests.BlackboxTestCase): self.cmdline_creds)) self.assertTrue("Replicate from" in out) self.assertTrue("was successful" in out) + + def test_samba_tool_drs_clone_dc(self): + """Tests 'samba-tool drs clone-dc-database' command.""" + server_rootdse = self._get_rootDSE(self.dc1) + server_nc_name = server_rootdse["defaultNamingContext"] + server_ds_name = server_rootdse["dsServiceName"] + server_ldap_service_name = str(server_rootdse["ldapServiceName"][0]) + server_realm = server_ldap_service_name.split(":")[0] + creds = self.get_credentials() + out = self.check_output("samba-tool drs clone-dc-database %s --server=%s %s --targetdir=%s" + % (server_realm, + self.dc1, + self.cmdline_creds, + self.tempdir)) + ldb_rootdse = self._get_rootDSE("tdb://" + os.path.join(self.tempdir, "private", "sam.ldb"), ldap_only=False) + nc_name = ldb_rootdse["defaultNamingContext"] + ds_name = ldb_rootdse["dsServiceName"] + ldap_service_name = str(server_rootdse["ldapServiceName"][0]) + self.assertEqual(nc_name, server_nc_name) + # The clone should pretend to be the source server + self.assertEqual(ds_name, server_ds_name) + self.assertEqual(ldap_service_name, server_ldap_service_name) + shutil.rmtree(os.path.join(self.tempdir, "private")) + shutil.rmtree(os.path.join(self.tempdir, "etc")) + shutil.rmtree(os.path.join(self.tempdir, "msg.lock")) + os.remove(os.path.join(self.tempdir, "names.tdb")) + shutil.rmtree(os.path.join(self.tempdir, "state")) -- 2.45.1