[PATCH] Domain backup and restore samba-tool commands

Tim Beale timbeale at catalyst.net.nz
Tue Jun 26 21:13:14 UTC 2018


Hi,

Attached is the latest patches for the backup/restore tool Aaron has
been working on. It's still not quite 100% complete, but it's getting close.

We've dropped the offline backup command for now (i.e. the option that
just tarred up the raw files on disk). There were still a few
outstanding issues with it, the main one being that it didn't support
LMDB at all. We'll address this once the backup-online, restore, and
backup-rename work is in master.

We've now added a sub-set of tests that run over a restored DC. Clean CI
results here:
https://gitlab.com/catalyst-samba/samba/pipelines/24560305

The main outstanding work is just a few more tidy ups to the code, and
to integrate the backup tool with the NTACL changes Joe is working on.

Git branch also available here:
https://gitlab.com/catalyst-samba/samba/tree/tim-backup-tool

Cheers,
Tim

-------------- next part --------------
From 7b3f0a01bfcc8a0af8905caad9260b36c5eb7e2c Mon Sep 17 00:00:00 2001
From: Aaron Haslett <aaronhaslett at catalyst.net.nz>
Date: Tue, 1 May 2018 15:48:38 +1200
Subject: [PATCH 01/11] samba: read backup date field on init and fail if
 present

This prevents a backup tar file, created with the new official
backup tools, from being extracted and replicated.

This is done here to ensure that samba-tool and ldbsearch can
still operate on the backup (eg for forensics) but starting
Samba as an AD DC will fail.

Signed-off-by: Aaron Haslett<aaronhaslett at catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet at samba.org>
---
 source4/smbd/server.c | 64 +++++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 52 insertions(+), 12 deletions(-)

diff --git a/source4/smbd/server.c b/source4/smbd/server.c
index ed81c01..a6babef 100644
--- a/source4/smbd/server.c
+++ b/source4/smbd/server.c
@@ -44,6 +44,7 @@
 #include "nsswitch/winbind_client.h"
 #include "libds/common/roles.h"
 #include "lib/util/tfork.h"
+#include "dsdb/samdb/ldb_modules/util.h"
 
 #ifdef HAVE_PTHREAD
 #include <pthread.h>
