diff options
author | Aaron Haslett <aaronhaslett@catalyst.net.nz> | 2018-05-01 11:10:11 +1200 |
---|---|---|
committer | Andrew Bartlett <abartlet@samba.org> | 2018-07-03 10:39:14 +0200 |
commit | e0301df111c5adbaa77299bb5d43c180cdd9df2c (patch) | |
tree | 3ee4669a964dd9223312089d64ddde4a103c1387 /python | |
parent | c2422593f46a7f4c1bd7421919f48b1fe7550e59 (diff) | |
download | samba-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.py | 1 | ||||
-rw-r--r-- | python/samba/netcmd/domain.py | 2 | ||||
-rw-r--r-- | python/samba/netcmd/domain_backup.py | 229 |
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()} |