[PATCH] WIP: domain rename tool

Tim Beale timbeale at catalyst.net.nz
Thu Jun 21 00:52:26 UTC 2018


Hi,

Here's a heads up of where I'm at with the domain rename tool. It turns
out it's fairly simple to rename the domain as part of the DRS
replication, just by changing the top-level object's DN in the first
chunk of replication data for each partition. The name change then flows
through to all child objects/links as part of the replication handling.

The rename builds on top of Aaron's domain backup tool work. I've added
a new command that's basically a DRS clone-with-rename. It works similar
to the 'backup online' command in that it writes the cloned DB to a
backup-file. The backup-file can then be restored on a fresh DC (the
restore process strips out all the original domain's DC info and adds a
brand new DC).

As discussed on the list earlier, the use-case for the rename tool are:
- creating a realistic lab DC for pre-production testing.
- creating a temporary backup domain that can run in parallel with the
failed original domain (i.e. admins can diagnose/repair the failed DCs
without them interfering with the new domain's operation).
There'll be more documentation on how to use the rename tool as part of
the final patch-set.

The attached patch-set refactors the join/clone code slightly in order
to support the new clone-with-rename behaviour, extends the domain
backup commands, and adds a new testenv that gets generated by doing a
backup-rename then restore. The rename command basically works OK in the
manual testing I've done, and the current changes pass CI tests:
https://gitlab.com/catalyst-samba/samba/pipelines/24113075

Still to come is more testing.

Also, we still need to integrate the backup/restore of sysvol files with
the NTACL work Joe has done. If someone could review those changes, that
would be greatly appreciated:
https://lists.samba.org/archive/samba-technical/2018-June/128386.html

Cheers,
Tim

-------------- next part --------------
From 4656ef86c8efdbed2e20acaf9c4435ef08dc1e22 Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Fri, 25 May 2018 14:05:27 +1200
Subject: [PATCH 01/10] dbchecker: Fixing up incorrect DNs wasn't working

dbcheck would fail to fix up attributes where the extended DN's GUID is
correct, but the DN itself is incorrect. The code failed attempting to
remove the old/incorrect DN, e.g.

 NOTE: old (due to rename or delete) DN string component for
 objectCategory in object CN=alice,CN=Users,DC=samba,DC=example,DC=com -
 <GUID=7bfdf9d8-62f9-420c-8a71-e3d3e931c91e>;
   CN=Person,CN=Schema,CN=Configuration,DC=samba,DC=bad,DC=com
 Change DN to <GUID=7bfdf9d8-62f9-420c-8a71-e3d3e931c91e>;
   CN=Person,CN=Schema,CN=Configuration,DC=samba,DC=example,DC=com?
 [y/N/all/none] y
 Failed to fix old DN string on attribute objectCategory : (16,
 "attribute 'objectCategory': no matching attribute value while deleting
 attribute on 'CN=alice,CN=Users,DC=samba,DC=example,DC=com'")

The problem was the LDB message specified the value to delete with its
full DN, including the GUID. The LDB code then helpfully corrected this
value on the way through, so that the DN got updated to reflect the
correct DN (i.e. 'DC=example,DC=com') of the object matching that GUID,
rather than the incorrect DN (i.e. 'DC=bad,DC=com') that we were trying
to remove. Because the requested value and the existing DB value didn't
match, the operation failed.

We can avoid this problem by passing down just the DN (not the extended
DN) of the value we want to delete. Without the GUID portion of the DN,
the LDB code will no longer try to correct it on the way through, and
the dbcheck operation will succeed.

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
 python/samba/dbchecker.py | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/python/samba/dbchecker.py b/python/samba/dbchecker.py
index 9d72fc6..f1e2d4e 100644
--- a/python/samba/dbchecker.py
+++ b/python/samba/dbchecker.py
@@ -1298,14 +1298,22 @@ newSuperior: %s""" % (str(from_dn), str(to_rdn), str(to_base)))
                                                       res[0].dn, "SID")
                 continue
 
+            # Only for non-links, not even forward-only links
+            # (otherwise this breaks repl_meta_data):
+            #
             # Now we have checked the GUID and SID, offer to fix old
-            # DN strings as a non-error (for forward links with no
+            # DN strings as a non-error (DNs, not links so no
             # backlink).  Samba does not maintain this string
             # otherwise, so we don't increment error_count.
             if reverse_link_name is None:
-                if str(res[0].dn) != str(dsdb_dn.dn):
-                    self.err_dn_string_component_old(obj.dn, attrname, val, dsdb_dn,
-                                                     res[0].dn)
+                if linkID == 0 and str(res[0].dn) != str(dsdb_dn.dn):
+                    # Pass in the old/bad DN without the <GUID=...> part,
+                    # otherwise the LDB code will correct it on the way through
+                    # (Note: we still want to preserve the DSDB DN prefix in the
+                    # case of binary DNs)
+                    bad_dn = dsdb_dn.prefix + dsdb_dn.dn.get_linearized()
+                    self.err_dn_string_component_old(obj.dn, attrname, bad_dn,
+                                                     dsdb_dn, res[0].dn)
                 continue
 
             # check the reverse_link is correct if there should be one
-- 
2.7.4


From 05b9e243baafa8237b85f17a76461cf62a619211 Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Wed, 6 Jun 2018 10:04:29 +1200
Subject: [PATCH 02/10] drs_utils: Add infrastructure to support 'clone with
 rename'

Our end goal is to create a backup clone of a DB, but rename the
domain/realm so we can startup the backup DC without interferring with
the existing Samba network. The basic strategy to do this is to leverage
DRS replication - by renaming the first object in the partition, all
subsequent objects will automatically be renamed.

This patch adds the infrastructure to do this. I've used object
inheritance to handle the special case of renaming the partition
objects. This means the domain-rename special case doesn't really
pollute the existing DRS replication code. All it needs is a small
refactor to create a new 'process_chunk()' function that the new
sub-class can then override.

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
 python/samba/drs_utils.py | 72 ++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 68 insertions(+), 4 deletions(-)

diff --git a/python/samba/drs_utils.py b/python/samba/drs_utils.py
index 7fab480..4e025a1 100644
--- a/python/samba/drs_utils.py
+++ b/python/samba/drs_utils.py
@@ -24,7 +24,8 @@ from samba import dsdb
 from samba import werror
 from samba import WERRORError
 import samba, ldb
-
+from samba.dcerpc.drsuapi import DRSUAPI_ATTID_name
+import re
 
 class drsException(Exception):
     """Base element for drs errors"""
@@ -213,6 +214,12 @@ class drs_Replicate(object):
                 (req.more_flags & drsuapi.DRSUAPI_DRS_GET_TGT) == 0 and
                 self.supported_extensions & drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V10)
 
+    def process_chunk(self, level, ctr, schema, req_level, req, first_chunk):
+        '''Processes a single chunk of received replication data'''
+        # pass the replication into the py_net.c python bindings for processing
+        self.net.replicate_chunk(self.replication_state, level, ctr,
+                                 schema=schema, req_level=req_level, req=req)
+
     def replicate(self, dn, source_dsa_invocation_id, destination_dsa_guid,
                   schema=False, exop=drsuapi.DRSUAPI_EXOP_NONE, rodc=False,
                   replica_flags=None, full_sync=True, sync_forced=False, more_flags=0):
@@ -309,14 +316,15 @@ class drs_Replicate(object):
 
         num_objects = 0
         num_links = 0
+        first_chunk = True
+
         while True:
             (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, req_level, req)
             if ctr.first_object is None and ctr.object_count != 0:
                 raise RuntimeError("DsGetNCChanges: NULL first_object with object_count=%u" % (ctr.object_count))
 
             try:
-                self.net.replicate_chunk(self.replication_state, level, ctr,
-                    schema=schema, req_level=req_level, req=req)
+                self.process_chunk(level, ctr, schema, req_level, req, first_chunk)
             except WERRORError as e:
                 # Check if retrying with the GET_TGT flag set might resolve this error
                 if self._should_retry_with_get_tgt(e[0], req):
@@ -324,11 +332,14 @@ class drs_Replicate(object):
                     print("Missing target object - retrying with DRS_GET_TGT")
                     req.more_flags |= drsuapi.DRSUAPI_DRS_GET_TGT
 
-                    # try sending the request again
+                    # try sending the request again (this has the side-effect
+                    # of causing the DC to restart the replication from scratch)
+                    first_chunk = True
                     continue
                 else:
                     raise e
 
+            first_chunk = False
             num_objects += ctr.object_count
 
             # Cope with servers that do not return level 6, so do not return any links
@@ -342,3 +353,56 @@ class drs_Replicate(object):
             req.highwatermark = ctr.new_highwatermark
 
         return (num_objects, num_links)
+
+# Handles the special case of creating a new clone of a DB, while also renaming
+# the entire DB's objects on the way through
+class drs_ReplicateRenamer(drs_Replicate):
+    '''Uses DRS replication to rename the entire DB'''
+
+    def __init__(self, binding_string, lp, creds, samdb, invocation_id,
+                 old_base_dn, new_base_dn):
+        super(drs_ReplicateRenamer, self).__init__(binding_string, lp, creds,
+                                                   samdb, invocation_id)
+        self.old_base_dn = old_base_dn
+        self.new_base_dn = new_base_dn
+
+    def rename_dn(self, dn_str):
+        '''Uses string substitution to replace the base DN'''
+        return re.sub('%s$' % self.old_base_dn, self.new_base_dn, dn_str)
+
+    def update_name_attr(self, base_obj):
+        '''Updates the 'name' attribute for the base DN object'''
+        for attr in base_obj.attribute_ctr.attributes:
+            if attr.attid == DRSUAPI_ATTID_name:
+                base_dn = ldb.Dn(self.samdb, base_obj.identifier.dn)
+                new_name = base_dn.get_rdn_value()
+                attr.value_ctr.values[0].blob = new_name.encode('utf-16-le')
+
+    def rename_top_level_object(self, first_obj):
+        '''Renames the first/top-level object in a partition'''
+        old_dn = first_obj.identifier.dn
+        first_obj.identifier.dn = self.rename_dn(first_obj.identifier.dn)
+        print("Renaming partition %s --> %s" %(old_dn, first_obj.identifier.dn))
+
+        # we also need to fix up the 'name' attribute for the base DN,
+        # otherwise the RDNs won't match
+        if first_obj.identifier.dn == self.new_base_dn:
+            self.update_name_attr(first_obj)
+
+    def process_chunk(self, level, ctr, schema, req_level, req, first_chunk):
+        '''Processes a single chunk of received replication data'''
+
+        # we need to rename the NC in every chunk - this gets used in searches
+        # when applying the chunk
+        if ctr.naming_context:
+            ctr.naming_context.dn = self.rename_dn(ctr.naming_context.dn)
+
+        # rename the first object in each partition. This will cause every
+        # subsequent object in the partiton to be renamed as a side-effect
+        if first_chunk and ctr.object_count != 0:
+            self.rename_top_level_object(ctr.first_object.object)
+
+        # then do the normal repl processing to apply this chunk to our DB
+        super(drs_ReplicateRenamer, self).process_chunk(level, ctr, schema,
+                                                        req_level, req,
+                                                        first_chunk)
-- 
2.7.4


From d418239f7b3b48a8e03856b58e6e9b2715418d6e Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Mon, 11 Jun 2018 09:14:06 +1200
Subject: [PATCH 03/10] provision: Small refactor to host-IP logic

Split out the code that determines the host-IP of the new server into
separate functions. This will allow us to re-use the same logic in the
backup/restore case.

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz
---
 python/samba/provision/__init__.py | 54 ++++++++++++++++++++++----------------
 1 file changed, 31 insertions(+), 23 deletions(-)

diff --git a/python/samba/provision/__init__.py b/python/samba/provision/__init__.py
index d9df425..f6acff1 100644
--- a/python/samba/provision/__init__.py
+++ b/python/samba/provision/__init__.py
@@ -2040,6 +2040,35 @@ def directory_create_or_exists(path, mode=0o755):
             else:
                 raise ProvisioningError("Failed to create directory %s: %s" % (path, e.strerror))
 
+def determine_host_ip(logger, lp, hostip=None):
+    if hostip is None:
+        logger.info("Looking up IPv4 addresses")
+        hostips = interface_ips_v4(lp)
+        if len(hostips) > 0:
+            hostip = hostips[0]
+            if len(hostips) > 1:
+                logger.warning("More than one IPv4 address found. Using %s",
+                    hostip)
+    if hostip == "127.0.0.1":
+        hostip = None
+    if hostip is None:
+        logger.warning("No IPv4 address will be assigned")
+
+    return hostip
+
+def determine_host_ip6(logger, lp, hostip6=None):
+    if hostip6 is None:
+        logger.info("Looking up IPv6 addresses")
+        hostips = interface_ips_v6(lp)
+        if hostips:
+            hostip6 = hostips[0]
+        if len(hostips) > 1:
+            logger.warning("More than one IPv6 address found. Using %s", hostip6)
+    if hostip6 is None:
+        logger.warning("No IPv6 address will be assigned")
+
+    return hostip6
+
 def provision(logger, session_info, smbconf=None,
         targetdir=None, samdb_fill=FILL_FULL, realm=None, rootdn=None,
         domaindn=None, schemadn=None, configdn=None, serverdn=None,
@@ -2147,29 +2176,8 @@ def provision(logger, session_info, smbconf=None,
     paths.root_uid = root_uid;
     paths.root_gid = root_gid
 
-    if hostip is None:
-        logger.info("Looking up IPv4 addresses")
-        hostips = interface_ips_v4(lp)
-        if len(hostips) > 0:
-            hostip = hostips[0]
-            if len(hostips) > 1:
-                logger.warning("More than one IPv4 address found. Using %s",
-                    hostip)
-    if hostip == "127.0.0.1":
-        hostip = None
-    if hostip is None:
-        logger.warning("No IPv4 address will be assigned")
-
-    if hostip6 is None:
-        logger.info("Looking up IPv6 addresses")
-        hostips = interface_ips_v6(lp)
-        if hostips:
-            hostip6 = hostips[0]
-        if len(hostips) > 1:
-            logger.warning("More than one IPv6 address found. Using %s", hostip6)
-    if hostip6 is None:
-        logger.warning("No IPv6 address will be assigned")
-
+    hostip = determine_host_ip(logger, lp, hostip)
+    hostip6 = determine_host_ip6(logger, lp, hostip6)
     names.hostip = hostip
     names.hostip6 = hostip6
     names.domainguid = domainguid
-- 
2.7.4


From 751a3d0e6b1b1586a3819756af1d5d056f15a1bc Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Mon, 11 Jun 2018 15:26:44 +1200
Subject: [PATCH 04/10] join: Rename dc_join() so it looks like an object

dc_join() is creating an object, but it currently looks like it's
just a function call. Rename it to look more object-like.

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
 python/samba/join.py                    | 29 +++++++++++++++++------------
 python/samba/netcmd/domain_backup.py    |  6 +++---
 python/samba/tests/join.py              | 13 +++++++------
 source4/dsdb/tests/python/acl.py        | 13 +++++++------
 source4/torture/drs/python/repl_rodc.py | 20 +++++++++++++-------
 5 files changed, 47 insertions(+), 34 deletions(-)

diff --git a/python/samba/join.py b/python/samba/join.py
index f937c36..4bceb89 100644
--- a/python/samba/join.py
+++ b/python/samba/join.py
@@ -50,7 +50,7 @@ class DCJoinException(Exception):
         super(DCJoinException, self).__init__("Can't join, error: %s" % msg)
 
 
-class dc_join(object):
+class DCJoinContext(object):
     """Perform a DC join."""
 
     def __init__(ctx, logger=None, server=None, creds=None, lp=None, site=None,
@@ -1429,9 +1429,10 @@ def join_RODC(logger=None, server=None, creds=None, lp=None, site=None, netbios_
               backend_store=None):
     """Join as a RODC."""
 
-    ctx = dc_join(logger, server, creds, lp, site, netbios_name, targetdir, domain,
-                  machinepass, use_ntvfs, dns_backend, promote_existing,
-                  plaintext_secrets, backend_store=backend_store)
+    ctx = DCJoinContext(logger, server, creds, lp, site, netbios_name,
+                        targetdir, domain, machinepass, use_ntvfs, dns_backend,
+                        promote_existing, plaintext_secrets,
+                        backend_store=backend_store)
 
     lp.set("workgroup", ctx.domain_name)
     logger.info("workgroup is %s" % ctx.domain_name)
@@ -1481,9 +1482,10 @@ def join_DC(logger=None, server=None, creds=None, lp=None, site=None, netbios_na
             promote_existing=False, plaintext_secrets=False,
             backend_store=None):
     """Join as a DC."""
-    ctx = dc_join(logger, server, creds, lp, site, netbios_name, targetdir, domain,
-                  machinepass, use_ntvfs, dns_backend, promote_existing,
-                  plaintext_secrets, backend_store=backend_store)
+    ctx = DCJoinContext(logger, server, creds, lp, site, netbios_name,
+                        targetdir, domain, machinepass, use_ntvfs, dns_backend,
+                        promote_existing, plaintext_secrets,
+                        backend_store=backend_store)
 
     lp.set("workgroup", ctx.domain_name)
     logger.info("workgroup is %s" % ctx.domain_name)
@@ -1508,8 +1510,10 @@ def join_DC(logger=None, server=None, creds=None, lp=None, site=None, netbios_na
 def join_clone(logger=None, server=None, creds=None, lp=None,
                targetdir=None, domain=None, include_secrets=False, dns_backend="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=dns_backend, promote_existing=False, clone_only=True)
+    ctx = DCJoinContext(logger, server, creds, lp, site=None, netbios_name=None,
+                        targetdir=targetdir, domain=domain, machinepass=None,
+                        use_ntvfs=False, dns_backend=dns_backend,
+                        promote_existing=False, clone_only=True)
 
     lp.set("workgroup", ctx.domain_name)
     logger.info("workgroup is %s" % ctx.domain_name)
@@ -1532,9 +1536,10 @@ def join_subdomain(logger=None, server=None, creds=None, lp=None, site=None,
         dns_backend=None, plaintext_secrets=False,
         backend_store=None):
     """Join as a DC."""
-    ctx = dc_join(logger, server, creds, lp, site, netbios_name, targetdir, parent_domain,
-                  machinepass, use_ntvfs, dns_backend, plaintext_secrets,
-                  backend_store=backend_store)
+    ctx = DCJoinContext(logger, server, creds, lp, site, netbios_name,
+                        targetdir, parent_domain, machinepass, use_ntvfs,
+                        dns_backend, plaintext_secrets,
+                        backend_store=backend_store)
     ctx.subdomain = True
     if adminpass is None:
         ctx.adminpass = samba.generate_random_password(12, 32)
diff --git a/python/samba/netcmd/domain_backup.py b/python/samba/netcmd/domain_backup.py
index 2d5fa1b..72db0a2 100644
--- a/python/samba/netcmd/domain_backup.py
+++ b/python/samba/netcmd/domain_backup.py
@@ -27,7 +27,7 @@ import ldb
 from samba.auth import system_session
 from samba.upgradehelpers import update_krbtgt_account_password
 from samba.emulate.traffic import create_machine_account
-from samba.join import dc_join, join_clone
+from samba.join import DCJoinContext, join_clone
 from samba.dcerpc.security import dom_sid
 from samba.netcmd import Option, CommandError
 from samba.dcerpc import misc
@@ -504,8 +504,8 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
                            attrs=['namingContexts'])
         ncs = [str(r) for r in res[0].get('namingContexts')]
 
-        ctx = dc_join(logger, creds=creds, lp=lp, forced_local_samdb=samdb,
-                      netbios_name=newservername, full_nc_list=ncs)
+        ctx = DCJoinContext(logger, creds=creds, lp=lp, forced_local_samdb=samdb,
+                            netbios_name=newservername, full_nc_list=ncs)
         ctx.userAccountControl = samba.dsdb.UF_SERVER_TRUST_ACCOUNT |\
                                  samba.dsdb.UF_TRUSTED_FOR_DELEGATION
 
diff --git a/python/samba/tests/join.py b/python/samba/tests/join.py
index 1f9fab1..904c283 100644
--- a/python/samba/tests/join.py
+++ b/python/samba/tests/join.py
@@ -21,7 +21,7 @@ import sys
 import shutil
 import os
 from samba.tests.dns_base import DNSTKeyTest
-from samba.join import dc_join
+from samba.join import DCJoinContext
 from samba.dcerpc import drsuapi, misc, dns
 from samba.credentials import Credentials
 
@@ -42,11 +42,12 @@ class JoinTestCase(DNSTKeyTest):
         self.netbios_name = "jointest1"
         logger = get_logger()
 
-        self.join_ctx = dc_join(server=self.server, creds=self.creds, lp=self.get_loadparm(),
-                                netbios_name=self.netbios_name,
-                                targetdir=self.tempdir,
-                                domain=None, logger=logger,
-                                dns_backend="SAMBA_INTERNAL")
+        self.join_ctx = DCJoinContext(server=self.server, creds=self.creds,
+                                      lp=self.get_loadparm(),
+                                      netbios_name=self.netbios_name,
+                                      targetdir=self.tempdir,
+                                      domain=None, logger=logger,
+                                      dns_backend="SAMBA_INTERNAL")
         self.join_ctx.userAccountControl = (samba.dsdb.UF_SERVER_TRUST_ACCOUNT |
                                             samba.dsdb.UF_TRUSTED_FOR_DELEGATION)
 
diff --git a/source4/dsdb/tests/python/acl.py b/source4/dsdb/tests/python/acl.py
index 29fcf55..c698bf1 100755
--- a/source4/dsdb/tests/python/acl.py
+++ b/source4/dsdb/tests/python/acl.py
@@ -13,7 +13,7 @@ import samba
 from samba.tests.subunitrun import SubunitOptions, TestProgram
 
 import samba.getopt as options
-from samba.join import dc_join
+from samba.join import DCJoinContext
 
 from ldb import (
     SCOPE_BASE, SCOPE_SUBTREE, LdbError, ERR_NO_SUCH_OBJECT,
@@ -1841,11 +1841,12 @@ class AclSPNTests(AclTests):
         self.computerdn = "CN=%s,CN=computers,%s" % (self.computername, self.base_dn)
         self.dc_dn = "CN=%s,OU=Domain Controllers,%s" % (self.dcname, self.base_dn)
         self.site = "Default-First-Site-Name"
-        self.rodcctx = dc_join(server=host, creds=creds, lp=lp,
-            site=self.site, netbios_name=self.rodcname, targetdir=None,
-            domain=None)
-        self.dcctx = dc_join(server=host, creds=creds, lp=lp, site=self.site,
-                netbios_name=self.dcname, targetdir=None, domain=None)
+        self.rodcctx = DCJoinContext(server=host, creds=creds, lp=lp,
+                                     site=self.site, netbios_name=self.rodcname,
+                                     targetdir=None, domain=None)
+        self.dcctx = DCJoinContext(server=host, creds=creds, lp=lp,
+                                   site=self.site, netbios_name=self.dcname,
+                                   targetdir=None, domain=None)
         self.ldb_admin.newuser(self.test_user, self.user_pass)
         self.ldb_user1 = self.get_ldb_connection(self.test_user, self.user_pass)
         self.user_sid1 = self.sd_utils.get_object_sid(self.get_user_dn(self.test_user))
diff --git a/source4/torture/drs/python/repl_rodc.py b/source4/torture/drs/python/repl_rodc.py
index 1d84c99..a785932 100644
--- a/source4/torture/drs/python/repl_rodc.py
+++ b/source4/torture/drs/python/repl_rodc.py
@@ -33,7 +33,7 @@ import ldb
 from ldb import SCOPE_BASE
 
 from samba import WERRORError
-from samba.join import dc_join
+from samba.join import DCJoinContext
 from samba.dcerpc import drsuapi, misc, drsblobs, security
 from samba.drs_utils import drs_DsBind, drs_Replicate
 from samba.ndr import ndr_unpack, ndr_pack
@@ -101,9 +101,12 @@ class DrsRodcTestCase(drs_base.DrsBaseTestCase):
         self.computer_dn = "CN=%s,OU=Domain Controllers,%s" % (self.rodc_name, self.base_dn)
 
 
-        self.rodc_ctx = dc_join(server=self.ldb_dc1.host_dns_name(), creds=self.get_credentials(), lp=self.get_loadparm(),
-                                site=self.site, netbios_name=self.rodc_name,
-                                targetdir=None, domain=None, machinepass=self.rodc_pass)
+        self.rodc_ctx = DCJoinContext(server=self.ldb_dc1.host_dns_name(),
+                                      creds=self.get_credentials(),
+                                      lp=self.get_loadparm(), site=self.site,
+                                      netbios_name=self.rodc_name,
+                                      targetdir=None, domain=None,
+                                      machinepass=self.rodc_pass)
         self._create_rodc(self.rodc_ctx)
         self.rodc_ctx.create_tmp_samdb()
         self.tmp_samdb = self.rodc_ctx.tmp_samdb
@@ -459,9 +462,12 @@ class DrsRodcTestCase(drs_base.DrsBaseTestCase):
         """
         # Create a new identical RODC with just the first letter missing
         other_rodc_name = self.rodc_name[1:]
-        other_rodc_ctx = dc_join(server=self.ldb_dc1.host_dns_name(), creds=self.get_credentials(), lp=self.get_loadparm(),
-                                 site=self.site, netbios_name=other_rodc_name,
-                                 targetdir=None, domain=None, machinepass=self.rodc_pass)
+        other_rodc_ctx = DCJoinContext(server=self.ldb_dc1.host_dns_name(),
+                                       creds=self.get_credentials(),
+                                       lp=self.get_loadparm(), site=self.site,
+                                       netbios_name=other_rodc_name,
+                                       targetdir=None, domain=None,
+                                       machinepass=self.rodc_pass)
         self._create_rodc(other_rodc_ctx)
 
         other_rodc_creds = Credentials()
-- 
2.7.4


From 431f163e552fcb7d9a8bc1ac32033050a3efe890 Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Mon, 11 Jun 2018 16:33:19 +1200
Subject: [PATCH 05/10] join: Refactor clone_only case to simplify code

Currently for DC clones, we create a regular DCJoinContext, se a
'clone_only' flag, and then make lots of special checks for this flag
throughout the code. Instead, we can use inheritance to create a
DCCloneContext sub-class, and put the specialization there.

This means we can remove all the 'clone_only' checks from the code. The
only 2 methods that really differ are do_join() and join_finalize(), and
these don't share much code at all. (To avoid duplication, I split the
first part of do_join() into a new build_nc_lists() function, but this
is a pretty trivial code move).

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
 python/samba/join.py | 123 ++++++++++++++++++++++++++++++---------------------
 1 file changed, 73 insertions(+), 50 deletions(-)

diff --git a/python/samba/join.py b/python/samba/join.py
index 4bceb89..959c880 100644
--- a/python/samba/join.py
+++ b/python/samba/join.py
@@ -56,14 +56,12 @@ class DCJoinContext(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, clone_only=False,
+                 promote_existing=False,
                  plaintext_secrets=False, backend_store=None,
                  forced_local_samdb=None, full_nc_list=[]):
         if site is None:
             site = "Default-First-Site-Name"
 
-        ctx.clone_only=clone_only
-
         ctx.logger = logger
         ctx.creds = creds
         ctx.lp = lp
@@ -124,18 +122,7 @@ class DCJoinContext(object):
             ctx.acct_pass = samba.generate_random_machine_password(128, 255)
 
         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
-            ctx.rid_manager_dn = None
-
-            # Save this early
-            ctx.remote_dc_ntds_guid = ctx.samdb.get_ntds_GUID()
-        else:
+        if netbios_name:
             # work out the DNs of all the objects we will be adding
             ctx.myname = netbios_name
             ctx.samname = "%s$" % ctx.myname
@@ -1198,12 +1185,11 @@ class DCJoinContext(object):
         # 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
-        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)
+        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 and ctx.RODC:
+        if ctx.RODC:
             print("Setting RODC invocationId")
             ctx.local_samdb.set_invocation_id(str(ctx.invocation_id))
             ctx.local_samdb.set_opaque_integer("domainFunctionality",
@@ -1234,17 +1220,12 @@ class DCJoinContext(object):
         m.dn = ldb.Dn(ctx.local_samdb, '@ROOTDSE')
         m["isSynchronized"] = ldb.MessageElement("TRUE", ldb.FLAG_MOD_REPLACE, "isSynchronized")
 
-        # We want to appear to be the server we just cloned
-        if ctx.clone_only:
-            guid = ctx.remote_dc_ntds_guid
-        else:
-            guid = ctx.ntds_guid
-
+        guid = ctx.ntds_guid
         m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % str(guid),
                                                 ldb.FLAG_MOD_REPLACE, "dsServiceName")
         ctx.local_samdb.modify(m)
 