@@ -232,24 +233,51 @@ _NORETURN_ static void max_runtime_handler(struct tevent_context *ev,
   pre-open the key databases. This saves a lot of time in child
   processes
  */
-static void prime_ldb_databases(struct tevent_context *event_ctx)
+static int prime_ldb_databases(struct tevent_context *event_ctx, bool *am_backup)
 {
-	TALLOC_CTX *db_context;
-	db_context = talloc_new(event_ctx);
-
-	samdb_connect(db_context,
-		      event_ctx,
-		      cmdline_lp_ctx,
-		      system_session(cmdline_lp_ctx),
-		      NULL,
-		      0);
+	struct ldb_result *res;
+	struct ldb_dn *samba_dsdb_dn;
+	struct ldb_context *ldb_ctx;
+	static const char *attrs[] = { "backupDate", NULL };
+	const char *msg;
+	int ret;
+	TALLOC_CTX *db_context = talloc_new(event_ctx);;
+	if (db_context == NULL) {
+		return LDB_ERR_OPERATIONS_ERROR;
+	}
+
+	ldb_ctx = samdb_connect(db_context,
+				event_ctx,
+				cmdline_lp_ctx,
+				system_session(cmdline_lp_ctx),
+				NULL,
+				0);
 	privilege_connect(db_context, cmdline_lp_ctx);
 
+	samba_dsdb_dn = ldb_dn_new(db_context, ldb_ctx, "@SAMBA_DSDB");
+	if (!samba_dsdb_dn) {
+		talloc_free(db_context);
+		return LDB_ERR_OPERATIONS_ERROR;
+	}
+
+	ret = dsdb_search_dn(ldb_ctx, db_context, &res, samba_dsdb_dn, attrs,
+			     DSDB_FLAG_AS_SYSTEM);
+	if (ret != LDB_SUCCESS || res == NULL || res->count == 0) {
+		talloc_free(db_context);
+		return ret;
+	}
+
+	msg = ldb_msg_find_attr_as_string(res->msgs[0], "backupDate", NULL);
+	if (msg != NULL) {
+		*am_backup = true;
+	}
+	*am_backup = false;
+
+	return LDB_SUCCESS;
 	/* we deliberately leave these open, which allows them to be
 	 * re-used in ldb_wrap_connect() */
 }
 
-
 /*
   called when a fatal condition occurs in a child task
  */
@@ -366,7 +394,9 @@ static int binary_smbd_main(const char *binary_name,
 	bool opt_fork = true;
 	bool opt_interactive = false;
 	bool opt_no_process_group = false;
+	bool db_is_backup = false;
 	int opt;
+	int ret;
 	poptContext pc;
 #define _MODULE_PROTO(init) extern NTSTATUS init(TALLOC_CTX *);
 	STATIC_service_MODULES_PROTO;
@@ -631,7 +661,17 @@ static int binary_smbd_main(const char *binary_name,
 			"and exited. Check logs for details", EINVAL);
 	};
 
-	prime_ldb_databases(state->event_ctx);
+	ret = prime_ldb_databases(state->event_ctx, &db_is_backup);
+	if (ret != LDB_SUCCESS) {
+		TALLOC_FREE(state);
+		exit_daemon("Samba failed to prime database", EINVAL);
+	}
+
+	if (db_is_backup) {
+		TALLOC_FREE(state);
+		exit_daemon("Database is a backup. Please run samba-tool domain"
+			    " backup restore", EINVAL);
+	}
 
 	status = setup_parent_messaging(state, cmdline_lp_ctx);
 	if (!NT_STATUS_IS_OK(status)) {
-- 
2.7.4


From cd409d713b8aad602c0320b969cb20fab582f7bb Mon Sep 17 00:00:00 2001
From: Aaron Haslett <aaronhaslett at catalyst.net.nz>
Date: Tue, 1 May 2018 11:10:36 +1200
Subject: [PATCH 02/11] param: adding non-global smb.cfg option

Signed-off-by: Aaron Haslett <aaronhaslett at catalyst.net.nz>
---
 lib/param/loadparm.c    | 15 +++++++++++++--
 source4/param/pyparam.c | 31 ++++++++++++++++++++++++++++++-
 2 files changed, 43 insertions(+), 3 deletions(-)

diff --git a/lib/param/loadparm.c b/lib/param/loadparm.c
index 3b7f805..9684a52 100644
--- a/lib/param/loadparm.c
+++ b/lib/param/loadparm.c
@@ -3148,7 +3148,8 @@ bool lpcfg_load_default(struct loadparm_context *lp_ctx)
  *
  * Return True on success, False on failure.
  */
-bool lpcfg_load(struct loadparm_context *lp_ctx, const char *filename)
+static bool lpcfg_load_internal(struct loadparm_context *lp_ctx,
+				const char *filename, bool set_global)
 {
 	char *n2;
 	bool bRetval;
@@ -3183,7 +3184,7 @@ bool lpcfg_load(struct loadparm_context *lp_ctx, const char *filename)
 	   for a missing smb.conf */
 	reload_charcnv(lp_ctx);
 
-	if (bRetval == true) {
+	if (bRetval == true && set_global) {
 		/* set this up so that any child python tasks will
 		   find the right smb.conf */
 		setenv("SMB_CONF_PATH", filename, 1);
@@ -3197,6 +3198,16 @@ bool lpcfg_load(struct loadparm_context *lp_ctx, const char *filename)
 	return bRetval;
 }
 
+bool lpcfg_load_no_global(struct loadparm_context *lp_ctx, const char *filename)
+{
+    return lpcfg_load_internal(lp_ctx, filename, false);
+}
+
+bool lpcfg_load(struct loadparm_context *lp_ctx, const char *filename)
+{
+    return lpcfg_load_internal(lp_ctx, filename, true);
+}
+
 /**
  * Return the max number of services.
  */
diff --git a/source4/param/pyparam.c b/source4/param/pyparam.c
index e7e908f..d94532d 100644
--- a/source4/param/pyparam.c
+++ b/source4/param/pyparam.c
@@ -445,7 +445,36 @@ static PyGetSetDef py_lp_ctx_getset[] = {
 
 static PyObject *py_lp_ctx_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
 {
-	return pytalloc_reference(type, loadparm_init_global(false));
+	const char *kwnames[] = {"_filename_for_non_global_lp", NULL};
+	PyObject *lp_ctx;
+	const char *conf_filename = NULL;
+	struct loadparm_context *ctx;
+
+	if (!PyArg_ParseTupleAndKeywords(args,
+					 kwargs,
+					 "|s",
+					 discard_const_p(char *,
+							 kwnames),
+					 &conf_filename)) {
+		return NULL;
+	}
+	if (conf_filename != NULL) {
+		ctx = loadparm_init(NULL);
+		if (ctx == NULL) {
+			return NULL;
+		}
+		
+		lp_ctx = pytalloc_reference(type, ctx);
+		if (lp_ctx == NULL) {
+			return NULL;
+		}
+
+		lpcfg_load_no_global(PyLoadparmContext_AsLoadparmContext(lp_ctx),
+				     conf_filename);
+		return lp_ctx;
+	} else{
+		return pytalloc_reference(type, loadparm_init_global(false));
+	}
 }
 
 static Py_ssize_t py_lp_ctx_len(PyObject *self)
-- 
2.7.4


From 59a151411c946320ca4a426f2cd4e85f4c11cca7 Mon Sep 17 00:00:00 2001
From: Aaron Haslett <aaronhaslett at catalyst.net.nz>
Date: Tue, 1 May 2018 11:10:11 +1200
Subject: [PATCH 03/11] netcmd: domain backup online command

This adds a samba-tool command that can be run against a remote DC to
produce a backup-file for the current domain. The backup stores similar
info to what a new DC would get if it joined the network.

Signed-off-by: Aaron Haslett<aaronhaslett at catalyst.net.nz>
---
 docs-xml/manpages/samba-tool.8.xml   |  10 ++
 python/samba/join.py                 |   4 +-
 python/samba/netcmd/domain.py        |   2 +
 python/samba/netcmd/domain_backup.py | 265 +++++++++++++++++++++++++++++++++++
 4 files changed, 279 insertions(+), 2 deletions(-)
 create mode 100644 python/samba/netcmd/domain_backup.py

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index f785e53..70ff956 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -292,6 +292,16 @@
 </refsect2>
 
 <refsect3>
+	<title>domain backup</title>
+	<para>Create or restore a backup of the domain.</para>
+</refsect3>
+
+<refsect3>
+	<title>domain backup online</title>
+	<para>Copy a running DC's current DB into a backup tar file.</para>
+</refsect3>
+
+<refsect3>
 	<title>domain classicupgrade [options] <replaceable>classic_smb_conf</replaceable></title>
 	<para>Upgrade from Samba classic (NT4-like) database to Samba AD DC
 	database.</para>
diff --git a/python/samba/join.py b/python/samba/join.py
index 30ecce7..b5cdf52 100644
--- a/python/samba/join.py
+++ b/python/samba/join.py
@@ -1495,10 +1495,10 @@ def join_DC(logger=None, server=None, creds=None, lp=None, site=None, netbios_na
     logger.info("Joined domain %s (SID %s) as a DC" % (ctx.domain_name, ctx.domsid))
 
 def join_clone(logger=None, server=None, creds=None, lp=None,
-               targetdir=None, domain=None, include_secrets=False):
+               targetdir=None, domain=None, include_secrets=False, dns_backend="NONE"):
     """Join as a DC."""
     ctx = dc_join(logger, server, creds, lp, site=None, netbios_name=None, targetdir=targetdir, domain=domain,
-                  machinepass=None, use_ntvfs=False, dns_backend="NONE", promote_existing=False, clone_only=True)
+                  machinepass=None, use_ntvfs=False, dns_backend=dns_backend, promote_existing=False, clone_only=True)
 
     lp.set("workgroup", ctx.domain_name)
     logger.info("workgroup is %s" % ctx.domain_name)
diff --git a/python/samba/netcmd/domain.py b/python/samba/netcmd/domain.py
index 3dbe2fb..cc97ed0 100644
--- a/python/samba/netcmd/domain.py
+++ b/python/samba/netcmd/domain.py
@@ -4334,3 +4334,5 @@ class cmd_domain(SuperCommand):
     subcommands["tombstones"] = cmd_domain_tombstones()
     subcommands["schemaupgrade"] = cmd_domain_schema_upgrade()
     subcommands["functionalprep"] = cmd_domain_functional_prep()
+    from domain_backup import cmd_domain_backup
+    subcommands["backup"] = cmd_domain_backup()
diff --git a/python/samba/netcmd/domain_backup.py b/python/samba/netcmd/domain_backup.py
new file mode 100644
index 0000000..9d4c96f
--- /dev/null
+++ b/python/samba/netcmd/domain_backup.py
@@ -0,0 +1,265 @@
+# domain_backup
+#
+# Copyright Andrew Bartlett <abartlet at samba.org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import datetime, os, sys, tarfile, subprocess, logging, shutil, errno
+import tempfile
+import samba
+from samba import Ldb
+import tdb
+import samba.getopt as options
+from samba.samdb import SamDB
+import ldb
+from samba.auth import system_session
+from samba.join import dc_join, join_clone
+from samba.dcerpc.security import dom_sid
+from samba.netcmd import Option, CommandError
+import traceback
+
+tmpdir = 'backup_temp_dir'
+def rm_tmp():
+    if os.path.exists(tmpdir):
+        shutil.rmtree(tmpdir)
+def using_tmp_dir(func):
+    def inner(*args, **kwargs):
+        try:
+            rm_tmp()
+            os.makedirs(tmpdir)
+            rval = func(*args, **kwargs)
+            rm_tmp()
+            return rval
+        except Exception as e:
+            rm_tmp()
+
+            # print a useful stack-trace for unexpected exceptions
+            if type(e) is not CommandError:
+                traceback.print_exc()
+            raise e
+    return inner
+
+# work out a SID (based on a free RID) to use when the domain gets restored.
+# This ensures that the restored DC's SID won't clash with any other RIDs
+# already in use in the domain
+def get_sid_for_restore(samdb):
+    # Find the DN of the RID set of the server
+    res = samdb.search(base=ldb.Dn(samdb, samdb.get_serverName()),
+                       scope=ldb.SCOPE_BASE, attrs=["serverReference"])
+    server_ref_dn = ldb.Dn(samdb, res[0]['serverReference'][0])
+    res = samdb.search(base=server_ref_dn,
+                       scope=ldb.SCOPE_BASE,
+                       attrs=['rIDSetReferences'])
+    rid_set_dn = ldb.Dn(samdb, res[0]['rIDSetReferences'][0])
+
+    # Get the alloc pools and next RID of the RID set
+    res = samdb.search(base=rid_set_dn,
+                       scope=ldb.SCOPE_SUBTREE,
+                       expression="(rIDNextRID=*)",
+                       attrs=['rIDAllocationPool',
+                              'rIDPreviousAllocationPool',
+                              'rIDNextRID'])
+
+    # Decode the bounds of the RID allocation pools
+    rid = int(res[0].get('rIDNextRID')[0])
+    def split_val(num):
+        high = (0xFFFFFFFF00000000 & int(num)) >> 32
+        low = 0x00000000FFFFFFFF & int(num)
+        return low, high
+    pool_l, pool_h = split_val(res[0].get('rIDPreviousAllocationPool')[0])
+    npool_l, npool_h = split_val(res[0].get('rIDAllocationPool')[0])
+
+    # Calculate next RID based on pool bounds
+    if rid == npool_h:
+        raise CommandError('Out of RIDs, finished AllocPool')
+    if rid == pool_h:
+        if pool_h == npool_h:
+            raise CommandError('Out of RIDs, finished PrevAllocPool.')
+        rid = npool_l
+    else:
+        rid += 1
+
+    # Construct full SID
+    sid = dom_sid(samdb.get_domain_sid())
+    return str(sid) + '-' + str(rid)
+
+def copy_sysvol_files(server, creds, output_dir):
+    '''Copies the remote DC's sysvol files into the given output directory'''
+
+    # Run smbclient to get the remote sysvol files
+    cmd = os.path.join(samba.param.bin_dir(), 'smbclient')
+    cmd += ' //{s}/SYSVOL -U{user}%{pwd}'.format(s=server,
+            user=creds.get_username(), pwd=creds.get_password())
+    cmd = cmd.split(' ')
+
+    # download the sysvol files into a temporary tar file (for convenience)
+    tmp_sysvol_tar = os.path.join(output_dir, 'tmpsysvol.tar')
+    cmd += ['-c',  'tar cF {} ./'.format(tmp_sysvol_tar)]
+    status = subprocess.call(cmd, close_fds=True, shell=False)
+
+    # extract the sysvol files into the temporary directory (we'll tar up
+    # everything in the tmpdir later)
+    if not os.path.exists(output_dir):
+        os.makedirs(output_dir)
+    tf = tarfile.open(tmp_sysvol_tar)
+    tf.extractall(output_dir)
+    tf.close()
+    os.remove(tmp_sysvol_tar)
+
+def get_timestamp():
+    return datetime.datetime.now().isoformat().replace(':','-')
+
+def backup_filepath(targetdir, name, time_str):
+    filename = 'samba-backup-{}-{}.tar.bz2'.format(name, time_str)
+    return os.path.join(targetdir, filename)
+
+def create_backup_tar(logger, tmpdir, backup_filepath):
+    # Adds everything in the tmpdir into a new tar file
+    logger.info("Creating backup file %s..." % backup_filepath)
+    tf = tarfile.open(backup_filepath, 'w:bz2')
+    tf.add(tmpdir, arcname='./')
+    tf.close()
+
+# Add a backup-specific marker to the DB with info that we'll use during
+# the restore process
+def add_backup_marker(samdb, marker, value):
+    m = ldb.Message()
+    m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
+    m[marker] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, marker)
+    samdb.modify(m)
+
+def check_online_backup_args(logger, credopts, server, targetdir):
+    # Make sure we have all the required args.
+    u_p = {'user': credopts.creds.get_username(),
+           'pass': credopts.creds.get_password()}
+    if None in u_p.values():
+        raise CommandError("Creds required.")
+    if server is None:
+        raise CommandError('Server required')
+    if targetdir is None:
+        raise CommandError('Target directory required')
+
+    if not os.path.exists(targetdir):
+        logger.info('Creating targetdir %s...' % targetdir)
+        os.makedirs(targetdir)
+
+def sed_file(fn, find, replace, delete_lines=[]):
+    # Make paths in the restored config file point the new temp directory
+    sed_lines = ""
+    with open(fn, "r") as cfg_file:
+        sed_lines = cfg_file.read().replace(find, replace)
+        if delete_lines:
+            sed_lines = '\n'.join([l for l in sed_lines.split('\n')
+                                   if not any([d in l for d in delete_lines])])
+    with open(fn, "w") as cfg_file:
+        cfg_file.write(sed_lines)
+
+class cmd_domain_backup_online(samba.netcmd.Command):
+    '''Copy a running DC's current DB into a backup tar file.
+
+    Takes a backup copy of the current domain from a running DC. If the domain
+    were to undergo a catastrophic failure, then the backup file can be used to
+    recover the domain. The backup created is similar to the DB that a new DC
+    would receive when it joins the 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.'''
+
+    synopsis = "%prog --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", type=str,
+               help="Directory to write the backup file to"),
+       ]
+
+    @using_tmp_dir
+    def run(self, sambaopts=None, credopts=None, server=None, targetdir=None):
+        logger = self.get_logger()
+        logger.setLevel(logging.DEBUG)
+
+        # Make sure we have all the required args.
+        server = server or os.environ.get('SERVER', None)
+        check_online_backup_args(logger, credopts, server, targetdir)
+
+        # Make _config_file /dev/null so it's forced to generate a new one.
+        sambaopts._config_file = '/dev/null'
+        remote_lp = sambaopts.get_loadparm()
+        remote_creds = credopts.get_credentials(remote_lp)
+
+        if not os.path.exists(targetdir):
+            logger.info('Creating targetdir %s...' % targetdir)
+            os.makedirs(targetdir)
+
+        # Run a clone join on the remote
+        join_clone(logger=logger, creds=remote_creds, lp=remote_lp,
+                   include_secrets=True, dns_backend='SAMBA_INTERNAL',
+                   server=server, targetdir=tmpdir)
+
+        # Get the relative sysvol path to extract smbclient's tar to
+        cfg_fn = os.path.join(tmpdir, 'etc', 'smb.conf')
+        local_lp = samba.param.LoadParm(_filename_for_non_global_lp = cfg_fn)
+        non_tmp_paths = [local_lp.get('path', s) for s in local_lp.services()]
+        non_tmp_paths = [p for p in non_tmp_paths if tmpdir not in p]
+        paths_cp = os.path.commonprefix(non_tmp_paths)
+        paths_cp = os.path.realpath(os.path.join(paths_cp, '../'))
+        sed_file(cfg_fn, paths_cp, os.path.realpath(tmpdir))
+
+        local_creds = credopts.get_credentials(local_lp)
+        p = samba.provision.provision_paths_from_lp(local_lp,
+                                                    local_lp.get('realm'))
+        backup_dirs = [p.private_dir, p.state_dir, p.sysvol]
+
+        # Get a free RID to use as the new DC's SID (when it gets restored)
+        remote_sam = SamDB(url='ldap://'+server, session_info=system_session(),
+                           credentials=remote_creds, lp=remote_lp)
+        new_sid = get_sid_for_restore(remote_sam)
+
+        # Edit the downloaded sam.ldb to mark it as a backup
+        common_prefix_len = len(os.path.commonprefix(backup_dirs))
+        sam_path = os.path.join(tmpdir, p.samdb[common_prefix_len:])
+        samdb = SamDB(url=sam_path, session_info=system_session(),
+                      credentials=local_creds, lp=local_lp)
+        time_str = get_timestamp()
+        add_backup_marker(samdb, "backupDate", time_str)
+        add_backup_marker(samdb, "sidForRestore", new_sid)
+
+        # Extract the remote DC's sysvol files into our tmp backup-dir
+        full_sysvol_path = os.path.join(tmpdir, p.sysvol[common_prefix_len:])
+        copy_sysvol_files(server, remote_creds, full_sysvol_path)
+
+        # Move smb.conf to etc so we can always find in during restore
+        etc_dir = os.path.join(p.private_dir[:common_prefix_len], 'etc')
+        if not os.path.exists(etc_dir):
+            os.mkdir(etc_dir)
+        os.rename(p.smbconf, os.path.join(etc_dir, 'smb.conf'))
+
+        # Add everything in the tmpdir to the backup tar file
+        realm = remote_sam.domain_dns_name()
+        backup_file = backup_filepath(targetdir, realm, time_str)
+        create_backup_tar(logger, tmpdir, backup_file)
+
+class cmd_domain_backup(samba.netcmd.SuperCommand):
+    '''Domain backup'''
+    subcommands = {'online': cmd_domain_backup_online()}
-- 
2.7.4


From 53fc0f2cbcf077e0e556adede0b108a81c923e2f Mon Sep 17 00:00:00 2001
From: Aaron Haslett <aaronhaslett at catalyst.net.nz>
Date: Mon, 11 Jun 2018 19:13:35 +1200
Subject: [PATCH 04/11] tests: Add 'domain backup online' command test

Signed-off-by: Aaron Haslett <aaronhaslett at catalyst.net.nz>
---
 python/samba/tests/domain_backup.py | 179 ++++++++++++++++++++++++++++++++++++
 source4/selftest/tests.py           |   3 +
 2 files changed, 182 insertions(+)
 create mode 100644 python/samba/tests/domain_backup.py

diff --git a/python/samba/tests/domain_backup.py b/python/samba/tests/domain_backup.py
new file mode 100644
index 0000000..ecc4682
--- /dev/null
+++ b/python/samba/tests/domain_backup.py
@@ -0,0 +1,179 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Andrew Bartlett <abartlet at samba.org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+from samba.credentials import Credentials
+from samba import gensec, auth, tests, provision, param
+import tarfile, os
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba import NTSTATUSError
+import ldb
+from samba.samdb import SamDB
+from samba.auth import system_session
+from samba import Ldb
+
+class DomainBackup(SambaToolCmdTest):
+
+    def test_online_untar(self):
+        cfg_fn = self.run_backup_and_restore()
+        bkp_lp = param.LoadParm(_filename_for_non_global_lp = cfg_fn)
+        self.kerberos_test(server_lp=bkp_lp, expect_failure=True)
+        p = provision.provision_paths_from_lp(bkp_lp, bkp_lp.get("realm"))
+
+        samdb = SamDB(url=p.samdb, session_info=system_session(), lp=bkp_lp,
+                      credentials=self.get_credentials())
+        res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
+                           scope=ldb.SCOPE_BASE,
+                           attrs=['sidForRestore', 'backupDate'])
+
+        self.assertEqual(len(res), 1)
+        self.assertIsNotNone(res[0].get('sidForRestore'))
+        self.assertIsNotNone(res[0].get('backupDate'))
+
+        # We have no secrets.ldb entry as we never got that during
+        # the backup.
+        p = provision.provision_paths_from_lp(bkp_lp, bkp_lp.get("realm"))
+        secrets_ldb = Ldb(p.secrets, session_info=system_session(), lp=bkp_lp)
+        res = secrets_ldb.search(base="CN=Primary Domains",
+                                 attrs=['objectClass', 'samAccountName',
+                                        'secret'],
+                                 scope=ldb.SCOPE_SUBTREE,
+                                 expression="(objectClass=kerberosSecret)")
+
+        self.assertEqual(len(res), 0)
+
+    def run_backup_and_restore(self):
+        # Make a clean temp directory for the backup
+        tmp_backup_dir = os.path.join(os.environ["SELFTEST_PREFIX"],
+                                      ".backup-test-tmp")
+        if os.path.exists(tmp_backup_dir):
+            import shutil
+            shutil.rmtree(tmp_backup_dir)
+        os.mkdir(tmp_backup_dir)
+
+        c = self.get_credentials()
+        cred_str = "-U{}%{}".format(c.get_username(), c.get_password())
+        # Run the backup and check we got one backup tar file
+        nb_name = tests.env_loadparm().get("netbios name")
+        args = ["domain", "backup", "online",
+                "--server="+nb_name]
+        args += [cred_str, "--targetdir=" + tmp_backup_dir]
+        (result, out, err) = self.runsubcmd(*args)
+
+        self.assertCmdSuccess(result, out, err,
+                              "Ensuring domain backup command ran successfully")
+        tar_files = [fn for fn in os.listdir(tmp_backup_dir)
+                     if fn.startswith("samba-backup-") and
+                        fn.endswith(".tar.bz2")]
+        assert len(tar_files) == 1, "expected domain backup to create one" +\
+                                    "tar file but got {}".format(len(tar_files))
+
+        extract_dir = os.path.join(tmp_backup_dir, 'tree')
+        os.mkdir(extract_dir)
+
+        # Run restore or extract tar
+        tar_fn = os.path.join(tmp_backup_dir, tar_files[0])
+        with tarfile.open(tar_fn) as tf:
+            tf.extractall(extract_dir)
+
+        # Fill the temp backup dir with the empty folder that are needed,
+        for tmp_subdir_to_make in ["lockdir", "cachedir", "pid"]:
+            path = os.path.join(extract_dir, tmp_subdir_to_make)
+            if not os.path.exists(path):
+                os.mkdir(path)
+
+        # Make paths in the restored config file point the new temp directory
+        cfg_fn = os.path.join(os.getcwd(), extract_dir, "etc", "smb.conf")
+        lp = param.LoadParm(_filename_for_non_global_lp = cfg_fn)
+
+        # Get the root of the backup folders from loadparm to replace
+        p = provision.provision_paths_from_lp(lp, lp.get("realm"))
+        backup_dirs = [p.private_dir, p.state_dir, p.sysvol]
+        backup_root = os.path.commonprefix(backup_dirs)
+        if backup_root[-1] == '/':
+            backup_root = backup_root[:-1]
+
+        sed_lines = ""
+        with open(cfg_fn, "r") as cfg_file:
+            sed_lines = cfg_file.read().replace(backup_root,
+                                                os.path.realpath(extract_dir))
+        with open(cfg_fn, "w") as cfg_file:
+            cfg_file.write(sed_lines)
+
+        return cfg_fn
+
+    def kerberos_test(self, server_lp, expect_failure=False):
+        client_lp = tests.env_loadparm()
+
+        client_finished = False
+        server_finished = False
+        server_to_client = b""
+        client_to_server = b""
+
+        client_settings = {"lp_ctx": client_lp,
+                           "target_hostname": client_lp.get("netbios name")}
+        client_lp.set("spnego:simulate_w2k", "no")
+        gensec_client = gensec.Security.start_client(client_settings)
+        gensec_client.set_credentials(self.get_credentials())
+        gensec_client.want_feature(gensec.FEATURE_SEAL)
+        gensec_client.start_mech_by_sasl_name("GSSAPI")
+        result = gensec_client.update(server_to_client)
+        (client_finished, client_to_server) = result
+
+        # Initialise server to the restored domain
+        serv_settings = {"target_hostname": server_lp.get("netbios name"),
+                           "lp_ctx": server_lp}
+        server_lp.set("spnego:simulate_w2k", "no")
+
+        exception = None
+        try:
+            gensec_server = gensec.Security.start_server(settings=serv_settings,
+                                auth_context=auth.AuthContext(lp_ctx=server_lp))
+            creds = Credentials()
+            creds.guess(server_lp)
+            creds.set_machine_account(server_lp)
+            gensec_server.set_credentials(creds)
+            gensec_server.want_feature(gensec.FEATURE_SEAL)
+            gensec_server.start_mech_by_sasl_name("GSSAPI")
+
+            # Run update routines until done
+            while True:
+                if not server_finished:
+                    print("running server gensec_update")
+                    result = gensec_server.update(client_to_server)
+                    (server_finished, server_to_client) = result
+
+                if not client_finished:
+                    print("running client gensec_update")
+                    result = gensec_client.update(server_to_client)
+                    (client_finished, client_to_server) = result
+
+                if client_finished and server_finished:
+                    break
+        except NTSTATUSError as e:
+            exception = e
+
+        if expect_failure:
+            # If the test was intended to fail, make sure
+            self.assertIsNotNone(exception)
+        else:
+            # If the test was intended to pass, raise any exceptions or
+            # do a final session key equality check.
+            if exception:
+                raise exception
+            client_session_key = gensec_client.session_key()
+            server_session_key = gensec_server.session_key()
+            self.assertEqual(client_session_key, server_session_key)
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index 8b1fb7b..c7b996e 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -727,6 +727,9 @@ planoldpythontestsuite("fl2003dc:local",
 planoldpythontestsuite("ad_dc",
                        "samba.tests.password_hash_ldap",
                        extra_args=['-U"$USERNAME%$PASSWORD"'])
+planoldpythontestsuite("ad_dc_ntvfs:local",
+                       "samba.tests.domain_backup",
+                       extra_args=['-U"$USERNAME%$PASSWORD"'])
 # Encrypted secrets
 # ensure default provision (ad_dc) and join (vampire_dc)
 # encrypt secret values on disk.
-- 
2.7.4


From 46e967379ce46b190e3e10865419ddfc40736e18 Mon Sep 17 00:00:00 2001
From: Aaron Haslett <aaronhaslett at catalyst.net.nz>
Date: Tue, 1 May 2018 11:11:01 +1200
Subject: [PATCH 05/11] netcmd: domain backup restore command

Add a command option that restores a backup file. This is only intended
for recovering from a catastrophic failure of the domain. The old domain
DCs are removed from the DB and a new DC is added.

Signed-off-by: Aaron Haslett <aaronhaslett at catalyst.net.nz>
---
 docs-xml/manpages/samba-tool.8.xml   |   5 ++
 python/samba/join.py                 |  41 +++++----
 python/samba/netcmd/domain_backup.py | 169 ++++++++++++++++++++++++++++++++++-
 3 files changed, 199 insertions(+), 16 deletions(-)

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index 70ff956..b8038bc 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 restore</title>
+	<para>Restore the domain's DB from a backup-file.</para>
+</refsect3>
+
+<refsect3>
 	<title>domain classicupgrade [options] <replaceable>classic_smb_conf</replaceable></title>
 	<para>Upgrade from Samba classic (NT4-like) database to Samba AD DC
 	database.</para>
diff --git a/python/samba/join.py b/python/samba/join.py
index b5cdf52..f937c36 100644
--- a/python/samba/join.py
+++ b/python/samba/join.py
@@ -57,7 +57,8 @@ class dc_join(object):
                  netbios_name=None, targetdir=None, domain=None,
                  machinepass=None, use_ntvfs=False, dns_backend=None,
                  promote_existing=False, clone_only=False,
-                 plaintext_secrets=False, backend_store=None):
+                 plaintext_secrets=False, backend_store=None,
+                 forced_local_samdb=None, full_nc_list=[]):
         if site is None:
             site = "Default-First-Site-Name"
 
