import samba
import tdb
import samba.getopt as options
-from samba.samdb import SamDB
+from samba.samdb import SamDB, get_default_backend_store
import ldb
-from samba import smb
+from samba.samba3 import libsmb_samba_internal as libsmb
+from samba.samba3 import param as s3param
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.netcmd import Option, CommandError
-from samba.dcerpc import misc, security
+from samba.dcerpc import misc, security, drsblobs
from samba import Ldb
from . fsmo import cmd_fsmo_seize
-from samba.provision import make_smbconf
+from samba.provision import make_smbconf, DEFAULTSITE
from samba.upgradehelpers import update_krbtgt_account_password
from samba.remove_dc import remove_dc
from samba.provision import secretsdb_self_join
from samba.tdb_util import tdb_copy
from samba.mdb_util import mdb_copy
import errno
-import tdb
from subprocess import CalledProcessError
+from samba import sites
+from samba.dsdb import _dsdb_load_udv_v2
+from samba.ndr import ndr_pack
# work out a SID (based on a free RID) to use when the domain gets restored.
# 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])
+ server_ref_dn = ldb.Dn(samdb, str(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])
+ rid_set_dn = ldb.Dn(samdb, str(res[0]['rIDSetReferences'][0]))
# Get the alloc pools and next RID of the RID set
res = samdb.search(base=rid_set_dn,
return str(sid) + '-' + str(rid)
+def smb_sysvol_conn(server, lp, creds):
+ """Returns an SMB connection to the sysvol share on the DC"""
+ # the SMB bindings rely on having a s3 loadparm
+ s3_lp = s3param.get_context()
+ s3_lp.load(lp.configfile)
+ return libsmb.Conn(server, "sysvol", lp=s3_lp, creds=creds, sign=True)
+
+
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)
+ filename = 'samba-backup-%s-%s.tar.bz2' % (name, time_str)
return os.path.join(targetdir, filename)
# match the admin user by RID
domainsid = samdb.get_domain_sid()
- match_admin = "(objectsid={}-{})".format(domainsid,
- security.DOMAIN_RID_ADMINISTRATOR)
- search_expr = "(&(objectClass=user){})".format(match_admin)
+ match_admin = "(objectsid=%s-%s)" % (domainsid,
+ security.DOMAIN_RID_ADMINISTRATOR)
+ search_expr = "(&(objectClass=user)%s)" % (match_admin,)
# retrieve the admin username (just in case it's been renamed)
res = samdb.search(base=samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
Option("--targetdir", type=str,
help="Directory to write the backup file to"),
Option("--no-secrets", action="store_true", default=False,
- help="Exclude secret values from the backup created")
+ help="Exclude secret values from the backup created"),
+ Option("--backend-store", type="choice", metavar="BACKENDSTORE",
+ choices=["tdb", "mdb"],
+ help="Specify the database backend to be used "
+ "(default is %s)" % get_default_backend_store()),
]
def run(self, sambaopts=None, credopts=None, server=None, targetdir=None,
- no_secrets=False):
+ no_secrets=False, backend_store=None):
logger = self.get_logger()
logger.setLevel(logging.DEBUG)
include_secrets = not no_secrets
ctx = join_clone(logger=logger, creds=creds, lp=lp,
include_secrets=include_secrets, server=server,
- dns_backend='SAMBA_INTERNAL', targetdir=tmpdir)
+ dns_backend='SAMBA_INTERNAL', targetdir=tmpdir,
+ backend_store=backend_store)
# get the paths used for the clone, then drop the old samdb connection
paths = ctx.paths
# 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)
+ smb_conn = smb_sysvol_conn(server, lp, creds)
backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
# remove the default sysvol files created by the clone (we want to
time_str = get_timestamp()
add_backup_marker(samdb, "backupDate", time_str)
add_backup_marker(samdb, "sidForRestore", new_sid)
+ add_backup_marker(samdb, "backupType", "online")
# ensure the admin user always has a password set (same as provision)
if no_secrets:
help="set IPv4 ipaddress"),
Option("--host-ip6", type="string", metavar="IP6ADDRESS",
help="set IPv6 ipaddress"),
+ Option("--site", help="Site to add the new server in", type=str),
]
takes_optiongroups = {
}
def register_dns_zone(self, logger, samdb, lp, ntdsguid, host_ip,
- host_ip6):
+ host_ip6, site):
'''
Registers the new realm's DNS objects when a renamed domain backup
is restored.
# Add the DNS objects for the new realm (note: the backup clone already
# has the root server objects, so don't add them again)
- fill_dns_data_partitions(samdb, domainsid, names.sitename, domaindn,
+ fill_dns_data_partitions(samdb, domainsid, site, domaindn,
forestdn, dnsdomain, dnsforest, hostname,
host_ip, host_ip6, domainguid, ntdsguid,
dnsadmins_sid, add_root=False)
chk.check_database(controls=controls, attrs=attrs)
samdb.transaction_commit()
+ def create_default_site(self, samdb, logger):
+ '''Creates the default site, if it doesn't already exist'''
+
+ sitename = DEFAULTSITE
+ search_expr = "(&(cn={0})(objectclass=site))".format(sitename)
+ res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
+ expression=search_expr)
+
+ if len(res) == 0:
+ logger.info("Creating default site '{0}'".format(sitename))
+ sites.create_site(samdb, samdb.get_config_basedn(), sitename)
+
+ return sitename
+
+ def remove_backup_markers(self, samdb):
+ """Remove DB markers added by the backup process"""
+
+ # check what markers we need to remove (this may vary)
+ markers = ['sidForRestore', 'backupRename', 'backupDate', 'backupType']
+ res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
+ scope=ldb.SCOPE_BASE,
+ attrs=markers)
+
+ # remove any markers that exist in the DB
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
+
+ for attr in markers:
+ if attr in res[0]:
+ m[attr] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, attr)
+
+ samdb.modify(m)
+
+ def get_backup_type(self, samdb):
+ res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
+ scope=ldb.SCOPE_BASE,
+ attrs=['backupRename', 'backupType'])
+
+ # note that the backupType marker won't exist on backups created on
+ # v4.9. However, we can still infer the type, as only rename and
+ # online backups are supported on v4.9
+ if 'backupType' in res[0]:
+ backup_type = str(res[0]['backupType'])
+ elif 'backupRename' in res[0]:
+ backup_type = "rename"
+ else:
+ backup_type = "online"
+
+ return backup_type
+
+ def save_uptodate_vectors(self, samdb, partitions):
+ """Ensures the UTDV used by DRS is correct after an offline backup"""
+ for nc in partitions:
+ # load the replUpToDateVector we *should* have
+ utdv = _dsdb_load_udv_v2(samdb, nc)
+
+ # convert it to NDR format and write it into the DB
+ utdv_blob = drsblobs.replUpToDateVectorBlob()
+ utdv_blob.version = 2
+ utdv_blob.ctr.cursors = utdv
+ utdv_blob.ctr.count = len(utdv)
+ new_value = ndr_pack(utdv_blob)
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, nc)
+ m["replUpToDateVector"] = ldb.MessageElement(new_value,
+ ldb.FLAG_MOD_REPLACE,
+ "replUpToDateVector")
+ samdb.modify(m)
+
def run(self, sambaopts=None, credopts=None, backup_file=None,
- targetdir=None, newservername=None, host_ip=None, host_ip6=None):
+ targetdir=None, newservername=None, host_ip=None, host_ip6=None,
+ site=None):
if not (backup_file and os.path.exists(backup_file)):
raise CommandError('Backup file not found.')
if targetdir is None:
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)
+ backup_type = self.get_backup_type(samdb)
- # 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.
+ if site is None:
+ # There's no great way to work out the correct site to add the
+ # restored DC to. By default, add it to Default-First-Site-Name,
+ # creating the site if it doesn't already exist
+ site = self.create_default_site(samdb, logger)
+ logger.info("Adding new DC to site '{0}'".format(site))
+
+ # read the naming contexts out of the DB
res = samdb.search(base="", scope=ldb.SCOPE_BASE,
attrs=['namingContexts'])
ncs = [str(r) for r in res[0].get('namingContexts')]
+ # for offline backups we need to make sure the upToDateness info
+ # contains the invocation-ID and highest-USN of the DC we backed up.
+ # Otherwise replication propagation dampening won't correctly filter
+ # objects created by that DC
+ if backup_type == "offline":
+ self.save_uptodate_vectors(samdb, ncs)
+
+ # 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.
creds = credopts.get_credentials(lp)
- ctx = DCJoinContext(logger, creds=creds, lp=lp,
+ ctx = DCJoinContext(logger, creds=creds, lp=lp, site=site,
forced_local_samdb=samdb,
netbios_name=newservername)
ctx.nc_list = ncs
# 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', 'backupRename'])
- is_rename = True if 'backupRename' in res[0] else False
+ 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))
+ ctx.join_add_objects(specified_sid=dom_sid(str(sid)))
m = ldb.Message()
m.dn = ldb.Dn(samdb, '@ROOTDSE')
# if we renamed the backed-up domain, then we need to add the DNS
# objects for the new realm (we do this in the restore, now that we
# know the new DC's IP address)
- if is_rename:
+ if backup_type == "rename":
self.register_dns_zone(logger, samdb, lp, ctx.ntds_guid,
- host_ip, host_ip6)
+ host_ip, host_ip6, site)
secrets_path = os.path.join(private_dir, 'secrets.ldb')
secrets_ldb = Ldb(secrets_path, session_info=system_session(), lp=lp)
res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
expression=search_expr)
for m in res:
- cn = m.get('cn')[0]
+ cn = str(m.get('cn')[0])
if cn != newservername:
remove_dc(samdb, logger, cn)
self.fix_old_dc_references(samdb)
# 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")
- if is_rename:
- m["backupRename"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
- "backupRename")
- samdb.modify(m)
+ self.remove_backup_markers(samdb)
logger.info("Backup file successfully restored to %s" % targetdir)
logger.info("Please check the smb.conf settings are correct before "
Option("--keep-dns-realm", action="store_true", default=False,
help="Retain the DNS entries for the old realm in the backup"),
Option("--no-secrets", action="store_true", default=False,
- help="Exclude secret values from the backup created")
+ help="Exclude secret values from the backup created"),
+ Option("--backend-store", type="choice", metavar="BACKENDSTORE",
+ choices=["tdb", "mdb"],
+ help="Specify the database backend to be used "
+ "(default is %s)" % get_default_backend_store()),
]
takes_args = ["new_domain_name", "new_dns_realm"]
for res_msg in res:
# dnsRoot can be multi-valued, so only look for the old realm
for dns_root in res_msg["dnsRoot"]:
+ dns_root = str(dns_root)
dn = res_msg.dn
if old_realm in dns_root:
new_dns_root = re.sub('%s$' % old_realm, new_realm,
def run(self, new_domain_name, new_dns_realm, sambaopts=None,
credopts=None, server=None, targetdir=None, keep_dns_realm=False,
- no_secrets=False):
+ no_secrets=False, backend_store=None):
logger = self.get_logger()
logger.setLevel(logging.INFO)
creds=creds, lp=lp,
include_secrets=include_secrets,
dns_backend='SAMBA_INTERNAL',
- server=server, targetdir=tmpdir)
+ server=server, targetdir=tmpdir,
+ backend_store=backend_store)
# sanity-check we're not "renaming" the domain to the same values
old_domain = ctx.domain_name
# use the old realm) backed here, as well as default files generated
# for the new realm as part of the clone/join.
sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
- smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
+ smb_conn = smb_sysvol_conn(server, lp, creds)
backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
# connect to the local DB (making sure we use the new/renamed config)
add_backup_marker(samdb, "backupDate", time_str)
add_backup_marker(samdb, "sidForRestore", new_sid)
add_backup_marker(samdb, "backupRename", old_realm)
+ add_backup_marker(samdb, "backupType", "rename")
# fix up the DNS objects that are using the old dnsRoot value
self.update_dns_root(logger, samdb, old_realm, delete_old_dns)
raise e
raise copy_err
if not os.path.exists(backup_path):
- s = "tdbbackup said backup succeeded but {} not found"
+ s = "tdbbackup said backup succeeded but {0} not found"
raise CommandError(s.format(backup_path))
def offline_mdb_copy(self, path):
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'
+ mdb_backend = store_label in res[0] and str(res[0][store_label][0]) == 'mdb'
sam_ldb_path = os.path.join(private_dir, 'sam.ldb')
copy_function = None
backup_dirs = [paths.private_dir, paths.state_dir,
os.path.dirname(paths.smbconf)] # etc dir
- logger.info('running backup on dirs: {}'.format(backup_dirs))
+ logger.info('running backup on dirs: {0}'.format(' '.join(backup_dirs)))
# Recursively get all file paths in the backup directories
all_files = []
for (working_dir, _, filenames) in os.walk(backup_dir):
if working_dir.startswith(paths.sysvol):
continue
+ if working_dir.endswith('.sock') or '.sock/' in working_dir:
+ continue
for filename in filenames:
if filename in all_files:
if filename.endswith(self.backup_ext):
os.remove(os.path.join(working_dir, filename))
continue
+
+ # Sock files are autogenerated at runtime, ignore.
+ if filename.endswith('.sock'):
+ continue
+
all_files.append(os.path.join(working_dir, filename))
# Backup secrets, sam.ldb and their downstream files
time_str = get_timestamp()
add_backup_marker(samdb, "backupDate", time_str)
add_backup_marker(samdb, "sidForRestore", sid)
+ add_backup_marker(samdb, "backupType", "offline")
# Now handle all the LDB and TDB files that are not linked to
# anything else. Use transactions for LDBs.
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.rename(temp_tar_name,
+ os.path.join(targetdir,
+ 'samba-backup-{0}.tar.bz2'.format(time_str)))
os.rmdir(temp_tar_dir)
logger.info('Backup succeeded.')