python/samba: changes to make samba.tests.samba_tool.join run under py3
[Samba.git] / python / samba / netcmd / domain_backup.py
blobde53c6e5113c79642d9c67a5dc758c102fefde73
1 # domain_backup
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/>.
18 import datetime
19 import os
20 import sys
21 import tarfile
22 import logging
23 import shutil
24 import tempfile
25 import samba
26 import tdb
27 import samba.getopt as options
28 from samba.samdb import SamDB
29 import ldb
30 from samba import smb
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
37 from samba import Ldb
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
44 import re
45 from samba.provision import guess_names, determine_host_ip, determine_host_ip6
46 from samba.provision.sambadns import (fill_dns_data_partitions,
47 get_dnsadmins_sid,
48 get_domainguid)
49 from samba.tdb_util import tdb_copy
50 from samba.mdb_util import mdb_copy
51 import errno
52 import tdb
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,
65 scope=ldb.SCOPE_BASE,
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',
75 'rIDNextRID'])
77 # Decode the bounds of the RID allocation pools
78 rid = int(res[0].get('rIDNextRID')[0])
80 def split_val(num):
81 high = (0xFFFFFFFF00000000 & int(num)) >> 32
82 low = 0x00000000FFFFFFFF & int(num)
83 return low, high
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
88 if rid == npool_h:
89 raise CommandError('Out of RIDs, finished AllocPool')
90 if rid == pool_h:
91 if pool_h == npool_h:
92 raise CommandError('Out of RIDs, finished PrevAllocPool.')
93 rid = npool_l
94 else:
95 rid += 1
97 # Construct full SID
98 sid = dom_sid(samdb.get_domain_sid())
99 return str(sid) + '-' + str(rid)
102 def get_timestamp():
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='./')
116 tf.close()
119 def create_log_file(targetdir, lp, backup_type, server, include_secrets,
120 extra_info=None):
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')
125 try:
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))
133 if extra_info:
134 f.write("%s\n" % extra_info)
135 finally:
136 f.close()
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):
142 m = ldb.Message()
143 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
144 m[marker] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, marker)
145 samdb.modify(m)
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 # For '--no-secrets' backups, this sets the Administrator user's password to a
160 # randomly-generated value. This is similar to the provision behaviour
161 def set_admin_password(logger, samdb):
162 """Sets a randomly generated password for the backup DB's admin user"""
164 # match the admin user by RID
165 domainsid = samdb.get_domain_sid()
166 match_admin = "(objectsid={}-{})".format(domainsid,
167 security.DOMAIN_RID_ADMINISTRATOR)
168 search_expr = "(&(objectClass=user){})".format(match_admin)
170 # retrieve the admin username (just in case it's been renamed)
171 res = samdb.search(base=samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
172 expression=search_expr)
173 username = str(res[0]['samaccountname'])
175 adminpass = samba.generate_random_password(12, 32)
176 logger.info("Setting %s password in backup to: %s" % (username, adminpass))
177 logger.info("Run 'samba-tool user setpassword %s' after restoring DB" %
178 username)
179 samdb.setpassword(search_expr, adminpass, force_change_at_next_login=False,
180 username=username)
183 class cmd_domain_backup_online(samba.netcmd.Command):
184 '''Copy a running DC's current DB into a backup tar file.
186 Takes a backup copy of the current domain from a running DC. If the domain
187 were to undergo a catastrophic failure, then the backup file can be used to
188 recover the domain. The backup created is similar to the DB that a new DC
189 would receive when it joins the domain.
191 Note that:
192 - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
193 and fix any errors it reports.
194 - all the domain's secrets are included in the backup file.
195 - although the DB contents can be untarred and examined manually, you need
196 to run 'samba-tool domain backup restore' before you can start a Samba DC
197 from the backup file.'''
199 synopsis = "%prog --server=<DC-to-backup> --targetdir=<output-dir>"
200 takes_optiongroups = {
201 "sambaopts": options.SambaOptions,
202 "credopts": options.CredentialsOptions,
205 takes_options = [
206 Option("--server", help="The DC to backup", type=str),
207 Option("--targetdir", type=str,
208 help="Directory to write the backup file to"),
209 Option("--no-secrets", action="store_true", default=False,
210 help="Exclude secret values from the backup created")
213 def run(self, sambaopts=None, credopts=None, server=None, targetdir=None,
214 no_secrets=False):
215 logger = self.get_logger()
216 logger.setLevel(logging.DEBUG)
218 lp = sambaopts.get_loadparm()
219 creds = credopts.get_credentials(lp)
221 # Make sure we have all the required args.
222 if server is None:
223 raise CommandError('Server required')
225 check_targetdir(logger, targetdir)
227 tmpdir = tempfile.mkdtemp(dir=targetdir)
229 # Run a clone join on the remote
230 include_secrets = not no_secrets
231 ctx = join_clone(logger=logger, creds=creds, lp=lp,
232 include_secrets=include_secrets, server=server,
233 dns_backend='SAMBA_INTERNAL', targetdir=tmpdir)
235 # get the paths used for the clone, then drop the old samdb connection
236 paths = ctx.paths
237 del ctx
239 # Get a free RID to use as the new DC's SID (when it gets restored)
240 remote_sam = SamDB(url='ldap://' + server, credentials=creds,
241 session_info=system_session(), lp=lp)
242 new_sid = get_sid_for_restore(remote_sam)
243 realm = remote_sam.domain_dns_name()
245 # Grab the remote DC's sysvol files and bundle them into a tar file
246 sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
247 smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
248 backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
250 # remove the default sysvol files created by the clone (we want to
251 # make sure we restore the sysvol.tar.gz files instead)
252 shutil.rmtree(paths.sysvol)
254 # Edit the downloaded sam.ldb to mark it as a backup
255 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
256 time_str = get_timestamp()
257 add_backup_marker(samdb, "backupDate", time_str)
258 add_backup_marker(samdb, "sidForRestore", new_sid)
260 # ensure the admin user always has a password set (same as provision)
261 if no_secrets:
262 set_admin_password(logger, samdb)
264 # Add everything in the tmpdir to the backup tar file
265 backup_file = backup_filepath(targetdir, realm, time_str)
266 create_log_file(tmpdir, lp, "online", server, include_secrets)
267 create_backup_tar(logger, tmpdir, backup_file)
269 shutil.rmtree(tmpdir)
272 class cmd_domain_backup_restore(cmd_fsmo_seize):
273 '''Restore the domain's DB from a backup-file.
275 This restores a previously backed up copy of the domain's DB on a new DC.
277 Note that the restored DB will not contain the original DC that the backup
278 was taken from (or any other DCs in the original domain). Only the new DC
279 (specified by --newservername) will be present in the restored DB.
281 Samba can then be started against the restored DB. Any existing DCs for the
282 domain should be shutdown before the new DC is started. Other DCs can then
283 be joined to the new DC to recover the network.
285 Note that this command should be run as the root user - it will fail
286 otherwise.'''
288 synopsis = ("%prog --backup-file=<tar-file> --targetdir=<output-dir> "
289 "--newservername=<DC-name>")
290 takes_options = [
291 Option("--backup-file", help="Path to backup file", type=str),
292 Option("--targetdir", help="Path to write to", type=str),
293 Option("--newservername", help="Name for new server", type=str),
294 Option("--host-ip", type="string", metavar="IPADDRESS",
295 help="set IPv4 ipaddress"),
296 Option("--host-ip6", type="string", metavar="IP6ADDRESS",
297 help="set IPv6 ipaddress"),
300 takes_optiongroups = {
301 "sambaopts": options.SambaOptions,
302 "credopts": options.CredentialsOptions,
305 def register_dns_zone(self, logger, samdb, lp, ntdsguid, host_ip,
306 host_ip6):
308 Registers the new realm's DNS objects when a renamed domain backup
309 is restored.
311 names = guess_names(lp)
312 domaindn = names.domaindn
313 forestdn = samdb.get_root_basedn().get_linearized()
314 dnsdomain = names.dnsdomain.lower()
315 dnsforest = dnsdomain
316 hostname = names.netbiosname.lower()
317 domainsid = dom_sid(samdb.get_domain_sid())
318 dnsadmins_sid = get_dnsadmins_sid(samdb, domaindn)
319 domainguid = get_domainguid(samdb, domaindn)
321 # work out the IP address to use for the new DC's DNS records
322 host_ip = determine_host_ip(logger, lp, host_ip)
323 host_ip6 = determine_host_ip6(logger, lp, host_ip6)
325 if host_ip is None and host_ip6 is None:
326 raise CommandError('Please specify a host-ip for the new server')
328 logger.info("DNS realm was renamed to %s" % dnsdomain)
329 logger.info("Populating DNS partitions for new realm...")
331 # Add the DNS objects for the new realm (note: the backup clone already
332 # has the root server objects, so don't add them again)
333 fill_dns_data_partitions(samdb, domainsid, names.sitename, domaindn,
334 forestdn, dnsdomain, dnsforest, hostname,
335 host_ip, host_ip6, domainguid, ntdsguid,
336 dnsadmins_sid, add_root=False)
338 def fix_old_dc_references(self, samdb):
339 '''Fixes attributes that reference the old/removed DCs'''
341 # we just want to fix up DB problems here that were introduced by us
342 # removing the old DCs. We restrict what we fix up so that the restored
343 # DB matches the backed-up DB as close as possible. (There may be other
344 # DB issues inherited from the backed-up DC, but it's not our place to
345 # silently try to fix them here).
346 samdb.transaction_start()
347 chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
348 in_transaction=True)
350 # fix up stale references to the old DC
351 setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
352 attrs = ['lastKnownParent', 'interSiteTopologyGenerator']
354 # fix-up stale one-way links that point to the old DC
355 setattr(chk, 'remove_plausible_deleted_DN_links', 'ALL')
356 attrs += ['msDS-NC-Replica-Locations']
358 cross_ncs_ctrl = 'search_options:1:2'
359 controls = ['show_deleted:1', cross_ncs_ctrl]
360 chk.check_database(controls=controls, attrs=attrs)
361 samdb.transaction_commit()
363 def run(self, sambaopts=None, credopts=None, backup_file=None,
364 targetdir=None, newservername=None, host_ip=None, host_ip6=None):
365 if not (backup_file and os.path.exists(backup_file)):
366 raise CommandError('Backup file not found.')
367 if targetdir is None:
368 raise CommandError('Please specify a target directory')
369 # allow restoredc to install into a directory prepopulated by selftest
370 if (os.path.exists(targetdir) and os.listdir(targetdir) and
371 os.environ.get('SAMBA_SELFTEST') != '1'):
372 raise CommandError('Target directory is not empty')
373 if not newservername:
374 raise CommandError('Server name required')
376 logger = logging.getLogger()
377 logger.setLevel(logging.DEBUG)
378 logger.addHandler(logging.StreamHandler(sys.stdout))
380 # ldapcmp prefers the server's netBIOS name in upper-case
381 newservername = newservername.upper()
383 # extract the backup .tar to a temp directory
384 targetdir = os.path.abspath(targetdir)
385 tf = tarfile.open(backup_file)
386 tf.extractall(targetdir)
387 tf.close()
389 # use the smb.conf that got backed up, by default (save what was
390 # actually backed up, before we mess with it)
391 smbconf = os.path.join(targetdir, 'etc', 'smb.conf')
392 shutil.copyfile(smbconf, smbconf + ".orig")
394 # if a smb.conf was specified on the cmd line, then use that instead
395 cli_smbconf = sambaopts.get_loadparm_path()
396 if cli_smbconf:
397 logger.info("Using %s as restored domain's smb.conf" % cli_smbconf)
398 shutil.copyfile(cli_smbconf, smbconf)
400 lp = samba.param.LoadParm()
401 lp.load(smbconf)
403 # open a DB connection to the restored DB
404 private_dir = os.path.join(targetdir, 'private')
405 samdb_path = os.path.join(private_dir, 'sam.ldb')
406 samdb = SamDB(url=samdb_path, session_info=system_session(), lp=lp)
408 # Create account using the join_add_objects function in the join object
409 # We need namingContexts, account control flags, and the sid saved by
410 # the backup process.
411 res = samdb.search(base="", scope=ldb.SCOPE_BASE,
412 attrs=['namingContexts'])
413 ncs = [str(r) for r in res[0].get('namingContexts')]
415 creds = credopts.get_credentials(lp)
416 ctx = DCJoinContext(logger, creds=creds, lp=lp,
417 forced_local_samdb=samdb,
418 netbios_name=newservername)
419 ctx.nc_list = ncs
420 ctx.full_nc_list = ncs
421 ctx.userAccountControl = (samba.dsdb.UF_SERVER_TRUST_ACCOUNT |
422 samba.dsdb.UF_TRUSTED_FOR_DELEGATION)
424 # rewrite the smb.conf to make sure it uses the new targetdir settings.
425 # (This doesn't update all filepaths in a customized config, but it
426 # corrects the same paths that get set by a new provision)
427 logger.info('Updating basic smb.conf settings...')
428 make_smbconf(smbconf, newservername, ctx.domain_name,
429 ctx.realm, targetdir, lp=lp,
430 serverrole="active directory domain controller")
432 # Get the SID saved by the backup process and create account
433 res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
434 scope=ldb.SCOPE_BASE,
435 attrs=['sidForRestore', 'backupRename'])
436 is_rename = True if 'backupRename' in res[0] else False
437 sid = res[0].get('sidForRestore')[0]
438 logger.info('Creating account with SID: ' + str(sid))
439 ctx.join_add_objects(specified_sid=dom_sid(sid))
441 m = ldb.Message()
442 m.dn = ldb.Dn(samdb, '@ROOTDSE')
443 ntds_guid = str(ctx.ntds_guid)
444 m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % ntds_guid,
445 ldb.FLAG_MOD_REPLACE,
446 "dsServiceName")
447 samdb.modify(m)
449 # if we renamed the backed-up domain, then we need to add the DNS
450 # objects for the new realm (we do this in the restore, now that we
451 # know the new DC's IP address)
452 if is_rename:
453 self.register_dns_zone(logger, samdb, lp, ctx.ntds_guid,
454 host_ip, host_ip6)
456 secrets_path = os.path.join(private_dir, 'secrets.ldb')
457 secrets_ldb = Ldb(secrets_path, session_info=system_session(), lp=lp)
458 secretsdb_self_join(secrets_ldb, domain=ctx.domain_name,
459 realm=ctx.realm, dnsdomain=ctx.dnsdomain,
460 netbiosname=ctx.myname, domainsid=ctx.domsid,
461 machinepass=ctx.acct_pass,
462 key_version_number=ctx.key_version_number,
463 secure_channel_type=misc.SEC_CHAN_BDC)
465 # Seize DNS roles
466 domain_dn = samdb.domain_dn()
467 forest_dn = samba.dn_from_dns_name(samdb.forest_dns_name())
468 domaindns_dn = ("CN=Infrastructure,DC=DomainDnsZones,", domain_dn)
469 forestdns_dn = ("CN=Infrastructure,DC=ForestDnsZones,", forest_dn)
470 for dn_prefix, dns_dn in [forestdns_dn, domaindns_dn]:
471 if dns_dn not in ncs:
472 continue
473 full_dn = dn_prefix + dns_dn
474 m = ldb.Message()
475 m.dn = ldb.Dn(samdb, full_dn)
476 m["fSMORoleOwner"] = ldb.MessageElement(samdb.get_dsServiceName(),
477 ldb.FLAG_MOD_REPLACE,
478 "fSMORoleOwner")
479 samdb.modify(m)
481 # Seize other roles
482 for role in ['rid', 'pdc', 'naming', 'infrastructure', 'schema']:
483 self.seize_role(role, samdb, force=True)
485 # Get all DCs and remove them (this ensures these DCs cannot
486 # replicate because they will not have a password)
487 search_expr = "(&(objectClass=Server)(serverReference=*))"
488 res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
489 expression=search_expr)
490 for m in res:
491 cn = m.get('cn')[0]
492 if cn != newservername:
493 remove_dc(samdb, logger, cn)
495 # Remove the repsFrom and repsTo from each NC to ensure we do
496 # not try (and fail) to talk to the old DCs
497 for nc in ncs:
498 msg = ldb.Message()
499 msg.dn = ldb.Dn(samdb, nc)
501 msg["repsFrom"] = ldb.MessageElement([],
502 ldb.FLAG_MOD_REPLACE,
503 "repsFrom")
504 msg["repsTo"] = ldb.MessageElement([],
505 ldb.FLAG_MOD_REPLACE,
506 "repsTo")
507 samdb.modify(msg)
509 # Update the krbtgt passwords twice, ensuring no tickets from
510 # the old domain are valid
511 update_krbtgt_account_password(samdb)
512 update_krbtgt_account_password(samdb)
514 # restore the sysvol directory from the backup tar file, including the
515 # original NTACLs. Note that the backup_restore() will fail if not root
516 sysvol_tar = os.path.join(targetdir, 'sysvol.tar.gz')
517 dest_sysvol_dir = lp.get('path', 'sysvol')
518 if not os.path.exists(dest_sysvol_dir):
519 os.makedirs(dest_sysvol_dir)
520 backup_restore(sysvol_tar, dest_sysvol_dir, samdb, smbconf)
521 os.remove(sysvol_tar)
523 # fix up any stale links to the old DCs we just removed
524 logger.info("Fixing up any remaining references to the old DCs...")
525 self.fix_old_dc_references(samdb)
527 # Remove DB markers added by the backup process
528 m = ldb.Message()
529 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
530 m["backupDate"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
531 "backupDate")
532 m["sidForRestore"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
533 "sidForRestore")
534 if is_rename:
535 m["backupRename"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
536 "backupRename")
537 samdb.modify(m)
539 logger.info("Backup file successfully restored to %s" % targetdir)
540 logger.info("Please check the smb.conf settings are correct before "
541 "starting samba.")
544 class cmd_domain_backup_rename(samba.netcmd.Command):
545 '''Copy a running DC's DB to backup file, renaming the domain in the process.
547 Where <new-domain> is the new domain's NetBIOS name, and <new-dnsrealm> is
548 the new domain's realm in DNS form.
550 This is similar to 'samba-tool backup online' in that it clones the DB of a
551 running DC. However, this option also renames all the domain entries in the
552 DB. Renaming the domain makes it possible to restore and start a new Samba
553 DC without it interfering with the existing Samba domain. In other words,
554 you could use this option to clone your production samba domain and restore
555 it to a separate pre-production environment that won't overlap or interfere
556 with the existing production Samba domain.
558 Note that:
559 - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
560 and fix any errors it reports.
561 - all the domain's secrets are included in the backup file.
562 - although the DB contents can be untarred and examined manually, you need
563 to run 'samba-tool domain backup restore' before you can start a Samba DC
564 from the backup file.
565 - GPO and sysvol information will still refer to the old realm and will
566 need to be updated manually.
567 - if you specify 'keep-dns-realm', then the DNS records will need updating
568 in order to work (they will still refer to the old DC's IP instead of the
569 new DC's address).
570 - we recommend that you only use this option if you know what you're doing.
573 synopsis = ("%prog <new-domain> <new-dnsrealm> --server=<DC-to-backup> "
574 "--targetdir=<output-dir>")
575 takes_optiongroups = {
576 "sambaopts": options.SambaOptions,
577 "credopts": options.CredentialsOptions,
580 takes_options = [
581 Option("--server", help="The DC to backup", type=str),
582 Option("--targetdir", help="Directory to write the backup file",
583 type=str),
584 Option("--keep-dns-realm", action="store_true", default=False,
585 help="Retain the DNS entries for the old realm in the backup"),
586 Option("--no-secrets", action="store_true", default=False,
587 help="Exclude secret values from the backup created")
590 takes_args = ["new_domain_name", "new_dns_realm"]
592 def update_dns_root(self, logger, samdb, old_realm, delete_old_dns):
593 '''Updates dnsRoot for the partition objects to reflect the rename'''
595 # lookup the crossRef objects that hold the old realm's dnsRoot
596 partitions_dn = samdb.get_partitions_dn()
597 res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
598 attrs=["dnsRoot"],
599 expression='(&(objectClass=crossRef)(dnsRoot=*))')
600 new_realm = samdb.domain_dns_name()
602 # go through and add the new realm
603 for res_msg in res:
604 # dnsRoot can be multi-valued, so only look for the old realm
605 for dns_root in res_msg["dnsRoot"]:
606 dn = res_msg.dn
607 if old_realm in dns_root:
608 new_dns_root = re.sub('%s$' % old_realm, new_realm,
609 dns_root)
610 logger.info("Adding %s dnsRoot to %s" % (new_dns_root, dn))
612 m = ldb.Message()
613 m.dn = dn
614 m["dnsRoot"] = ldb.MessageElement(new_dns_root,
615 ldb.FLAG_MOD_ADD,
616 "dnsRoot")
617 samdb.modify(m)
619 # optionally remove the dnsRoot for the old realm
620 if delete_old_dns:
621 logger.info("Removing %s dnsRoot from %s" % (dns_root,
622 dn))
623 m["dnsRoot"] = ldb.MessageElement(dns_root,
624 ldb.FLAG_MOD_DELETE,
625 "dnsRoot")
626 samdb.modify(m)
628 # Updates the CN=<domain>,CN=Partitions,CN=Configuration,... object to
629 # reflect the domain rename
630 def rename_domain_partition(self, logger, samdb, new_netbios_name):
631 '''Renames the domain parition object and updates its nETBIOSName'''
633 # lookup the crossRef object that holds the nETBIOSName (nCName has
634 # already been updated by this point, but the netBIOS hasn't)
635 base_dn = samdb.get_default_basedn()
636 nc_name = ldb.binary_encode(str(base_dn))
637 partitions_dn = samdb.get_partitions_dn()
638 res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
639 attrs=["nETBIOSName"],
640 expression='ncName=%s' % nc_name)
642 logger.info("Changing backup domain's NetBIOS name to %s" %
643 new_netbios_name)
644 m = ldb.Message()
645 m.dn = res[0].dn
646 m["nETBIOSName"] = ldb.MessageElement(new_netbios_name,
647 ldb.FLAG_MOD_REPLACE,
648 "nETBIOSName")
649 samdb.modify(m)
651 # renames the object itself to reflect the change in domain
652 new_dn = "CN=%s,%s" % (new_netbios_name, partitions_dn)
653 logger.info("Renaming %s --> %s" % (res[0].dn, new_dn))
654 samdb.rename(res[0].dn, new_dn, controls=['relax:0'])
656 def delete_old_dns_zones(self, logger, samdb, old_realm):
657 # remove the top-level DNS entries for the old realm
658 basedn = samdb.get_default_basedn()
659 dn = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (old_realm, basedn)
660 logger.info("Deleting old DNS zone %s" % dn)
661 samdb.delete(dn, ["tree_delete:1"])
663 forestdn = samdb.get_root_basedn().get_linearized()
664 dn = "DC=_msdcs.%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (old_realm,
665 forestdn)
666 logger.info("Deleting old DNS zone %s" % dn)
667 samdb.delete(dn, ["tree_delete:1"])
669 def fix_old_dn_attributes(self, samdb):
670 '''Fixes attributes (i.e. objectCategory) that still use the old DN'''
672 samdb.transaction_start()
673 # Just fix any mismatches in DN detected (leave any other errors)
674 chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
675 in_transaction=True)
676 # fix up incorrect objectCategory/etc attributes
677 setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
678 cross_ncs_ctrl = 'search_options:1:2'
679 controls = ['show_deleted:1', cross_ncs_ctrl]
680 chk.check_database(controls=controls)
681 samdb.transaction_commit()
683 def run(self, new_domain_name, new_dns_realm, sambaopts=None,
684 credopts=None, server=None, targetdir=None, keep_dns_realm=False,
685 no_secrets=False):
686 logger = self.get_logger()
687 logger.setLevel(logging.INFO)
689 lp = sambaopts.get_loadparm()
690 creds = credopts.get_credentials(lp)
692 # Make sure we have all the required args.
693 if server is None:
694 raise CommandError('Server required')
696 check_targetdir(logger, targetdir)
698 delete_old_dns = not keep_dns_realm
700 new_dns_realm = new_dns_realm.lower()
701 new_domain_name = new_domain_name.upper()
703 new_base_dn = samba.dn_from_dns_name(new_dns_realm)
704 logger.info("New realm for backed up domain: %s" % new_dns_realm)
705 logger.info("New base DN for backed up domain: %s" % new_base_dn)
706 logger.info("New domain NetBIOS name: %s" % new_domain_name)
708 tmpdir = tempfile.mkdtemp(dir=targetdir)
710 # setup a join-context for cloning the remote server
711 include_secrets = not no_secrets
712 ctx = DCCloneAndRenameContext(new_base_dn, new_domain_name,
713 new_dns_realm, logger=logger,
714 creds=creds, lp=lp,
715 include_secrets=include_secrets,
716 dns_backend='SAMBA_INTERNAL',
717 server=server, targetdir=tmpdir)
719 # sanity-check we're not "renaming" the domain to the same values
720 old_domain = ctx.domain_name
721 if old_domain == new_domain_name:
722 shutil.rmtree(tmpdir)
723 raise CommandError("Cannot use the current domain NetBIOS name.")
725 old_realm = ctx.realm
726 if old_realm == new_dns_realm:
727 shutil.rmtree(tmpdir)
728 raise CommandError("Cannot use the current domain DNS realm.")
730 # do the clone/rename
731 ctx.do_join()
733 # get the paths used for the clone, then drop the old samdb connection
734 del ctx.local_samdb
735 paths = ctx.paths
737 # get a free RID to use as the new DC's SID (when it gets restored)
738 remote_sam = SamDB(url='ldap://' + server, credentials=creds,
739 session_info=system_session(), lp=lp)
740 new_sid = get_sid_for_restore(remote_sam)
742 # Grab the remote DC's sysvol files and bundle them into a tar file.
743 # Note we end up with 2 sysvol dirs - the original domain's files (that
744 # use the old realm) backed here, as well as default files generated
745 # for the new realm as part of the clone/join.
746 sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
747 smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
748 backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
750 # connect to the local DB (making sure we use the new/renamed config)
751 lp.load(paths.smbconf)
752 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
754 # Edit the cloned sam.ldb to mark it as a backup
755 time_str = get_timestamp()
756 add_backup_marker(samdb, "backupDate", time_str)
757 add_backup_marker(samdb, "sidForRestore", new_sid)
758 add_backup_marker(samdb, "backupRename", old_realm)
760 # fix up the DNS objects that are using the old dnsRoot value
761 self.update_dns_root(logger, samdb, old_realm, delete_old_dns)
763 # update the netBIOS name and the Partition object for the domain
764 self.rename_domain_partition(logger, samdb, new_domain_name)
766 if delete_old_dns:
767 self.delete_old_dns_zones(logger, samdb, old_realm)
769 logger.info("Fixing DN attributes after rename...")
770 self.fix_old_dn_attributes(samdb)
772 # ensure the admin user always has a password set (same as provision)
773 if no_secrets:
774 set_admin_password(logger, samdb)
776 # Add everything in the tmpdir to the backup tar file
777 backup_file = backup_filepath(targetdir, new_dns_realm, time_str)
778 create_log_file(tmpdir, lp, "rename", server, include_secrets,
779 "Original domain %s (NetBIOS), %s (DNS realm)" %
780 (old_domain, old_realm))
781 create_backup_tar(logger, tmpdir, backup_file)
783 shutil.rmtree(tmpdir)
786 class cmd_domain_backup_offline(samba.netcmd.Command):
787 '''Backup the local domain directories safely into a tar file.
789 Takes a backup copy of the current domain from the local files on disk,
790 with proper locking of the DB to ensure consistency. If the domain were to
791 undergo a catastrophic failure, then the backup file can be used to recover
792 the domain.
794 An offline backup differs to an online backup in the following ways:
795 - a backup can be created even if the DC isn't currently running.
796 - includes non-replicated attributes that an online backup wouldn't store.
797 - takes a copy of the raw database files, which has the risk that any
798 hidden problems in the DB are preserved in the backup.'''
800 synopsis = "%prog [options]"
801 takes_optiongroups = {
802 "sambaopts": options.SambaOptions,
805 takes_options = [
806 Option("--targetdir",
807 help="Output directory (required)",
808 type=str),
811 backup_ext = '.bak-offline'
813 def offline_tdb_copy(self, path):
814 backup_path = path + self.backup_ext
815 try:
816 tdb_copy(path, backup_path, readonly=True)
817 except CalledProcessError as copy_err:
818 # If the copy didn't work, check if it was caused by an EINVAL
819 # error on opening the DB. If so, it's a mutex locked database,
820 # which we can safely ignore.
821 try:
822 tdb.open(path)
823 except Exception as e:
824 if hasattr(e, 'errno') and e.errno == errno.EINVAL:
825 return
826 raise e
827 raise copy_err
828 if not os.path.exists(backup_path):
829 s = "tdbbackup said backup succeeded but {} not found"
830 raise CommandError(s.format(backup_path))
832 def offline_mdb_copy(self, path):
833 mdb_copy(path, path + self.backup_ext)
835 # Secrets databases are a special case: a transaction must be started
836 # on the secrets.ldb file before backing up that file and secrets.tdb
837 def backup_secrets(self, private_dir, lp, logger):
838 secrets_path = os.path.join(private_dir, 'secrets')
839 secrets_obj = Ldb(secrets_path + '.ldb', lp=lp)
840 logger.info('Starting transaction on ' + secrets_path)
841 secrets_obj.transaction_start()
842 self.offline_tdb_copy(secrets_path + '.ldb')
843 self.offline_tdb_copy(secrets_path + '.tdb')
844 secrets_obj.transaction_cancel()
846 # sam.ldb must have a transaction started on it before backing up
847 # everything in sam.ldb.d with the appropriate backup function.
848 def backup_smb_dbs(self, private_dir, samdb, lp, logger):
849 # First, determine if DB backend is MDB. Assume not unless there is a
850 # 'backendStore' attribute on @PARTITION containing the text 'mdb'
851 store_label = "backendStore"
852 res = samdb.search(base="@PARTITION", scope=ldb.SCOPE_BASE,
853 attrs=[store_label])
854 mdb_backend = store_label in res[0] and res[0][store_label][0] == 'mdb'
856 sam_ldb_path = os.path.join(private_dir, 'sam.ldb')
857 copy_function = None
858 if mdb_backend:
859 logger.info('MDB backend detected. Using mdb backup function.')
860 copy_function = self.offline_mdb_copy
861 else:
862 logger.info('Starting transaction on ' + sam_ldb_path)
863 copy_function = self.offline_tdb_copy
864 sam_obj = Ldb(sam_ldb_path, lp=lp)
865 sam_obj.transaction_start()
867 logger.info(' backing up ' + sam_ldb_path)
868 self.offline_tdb_copy(sam_ldb_path)
869 sam_ldb_d = sam_ldb_path + '.d'
870 for sam_file in os.listdir(sam_ldb_d):
871 sam_file = os.path.join(sam_ldb_d, sam_file)
872 if sam_file.endswith('.ldb'):
873 logger.info(' backing up locked/related file ' + sam_file)
874 copy_function(sam_file)
875 else:
876 logger.info(' copying locked/related file ' + sam_file)
877 shutil.copyfile(sam_file, sam_file + self.backup_ext)
879 if not mdb_backend:
880 sam_obj.transaction_cancel()
882 # Find where a path should go in the fixed backup archive structure.
883 def get_arc_path(self, path, conf_paths):
884 backup_dirs = {"private": conf_paths.private_dir,
885 "statedir": conf_paths.state_dir,
886 "etc": os.path.dirname(conf_paths.smbconf)}
887 matching_dirs = [(_, p) for (_, p) in backup_dirs.items() if
888 path.startswith(p)]
889 arc_path, fs_path = matching_dirs[0]
891 # If more than one directory is a parent of this path, then at least
892 # one configured path is a subdir of another. Use closest match.
893 if len(matching_dirs) > 1:
894 arc_path, fs_path = max(matching_dirs, key=lambda p: len(p[1]))
895 arc_path += path[len(fs_path):]
897 return arc_path
899 def run(self, sambaopts=None, targetdir=None):
901 logger = logging.getLogger()
902 logger.setLevel(logging.DEBUG)
903 logger.addHandler(logging.StreamHandler(sys.stdout))
905 # Get the absolute paths of all the directories we're going to backup
906 lp = sambaopts.get_loadparm()
908 paths = samba.provision.provision_paths_from_lp(lp, lp.get('realm'))
909 if not (paths.samdb and os.path.exists(paths.samdb)):
910 raise CommandError('No sam.db found. This backup ' +
911 'tool is only for AD DCs')
913 check_targetdir(logger, targetdir)
915 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
916 sid = get_sid_for_restore(samdb)
918 backup_dirs = [paths.private_dir, paths.state_dir,
919 os.path.dirname(paths.smbconf)] # etc dir
920 logger.info('running backup on dirs: {}'.format(backup_dirs))
922 # Recursively get all file paths in the backup directories
923 all_files = []
924 for backup_dir in backup_dirs:
925 for (working_dir, _, filenames) in os.walk(backup_dir):
926 if working_dir.startswith(paths.sysvol):
927 continue
929 for filename in filenames:
930 if filename in all_files:
931 continue
933 # Assume existing backup files are from a previous backup.
934 # Delete and ignore.
935 if filename.endswith(self.backup_ext):
936 os.remove(os.path.join(working_dir, filename))
937 continue
938 all_files.append(os.path.join(working_dir, filename))
940 # Backup secrets, sam.ldb and their downstream files
941 self.backup_secrets(paths.private_dir, lp, logger)
942 self.backup_smb_dbs(paths.private_dir, samdb, lp, logger)
944 # Open the new backed up samdb, flag it as backed up, and write
945 # the next SID so the restore tool can add objects.
946 # WARNING: Don't change this code unless you know what you're doing.
947 # Writing to a .bak file only works because the DN being
948 # written to happens to be top level.
949 samdb = SamDB(url=paths.samdb + self.backup_ext,
950 session_info=system_session(), lp=lp)
951 time_str = get_timestamp()
952 add_backup_marker(samdb, "backupDate", time_str)
953 add_backup_marker(samdb, "sidForRestore", sid)
955 # Now handle all the LDB and TDB files that are not linked to
956 # anything else. Use transactions for LDBs.
957 for path in all_files:
958 if not os.path.exists(path + self.backup_ext):
959 if path.endswith('.ldb'):
960 logger.info('Starting transaction on solo db: ' + path)
961 ldb_obj = Ldb(path, lp=lp)
962 ldb_obj.transaction_start()
963 logger.info(' running tdbbackup on the same file')
964 self.offline_tdb_copy(path)
965 ldb_obj.transaction_cancel()
966 elif path.endswith('.tdb'):
967 logger.info('running tdbbackup on lone tdb file ' + path)
968 self.offline_tdb_copy(path)
970 # Now make the backup tar file and add all
971 # backed up files and any other files to it.
972 temp_tar_dir = tempfile.mkdtemp(dir=targetdir,
973 prefix='INCOMPLETEsambabackupfile')
974 temp_tar_name = os.path.join(temp_tar_dir, "samba-backup.tar.bz2")
975 tar = tarfile.open(temp_tar_name, 'w:bz2')
977 logger.info('running offline ntacl backup of sysvol')
978 sysvol_tar_fn = 'sysvol.tar.gz'
979 sysvol_tar = os.path.join(temp_tar_dir, sysvol_tar_fn)
980 backup_offline(paths.sysvol, sysvol_tar, samdb, paths.smbconf)
981 tar.add(sysvol_tar, sysvol_tar_fn)
982 os.remove(sysvol_tar)
984 create_log_file(temp_tar_dir, lp, "offline", "localhost", True)
985 backup_fn = os.path.join(temp_tar_dir, "backup.txt")
986 tar.add(backup_fn, os.path.basename(backup_fn))
987 os.remove(backup_fn)
989 logger.info('building backup tar')
990 for path in all_files:
991 arc_path = self.get_arc_path(path, paths)
993 if os.path.exists(path + self.backup_ext):
994 logger.info(' adding backup ' + arc_path + self.backup_ext +
995 ' to tar and deleting file')
996 tar.add(path + self.backup_ext, arcname=arc_path)
997 os.remove(path + self.backup_ext)
998 elif path.endswith('.ldb') or path.endswith('.tdb'):
999 logger.info(' skipping ' + arc_path)
1000 else:
1001 logger.info(' adding misc file ' + arc_path)
1002 tar.add(path, arcname=arc_path)
1004 tar.close()
1005 os.rename(temp_tar_name, os.path.join(targetdir,
1006 'samba-backup-{}.tar.bz2'.format(time_str)))
1007 os.rmdir(temp_tar_dir)
1008 logger.info('Backup succeeded.')
1011 class cmd_domain_backup(samba.netcmd.SuperCommand):
1012 '''Create or restore a backup of the domain.'''
1013 subcommands = {'offline': cmd_domain_backup_offline(),
1014 'online': cmd_domain_backup_online(),
1015 'rename': cmd_domain_backup_rename(),
1016 'restore': cmd_domain_backup_restore()}