@@ -76,21 +77,25 @@ class dc_join(object):
         ctx.promote_from_dn = None
 
         ctx.nc_list = []
-        ctx.full_nc_list = []
+        ctx.full_nc_list = full_nc_list
 
         ctx.creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
         ctx.net = Net(creds=ctx.creds, lp=ctx.lp)
 
-        if server is not None:
-            ctx.server = server
-        else:
-            ctx.logger.info("Finding a writeable DC for domain '%s'" % domain)
-            ctx.server = ctx.find_dc(domain)
-            ctx.logger.info("Found DC %s" % ctx.server)
+        ctx.server = server
+        ctx.forced_local_samdb = forced_local_samdb
 
-        ctx.samdb = SamDB(url="ldap://%s" % ctx.server,
-                          session_info=system_session(),
-                          credentials=ctx.creds, lp=ctx.lp)
+        if forced_local_samdb:
+            ctx.samdb = forced_local_samdb
+            ctx.server = ctx.samdb.url
+        else:
+            if not ctx.server:
+                ctx.logger.info("Finding a writeable DC for domain '%s'" % domain)
+                ctx.server = ctx.find_dc(domain)
+                ctx.logger.info("Found DC %s" % ctx.server)
+            ctx.samdb = SamDB(url="ldap://%s" % ctx.server,
+                              session_info=system_session(),
+                              credentials=ctx.creds, lp=ctx.lp)
 
         try:
             ctx.samdb.search(scope=ldb.SCOPE_ONELEVEL, attrs=["dn"])