-        if ctx.clone_only or ctx.subdomain:
+        if ctx.subdomain:
             return
 
         secrets_ldb = Ldb(ctx.paths.secrets, session_info=system_session(), lp=ctx.lp)
@@ -1369,7 +1350,7 @@ class DCJoinContext(object):
         ctx.local_samdb.add(rec)
 
 
-    def do_join(ctx):
+    def build_nc_lists(ctx):
         # nc_list is the list of naming context (NC) for which we will
         # replicate in and send a updateRef command to the partner DC
 
@@ -1391,23 +1372,24 @@ class DCJoinContext(object):
                 ctx.full_nc_list += [ctx.domaindns_zone]
                 ctx.full_nc_list += [ctx.forestdns_zone]
 
-        if not ctx.clone_only:
-            if ctx.promote_existing:
-                ctx.promote_possible()
-            else:
-                ctx.cleanup_old_join()
+    def do_join(ctx):
+        ctx.build_nc_lists()
+
+        if ctx.promote_existing:
+            ctx.promote_possible()
+        else:
+            ctx.cleanup_old_join()
 
         try:
-            if not ctx.clone_only:
-                ctx.join_add_objects()
+            ctx.join_add_objects()
             ctx.join_provision()
             ctx.join_replicate()
-            if (not ctx.clone_only and ctx.subdomain):
+            if ctx.subdomain:
                 ctx.join_add_objects2()
                 ctx.join_provision_own_domain()
                 ctx.join_setup_trusts()
 
