[PATCH] Domain backup and restore samba-tool commands
Tim Beale
timbeale at catalyst.net.nz
Thu Jun 28 05:14:09 UTC 2018
Hi,
FYI, attached are the latest patches for the backup/restore tool. The
samba-tool patches have been reworked quite a bit, and I think are
pretty much ready now (apart from integrating with Joe's NTACL patches).
The backup-specific tests (patch #4) probably still need extending a bit.
CI link:
https://gitlab.com/catalyst-samba/samba/pipelines/24728231
Cheers,
Tim
-------------- next part --------------
From 1535cc430c60d40e2b81f6421e7ba2a5f000f69c 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 1/6] join: Pipe through dns_backend option for clones
Allow join_clone() calls to specify a dns_backend parameter for the new
cloned DB.
Signed-off-by: Aaron Haslett<aaronhaslett at catalyst.net.nz>
---
python/samba/join.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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)
--
2.7.4
From ae254a8b01d648fed29aa76336646fe739d9a5f6 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 2/6] 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>
Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
docs-xml/manpages/samba-tool.8.xml | 10 ++
python/samba/join.py | 1 +
python/samba/netcmd/domain.py | 2 +
python/samba/netcmd/domain_backup.py | 231 +++++++++++++++++++++++++++++++++++
4 files changed, 244 insertions(+)
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 b5cdf52..61f202c 100644
--- a/python/samba/join.py
+++ b/python/samba/join.py
@@ -1514,6 +1514,7 @@ def join_clone(logger=None, server=None, creds=None, lp=None,
ctx.do_join()
logger.info("Cloned domain %s (SID %s)" % (ctx.domain_name, ctx.domsid))
+ return ctx
def join_subdomain(logger=None, server=None, creds=None, lp=None, site=None,
netbios_name=None, targetdir=None, parent_domain=None, dnsdomain=None,
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..74f105f
--- /dev/null
+++ b/python/samba/netcmd/domain_backup.py
@@ -0,0 +1,231 @@
+# 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)
+
+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.
+ check_online_backup_args(logger, credopts, server, targetdir)
+
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp)
+
+ if not os.path.exists(targetdir):
+ logger.info('Creating targetdir %s...' % targetdir)
+ os.makedirs(targetdir)
+
+ # Run a clone join on the remote
+ ctx = join_clone(logger=logger, creds=creds, lp=lp,
+ include_secrets=True, dns_backend='SAMBA_INTERNAL',
+ server=server, targetdir=tmpdir)
+
+ # get the paths used for the clone, then drop the old samdb connection
+ paths = ctx.paths
+ del ctx
+
+ # 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=creds, lp=lp)
+ new_sid = get_sid_for_restore(remote_sam)
+ realm = remote_sam.domain_dns_name()
+
+ # Edit the downloaded sam.ldb to mark it as a backup
+ samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=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
+ copy_sysvol_files(server, creds, paths.sysvol)
+
+ # Add everything in the tmpdir to the backup tar file
+ 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 b5fd163f91cd3f6d14092595ed3dc14863f65bf6 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 3/6] 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>
Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
docs-xml/manpages/samba-tool.8.xml | 5 ++
python/samba/join.py | 36 +++++---
python/samba/netcmd/domain_backup.py | 170 ++++++++++++++++++++++++++++++++++-
3 files changed, 197 insertions(+), 14 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 61f202c..a9488ef 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):
if site is None:
site = "Default-First-Site-Name"
@@ -81,16 +82,20 @@ class dc_join(object):
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()
diff --git a/python/samba/netcmd/domain_backup.py b/python/samba/netcmd/domain_backup.py
index 74f105f..e47e733 100644
--- a/python/samba/netcmd/domain_backup.py
+++ b/python/samba/netcmd/domain_backup.py
@@ -21,13 +21,18 @@ 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.provision import make_smbconf
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'
@@ -226,6 +231,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,
+ }
+
+ 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))
+
+ # ldapcmp prefers the server's netBIOS name in upper-case
+ newservername = newservername.upper()
+
+ # extract the backup .tar to a temp directory
+ targetdir = os.path.abspath(targetdir)
+ tf = tarfile.open(backup_file)
+ tf.extractall(targetdir)
+ tf.close()
+
+ # use the smb.conf that got backed up, by default (save what was
+ # actually backed up, before we mess with it)
+ smbconf = os.path.join(targetdir, 'etc', 'smb.conf')
+ shutil.copyfile(smbconf, smbconf + ".orig")
+
+ # if a smb.conf was specified on the cmd line, then use that instead
+ cli_smbconf = sambaopts.get_loadparm_path()
+ if cli_smbconf:
+ logger.info("Using %s as restored domain's smb.conf" % cli_smbconf)
+ shutil.copyfile(cli_smbconf, smbconf)
+
+ lp = samba.param.LoadParm()
+ lp.load(smbconf)
+
+ # open a DB connection to the restored DB
+ private_dir = os.path.join(targetdir, 'private')
+ samdb_path = os.path.join(private_dir, 'sam.ldb')
+ samdb = SamDB(url=samdb_path, session_info=system_session(), 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')]
+
+ creds = credopts.get_credentials(lp)
+ ctx = dc_join(logger, creds=creds, lp=lp, forced_local_samdb=samdb,
+ netbios_name=newservername)
+ ctx.nc_list = ncs
+ ctx.full_nc_list = ncs
+ ctx.userAccountControl = samba.dsdb.UF_SERVER_TRUST_ACCOUNT |\
+ samba.dsdb.UF_TRUSTED_FOR_DELEGATION
+
+ # rewrite the smb.conf to make sure it uses the new targetdir settings.
+ # (This doesn't update all filepaths in a customized config, but it
+ # corrects the same paths that get set by a new provision)
+ logger.info('Updating basic smb.conf settings...')
+ make_smbconf(smbconf, newservername, ctx.domain_name, ctx.realm,
+ targetdir, serverrole="active directory domain controller",
+ lp=lp)
+
+ # 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_path = os.path.join(private_dir, 'secrets.ldb')
+ secrets_ldb = Ldb(secrets_path, 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 DB markers added by the 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)
+
+ logger.info("Backup file successfully restored to %s" % targetdir)
+ logger.info("Check the smb.conf settings are correct before starting samba.")
+
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 e7c989271631a89decad9c388fc754a7078f7aaa 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 4/6] tests: Add tests for the domain backup online/restore
commands
Signed-off-by: Aaron Haslett <aaronhaslett at catalyst.net.nz>
---
python/samba/tests/domain_backup.py | 174 ++++++++++++++++++++++++++++++++++++
source4/selftest/tests.py | 3 +
2 files changed, 177 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..d4920ab
--- /dev/null
+++ b/python/samba/tests/domain_backup.py
@@ -0,0 +1,174 @@
+# 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, 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):
+
+ def test_online_untar(self):
+ cfg_fn = self.run_backup_and_restore()
+ bkp_lp = param.LoadParm(filename_for_non_global_lp = cfg_fn)
+ 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.
+ 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"))
+
+ 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'))
+
+ 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")
+ 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)
+ 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])
+ 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"]:
+ path = os.path.join(extract_dir, tmp_subdir_to_make)
+ if not os.path.exists(path):
+ os.mkdir(path)
+
+ return cfg_fn
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index 1b592c3..7a99fe9 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 06ba85e2d12401712c3d346248f73a5b2edd83ab 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 5/6] 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>
---
python/samba/netcmd/domain_backup.py | 4 +-
selftest/target/Samba.pm | 2 +
selftest/target/Samba4.pm | 212 +++++++++++++++++++++++++++++++++++
3 files changed, 217 insertions(+), 1 deletion(-)
diff --git a/python/samba/netcmd/domain_backup.py b/python/samba/netcmd/domain_backup.py
index e47e733..e5b2ddf 100644
--- a/python/samba/netcmd/domain_backup.py
+++ b/python/samba/netcmd/domain_backup.py
@@ -263,7 +263,9 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
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):
+ # allow restoredc to install into a directory prepopulated by selftest
+ if (os.path.exists(targetdir) and os.listdir(targetdir) and
+ os.environ.get('SAMBA_SELFTEST') != '1'):
raise CommandError('Target directory is not empty')
if not newservername:
raise CommandError('Server name required')
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..4e8c83a 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,215 @@ 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, $smbconf) = @_;
+
+ # pass the restore command the testenv's smb.conf that we've already
+ # generated. But move it to a temp-dir first, so that the restore doesn't
+ # overwrite it
+ my $tmpdir = File::Temp->newdir();
+ my $tmpconf = "$tmpdir/smb.conf";
+ my $cmd = "cp $smbconf $tmpconf";
+ unless(system($cmd) == 0) {
+ warn("Failed to backup smb.conf using: \n$cmd");
+ return -1;
+ }
+
+ my $samba_tool = Samba::bindir_path($self, "samba-tool");
+ $cmd = "$samba_tool domain backup restore --backup-file=$backup_file";
+ $cmd .= " --targetdir=$restoredir $restore_opts --configfile=$tmpconf";
+
+ print "Executing: $cmd\n";
+ unless(system($cmd) == 0) {
+ warn("Failed to restore backup 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, $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) = @_;
--
2.7.4
From 6d3e6ecf83c883870cacd389b06d61a5a9198aeb 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 6/6] 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 7a99fe9..61af03b 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -807,6 +807,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"),
@@ -853,6 +864,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)
@@ -883,8 +901,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")
@@ -1063,7 +1081,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)
@@ -1086,7 +1105,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