[PATCH] domain rename tool

Tim Beale timbeale at catalyst.net.nz
Tue Jul 3 04:35:55 UTC 2018


Hi,

Attached are patches that extend the backup/restore tool to handle
renaming a domain. In case you missed it, there's more background on the
what/why behind this here:
https://lists.samba.org/archive/samba-technical/2018-June/128546.html

Note these patches are dependent on the backup/restore patch-set sent
out earlier today (still pending delivery to master).

CI link: https://gitlab.com/catalyst-samba/samba/pipelines/25003510
Git branch:
https://gitlab.com/catalyst-samba/samba/commits/tim-backup-rename

There's still a little more testing I'd like to do, but otherwise I
think these changes are close to complete.

Thanks,
Tim
-------------- next part --------------
From 582ad4abb3f9bd622976f6bf79d86f4fc3cbcc31 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/7] 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 f18fca2baaf7722492e95b7e96796433fc437fc2 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 2/7] 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 | 217 ++++++++++++++++++++++++++++++++++-
 2 files changed, 221 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..db93eda 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,220 @@ 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"
+        chk.check_database(controls=[cross_ncs_ctrl])
+        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 d14aac40d085f3752428c133c46c7be8a9b50410 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 3/7] 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 db93eda..4f463e0 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 e7abaf4e742e4cf81fa3fc1195c29d68c5247d74 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 4/7] 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 3f9358c7ab0a010fa2a4e7e709554593d5878d86 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 5/7] 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 7875d5bce27d053b23f6338cd9111ef1c1ec1061 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 6/7] 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


From dda9f1b0e20b74b8741ce7571c623d4540ce4bed 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 7/7] tests: Add new tests for backup-rename command

Extend the existing 'backup online' tests to also test the domain
rename case.

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

diff --git a/python/samba/tests/domain_backup.py b/python/samba/tests/domain_backup.py
index 845ae3b..34eae91 100644
--- a/python/samba/tests/domain_backup.py
+++ b/python/samba/tests/domain_backup.py
@@ -332,3 +332,69 @@ 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()
+
+    # 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")
-- 
2.7.4



More information about the samba-technical mailing list