-            if not ctx.clone_only and ctx.dns_backend != "NONE":
+            if ctx.dns_backend != "NONE":
                 ctx.join_add_dns_records()
                 ctx.join_replicate_new_dns_records()
 
@@ -1417,8 +1399,7 @@ class DCJoinContext(object):
                 print("Join failed - cleaning up")
             except IOError:
                 pass
-            if not ctx.clone_only:
-                ctx.cleanup_old_join()
+            ctx.cleanup_old_join()
             raise
 
 
@@ -1509,11 +1490,10 @@ def join_DC(logger=None, server=None, creds=None, lp=None, site=None, netbios_na
 
 def join_clone(logger=None, server=None, creds=None, lp=None,
                targetdir=None, domain=None, include_secrets=False, dns_backend="NONE"):
-    """Join as a DC."""
-    ctx = DCJoinContext(logger, server, creds, lp, site=None, netbios_name=None,
-                        targetdir=targetdir, domain=domain, machinepass=None,
-                        use_ntvfs=False, dns_backend=dns_backend,
-                        promote_existing=False, clone_only=True)
+    """Creates a local clone of a remote DC."""
+    ctx = DCCloneContext(logger, server, creds, lp, targetdir=targetdir,
+                         domain=domain, dns_backend=dns_backend,
+                         include_secrets=include_secrets)
 
     lp.set("workgroup", ctx.domain_name)
     logger.info("workgroup is %s" % ctx.domain_name)
@@ -1521,12 +1501,6 @@ def join_clone(logger=None, server=None, creds=None, lp=None,
     lp.set("realm", ctx.realm)
     logger.info("realm is %s" % ctx.realm)
 
-    ctx.replica_flags |= (drsuapi.DRSUAPI_DRS_WRIT_REP |
-                          drsuapi.DRSUAPI_DRS_FULL_SYNC_IN_PROGRESS)
-    if not include_secrets:
-        ctx.replica_flags |= drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING
-    ctx.domain_replica_flags = ctx.replica_flags
-
     ctx.do_join()
     logger.info("Cloned domain %s (SID %s)" % (ctx.domain_name, ctx.domsid))
 
@@ -1583,3 +1557,52 @@ def join_subdomain(logger=None, server=None, creds=None, lp=None, site=None,
 
     ctx.do_join()
     ctx.logger.info("Created domain %s (SID %s) as a DC" % (ctx.domain_name, ctx.domsid))
+
+
+class DCCloneContext(DCJoinContext):
+    """Clones a remote DC."""
+
+    def __init__(ctx, logger=None, server=None, creds=None, lp=None,
+                 targetdir=None, domain=None, dns_backend=None,
+                 include_secrets=False):
+        super(DCCloneContext, ctx).__init__(logger, server, creds, lp,
+                                            targetdir=targetdir, domain=domain,
+                                            dns_backend=dns_backend)
+
+        # 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
+        ctx.rid_manager_dn = None
+
+        # Save this early
+        ctx.remote_dc_ntds_guid = ctx.samdb.get_ntds_GUID()
+
+        ctx.replica_flags |= (drsuapi.DRSUAPI_DRS_WRIT_REP |
+                              drsuapi.DRSUAPI_DRS_FULL_SYNC_IN_PROGRESS)
+        if not include_secrets:
+            ctx.replica_flags |= drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING
+        ctx.domain_replica_flags = ctx.replica_flags
+
+    def join_finalise(ctx):
+        ctx.logger.info("Setting isSynchronized and dsServiceName")
+        m = ldb.Message()
+        m.dn = ldb.Dn(ctx.local_samdb, '@ROOTDSE')
+        m["isSynchronized"] = ldb.MessageElement("TRUE", ldb.FLAG_MOD_REPLACE, "isSynchronized")
+
+        # We want to appear to be the server we just cloned
+        guid = ctx.remote_dc_ntds_guid
+        m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % str(guid),
+                                                ldb.FLAG_MOD_REPLACE, "dsServiceName")
+        ctx.local_samdb.modify(m)
+
+    def do_join(ctx):
+        ctx.build_nc_lists()
+
+        # When cloning a DC, we just want to provision a DC locally, then
+        # grab the remote DC's entire DB via DRS replication
+        ctx.join_provision()
+        ctx.join_replicate()
+        ctx.join_finalise()
-- 
2.7.4


From c120f5fced3ba0c1bea7c0dced97474d556434b3 Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Mon, 11 Jun 2018 16:50:28 +1200
Subject: [PATCH 06/10] join: Add more framework for renaming a domain

Add a DCCloneContext subclass which will rename the DB objects as they
get cloned. This uses the drs_ReplicateRenamer class added to drs_utils
in an earlier patch. Where the drs_Replicate object currently gets
created has been split out into a simple new function, which we can then
override in the rename case.

The other important difference is overriding the provision step, so that
we use the new domain-DN/realm when setting up the initial SAM DB (and
smb.conf, secrets.ldb, etc).

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
 python/samba/join.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 78 insertions(+), 3 deletions(-)

diff --git a/python/samba/join.py b/python/samba/join.py
index 959c880..9898af4 100644
--- a/python/samba/join.py
+++ b/python/samba/join.py
@@ -43,6 +43,8 @@ import logging
 import talloc
 import random
 import time
+import re
+import os
 
 class DCJoinException(Exception):
 
@@ -899,6 +901,12 @@ class DCJoinContext(object):
                                  dns_backend=ctx.dns_backend, adminpass=ctx.adminpass)
         print("Provision OK for domain %s" % ctx.names.dnsdomain)
 
+    def create_replicator(ctx, repl_creds, binding_options):
+        '''Creates a new DRS object for managing replications'''
+        return drs_utils.drs_Replicate(
+                "ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options),
+                ctx.lp, repl_creds, ctx.local_samdb, ctx.invocation_id)
+
     def join_replicate(ctx):
         """Replicate the SAM."""
 
@@ -924,9 +932,8 @@ class DCJoinContext(object):
             binding_options = "seal"
             if ctx.lp.log_level() >= 9:
                 binding_options += ",print"
-            repl = drs_utils.drs_Replicate(
-                "ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options),
-                ctx.lp, repl_creds, ctx.local_samdb, ctx.invocation_id)
+
+            repl = ctx.create_replicator(repl_creds, binding_options)
 
             repl.replicate(ctx.schema_dn, source_dsa_invocation_id,
                     destination_dsa_guid, schema=True, rodc=ctx.RODC,
@@ -1606,3 +1613,71 @@ class DCCloneContext(DCJoinContext):
         ctx.join_provision()
         ctx.join_replicate()
         ctx.join_finalise()
+
+# Used to create a renamed backup of a DC. Renaming the domain means that the
+# cloned/backup DC can be started without interfering with the production DC.
+class DCCloneAndRenameContext(DCCloneContext):
+    """Clones a remote DC, renaming the domain along the way."""
+
+    def __init__(ctx, new_base_dn, new_domain_name, new_realm, logger=None,
+                 server=None, creds=None, lp=None, targetdir=None, domain=None,
+                 dns_backend=None, include_secrets=True):
+        super(DCCloneAndRenameContext, ctx).__init__(logger, server, creds, lp,
+                                                     targetdir=targetdir,
+                                                     domain=domain,
+                                                     dns_backend=dns_backend,
+                                                     include_secrets=include_secrets)
+        # store the new DN (etc) that we want the cloned DB to use
+        ctx.new_base_dn = new_base_dn
+        ctx.new_domain_name = new_domain_name
+        ctx.new_realm = new_realm
+
+    def create_replicator(ctx, repl_creds, binding_options):
+        """Creates a new DRS object for managing replications"""
+
+        # We want to rename all the domain objects, and the simplest way to do
+        # this is during replication. This is because the base DN of the top-
+        # level replicated object will flow through to all the objects below it
+        return drs_utils.drs_ReplicateRenamer(
+                    "ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options),
+                    ctx.lp, repl_creds, ctx.local_samdb, ctx.invocation_id,
+                    ctx.base_dn, ctx.new_base_dn)
+
+    def rename_dn(ctx, dn_str):
+        '''Uses string substitution to replace the base DN'''
+        old_base_dn = ctx.base_dn
+        return re.sub('%s$' % old_base_dn, ctx.new_base_dn, dn_str)
+
+    # we want to override the normal DCCloneContext's join_provision() so that
+    # use the new domain DNs during the provision. We do this because:
+    # - it sets up smb.conf/secrets.ldb with the new realm/workgroup values
+    # - it sets up a default SAM DB that uses the new Schema DNs (without which
+    #   we couldn't apply the renamed DRS objects during replication)
+    def join_provision(ctx):
+        """Provision the local (renamed) SAM."""
+
+        print("Provisioning the new (renamed) domain...")
+
+        # the provision will call make_smbconf() to create a new smb.conf,
+        # however, this uses lp.dump() to rewrite smb.conf. By default it uses
+        # the global lp context, which still holds the old domain details. So
+        # we need to pass in a non-global lp here, so that smb.conf is actually
+        # generated with the new realm/domain values we pass in
+        smbconf = os.path.join(ctx.targetdir, "etc", "smb.conf")
+        blank_lp = samba.param.LoadParm(_filename_for_non_global_lp = smbconf)
+
+        # do the provision with the new/renamed domain DN values
+        presult = provision(ctx.logger, system_session(),
+                targetdir=ctx.targetdir, samdb_fill=FILL_DRS,
+                realm=ctx.new_realm,
+                rootdn=ctx.rename_dn(ctx.root_dn), domaindn=ctx.new_base_dn,
+                schemadn=ctx.rename_dn(ctx.schema_dn),
+                configdn=ctx.rename_dn(ctx.config_dn),
+                domain=ctx.new_domain_name,
+                domainsid=ctx.domsid,
+                serverrole="active directory domain controller", lp=blank_lp,
+                dns_backend=ctx.dns_backend)
+
+        print("Provision OK for renamed domain DN %s" % presult.domaindn)
+        ctx.local_samdb = presult.samdb
+        ctx.paths       = presult.paths
-- 
2.7.4


From e752d06b010cd695526d06c850e87c045ef27646 Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Wed, 13 Jun 2018 12:22:19 +1200
Subject: [PATCH 07/10] netcmd: Add 'samba-tool domain backup rename' command

Add a new command that takes a clone of the domain's DB, and renames the
domain as well. (We rename the domain during the clone because it's
easier to implement - the DRS code handles most of the renaming for us,
as it applies the received replication chunks).

The new option is similar to an online backup, except we also do the
following:
- use the new DCCloneAndRenameContext code to clone the DB
- run dbcheck to fix up any residual old DNs (mostly objectCategory
  references)
- rename the domain's netBIOSName
- add dnsRoot objects for the new DNS realm
- by default, remove the old realm's DNS objects (optional)
- add an extra backupRename marker to the backed-up DB. In the restore
  code, if the backup was renamed, then we need to register the new
  domain's DNS zone at that point (we only know the new DC's host IP
  at the restore stage).

Note that the backup will contain the old DC entries that still use the
old dnsHostname, but these DC entries will all be removed during the
restore, and a new DC will be added with the correct dnsHostname.

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
 docs-xml/manpages/samba-tool.8.xml   |   5 +
 python/samba/netcmd/domain_backup.py | 211 ++++++++++++++++++++++++++++++++++-
 2 files changed, 214 insertions(+), 2 deletions(-)

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index 71e2ecf..b488f1f 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -307,6 +307,11 @@
 </refsect3>
 
 <refsect3>
+	<title>domain backup rename</title>
+	<para>Copy a running DC's DB to backup file, renaming the domain in the process.</para>
+</refsect3>
+
+<refsect3>
 	<title>domain backup restore</title>
 	<para>Restore the domain's DB from a backup-file.</para>
 </refsect3>
diff --git a/python/samba/netcmd/domain_backup.py b/python/samba/netcmd/domain_backup.py
index 72db0a2..7489063 100644
--- a/python/samba/netcmd/domain_backup.py
+++ b/python/samba/netcmd/domain_backup.py
@@ -27,7 +27,9 @@ import ldb
 from samba.auth import system_session
 from samba.upgradehelpers import update_krbtgt_account_password
 from samba.emulate.traffic import create_machine_account
-from samba.join import DCJoinContext, join_clone
+from samba.join import DCJoinContext, join_clone, DCCloneAndRenameContext
+from samba.dbchecker import dbcheck
+import re
 from samba.dcerpc.security import dom_sid
 from samba.netcmd import Option, CommandError
 from samba.dcerpc import misc
@@ -580,8 +582,213 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
         for item in os.listdir(full_tmpdir_path):
             shutil.move(os.path.join(full_tmpdir_path, item), targetdir)
 
+        logger.info("Backup file successfully restored to %s" % targetdir)
+        logger.info("Check the smb.conf settings are correct before starting samba.")
+        if not is_rename:
+            logger.warning("Also ensure all old DCs for the domain are stopped.")
+
+class cmd_domain_backup_rename(samba.netcmd.Command):
+    '''Copy a running DC's DB to backup file, renaming the domain in the process.
+
+    Where <new-domain> is the NetBIOS name of the new domain, and <new-dnsrealm>
+    is the new domain's realm in DNS form.
+
+    This is similar to 'samba-tool backup online' in that it clones the DB of a
+    running DC. However, this option also renames all the domain entries in the
+    DB. Renaming the domain makes it possible to restore and start a new Samba
+    DC without it interfering with the existing Samba domain. In other words,
+    you could use this option to clone your production samba domain and restore
+    it to a separate pre-production environment that won't overlap or interfere
+    with the existing production Samba domain.
+
+    Note that:
+    - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
+      and fix any errors it reports.
+    - all the domain's secrets are included in the backup file.
+    - although the DB contents can be untarred and examined manually, you need
+      to run 'samba-tool domain backup restore' before you can start a Samba DC
+      from the backup file.
+    - GPO and sysvol information will still refer to the old realm and will need
+      to be updated manually.
+    - if you use the 'keep-dns-realm' option, the DNS records will need updating
+      in order to work (they will still refer to the old DC's IP instead of the
+      new DC's address).
+    - we recommend that you only use this option if you know what you're doing.'''
+
+    synopsis = "%prog <new-domain> <new-dnsrealm> --server=<DC-to-backup> --targetdir=<output-dir>"
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+    }
+
+    takes_options = [
+        Option("--server", help="The DC to backup", type=str),
+        Option("--targetdir", help="Directory to write the backup file",
+               type=str),
+        Option("--keep-dns-realm", action="store_true", default=False,
+               help="Retain the DNS entries for the old realm in the backup"),
+       ]
+
+    takes_args = ["new_domain_name", "new_dns_realm"]
+
+    def update_dns_root(self, logger, samdb, old_realm, delete_old_dns):
+        '''Updates the dnsRoot for the partition objects to reflect the rename'''
+
+        # lookup the crossRef objects that hold the old realm's dnsRoot
+        base_dn = samdb.get_default_basedn()
+        partitions_dn = samdb.get_partitions_dn()
+        res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
+                           attrs=["dnsRoot"],
+                           expression='(&(objectClass=crossRef)(dnsRoot=*))')
+        new_realm = samdb.domain_dns_name()
+
+        # go through and add the new realm
+        for res_msg in res:
+            # dnsRoot can be multi-valued, so only look for the old realm
+            for dns_root in res_msg["dnsRoot"]:
+                dn = res_msg.dn
+                if old_realm in dns_root:
+                    new_dns_root = re.sub('%s$' % old_realm, new_realm, dns_root)
+                    logger.info("Adding %s dnsRoot to %s" %(new_dns_root, dn))
+
+                    m = ldb.Message()
+                    m.dn = dn
+                    m["dnsRoot"] = ldb.MessageElement(new_dns_root,
+                                                      ldb.FLAG_MOD_ADD, "dnsRoot")
+                    samdb.modify(m)
+
+                    # optionally remove the dnsRoot for the old realm
+                    if delete_old_dns:
+                        logger.info("Removing %s dnsRoot from %s" %(dns_root, dn))
+                        m["dnsRoot"] = ldb.MessageElement(dns_root,
+                                                          ldb.FLAG_MOD_DELETE,
+                                                          "dnsRoot")
+                        samdb.modify(m)
+
+    # Updates the CN=<domain>,CN=Partitions,CN=Configuration,... object to reflect
+    # the domain rename
+    def rename_domain_partition(self, logger, samdb, new_netbios_name):
+        '''Renames the domain parition object and updates its nETBIOSName'''
+
+        # lookup the crossRef object that holds the nETBIOSName
+        # (nCName has already been updated by this point, but the netBIOS hasn't)
+        base_dn = samdb.get_default_basedn()
+        partitions_dn = samdb.get_partitions_dn()
+        res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
+                           attrs=["nETBIOSName"],
+                           expression='ncName=%s' % ldb.binary_encode(str(base_dn)))
+
+        logger.info("Changing backup domain's NetBIOS name to %s" % new_netbios_name)
+        m = ldb.Message()
+        m.dn = res[0].dn
+        m["nETBIOSName"] = ldb.MessageElement(new_netbios_name,
+                                              ldb.FLAG_MOD_REPLACE, "nETBIOSName")
+        samdb.modify(m)
+
+        # renames the object itself to reflect the change in domain
+        new_dn = "CN=%s,%s" %(new_netbios_name, partitions_dn)
+        logger.info("Renaming %s --> %s" % (res[0].dn, new_dn))
+        samdb.rename(res[0].dn, new_dn, controls=['relax:0'])
+
+    def delete_old_dns_zones(self, logger, samdb, old_realm):
+        # remove the top-level DNS entries for the old realm
+        basedn = samdb.get_default_basedn()
+        dn = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" %(old_realm, basedn)
+        logger.info("Deleting old DNS zone %s" % dn)
+        samdb.delete(dn, ["tree_delete:1"])
+
+        forestdn = samdb.get_root_basedn().get_linearized()
+        dn = "DC=_msdcs.%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" %(old_realm,
+                                                                   forestdn)
+        logger.info("Deleting old DNS zone %s" % dn)
+        samdb.delete(dn, ["tree_delete:1"])
+
+    def fix_old_dn_attributes(self, samdb):
+        '''Fixes attributes (i.e. objectCategory) that still use the old DN'''
+
+        samdb.transaction_start()
+        # Just fix any mismatches in DN detected (leave any other errors)
+        chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
+                      in_transaction=True)
+        # fix up incorrect objectCategory/etc attributes
+        setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
+        cross_ncs_ctrl = "search_options:1:2"
+        error_count = chk.check_database(controls=[cross_ncs_ctrl])
+        samdb.transaction_commit()
+
+    @using_tmp_dir
+    def run(self, new_domain_name, new_dns_realm, sambaopts=None, credopts=None,
+            server=None, targetdir=None, keep_dns_realm=False):
+        logger = self.get_logger()
+        logger.setLevel(logging.INFO)
+
+        # Make sure we have all the required args.
+        server = server or os.environ.get('SERVER', None)
+        check_online_backup_args(logger, credopts, server, targetdir)
+        delete_old_dns = not keep_dns_realm
+
+        new_dns_realm = new_dns_realm.lower()
+        new_domain_name = new_domain_name.upper()
+
+        new_base_dn = samba.dn_from_dns_name(new_dns_realm)
+        logger.info("New realm for backed up domain: %s" % new_dns_realm)
+        logger.info("New base DN for backed up domain: %s" % new_base_dn)
+        logger.info("New domain NetBIOS name: %s" % new_domain_name)
+
+        # Clone and rename the remote server
+        remote_lp = sambaopts.get_loadparm()
+        remote_creds = credopts.get_credentials(remote_lp)
+        ctx = DCCloneAndRenameContext(new_base_dn, new_domain_name,
+                                      new_dns_realm, logger=logger,
+                                      creds=remote_creds, lp=remote_lp,
+                                      include_secrets=True,
+                                      dns_backend='SAMBA_INTERNAL',
+                                      server=server,
+                                      targetdir=tmpdir)
+        ctx.do_join()
+
+        # drop the local SAM DB connection and reload it
+        del ctx.local_samdb
+        paths = ctx.paths
+        local_lp = samba.param.LoadParm(_filename_for_non_global_lp = paths.smbconf)
+        samdb = SamDB(url=ctx.paths.samdb, session_info=system_session(),
+                      lp=local_lp)
+
+        # get a free RID to use as the new DC's SID (when it gets restored)
+        remote_sam = SamDB(url='ldap://'+server, session_info=system_session(),
+                           credentials=remote_creds, lp=remote_lp)
+        new_sid = get_sid_for_restore(remote_sam)
+        old_realm = remote_sam.domain_dns_name()
+
+        # Edit the cloned sam.ldb to mark it as a backup
+        time_str = get_timestamp()
+        add_backup_marker(samdb, "backupDate", time_str)
+        add_backup_marker(samdb, "sidForRestore", new_sid)
+        add_backup_marker(samdb, "backupRename", old_realm)
+
+        # fix up the DNS objects that are using the old dnsRoot value
+        self.update_dns_root(logger, samdb, old_realm, delete_old_dns)
+
+        # update the netBIOS name and the Partition object for the domain
+        self.rename_domain_partition(logger, samdb, new_domain_name)
+
+        if delete_old_dns:
+            self.delete_old_dns_zones(logger, samdb, old_realm)
+
+        logger.info("Fixing DN attributes after rename...")
+        self.fix_old_dn_attributes(samdb)
+
+        # extract the remote DC's sysvol files into our tmp backup-dir
+        full_sysvol_path = paths.sysvol
+        copy_sysvol_files(server, remote_creds, full_sysvol_path)
+
+        # Add everything in the tmpdir to the backup tar file
+        backup_file = backup_filepath(targetdir, new_dns_realm, time_str)
+        create_backup_tar(logger, tmpdir, backup_file)
+
 class cmd_domain_backup(samba.netcmd.SuperCommand):
