summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorAaron Haslett <aaronhaslett@catalyst.net.nz>2018-05-01 11:10:11 +1200
committerAndrew Bartlett <abartlet@samba.org>2018-07-03 10:39:14 +0200
commite0301df111c5adbaa77299bb5d43c180cdd9df2c (patch)
tree3ee4669a964dd9223312089d64ddde4a103c1387 /python
parentc2422593f46a7f4c1bd7421919f48b1fe7550e59 (diff)
downloadsamba-e0301df111c5adbaa77299bb5d43c180cdd9df2c.tar.gz
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@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>
Diffstat (limited to 'python')
-rw-r--r--python/samba/join.py1
-rw-r--r--python/samba/netcmd/domain.py2
-rw-r--r--python/samba/netcmd/domain_backup.py229
3 files changed, 232 insertions, 0 deletions
diff --git a/python/samba/join.py b/python/samba/join.py
index b7ab1eded58..e7ea11187ef 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 3dbe2fba9e9..86249073652 100644
--- a/python/samba/netcmd/domain.py
+++ b/python/samba/netcmd/domain.py
@@ -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
index 00000000000..53e65b9373f
--- /dev/null
+++ b/python/samba/netcmd/domain_backup.py
@@ -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()}