[PATCH] domain rename tool

Tim Beale timbeale at catalyst.net.nz
Wed Jul 4 05:20:05 UTC 2018


Hi,

Updated changes attached. I've split the changes into two patch-sets, in
the hope it's a bit easier to digest that way.

patch-set1: This just lays more groundwork for the rename tool. These
changes are identical to what I sent out yesterday.
CI link: https://gitlab.com/catalyst-samba/samba/pipelines/25070802

patch-set2: This adds the rename tool. Since yesterday's patch, I've
changed:
- #1 - minor change to add 'show_deleted:1' to the dbcheck controls in
fix_old_dn_attributes()
- #4 - added a new test_one_way_links() test-case that adds some basic
objects/links and checks they get renamed correctly.
- #5 - new patch to run dbcheck over the restored DB (I noticed that
removing the old DCs introduces a couple of dbcheck errors)
CI link: https://gitlab.com/catalyst-samba/samba/pipelines/25082629

Review appreciated.

Thanks,
Tim
-------------- next part --------------
From fba746754a687cac1523c23ee10a57decd2fb142 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 1/5] 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 | 218 ++++++++++++++++++++++++++++++++++-
 2 files changed, 222 insertions(+), 1 deletion(-)

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index b8038bc..7f000e9 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -302,6 +302,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 c706c3c..24407a4 100644
--- a/python/samba/netcmd/domain_backup.py
+++ b/python/samba/netcmd/domain_backup.py
@@ -29,7 +29,7 @@ import ldb
 from samba import smb
 from samba.ntacls import backup_online, backup_restore
 from samba.auth import system_session
-from samba.join import DCJoinContext, join_clone
+from samba.join import DCJoinContext, join_clone, DCCloneAndRenameContext
 from samba.dcerpc.security import dom_sid
 from samba.netcmd import Option, CommandError
 from samba.dcerpc import misc
@@ -39,6 +39,8 @@ from samba.provision import make_smbconf
 from samba.upgradehelpers import update_krbtgt_account_password
 from samba.remove_dc import remove_dc
 from samba.provision import secretsdb_self_join
+from samba.dbchecker import dbcheck
+import re
 
 
 
@@ -406,7 +408,221 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
                     "starting samba.")
 
 
+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 new domain's NetBIOS name, 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 specify 'keep-dns-realm', then 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 dnsRoot for the partition objects to reflect the rename'''
+
+        # lookup the crossRef objects that hold the old realm's dnsRoot
+        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()
+        nc_name = ldb.binary_encode(str(base_dn))
+        partitions_dn = samdb.get_partitions_dn()
+        res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
+                           attrs=["nETBIOSName"],
+                           expression='ncName=%s' % nc_name)
+
+        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'
+        controls = ['show_deleted:1', cross_ncs_ctrl]
+        chk.check_database(controls=controls)
+        samdb.transaction_commit()
+
+    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.
+        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)
+
+        tmpdir = tempfile.mkdtemp(dir=targetdir)
+
+        # Clone and rename the remote server
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+        ctx = DCCloneAndRenameContext(new_base_dn, new_domain_name,
+                                      new_dns_realm, logger=logger,
+                                      creds=creds, lp=lp, include_secrets=True,
+                                      dns_backend='SAMBA_INTERNAL',
+                                      server=server, targetdir=tmpdir)
+        ctx.do_join()
+
+        # get the paths used for the clone, then drop the old samdb connection
+        del ctx.local_samdb
+        paths = ctx.paths
+
+        # get a free RID to use as the new DC's SID (when it gets restored)
+        remote_sam = SamDB(url='ldap://' + server, credentials=creds,
+                           session_info=system_session(), lp=lp)
+        new_sid = get_sid_for_restore(remote_sam)
+        old_realm = remote_sam.domain_dns_name()
+
+        # Grab the remote DC's sysvol files and bundle them into a tar file.
+        # Note we end up with 2 sysvol dirs - the original domain's files (that
+        # use the old realm) backed here, as well as default files generated
+        # for the new realm as part of the clone/join.
+        sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
+        smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
+        backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
+
+        # connect to the local DB (making sure we use the new/renamed config)
+        lp.load(paths.smbconf)
+        samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
+
+        # 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)
+
+        # 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)
+
+        shutil.rmtree(tmpdir)
+
+
 class cmd_domain_backup(samba.netcmd.SuperCommand):
     '''Create or restore a backup of the domain.'''
     subcommands = {'online': cmd_domain_backup_online(),
+                   'rename': cmd_domain_backup_rename(),
                    'restore': cmd_domain_backup_restore()}