-    '''Domain backup'''
+    '''Create or restore a backup of the domain.'''
     subcommands = {'online': cmd_domain_backup_online(),
                    'offline': cmd_domain_backup_offline(),
+                   'rename': cmd_domain_backup_rename(),
                    'restore': cmd_domain_backup_restore()}
-- 
2.7.4


From 0f18824ef586b89a01d1cf4e80a594a1255bc69e Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Mon, 11 Jun 2018 11:18:09 +1200
Subject: [PATCH 08/10] netcmd: Extend 'backup restore' command to handle
 renamed domains

When restoring a renamed domain backup, we need to register the new
realm's DNS zone. We do this in the restore step because we don't know
the new server's IP/hostname in the backup step.

Because we may have removed the old realm's DNS entries in the rename
step, the remove_dc() code may fail to find the expected DNS entries for
the DC's domain (the DCs' dnsHostname still maps to the old DNS realm).
We just needed to adjust remove_dns_references() as it was getting a
slightly different error code.

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
 python/samba/netcmd/domain_backup.py | 58 ++++++++++++++++++++++++++++++++++--
 python/samba/provision/sambadns.py   |  5 ++--
 python/samba/remove_dc.py            |  3 +-
 3 files changed, 60 insertions(+), 6 deletions(-)

diff --git a/python/samba/netcmd/domain_backup.py b/python/samba/netcmd/domain_backup.py
index 7489063..af74036 100644
--- a/python/samba/netcmd/domain_backup.py
+++ b/python/samba/netcmd/domain_backup.py
@@ -24,6 +24,11 @@ from fsmo import cmd_fsmo_seize
 import samba.getopt as options
 from samba.samdb import SamDB
 import ldb
