netcmd: domain backup online command
authorAaron Haslett <aaronhaslett@catalyst.net.nz>
Mon, 30 Apr 2018 23:10:11 +0000 (11:10 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Tue, 3 Jul 2018 08:39:14 +0000 (10:39 +0200)
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@catalyst.net.nz>
Signed-off-by: Tim Beale <timbeale@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Gary Lockyer <gary@catalyst.net.nz>
docs-xml/manpages/samba-tool.8.xml
python/samba/join.py
python/samba/netcmd/domain.py
python/samba/netcmd/domain_backup.py [new file with mode: 0644]

index f785e53ae73c8e800911085cd473ab81e50b4b58..70ff956cef7bab43d4d7f5545853c7a4477aee9f 100644 (file)
        <para>Manage Domain.</para>
 </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
index b7ab1eded58ffa607de83dc725eed91d438698e9..e7ea11187ef1bf8d6410a2611ff5781036a2a862 100644 (file)
@@ -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,
index 3dbe2fba9e9721e7b712ba98e3a5858933e160a8..86249073652cc0a01e002fcafae0b5e2c411333a 100644 (file)
@@ -100,6 +100,7 @@ from samba.provision.common import (
 )
 
 from samba.netcmd.pso import cmd_domain_passwordsettings_pso
+from samba.netcmd.domain_backup import cmd_domain_backup
 
 string_version_to_constant = {
     "2008_R2" : DS_DOMAIN_FUNCTION_2008_R2,
@@ -4334,3 +4335,4 @@ class cmd_domain(SuperCommand):
     subcommands["tombstones"] = cmd_domain_tombstones()
     subcommands["schemaupgrade"] = cmd_domain_schema_upgrade()
     subcommands["functionalprep"] = cmd_domain_functional_prep()
+    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 (file)
index 0000000..53e65b9
--- /dev/null
@@ -0,0 +1,229 @@
+# domain_backup
+#
+# Copyright Andrew Bartlett <abartlet@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
+import os
+import sys
+import tarfile
+import logging
+import shutil
+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, credentials=creds,
+                           session_info=system_session(), 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()}