-- 
2.7.4


From 473fae9050722b6b986101ecdc32523a612982c4 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 2/5] 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 | 61 +++++++++++++++++++++++++++++++++---
 python/samba/provision/sambadns.py   |  5 +--
 python/samba/remove_dc.py            |  3 +-
 3 files changed, 61 insertions(+), 8 deletions(-)

diff --git a/python/samba/netcmd/domain_backup.py b/python/samba/netcmd/domain_backup.py
index 24407a4..931e333 100644
--- a/python/samba/netcmd/domain_backup.py
+++ b/python/samba/netcmd/domain_backup.py
@@ -41,7 +41,10 @@ from samba.remove_dc import remove_dc
 from samba.provision import secretsdb_self_join
 from samba.dbchecker import dbcheck
 import re
-
+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)
 
 
 # work out a SID (based on a free RID) to use when the domain gets restored.
@@ -235,6 +238,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 = {
@@ -242,8 +249,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 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)
+
     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:
@@ -314,7 +354,8 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
         # 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))
@@ -327,6 +368,13 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
                                                 "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_path = os.path.join(private_dir, 'secrets.ldb')
         secrets_ldb = Ldb(secrets_path, session_info=system_session(), lp=lp)
         secretsdb_self_join(secrets_ldb, domain=ctx.domain_name,
@@ -376,8 +424,8 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
                                                  ldb.FLAG_MOD_REPLACE,
                                                  "repsFrom")
             msg["repsTo"] = ldb.MessageElement([],
-                                                 ldb.FLAG_MOD_REPLACE,
-                                                 "repsTo")
+                                               ldb.FLAG_MOD_REPLACE,
+                                               "repsTo")
             samdb.modify(msg)
 
         # Update the krbtgt passwords twice, ensuring no tickets from
@@ -401,6 +449,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)
 
         logger.info("Backup file successfully restored to %s" % 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 71831498449cfd4f1392e685a65a3ab49300cb55 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 3/5] 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.

Run the same sub-set of RESTOREDC tests to prove that the new testenv is
sound.

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
 selftest/target/Samba.pm  |  1 +
 selftest/target/Samba4.pm | 45 +++++++++++++++++++++++++++++++++++++++++++++
 source4/selftest/tests.py | 11 ++++++-----
 3 files changed, 52 insertions(+), 5 deletions(-)

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 eea84c1..58e5e91 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                 => [],
 );