+from samba.provision import guess_names, determine_host_ip, determine_host_ip6
+from samba.provision.sambadns import (fill_dns_data_partitions,
+                                      get_dnsadmins_sid,
+                                      get_domainguid)
+from samba.remove_dc import remove_dc
 from samba.auth import system_session
 from samba.upgradehelpers import update_krbtgt_account_password
 from samba.emulate.traffic import create_machine_account
@@ -442,6 +447,10 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
         Option("--backup-file", help="Path to backup file", type=str),
         Option("--targetdir", help="Path to write to", type=str),
         Option("--newservername", help="Name for new server", type=str),
+        Option("--host-ip", type="string", metavar="IPADDRESS",
+               help="set IPv4 ipaddress"),
+        Option("--host-ip6", type="string", metavar="IP6ADDRESS",
+               help="set IPv6 ipaddress"),
     ]
 
     takes_optiongroups = {
@@ -449,9 +458,41 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
         "credopts": options.CredentialsOptions,
     }
 
+    def register_dns_zone(self, logger, samdb, lp, ntdsguid, host_ip, host_ip6):
+        '''
+        Registers the new realm's DNS objects when a renamed domain backup
+        is restored.
+        '''
+        names = guess_names(lp)
+        domaindn = names.domaindn
+        forestdn = samdb.get_root_basedn().get_linearized()
+        dnsdomain = names.dnsdomain.lower()
+        dnsforest = dnsdomain
+        hostname = names.netbiosname.lower()
+        domainsid = dom_sid(samdb.get_domain_sid())
+        dnsadmins_sid = get_dnsadmins_sid(samdb, domaindn)
+        domainguid = get_domainguid(samdb, domaindn)
+
+        # work out the IP address to use for the new DC's DNS records
+        host_ip = determine_host_ip(logger, lp, host_ip)
+        host_ip6 = determine_host_ip6(logger, lp, host_ip6)
+
+        if host_ip is None and host_ip6 is None:
+            raise CommandError('Please specify a host-ip address for the new server')
+
+        logger.info("DNS realm was renamed to %s" % dnsdomain)
+        logger.info("Populating DNS partitions for new realm...")
+
+        # Add the DNS objects for the new realm (note: the backup clone already
+        # has the root server objects, so don't add them again)
+        fill_dns_data_partitions(samdb, domainsid, names.sitename, domaindn,
+                                 forestdn, dnsdomain, dnsforest, hostname,
+                                 host_ip, host_ip6, domainguid, ntdsguid,
+                                 dnsadmins_sid, add_root=False)
+
     @using_tmp_dir
     def run(self, sambaopts=None, credopts=None, backup_file=None,
-            targetdir=None, newservername=None):
+            targetdir=None, newservername=None, host_ip=None, host_ip6=None):
         if not (backup_file and os.path.exists(backup_file)):
             raise CommandError('Backup file not found.')
         if targetdir is None:
