diff options
-rw-r--r-- | docs-xml/manpages/samba-tool.8.xml | 5 | ||||
-rw-r--r-- | python/samba/join.py | 38 | ||||
-rw-r--r-- | python/samba/netcmd/domain_backup.py | 239 |
3 files changed, 240 insertions, 42 deletions
diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml index 70ff956cef7..b8038bc510c 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 e7ea11187ef..39c9a3a6902 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,18 @@ 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) + controls = None + if specified_sid is not None: + controls = ["relax:0"] + ctx.samdb.add(rec, controls=controls) 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 53e65b9373f..5a25da13e7c 100644 --- a/python/samba/netcmd/domain_backup.py +++ b/python/samba/netcmd/domain_backup.py @@ -21,42 +21,25 @@ import sys import tarfile import logging import 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.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 +from samba.provision import secretsdb_self_join -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. @@ -175,7 +158,6 @@ class cmd_domain_backup_online(samba.netcmd.Command): 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) @@ -190,6 +172,8 @@ class cmd_domain_backup_online(samba.netcmd.Command): logger.info('Creating targetdir %s...' % targetdir) os.makedirs(targetdir) + tmpdir = tempfile.mkdtemp(dir=targetdir) + # Run a clone join on the remote ctx = join_clone(logger=logger, creds=creds, lp=lp, include_secrets=True, dns_backend='SAMBA_INTERNAL', @@ -224,6 +208,203 @@ class cmd_domain_backup_online(samba.netcmd.Command): backup_file = backup_filepath(targetdir, realm, time_str) create_backup_tar(logger, tmpdir, backup_file) + shutil.rmtree(tmpdir) + + +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, lp=lp, + serverrole="active directory domain controller") + + # 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') + ntds_guid = str(ctx.ntds_guid) + m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % 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) + 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 (this ensures these DCs cannot + # replicate because they will not have a password) + search_expr = "(&(objectClass=Server)(serverReference=*))" + res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE, + expression=search_expr) + for m in res: + cn = m.get('cn')[0] + if cn != newservername: + remove_dc(samdb, logger, cn) + + # Remove the repsFrom and repsTo from each NC to ensure we do + # not try (and fail) to talk to the old DCs + for nc in ncs: + msg = ldb.Message() + msg.dn = ldb.Dn(samdb, nc) + + msg["repsFrom"] = ldb.MessageElement([], + ldb.FLAG_MOD_REPLACE, + "repsFrom") + msg["repsTo"] = ldb.MessageElement([], + ldb.FLAG_MOD_REPLACE, + "repsTo") + samdb.modify(msg) + + # Update the krbtgt passwords twice, ensuring no tickets from + # the old domain are valid + 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("Please 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()} |