@@ -573,7 +578,9 @@ class dc_join(object):
         '''add the ntdsdsa object'''
 
         rec = ctx.join_ntdsdsa_obj()
-        if ctx.RODC:
+        if ctx.forced_local_samdb:
+            ctx.samdb.add(rec, controls=["relax:0"])
+        elif ctx.RODC:
             ctx.samdb.add(rec, ["rodc_join:1:1"])
         else:
             ctx.DsAddEntry([rec])
@@ -582,7 +589,7 @@ class dc_join(object):
         res = ctx.samdb.search(base=ctx.ntds_dn, scope=ldb.SCOPE_BASE, attrs=["objectGUID"])
         ctx.ntds_guid = misc.GUID(ctx.samdb.schema_format_value("objectGUID", res[0]["objectGUID"][0]))
 
-    def join_add_objects(ctx):
+    def join_add_objects(ctx, specified_sid=None):
         '''add the various objects needed for the join'''
         if ctx.acct_dn:
             print("Adding %s" % ctx.acct_dn)
@@ -612,12 +619,15 @@ class dc_join(object):
             elif ctx.promote_existing:
                 rec["msDS-RevealOnDemandGroup"] = []
 
+            if specified_sid:
+                rec["objectSid"] = ndr_pack(specified_sid)
+
             if ctx.promote_existing:
                 if ctx.promote_from_dn != ctx.acct_dn:
                     ctx.samdb.rename(ctx.promote_from_dn, ctx.acct_dn)
                 ctx.samdb.modify(ldb.Message.from_dict(ctx.samdb, rec, ldb.FLAG_MOD_REPLACE))
             else:
-                ctx.samdb.add(rec)
+                ctx.samdb.add(rec, controls=["relax:0"])
 
         if ctx.krbtgt_dn:
             ctx.add_krbtgt_account()
@@ -1366,7 +1376,8 @@ class dc_join(object):
         # full_nc_list is the list of naming context (NC) we hold
         # read/write copies of.  These are not subsets of each other.
         ctx.nc_list = [ ctx.config_dn, ctx.schema_dn ]
-        ctx.full_nc_list = [ ctx.base_dn, ctx.config_dn, ctx.schema_dn ]
+        if not ctx.full_nc_list:
+            ctx.full_nc_list = [ ctx.base_dn, ctx.config_dn, ctx.schema_dn ]
 
         if ctx.subdomain and ctx.dns_backend != "NONE":
             ctx.full_nc_list += [ctx.domaindns_zone]
diff --git a/python/samba/netcmd/domain_backup.py b/python/samba/netcmd/domain_backup.py
index 9d4c96f..7987e1a 100644
--- a/python/samba/netcmd/domain_backup.py
+++ b/python/samba/netcmd/domain_backup.py
@@ -21,13 +21,17 @@ import tempfile
 import samba
 from samba import Ldb
 import tdb
+from fsmo import cmd_fsmo_seize
 import samba.getopt as options
 from samba.samdb import SamDB
 import ldb
 from samba.auth import system_session
+from samba.upgradehelpers import update_krbtgt_account_password
+from samba.emulate.traffic import create_machine_account
 from samba.join import dc_join, join_clone
 from samba.dcerpc.security import dom_sid
 from samba.netcmd import Option, CommandError
+from samba.dcerpc import misc
 import traceback
 
 tmpdir = 'backup_temp_dir'
@@ -260,6 +264,169 @@ class cmd_domain_backup_online(samba.netcmd.Command):
         backup_file = backup_filepath(targetdir, realm, time_str)
         create_backup_tar(logger, tmpdir, backup_file)
 
+class cmd_domain_backup_restore(cmd_fsmo_seize):
+    '''Restore the domain's DB from a backup-file.
+
+    This restores a previously backed up copy of the domain's DB on a new DC.
+
+    Note that the restored DB will not contain the original DC that the backup
+    was taken from (or any other DCs in the original domain). Only the new DC
+    (specified by --newservername) will be present in the restored DB.
+
+    Samba can then be started against the restored DB. Any existing DCs for the
+    domain should be shutdown before the new DC is started. Other DCs can then
+    be joined to the new DC to recover the network.
+    '''
+
+    synopsis = "%prog --backup-file=<tar-file> --targetdir=<output-dir> --newservername=<DC-name>"
+    takes_options = [
+        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),
+    ]
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+    }
+
+    @using_tmp_dir
+    def run(self, sambaopts=None, credopts=None, backup_file=None,
+            targetdir=None, newservername=None):
+        if not (backup_file and os.path.exists(backup_file)):
+            raise CommandError('Backup file not found.')
+        if targetdir is None:
+            raise CommandError('Please specify a target directory')
+        if os.path.exists(targetdir) and os.listdir(targetdir):
+            raise CommandError('Target directory is not empty')
+        if not newservername:
+            raise CommandError('Server name required')
+
+        logger = logging.getLogger()
+        logger.setLevel(logging.DEBUG)
+        logger.addHandler(logging.StreamHandler(sys.stdout))
+
+        tf = tarfile.open(backup_file)
+        tf.extractall(tmpdir)
+        tf.close()
+
+        newservername = newservername.upper()
+
+        if not os.path.exists(targetdir):
+            logger.info('Creating targetdir %s...' % targetdir)
+            os.makedirs(targetdir)
+
+        cfg_fn = os.path.join(tmpdir, 'etc', 'smb.conf')
+        lp = samba.param.LoadParm(_filename_for_non_global_lp = cfg_fn)
+
+        # Fix the netbios name in the config file
+        def replace_def(key, val):
+            prefix = key + ' = '
+            sed_file(cfg_fn, prefix + lp.get(key), prefix + val)
+        replace_def('netbios name', newservername)
+
+        # Get the root of the backup folders and replace all instances with
+        # the new path
+        p = samba.provision.provision_paths_from_lp(lp, lp.get("realm"))
+        backup_dirs = [p.private_dir, p.state_dir, p.sysvol]
+        backup_root = os.path.commonprefix(backup_dirs)
+        if backup_root[-1] == '/':
+            backup_root = backup_root[:-1]
+        full_tmpdir_path = os.path.realpath(tmpdir)
+        sed_file(cfg_fn, backup_root, full_tmpdir_path, ['gpg key ids'])
+
+        # Reload the cfg file and connect to the samdb
+        lp = samba.param.LoadParm(_filename_for_non_global_lp = cfg_fn)
+        p = samba.provision.provision_paths_from_lp(lp, lp.get("realm"))
+        creds = credopts.get_credentials(lp)
+        samdb = SamDB(url=p.samdb, session_info=system_session(),
+                      credentials=creds, lp=lp)
+
+        # Create account using the join_add_objects function in the join object
+        # We need namingContexts, account control flags, and the sid saved by
+        # the backup process.
+        res = samdb.search(base="", scope=ldb.SCOPE_BASE,
+                           attrs=['namingContexts'])
+        ncs = [str(r) for r in res[0].get('namingContexts')]
+
+        ctx = dc_join(logger, creds=creds, lp=lp, forced_local_samdb=samdb,
+                      netbios_name=newservername, full_nc_list=ncs)
+        ctx.nc_list = ncs
+        ctx.userAccountControl = samba.dsdb.UF_SERVER_TRUST_ACCOUNT |\
+                                 samba.dsdb.UF_TRUSTED_FOR_DELEGATION
+
+        # Finally, get the SID saved by the backup process and create account
+        res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
+                           scope=ldb.SCOPE_BASE,
+                           attrs=['sidForRestore'])
+        sid = res[0].get('sidForRestore')[0]
+        logger.info('creating account with SID: ' + str(sid))
+        ctx.join_add_objects(specified_sid=dom_sid(sid))
+
+        m = ldb.Message()
+        m.dn = ldb.Dn(samdb, '@ROOTDSE')
+        m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % str(ctx.ntds_guid),
+                                                ldb.FLAG_MOD_REPLACE, "dsServiceName")
+        samdb.modify(m)
+
+        secrets_ldb = Ldb(p.secrets, session_info=system_session(), lp=lp)
+        samba.provision.secretsdb_self_join(secrets_ldb, domain=ctx.domain_name,
+                                            realm=ctx.realm,
+                                            dnsdomain=ctx.dnsdomain,
+                                            netbiosname=ctx.myname,
+                                            domainsid=ctx.domsid,
+                                            machinepass=ctx.acct_pass,
+                                            key_version_number=ctx.key_version_number,
+                                            secure_channel_type=misc.SEC_CHAN_BDC)
+
+        # Seize DNS roles
+        domain_dn = samdb.domain_dn()
+        forest_dn = samba.dn_from_dns_name(samdb.forest_dns_name())
+        domaindns_dn = ("CN=Infrastructure,DC=DomainDnsZones,", domain_dn)
+        forestdns_dn = ("CN=Infrastructure,DC=ForestDnsZones,", forest_dn)
+        for dn_prefix, dns_dn in [forestdns_dn, domaindns_dn]:
+            if dns_dn not in ncs:
+                continue
+            full_dn = dn_prefix + dns_dn
+            m = ldb.Message()
+            m.dn = ldb.Dn(samdb, full_dn)
+            m["fSMORoleOwner"] = ldb.MessageElement(samdb.get_dsServiceName(),
+                                                    ldb.FLAG_MOD_REPLACE,
+                                                    "fSMORoleOwner")
+            samdb.modify(m)
+
+        # Seize other roles
+        for role in ['rid', 'pdc', 'naming', 'infrastructure', 'schema']:
+            self.seize_role(role, samdb, force=True)
+
+        # Get all DCs and remove them
+        res = samdb.search(samdb.get_config_basedn(),
+                           scope=ldb.SCOPE_SUBTREE,
+                       expression="(&(objectClass=Server)(serverReference=*))")
+        for m in res:
+            cn = m.get('cn')[0]
+            if cn != newservername:
+                samba.remove_dc.remove_dc(samdb, logger, cn)
+
+        # Update tgt and DC passwords twice
+        update_krbtgt_account_password(samdb)
+        update_krbtgt_account_password(samdb)
+
+        # Remove fields added by backup process
+        m = ldb.Message()
+        m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
+        m["backupDate"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
+                                             "backupDate")
+        m["sidForRestore"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
+                                                "sidForRestore")
+        samdb.modify(m)
+
+        # Fix paths again and move files to targetdir
+        sed_file(cfg_fn, full_tmpdir_path, os.path.realpath(targetdir))
+        for item in os.listdir(full_tmpdir_path):
+            shutil.move(os.path.join(full_tmpdir_path, item), targetdir)
+
 class cmd_domain_backup(samba.netcmd.SuperCommand):
     '''Domain backup'''