@@ -514,7 +555,8 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
         # Finally, get the SID saved by the backup process and create account
         res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
                            scope=ldb.SCOPE_BASE,
-                           attrs=['sidForRestore'])
+                           attrs=['sidForRestore', 'backupRename'])
+        is_rename = True if 'backupRename' in res[0] else False
         sid = res[0].get('sidForRestore')[0]
         logger.info('creating account with SID: ' + str(sid))
         ctx.join_add_objects(specified_sid=dom_sid(sid))
@@ -525,6 +567,13 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
                                                 ldb.FLAG_MOD_REPLACE, "dsServiceName")
         samdb.modify(m)
 
+        # if we renamed the backed-up domain, then we need to add the DNS
+        # objects for the new realm (we do this in the restore, now that we
+        # know the new DC's IP address)
+        if is_rename:
+            self.register_dns_zone(logger, samdb, lp, ctx.ntds_guid,
+                                   host_ip, host_ip6)
+
         secrets_ldb = Ldb(p.secrets, session_info=system_session(), lp=lp)
         samba.provision.secretsdb_self_join(secrets_ldb, domain=ctx.domain_name,
                                             realm=ctx.realm,
@@ -562,7 +611,7 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
         for m in res:
             cn = m.get('cn')[0]
             if cn != newservername:
-                samba.remove_dc.remove_dc(samdb, logger, cn)
+                remove_dc(samdb, logger, cn)
 
         # Update tgt and DC passwords twice
         update_krbtgt_account_password(samdb)
@@ -575,6 +624,9 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
                                              "backupDate")
         m["sidForRestore"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
                                                 "sidForRestore")
