import shutil
import tempfile
import samba
+import tdb
import samba.getopt as options
from samba.samdb import SamDB
import ldb
from samba import smb
-from samba.ntacls import backup_online, backup_restore
+from samba.ntacls import backup_online, backup_restore, backup_offline
from samba.auth import system_session
from samba.join import DCJoinContext, join_clone, DCCloneAndRenameContext
from samba.dcerpc.security import dom_sid
from samba.provision.sambadns import (fill_dns_data_partitions,
get_dnsadmins_sid,
get_domainguid)
+from samba.tdb_util import tdb_copy
+from samba.mdb_util import mdb_copy
+import errno
+import tdb
+from subprocess import CalledProcessError
# work out a SID (based on a free RID) to use when the domain gets restored.
shutil.rmtree(tmpdir)
+class cmd_domain_backup_offline(samba.netcmd.Command):
+ '''Backup the local domain directories safely into a tar file.
+
+ Takes a backup copy of the current domain from the local files on disk,
+ with proper locking of the DB to ensure consistency. If the domain were to
+ undergo a catastrophic failure, then the backup file can be used to recover
+ the domain.
+
+ An offline backup differs to an online backup in the following ways:
+ - a backup can be created even if the DC isn't currently running.
+ - includes non-replicated attributes that an online backup wouldn't store.
+ - takes a copy of the raw database files, which has the risk that any
+ hidden problems in the DB are preserved in the backup.'''
+
+ synopsis = "%prog [options]"
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ }
+
+ takes_options = [
+ Option("--targetdir",
+ help="Output directory (required)",
+ type=str),
+ ]
+
+ backup_ext = '.bak-offline'
+
+ def offline_tdb_copy(self, path):
+ backup_path = path + self.backup_ext
+ try:
+ tdb_copy(path, backup_path, readonly=True)
+ except CalledProcessError as copy_err:
+ # If the copy didn't work, check if it was caused by an EINVAL
+ # error on opening the DB. If so, it's a mutex locked database,
+ # which we can safely ignore.
+ try:
+ tdb.open(path)
+ except Exception as e:
+ if hasattr(e, 'errno') and e.errno == errno.EINVAL:
+ return
+ raise e
+ raise copy_err
+ if not os.path.exists(backup_path):
+ s = "tdbbackup said backup succeeded but {} not found"
+ raise CommandError(s.format(backup_path))
+
+ def offline_mdb_copy(self, path):
+ mdb_copy(path, path + self.backup_ext)
+
+ # Secrets databases are a special case: a transaction must be started
+ # on the secrets.ldb file before backing up that file and secrets.tdb
+ def backup_secrets(self, private_dir, lp, logger):
+ secrets_path = os.path.join(private_dir, 'secrets')
+ secrets_obj = Ldb(secrets_path + '.ldb', lp=lp)
+ logger.info('Starting transaction on ' + secrets_path)
+ secrets_obj.transaction_start()
+ self.offline_tdb_copy(secrets_path + '.ldb')
+ self.offline_tdb_copy(secrets_path + '.tdb')
+ secrets_obj.transaction_cancel()
+
+ # sam.ldb must have a transaction started on it before backing up
+ # everything in sam.ldb.d with the appropriate backup function.
+ def backup_smb_dbs(self, private_dir, samdb, lp, logger):
+ # First, determine if DB backend is MDB. Assume not unless there is a
+ # 'backendStore' attribute on @PARTITION containing the text 'mdb'
+ store_label = "backendStore"
+ res = samdb.search(base="@PARTITION", scope=ldb.SCOPE_BASE,
+ attrs=[store_label])
+ mdb_backend = store_label in res[0] and res[0][store_label][0] == 'mdb'
+
+ sam_ldb_path = os.path.join(private_dir, 'sam.ldb')
+ copy_function = None
+ if mdb_backend:
+ logger.info('MDB backend detected. Using mdb backup function.')
+ copy_function = self.offline_mdb_copy
+ else:
+ logger.info('Starting transaction on ' + sam_ldb_path)
+ copy_function = self.offline_tdb_copy
+ sam_obj = Ldb(sam_ldb_path, lp=lp)
+ sam_obj.transaction_start()
+
+ logger.info(' backing up ' + sam_ldb_path)
+ self.offline_tdb_copy(sam_ldb_path)
+ sam_ldb_d = sam_ldb_path + '.d'
+ for sam_file in os.listdir(sam_ldb_d):
+ sam_file = os.path.join(sam_ldb_d, sam_file)
+ if sam_file.endswith('.ldb'):
+ logger.info(' backing up locked/related file ' + sam_file)
+ copy_function(sam_file)
+ else:
+ logger.info(' copying locked/related file ' + sam_file)
+ shutil.copyfile(sam_file, sam_file + self.backup_ext)
+
+ if not mdb_backend:
+ sam_obj.transaction_cancel()
+
+ # Find where a path should go in the fixed backup archive structure.
+ def get_arc_path(self, path, conf_paths):
+ backup_dirs = {"private": conf_paths.private_dir,
+ "statedir": conf_paths.state_dir,
+ "etc": os.path.dirname(conf_paths.smbconf)}
+ matching_dirs = [(_, p) for (_, p) in backup_dirs.items() if
+ path.startswith(p)]
+ arc_path, fs_path = matching_dirs[0]
+
+ # If more than one directory is a parent of this path, then at least
+ # one configured path is a subdir of another. Use closest match.
+ if len(matching_dirs) > 1:
+ arc_path, fs_path = max(matching_dirs, key=lambda (_, p): len(p))
+ arc_path += path[len(fs_path):]
+
+ return arc_path
+
+ def run(self, sambaopts=None, targetdir=None):
+
+ logger = logging.getLogger()
+ logger.setLevel(logging.DEBUG)
+ logger.addHandler(logging.StreamHandler(sys.stdout))
+
+ # Get the absolute paths of all the directories we're going to backup
+ lp = sambaopts.get_loadparm()
+
+ paths = samba.provision.provision_paths_from_lp(lp, lp.get('realm'))
+ if not (paths.samdb and os.path.exists(paths.samdb)):
+ raise CommandError('No sam.db found. This backup ' +
+ 'tool is only for AD DCs')
+
+ check_targetdir(logger, targetdir)
+
+ samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
+ sid = get_sid_for_restore(samdb)
+
+ backup_dirs = [paths.private_dir, paths.state_dir,
+ os.path.dirname(paths.smbconf)] # etc dir
+ logger.info('running backup on dirs: {}'.format(backup_dirs))
+
+ # Recursively get all file paths in the backup directories
+ all_files = []
+ for backup_dir in backup_dirs:
+ for (working_dir, _, filenames) in os.walk(backup_dir):
+ if working_dir.startswith(paths.sysvol):
+ continue
+
+ for filename in filenames:
+ if filename in all_files:
+ continue
+
+ # Assume existing backup files are from a previous backup.
+ # Delete and ignore.
+ if filename.endswith(self.backup_ext):
+ os.remove(os.path.join(working_dir, filename))
+ continue
+ all_files.append(os.path.join(working_dir, filename))
+
+ # Backup secrets, sam.ldb and their downstream files
+ self.backup_secrets(paths.private_dir, lp, logger)
+ self.backup_smb_dbs(paths.private_dir, samdb, lp, logger)
+
+ # Open the new backed up samdb, flag it as backed up, and write
+ # the next SID so the restore tool can add objects.
+ # WARNING: Don't change this code unless you know what you're doing.
+ # Writing to a .bak file only works because the DN being
+ # written to happens to be top level.
+ samdb = SamDB(url=paths.samdb + self.backup_ext,
+ session_info=system_session(), lp=lp)
+ time_str = get_timestamp()
+ add_backup_marker(samdb, "backupDate", time_str)
+ add_backup_marker(samdb, "sidForRestore", sid)
+
+ # Now handle all the LDB and TDB files that are not linked to
+ # anything else. Use transactions for LDBs.
+ for path in all_files:
+ if not os.path.exists(path + self.backup_ext):
+ if path.endswith('.ldb'):
+ logger.info('Starting transaction on solo db: ' + path)
+ ldb_obj = Ldb(path, lp=lp)
+ ldb_obj.transaction_start()
+ logger.info(' running tdbbackup on the same file')
+ self.offline_tdb_copy(path)
+ ldb_obj.transaction_cancel()
+ elif path.endswith('.tdb'):
+ logger.info('running tdbbackup on lone tdb file ' + path)
+ self.offline_tdb_copy(path)
+
+ # Now make the backup tar file and add all
+ # backed up files and any other files to it.
+ temp_tar_dir = tempfile.mkdtemp(dir=targetdir,
+ prefix='INCOMPLETEsambabackupfile')
+ temp_tar_name = os.path.join(temp_tar_dir, "samba-backup.tar.bz2")
+ tar = tarfile.open(temp_tar_name, 'w:bz2')
+
+ logger.info('running offline ntacl backup of sysvol')
+ sysvol_tar_fn = 'sysvol.tar.gz'
+ sysvol_tar = os.path.join(temp_tar_dir, sysvol_tar_fn)
+ backup_offline(paths.sysvol, sysvol_tar, samdb, paths.smbconf)
+ tar.add(sysvol_tar, sysvol_tar_fn)
+ os.remove(sysvol_tar)
+
+ create_log_file(temp_tar_dir, lp, "offline", "localhost", True)
+ backup_fn = os.path.join(temp_tar_dir, "backup.txt")
+ tar.add(backup_fn, os.path.basename(backup_fn))
+ os.remove(backup_fn)
+
+ logger.info('building backup tar')
+ for path in all_files:
+ arc_path = self.get_arc_path(path, paths)
+
+ if os.path.exists(path + self.backup_ext):
+ logger.info(' adding backup ' + arc_path + self.backup_ext +
+ ' to tar and deleting file')
+ tar.add(path + self.backup_ext, arcname=arc_path)
+ os.remove(path + self.backup_ext)
+ elif path.endswith('.ldb') or path.endswith('.tdb'):
+ logger.info(' skipping ' + arc_path)
+ else:
+ logger.info(' adding misc file ' + arc_path)
+ tar.add(path, arcname=arc_path)
+
+ tar.close()
+ os.rename(temp_tar_name, os.path.join(targetdir,
+ 'samba-backup-{}.tar.bz2'.format(time_str)))
+ os.rmdir(temp_tar_dir)
+ logger.info('Backup succeeded.')
+
+
class cmd_domain_backup(samba.netcmd.SuperCommand):
'''Create or restore a backup of the domain.'''
- subcommands = {'online': cmd_domain_backup_online(),
+ subcommands = {'offline': cmd_domain_backup_offline(),
+ 'online': cmd_domain_backup_online(),
'rename': cmd_domain_backup_rename(),
'restore': cmd_domain_backup_restore()}
+++ /dev/null
-#!/bin/sh
-#
-# Copyright (C) Matthieu Patou <mat@matws.net> 2010-2011
-#
-# 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/>.
-#
-# Revised 2013-09-25, Brian Martin, as follows:
-# - Allow retention period ("DAYS") to be specified as a parameter.
-# - Allow individual positional parameters to be left at the default
-# by specifying "-"
-# - Use IS0 8601 standard dates (yyyy-mm-dd instead of mmddyyyy).
-# - Display tar exit codes when reporting errors.
-# - Don't send error messages to /dev/null, so we know what failed.
-# - Suppress useless tar "socket ignored" message.
-# - Fix retention period bug when deleting old backups ($DAYS variable
-# could be set, but was ignored).
-
-
-
-FROMWHERE=/usr/local/samba
-WHERE=/usr/local/backups
-DAYS=90 # Set default retention period.
-if [ -n "$1" ] && [ "$1" = "-h" -o "$1" = "--usage" ]; then
- echo "samba_backup [provisiondir] [destinationdir] [retpd]"
- echo "Will backup your provision located in provisiondir to archive stored"
- echo "in destinationdir for retpd days. Use - to leave an option unchanged."
- echo "Default provisiondir: $FROMWHERE"
- echo "Default destinationdir: $WHERE"
- echo "Default destinationdir: $DAYS"
- exit 0
-fi
-
-[ -n "$1" -a "$1" != "-" ]&&FROMWHERE=$1 # Use parm or default if "-". Validate later.
-[ -n "$2" -a "$2" != "-" ]&&WHERE=$2 # Use parm or default if "-". Validate later.
-[ -n "$3" -a "$3" -eq "$3" 2> /dev/null ]&&DAYS=$3 # Use parm or default if non-numeric (incl "-").
-
-DIRS="private etc sysvol"
-#Number of days to keep the backup
-WHEN=`date +%Y-%m-%d` # ISO 8601 standard date.
-
-if [ ! -d $WHERE ]; then
- echo "Missing backup directory $WHERE"
- exit 1
-fi
-
-if [ ! -d $FROMWHERE ]; then
- echo "Missing or wrong provision directory $FROMWHERE"
- exit 1
-fi
-
-cd $FROMWHERE
-for d in $DIRS;do
- relativedirname=`find . -type d -name "$d" -prune`
- n=`echo $d | sed 's/\//_/g'`
- if [ "$d" = "private" ]; then
- find $relativedirname -name "*.ldb.bak" -exec rm {} \;
- for ldb in `find $relativedirname -name "*.ldb"`; do
- tdbbackup $ldb
- Status=$? # Preserve $? for message, since [ alters it.
- if [ $Status -ne 0 ]; then
- echo "Error while backing up $ldb - status $Status"
- exit 1
- fi
- done
- # Run the backup.
- # --warning=no-file-ignored set to suppress "socket ignored" messages.
- tar cjf ${WHERE}/samba4_${n}.${WHEN}.tar.bz2 $relativedirname --exclude=\*.ldb --warning=no-file-ignored --transform 's/.ldb.bak$/.ldb/'
- Status=$? # Preserve $? for message, since [ alters it.
- if [ $Status -ne 0 -a $Status -ne 1 ]; then # Ignore 1 - private dir is always changing.
- echo "Error while archiving ${WHERE}/samba4_${n}.${WHEN}.tar.bz2 - status = $Status"
- exit 1
- fi
- find $relativedirname -name "*.ldb.bak" -exec rm {} \;
- else
- # Run the backup.
- # --warning=no-file-ignored set to suppress "socket ignored" messages.
- tar cjf ${WHERE}/${n}.${WHEN}.tar.bz2 $relativedirname --warning=no-file-ignored
- Status=$? # Preserve $? for message, since [ alters it.
- if [ $Status -ne 0 ]; then
- echo "Error while archiving ${WHERE}/${n}.${WHEN}.tar.bz2 - status = $Status"
- exit 1
- fi
- fi
-done
-
-find $WHERE -name "samba4_*bz2" -mtime +$DAYS -exec rm {} \;