[PATCH] Domain backup and restore samba-tool commands
Tim Beale
timbeale at catalyst.net.nz
Fri Jun 29 04:35:07 UTC 2018
Hi,
Updated patch-set (attached) is ready for review and delivery. Branch
also here:
https://gitlab.com/catalyst-samba/samba/commits/tim-backup-tool
Note that this patch-set is dependent on the rename changes sent out
earlier today, which are pending autobuild/delivery to master.
https://lists.samba.org/archive/samba-technical/2018-June/128786.html
CI link:
https://gitlab.com/catalyst-samba/samba/pipelines/24803625
Review appreciated.
Thanks,
Tim
-------------- next part --------------
From e5676fb21b2d1b15626498caa74697b56fb6242f 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/5] 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 | 214 +++++++++++++++++++++++++++++++++++
4 files changed, 227 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 41a2051..a97924d 100644
--- a/python/samba/join.py
+++ b/python/samba/join.py
@@ -1496,6 +1496,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..114e993
--- /dev/null
+++ b/python/samba/netcmd/domain_backup.py
@@ -0,0 +1,214 @@
+# 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
+import tempfile
+import samba
+import samba.getopt as options
+from samba.samdb import SamDB
+import ldb
+from samba import smb
+from samba.ntacls import backup_online
+from samba.auth import system_session
+from samba.join import DCJoinContext, 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 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()
+
+ # Grab the remote DC's sysvol files and bundle them into a tar file
+ sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
+ smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
+ backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
+
+ # remove the default sysvol files created by the clone (we want to
+ # make sure we restore the sysvol.tar.gz files instead)
+ shutil.rmtree(paths.sysvol)
+
+ # 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)
+
+ # 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 b616922a6afef40946bc794a90a98294d5f6e62c 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 2/5] 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 | 35 ++++---
python/samba/netcmd/domain_backup.py | 187 ++++++++++++++++++++++++++++++++++-
3 files changed, 211 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 a97924d..22854ae 100644
--- a/python/samba/join.py
+++ b/python/samba/join.py
@@ -57,7 +57,7 @@ class DCJoinContext(object):
netbios_name=None, targetdir=None, domain=None,
machinepass=None, use_ntvfs=False, dns_backend=None,
promote_existing=False, plaintext_secrets=False,
- backend_store=None):
+ backend_store=None, forced_local_samdb=None):
if site is None:
site = "Default-First-Site-Name"
@@ -79,16 +79,20 @@ class DCJoinContext(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"])
@@ -563,7 +567,9 @@ class DCJoinContext(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])
@@ -572,7 +578,7 @@ class DCJoinContext(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)
@@ -602,12 +608,15 @@ class DCJoinContext(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 114e993..2a79c51 100644
--- a/python/samba/netcmd/domain_backup.py
+++ b/python/samba/netcmd/domain_backup.py
@@ -23,12 +23,18 @@ import samba.getopt as options
from samba.samdb import SamDB
import ldb
from samba import smb
-from samba.ntacls import backup_online
+from samba.ntacls import backup_online, backup_restore
from samba.auth import system_session
from samba.join import DCJoinContext, join_clone
from samba.dcerpc.security import dom_sid
from samba.netcmd import Option, CommandError
import traceback
+from samba.dcerpc import misc
+from samba import Ldb
+from fsmo import cmd_fsmo_seize
+from samba.provision import make_smbconf
+from samba.upgradehelpers import update_krbtgt_account_password
+from samba.remove_dc import remove_dc
tmpdir = 'backup_temp_dir'
def rm_tmp():
@@ -209,6 +215,181 @@ 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.
+
+ Note that this command should be run as the root user - it will fail
+ otherwise.'''
+
+ 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 = DCJoinContext(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:
+ remove_dc(samdb, logger, cn)
+
+ # Update tgt and DC passwords twice
+ update_krbtgt_account_password(samdb)
+ update_krbtgt_account_password(samdb)
+
+ # restore the sysvol directory from the backup tar file, including the
+ # original NTACLs. Note that the backup_restore() will fail if not root
+ sysvol_tar = os.path.join(targetdir, 'sysvol.tar.gz')
+ dest_sysvol_dir = lp.get('path', 'sysvol')
+ if not os.path.exists(dest_sysvol_dir):
+ os.makedirs(dest_sysvol_dir)
+ backup_restore(sysvol_tar, dest_sysvol_dir, samdb, smbconf)
+ os.remove(sysvol_tar)
+
+ # 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()}
+ '''Create or restore a backup of the domain.'''
+ subcommands = {'online': cmd_domain_backup_online(),
+ 'restore': cmd_domain_backup_restore()}
--
2.7.4
From 3acb77df006169e90c91655217824f7c4dff5784 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 3/5] tests: Add tests for the domain backup online/restore
commands
Signed-off-by: Aaron Haslett <aaronhaslett at catalyst.net.nz>
Signed-off-by: Tim Beale <timbeale at catalyst.net.nz>
---
python/samba/tests/domain_backup.py | 257 ++++++++++++++++++++++++++++++++++++
source4/selftest/tests.py | 3 +
2 files changed, 260 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..42aa7f5
--- /dev/null
+++ b/python/samba/tests/domain_backup.py
@@ -0,0 +1,257 @@
+# 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, shutil
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba.tests import TestCaseInTempDir, env_loadparm
+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(secrets_path, lp):
+ secrets_ldb = Ldb(secrets_path, 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, TestCaseInTempDir):
+
+ def setUp(self):
+ super(SambaToolCmdTest, self).setUp()
+
+ server = os.environ["DC_SERVER"]
+ self.user_auth = "-U%s%%%s" % (os.environ["DC_USERNAME"],
+ os.environ["DC_PASSWORD"])
+
+ # LDB connection to the original server being backed up
+ self.ldb = self.getSamDB("-H", "ldap://%s" % server,
+ self.user_auth)
+ self.new_server = "BACKUPSERV"
+ self.server = server.upper()
+ self.basedn = str(self.ldb.get_default_basedn())
+
+ def assert_partitions_present(self, samdb):
+ """Asserts all expected partitions are present in the backup samdb"""
+ res = samdb.search(base="", scope=ldb.SCOPE_BASE,
+ attrs=['namingContexts'])
+ actual_ncs = [str(r) for r in res[0].get('namingContexts')]
+
+ basedn = self.basedn
+ config_dn = "CN=Configuration,%s" % basedn
+ expected_ncs = [ basedn, config_dn, "CN=Schema,%s" % config_dn,
+ "DC=DomainDnsZones,%s" % basedn,
+ "DC=ForestDnsZones,%s" % basedn ]
+
+ for nc in expected_ncs:
+ self.assertTrue(nc in actual_ncs,
+ "%s not in %s" %(nc, str(actual_ncs)))
+
+ def assert_dcs_present(self, samdb, expected_server):
+ """Asserts that only the expected server is present in the restored DB"""
+ res = samdb.search(samdb.get_config_basedn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression="(&(objectClass=Server)(serverReference=*))")
+ self.assertTrue(len(res) == 1)
+ self.assertTrue(expected_server in str(res[0].dn))
+
+ def restore_dir(self):
+ extract_dir = os.path.join(self.tempdir, 'tree')
+ if not os.path.exists(extract_dir):
+ os.mkdir(extract_dir)
+ self.addCleanup(shutil.rmtree, extract_dir)
+ return extract_dir
+
+ def untar_backup(self, backup_file):
+ """Untar the backup file's raw contents (i.e. not a proper restore)"""
+ extract_dir = self.restore_dir()
+ with tarfile.open(backup_file) as tf:
+ tf.extractall(extract_dir)
+
+ def test_online_untar(self):
+ """Creates a backup, untars the raw files, and sanity-checks the DB"""
+ backup_file = self.create_backup()
+ self.untar_backup(backup_file)
+
+ private_dir = os.path.join(self.restore_dir(), "private")
+ samdb_path = os.path.join(private_dir, "sam.ldb")
+ lp = env_loadparm()
+ samdb = SamDB(url=samdb_path, session_info=system_session(), lp=lp)
+
+ # check that backup markers were added to the DB
+ res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
+ scope=ldb.SCOPE_BASE,
+ attrs=['sidForRestore', 'backupDate'])
+ 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.
+ secrets_path = os.path.join(private_dir, "secrets.ldb")
+ res = get_prim_dom(secrets_path, lp)
+ self.assertEqual(len(res), 0)
+
+ # sanity-check that all the partitions got backed up
+ self.assert_partitions_present(samdb)
+
+ def test_online_restore(self):
+ """Does a backup/restore, with specific checks of the resulting DB"""
+ backup_file = self.create_backup()
+ self.restore_backup(backup_file)
+ lp = self.check_restored_smbconf()
+ self.check_restored_database(lp)
+
+ def create_smbconf(self, settings):
+ """Creates a very basic smb.conf to pass to the restore tool"""
+ smbconf = os.path.join(self.tempdir, "smb.conf")
+ f = open(smbconf, 'w')
+ try:
+ f.write("[globals]\n")
+ for key, val in settings.items():
+ f.write("\t%s = %s\n" % (key, val))
+ finally:
+ f.close()
+ self.addCleanup(os.remove, smbconf)
+ return smbconf
+
+ def test_online_restore_with_conf(self):
+ """Checks smb.conf values passed to the restore are honoured appropriately"""
+ backup_file = self.create_backup()
+
+ # create an smb.conf that we pass to the restore. The netbios/state
+ # dir should get overridden by the restore, the other settings should
+ # trickle through into the restored dir's smb.conf
+ settings = { 'state directory' : '/var/run',
+ 'netbios name' : 'FOOBAR' }
+ assert_settings = { 'log level' : '3',
+ 'prefork children' : '20' }
+ settings.update(assert_settings)
+ smbconf = self.create_smbconf(settings)
+ self.restore_backup(backup_file, ["--configfile=" + smbconf])
+
+ # this will check netbios name/state dir
+ lp = self.check_restored_smbconf()
+ self.check_restored_database(lp)
+
+ # check the remaining settings are still intact
+ for key, val in assert_settings.items():
+ self.assertEqual(str(lp.get(key)), val,
+ "'%s' was '%s' in smb.conf" % (key, lp.get(key)))
+
+ def check_restored_smbconf(self):
+ """Sanity-check important values in the restored smb.conf are correct"""
+ smbconf = os.path.join(self.restore_dir(), "etc", "smb.conf")
+ bkp_lp = param.LoadParm(filename_for_non_global_lp = smbconf)
+ self.assertEqual(bkp_lp.get('netbios name'), self.new_server)
+
+ # we restore with a fixed directory structure, so we can sanity-check
+ # that the core filepaths settings are what we expect them to be
+ private_dir = os.path.join(self.restore_dir(), "private")
+ self.assertEqual(bkp_lp.get('private dir'), private_dir)
+ state_dir = os.path.join(self.restore_dir(), "state")
+ self.assertEqual(bkp_lp.get('state directory'), state_dir)
+ return bkp_lp
+
+ def check_restored_database(self, bkp_lp):
+ paths = provision.provision_paths_from_lp(bkp_lp, bkp_lp.get("realm"))
+
+ bkp_pd = get_prim_dom(paths.secrets, bkp_lp)
+ self.assertEqual(len(bkp_pd), 1)
+ acn = bkp_pd[0].get('samAccountName')
+ self.assertIsNotNone(acn)
+ self.assertEqual(acn[0].replace('$', ''), self.new_server)
+ self.assertIsNotNone(bkp_pd[0].get('secret'))
+
+ samdb = SamDB(url=paths.samdb, session_info=system_session(),
+ lp=bkp_lp, credentials=self.get_credentials())
+
+ # check that the backup markers have been removed from the restored DB
+ res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
+ scope=ldb.SCOPE_BASE,
+ attrs=['sidForRestore', 'backupDate'])
+ self.assertEqual(len(res), 1)
+ self.assertIsNone(res[0].get('sidForRestore'))
+ self.assertIsNone(res[0].get('backupDate'))
+
+ # check the restored DB has the expected partitions/DC/FSMO roles
+ self.assert_partitions_present(samdb)
+ self.assert_dcs_present(samdb, self.new_server)
+ self.assert_fsmo_roles(samdb, self.new_server, self.server)
+
+ def assert_fsmo_roles(self, samdb, server, exclude_server):
+ """Asserts the expected server is the FSMO role owner"""
+ 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(server) in owner.extended_str(),
+ "Expected %s to own FSMO role %s" %(server, role))
+ self.assertTrue("CN={},".format(exclude_server)
+ not in owner.extended_str(),
+ "%s found as FSMO %s role owner" %(server, role))
+
+ def create_backup(self):
+ """Runs the backup cmd to produce a backup file for the testenv DC"""
+ # Run the backup command and check we got one backup tar file
+ args = ["domain", "backup", "online",
+ "--server=" + self.server]
+ args += [self.user_auth, "--targetdir=" + self.tempdir]
+ (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(self.tempdir)
+ if fn.startswith("samba-backup-") and
+ fn.endswith(".tar.bz2")]
+ self.assertTrue(len(tar_files) == 1,
+ "Domain backup created %u tar files" % len(tar_files))
+
+ # clean up the backup file once the test finishes
+ backup_file = os.path.join(self.tempdir, tar_files[0])
+ self.addCleanup(os.remove, backup_file)
+ return backup_file
+
+ def restore_backup(self, backup_file, extra_args=None):
+ """Restores the samba directory files from a given backup"""
+ # Run the restore command
+ extract_dir = self.restore_dir()
+ args = ["domain", "backup", "restore", "--backup-file=" + backup_file,
+ "--targetdir=" + extract_dir,
+ "--newservername=BACKUPSERV"]
+ if extra_args:
+ args += extra_args
+ (result, out, err) = self.runsubcmd(*args)
+ self.assertCmdSuccess(result, out, err,
+ "Ensuring domain backup restore ran successfully")
+
+ # sanity-check the restore doesn't modify the original DC as a side-effect
+ self.assert_partitions_present(self.ldb)
+ self.assert_dcs_present(self.ldb, self.server)
+ self.assert_fsmo_roles(self.ldb, self.server, self.new_server)
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index 2551603..af06828 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -731,6 +731,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 6bd61ceac170a78fb20fafd7471c71a424877b98 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 4/5] 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 2a79c51..6270ffd 100644
--- a/python/samba/netcmd/domain_backup.py
+++ b/python/samba/netcmd/domain_backup.py
@@ -249,7 +249,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 56a3d292c523ff404f18a66eb1fcc8c10eefe3fc 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 5/5] 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 af06828..ee1d81f 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -811,6 +811,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"),
@@ -857,6 +868,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)
@@ -887,8 +905,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")
@@ -1067,7 +1085,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)
@@ -1090,7 +1109,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