@@ -2790,6 +2791,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, $env->{SERVERCONFFILE});
+	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) = @_;
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index 4504a2b..121d399 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -812,7 +812,7 @@ plantestsuite_loadlist("samba4.ldap.vlv.python(ad_dc_ntvfs)", "ad_dc_ntvfs", [py
 plantestsuite_loadlist("samba4.ldap.linked_attributes.python(ad_dc_ntvfs)", "ad_dc_ntvfs:local", [python, os.path.join(samba4srcdir, "dsdb/tests/python/linked_attributes.py"), '$PREFIX_ABS/ad_dc_ntvfs/private/sam.ldb', '-U"$USERNAME%$PASSWORD"', '--workgroup=$DOMAIN', '$LOADLIST', '$LISTOPT'])
 
 # These should be the first tests run against testenvs created by backup/restore
-for env in ['restoredc']:
+for env in ['restoredc', 'renamedc']:
     # check that a restored DC matches the original DC (backupfromdc)
     plantestsuite("samba4.blackbox.ldapcmp_restore", env,
         ["PYTHON=%s" % python,
@@ -869,7 +869,7 @@ for env in ["ad_dc_ntvfs"]:
                            )
 
 # this is a basic sanity-check of Kerberos/NTLM user login
-for env in ["restoredc"]:
+for env in ["restoredc", "renamedc"]:
     plantestsuite_loadlist("samba4.ldap.login_basics.python(%s)" % env, env,
         [python, os.path.join(samba4srcdir, "dsdb/tests/python/login_basics.py"),
          "$SERVER", '-U"$USERNAME%$PASSWORD"', "-W$DOMAIN", "--realm=$REALM",
@@ -906,7 +906,7 @@ plansmbtorture4testsuite(t, "vampire_dc", ['$SERVER', '-U$USERNAME%$PASSWORD', '
 plansmbtorture4testsuite(t, "vampire_dc", ['$SERVER', '-U$USERNAME%$PASSWORD', '--workgroup=$DOMAIN'], modname="samba4.%s.two" % t)
 
 # RPC smoke-tests for testenvs of interest (RODC, etc)
-for env in ['rodc', 'restoredc']:
+for env in ['rodc', 'restoredc', 'renamedc']:
     plansmbtorture4testsuite('rpc.echo', env, ['ncacn_np:$SERVER', "-k", "yes", '-U$USERNAME%$PASSWORD', '--workgroup=$DOMAIN'], modname="samba4.rpc.echo")
     plansmbtorture4testsuite('rpc.echo', "%s:local" % env, ['ncacn_np:$SERVER', "-k", "yes", '-P', '--workgroup=$DOMAIN'], modname="samba4.rpc.echo")
     plansmbtorture4testsuite('rpc.echo', "%s:local" % env, ['ncacn_np:$SERVER', "-k", "no", '-Utestallowed\ account%$DC_PASSWORD', '--workgroup=$DOMAIN'], modname="samba4.rpc.echo.testallowed")
@@ -1086,7 +1086,7 @@ for env in [
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.kcc.kcc_utils")
 
 for env in [ "simpleserver", "fileserver", "nt4_dc", "ad_dc", "ad_dc_ntvfs",
-             "ad_member", "restoredc" ]:
+             "ad_member", "restoredc", "renamedc" ]:
     planoldpythontestsuite(env, "netlogonsvc",
                            extra_path=[os.path.join(srcdir(), 'python/samba/tests')],
                            name="samba.tests.netlogonsvc.python(%s)" % env)
@@ -1110,7 +1110,8 @@ for env in ['vampire_dc', 'promoted_dc', 'rodc']:
 # environment teardown.
 # check the databases are all OK. PLEASE LEAVE THIS AS THE LAST TEST
 for env in ["ad_dc_ntvfs", "ad_dc", "fl2000dc", "fl2003dc", "fl2008r2dc",
-            'vampire_dc', 'promoted_dc', 'backupfromdc', 'restoredc']:
+            'vampire_dc', 'promoted_dc', 'backupfromdc', 'restoredc',
+            'renamedc']:
     plantestsuite("samba4.blackbox.dbcheck(%s)" % env, env + ":local" , ["PYTHON=%s" % python, os.path.join(bbdir, "dbcheck.sh"), '$PREFIX/provision', configuration])
 
 # cmocka tests not requiring a specific encironment
-- 
2.7.4


From b4b8166f5d7a01271784e575972712be5438b512 Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Tue, 3 Jul 2018 13:55:53 +1200
Subject: [PATCH 4/5] tests: Add new tests for backup-rename command

Extend the existing 'backup online' tests to also test the domain
rename case. This mostly involves some extra assertions that the
restored DB has been modified appropriatelt (i.e. domain NetBIOS
name is updated, etc).

I've also added an extra test case that creates a few objects and
links and specifically asserts that they get renamed appropriately.

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

diff --git a/python/samba/tests/domain_backup.py b/python/samba/tests/domain_backup.py
index 845ae3b..dabe56f 100644
--- a/python/samba/tests/domain_backup.py
+++ b/python/samba/tests/domain_backup.py
@@ -19,12 +19,13 @@ import tarfile
 import os
 import shutil
 from samba.tests.samba_tool.base import SambaToolCmdTest
-from samba.tests import TestCaseInTempDir, env_loadparm
+from samba.tests import TestCaseInTempDir, env_loadparm, create_test_ou
 import ldb
 from samba.samdb import SamDB
 from samba.auth import system_session
 from samba import Ldb, dn_from_dns_name
 from samba.netcmd.fsmo import get_fsmo_roleowner
+import re
 
 
 def get_prim_dom(secrets_path, lp):
@@ -332,3 +333,122 @@ class DomainBackupOnline(DomainBackupBase):
 
     def test_backup_restore_with_conf(self):
         self._test_backup_restore_with_conf()
+
+
+class DomainBackupRename(DomainBackupBase):
+
+    # run the above test cases using a rename backup
+    def setUp(self):
+        super(DomainBackupRename, self).setUp()
+        self.new_server = "RENAMESERV"
+        self.restore_domain = "NEWDOMAIN"
+        self.restore_realm = "rename.test.net"
+        self.new_basedn = "DC=rename,DC=test,DC=net"
+        self.base_cmd = ["domain", "backup", "rename", self.restore_domain,
+                         self.restore_realm]
+        self.backup_markers += ['backupRename']
+
+    # run the common test case code for backup-renames
+    def test_backup_untar(self):
+        self._test_backup_untar()
+
+    def test_backup_restore(self):
+        self._test_backup_restore()
+
+    def test_backup_restore_with_conf(self):
+        self._test_backup_restore_with_conf()
+
+    def add_link(self, attr, source, target):
+        m = ldb.Message()
+        m.dn = ldb.Dn(self.ldb, source)
+        m[attr] = ldb.MessageElement(target, ldb.FLAG_MOD_ADD, attr)
+        self.ldb.modify(m)
+
+    def test_one_way_links(self):
+        """Sanity-check that a rename handles one-way links correctly"""
+
+        # Do some initial setup on the DC before back it up:
+        # create an OU to hold the test objects we'll create
+        test_ou = create_test_ou(self.ldb, "rename_test")
+        self.addCleanup(self.ldb.delete, test_ou, ["tree_delete:1"])
+
+        # create the source and target objects and link them together.
+        # We use addressBookRoots2 here because it's a one-way link
+        src_dn = "CN=link_src,%s" % test_ou
+        self.ldb.add({"dn": src_dn,
+                      "objectclass": "msExchConfigurationContainer"})
+        target_dn = "OU=link_tgt,%s" % test_ou
+        self.ldb.add({"dn": target_dn, "objectclass": "organizationalunit"})
+        link_attr = "addressBookRoots2"
+        self.add_link(link_attr, src_dn, target_dn)
+
+        # add a second link target that's in a different partition
+        server_dn = ("CN=testrename,CN=Servers,CN=Default-First-Site-Name,"
+                     "CN=Sites,%s" % str(self.ldb.get_config_basedn()))
+        self.ldb.add({"dn": server_dn, "objectclass": "server"})
+        self.addCleanup(self.ldb.delete, server_dn)
+        self.add_link(link_attr, src_dn, server_dn)
+
+        # do the backup/restore
+        backup_file = self.create_backup()
+        self.restore_backup(backup_file)
+        lp = self.check_restored_smbconf()
+        restored_ldb = self.check_restored_database(lp)
+
+        # work out what the new DNs should be
+        old_basedn = str(self.ldb.get_default_basedn())
+        new_target_dn = re.sub(old_basedn + '$', self.new_basedn, target_dn)
+        new_src_dn = re.sub(old_basedn + '$', self.new_basedn, src_dn)
+        new_server_dn = re.sub(old_basedn + '$', self.new_basedn, server_dn)
+
+        # check the links exist in the renamed DB with the correct DNs
+        res = restored_ldb.search(base=new_src_dn, scope=ldb.SCOPE_BASE,
+                                  attrs=[link_attr])
+        self.assertEqual(len(res), 1,
+                         "Failed to find renamed link source object")
+        self.assertTrue(link_attr in res[0], "Missing link attribute")
+        self.assertTrue(new_target_dn in res[0][link_attr])
+        self.assertTrue(new_server_dn in res[0][link_attr])
+
+    # extra checks we run on the restored DB in the rename case
+    def check_restored_database(self, lp):
+        # run the common checks over the restored DB
+        samdb = super(DomainBackupRename, self).check_restored_database(lp)
+
+        # check we have actually renamed the DNs
+        basedn = str(samdb.get_default_basedn())
+        self.assertEqual(basedn, self.new_basedn)
+
+        # check the partition and netBIOS name match the new domain
+        partitions_dn = samdb.get_partitions_dn()
+        nc_name = ldb.binary_encode(str(basedn))
+        res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
+                           attrs=["nETBIOSName", "cn"],
+                           expression='ncName=%s' % nc_name)
+        self.assertEqual(len(res), 1,
+                         "Looking up partition's NetBIOS name failed")
+        self.assertEqual(str(res[0].get("nETBIOSName")), self.restore_domain)
+        self.assertEqual(str(res[0].get("cn")), self.restore_domain)
+
+        # check the DC has the correct dnsHostname
+        realm = self.restore_realm
+        dn = "CN=%s,OU=Domain Controllers,%s" % (self.new_server,
+                                                 self.new_basedn)
+        res = samdb.search(base=dn, scope=ldb.SCOPE_BASE,
+                           attrs=["dNSHostName"])
+        self.assertEqual(len(res), 1,
+                         "Looking up new DC's dnsHostname failed")
+        expected_val = "%s.%s" % (self.new_server.lower(), realm)
+        self.assertEqual(str(res[0].get("dNSHostName")), expected_val)
+
+        # check the DNS zones for the new realm are present
+        dn = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (realm, basedn)
+        res = samdb.search(base=dn, scope=ldb.SCOPE_BASE)
+        self.assertEqual(len(res), 1, "Lookup of new domain's DNS zone failed")
+
+        forestdn = samdb.get_root_basedn().get_linearized()
+        dn = "DC=_msdcs.%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (realm,
+                                                                    forestdn)
+        res = samdb.search(base=dn, scope=ldb.SCOPE_BASE)
+        self.assertEqual(len(res), 1, "Lookup of new domain's DNS zone failed")
+        return samdb
-- 
2.7.4


From f1473e90e3aeeaf55e538adaa52cfff9a95b7f05 Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Wed, 4 Jul 2018 13:23:59 +1200
Subject: [PATCH 5/5] netcmd: Use dbcheck to fix DB problems introduced by
 restore itself

As part of the restore process, we remove all the old DCs from the DB.
However, this introduces some dbcheck errors - there are some DN
attributes and one-way links that reference the deleted objects that
need fixing up. To resolve this, we can run dbcheck as part of the
restore process. This problem affects both renames and plain restores.

The dbcheck.sh test didn't spot this problem because it fixes this type
of DB error first, before it checks the DB.

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

diff --git a/python/samba/netcmd/domain_backup.py b/python/samba/netcmd/domain_backup.py
index 931e333..c156046 100644
--- a/python/samba/netcmd/domain_backup.py
+++ b/python/samba/netcmd/domain_backup.py
@@ -282,6 +282,31 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
                                  host_ip, host_ip6, domainguid, ntdsguid,
                                  dnsadmins_sid, add_root=False)
 
+    def fix_old_dc_references(self, samdb):
+        '''Fixes attributes that reference the old/removed DCs'''
+
+        # we just want to fix up DB problems here that were introduced by us
+        # removing the old DCs. We restrict what we fix up so that the restored
+        # DB matches the backed-up DB as close as possible. (There may be other
+        # DB issues inherited from the backed-up DC, but it's not our place to
+        # silently try to fix them here).
+        samdb.transaction_start()
+        chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
+                      in_transaction=True)
+
+        # fix up stale references to the old DC
+        setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
+        attrs = ['lastKnownParent', 'interSiteTopologyGenerator']
+
+        # fix-up stale one-way links that point to the old DC
+        setattr(chk, 'remove_plausible_deleted_DN_links', 'ALL')
+        attrs += ['msDS-NC-Replica-Locations']
+
+        cross_ncs_ctrl = 'search_options:1:2'
+        controls = ['show_deleted:1', cross_ncs_ctrl]
+        chk.check_database(controls=controls, attrs=attrs)
+        samdb.transaction_commit()
+
     def run(self, sambaopts=None, credopts=None, backup_file=None,
             targetdir=None, newservername=None, host_ip=None, host_ip6=None):
         if not (backup_file and os.path.exists(backup_file)):
@@ -442,6 +467,10 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
         backup_restore(sysvol_tar, dest_sysvol_dir, samdb, smbconf)
         os.remove(sysvol_tar)
 
+        # fix up any stale links to the old DCs we just removed
+        logger.info("Fixing up any remaining references to the old DCs...")
+        self.fix_old_dc_references(samdb)
+
         # Remove DB markers added by the backup process
         m = ldb.Message()
         m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
-- 
2.7.4

-------------- next part --------------
From 0294826b54e5818c0d8f41ed3861002b6801e075 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 1/3] 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 | 100 +++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 97 insertions(+), 3 deletions(-)

diff --git a/python/samba/join.py b/python/samba/join.py
index 39c9a3a..3b648f5 100644
--- a/python/samba/join.py
+++ b/python/samba/join.py
@@ -43,6 +43,9 @@ import logging
 import talloc
 import random
 import time
+import re
+import os
+import tempfile
 
 class DCJoinException(Exception):
 
@@ -904,6 +907,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."""
 
@@ -929,9 +938,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,
@@ -1614,3 +1622,89 @@ 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
+        binding_str = "ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options)
+        return drs_utils.drs_ReplicateRenamer(binding_str, ctx.lp, repl_creds,
+                                              ctx.local_samdb,
+                                              ctx.invocation_id,
+                                              ctx.base_dn, ctx.new_base_dn)
+
+    def create_non_global_lp(ctx, global_lp):
+        '''Creates a non-global LoadParm based on the global LP's settings'''
+
+        # the samba code shares a global LoadParm by default. Here we create a
+        # new LoadParm that retains the global settings, but any changes we
+        # make to it won't automatically affect the rest of the samba code.
+        # The easiest way to do this is to dump the global settings to a
+        # temporary smb.conf file, and then load the temp file into a new
+        # non-global LoadParm
+        fd, tmp_file = tempfile.mkstemp()
+        global_lp.dump(False, tmp_file)
+        local_lp = samba.param.LoadParm(filename_for_non_global_lp=tmp_file)
+        os.remove(tmp_file)
+        return local_lp
+
+    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() calls make_smbconf() which uses lp.dump()/lp.load()
+        # to create a new smb.conf. By default, it uses the global LoadParm to
+        # do this, and so it would overwrite the realm/domain values globally.
+        # We still need the global LoadParm to retain the old domain's details,
+        # so we can connect to (and clone) the existing DC.
+        # So, copy the global settings into a non-global LoadParm, which we can
+        # then pass into provision(). This generates a new smb.conf correctly,
+        # without overwriting the global realm/domain values just yet.
+        non_global_lp = ctx.create_non_global_lp(ctx.lp)
+
+        # 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, lp=non_global_lp,
+                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",
+                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 3a77332192be5f4e8a23beb5484f1bb62a6abddd 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 2/3] 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 db83267..66f4750 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
@@ -367,6 +368,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 9edb18f1d556976d0bb05e96ad6a95fae780fd7d Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Tue, 3 Jul 2018 13:43:29 +1200
Subject: [PATCH 3/3] tests: Tweak the backup online tests so they're generic

Update backup-online tests to be more generic. We can then re-use the
common framework for other types of backups (offline, rename), and just
change what's specific to those particular cases.

This change includes asserting the restored backup's domain/realm are
correct, which we weren't doing previously but makes sense.

The new 'return samdb' is for convenience, so that child classes can
easily extend the checks we run over the restored DB.

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
 python/samba/tests/domain_backup.py | 57 +++++++++++++++++++++++++++----------
 1 file changed, 42 insertions(+), 15 deletions(-)

diff --git a/python/samba/tests/domain_backup.py b/python/samba/tests/domain_backup.py
index fbe03f8..845ae3b 100644
--- a/python/samba/tests/domain_backup.py
+++ b/python/samba/tests/domain_backup.py
@@ -36,10 +36,10 @@ def get_prim_dom(secrets_path, lp):
                               expression="(objectClass=kerberosSecret)")
 
 
-class DomainBackup(SambaToolCmdTest, TestCaseInTempDir):
+class DomainBackupBase(SambaToolCmdTest, TestCaseInTempDir):
 
     def setUp(self):
-        super(DomainBackup, self).setUp()
+        super(DomainBackupBase, self).setUp()
 
         server = os.environ["DC_SERVER"]
         self.user_auth = "-U%s%%%s" % (os.environ["DC_USERNAME"],
@@ -50,6 +50,10 @@ class DomainBackup(SambaToolCmdTest, TestCaseInTempDir):
                                  self.user_auth)
         self.new_server = "BACKUPSERV"
         self.server = server.upper()
+        self.base_cmd = None
+        self.backup_markers = ['sidForRestore', 'backupDate']
+        self.restore_domain = os.environ["DOMAIN"]
+        self.restore_realm = os.environ["REALM"]
 
     def assert_partitions_present(self, samdb):
         """Asserts all expected partitions are present in the backup samdb"""
@@ -97,7 +101,7 @@ class DomainBackup(SambaToolCmdTest, TestCaseInTempDir):
         with tarfile.open(backup_file) as tf:
             tf.extractall(extract_dir)
 
-    def test_backup_untar(self):
+    def _test_backup_untar(self):
         """Creates a backup, untars the raw files, and sanity-checks the DB"""
         backup_file = self.create_backup()
         self.untar_backup(backup_file)
@@ -110,10 +114,11 @@ class DomainBackup(SambaToolCmdTest, TestCaseInTempDir):
         # check that backup markers were added to the DB
         res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
                            scope=ldb.SCOPE_BASE,
-                           attrs=['sidForRestore', 'backupDate'])
+                           attrs=self.backup_markers)
         self.assertEqual(len(res), 1)
-        self.assertIsNotNone(res[0].get('sidForRestore'))
-        self.assertIsNotNone(res[0].get('backupDate'))
+        for marker in self.backup_markers:
+            self.assertIsNotNone(res[0].get(marker),
+                                 "%s backup marker missing" % marker)
 
         # We have no secrets.ldb entry as we never got that during the backup.
         secrets_path = os.path.join(private_dir, "secrets.ldb")
@@ -123,7 +128,7 @@ class DomainBackup(SambaToolCmdTest, TestCaseInTempDir):
         # sanity-check that all the partitions got backed up
         self.assert_partitions_present(samdb)
 
-    def test_backup_restore(self):
+    def _test_backup_restore(self):
         """Does a backup/restore, with specific checks of the resulting DB"""
         backup_file = self.create_backup()
         self.restore_backup(backup_file)
@@ -151,7 +156,7 @@ class DomainBackup(SambaToolCmdTest, TestCaseInTempDir):
         self.addCleanup(os.remove, new_smbconf)
         return new_smbconf
 
-    def test_backup_restore_with_conf(self):
+    def _test_backup_restore_with_conf(self):
         """Checks smb.conf values passed to the restore are retained"""
         backup_file = self.create_backup()
 
@@ -159,7 +164,9 @@ class DomainBackup(SambaToolCmdTest, TestCaseInTempDir):
         # dir should get overridden by the restore, the other settings should
         # trickle through into the restored dir's smb.conf
         settings = {'state directory': '/var/run',
-                    'netbios name': 'FOOBAR'}
+                    'netbios name': 'FOOBAR',
+                    'workgroup': 'NOTMYDOMAIN',
+                    'realm': 'NOT.MY.REALM'}
         assert_settings = {'drs: max link sync': '275',
                            'prefork children': '7'}
         settings.update(assert_settings)
@@ -181,6 +188,8 @@ class DomainBackup(SambaToolCmdTest, TestCaseInTempDir):
         smbconf = os.path.join(self.restore_dir(), "etc", "smb.conf")
         bkp_lp = param.LoadParm(filename_for_non_global_lp=smbconf)
         self.assertEqual(bkp_lp.get('netbios name'), self.new_server)
+        self.assertEqual(bkp_lp.get('workgroup'), self.restore_domain)
+        self.assertEqual(bkp_lp.get('realm'), self.restore_realm.upper())
 
         # we restore with a fixed directory structure, so we can sanity-check
         # that the core filepaths settings are what we expect them to be
@@ -206,10 +215,11 @@ class DomainBackup(SambaToolCmdTest, TestCaseInTempDir):
         # check that the backup markers have been removed from the restored DB
         res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
                            scope=ldb.SCOPE_BASE,
-                           attrs=['sidForRestore', 'backupDate'])
+                           attrs=self.backup_markers)
         self.assertEqual(len(res), 1)
-        self.assertIsNone(res[0].get('sidForRestore'))
-        self.assertIsNone(res[0].get('backupDate'))
+        for marker in self.backup_markers:
+            self.assertIsNone(res[0].get(marker),
+                              "%s backup-marker left behind" % marker)
 
         # check that the repsFrom and repsTo values have been removed
         # from the restored DB
@@ -231,6 +241,7 @@ class DomainBackup(SambaToolCmdTest, TestCaseInTempDir):
         self.assert_partitions_present(samdb)
         self.assert_dcs_present(samdb, self.new_server, expected_count=1)
         self.assert_fsmo_roles(samdb, self.new_server, self.server)
+        return samdb
 
     def assert_fsmo_roles(self, samdb, server, exclude_server):
         """Asserts the expected server is the FSMO role owner"""
@@ -268,9 +279,8 @@ class DomainBackup(SambaToolCmdTest, TestCaseInTempDir):
     def create_backup(self):
         """Runs the backup cmd to produce a backup file for the testenv DC"""
         # Run the backup command and check we got one backup tar file
-        args = ["domain", "backup", "online",
-                "--server=" + self.server]
-        args += [self.user_auth, "--targetdir=" + self.tempdir]
+        args = self.base_cmd + ["--server=" + self.server, self.user_auth,
+                                "--targetdir=" + self.tempdir]
 
         self.run_cmd(args)
 
@@ -305,3 +315,20 @@ class DomainBackup(SambaToolCmdTest, TestCaseInTempDir):
         self.assert_partitions_present(self.ldb)
         self.assert_dcs_present(self.ldb, self.server)
         self.assert_fsmo_roles(self.ldb, self.server, self.new_server)
+
+
+class DomainBackupOnline(DomainBackupBase):
+
+    def setUp(self):
+        super(DomainBackupOnline, self).setUp()
+        self.base_cmd = ["domain", "backup", "online"]
+
+    # run the common test cases above using online backups
+    def test_backup_untar(self):
+        self._test_backup_untar()
+
+    def test_backup_restore(self):
+        self._test_backup_restore()
+
+    def test_backup_restore_with_conf(self):
+        self._test_backup_restore_with_conf()
-- 
2.7.4



More information about the samba-technical mailing list