+        if is_rename:
+            m["backupRename"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
+                                                    "backupRename")
         samdb.modify(m)
 
         # Fix paths again and move files to targetdir
diff --git a/python/samba/provision/sambadns.py b/python/samba/provision/sambadns.py
index 63ebff0..e2b6fcd 100644
--- a/python/samba/provision/sambadns.py
+++ b/python/samba/provision/sambadns.py
@@ -1039,7 +1039,7 @@ def create_dns_partitions(samdb, domainsid, names, domaindn, forestdn,
 def fill_dns_data_partitions(samdb, domainsid, site, domaindn, forestdn,
                              dnsdomain, dnsforest, hostname, hostip, hostip6,
                              domainguid, ntdsguid, dnsadmins_sid, autofill=True,
-                             fill_level=FILL_FULL):
+                             fill_level=FILL_FULL, add_root=True):
     """Fill data in various AD partitions
 
     :param samdb: LDB object connected to sam.ldb file
@@ -1060,7 +1060,8 @@ def fill_dns_data_partitions(samdb, domainsid, site, domaindn, forestdn,
 
     ##### Set up DC=DomainDnsZones,<DOMAINDN>
     # Add rootserver records
-    add_rootservers(samdb, domaindn, "DC=DomainDnsZones")
+    if add_root:
+        add_rootservers(samdb, domaindn, "DC=DomainDnsZones")
 
     # Add domain record
     add_domain_record(samdb, domaindn, "DC=DomainDnsZones", dnsdomain,
diff --git a/python/samba/remove_dc.py b/python/samba/remove_dc.py
index d190461..e6513ae 100644
--- a/python/samba/remove_dc.py
+++ b/python/samba/remove_dc.py
@@ -102,7 +102,8 @@ def remove_dns_references(samdb, logger, dnsHostName, ignore_no_name=False):
         (dn, primary_recs) = samdb.dns_lookup(dnsHostName)
     except RuntimeError as e4:
         (enum, estr) = e4.args
-        if enum == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
+        if (enum == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST or
+            enum == werror.WERR_DNS_ERROR_RCODE_NAME_ERROR):
             if ignore_no_name:
                 remove_hanging_dns_references(samdb, logger,
                                               dnsHostNameUpper,
-- 
2.7.4


From 05afc424f56bd5442cc0f7ef9d965f5a37b656e5 Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Wed, 13 Jun 2018 14:09:06 +1200
Subject: [PATCH 09/10] drs_utils: Always set the GET_TGT flag for clone
 renames

The DCCloneAndRenameContext replication was a little inefficient, in
that it would essentially replicate the entire DB twice. This was due to
resolving the link targets - it finds a target object it doesn't know
about, so retries the entire replication again with the GET_TGT flag set
this time.

Normally, the repl_meta_data code will use the target object's GUID,
however, it can't do this for cross-partition links (if it hasn't
replicated the target partition yet). The repl_md code can normally
detect that the link is a cross-parition link by checking the base-DN,
however, this doesn't work in the DCCloneAndRenameContext case because
we have renamed the base-DN.

This is not a big deal - it just means extra work. However, because the
domains being backed up could potentially be quite large, it probably
makes sense to just always set the GET_TGT in the rename case and skip
this extra work.

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
 python/samba/drs_utils.py | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/python/samba/drs_utils.py b/python/samba/drs_utils.py
index 4e025a1..82eb7ab 100644
--- a/python/samba/drs_utils.py
+++ b/python/samba/drs_utils.py
@@ -200,6 +200,7 @@ class drs_Replicate(object):
         if invocation_id == misc.GUID("00000000-0000-0000-0000-000000000000"):
             raise RuntimeError("Must not set GUID 00000000-0000-0000-0000-000000000000 as invocation_id")
         self.replication_state = self.net.replicate_init(self.samdb, lp, self.drs, invocation_id)
+        self.more_flags = 0
 
     def _should_retry_with_get_tgt(self, error_code, req):
 
@@ -228,7 +229,7 @@ class drs_Replicate(object):
         # setup for a GetNCChanges call
         if self.supported_extensions & drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V10:
             req = drsuapi.DsGetNCChangesRequest10()
-            req.more_flags = more_flags
+            req.more_flags = (more_flags | self.more_flags)
             req_level = 10
         else:
             req_level = 8
@@ -366,6 +367,12 @@ class drs_ReplicateRenamer(drs_Replicate):
         self.old_base_dn = old_base_dn
         self.new_base_dn = new_base_dn
 
+        # because we're renaming the DNs, we know we're going to have trouble
+        # resolving link targets. Normally we'd get to the end of replication
+        # only to find we need to retry the whole replication with the GET_TGT
+        # flag set. Always setting the GET_TGT flag avoids this extra work.
+        self.more_flags = drsuapi.DRSUAPI_DRS_GET_TGT
+
     def rename_dn(self, dn_str):
         '''Uses string substitution to replace the base DN'''
         return re.sub('%s$' % self.old_base_dn, self.new_base_dn, dn_str)
-- 
2.7.4


From 8cf2201bf040b672ef51bef42bc2c5b1165cfeb6 Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Mon, 11 Jun 2018 11:02:11 +1200
Subject: [PATCH 10/10] selftest: Add dedicated RENAMEDC testenv for 'backup
 rename'

Add a new testenv that's similar to the existing restoredc, except we
use 'backup rename' to rename the domain as we back it up.

Restoring this backup then proves that a valid DC can be started from a
renamed backup.

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
 selftest/target/Samba.pm  |  1 +
 selftest/target/Samba4.pm | 45 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+)

diff --git a/selftest/target/Samba.pm b/selftest/target/Samba.pm
index 9c79345..3498567 100644
--- a/selftest/target/Samba.pm
+++ b/selftest/target/Samba.pm
@@ -409,6 +409,7 @@ sub get_interface($)
     $interfaces{"vampire2000dc"} = 39;
     $interfaces{"backupfromdc"} = 40;
     $interfaces{"restoredc"} = 41;
+    $interfaces{"renamedc"} = 42;
 
     # update lib/socket_wrapper/socket_wrapper.c
     #  #define MAX_WRAPPED_INTERFACES 64
diff --git a/selftest/target/Samba4.pm b/selftest/target/Samba4.pm
index 54a031a..57ab82d 100755
--- a/selftest/target/Samba4.pm
+++ b/selftest/target/Samba4.pm
@@ -2161,6 +2161,7 @@ sub check_env($$)
 	s4member             => ["ad_dc_ntvfs"],
 
 	restoredc            => ["backupfromdc"],
+	renamedc             => ["backupfromdc"],
 
 	none                 => [],
 );
@@ -2773,6 +2774,50 @@ sub setup_restoredc
 	return $env;
 }
 
+# Set up a DC testenv solely by using the 'samba-tool domain backup rename' and
+# restore commands. This proves that we can backup and rename an online DC
+# ('backupfromdc') and use the backup file to create a valid, working samba DC.
+sub setup_renamedc
+{
+	# note: dcvars contains the env info for the dependent testenv ('backupfromdc')
+	my ($self, $prefix, $dcvars) = @_;
+	print "Preparing RENAME DC...\n";
+
+	my $env = $self->prepare_dc_testenv($prefix, "renamedc",
+						"RENAMEDOMAIN", "renamedom.samba.example.com",
+						$dcvars->{PASSWORD});
+
+	# create a backup of the 'backupfromdc' which renames the domain
+	my $backupdir = File::Temp->newdir();
+	my $backup_args = "rename $env->{DOMAIN} $env->{REALM}";
+	my $backup_file = $self->create_backup($env, $dcvars, $backupdir,
+							$backup_args);
+	unless($backup_file) {
+		return undef;
+	}
+
+	# restore the backup file to populate the rename-DC testenv
+	my $restore_dir = abs_path($prefix);
+	my $restore_opts =  "--newservername=$env->{SERVER} --host-ip=$env->{SERVER_IP}";
+	my $ret = $self->restore_backup_file($backup_file, $restore_opts,
+							$restore_dir);
+	unless ($ret == 0) {
+		return undef;
+	}
+
+	# start samba for the restored DC
+	if (not defined($self->check_or_start($env, "standard"))) {
+	    return undef;
+	}
+
+	my $upn_array = ["$env->{REALM}.upn"];
+	my $spn_array = ["$env->{REALM}.spn"];
+
+	$self->setup_namespaces($env, $upn_array, $spn_array);
+
+	return $env;
+}
+
 sub setup_none
 {
 	my ($self, $path) = @_;
-- 
2.7.4



More information about the samba-technical mailing list