-    subcommands = {'online': cmd_domain_backup_online()}
+    subcommands = {'online': cmd_domain_backup_online(),
+                   'restore': cmd_domain_backup_restore()}
-- 
2.7.4


From 82af9ac21360aa9d73ebf5a971abf6afa7f77bef Mon Sep 17 00:00:00 2001
From: Aaron Haslett <aaronhaslett at catalyst.net.nz>
Date: Tue, 26 Jun 2018 14:29:35 +1200
Subject: [PATCH 06/11] tests: Add 'domain backup restore' command test

Signed-off-by: Aaron Haslett<aaronhaslett at catalyst.net.nz>
---
 python/samba/tests/domain_backup.py | 121 +++++++++++++++++++++++++++---------
 1 file changed, 90 insertions(+), 31 deletions(-)

diff --git a/python/samba/tests/domain_backup.py b/python/samba/tests/domain_backup.py
index ecc4682..8888c6a 100644
--- a/python/samba/tests/domain_backup.py
+++ b/python/samba/tests/domain_backup.py
@@ -23,7 +23,17 @@ from samba import NTSTATUSError
 import ldb
 from samba.samdb import SamDB
 from samba.auth import system_session
-from samba import Ldb
+from samba import Ldb, dn_from_dns_name
+from samba.netcmd.fsmo import get_fsmo_roleowner
+
+def get_prim_dom(lp):
+    p = provision.provision_paths_from_lp(lp, lp.get("realm"))
+    secrets_ldb = Ldb(p.secrets, session_info=system_session(), lp=lp)
+    return secrets_ldb.search(base="CN=Primary Domains",
+                              attrs=['objectClass', 'samAccountName',
+                                     'secret', 'msDS-KeyVersionNumber'],
+                              scope=ldb.SCOPE_SUBTREE,
+                              expression="(objectClass=kerberosSecret)")
 
 class DomainBackup(SambaToolCmdTest):
 
@@ -43,19 +53,61 @@ class DomainBackup(SambaToolCmdTest):
         self.assertIsNotNone(res[0].get('sidForRestore'))
         self.assertIsNotNone(res[0].get('backupDate'))
 
-        # We have no secrets.ldb entry as we never got that during
-        # the backup.
+        # We have no secrets.ldb entry as we never got that during the backup.
+        res = get_prim_dom(bkp_lp)
+        self.assertEqual(len(res), 0)
+
+    def test_online_restore(self):
+        cfg_fn = self.run_backup_and_restore(use_restore=True)
+        self.check_restored_database(cfg_fn)
+
+    # We can't run kerberos test here because the host name changes during the
+    # restore.  In this test we will just check that the restore procedure
+    # modified the database correctly.  Full restore tool testing will be done
+    # using the restoredc testenv.
+    def check_restored_database(self, cfg_fn):
+        bkp_lp = param.LoadParm(_filename_for_non_global_lp = cfg_fn)
         p = provision.provision_paths_from_lp(bkp_lp, bkp_lp.get("realm"))
-        secrets_ldb = Ldb(p.secrets, session_info=system_session(), lp=bkp_lp)
-        res = secrets_ldb.search(base="CN=Primary Domains",
-                                 attrs=['objectClass', 'samAccountName',
-                                        'secret'],
-                                 scope=ldb.SCOPE_SUBTREE,
-                                 expression="(objectClass=kerberosSecret)")
 
-        self.assertEqual(len(res), 0)
+        bkp_pd = get_prim_dom(bkp_lp)
+        self.assertEqual(len(bkp_pd), 1)
+        acn = bkp_pd[0].get('samAccountName')
+        self.assertIsNotNone(acn)
+        self.assertEqual(acn[0].replace('$', ''), bkp_lp.get("netbios name"))
+        self.assertIsNotNone(bkp_pd[0].get('secret'))
 
-    def run_backup_and_restore(self):
+        local_lp = tests.env_loadparm()
+        local_pd = get_prim_dom(local_lp)
+        self.assertEqual(int(local_pd[0].get('msDS-KeyVersionNumber')[0]) + 1,
+                         int(bkp_pd[0].get('msDS-KeyVersionNumber')[0]))
+
+        samdb = SamDB(url=p.samdb, session_info=system_session(), lp=bkp_lp,
+                      credentials=self.get_credentials())
+        res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
+                           scope=ldb.SCOPE_BASE,
+                           attrs=['sidForRestore', 'backupDate'])
+
+        self.assertEqual(len(res), 1)
+        self.assertIsNone(res[0].get('sidForRestore'))
+        self.assertIsNone(res[0].get('backupDate'))
+
+        domain_dn = samdb.domain_dn()
+        forest_dn = dn_from_dns_name(samdb.forest_dns_name())
+        fsmos = {'infrastructure': "CN=Infrastructure," + domain_dn,
+                 'naming': "CN=Partitions,%s" % samdb.get_config_basedn(),
+                 'schema': str(samdb.get_schema_basedn()),
+                 'rid': "CN=RID Manager$,CN=System," + domain_dn,
+                 'pdc': domain_dn,
+                 'domaindns': "CN=Infrastructure,DC=DomainDnsZones," +domain_dn,
+                 'forestdns': "CN=Infrastructure,DC=ForestDnsZones," +forest_dn}
+        for role, dn in fsmos.items():
+            owner = get_fsmo_roleowner(samdb, ldb.Dn(samdb, dn), role)
+            self.assertTrue("CN={},".format(bkp_lp.get("netbios name"))\
+                            in owner.extended_str())
+            self.assertTrue("CN={},".format(local_lp.get("netbios name"))\
+                            not in owner.extended_str())
+
+    def run_backup_and_restore(self, use_restore=False):
         # Make a clean temp directory for the backup
         tmp_backup_dir = os.path.join(os.environ["SELFTEST_PREFIX"],
                                       ".backup-test-tmp")
@@ -83,11 +135,36 @@ class DomainBackup(SambaToolCmdTest):
 
         extract_dir = os.path.join(tmp_backup_dir, 'tree')
         os.mkdir(extract_dir)
+        cfg_fn = os.path.join(os.getcwd(), extract_dir, "etc", "smb.conf")
 
         # Run restore or extract tar
         tar_fn = os.path.join(tmp_backup_dir, tar_files[0])
-        with tarfile.open(tar_fn) as tf:
-            tf.extractall(extract_dir)
+        if use_restore:
+            args = ["domain", "backup", "restore", "--backup-file=" + tar_fn,
+                    "--targetdir=" + extract_dir,
+                    "--newservername=BACKUPSERV"]
+            (result, out, err) = self.runsubcmd(*args)
+            self.assertCmdSuccess(result, out, err,
+                              "Ensuring domain backup restore ran successfully")
+        else:
+            with tarfile.open(tar_fn) as tf:
+                tf.extractall(extract_dir)
+
+            # Get the root of the backup folders from loadparm to F/R in conf
+            lp = param.LoadParm(_filename_for_non_global_lp = cfg_fn)
+            p = provision.provision_paths_from_lp(lp, lp.get("realm"))
+            backup_dirs = [p.private_dir, p.state_dir, p.sysvol]
+            backup_root = os.path.commonprefix(backup_dirs)
+            if backup_root[-1] == '/':
+                backup_root = backup_root[:-1]
+
+            # Make paths in the restored config file point the new temp dir
+            sed_lines = ""
+            with open(cfg_fn, "r") as cfg_file:
+                sed_lines = cfg_file.read().replace(backup_root,
+                                                os.path.realpath(extract_dir))
+            with open(cfg_fn, "w") as cfg_file:
+                cfg_file.write(sed_lines)
 
         # Fill the temp backup dir with the empty folder that are needed,
         for tmp_subdir_to_make in ["lockdir", "cachedir", "pid"]:
@@ -95,24 +172,6 @@ class DomainBackup(SambaToolCmdTest):
             if not os.path.exists(path):
                 os.mkdir(path)
 
-        # Make paths in the restored config file point the new temp directory
-        cfg_fn = os.path.join(os.getcwd(), extract_dir, "etc", "smb.conf")
-        lp = param.LoadParm(_filename_for_non_global_lp = cfg_fn)
-
-        # Get the root of the backup folders from loadparm to replace
-        p = provision.provision_paths_from_lp(lp, lp.get("realm"))
-        backup_dirs = [p.private_dir, p.state_dir, p.sysvol]
-        backup_root = os.path.commonprefix(backup_dirs)
-        if backup_root[-1] == '/':
-            backup_root = backup_root[:-1]
-
-        sed_lines = ""
-        with open(cfg_fn, "r") as cfg_file:
-            sed_lines = cfg_file.read().replace(backup_root,
-                                                os.path.realpath(extract_dir))
-        with open(cfg_fn, "w") as cfg_file:
-            cfg_file.write(sed_lines)
-
         return cfg_fn
 
     def kerberos_test(self, server_lp, expect_failure=False):
-- 
2.7.4


From a0eb0dae6ce7e74d7d868bf508eb97ae35616e27 Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Tue, 29 May 2018 15:22:07 +1200
Subject: [PATCH 07/11] selftest: Update MAX_WRAPPED_INTERFACES comment to
 match code

Commit 19606e4dc657b0baf3ea84d updated the MAX_WRAPPED_INTERFACES define
in the C code from 40 to 64.

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

diff --git a/selftest/target/Samba.pm b/selftest/target/Samba.pm
index b0482d3..81d3d21 100644
--- a/selftest/target/Samba.pm
+++ b/selftest/target/Samba.pm
@@ -409,8 +409,8 @@ sub get_interface($)
     $interfaces{"vampire2000dc"} = 39;
 
     # update lib/socket_wrapper/socket_wrapper.c
