3 # Copyright Andrew Bartlett <abartlet@samba.org>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 import samba.getopt as options
28 from samba.samdb import SamDB, get_default_backend_store
31 from samba.ntacls import backup_online, backup_restore, backup_offline
32 from samba.auth import system_session
33 from samba.join import DCJoinContext, join_clone, DCCloneAndRenameContext
34 from samba.dcerpc.security import dom_sid
35 from samba.netcmd import Option, CommandError
36 from samba.dcerpc import misc, security, drsblobs
38 from . fsmo import cmd_fsmo_seize
39 from samba.provision import make_smbconf, DEFAULTSITE
40 from samba.upgradehelpers import update_krbtgt_account_password
41 from samba.remove_dc import remove_dc
42 from samba.provision import secretsdb_self_join
43 from samba.dbchecker import dbcheck
45 from samba.provision import guess_names, determine_host_ip, determine_host_ip6
46 from samba.provision.sambadns import (fill_dns_data_partitions,
49 from samba.tdb_util import tdb_copy
50 from samba.mdb_util import mdb_copy
52 from subprocess import CalledProcessError
53 from samba import sites
54 from samba.dsdb import _dsdb_load_udv_v2
55 from samba.ndr import ndr_pack
58 # work out a SID (based on a free RID) to use when the domain gets restored.
59 # This ensures that the restored DC's SID won't clash with any other RIDs
60 # already in use in the domain
61 def get_sid_for_restore(samdb):
62 # Find the DN of the RID set of the server
63 res = samdb.search(base=ldb.Dn(samdb, samdb.get_serverName()),
64 scope=ldb.SCOPE_BASE, attrs=["serverReference"])
65 server_ref_dn = ldb.Dn(samdb, str(res[0]['serverReference'][0]))
66 res = samdb.search(base=server_ref_dn,
68 attrs=['rIDSetReferences'])
69 rid_set_dn = ldb.Dn(samdb, str(res[0]['rIDSetReferences'][0]))
71 # Get the alloc pools and next RID of the RID set
72 res = samdb.search(base=rid_set_dn,
73 scope=ldb.SCOPE_SUBTREE,
74 expression="(rIDNextRID=*)",
75 attrs=['rIDAllocationPool',
76 'rIDPreviousAllocationPool',
79 # Decode the bounds of the RID allocation pools
80 rid = int(res[0].get('rIDNextRID')[0])
83 high = (0xFFFFFFFF00000000 & int(num)) >> 32
84 low = 0x00000000FFFFFFFF & int(num)
86 pool_l, pool_h = split_val(res[0].get('rIDPreviousAllocationPool')[0])
87 npool_l, npool_h = split_val(res[0].get('rIDAllocationPool')[0])
89 # Calculate next RID based on pool bounds
91 raise CommandError('Out of RIDs, finished AllocPool')
94 raise CommandError('Out of RIDs, finished PrevAllocPool.')
100 sid = dom_sid(samdb.get_domain_sid())
101 return str(sid) + '-' + str(rid)
105 return datetime.datetime.now().isoformat().replace(':', '-')
108 def backup_filepath(targetdir, name, time_str):
109 filename = 'samba-backup-%s-%s.tar.bz2' % (name, time_str)
110 return os.path.join(targetdir, filename)
113 def create_backup_tar(logger, tmpdir, backup_filepath):
114 # Adds everything in the tmpdir into a new tar file
115 logger.info("Creating backup file %s..." % backup_filepath)
116 tf = tarfile.open(backup_filepath, 'w:bz2')
117 tf.add(tmpdir, arcname='./')
121 def create_log_file(targetdir, lp, backup_type, server, include_secrets,
123 # create a summary file about the backup, which will get included in the
124 # tar file. This makes it easy for users to see what the backup involved,
125 # without having to untar the DB and interrogate it
126 f = open(os.path.join(targetdir, "backup.txt"), 'w')
128 time_str = datetime.datetime.now().strftime('%Y-%b-%d %H:%M:%S')
129 f.write("Backup created %s\n" % time_str)
130 f.write("Using samba-tool version: %s\n" % lp.get('server string'))
131 f.write("Domain %s backup, using DC '%s'\n" % (backup_type, server))
132 f.write("Backup for domain %s (NetBIOS), %s (DNS realm)\n" %
133 (lp.get('workgroup'), lp.get('realm').lower()))
134 f.write("Backup contains domain secrets: %s\n" % str(include_secrets))
136 f.write("%s\n" % extra_info)
141 # Add a backup-specific marker to the DB with info that we'll use during
142 # the restore process
143 def add_backup_marker(samdb, marker, value):
145 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
146 m[marker] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, marker)
150 def check_targetdir(logger, targetdir):
151 if targetdir is None:
152 raise CommandError('Target directory required')
154 if not os.path.exists(targetdir):
155 logger.info('Creating targetdir %s...' % targetdir)
156 os.makedirs(targetdir)
157 elif not os.path.isdir(targetdir):
158 raise CommandError("%s is not a directory" % targetdir)
161 # For '--no-secrets' backups, this sets the Administrator user's password to a
162 # randomly-generated value. This is similar to the provision behaviour
163 def set_admin_password(logger, samdb):
164 """Sets a randomly generated password for the backup DB's admin user"""
166 # match the admin user by RID
167 domainsid = samdb.get_domain_sid()
168 match_admin = "(objectsid=%s-%s)" % (domainsid,
169 security.DOMAIN_RID_ADMINISTRATOR)
170 search_expr = "(&(objectClass=user)%s)" % (match_admin,)
172 # retrieve the admin username (just in case it's been renamed)
173 res = samdb.search(base=samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
174 expression=search_expr)
175 username = str(res[0]['samaccountname'])
177 adminpass = samba.generate_random_password(12, 32)
178 logger.info("Setting %s password in backup to: %s" % (username, adminpass))
179 logger.info("Run 'samba-tool user setpassword %s' after restoring DB" %
181 samdb.setpassword(search_expr, adminpass, force_change_at_next_login=False,
185 class cmd_domain_backup_online(samba.netcmd.Command):
186 '''Copy a running DC's current DB into a backup tar file.
188 Takes a backup copy of the current domain from a running DC. If the domain
189 were to undergo a catastrophic failure, then the backup file can be used to
190 recover the domain. The backup created is similar to the DB that a new DC
191 would receive when it joins the domain.
194 - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
195 and fix any errors it reports.
196 - all the domain's secrets are included in the backup file.
197 - although the DB contents can be untarred and examined manually, you need
198 to run 'samba-tool domain backup restore' before you can start a Samba DC
199 from the backup file.'''
201 synopsis = "%prog --server=<DC-to-backup> --targetdir=<output-dir>"
202 takes_optiongroups = {
203 "sambaopts": options.SambaOptions,
204 "credopts": options.CredentialsOptions,
208 Option("--server", help="The DC to backup", type=str),
209 Option("--targetdir", type=str,
210 help="Directory to write the backup file to"),
211 Option("--no-secrets", action="store_true", default=False,
212 help="Exclude secret values from the backup created"),
213 Option("--backend-store", type="choice", metavar="BACKENDSTORE",
214 choices=["tdb", "mdb"],
215 help="Specify the database backend to be used "
216 "(default is %s)" % get_default_backend_store()),
219 def run(self, sambaopts=None, credopts=None, server=None, targetdir=None,
220 no_secrets=False, backend_store=None):
221 logger = self.get_logger()
222 logger.setLevel(logging.DEBUG)
224 lp = sambaopts.get_loadparm()
225 creds = credopts.get_credentials(lp)
227 # Make sure we have all the required args.
229 raise CommandError('Server required')
231 check_targetdir(logger, targetdir)
233 tmpdir = tempfile.mkdtemp(dir=targetdir)
235 # Run a clone join on the remote
236 include_secrets = not no_secrets
237 ctx = join_clone(logger=logger, creds=creds, lp=lp,
238 include_secrets=include_secrets, server=server,
239 dns_backend='SAMBA_INTERNAL', targetdir=tmpdir,
240 backend_store=backend_store)
242 # get the paths used for the clone, then drop the old samdb connection
246 # Get a free RID to use as the new DC's SID (when it gets restored)
247 remote_sam = SamDB(url='ldap://' + server, credentials=creds,
248 session_info=system_session(), lp=lp)
249 new_sid = get_sid_for_restore(remote_sam)
250 realm = remote_sam.domain_dns_name()
252 # Grab the remote DC's sysvol files and bundle them into a tar file
253 sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
254 smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds, sign=True)
255 backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
257 # remove the default sysvol files created by the clone (we want to
258 # make sure we restore the sysvol.tar.gz files instead)
259 shutil.rmtree(paths.sysvol)
261 # Edit the downloaded sam.ldb to mark it as a backup
262 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
263 time_str = get_timestamp()
264 add_backup_marker(samdb, "backupDate", time_str)
265 add_backup_marker(samdb, "sidForRestore", new_sid)
266 add_backup_marker(samdb, "backupType", "online")
268 # ensure the admin user always has a password set (same as provision)
270 set_admin_password(logger, samdb)
272 # Add everything in the tmpdir to the backup tar file
273 backup_file = backup_filepath(targetdir, realm, time_str)
274 create_log_file(tmpdir, lp, "online", server, include_secrets)
275 create_backup_tar(logger, tmpdir, backup_file)
277 shutil.rmtree(tmpdir)
280 class cmd_domain_backup_restore(cmd_fsmo_seize):
281 '''Restore the domain's DB from a backup-file.
283 This restores a previously backed up copy of the domain's DB on a new DC.
285 Note that the restored DB will not contain the original DC that the backup
286 was taken from (or any other DCs in the original domain). Only the new DC
287 (specified by --newservername) will be present in the restored DB.
289 Samba can then be started against the restored DB. Any existing DCs for the
290 domain should be shutdown before the new DC is started. Other DCs can then
291 be joined to the new DC to recover the network.
293 Note that this command should be run as the root user - it will fail
296 synopsis = ("%prog --backup-file=<tar-file> --targetdir=<output-dir> "
297 "--newservername=<DC-name>")
299 Option("--backup-file", help="Path to backup file", type=str),
300 Option("--targetdir", help="Path to write to", type=str),
301 Option("--newservername", help="Name for new server", type=str),
302 Option("--host-ip", type="string", metavar="IPADDRESS",
303 help="set IPv4 ipaddress"),
304 Option("--host-ip6", type="string", metavar="IP6ADDRESS",
305 help="set IPv6 ipaddress"),
306 Option("--site", help="Site to add the new server in", type=str),
309 takes_optiongroups = {
310 "sambaopts": options.SambaOptions,
311 "credopts": options.CredentialsOptions,
314 def register_dns_zone(self, logger, samdb, lp, ntdsguid, host_ip,
317 Registers the new realm's DNS objects when a renamed domain backup
320 names = guess_names(lp)
321 domaindn = names.domaindn
322 forestdn = samdb.get_root_basedn().get_linearized()
323 dnsdomain = names.dnsdomain.lower()
324 dnsforest = dnsdomain
325 hostname = names.netbiosname.lower()
326 domainsid = dom_sid(samdb.get_domain_sid())
327 dnsadmins_sid = get_dnsadmins_sid(samdb, domaindn)
328 domainguid = get_domainguid(samdb, domaindn)
330 # work out the IP address to use for the new DC's DNS records
331 host_ip = determine_host_ip(logger, lp, host_ip)
332 host_ip6 = determine_host_ip6(logger, lp, host_ip6)
334 if host_ip is None and host_ip6 is None:
335 raise CommandError('Please specify a host-ip for the new server')
337 logger.info("DNS realm was renamed to %s" % dnsdomain)
338 logger.info("Populating DNS partitions for new realm...")
340 # Add the DNS objects for the new realm (note: the backup clone already
341 # has the root server objects, so don't add them again)
342 fill_dns_data_partitions(samdb, domainsid, site, domaindn,
343 forestdn, dnsdomain, dnsforest, hostname,
344 host_ip, host_ip6, domainguid, ntdsguid,
345 dnsadmins_sid, add_root=False)
347 def fix_old_dc_references(self, samdb):
348 '''Fixes attributes that reference the old/removed DCs'''
350 # we just want to fix up DB problems here that were introduced by us
351 # removing the old DCs. We restrict what we fix up so that the restored
352 # DB matches the backed-up DB as close as possible. (There may be other
353 # DB issues inherited from the backed-up DC, but it's not our place to
354 # silently try to fix them here).
355 samdb.transaction_start()
356 chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
359 # fix up stale references to the old DC
360 setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
361 attrs = ['lastKnownParent', 'interSiteTopologyGenerator']
363 # fix-up stale one-way links that point to the old DC
364 setattr(chk, 'remove_plausible_deleted_DN_links', 'ALL')
365 attrs += ['msDS-NC-Replica-Locations']
367 cross_ncs_ctrl = 'search_options:1:2'
368 controls = ['show_deleted:1', cross_ncs_ctrl]
369 chk.check_database(controls=controls, attrs=attrs)
370 samdb.transaction_commit()
372 def create_default_site(self, samdb, logger):
373 '''Creates the default site, if it doesn't already exist'''
375 sitename = DEFAULTSITE
376 search_expr = "(&(cn={0})(objectclass=site))".format(sitename)
377 res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
378 expression=search_expr)
381 logger.info("Creating default site '{0}'".format(sitename))
382 sites.create_site(samdb, samdb.get_config_basedn(), sitename)
386 def remove_backup_markers(self, samdb):
387 """Remove DB markers added by the backup process"""
389 # check what markers we need to remove (this may vary)
390 markers = ['sidForRestore', 'backupRename', 'backupDate', 'backupType']
391 res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
392 scope=ldb.SCOPE_BASE,
395 # remove any markers that exist in the DB
397 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
401 m[attr] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, attr)
405 def get_backup_type(self, samdb):
406 res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
407 scope=ldb.SCOPE_BASE,
408 attrs=['backupRename', 'backupType'])
410 # note that the backupType marker won't exist on backups created on
411 # v4.9. However, we can still infer the type, as only rename and
412 # online backups are supported on v4.9
413 if 'backupType' in res[0]:
414 backup_type = str(res[0]['backupType'])
415 elif 'backupRename' in res[0]:
416 backup_type = "rename"
418 backup_type = "online"
422 def save_uptodate_vectors(self, samdb, partitions):
423 """Ensures the UTDV used by DRS is correct after an offline backup"""
424 for nc in partitions:
425 # load the replUpToDateVector we *should* have
426 utdv = _dsdb_load_udv_v2(samdb, nc)
428 # convert it to NDR format and write it into the DB
429 utdv_blob = drsblobs.replUpToDateVectorBlob()
430 utdv_blob.version = 2
431 utdv_blob.ctr.cursors = utdv
432 utdv_blob.ctr.count = len(utdv)
433 new_value = ndr_pack(utdv_blob)
436 m.dn = ldb.Dn(samdb, nc)
437 m["replUpToDateVector"] = ldb.MessageElement(new_value,
438 ldb.FLAG_MOD_REPLACE,
439 "replUpToDateVector")
442 def run(self, sambaopts=None, credopts=None, backup_file=None,
443 targetdir=None, newservername=None, host_ip=None, host_ip6=None,
445 if not (backup_file and os.path.exists(backup_file)):
446 raise CommandError('Backup file not found.')
447 if targetdir is None:
448 raise CommandError('Please specify a target directory')
449 # allow restoredc to install into a directory prepopulated by selftest
450 if (os.path.exists(targetdir) and os.listdir(targetdir) and
451 os.environ.get('SAMBA_SELFTEST') != '1'):
452 raise CommandError('Target directory is not empty')
453 if not newservername:
454 raise CommandError('Server name required')
456 logger = logging.getLogger()
457 logger.setLevel(logging.DEBUG)
458 logger.addHandler(logging.StreamHandler(sys.stdout))
460 # ldapcmp prefers the server's netBIOS name in upper-case
461 newservername = newservername.upper()
463 # extract the backup .tar to a temp directory
464 targetdir = os.path.abspath(targetdir)
465 tf = tarfile.open(backup_file)
466 tf.extractall(targetdir)
469 # use the smb.conf that got backed up, by default (save what was
470 # actually backed up, before we mess with it)
471 smbconf = os.path.join(targetdir, 'etc', 'smb.conf')
472 shutil.copyfile(smbconf, smbconf + ".orig")
474 # if a smb.conf was specified on the cmd line, then use that instead
475 cli_smbconf = sambaopts.get_loadparm_path()
477 logger.info("Using %s as restored domain's smb.conf" % cli_smbconf)
478 shutil.copyfile(cli_smbconf, smbconf)
480 lp = samba.param.LoadParm()
483 # open a DB connection to the restored DB
484 private_dir = os.path.join(targetdir, 'private')
485 samdb_path = os.path.join(private_dir, 'sam.ldb')
486 samdb = SamDB(url=samdb_path, session_info=system_session(), lp=lp)
487 backup_type = self.get_backup_type(samdb)
490 # There's no great way to work out the correct site to add the
491 # restored DC to. By default, add it to Default-First-Site-Name,
492 # creating the site if it doesn't already exist
493 site = self.create_default_site(samdb, logger)
494 logger.info("Adding new DC to site '{0}'".format(site))
496 # read the naming contexts out of the DB
497 res = samdb.search(base="", scope=ldb.SCOPE_BASE,
498 attrs=['namingContexts'])
499 ncs = [str(r) for r in res[0].get('namingContexts')]
501 # for offline backups we need to make sure the upToDateness info
502 # contains the invocation-ID and highest-USN of the DC we backed up.
503 # Otherwise replication propagation dampening won't correctly filter
504 # objects created by that DC
505 if backup_type == "offline":
506 self.save_uptodate_vectors(samdb, ncs)
508 # Create account using the join_add_objects function in the join object
509 # We need namingContexts, account control flags, and the sid saved by
510 # the backup process.
511 creds = credopts.get_credentials(lp)
512 ctx = DCJoinContext(logger, creds=creds, lp=lp, site=site,
513 forced_local_samdb=samdb,
514 netbios_name=newservername)
516 ctx.full_nc_list = ncs
517 ctx.userAccountControl = (samba.dsdb.UF_SERVER_TRUST_ACCOUNT |
518 samba.dsdb.UF_TRUSTED_FOR_DELEGATION)
520 # rewrite the smb.conf to make sure it uses the new targetdir settings.
521 # (This doesn't update all filepaths in a customized config, but it
522 # corrects the same paths that get set by a new provision)
523 logger.info('Updating basic smb.conf settings...')
524 make_smbconf(smbconf, newservername, ctx.domain_name,
525 ctx.realm, targetdir, lp=lp,
526 serverrole="active directory domain controller")
528 # Get the SID saved by the backup process and create account
529 res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
530 scope=ldb.SCOPE_BASE,
531 attrs=['sidForRestore'])
532 sid = res[0].get('sidForRestore')[0]
533 logger.info('Creating account with SID: ' + str(sid))
534 ctx.join_add_objects(specified_sid=dom_sid(str(sid)))
537 m.dn = ldb.Dn(samdb, '@ROOTDSE')
538 ntds_guid = str(ctx.ntds_guid)
539 m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % ntds_guid,
540 ldb.FLAG_MOD_REPLACE,
544 # if we renamed the backed-up domain, then we need to add the DNS
545 # objects for the new realm (we do this in the restore, now that we
546 # know the new DC's IP address)
547 if backup_type == "rename":
548 self.register_dns_zone(logger, samdb, lp, ctx.ntds_guid,
549 host_ip, host_ip6, site)
551 secrets_path = os.path.join(private_dir, 'secrets.ldb')
552 secrets_ldb = Ldb(secrets_path, session_info=system_session(), lp=lp)
553 secretsdb_self_join(secrets_ldb, domain=ctx.domain_name,
554 realm=ctx.realm, dnsdomain=ctx.dnsdomain,
555 netbiosname=ctx.myname, domainsid=ctx.domsid,
556 machinepass=ctx.acct_pass,
557 key_version_number=ctx.key_version_number,
558 secure_channel_type=misc.SEC_CHAN_BDC)
561 domain_dn = samdb.domain_dn()
562 forest_dn = samba.dn_from_dns_name(samdb.forest_dns_name())
563 domaindns_dn = ("CN=Infrastructure,DC=DomainDnsZones,", domain_dn)
564 forestdns_dn = ("CN=Infrastructure,DC=ForestDnsZones,", forest_dn)
565 for dn_prefix, dns_dn in [forestdns_dn, domaindns_dn]:
566 if dns_dn not in ncs:
568 full_dn = dn_prefix + dns_dn
570 m.dn = ldb.Dn(samdb, full_dn)
571 m["fSMORoleOwner"] = ldb.MessageElement(samdb.get_dsServiceName(),
572 ldb.FLAG_MOD_REPLACE,
577 for role in ['rid', 'pdc', 'naming', 'infrastructure', 'schema']:
578 self.seize_role(role, samdb, force=True)
580 # Get all DCs and remove them (this ensures these DCs cannot
581 # replicate because they will not have a password)
582 search_expr = "(&(objectClass=Server)(serverReference=*))"
583 res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
584 expression=search_expr)
586 cn = str(m.get('cn')[0])
587 if cn != newservername:
588 remove_dc(samdb, logger, cn)
590 # Remove the repsFrom and repsTo from each NC to ensure we do
591 # not try (and fail) to talk to the old DCs
594 msg.dn = ldb.Dn(samdb, nc)
596 msg["repsFrom"] = ldb.MessageElement([],
597 ldb.FLAG_MOD_REPLACE,
599 msg["repsTo"] = ldb.MessageElement([],
600 ldb.FLAG_MOD_REPLACE,
604 # Update the krbtgt passwords twice, ensuring no tickets from
605 # the old domain are valid
606 update_krbtgt_account_password(samdb)
607 update_krbtgt_account_password(samdb)
609 # restore the sysvol directory from the backup tar file, including the
610 # original NTACLs. Note that the backup_restore() will fail if not root
611 sysvol_tar = os.path.join(targetdir, 'sysvol.tar.gz')
612 dest_sysvol_dir = lp.get('path', 'sysvol')
613 if not os.path.exists(dest_sysvol_dir):
614 os.makedirs(dest_sysvol_dir)
615 backup_restore(sysvol_tar, dest_sysvol_dir, samdb, smbconf)
616 os.remove(sysvol_tar)
618 # fix up any stale links to the old DCs we just removed
619 logger.info("Fixing up any remaining references to the old DCs...")
620 self.fix_old_dc_references(samdb)
622 # Remove DB markers added by the backup process
623 self.remove_backup_markers(samdb)
625 logger.info("Backup file successfully restored to %s" % targetdir)
626 logger.info("Please check the smb.conf settings are correct before "
630 class cmd_domain_backup_rename(samba.netcmd.Command):
631 '''Copy a running DC's DB to backup file, renaming the domain in the process.
633 Where <new-domain> is the new domain's NetBIOS name, and <new-dnsrealm> is
634 the new domain's realm in DNS form.
636 This is similar to 'samba-tool backup online' in that it clones the DB of a
637 running DC. However, this option also renames all the domain entries in the
638 DB. Renaming the domain makes it possible to restore and start a new Samba
639 DC without it interfering with the existing Samba domain. In other words,
640 you could use this option to clone your production samba domain and restore
641 it to a separate pre-production environment that won't overlap or interfere
642 with the existing production Samba domain.
645 - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
646 and fix any errors it reports.
647 - all the domain's secrets are included in the backup file.
648 - although the DB contents can be untarred and examined manually, you need
649 to run 'samba-tool domain backup restore' before you can start a Samba DC
650 from the backup file.
651 - GPO and sysvol information will still refer to the old realm and will
652 need to be updated manually.
653 - if you specify 'keep-dns-realm', then the DNS records will need updating
654 in order to work (they will still refer to the old DC's IP instead of the
656 - we recommend that you only use this option if you know what you're doing.
659 synopsis = ("%prog <new-domain> <new-dnsrealm> --server=<DC-to-backup> "
660 "--targetdir=<output-dir>")
661 takes_optiongroups = {
662 "sambaopts": options.SambaOptions,
663 "credopts": options.CredentialsOptions,
667 Option("--server", help="The DC to backup", type=str),
668 Option("--targetdir", help="Directory to write the backup file",
670 Option("--keep-dns-realm", action="store_true", default=False,
671 help="Retain the DNS entries for the old realm in the backup"),
672 Option("--no-secrets", action="store_true", default=False,
673 help="Exclude secret values from the backup created"),
674 Option("--backend-store", type="choice", metavar="BACKENDSTORE",
675 choices=["tdb", "mdb"],
676 help="Specify the database backend to be used "
677 "(default is %s)" % get_default_backend_store()),
680 takes_args = ["new_domain_name", "new_dns_realm"]
682 def update_dns_root(self, logger, samdb, old_realm, delete_old_dns):
683 '''Updates dnsRoot for the partition objects to reflect the rename'''
685 # lookup the crossRef objects that hold the old realm's dnsRoot
686 partitions_dn = samdb.get_partitions_dn()
687 res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
689 expression='(&(objectClass=crossRef)(dnsRoot=*))')
690 new_realm = samdb.domain_dns_name()
692 # go through and add the new realm
694 # dnsRoot can be multi-valued, so only look for the old realm
695 for dns_root in res_msg["dnsRoot"]:
696 dns_root = str(dns_root)
698 if old_realm in dns_root:
699 new_dns_root = re.sub('%s$' % old_realm, new_realm,
701 logger.info("Adding %s dnsRoot to %s" % (new_dns_root, dn))
705 m["dnsRoot"] = ldb.MessageElement(new_dns_root,
710 # optionally remove the dnsRoot for the old realm
712 logger.info("Removing %s dnsRoot from %s" % (dns_root,
714 m["dnsRoot"] = ldb.MessageElement(dns_root,
719 # Updates the CN=<domain>,CN=Partitions,CN=Configuration,... object to
720 # reflect the domain rename
721 def rename_domain_partition(self, logger, samdb, new_netbios_name):
722 '''Renames the domain parition object and updates its nETBIOSName'''
724 # lookup the crossRef object that holds the nETBIOSName (nCName has
725 # already been updated by this point, but the netBIOS hasn't)
726 base_dn = samdb.get_default_basedn()
727 nc_name = ldb.binary_encode(str(base_dn))
728 partitions_dn = samdb.get_partitions_dn()
729 res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
730 attrs=["nETBIOSName"],
731 expression='ncName=%s' % nc_name)
733 logger.info("Changing backup domain's NetBIOS name to %s" %
737 m["nETBIOSName"] = ldb.MessageElement(new_netbios_name,
738 ldb.FLAG_MOD_REPLACE,
742 # renames the object itself to reflect the change in domain
743 new_dn = "CN=%s,%s" % (new_netbios_name, partitions_dn)
744 logger.info("Renaming %s --> %s" % (res[0].dn, new_dn))
745 samdb.rename(res[0].dn, new_dn, controls=['relax:0'])
747 def delete_old_dns_zones(self, logger, samdb, old_realm):
748 # remove the top-level DNS entries for the old realm
749 basedn = samdb.get_default_basedn()
750 dn = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (old_realm, basedn)
751 logger.info("Deleting old DNS zone %s" % dn)
752 samdb.delete(dn, ["tree_delete:1"])
754 forestdn = samdb.get_root_basedn().get_linearized()
755 dn = "DC=_msdcs.%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (old_realm,
757 logger.info("Deleting old DNS zone %s" % dn)
758 samdb.delete(dn, ["tree_delete:1"])
760 def fix_old_dn_attributes(self, samdb):
761 '''Fixes attributes (i.e. objectCategory) that still use the old DN'''
763 samdb.transaction_start()
764 # Just fix any mismatches in DN detected (leave any other errors)
765 chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
767 # fix up incorrect objectCategory/etc attributes
768 setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
769 cross_ncs_ctrl = 'search_options:1:2'
770 controls = ['show_deleted:1', cross_ncs_ctrl]
771 chk.check_database(controls=controls)
772 samdb.transaction_commit()
774 def run(self, new_domain_name, new_dns_realm, sambaopts=None,
775 credopts=None, server=None, targetdir=None, keep_dns_realm=False,
776 no_secrets=False, backend_store=None):
777 logger = self.get_logger()
778 logger.setLevel(logging.INFO)
780 lp = sambaopts.get_loadparm()
781 creds = credopts.get_credentials(lp)
783 # Make sure we have all the required args.
785 raise CommandError('Server required')
787 check_targetdir(logger, targetdir)
789 delete_old_dns = not keep_dns_realm
791 new_dns_realm = new_dns_realm.lower()
792 new_domain_name = new_domain_name.upper()
794 new_base_dn = samba.dn_from_dns_name(new_dns_realm)
795 logger.info("New realm for backed up domain: %s" % new_dns_realm)
796 logger.info("New base DN for backed up domain: %s" % new_base_dn)
797 logger.info("New domain NetBIOS name: %s" % new_domain_name)
799 tmpdir = tempfile.mkdtemp(dir=targetdir)
801 # setup a join-context for cloning the remote server
802 include_secrets = not no_secrets
803 ctx = DCCloneAndRenameContext(new_base_dn, new_domain_name,
804 new_dns_realm, logger=logger,
806 include_secrets=include_secrets,
807 dns_backend='SAMBA_INTERNAL',
808 server=server, targetdir=tmpdir,
809 backend_store=backend_store)
811 # sanity-check we're not "renaming" the domain to the same values
812 old_domain = ctx.domain_name
813 if old_domain == new_domain_name:
814 shutil.rmtree(tmpdir)
815 raise CommandError("Cannot use the current domain NetBIOS name.")
817 old_realm = ctx.realm
818 if old_realm == new_dns_realm:
819 shutil.rmtree(tmpdir)
820 raise CommandError("Cannot use the current domain DNS realm.")
822 # do the clone/rename
825 # get the paths used for the clone, then drop the old samdb connection
829 # get a free RID to use as the new DC's SID (when it gets restored)
830 remote_sam = SamDB(url='ldap://' + server, credentials=creds,
831 session_info=system_session(), lp=lp)
832 new_sid = get_sid_for_restore(remote_sam)
834 # Grab the remote DC's sysvol files and bundle them into a tar file.
835 # Note we end up with 2 sysvol dirs - the original domain's files (that
836 # use the old realm) backed here, as well as default files generated
837 # for the new realm as part of the clone/join.
838 sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
839 smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds, sign=True)
840 backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
842 # connect to the local DB (making sure we use the new/renamed config)
843 lp.load(paths.smbconf)
844 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
846 # Edit the cloned sam.ldb to mark it as a backup
847 time_str = get_timestamp()
848 add_backup_marker(samdb, "backupDate", time_str)
849 add_backup_marker(samdb, "sidForRestore", new_sid)
850 add_backup_marker(samdb, "backupRename", old_realm)
851 add_backup_marker(samdb, "backupType", "rename")
853 # fix up the DNS objects that are using the old dnsRoot value
854 self.update_dns_root(logger, samdb, old_realm, delete_old_dns)
856 # update the netBIOS name and the Partition object for the domain
857 self.rename_domain_partition(logger, samdb, new_domain_name)
860 self.delete_old_dns_zones(logger, samdb, old_realm)
862 logger.info("Fixing DN attributes after rename...")
863 self.fix_old_dn_attributes(samdb)
865 # ensure the admin user always has a password set (same as provision)
867 set_admin_password(logger, samdb)
869 # Add everything in the tmpdir to the backup tar file
870 backup_file = backup_filepath(targetdir, new_dns_realm, time_str)
871 create_log_file(tmpdir, lp, "rename", server, include_secrets,
872 "Original domain %s (NetBIOS), %s (DNS realm)" %
873 (old_domain, old_realm))
874 create_backup_tar(logger, tmpdir, backup_file)
876 shutil.rmtree(tmpdir)
879 class cmd_domain_backup_offline(samba.netcmd.Command):
880 '''Backup the local domain directories safely into a tar file.
882 Takes a backup copy of the current domain from the local files on disk,
883 with proper locking of the DB to ensure consistency. If the domain were to
884 undergo a catastrophic failure, then the backup file can be used to recover
887 An offline backup differs to an online backup in the following ways:
888 - a backup can be created even if the DC isn't currently running.
889 - includes non-replicated attributes that an online backup wouldn't store.
890 - takes a copy of the raw database files, which has the risk that any
891 hidden problems in the DB are preserved in the backup.'''
893 synopsis = "%prog [options]"
894 takes_optiongroups = {
895 "sambaopts": options.SambaOptions,
899 Option("--targetdir",
900 help="Output directory (required)",
904 backup_ext = '.bak-offline'
906 def offline_tdb_copy(self, path):
907 backup_path = path + self.backup_ext
909 tdb_copy(path, backup_path, readonly=True)
910 except CalledProcessError as copy_err:
911 # If the copy didn't work, check if it was caused by an EINVAL
912 # error on opening the DB. If so, it's a mutex locked database,
913 # which we can safely ignore.
916 except Exception as e:
917 if hasattr(e, 'errno') and e.errno == errno.EINVAL:
921 if not os.path.exists(backup_path):
922 s = "tdbbackup said backup succeeded but {0} not found"
923 raise CommandError(s.format(backup_path))
925 def offline_mdb_copy(self, path):
926 mdb_copy(path, path + self.backup_ext)
928 # Secrets databases are a special case: a transaction must be started
929 # on the secrets.ldb file before backing up that file and secrets.tdb
930 def backup_secrets(self, private_dir, lp, logger):
931 secrets_path = os.path.join(private_dir, 'secrets')
932 secrets_obj = Ldb(secrets_path + '.ldb', lp=lp)
933 logger.info('Starting transaction on ' + secrets_path)
934 secrets_obj.transaction_start()
935 self.offline_tdb_copy(secrets_path + '.ldb')
936 self.offline_tdb_copy(secrets_path + '.tdb')
937 secrets_obj.transaction_cancel()
939 # sam.ldb must have a transaction started on it before backing up
940 # everything in sam.ldb.d with the appropriate backup function.
941 def backup_smb_dbs(self, private_dir, samdb, lp, logger):
942 # First, determine if DB backend is MDB. Assume not unless there is a
943 # 'backendStore' attribute on @PARTITION containing the text 'mdb'
944 store_label = "backendStore"
945 res = samdb.search(base="@PARTITION", scope=ldb.SCOPE_BASE,
947 mdb_backend = store_label in res[0] and str(res[0][store_label][0]) == 'mdb'
949 sam_ldb_path = os.path.join(private_dir, 'sam.ldb')
952 logger.info('MDB backend detected. Using mdb backup function.')
953 copy_function = self.offline_mdb_copy
955 logger.info('Starting transaction on ' + sam_ldb_path)
956 copy_function = self.offline_tdb_copy
957 sam_obj = Ldb(sam_ldb_path, lp=lp)
958 sam_obj.transaction_start()
960 logger.info(' backing up ' + sam_ldb_path)
961 self.offline_tdb_copy(sam_ldb_path)
962 sam_ldb_d = sam_ldb_path + '.d'
963 for sam_file in os.listdir(sam_ldb_d):
964 sam_file = os.path.join(sam_ldb_d, sam_file)
965 if sam_file.endswith('.ldb'):
966 logger.info(' backing up locked/related file ' + sam_file)
967 copy_function(sam_file)
969 logger.info(' copying locked/related file ' + sam_file)
970 shutil.copyfile(sam_file, sam_file + self.backup_ext)
973 sam_obj.transaction_cancel()
975 # Find where a path should go in the fixed backup archive structure.
976 def get_arc_path(self, path, conf_paths):
977 backup_dirs = {"private": conf_paths.private_dir,
978 "statedir": conf_paths.state_dir,
979 "etc": os.path.dirname(conf_paths.smbconf)}
980 matching_dirs = [(_, p) for (_, p) in backup_dirs.items() if
982 arc_path, fs_path = matching_dirs[0]
984 # If more than one directory is a parent of this path, then at least
985 # one configured path is a subdir of another. Use closest match.
986 if len(matching_dirs) > 1:
987 arc_path, fs_path = max(matching_dirs, key=lambda p: len(p[1]))
988 arc_path += path[len(fs_path):]
992 def run(self, sambaopts=None, targetdir=None):
994 logger = logging.getLogger()
995 logger.setLevel(logging.DEBUG)
996 logger.addHandler(logging.StreamHandler(sys.stdout))
998 # Get the absolute paths of all the directories we're going to backup
999 lp = sambaopts.get_loadparm()
1001 paths = samba.provision.provision_paths_from_lp(lp, lp.get('realm'))
1002 if not (paths.samdb and os.path.exists(paths.samdb)):
1003 raise CommandError('No sam.db found. This backup ' +
1004 'tool is only for AD DCs')
1006 check_targetdir(logger, targetdir)
1008 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
1009 sid = get_sid_for_restore(samdb)
1011 backup_dirs = [paths.private_dir, paths.state_dir,
1012 os.path.dirname(paths.smbconf)] # etc dir
1013 logger.info('running backup on dirs: {0}'.format(' '.join(backup_dirs)))
1015 # Recursively get all file paths in the backup directories
1017 for backup_dir in backup_dirs:
1018 for (working_dir, _, filenames) in os.walk(backup_dir):
1019 if working_dir.startswith(paths.sysvol):
1021 if working_dir.endswith('.sock') or '.sock/' in working_dir:
1024 for filename in filenames:
1025 if filename in all_files:
1028 # Assume existing backup files are from a previous backup.
1029 # Delete and ignore.
1030 if filename.endswith(self.backup_ext):
1031 os.remove(os.path.join(working_dir, filename))
1034 # Sock files are autogenerated at runtime, ignore.
1035 if filename.endswith('.sock'):
1038 all_files.append(os.path.join(working_dir, filename))
1040 # Backup secrets, sam.ldb and their downstream files
1041 self.backup_secrets(paths.private_dir, lp, logger)
1042 self.backup_smb_dbs(paths.private_dir, samdb, lp, logger)
1044 # Open the new backed up samdb, flag it as backed up, and write
1045 # the next SID so the restore tool can add objects.
1046 # WARNING: Don't change this code unless you know what you're doing.
1047 # Writing to a .bak file only works because the DN being
1048 # written to happens to be top level.
1049 samdb = SamDB(url=paths.samdb + self.backup_ext,
1050 session_info=system_session(), lp=lp)
1051 time_str = get_timestamp()
1052 add_backup_marker(samdb, "backupDate", time_str)
1053 add_backup_marker(samdb, "sidForRestore", sid)
1054 add_backup_marker(samdb, "backupType", "offline")
1056 # Now handle all the LDB and TDB files that are not linked to
1057 # anything else. Use transactions for LDBs.
1058 for path in all_files:
1059 if not os.path.exists(path + self.backup_ext):
1060 if path.endswith('.ldb'):
1061 logger.info('Starting transaction on solo db: ' + path)
1062 ldb_obj = Ldb(path, lp=lp)
1063 ldb_obj.transaction_start()
1064 logger.info(' running tdbbackup on the same file')
1065 self.offline_tdb_copy(path)
1066 ldb_obj.transaction_cancel()
1067 elif path.endswith('.tdb'):
1068 logger.info('running tdbbackup on lone tdb file ' + path)
1069 self.offline_tdb_copy(path)
1071 # Now make the backup tar file and add all
1072 # backed up files and any other files to it.
1073 temp_tar_dir = tempfile.mkdtemp(dir=targetdir,
1074 prefix='INCOMPLETEsambabackupfile')
1075 temp_tar_name = os.path.join(temp_tar_dir, "samba-backup.tar.bz2")
1076 tar = tarfile.open(temp_tar_name, 'w:bz2')
1078 logger.info('running offline ntacl backup of sysvol')
1079 sysvol_tar_fn = 'sysvol.tar.gz'
1080 sysvol_tar = os.path.join(temp_tar_dir, sysvol_tar_fn)
1081 backup_offline(paths.sysvol, sysvol_tar, samdb, paths.smbconf)
1082 tar.add(sysvol_tar, sysvol_tar_fn)
1083 os.remove(sysvol_tar)
1085 create_log_file(temp_tar_dir, lp, "offline", "localhost", True)
1086 backup_fn = os.path.join(temp_tar_dir, "backup.txt")
1087 tar.add(backup_fn, os.path.basename(backup_fn))
1088 os.remove(backup_fn)
1090 logger.info('building backup tar')
1091 for path in all_files:
1092 arc_path = self.get_arc_path(path, paths)
1094 if os.path.exists(path + self.backup_ext):
1095 logger.info(' adding backup ' + arc_path + self.backup_ext +
1096 ' to tar and deleting file')
1097 tar.add(path + self.backup_ext, arcname=arc_path)
1098 os.remove(path + self.backup_ext)
1099 elif path.endswith('.ldb') or path.endswith('.tdb'):
1100 logger.info(' skipping ' + arc_path)
1102 logger.info(' adding misc file ' + arc_path)
1103 tar.add(path, arcname=arc_path)
1106 os.rename(temp_tar_name,
1107 os.path.join(targetdir,
1108 'samba-backup-{0}.tar.bz2'.format(time_str)))
1109 os.rmdir(temp_tar_dir)
1110 logger.info('Backup succeeded.')
1113 class cmd_domain_backup(samba.netcmd.SuperCommand):
1114 '''Create or restore a backup of the domain.'''
1115 subcommands = {'offline': cmd_domain_backup_offline(),
1116 'online': cmd_domain_backup_online(),
1117 'rename': cmd_domain_backup_rename(),
1118 'restore': cmd_domain_backup_restore()}