mirror of
https://github.com/samba-team/samba.git
synced 2025-01-11 05:18:09 +03:00
netcmd: domain backup restore command
Add a command option that restores a backup file. This is only intended for recovering from a catastrophic failure of the domain. The old domain DCs are removed from the DB and a new DC is added. 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>
This commit is contained in:
parent
e0301df111
commit
7844074621
@ -301,6 +301,11 @@
|
||||
<para>Copy a running DC's current DB into a backup tar file.</para>
|
||||
</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
|
||||
|
@ -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()
|
||||
|
@ -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()}
|
||||
|
Loading…
Reference in New Issue
Block a user