-    #  #define MAX_WRAPPED_INTERFACES 40
-    # if you wish to have more than 40 interfaces
+    #  #define MAX_WRAPPED_INTERFACES 64
+    # if you wish to have more than 64 interfaces
 
     if (not defined($interfaces{$netbiosname})) {
 	die();
-- 
2.7.4


From 4386b9a58699707691833fe7971481cf7b0d877d Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Fri, 15 Jun 2018 11:54:37 +1200
Subject: [PATCH 08/11] remove_dc: Fix removal of an old Windows DC

Windows has 'CN=DNS Settings' child object underneath the Server object.
This was causing the removal of the server object in remove_dc() to
fail.

Noticed this problem while testing the backup/restore tool manually
against a Windows VM.

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

diff --git a/python/samba/remove_dc.py b/python/samba/remove_dc.py
index b9726f5..d190461 100644
--- a/python/samba/remove_dc.py
+++ b/python/samba/remove_dc.py
@@ -240,8 +240,9 @@ def offline_remove_server(samdb, logger,
         dnsHostName = None
 
     if remove_server_obj:
-        # Remove the server DN
-        samdb.delete(server_dn)
+        # Remove the server DN (do a tree-delete as it could still have a
+        # 'DNS Settings' child object if it's a Windows DC)
+        samdb.delete(server_dn, ["tree_delete:0"])
 
     if computer_dn is not None:
         computer_msgs = samdb.search(base=computer_dn,
-- 
2.7.4


From 8187afb9b8fc48b515f9537ae30d3951027f67aa Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Mon, 25 Jun 2018 14:00:59 +1200
Subject: [PATCH 09/11] provision: set 'binddns dir' when making new smb.conf

When creating a new smb.conf from scratch during a join/clone/etc, the
'binddns dir' setting still uses the source smb.conf/default setting,
instead of the targetdir sub-directory.

I noticed this problem when trying to create a new testenv - the
provision() was trying to create /usr/local/samba/bind-dns directory,
which would fail if samba hadn't already been installed on the host
machine.

Now that this is fixed, we also need to fix tests that were explicitly
asserting that no unexpected directories were left behind after the test
completes.

This change also breaks the upgradeprovision script. The upgrade-
provision calls newprovision() to create a reference provision in a
temporary directory. However, previously this temporary provision was
creating the bind-dns directory in the actual upgrade directory as a
side-effect, e.g. it did a provision() with
targetdir=alpha13_upgrade_full/private/referenceprovisionLBKBh2 and this
ended up creating alpha13_upgrade_full/bind-dns as a side-effect.
The provision() now creates bind-dns in the specified targetdir, but
this means check_for_DNS() fails (it tries to create bind-dns sub-
directories, but the upgrade's bind-dns doesn't exist). I've avoided
this problem by making sure bind-dns exists as part of the
check_for_DNS() processing.

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
 python/samba/provision/__init__.py           | 2 ++
 python/samba/tests/join.py                   | 1 +
 python/samba/tests/samdb.py                  | 2 +-
 source4/scripting/bin/samba_upgradeprovision | 3 +++
 source4/torture/drs/python/samba_tool_drs.py | 1 +
 5 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/python/samba/provision/__init__.py b/python/samba/provision/__init__.py
index 36f50f2..e571894 100644
--- a/python/samba/provision/__init__.py
+++ b/python/samba/provision/__init__.py
@@ -744,10 +744,12 @@ def make_smbconf(smbconf, hostname, domain, realm, targetdir,
         global_settings["lock dir"] = os.path.abspath(targetdir)
         global_settings["state directory"] = os.path.abspath(os.path.join(targetdir, "state"))
         global_settings["cache directory"] = os.path.abspath(os.path.join(targetdir, "cache"))
+        global_settings["binddns dir"] = os.path.abspath(os.path.join(targetdir, "bind-dns"))
 
         lp.set("lock dir", os.path.abspath(targetdir))
         lp.set("state directory",  global_settings["state directory"])
         lp.set("cache directory", global_settings["cache directory"])
+        lp.set("binddns dir", global_settings["binddns dir"])
 
     if eadb:
         if use_ntvfs and not lp.get("posix:eadb"):
diff --git a/python/samba/tests/join.py b/python/samba/tests/join.py
index 1f9fab1..17de3ab 100644
--- a/python/samba/tests/join.py
+++ b/python/samba/tests/join.py
@@ -73,6 +73,7 @@ class JoinTestCase(DNSTKeyTest):
             shutil.rmtree(os.path.join(self.tempdir, "etc"))
             shutil.rmtree(os.path.join(self.tempdir, "msg.lock"))
             os.unlink(os.path.join(self.tempdir, "names.tdb"))
+            shutil.rmtree(os.path.join(self.tempdir, "bind-dns"))
 
         self.join_ctx.cleanup_old_join(force=True)
 
diff --git a/python/samba/tests/samdb.py b/python/samba/tests/samdb.py
index d4279b4..8c477b2 100644
--- a/python/samba/tests/samdb.py
+++ b/python/samba/tests/samdb.py
@@ -59,7 +59,7 @@ class SamDBTestCase(TestCaseInTempDir):
         for f in ['names.tdb']:
             os.remove(os.path.join(self.tempdir, f))
 
-        for d in ['etc', 'msg.lock', 'private', 'state']:
+        for d in ['etc', 'msg.lock', 'private', 'state', 'bind-dns']:
             shutil.rmtree(os.path.join(self.tempdir, d))
 
         super(SamDBTestCase, self).tearDown()
diff --git a/source4/scripting/bin/samba_upgradeprovision b/source4/scripting/bin/samba_upgradeprovision
index 9d3e736..5d040d2 100755
--- a/source4/scripting/bin/samba_upgradeprovision
+++ b/source4/scripting/bin/samba_upgradeprovision
@@ -226,6 +226,9 @@ def check_for_DNS(refprivate, private, refbinddns_dir, binddns_dir, dns_backend)
     if not os.path.exists(dnsfile):
         shutil.copy("%s/dns_update_list" % refprivate, "%s" % dnsfile)
 
+    if not os.path.exists(binddns_dir):
+        os.mkdir(binddns_dir)
+
     if dns_backend not in ['BIND9_DLZ', 'BIND9_FLATFILE']:
        return
 
diff --git a/source4/torture/drs/python/samba_tool_drs.py b/source4/torture/drs/python/samba_tool_drs.py
index 502c809..29c6016 100644
--- a/source4/torture/drs/python/samba_tool_drs.py
+++ b/source4/torture/drs/python/samba_tool_drs.py
@@ -47,6 +47,7 @@ class SambaToolDrsTests(drs_base.DrsBaseTestCase):
             shutil.rmtree(os.path.join(self.tempdir, "msg.lock"))
             os.remove(os.path.join(self.tempdir, "names.tdb"))
             shutil.rmtree(os.path.join(self.tempdir, "state"))
+            shutil.rmtree(os.path.join(self.tempdir, "bind-dns"))
         except Exception:
             pass
 
-- 
2.7.4


From 41296a0cb7d1829b73a3ea13c26e102202063ba5 Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Tue, 29 May 2018 16:05:02 +1200
Subject: [PATCH 10/11] selftest: Add testenv for testing backup/restore

This adds a new testenv for testing that a DC created using the
samba-tool backup/restore can actually be started up. This actually
requires 2 new testenvs:

1. A 'backupfromdc' that solely exists to make a online backup of.
2. A 'restoredc' which takes the backup, and then uses the backup file
to do a restore, which we then start the DC based on.

The backupfromdc is just a plain vanilla AD DC. We use a separate test
env purely for this purpose, because the restoredc will use the same
domain (and so using an existing testenv would potentially interfere
with existing test cases).

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

diff --git a/selftest/target/Samba.pm b/selftest/target/Samba.pm
index 81d3d21..9c79345 100644
--- a/selftest/target/Samba.pm
+++ b/selftest/target/Samba.pm
@@ -407,6 +407,8 @@ sub get_interface($)
     $interfaces{"fakednsforwarder2"} = 37;
     $interfaces{"s4member_dflt"} = 38;
     $interfaces{"vampire2000dc"} = 39;
+    $interfaces{"backupfromdc"} = 40;
+    $interfaces{"restoredc"} = 41;
 
     # 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 7abc16e..e1e6041 100755
--- a/selftest/target/Samba4.pm
+++ b/selftest/target/Samba4.pm
@@ -2142,6 +2142,7 @@ sub check_env($$)
 	ad_dc_no_nss         => [],
 	ad_dc_no_ntlm        => [],
 	ad_dc_ntvfs          => [],
+	backupfromdc         => [],
 
 	fl2008r2dc           => ["ad_dc"],
 	fl2003dc             => ["ad_dc"],
@@ -2159,6 +2160,8 @@ sub check_env($$)
 	s4member_dflt_domain => ["ad_dc_ntvfs"],
 	s4member             => ["ad_dc_ntvfs"],
 
+	restoredc            => ["backupfromdc"],
+
 	none                 => [],
 );
 
@@ -2578,6 +2581,227 @@ sub setup_ad_dc_no_ntlm
 	return $env;
 }
 
+# Sets up a DC that's solely used to do a domain backup from. We then use the
+# backupfrom-DC to create the restore-DC - this proves that the backup/restore
+# process will create a Samba DC that will actually start up.
+# We don't use the backup-DC for anything else because its domain will conflict
+# with the restore DC.
+sub setup_backupfromdc
+{
+	my ($self, $path) = @_;
+
+	# If we didn't build with ADS, pretend this env was never available
+	if (not $self->{target3}->have_ads()) {
+	       return "UNKNOWN";
+	}
+
+	my $env = $self->provision_ad_dc($path, "backupfromdc", "BACKUPDOMAIN",
+					 "backupdom.samba.example.com", "");
+	unless ($env) {
+		return undef;
+	}
+
+	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;
+}
+
+# Creates a backup of a running testenv DC
+sub create_backup
+{
+	# note: dcvars contains the env info for the backup DC testenv
+	my ($self, $env, $dcvars, $backupdir, $backup_cmd) = @_;
+
+	# get all the env variables we pass in with the samba-tool command
+	my $cmd_env = "";
+	$cmd_env .= "SOCKET_WRAPPER_DEFAULT_IFACE=\"$env->{SOCKET_WRAPPER_DEFAULT_IFACE}\" ";
+	if (defined($env->{RESOLV_WRAPPER_CONF})) {
+		$cmd_env .= "RESOLV_WRAPPER_CONF=\"$env->{RESOLV_WRAPPER_CONF}\" ";
+	} else {
+		$cmd_env .= "RESOLV_WRAPPER_HOSTS=\"$env->{RESOLV_WRAPPER_HOSTS}\" ";
+	}
+	# Note: use the backupfrom-DC's krb5.conf to do the backup
+	$cmd_env .= " KRB5_CONFIG=\"$dcvars->{KRB5_CONFIG}\" ";
+	$cmd_env .= "KRB5CCNAME=\"$env->{KRB5_CCACHE}\" ";
+
+	# use samba-tool to create a backup from the 'backupfromdc' DC
+	my $cmd = "";
+	my $samba_tool = Samba::bindir_path($self, "samba-tool");
+	my $server = $dcvars->{DC_SERVER_IP};
+
+	$cmd .= "$cmd_env $samba_tool domain backup $backup_cmd --server=$server";
+	$cmd .= " --targetdir=$backupdir -U$dcvars->{DC_USERNAME}\%$dcvars->{DC_PASSWORD}";
+
+	print "Executing: $cmd\n";
+	unless(system($cmd) == 0) {
+		warn("Failed to create backup using: \n$cmd");
+		return undef;
+	}
+
+	# get the name of the backup file created
+	opendir(DIR, $backupdir);
+	my @files = grep(/\.tar/, readdir(DIR));
+	closedir(DIR);
+
+	if(scalar @files != 1) {
+		warn("Backup file not found in directory $backupdir\n");
+		return undef;
+	}
+	my $backup_file = "$backupdir/$files[0]";
+	print "Using backup file $backup_file...\n";
+
+	return $backup_file;
+}
+
+# Restores a backup-file to populate a testenv for a new DC
+sub restore_backup_file
+{
+	my ($self, $backup_file, $restore_opts, $restoredir) = @_;
+
+	# now use samba-tool to restore from the backup file (to a tmp directory)
+	my $tmpdir = File::Temp->newdir();
+	my $samba_tool = Samba::bindir_path($self, "samba-tool");
+	my $cmd = "$samba_tool domain backup restore --backup-file=$backup_file";
+	$cmd .= " --targetdir=$tmpdir $restore_opts";
+
+	print "Executing: $cmd\n";
+	unless(system($cmd) == 0) {
+		warn("Failed to restore backup using: \n$cmd");
+		return -1;
+	}
+
+	# Remove the backed up smb.conf so we avoid using it - we want to retain
+	# the testenv's smb.conf created by provision_raw_step1()
+	$cmd = "rm $tmpdir/etc/smb.conf";
+	print "Executing: $cmd\n";
+	unless(system($cmd) == 0) {
+		warn("Failed to replace backup smb.conf: \n$cmd");
+		return -1;
+	}
+
+	# we installed the restore into an empty tmp directory. Now copy the files
+	# into the directory we'll actually run samba from. This is a bit clunky,
+	# it keeps the testenv happy (by having all the special files that the
+	# testenv needs) and keeps the restore tool happy (by installing into an
+	# empty directory)
+	$cmd = "cp -rpv $tmpdir/. $restoredir/";
+	print "Executing: $cmd\n";
+	unless(system($cmd) == 0) {
+		warn("Failed to copy restored files across using: \n$cmd");
+		return -1;
+	}
+
+	print "Restore complete\n";
+	return 0
+}
+
+# sets up the initial directory and returns the new testenv's env info
+# (without actually doing a 'domain join')
+sub prepare_dc_testenv
+{
+	my ($self, $prefix, $dcname, $domain, $realm, $password) = @_;
+
+	my $ctx = $self->provision_raw_prepare($prefix, "domain controller",
+					       $dcname,
+					       $domain,
+					       $realm,
+					       undef,
+					       "2008",
+					       $password,
+					       undef,
+					       undef);
+
+	# the restore uses a slightly different state-dir location to other testenvs
+	$ctx->{statedir} = "$ctx->{prefix_abs}/state";
+	push(@{$ctx->{directories}}, "$ctx->{statedir}");
+
+	# add support for sysvol/netlogon/tmp shares
+	$ctx->{share} = "$ctx->{prefix_abs}/share";
+	push(@{$ctx->{directories}}, "$ctx->{share}");
+
+	$ctx->{smb_conf_extra_options} = "
+	max xmit = 32K
+	server max protocol = SMB2
+
+[sysvol]
+	path = $ctx->{statedir}/sysvol
+	read only = no
+
+[netlogon]
+	path = $ctx->{statedir}/sysvol/$ctx->{dnsname}/scripts
+	read only = no
+
+[tmp]
+	path = $ctx->{share}
+	read only = no
+	posix:sharedelay = 10000
+	posix:oplocktimeout = 3
+	posix:writetimeupdatedelay = 50000
+
+";
+
+	my $env = $self->provision_raw_step1($ctx);
+
+	$env->{DC_SERVER} = $env->{SERVER};
+	$env->{DC_SERVER_IP} = $env->{SERVER_IP};
+	$env->{DC_SERVER_IPV6} = $env->{SERVER_IPV6};
+	$env->{DC_NETBIOSNAME} = $env->{NETBIOSNAME};
+	$env->{DC_USERNAME} = $env->{USERNAME};
+	$env->{DC_PASSWORD} = $env->{PASSWORD};
+
+    return $env;
+}
+
+
+# Set up a DC testenv solely by using the samba-tool domain backup/restore
+# commands. This proves that we can backup an online DC ('backupfromdc') and
+# use the backup file to create a valid, working samba DC.
+sub setup_restoredc
+{
+	# note: dcvars contains the env info for the dependent testenv ('backupfromdc')
+	my ($self, $prefix, $dcvars) = @_;
+	print "Preparing RESTORE DC...\n";
+
+	my $env = $self->prepare_dc_testenv($prefix, "restoredc",
+						$dcvars->{DOMAIN}, $dcvars->{REALM},
+						$dcvars->{PASSWORD});
+
+	# create a backup of the 'backupfromdc'
+	my $backupdir = File::Temp->newdir();
+	my $backup_file = $self->create_backup($env, $dcvars, $backupdir, "online");
+	unless($backup_file) {
+		return undef;
+	}
+
+	# restore the backup file to populate the restore-DC testenv
+	my $restore_dir = abs_path($prefix);
+	my $ret = $self->restore_backup_file($backup_file,
+							"--newservername=$env->{SERVER}",
+							$restore_dir);
+	unless ($ret == 0) {
+		return undef;
+	}
+
+	# start samba for the restored DC
+	if (not defined($self->check_or_start($env, "standard"))) {
+	    return undef;
+	}
+
+	my $upn_array = ["$env->{REALM}.upn"];
+	my $spn_array = ["$env->{REALM}.spn"];
+
+	$self->setup_namespaces($env, $upn_array, $spn_array);
+
+	return $env;
+}
+
 sub setup_none
 {
 	my ($self, $path) = @_;
-- 
2.7.4


From 8eb46681cd7ccd17bab59c7049a9a15063d7ce74 Mon Sep 17 00:00:00 2001
From: Tim Beale <timbeale at catalyst.net.nz>
Date: Thu, 21 Jun 2018 15:04:00 +1200
Subject: [PATCH 11/11] tests: Add a sub-set of tests to show the restored DC
 is sound

+ Add a new ldapcmp_restoredc.sh test that asserts that the original DC
backed up (backupfromdc) matches the new restored DC.
+ Add a new join_ldapcmp.sh test that asserts we can join a given DC,
and that the resulting DB matches the joined DC
+ Add a new login_basics.py test that sanity-checks Kerberos and NTLM
user login works. (This reuses the password_lockout base code, without
taking as long as the password_lockout tests do). Basic LDAP and SAMR
connections are also tested as a side-effect.
+ run the netlogonsvc test against the restored DC to prove we can
establish a netlogon connection.
+ run the same subset of rpc.echo tests that we do for RODC
+ run dbcheck over the new testenvs at the end of the test run

Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
 source4/dsdb/tests/python/login_basics.py | 181 ++++++++++++++++++++++++++++++
 source4/selftest/tests.py                 |  28 ++++-
 testprogs/blackbox/join_ldapcmp.sh        |  41 +++++++
 testprogs/blackbox/ldapcmp_restoredc.sh   |  65 +++++++++++
 4 files changed, 311 insertions(+), 4 deletions(-)
 create mode 100755 source4/dsdb/tests/python/login_basics.py
 create mode 100755 testprogs/blackbox/join_ldapcmp.sh
 create mode 100755 testprogs/blackbox/ldapcmp_restoredc.sh

diff --git a/source4/dsdb/tests/python/login_basics.py b/source4/dsdb/tests/python/login_basics.py
new file mode 100755
index 0000000..3cb218a
--- /dev/null
+++ b/source4/dsdb/tests/python/login_basics.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Basic sanity-checks of user login. This sanity-checks that a user can login
+# over both NTLM and Kerberos, that incorrect passwords are rejected, and that
+# the user can change their password successfully.
+#
+# Copyright Andrew Bartlett 2018
+#
+from __future__ import print_function
+import optparse
+import sys
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+import samba.getopt as options
+from samba.auth import system_session
+from samba.credentials import Credentials, MUST_USE_KERBEROS
+from samba import dsdb
+from samba.samdb import SamDB
+import samba.tests
+
+parser = optparse.OptionParser("password_lockout.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+    parser.print_usage()
+    sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+global_creds = credopts.get_credentials(lp)
+
+from password_lockout_base import BasePasswordTestCase
+
+#
+# Tests start here
+#
+class BasicUserAuthTests(BasePasswordTestCase):
+
+    def setUp(self):
+        self.host = host
+        self.host_url = host_url
+        self.lp = lp
+        self.global_creds = global_creds
+        self.ldb = SamDB(url=self.host_url, session_info=system_session(self.lp),
+                         credentials=self.global_creds, lp=self.lp)
+        super(BasicUserAuthTests, self).setUp()
+
+    def _test_login_basics(self, creds):
+        username = creds.get_username()
+        userpass = creds.get_password()
+        userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+        if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+            logoncount_relation = 'greater'
+            lastlogon_relation = 'greater'
+            print("Performs a lockout attempt against LDAP using Kerberos")
+        else:
+            logoncount_relation = 'equal'
+            lastlogon_relation = 'equal'
+            print("Performs a lockout attempt against LDAP using NTLM")
+
+        # get the intial logon values for this user
+        res = self._check_account(userdn,
+                                  badPwdCount=0,
+                                  badPasswordTime=("greater", 0),
+                                  logonCount=(logoncount_relation, 0),
+                                  lastLogon=("greater", 0),
+                                  lastLogonTimestamp=("greater", 0),
+                                  userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+                                  msDSUserAccountControlComputed=0,
+                                  msg='Initial test setup...')
+        badPasswordTime = int(res[0]["badPasswordTime"][0])
+        logonCount = int(res[0]["logonCount"][0])
+        lastLogon = int(res[0]["lastLogon"][0])
+        lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+
+        test_creds = self.insta_creds(creds)
+
+        # check logging in with the wrong password fails
+        test_creds.set_password("thatsAcomplPASS1xBAD")
+        self.assertLoginFailure(self.host_url, test_creds, self.lp)
+        res = self._check_account(userdn,
+                                  badPwdCount=1,
+                                  badPasswordTime=("greater", badPasswordTime),
+                                  logonCount=logonCount,
+                                  lastLogon=lastLogon,
+                                  lastLogonTimestamp=lastLogonTimestamp,
+                                  userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+                                  msDSUserAccountControlComputed=0,
+                                  msg='Test login with wrong password')
+        badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+        # check logging in with the correct password succeeds
+        test_creds.set_password(userpass)
+        user_ldb = SamDB(url=self.host_url, credentials=test_creds, lp=self.lp)
+        res = self._check_account(userdn,
+                                  badPwdCount=0,
+                                  badPasswordTime=badPasswordTime,
+                                  logonCount=(logoncount_relation, logonCount),
+                                  lastLogon=('greater', lastLogon),
+                                  lastLogonTimestamp=lastLogonTimestamp,
+                                  userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+                                  msDSUserAccountControlComputed=0,
+                                  msg='Test login with correct password')
+        logonCount = int(res[0]["logonCount"][0])
+        lastLogon = int(res[0]["lastLogon"][0])
+
+        # check that the user can change its password
+        new_password = "thatsAcomplPASS2"
+        user_ldb.modify_ldif("""
+dn: %s
+changetype: modify
+delete: userPassword
+userPassword: %s
+add: userPassword
+userPassword: %s
+""" % (userdn, userpass, new_password))
+
+        # discard the old creds (i.e. get rid of our valid Kerberos ticket)
+        del test_creds
+        test_creds = self.insta_creds(creds)
+        test_creds.set_password(userpass)
+
+        # for Kerberos, logging in with the old password fails
+        if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+            self.assertLoginFailure(self.host_url, test_creds, self.lp)
+            res = self._check_account(userdn,
+                                      badPwdCount=1,
+                                      badPasswordTime=("greater", badPasswordTime),
+                                      logonCount=logonCount,
+                                      lastLogon=lastLogon,
+                                      lastLogonTimestamp=lastLogonTimestamp,
+                                      userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+                                      msDSUserAccountControlComputed=0,
+                                      msg='Test Kerberos login with old password fails')
+            badPasswordTime = int(res[0]["badPasswordTime"][0])
+        else:
+            # for NTLM, logging in with the old password succeeds
+            res = self._check_account(userdn,
+                                      badPwdCount=0,
+                                      badPasswordTime=badPasswordTime,
+                                      logonCount=(logoncount_relation, logonCount),
+                                      lastLogon=(lastlogon_relation, lastLogon),
+                                      lastLogonTimestamp=lastLogonTimestamp,
+                                      userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+                                      msDSUserAccountControlComputed=0,
+                                      msg='Test NTLM login with old password succeeds')
+
+        # check logging in with the new password succeeds
+        test_creds.set_password(new_password)
+        user_ldb = SamDB(url=self.host_url, credentials=test_creds, lp=self.lp)
+        res = self._check_account(userdn,
+                                  badPwdCount=0,
+                                  badPasswordTime=badPasswordTime,
+                                  logonCount=(logoncount_relation, logonCount),
+                                  lastLogon=(lastlogon_relation, lastLogon),
+                                  lastLogonTimestamp=lastLogonTimestamp,
+                                  userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+                                  msDSUserAccountControlComputed=0,
+                                  msg='Test login with new password succeeds')
+
+    def test_login_basics_krb5(self):
+        self._test_login_basics(self.lockout1krb5_creds)
+
+    def test_login_basics_ntlm(self):
+        self._test_login_basics(self.lockout1ntlm_creds)
+
+host_url = "ldap://%s" % host
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index c7b996e..381e7a4 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -805,6 +805,17 @@ plantestsuite_loadlist("samba4.ldap.sort.python(ad_dc_ntvfs)", "ad_dc_ntvfs", [p
 plantestsuite_loadlist("samba4.ldap.vlv.python(ad_dc_ntvfs)", "ad_dc_ntvfs", [python, os.path.join(samba4srcdir, "dsdb/tests/python/vlv.py"), '$SERVER', '-U"$USERNAME%$PASSWORD"', '--workgroup=$DOMAIN', '$LOADLIST', '$LISTOPT'])
 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']:
+    # check that a restored DC matches the original DC (backupfromdc)
+    plantestsuite("samba4.blackbox.ldapcmp_restore", env,
+        ["PYTHON=%s" % python,
+         os.path.join(bbdir, "ldapcmp_restoredc.sh"),
+         '$PREFIX_ABS/backupfromdc', '$PREFIX_ABS/%s' % env])
+    # basic test that we can join the testenv DC
+    plantestsuite("samba4.blackbox.join_ldapcmp", env,
+        ["PYTHON=%s" % python, os.path.join(bbdir, "join_ldapcmp.sh")])
+
 plantestsuite_loadlist("samba4.ldap.rodc.python(rodc)", "rodc",
                        [python,
                         os.path.join(samba4srcdir, "dsdb/tests/python/rodc.py"),
@@ -851,6 +862,13 @@ for env in ["ad_dc_ntvfs"]:
                            extra_path=[os.path.join(samba4srcdir, 'dsdb/tests/python')]
                            )
 
+# this is a basic sanity-check of Kerberos/NTLM user login
+for env in ["restoredc"]:
+    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",
+         '$LOADLIST', '$LISTOPT'])
+
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.upgradeprovisionneeddc")
 planpythontestsuite("ad_dc:local", "samba.tests.posixacl", py3_compatible=True)
 planpythontestsuite("ad_dc_no_nss:local", "samba.tests.posixacl", py3_compatible=True)
@@ -876,8 +894,8 @@ t = "rpc.samr.large-dc"
 plansmbtorture4testsuite(t, "vampire_dc", ['$SERVER', '-U$USERNAME%$PASSWORD', '--workgroup=$DOMAIN'], modname=("samba4.%s.one" % t))
 plansmbtorture4testsuite(t, "vampire_dc", ['$SERVER', '-U$USERNAME%$PASSWORD', '--workgroup=$DOMAIN'], modname="samba4.%s.two" % t)
 
-# some RODC testing
-for env in ['rodc']:
+# RPC smoke-tests for testenvs of interest (RODC, etc)
+for env in ['rodc', 'restoredc']:
     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")
@@ -1056,7 +1074,8 @@ 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"]:
+for env in [ "simpleserver", "fileserver", "nt4_dc", "ad_dc", "ad_dc_ntvfs",
+             "ad_member", "restoredc" ]:
     planoldpythontestsuite(env, "netlogonsvc",
                            extra_path=[os.path.join(srcdir(), 'python/samba/tests')],
                            name="samba.tests.netlogonsvc.python(%s)" % env)
@@ -1079,7 +1098,8 @@ for env in ['vampire_dc', 'promoted_dc', 'rodc']:
 # TODO: Verifying the databases really should be a part of the
 # 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']:
+for env in ["ad_dc_ntvfs", "ad_dc", "fl2000dc", "fl2003dc", "fl2008r2dc",
+            'vampire_dc', 'promoted_dc', 'backupfromdc', 'restoredc']:
     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
diff --git a/testprogs/blackbox/join_ldapcmp.sh b/testprogs/blackbox/join_ldapcmp.sh
new file mode 100755
index 0000000..30d3e1e
--- /dev/null
+++ b/testprogs/blackbox/join_ldapcmp.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+# Does a join against the testenv's DC and then runs ldapcmp on the resulting DB
+
+. `dirname $0`/subunit.sh
+
+TARGET_DIR="$PREFIX_ABS/join_$SERVER"
+
+cleanup_output_dir()
+{
+    if [ -d $TARGET_DIR ]; then
+        rm -fr $TARGET_DIR
+    fi
+}
+
+SAMBA_TOOL="$PYTHON $BINDIR/samba-tool"
+
+join_dc() {
+    JOIN_ARGS="--targetdir=$TARGET_DIR --server=$SERVER -U$USERNAME%$PASSWORD"
+    $SAMBA_TOOL domain join $REALM dc $JOIN_ARGS --option="netbios name = TESTJOINDC"
+}
+
+ldapcmp_result() {
+    DB1_PATH="tdb://$PREFIX_ABS/$SERVER/private/sam.ldb"
+    DB2_PATH="tdb://$TARGET_DIR/private/sam.ldb"
+
+    # interSiteTopologyGenerator gets periodically updated. With the restored
+    # testenvs, it can sometimes point to the old/deleted DC object still
+    $SAMBA_TOOL ldapcmp $DB1_PATH $DB2_PATH --filter=interSiteTopologyGenerator
+}
+
+cleanup_output_dir
+
+# check that we can join this DC
+testit "check_dc_join" join_dc
+
+# check resulting DB matches server DC
+testit "new_db_matches" ldapcmp_result
+
+cleanup_output_dir
+
+exit $failed
diff --git a/testprogs/blackbox/ldapcmp_restoredc.sh b/testprogs/blackbox/ldapcmp_restoredc.sh
new file mode 100755
index 0000000..51951ba
--- /dev/null
+++ b/testprogs/blackbox/ldapcmp_restoredc.sh
@@ -0,0 +1,65 @@
+#!/bin/sh
+# Does an ldapcmp between a newly restored testenv and the original testenv it
+# was based on
+
+if [ $# -lt 2 ]; then
+cat <<EOF
+Usage: $0 ORIG_DC_PREFIX RESTORED_DC_PREFIX
+EOF
+exit 1;
+fi
+
+ORIG_DC_PREFIX_ABS="$1"
+RESTORED_DC_PREFIX_ABS="$2"
+shift 2
+
+. `dirname $0`/subunit.sh
+
+basedn() {
+    SAMDB_PATH=$1
+    $BINDIR/ldbsearch -H $SAMDB_PATH --basedn='' -s base defaultNamingContext | grep defaultNamingContext | awk '{print $2}'
+}
+
+ldapcmp_with_orig() {
+
+    DB1_PATH="tdb://$ORIG_DC_PREFIX_ABS/private/sam.ldb"
+    DB2_PATH="tdb://$RESTORED_DC_PREFIX_ABS/private/sam.ldb"
+
+    # check if the 2 DCs are in different domains
+    DC1_BASEDN=$(basedn $DB1_PATH)
+    DC2_BASEDN=$(basedn $DB2_PATH)
+    BASE_DN_OPTS=""
+
+    # if necessary, pass extra args to ldapcmp to handle the difference in base DNs
+    if [ "$DC1_BASEDN" != "$DC2_BASEDN" ] ; then
+        BASE_DN_OPTS="--base=$DC1_BASEDN --base2=$DC2_BASEDN"
+    fi
+
+    # the restored DC will remove DNS entries for the old DC(s)
+    IGNORE_ATTRS="dnsRecord,dNSTombstoned"
+
+    # DC2 joined DC1, so it will have different DRS info
+    IGNORE_ATTRS="$IGNORE_ATTRS,msDS-NC-Replica-Locations,msDS-HasInstantiatedNCs"
+    IGNORE_ATTRS="$IGNORE_ATTRS,interSiteTopologyGenerator"
+
+    # there's a servicePrincipalName that uses the objectGUID of the DC's NTDS
+    # Settings that will differ between the two DCs
+    IGNORE_ATTRS="$IGNORE_ATTRS,servicePrincipalName"
+
+    # the restore changes the new DC's password twice
+    IGNORE_ATTRS="$IGNORE_ATTRS,lastLogonTimestamp"
+
+    # The RID pools get bumped during the restore process
+    IGNORE_ATTRS="$IGNORE_ATTRS,rIDAllocationPool,rIDAvailablePool"
+
+    # these are just differences between provisioning a domain and joining a DC
+    IGNORE_ATTRS="$IGNORE_ATTRS,localPolicyFlags,operatingSystem,displayName"
+
+    LDAPCMP_CMD="$PYTHON $BINDIR/samba-tool ldapcmp"
+    $LDAPCMP_CMD $DB1_PATH $DB2_PATH --two --filter=$IGNORE_ATTRS $BASE_DN_OPTS
+}
+
+# check that the restored testenv DC basically matches the original
+testit "orig_dc_matches" ldapcmp_with_orig
+
+exit $failed
-- 
2.7.4



More information about the samba-technical mailing list