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
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
38 from fsmo import cmd_fsmo_seize
39 from samba.provision import make_smbconf
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
53 from subprocess import CalledProcessError
56 # work out a SID (based on a free RID) to use when the domain gets restored.
57 # This ensures that the restored DC's SID won't clash with any other RIDs
58 # already in use in the domain
59 def get_sid_for_restore(samdb):
60 # Find the DN of the RID set of the server
61 res = samdb.search(base=ldb.Dn(samdb, samdb.get_serverName()),
62 scope=ldb.SCOPE_BASE, attrs=["serverReference"])
63 server_ref_dn = ldb.Dn(samdb, res[0]['serverReference'][0])
64 res = samdb.search(base=server_ref_dn,
66 attrs=['rIDSetReferences'])
67 rid_set_dn = ldb.Dn(samdb, res[0]['rIDSetReferences'][0])
69 # Get the alloc pools and next RID of the RID set
70 res = samdb.search(base=rid_set_dn,
71 scope=ldb.SCOPE_SUBTREE,
72 expression="(rIDNextRID=*)",
73 attrs=['rIDAllocationPool',
74 'rIDPreviousAllocationPool',
77 # Decode the bounds of the RID allocation pools
78 rid = int(res[0].get('rIDNextRID')[0])
81 high = (0xFFFFFFFF00000000 & int(num)) >> 32
82 low = 0x00000000FFFFFFFF & int(num)
84 pool_l, pool_h = split_val(res[0].get('rIDPreviousAllocationPool')[0])
85 npool_l, npool_h = split_val(res[0].get('rIDAllocationPool')[0])
87 # Calculate next RID based on pool bounds
89 raise CommandError('Out of RIDs, finished AllocPool')
92 raise CommandError('Out of RIDs, finished PrevAllocPool.')
98 sid = dom_sid(samdb.get_domain_sid())
99 return str(sid) + '-' + str(rid)
103 return datetime.datetime.now().isoformat().replace(':', '-')
106 def backup_filepath(targetdir, name, time_str):
107 filename = 'samba-backup-{}-{}.tar.bz2'.format(name, time_str)
108 return os.path.join(targetdir, filename)
111 def create_backup_tar(logger, tmpdir, backup_filepath):
112 # Adds everything in the tmpdir into a new tar file
113 logger.info("Creating backup file %s..." % backup_filepath)
114 tf = tarfile.open(backup_filepath, 'w:bz2')
115 tf.add(tmpdir, arcname='./')
119 def create_log_file(targetdir, lp, backup_type, server, include_secrets,
121 # create a summary file about the backup, which will get included in the
122 # tar file. This makes it easy for users to see what the backup involved,
123 # without having to untar the DB and interrogate it
124 f = open(os.path.join(targetdir, "backup.txt"), 'w')
126 time_str = datetime.datetime.now().strftime('%Y-%b-%d %H:%M:%S')
127 f.write("Backup created %s\n" % time_str)
128 f.write("Using samba-tool version: %s\n" % lp.get('server string'))
129 f.write("Domain %s backup, using DC '%s'\n" % (backup_type, server))
130 f.write("Backup for domain %s (NetBIOS), %s (DNS realm)\n" %
131 (lp.get('workgroup'), lp.get('realm').lower()))
132 f.write("Backup contains domain secrets: %s\n" % str(include_secrets))
134 f.write("%s\n" % extra_info)
139 # Add a backup-specific marker to the DB with info that we'll use during
140 # the restore process
141 def add_backup_marker(samdb, marker, value):
143 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
144 m[marker] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, marker)
148 def check_targetdir(logger, targetdir):
149 if targetdir is None:
150 raise CommandError('Target directory required')
152 if not os.path.exists(targetdir):
153 logger.info('Creating targetdir %s...' % targetdir)
154 os.makedirs(targetdir)
155 elif not os.path.isdir(targetdir):
156 raise CommandError("%s is not a directory" % targetdir)
159 def check_online_backup_args(logger, creds, server, targetdir):
160 # Make sure we have all the required args.
162 raise CommandError('Server required')
164 check_targetdir(logger, targetdir)
167 # For '--no-secrets' backups, this sets the Administrator user's password to a
168 # randomly-generated value. This is similar to the provision behaviour
169 def set_admin_password(logger, samdb, username):
170 """Sets a randomly generated password for the backup DB's admin user"""
172 adminpass = samba.generate_random_password(12, 32)
173 logger.info("Setting %s password in backup to: %s" % (username, adminpass))
174 logger.info("Run 'samba-tool user setpassword %s' after restoring DB" %
176 samdb.setpassword("(&(objectClass=user)(sAMAccountName=%s))"
177 % ldb.binary_encode(username), adminpass,
178 force_change_at_next_login=False,
182 class cmd_domain_backup_online(samba.netcmd.Command):
183 '''Copy a running DC's current DB into a backup tar file.
185 Takes a backup copy of the current domain from a running DC. If the domain
186 were to undergo a catastrophic failure, then the backup file can be used to
187 recover the domain. The backup created is similar to the DB that a new DC
188 would receive when it joins the domain.
191 - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
192 and fix any errors it reports.
193 - all the domain's secrets are included in the backup file.
194 - although the DB contents can be untarred and examined manually, you need
195 to run 'samba-tool domain backup restore' before you can start a Samba DC
196 from the backup file.'''
198 synopsis = "%prog --server=<DC-to-backup> --targetdir=<output-dir>"
199 takes_optiongroups = {
200 "sambaopts": options.SambaOptions,
201 "credopts": options.CredentialsOptions,
205 Option("--server", help="The DC to backup", type=str),
206 Option("--targetdir", type=str,
207 help="Directory to write the backup file to"),
208 Option("--no-secrets", action="store_true", default=False,
209 help="Exclude secret values from the backup created")
212 def run(self, sambaopts=None, credopts=None, server=None, targetdir=None,
214 logger = self.get_logger()
215 logger.setLevel(logging.DEBUG)
217 lp = sambaopts.get_loadparm()
218 creds = credopts.get_credentials(lp)
220 # Make sure we have all the required args.
221 check_online_backup_args(logger, creds, server, targetdir)
223 tmpdir = tempfile.mkdtemp(dir=targetdir)
225 # Run a clone join on the remote
226 include_secrets = not no_secrets
227 ctx = join_clone(logger=logger, creds=creds, lp=lp,
228 include_secrets=include_secrets, server=server,
229 dns_backend='SAMBA_INTERNAL', targetdir=tmpdir)
231 # get the paths used for the clone, then drop the old samdb connection
235 # Get a free RID to use as the new DC's SID (when it gets restored)
236 remote_sam = SamDB(url='ldap://' + server, credentials=creds,
237 session_info=system_session(), lp=lp)
238 new_sid = get_sid_for_restore(remote_sam)
239 realm = remote_sam.domain_dns_name()
241 # Grab the remote DC's sysvol files and bundle them into a tar file
242 sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
243 smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
244 backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
246 # remove the default sysvol files created by the clone (we want to
247 # make sure we restore the sysvol.tar.gz files instead)
248 shutil.rmtree(paths.sysvol)
250 # Edit the downloaded sam.ldb to mark it as a backup
251 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
252 time_str = get_timestamp()
253 add_backup_marker(samdb, "backupDate", time_str)
254 add_backup_marker(samdb, "sidForRestore", new_sid)
256 # ensure the admin user always has a password set (same as provision)
258 set_admin_password(logger, samdb, creds.get_username())
260 # Add everything in the tmpdir to the backup tar file
261 backup_file = backup_filepath(targetdir, realm, time_str)
262 create_log_file(tmpdir, lp, "online", server, include_secrets)
263 create_backup_tar(logger, tmpdir, backup_file)
265 shutil.rmtree(tmpdir)
268 class cmd_domain_backup_restore(cmd_fsmo_seize):
269 '''Restore the domain's DB from a backup-file.
271 This restores a previously backed up copy of the domain's DB on a new DC.
273 Note that the restored DB will not contain the original DC that the backup
274 was taken from (or any other DCs in the original domain). Only the new DC
275 (specified by --newservername) will be present in the restored DB.
277 Samba can then be started against the restored DB. Any existing DCs for the
278 domain should be shutdown before the new DC is started. Other DCs can then
279 be joined to the new DC to recover the network.
281 Note that this command should be run as the root user - it will fail
284 synopsis = ("%prog --backup-file=<tar-file> --targetdir=<output-dir> "
285 "--newservername=<DC-name>")
287 Option("--backup-file", help="Path to backup file", type=str),
288 Option("--targetdir", help="Path to write to", type=str),
289 Option("--newservername", help="Name for new server", type=str),
290 Option("--host-ip", type="string", metavar="IPADDRESS",
291 help="set IPv4 ipaddress"),
292 Option("--host-ip6", type="string", metavar="IP6ADDRESS",
293 help="set IPv6 ipaddress"),
296 takes_optiongroups = {
297 "sambaopts": options.SambaOptions,
298 "credopts": options.CredentialsOptions,
301 def register_dns_zone(self, logger, samdb, lp, ntdsguid, host_ip,
304 Registers the new realm's DNS objects when a renamed domain backup
307 names = guess_names(lp)
308 domaindn = names.domaindn
309 forestdn = samdb.get_root_basedn().get_linearized()
310 dnsdomain = names.dnsdomain.lower()
311 dnsforest = dnsdomain
312 hostname = names.netbiosname.lower()
313 domainsid = dom_sid(samdb.get_domain_sid())
314 dnsadmins_sid = get_dnsadmins_sid(samdb, domaindn)
315 domainguid = get_domainguid(samdb, domaindn)
317 # work out the IP address to use for the new DC's DNS records
318 host_ip = determine_host_ip(logger, lp, host_ip)
319 host_ip6 = determine_host_ip6(logger, lp, host_ip6)
321 if host_ip is None and host_ip6 is None:
322 raise CommandError('Please specify a host-ip for the new server')
324 logger.info("DNS realm was renamed to %s" % dnsdomain)
325 logger.info("Populating DNS partitions for new realm...")
327 # Add the DNS objects for the new realm (note: the backup clone already
328 # has the root server objects, so don't add them again)
329 fill_dns_data_partitions(samdb, domainsid, names.sitename, domaindn,
330 forestdn, dnsdomain, dnsforest, hostname,
331 host_ip, host_ip6, domainguid, ntdsguid,
332 dnsadmins_sid, add_root=False)
334 def fix_old_dc_references(self, samdb):
335 '''Fixes attributes that reference the old/removed DCs'''
337 # we just want to fix up DB problems here that were introduced by us
338 # removing the old DCs. We restrict what we fix up so that the restored
339 # DB matches the backed-up DB as close as possible. (There may be other
340 # DB issues inherited from the backed-up DC, but it's not our place to
341 # silently try to fix them here).
342 samdb.transaction_start()
343 chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
346 # fix up stale references to the old DC
347 setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
348 attrs = ['lastKnownParent', 'interSiteTopologyGenerator']
350 # fix-up stale one-way links that point to the old DC
351 setattr(chk, 'remove_plausible_deleted_DN_links', 'ALL')
352 attrs += ['msDS-NC-Replica-Locations']
354 cross_ncs_ctrl = 'search_options:1:2'
355 controls = ['show_deleted:1', cross_ncs_ctrl]
356 chk.check_database(controls=controls, attrs=attrs)
357 samdb.transaction_commit()
359 def run(self, sambaopts=None, credopts=None, backup_file=None,
360 targetdir=None, newservername=None, host_ip=None, host_ip6=None):
361 if not (backup_file and os.path.exists(backup_file)):
362 raise CommandError('Backup file not found.')
363 if targetdir is None:
364 raise CommandError('Please specify a target directory')
365 # allow restoredc to install into a directory prepopulated by selftest
366 if (os.path.exists(targetdir) and os.listdir(targetdir) and
367 os.environ.get('SAMBA_SELFTEST') != '1'):
368 raise CommandError('Target directory is not empty')
369 if not newservername:
370 raise CommandError('Server name required')
372 logger = logging.getLogger()
373 logger.setLevel(logging.DEBUG)
374 logger.addHandler(logging.StreamHandler(sys.stdout))
376 # ldapcmp prefers the server's netBIOS name in upper-case
377 newservername = newservername.upper()
379 # extract the backup .tar to a temp directory
380 targetdir = os.path.abspath(targetdir)
381 tf = tarfile.open(backup_file)
382 tf.extractall(targetdir)
385 # use the smb.conf that got backed up, by default (save what was
386 # actually backed up, before we mess with it)
387 smbconf = os.path.join(targetdir, 'etc', 'smb.conf')
388 shutil.copyfile(smbconf, smbconf + ".orig")
390 # if a smb.conf was specified on the cmd line, then use that instead
391 cli_smbconf = sambaopts.get_loadparm_path()
393 logger.info("Using %s as restored domain's smb.conf" % cli_smbconf)
394 shutil.copyfile(cli_smbconf, smbconf)
396 lp = samba.param.LoadParm()
399 # open a DB connection to the restored DB
400 private_dir = os.path.join(targetdir, 'private')
401 samdb_path = os.path.join(private_dir, 'sam.ldb')
402 samdb = SamDB(url=samdb_path, session_info=system_session(), lp=lp)
404 # Create account using the join_add_objects function in the join object
405 # We need namingContexts, account control flags, and the sid saved by
406 # the backup process.
407 res = samdb.search(base="", scope=ldb.SCOPE_BASE,
408 attrs=['namingContexts'])
409 ncs = [str(r) for r in res[0].get('namingContexts')]
411 creds = credopts.get_credentials(lp)
412 ctx = DCJoinContext(logger, creds=creds, lp=lp,
413 forced_local_samdb=samdb,
414 netbios_name=newservername)
416 ctx.full_nc_list = ncs
417 ctx.userAccountControl = (samba.dsdb.UF_SERVER_TRUST_ACCOUNT |
418 samba.dsdb.UF_TRUSTED_FOR_DELEGATION)
420 # rewrite the smb.conf to make sure it uses the new targetdir settings.
421 # (This doesn't update all filepaths in a customized config, but it
422 # corrects the same paths that get set by a new provision)
423 logger.info('Updating basic smb.conf settings...')
424 make_smbconf(smbconf, newservername, ctx.domain_name,
425 ctx.realm, targetdir, lp=lp,
426 serverrole="active directory domain controller")
428 # Get the SID saved by the backup process and create account
429 res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
430 scope=ldb.SCOPE_BASE,
431 attrs=['sidForRestore', 'backupRename'])
432 is_rename = True if 'backupRename' in res[0] else False
433 sid = res[0].get('sidForRestore')[0]
434 logger.info('Creating account with SID: ' + str(sid))
435 ctx.join_add_objects(specified_sid=dom_sid(sid))
438 m.dn = ldb.Dn(samdb, '@ROOTDSE')
439 ntds_guid = str(ctx.ntds_guid)
440 m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % ntds_guid,
441 ldb.FLAG_MOD_REPLACE,
445 # if we renamed the backed-up domain, then we need to add the DNS
446 # objects for the new realm (we do this in the restore, now that we
447 # know the new DC's IP address)
449 self.register_dns_zone(logger, samdb, lp, ctx.ntds_guid,
452 secrets_path = os.path.join(private_dir, 'secrets.ldb')
453 secrets_ldb = Ldb(secrets_path, session_info=system_session(), lp=lp)
454 secretsdb_self_join(secrets_ldb, domain=ctx.domain_name,
455 realm=ctx.realm, dnsdomain=ctx.dnsdomain,
456 netbiosname=ctx.myname, domainsid=ctx.domsid,
457 machinepass=ctx.acct_pass,
458 key_version_number=ctx.key_version_number,
459 secure_channel_type=misc.SEC_CHAN_BDC)
462 domain_dn = samdb.domain_dn()
463 forest_dn = samba.dn_from_dns_name(samdb.forest_dns_name())
464 domaindns_dn = ("CN=Infrastructure,DC=DomainDnsZones,", domain_dn)
465 forestdns_dn = ("CN=Infrastructure,DC=ForestDnsZones,", forest_dn)
466 for dn_prefix, dns_dn in [forestdns_dn, domaindns_dn]:
467 if dns_dn not in ncs:
469 full_dn = dn_prefix + dns_dn
471 m.dn = ldb.Dn(samdb, full_dn)
472 m["fSMORoleOwner"] = ldb.MessageElement(samdb.get_dsServiceName(),
473 ldb.FLAG_MOD_REPLACE,
478 for role in ['rid', 'pdc', 'naming', 'infrastructure', 'schema']:
479 self.seize_role(role, samdb, force=True)
481 # Get all DCs and remove them (this ensures these DCs cannot
482 # replicate because they will not have a password)
483 search_expr = "(&(objectClass=Server)(serverReference=*))"
484 res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
485 expression=search_expr)
488 if cn != newservername:
489 remove_dc(samdb, logger, cn)
491 # Remove the repsFrom and repsTo from each NC to ensure we do
492 # not try (and fail) to talk to the old DCs
495 msg.dn = ldb.Dn(samdb, nc)
497 msg["repsFrom"] = ldb.MessageElement([],
498 ldb.FLAG_MOD_REPLACE,
500 msg["repsTo"] = ldb.MessageElement([],
501 ldb.FLAG_MOD_REPLACE,
505 # Update the krbtgt passwords twice, ensuring no tickets from
506 # the old domain are valid
507 update_krbtgt_account_password(samdb)
508 update_krbtgt_account_password(samdb)
510 # restore the sysvol directory from the backup tar file, including the
511 # original NTACLs. Note that the backup_restore() will fail if not root
512 sysvol_tar = os.path.join(targetdir, 'sysvol.tar.gz')
513 dest_sysvol_dir = lp.get('path', 'sysvol')
514 if not os.path.exists(dest_sysvol_dir):
515 os.makedirs(dest_sysvol_dir)
516 backup_restore(sysvol_tar, dest_sysvol_dir, samdb, smbconf)
517 os.remove(sysvol_tar)
519 # fix up any stale links to the old DCs we just removed
520 logger.info("Fixing up any remaining references to the old DCs...")
521 self.fix_old_dc_references(samdb)
523 # Remove DB markers added by the backup process
525 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
526 m["backupDate"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
528 m["sidForRestore"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
531 m["backupRename"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
535 logger.info("Backup file successfully restored to %s" % targetdir)
536 logger.info("Please check the smb.conf settings are correct before "
540 class cmd_domain_backup_rename(samba.netcmd.Command):
541 '''Copy a running DC's DB to backup file, renaming the domain in the process.
543 Where <new-domain> is the new domain's NetBIOS name, and <new-dnsrealm> is
544 the new domain's realm in DNS form.
546 This is similar to 'samba-tool backup online' in that it clones the DB of a
547 running DC. However, this option also renames all the domain entries in the
548 DB. Renaming the domain makes it possible to restore and start a new Samba
549 DC without it interfering with the existing Samba domain. In other words,
550 you could use this option to clone your production samba domain and restore
551 it to a separate pre-production environment that won't overlap or interfere
552 with the existing production Samba domain.
555 - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
556 and fix any errors it reports.
557 - all the domain's secrets are included in the backup file.
558 - although the DB contents can be untarred and examined manually, you need
559 to run 'samba-tool domain backup restore' before you can start a Samba DC
560 from the backup file.
561 - GPO and sysvol information will still refer to the old realm and will
562 need to be updated manually.
563 - if you specify 'keep-dns-realm', then the DNS records will need updating
564 in order to work (they will still refer to the old DC's IP instead of the
566 - we recommend that you only use this option if you know what you're doing.
569 synopsis = ("%prog <new-domain> <new-dnsrealm> --server=<DC-to-backup> "
570 "--targetdir=<output-dir>")
571 takes_optiongroups = {
572 "sambaopts": options.SambaOptions,
573 "credopts": options.CredentialsOptions,
577 Option("--server", help="The DC to backup", type=str),
578 Option("--targetdir", help="Directory to write the backup file",
580 Option("--keep-dns-realm", action="store_true", default=False,
581 help="Retain the DNS entries for the old realm in the backup"),
582 Option("--no-secrets", action="store_true", default=False,
583 help="Exclude secret values from the backup created")
586 takes_args = ["new_domain_name", "new_dns_realm"]
588 def update_dns_root(self, logger, samdb, old_realm, delete_old_dns):
589 '''Updates dnsRoot for the partition objects to reflect the rename'''
591 # lookup the crossRef objects that hold the old realm's dnsRoot
592 partitions_dn = samdb.get_partitions_dn()
593 res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
595 expression='(&(objectClass=crossRef)(dnsRoot=*))')
596 new_realm = samdb.domain_dns_name()
598 # go through and add the new realm
600 # dnsRoot can be multi-valued, so only look for the old realm
601 for dns_root in res_msg["dnsRoot"]:
603 if old_realm in dns_root:
604 new_dns_root = re.sub('%s$' % old_realm, new_realm,
606 logger.info("Adding %s dnsRoot to %s" % (new_dns_root, dn))
610 m["dnsRoot"] = ldb.MessageElement(new_dns_root,
615 # optionally remove the dnsRoot for the old realm
617 logger.info("Removing %s dnsRoot from %s" % (dns_root,
619 m["dnsRoot"] = ldb.MessageElement(dns_root,
624 # Updates the CN=<domain>,CN=Partitions,CN=Configuration,... object to
625 # reflect the domain rename
626 def rename_domain_partition(self, logger, samdb, new_netbios_name):
627 '''Renames the domain parition object and updates its nETBIOSName'''
629 # lookup the crossRef object that holds the nETBIOSName (nCName has
630 # already been updated by this point, but the netBIOS hasn't)
631 base_dn = samdb.get_default_basedn()
632 nc_name = ldb.binary_encode(str(base_dn))
633 partitions_dn = samdb.get_partitions_dn()
634 res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
635 attrs=["nETBIOSName"],
636 expression='ncName=%s' % nc_name)
638 logger.info("Changing backup domain's NetBIOS name to %s" %
642 m["nETBIOSName"] = ldb.MessageElement(new_netbios_name,
643 ldb.FLAG_MOD_REPLACE,
647 # renames the object itself to reflect the change in domain
648 new_dn = "CN=%s,%s" % (new_netbios_name, partitions_dn)
649 logger.info("Renaming %s --> %s" % (res[0].dn, new_dn))
650 samdb.rename(res[0].dn, new_dn, controls=['relax:0'])
652 def delete_old_dns_zones(self, logger, samdb, old_realm):
653 # remove the top-level DNS entries for the old realm
654 basedn = samdb.get_default_basedn()
655 dn = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (old_realm, basedn)
656 logger.info("Deleting old DNS zone %s" % dn)
657 samdb.delete(dn, ["tree_delete:1"])
659 forestdn = samdb.get_root_basedn().get_linearized()
660 dn = "DC=_msdcs.%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (old_realm,
662 logger.info("Deleting old DNS zone %s" % dn)
663 samdb.delete(dn, ["tree_delete:1"])
665 def fix_old_dn_attributes(self, samdb):
666 '''Fixes attributes (i.e. objectCategory) that still use the old DN'''
668 samdb.transaction_start()
669 # Just fix any mismatches in DN detected (leave any other errors)
670 chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
672 # fix up incorrect objectCategory/etc attributes
673 setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
674 cross_ncs_ctrl = 'search_options:1:2'
675 controls = ['show_deleted:1', cross_ncs_ctrl]
676 chk.check_database(controls=controls)
677 samdb.transaction_commit()
679 def run(self, new_domain_name, new_dns_realm, sambaopts=None,
680 credopts=None, server=None, targetdir=None, keep_dns_realm=False,
682 logger = self.get_logger()
683 logger.setLevel(logging.INFO)
685 lp = sambaopts.get_loadparm()
686 creds = credopts.get_credentials(lp)
688 # Make sure we have all the required args.
689 check_online_backup_args(logger, creds, server, targetdir)
690 delete_old_dns = not keep_dns_realm
692 new_dns_realm = new_dns_realm.lower()
693 new_domain_name = new_domain_name.upper()
695 new_base_dn = samba.dn_from_dns_name(new_dns_realm)
696 logger.info("New realm for backed up domain: %s" % new_dns_realm)
697 logger.info("New base DN for backed up domain: %s" % new_base_dn)
698 logger.info("New domain NetBIOS name: %s" % new_domain_name)
700 tmpdir = tempfile.mkdtemp(dir=targetdir)
702 # setup a join-context for cloning the remote server
703 include_secrets = not no_secrets
704 ctx = DCCloneAndRenameContext(new_base_dn, new_domain_name,
705 new_dns_realm, logger=logger,
707 include_secrets=include_secrets,
708 dns_backend='SAMBA_INTERNAL',
709 server=server, targetdir=tmpdir)
711 # sanity-check we're not "renaming" the domain to the same values
712 old_domain = ctx.domain_name
713 if old_domain == new_domain_name:
714 shutil.rmtree(tmpdir)
715 raise CommandError("Cannot use the current domain NetBIOS name.")
717 old_realm = ctx.realm
718 if old_realm == new_dns_realm:
719 shutil.rmtree(tmpdir)
720 raise CommandError("Cannot use the current domain DNS realm.")
722 # do the clone/rename
725 # get the paths used for the clone, then drop the old samdb connection
729 # get a free RID to use as the new DC's SID (when it gets restored)
730 remote_sam = SamDB(url='ldap://' + server, credentials=creds,
731 session_info=system_session(), lp=lp)
732 new_sid = get_sid_for_restore(remote_sam)
734 # Grab the remote DC's sysvol files and bundle them into a tar file.
735 # Note we end up with 2 sysvol dirs - the original domain's files (that
736 # use the old realm) backed here, as well as default files generated
737 # for the new realm as part of the clone/join.
738 sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
739 smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
740 backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
742 # connect to the local DB (making sure we use the new/renamed config)
743 lp.load(paths.smbconf)
744 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
746 # Edit the cloned sam.ldb to mark it as a backup
747 time_str = get_timestamp()
748 add_backup_marker(samdb, "backupDate", time_str)
749 add_backup_marker(samdb, "sidForRestore", new_sid)
750 add_backup_marker(samdb, "backupRename", old_realm)
752 # fix up the DNS objects that are using the old dnsRoot value
753 self.update_dns_root(logger, samdb, old_realm, delete_old_dns)
755 # update the netBIOS name and the Partition object for the domain
756 self.rename_domain_partition(logger, samdb, new_domain_name)
759 self.delete_old_dns_zones(logger, samdb, old_realm)
761 logger.info("Fixing DN attributes after rename...")
762 self.fix_old_dn_attributes(samdb)
764 # ensure the admin user always has a password set (same as provision)
766 set_admin_password(logger, samdb, creds.get_username())
768 # Add everything in the tmpdir to the backup tar file
769 backup_file = backup_filepath(targetdir, new_dns_realm, time_str)
770 create_log_file(tmpdir, lp, "rename", server, include_secrets,
771 "Original domain %s (NetBIOS), %s (DNS realm)" %
772 (old_domain, old_realm))
773 create_backup_tar(logger, tmpdir, backup_file)
775 shutil.rmtree(tmpdir)
778 class cmd_domain_backup_offline(samba.netcmd.Command):
779 '''Backup the local domain directories safely into a tar file.
781 Takes a backup copy of the current domain from the local files on disk,
782 with proper locking of the DB to ensure consistency. If the domain were to
783 undergo a catastrophic failure, then the backup file can be used to recover
786 An offline backup differs to an online backup in the following ways:
787 - a backup can be created even if the DC isn't currently running.
788 - includes non-replicated attributes that an online backup wouldn't store.
789 - takes a copy of the raw database files, which has the risk that any
790 hidden problems in the DB are preserved in the backup.'''
792 synopsis = "%prog [options]"
793 takes_optiongroups = {
794 "sambaopts": options.SambaOptions,
798 Option("--targetdir",
799 help="Output directory (required)",
803 backup_ext = '.bak-offline'
805 def offline_tdb_copy(self, path):
806 backup_path = path + self.backup_ext
808 tdb_copy(path, backup_path, readonly=True)
809 except CalledProcessError as copy_err:
810 # If the copy didn't work, check if it was caused by an EINVAL
811 # error on opening the DB. If so, it's a mutex locked database,
812 # which we can safely ignore.
815 except Exception as e:
816 if hasattr(e, 'errno') and e.errno == errno.EINVAL:
820 if not os.path.exists(backup_path):
821 s = "tdbbackup said backup succeeded but {} not found"
822 raise CommandError(s.format(backup_path))
824 def offline_mdb_copy(self, path):
825 mdb_copy(path, path + self.backup_ext)
827 # Secrets databases are a special case: a transaction must be started
828 # on the secrets.ldb file before backing up that file and secrets.tdb
829 def backup_secrets(self, private_dir, lp, logger):
830 secrets_path = os.path.join(private_dir, 'secrets')
831 secrets_obj = Ldb(secrets_path + '.ldb', lp=lp)
832 logger.info('Starting transaction on ' + secrets_path)
833 secrets_obj.transaction_start()
834 self.offline_tdb_copy(secrets_path + '.ldb')
835 self.offline_tdb_copy(secrets_path + '.tdb')
836 secrets_obj.transaction_cancel()
838 # sam.ldb must have a transaction started on it before backing up
839 # everything in sam.ldb.d with the appropriate backup function.
840 def backup_smb_dbs(self, private_dir, samdb, lp, logger):
841 # First, determine if DB backend is MDB. Assume not unless there is a
842 # 'backendStore' attribute on @PARTITION containing the text 'mdb'
843 store_label = "backendStore"
844 res = samdb.search(base="@PARTITION", scope=ldb.SCOPE_BASE,
846 mdb_backend = store_label in res[0] and res[0][store_label][0] == 'mdb'
848 sam_ldb_path = os.path.join(private_dir, 'sam.ldb')
851 logger.info('MDB backend detected. Using mdb backup function.')
852 copy_function = self.offline_mdb_copy
854 logger.info('Starting transaction on ' + sam_ldb_path)
855 copy_function = self.offline_tdb_copy
856 sam_obj = Ldb(sam_ldb_path, lp=lp)
857 sam_obj.transaction_start()
859 logger.info(' backing up ' + sam_ldb_path)
860 self.offline_tdb_copy(sam_ldb_path)
861 sam_ldb_d = sam_ldb_path + '.d'
862 for sam_file in os.listdir(sam_ldb_d):
863 sam_file = os.path.join(sam_ldb_d, sam_file)
864 if sam_file.endswith('.ldb'):
865 logger.info(' backing up locked/related file ' + sam_file)
866 copy_function(sam_file)
868 logger.info(' copying locked/related file ' + sam_file)
869 shutil.copyfile(sam_file, sam_file + self.backup_ext)
872 sam_obj.transaction_cancel()
874 # Find where a path should go in the fixed backup archive structure.
875 def get_arc_path(self, path, conf_paths):
876 backup_dirs = {"private": conf_paths.private_dir,
877 "statedir": conf_paths.state_dir,
878 "etc": os.path.dirname(conf_paths.smbconf)}
879 matching_dirs = [(_, p) for (_, p) in backup_dirs.items() if
881 arc_path, fs_path = matching_dirs[0]
883 # If more than one directory is a parent of this path, then at least
884 # one configured path is a subdir of another. Use closest match.
885 if len(matching_dirs) > 1:
886 arc_path, fs_path = max(matching_dirs, key=lambda (_, p): len(p))
887 arc_path += path[len(fs_path):]
891 def run(self, sambaopts=None, targetdir=None):
893 logger = logging.getLogger()
894 logger.setLevel(logging.DEBUG)
895 logger.addHandler(logging.StreamHandler(sys.stdout))
897 # Get the absolute paths of all the directories we're going to backup
898 lp = sambaopts.get_loadparm()
900 paths = samba.provision.provision_paths_from_lp(lp, lp.get('realm'))
901 if not (paths.samdb and os.path.exists(paths.samdb)):
902 raise CommandError('No sam.db found. This backup ' +
903 'tool is only for AD DCs')
905 check_targetdir(logger, targetdir)
907 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
908 sid = get_sid_for_restore(samdb)
910 backup_dirs = [paths.private_dir, paths.state_dir,
911 os.path.dirname(paths.smbconf)] # etc dir
912 logger.info('running backup on dirs: {}'.format(backup_dirs))
914 # Recursively get all file paths in the backup directories
916 for backup_dir in backup_dirs:
917 for (working_dir, _, filenames) in os.walk(backup_dir):
918 if working_dir.startswith(paths.sysvol):
921 for filename in filenames:
922 if filename in all_files:
925 # Assume existing backup files are from a previous backup.
927 if filename.endswith(self.backup_ext):
928 os.remove(os.path.join(working_dir, filename))
930 all_files.append(os.path.join(working_dir, filename))
932 # Backup secrets, sam.ldb and their downstream files
933 self.backup_secrets(paths.private_dir, lp, logger)
934 self.backup_smb_dbs(paths.private_dir, samdb, lp, logger)
936 # Open the new backed up samdb, flag it as backed up, and write
937 # the next SID so the restore tool can add objects.
938 # WARNING: Don't change this code unless you know what you're doing.
939 # Writing to a .bak file only works because the DN being
940 # written to happens to be top level.
941 samdb = SamDB(url=paths.samdb + self.backup_ext,
942 session_info=system_session(), lp=lp)
943 time_str = get_timestamp()
944 add_backup_marker(samdb, "backupDate", time_str)
945 add_backup_marker(samdb, "sidForRestore", sid)
947 # Now handle all the LDB and TDB files that are not linked to
948 # anything else. Use transactions for LDBs.
949 for path in all_files:
950 if not os.path.exists(path + self.backup_ext):
951 if path.endswith('.ldb'):
952 logger.info('Starting transaction on solo db: ' + path)
953 ldb_obj = Ldb(path, lp=lp)
954 ldb_obj.transaction_start()
955 logger.info(' running tdbbackup on the same file')
956 self.offline_tdb_copy(path)
957 ldb_obj.transaction_cancel()
958 elif path.endswith('.tdb'):
959 logger.info('running tdbbackup on lone tdb file ' + path)
960 self.offline_tdb_copy(path)
962 # Now make the backup tar file and add all
963 # backed up files and any other files to it.
964 temp_tar_dir = tempfile.mkdtemp(dir=targetdir,
965 prefix='INCOMPLETEsambabackupfile')
966 temp_tar_name = os.path.join(temp_tar_dir, "samba-backup.tar.bz2")
967 tar = tarfile.open(temp_tar_name, 'w:bz2')
969 logger.info('running offline ntacl backup of sysvol')
970 sysvol_tar_fn = 'sysvol.tar.gz'
971 sysvol_tar = os.path.join(temp_tar_dir, sysvol_tar_fn)
972 backup_offline(paths.sysvol, sysvol_tar, samdb, paths.smbconf)
973 tar.add(sysvol_tar, sysvol_tar_fn)
974 os.remove(sysvol_tar)
976 create_log_file(temp_tar_dir, lp, "offline", "localhost", True)
977 backup_fn = os.path.join(temp_tar_dir, "backup.txt")
978 tar.add(backup_fn, os.path.basename(backup_fn))
981 logger.info('building backup tar')
982 for path in all_files:
983 arc_path = self.get_arc_path(path, paths)
985 if os.path.exists(path + self.backup_ext):
986 logger.info(' adding backup ' + arc_path + self.backup_ext +
987 ' to tar and deleting file')
988 tar.add(path + self.backup_ext, arcname=arc_path)
989 os.remove(path + self.backup_ext)
990 elif path.endswith('.ldb') or path.endswith('.tdb'):
991 logger.info(' skipping ' + arc_path)
993 logger.info(' adding misc file ' + arc_path)
994 tar.add(path, arcname=arc_path)
997 os.rename(temp_tar_name, os.path.join(targetdir,
998 'samba-backup-{}.tar.bz2'.format(time_str)))
999 os.rmdir(temp_tar_dir)
1000 logger.info('Backup succeeded.')
1003 class cmd_domain_backup(samba.netcmd.SuperCommand):
1004 '''Create or restore a backup of the domain.'''
1005 subcommands = {'offline': cmd_domain_backup_offline(),
1006 'online': cmd_domain_backup_online(),
1007 'rename': cmd_domain_backup_rename(),
1008 'restore': cmd_domain_backup_restore()}