smbd: Fix cached dos attributes
[Samba.git] / python / samba / netcmd / domain / backup.py
blobb27105116dccf2a68119b87f455d81e34574bef7
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 logging
22 import shutil
23 import tempfile
24 import samba
25 import tdb
26 import samba.getopt as options
27 from samba.samdb import SamDB, get_default_backend_store
28 import ldb
29 from ldb import LdbError
30 from samba.samba3 import libsmb_samba_internal as libsmb
31 from samba.samba3 import param as s3param
32 from samba.ntacls import backup_online, backup_restore, backup_offline
33 from samba.auth import system_session
34 from samba.join import DCJoinContext, join_clone, DCCloneAndRenameContext
35 from samba.dcerpc.security import dom_sid
36 from samba.netcmd import Option, CommandError
37 from samba.dcerpc import misc, security, drsblobs
38 from samba import Ldb
39 from samba.netcmd.fsmo import cmd_fsmo_seize
40 from samba.provision import make_smbconf, DEFAULTSITE
41 from samba.upgradehelpers import update_krbtgt_account_password
42 from samba.remove_dc import remove_dc
43 from samba.provision import secretsdb_self_join
44 from samba.dbchecker import dbcheck
45 import re
46 from samba.provision import guess_names, determine_host_ip, determine_host_ip6
47 from samba.provision.sambadns import (fill_dns_data_partitions,
48 get_dnsadmins_sid,
49 get_domainguid)
50 from samba.tdb_util import tdb_copy
51 from samba.mdb_util import mdb_copy
52 import errno
53 from subprocess import CalledProcessError
54 from samba import sites
55 from samba.dsdb import _dsdb_load_udv_v2
56 from samba.ndr import ndr_pack
57 from samba.credentials import SMB_SIGNING_REQUIRED
58 from samba import safe_tarfile as tarfile
59 import hashlib
62 # work out a SID (based on a free RID) to use when the domain gets restored.
63 # This ensures that the restored DC's SID won't clash with any other RIDs
64 # already in use in the domain
65 def get_sid_for_restore(samdb, logger):
66 # Allocate a new RID without modifying the database. This should be safe,
67 # because we acquire the RID master role after creating an account using
68 # this RID during the restore process. Acquiring the RID master role
69 # creates a new RID pool which we will fetch RIDs from, so we shouldn't get
70 # duplicates.
71 try:
72 rid = samdb.next_free_rid()
73 except LdbError as err:
74 logger.info("A SID could not be allocated for restoring the domain. "
75 "Either no RID Set was found on this DC, "
76 "or the RID Set was not usable.")
77 logger.info("To initialise this DC's RID pools, obtain a RID Set from "
78 "this domain's RID master, or run samba-tool dbcheck "
79 "to fix the existing RID Set.")
80 raise CommandError("Cannot create backup", err)
82 # Construct full SID
83 sid = dom_sid(samdb.get_domain_sid())
84 sid_for_restore = str(sid) + '-' + str(rid)
86 # Confirm the SID is not already in use
87 try:
88 res = samdb.search(scope=ldb.SCOPE_BASE,
89 base='<SID=%s>' % sid_for_restore,
90 attrs=[],
91 controls=['show_deleted:1',
92 'show_recycled:1'])
93 if len(res) != 1:
94 # This case makes no sense, but neither does a corrupt RID set
95 raise CommandError("Cannot create backup - "
96 "this DC's RID pool is corrupt, "
97 "the next SID (%s) appears to be in use." %
98 sid_for_restore)
99 raise CommandError("Cannot create backup - "
100 "this DC's RID pool is corrupt, "
101 "the next SID %s points to existing object %s. "
102 "Please run samba-tool dbcheck on the source DC." %
103 (sid_for_restore, res[0].dn))
104 except ldb.LdbError as e:
105 (enum, emsg) = e.args
106 if enum != ldb.ERR_NO_SUCH_OBJECT:
107 # We want NO_SUCH_OBJECT, anything else is a serious issue
108 raise
110 return str(sid) + '-' + str(rid)
113 def smb_sysvol_conn(server, lp, creds):
114 """Returns an SMB connection to the sysvol share on the DC"""
115 # the SMB bindings rely on having a s3 loadparm
116 s3_lp = s3param.get_context()
117 s3_lp.load(lp.configfile)
119 # Force signing for the connection
120 saved_signing_state = creds.get_smb_signing()
121 creds.set_smb_signing(SMB_SIGNING_REQUIRED)
122 conn = libsmb.Conn(server, "sysvol", lp=s3_lp, creds=creds)
123 # Reset signing state
124 creds.set_smb_signing(saved_signing_state)
125 return conn
128 def get_timestamp():
129 return datetime.datetime.now().isoformat().replace(':', '-')
132 def backup_filepath(targetdir, name, time_str):
133 filename = 'samba-backup-%s-%s.tar.bz2' % (name, time_str)
134 return os.path.join(targetdir, filename)
137 def create_sha256sum(filename):
138 hash = hashlib.new('sha256')
139 with open(filename, "rb") as f:
140 for chunk in iter(lambda: f.read(65536), b""):
141 hash.update(chunk)
142 return hash.hexdigest()
145 def create_backup_tar(logger, tmpdir, backup_filepath):
146 # Adds everything in the tmpdir into a new tar file
147 logger.info("Creating backup file %s..." % backup_filepath)
148 tf = tarfile.open(backup_filepath, 'w:bz2')
149 tf.add(tmpdir, arcname='./')
150 tf.close()
153 def create_log_file(targetdir, lp, backup_type, server, include_secrets,
154 extra_info=None):
155 # create a summary file about the backup, which will get included in the
156 # tar file. This makes it easy for users to see what the backup involved,
157 # without having to untar the DB and interrogate it
158 f = open(os.path.join(targetdir, "backup.txt"), 'w')
159 try:
160 time_str = datetime.datetime.now().strftime('%Y-%b-%d %H:%M:%S')
161 f.write("Backup created %s\n" % time_str)
162 f.write("Using samba-tool version: %s\n" % lp.get('server string'))
163 f.write("Domain %s backup, using DC '%s'\n" % (backup_type, server))
164 f.write("Backup for domain %s (NetBIOS), %s (DNS realm)\n" %
165 (lp.get('workgroup'), lp.get('realm').lower()))
166 f.write("Backup contains domain secrets: %s\n" % str(include_secrets))
167 if extra_info:
168 f.write("%s\n" % extra_info)
169 finally:
170 f.close()
173 # Add a backup-specific marker to the DB with info that we'll use during
174 # the restore process
175 def add_backup_marker(samdb, marker, value):
176 m = ldb.Message()
177 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
178 m[marker] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, marker)
179 samdb.modify(m)
182 def check_targetdir(logger, targetdir):
183 if targetdir is None:
184 raise CommandError('Target directory required')
186 if not os.path.exists(targetdir):
187 logger.info('Creating targetdir %s...' % targetdir)
188 os.makedirs(targetdir)
189 elif not os.path.isdir(targetdir):
190 raise CommandError("%s is not a directory" % targetdir)
193 # For '--no-secrets' backups, this sets the Administrator user's password to a
194 # randomly-generated value. This is similar to the provision behaviour
195 def set_admin_password(logger, samdb):
196 """Sets a randomly generated password for the backup DB's admin user"""
198 # match the admin user by RID
199 domainsid = samdb.get_domain_sid()
200 match_admin = "(objectsid=%s-%s)" % (domainsid,
201 security.DOMAIN_RID_ADMINISTRATOR)
202 search_expr = "(&(objectClass=user)%s)" % (match_admin,)
204 # retrieve the admin username (just in case it's been renamed)
205 res = samdb.search(base=samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
206 expression=search_expr)
207 username = str(res[0]['samaccountname'])
209 adminpass = samba.generate_random_password(12, 32)
210 logger.info("Setting %s password in backup to: %s" % (username, adminpass))
211 logger.info("Run 'samba-tool user setpassword %s' after restoring DB" %
212 username)
213 samdb.setpassword(search_expr, adminpass, force_change_at_next_login=False,
214 username=username)
217 class cmd_domain_backup_online(samba.netcmd.Command):
218 """Copy a running DC's current DB into a backup tar file.
220 Takes a backup copy of the current domain from a running DC. If the domain
221 were to undergo a catastrophic failure, then the backup file can be used to
222 recover the domain. The backup created is similar to the DB that a new DC
223 would receive when it joins the domain.
225 Note that:
226 - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
227 and fix any errors it reports.
228 - all the domain's secrets are included in the backup file.
229 - although the DB contents can be untarred and examined manually, you need
230 to run 'samba-tool domain backup restore' before you can start a Samba DC
231 from the backup file."""
233 synopsis = "%prog --server=<DC-to-backup> --targetdir=<output-dir>"
234 takes_optiongroups = {
235 "sambaopts": options.SambaOptions,
236 "credopts": options.CredentialsOptions,
239 takes_options = [
240 Option("--server", help="The DC to backup", type=str),
241 Option("--targetdir", type=str,
242 help="Directory to write the backup file to"),
243 Option("--no-secrets", action="store_true", default=False,
244 help="Exclude secret values from the backup created"),
245 Option("--backend-store", type="choice", metavar="BACKENDSTORE",
246 choices=["tdb", "mdb"],
247 help="Specify the database backend to be used "
248 "(default is %s)" % get_default_backend_store()),
251 def run(self, sambaopts=None, credopts=None, server=None, targetdir=None,
252 no_secrets=False, backend_store=None):
253 logger = self.get_logger()
254 logger.setLevel(logging.DEBUG)
256 lp = sambaopts.get_loadparm()
257 creds = credopts.get_credentials(lp)
259 # Make sure we have all the required args.
260 if server is None:
261 raise CommandError('Server required')
263 check_targetdir(logger, targetdir)
265 tmpdir = tempfile.mkdtemp(dir=targetdir)
267 # Run a clone join on the remote
268 include_secrets = not no_secrets
269 try:
270 ctx = join_clone(logger=logger, creds=creds, lp=lp,
271 include_secrets=include_secrets, server=server,
272 dns_backend='SAMBA_INTERNAL', targetdir=tmpdir,
273 backend_store=backend_store)
275 # get the paths used for the clone, then drop the old samdb connection
276 paths = ctx.paths
277 del ctx
279 # Get a free RID to use as the new DC's SID (when it gets restored)
280 remote_sam = SamDB(url='ldap://' + server, credentials=creds,
281 session_info=system_session(), lp=lp)
282 new_sid = get_sid_for_restore(remote_sam, logger)
283 realm = remote_sam.domain_dns_name()
285 # Grab the remote DC's sysvol files and bundle them into a tar file
286 logger.info("Backing up sysvol files (via SMB)...")
287 sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
288 smb_conn = smb_sysvol_conn(server, lp, creds)
289 backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
291 # remove the default sysvol files created by the clone (we want to
292 # make sure we restore the sysvol.tar.gz files instead)
293 shutil.rmtree(paths.sysvol)
295 # Edit the downloaded sam.ldb to mark it as a backup
296 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp,
297 flags=ldb.FLG_DONT_CREATE_DB)
298 time_str = get_timestamp()
299 add_backup_marker(samdb, "backupDate", time_str)
300 add_backup_marker(samdb, "sidForRestore", new_sid)
301 add_backup_marker(samdb, "backupType", "online")
303 # ensure the admin user always has a password set (same as provision)
304 if no_secrets:
305 set_admin_password(logger, samdb)
307 # Add everything in the tmpdir to the backup tar file
308 backup_file = backup_filepath(targetdir, realm, time_str)
309 create_log_file(tmpdir, lp, "online", server, include_secrets)
310 create_backup_tar(logger, tmpdir, backup_file)
311 finally:
312 shutil.rmtree(tmpdir)
315 class cmd_domain_backup_restore(cmd_fsmo_seize):
316 """Restore the domain's DB from a backup-file.
318 This restores a previously backed up copy of the domain's DB on a new DC.
320 Note that the restored DB will not contain the original DC that the backup
321 was taken from (or any other DCs in the original domain). Only the new DC
322 (specified by --newservername) will be present in the restored DB.
324 Samba can then be started against the restored DB. Any existing DCs for the
325 domain should be shutdown before the new DC is started. Other DCs can then
326 be joined to the new DC to recover the network.
328 Note that this command should be run as the root user - it will fail
329 otherwise."""
331 synopsis = ("%prog --backup-file=<tar-file> --targetdir=<output-dir> "
332 "--newservername=<DC-name>")
333 takes_options = [
334 Option("--backup-file", help="Path to backup file", type=str),
335 Option("--targetdir", help="Path to write to", type=str),
336 Option("--newservername", help="Name for new server", type=str),
337 Option("--host-ip", type="string", metavar="IPADDRESS",
338 help="set IPv4 ipaddress"),
339 Option("--host-ip6", type="string", metavar="IP6ADDRESS",
340 help="set IPv6 ipaddress"),
341 Option("--site", help="Site to add the new server in", type=str),
344 takes_optiongroups = {
345 "sambaopts": options.SambaOptions,
346 "credopts": options.CredentialsOptions,
349 def register_dns_zone(self, logger, samdb, lp, ntdsguid, host_ip,
350 host_ip6, site):
352 Registers the new realm's DNS objects when a renamed domain backup
353 is restored.
355 names = guess_names(lp)
356 domaindn = names.domaindn
357 forestdn = samdb.get_root_basedn().get_linearized()
358 dnsdomain = names.dnsdomain.lower()
359 dnsforest = dnsdomain
360 hostname = names.netbiosname.lower()
361 domainsid = dom_sid(samdb.get_domain_sid())
362 dnsadmins_sid = get_dnsadmins_sid(samdb, domaindn)
363 domainguid = get_domainguid(samdb, domaindn)
365 # work out the IP address to use for the new DC's DNS records
366 host_ip = determine_host_ip(logger, lp, host_ip)
367 host_ip6 = determine_host_ip6(logger, lp, host_ip6)
369 if host_ip is None and host_ip6 is None:
370 raise CommandError('Please specify a host-ip for the new server')
372 logger.info("DNS realm was renamed to %s" % dnsdomain)
373 logger.info("Populating DNS partitions for new realm...")
375 # Add the DNS objects for the new realm (note: the backup clone already
376 # has the root server objects, so don't add them again)
377 fill_dns_data_partitions(samdb, domainsid, site, domaindn,
378 forestdn, dnsdomain, dnsforest, hostname,
379 host_ip, host_ip6, domainguid, ntdsguid,
380 dnsadmins_sid, add_root=False)
382 def fix_old_dc_references(self, samdb):
383 """Fixes attributes that reference the old/removed DCs"""
385 # we just want to fix up DB problems here that were introduced by us
386 # removing the old DCs. We restrict what we fix up so that the restored
387 # DB matches the backed-up DB as close as possible. (There may be other
388 # DB issues inherited from the backed-up DC, but it's not our place to
389 # silently try to fix them here).
390 samdb.transaction_start()
391 chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
392 in_transaction=True)
394 # fix up stale references to the old DC
395 setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
396 attrs = ['lastKnownParent', 'interSiteTopologyGenerator']
398 # fix-up stale one-way links that point to the old DC
399 setattr(chk, 'remove_plausible_deleted_DN_links', 'ALL')
400 attrs += ['msDS-NC-Replica-Locations']
402 cross_ncs_ctrl = 'search_options:1:2'
403 controls = ['show_deleted:1', cross_ncs_ctrl]
404 chk.check_database(controls=controls, attrs=attrs)
405 samdb.transaction_commit()
407 def create_default_site(self, samdb, logger):
408 """Creates the default site, if it doesn't already exist"""
410 sitename = DEFAULTSITE
411 search_expr = "(&(cn={0})(objectclass=site))".format(sitename)
412 res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
413 expression=search_expr)
415 if len(res) == 0:
416 logger.info("Creating default site '{0}'".format(sitename))
417 sites.create_site(samdb, samdb.get_config_basedn(), sitename)
419 return sitename
421 def remove_backup_markers(self, samdb):
422 """Remove DB markers added by the backup process"""
424 # check what markers we need to remove (this may vary)
425 markers = ['sidForRestore', 'backupRename', 'backupDate', 'backupType']
426 res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
427 scope=ldb.SCOPE_BASE,
428 attrs=markers)
430 # remove any markers that exist in the DB
431 m = ldb.Message()
432 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
434 for attr in markers:
435 if attr in res[0]:
436 m[attr] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, attr)
438 samdb.modify(m)
440 def get_backup_type(self, samdb):
441 res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
442 scope=ldb.SCOPE_BASE,
443 attrs=['backupRename', 'backupType'])
445 # note that the backupType marker won't exist on backups created on
446 # v4.9. However, we can still infer the type, as only rename and
447 # online backups are supported on v4.9
448 if 'backupType' in res[0]:
449 backup_type = str(res[0]['backupType'])
450 elif 'backupRename' in res[0]:
451 backup_type = "rename"
452 else:
453 backup_type = "online"
455 return backup_type
457 def save_uptodate_vectors(self, samdb, partitions):
458 """Ensures the UTDV used by DRS is correct after an offline backup"""
459 for nc in partitions:
460 # load the replUpToDateVector we *should* have
461 utdv = _dsdb_load_udv_v2(samdb, nc)
463 # convert it to NDR format and write it into the DB
464 utdv_blob = drsblobs.replUpToDateVectorBlob()
465 utdv_blob.version = 2
466 utdv_blob.ctr.cursors = utdv
467 utdv_blob.ctr.count = len(utdv)
468 new_value = ndr_pack(utdv_blob)
470 m = ldb.Message()
471 m.dn = ldb.Dn(samdb, nc)
472 m["replUpToDateVector"] = ldb.MessageElement(new_value,
473 ldb.FLAG_MOD_REPLACE,
474 "replUpToDateVector")
475 samdb.modify(m)
477 def run(self, sambaopts=None, credopts=None, backup_file=None,
478 targetdir=None, newservername=None, host_ip=None, host_ip6=None,
479 site=None):
480 if not (backup_file and os.path.exists(backup_file)):
481 raise CommandError('Backup file not found.')
482 if targetdir is None:
483 raise CommandError('Please specify a target directory')
484 # allow restoredc to install into a directory prepopulated by selftest
485 if (os.path.exists(targetdir) and os.listdir(targetdir) and
486 os.environ.get('SAMBA_SELFTEST') != '1'):
487 raise CommandError('Target directory is not empty')
488 if not newservername:
489 raise CommandError('Server name required')
491 logger = logging.getLogger()
492 logger.setLevel(logging.DEBUG)
493 logger.addHandler(logging.StreamHandler(sys.stdout))
495 # ldapcmp prefers the server's netBIOS name in upper-case
496 newservername = newservername.upper()
498 # extract the backup .tar to a temp directory
499 targetdir = os.path.abspath(targetdir)
500 tf = tarfile.open(backup_file)
501 tf.extractall(targetdir)
502 tf.close()
504 # use the smb.conf that got backed up, by default (save what was
505 # actually backed up, before we mess with it)
506 smbconf = os.path.join(targetdir, 'etc', 'smb.conf')
507 shutil.copyfile(smbconf, smbconf + ".orig")
509 # if a smb.conf was specified on the cmd line, then use that instead
510 cli_smbconf = sambaopts.get_loadparm_path()
511 if cli_smbconf:
512 logger.info("Using %s as restored domain's smb.conf" % cli_smbconf)
513 shutil.copyfile(cli_smbconf, smbconf)
515 lp = samba.param.LoadParm()
516 lp.load(smbconf)
518 # open a DB connection to the restored DB
519 private_dir = os.path.join(targetdir, 'private')
520 samdb_path = os.path.join(private_dir, 'sam.ldb')
521 samdb = SamDB(url=samdb_path, session_info=system_session(), lp=lp,
522 flags=ldb.FLG_DONT_CREATE_DB)
523 backup_type = self.get_backup_type(samdb)
525 if site is None:
526 # There's no great way to work out the correct site to add the
527 # restored DC to. By default, add it to Default-First-Site-Name,
528 # creating the site if it doesn't already exist
529 site = self.create_default_site(samdb, logger)
530 logger.info("Adding new DC to site '{0}'".format(site))
532 # read the naming contexts out of the DB
533 res = samdb.search(base="", scope=ldb.SCOPE_BASE,
534 attrs=['namingContexts'])
535 ncs = [str(r) for r in res[0].get('namingContexts')]
537 # for offline backups we need to make sure the upToDateness info
538 # contains the invocation-ID and highest-USN of the DC we backed up.
539 # Otherwise replication propagation dampening won't correctly filter
540 # objects created by that DC
541 if backup_type == "offline":
542 self.save_uptodate_vectors(samdb, ncs)
544 # Create account using the join_add_objects function in the join object
545 # We need namingContexts, account control flags, and the sid saved by
546 # the backup process.
547 creds = credopts.get_credentials(lp)
548 ctx = DCJoinContext(logger, creds=creds, lp=lp, site=site,
549 forced_local_samdb=samdb,
550 netbios_name=newservername)
551 ctx.nc_list = ncs
552 ctx.full_nc_list = ncs
553 ctx.userAccountControl = (samba.dsdb.UF_SERVER_TRUST_ACCOUNT |
554 samba.dsdb.UF_TRUSTED_FOR_DELEGATION)
556 # rewrite the smb.conf to make sure it uses the new targetdir settings.
557 # (This doesn't update all filepaths in a customized config, but it
558 # corrects the same paths that get set by a new provision)
559 logger.info('Updating basic smb.conf settings...')
560 make_smbconf(smbconf, newservername, ctx.domain_name,
561 ctx.realm, targetdir, lp=lp,
562 serverrole="active directory domain controller")
564 # Get the SID saved by the backup process and create account
565 res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
566 scope=ldb.SCOPE_BASE,
567 attrs=['sidForRestore'])
568 sid = res[0].get('sidForRestore')[0]
569 logger.info('Creating account with SID: ' + str(sid))
570 try:
571 ctx.join_add_objects(specified_sid=dom_sid(str(sid)))
572 except LdbError as e:
573 (enum, emsg) = e.args
574 if enum != ldb.ERR_CONSTRAINT_VIOLATION:
575 raise
577 dup_res = []
578 try:
579 dup_res = samdb.search(base=ldb.Dn(samdb, "<SID=%s>" % sid),
580 scope=ldb.SCOPE_BASE,
581 attrs=['objectGUID'],
582 controls=["show_deleted:0",
583 "show_recycled:0"])
584 except LdbError as dup_e:
585 (dup_enum, _) = dup_e.args
586 if dup_enum != ldb.ERR_NO_SUCH_OBJECT:
587 raise
589 if (len(dup_res) != 1):
590 raise
592 objectguid = samdb.schema_format_value("objectGUID",
593 dup_res[0]["objectGUID"][0])
594 objectguid = objectguid.decode('utf-8')
595 logger.error("The RID Pool on the source DC for the backup in %s "
596 "may be corrupt "
597 "or in conflict with SIDs already allocated "
598 "in the domain. " % backup_file)
599 logger.error("Running 'samba-tool dbcheck' on the source "
600 "DC (and obtaining a new backup) may correct the issue.")
601 logger.error("Alternatively please obtain a new backup "
602 "against a different DC.")
603 logger.error("The SID we wish to use (%s) is recorded in "
604 "@SAMBA_DSDB as the sidForRestore attribute."
605 % sid)
607 raise CommandError("Domain restore failed because there "
608 "is already an existing object (%s) "
609 "with SID %s and objectGUID %s. "
610 "This conflicts with "
611 "the new DC account we want to add "
612 "for the restored domain. " % (
613 dup_res[0].dn, sid, objectguid))
615 m = ldb.Message()
616 m.dn = ldb.Dn(samdb, '@ROOTDSE')
617 ntds_guid = str(ctx.ntds_guid)
618 m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % ntds_guid,
619 ldb.FLAG_MOD_REPLACE,
620 "dsServiceName")
621 samdb.modify(m)
623 # if we renamed the backed-up domain, then we need to add the DNS
624 # objects for the new realm (we do this in the restore, now that we
625 # know the new DC's IP address)
626 if backup_type == "rename":
627 self.register_dns_zone(logger, samdb, lp, ctx.ntds_guid,
628 host_ip, host_ip6, site)
630 secrets_path = os.path.join(private_dir, 'secrets.ldb')
631 secrets_ldb = Ldb(secrets_path, session_info=system_session(), lp=lp,
632 flags=ldb.FLG_DONT_CREATE_DB)
633 secretsdb_self_join(secrets_ldb, domain=ctx.domain_name,
634 realm=ctx.realm, dnsdomain=ctx.dnsdomain,
635 netbiosname=ctx.myname, domainsid=ctx.domsid,
636 machinepass=ctx.acct_pass,
637 key_version_number=ctx.key_version_number,
638 secure_channel_type=misc.SEC_CHAN_BDC)
640 # Seize DNS roles
641 domain_dn = samdb.domain_dn()
642 forest_dn = samba.dn_from_dns_name(samdb.forest_dns_name())
643 dns_roles = [("domaindns", domain_dn),
644 ("forestdns", forest_dn)]
645 for role, dn in dns_roles:
646 if dn in ncs:
647 self.seize_dns_role(role, samdb, None, None, None, force=True)
649 # Seize other roles
650 for role in ['rid', 'pdc', 'naming', 'infrastructure', 'schema']:
651 self.seize_role(role, samdb, force=True)
653 # Get all DCs and remove them (this ensures these DCs cannot
654 # replicate because they will not have a password)
655 search_expr = "(&(objectClass=Server)(serverReference=*))"
656 res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
657 expression=search_expr)
658 for m in res:
659 cn = str(m.get('cn')[0])
660 if cn != newservername:
661 remove_dc(samdb, logger, cn)
663 # Remove the repsFrom and repsTo from each NC to ensure we do
664 # not try (and fail) to talk to the old DCs
665 for nc in ncs:
666 msg = ldb.Message()
667 msg.dn = ldb.Dn(samdb, nc)
669 msg["repsFrom"] = ldb.MessageElement([],
670 ldb.FLAG_MOD_REPLACE,
671 "repsFrom")
672 msg["repsTo"] = ldb.MessageElement([],
673 ldb.FLAG_MOD_REPLACE,
674 "repsTo")
675 samdb.modify(msg)
677 # Update the krbtgt passwords twice, ensuring no tickets from
678 # the old domain are valid
679 update_krbtgt_account_password(samdb)
680 update_krbtgt_account_password(samdb)
682 # restore the sysvol directory from the backup tar file, including the
683 # original NTACLs. Note that the backup_restore() will fail if not root
684 sysvol_tar = os.path.join(targetdir, 'sysvol.tar.gz')
685 dest_sysvol_dir = lp.get('path', 'sysvol')
686 if not os.path.exists(dest_sysvol_dir):
687 os.makedirs(dest_sysvol_dir)
688 backup_restore(sysvol_tar, dest_sysvol_dir, samdb, smbconf)
689 os.remove(sysvol_tar)
691 # fix up any stale links to the old DCs we just removed
692 logger.info("Fixing up any remaining references to the old DCs...")
693 self.fix_old_dc_references(samdb)
695 # Remove DB markers added by the backup process
696 self.remove_backup_markers(samdb)
698 logger.info("Backup file successfully restored to %s" % targetdir)
699 logger.info("Please check the smb.conf settings are correct before "
700 "starting samba.")
703 class cmd_domain_backup_rename(samba.netcmd.Command):
704 """Copy a running DC's DB to backup file, renaming the domain in the process.
706 Where <new-domain> is the new domain's NetBIOS name, and <new-dnsrealm> is
707 the new domain's realm in DNS form.
709 This is similar to 'samba-tool backup online' in that it clones the DB of a
710 running DC. However, this option also renames all the domain entries in the
711 DB. Renaming the domain makes it possible to restore and start a new Samba
712 DC without it interfering with the existing Samba domain. In other words,
713 you could use this option to clone your production samba domain and restore
714 it to a separate pre-production environment that won't overlap or interfere
715 with the existing production Samba domain.
717 Note that:
718 - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
719 and fix any errors it reports.
720 - all the domain's secrets are included in the backup file.
721 - although the DB contents can be untarred and examined manually, you need
722 to run 'samba-tool domain backup restore' before you can start a Samba DC
723 from the backup file.
724 - GPO and sysvol information will still refer to the old realm and will
725 need to be updated manually.
726 - if you specify 'keep-dns-realm', then the DNS records will need updating
727 in order to work (they will still refer to the old DC's IP instead of the
728 new DC's address).
729 - we recommend that you only use this option if you know what you're doing.
732 synopsis = ("%prog <new-domain> <new-dnsrealm> --server=<DC-to-backup> "
733 "--targetdir=<output-dir>")
734 takes_optiongroups = {
735 "sambaopts": options.SambaOptions,
736 "credopts": options.CredentialsOptions,
739 takes_options = [
740 Option("--server", help="The DC to backup", type=str),
741 Option("--targetdir", help="Directory to write the backup file",
742 type=str),
743 Option("--keep-dns-realm", action="store_true", default=False,
744 help="Retain the DNS entries for the old realm in the backup"),
745 Option("--no-secrets", action="store_true", default=False,
746 help="Exclude secret values from the backup created"),
747 Option("--backend-store", type="choice", metavar="BACKENDSTORE",
748 choices=["tdb", "mdb"],
749 help="Specify the database backend to be used "
750 "(default is %s)" % get_default_backend_store()),
753 takes_args = ["new_domain_name", "new_dns_realm"]
755 def update_dns_root(self, logger, samdb, old_realm, delete_old_dns):
756 """Updates dnsRoot for the partition objects to reflect the rename"""
758 # lookup the crossRef objects that hold the old realm's dnsRoot
759 partitions_dn = samdb.get_partitions_dn()
760 res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
761 attrs=["dnsRoot"],
762 expression='(&(objectClass=crossRef)(dnsRoot=*))')
763 new_realm = samdb.domain_dns_name()
765 # go through and add the new realm
766 for res_msg in res:
767 # dnsRoot can be multi-valued, so only look for the old realm
768 for dns_root in res_msg["dnsRoot"]:
769 dns_root = str(dns_root)
770 dn = res_msg.dn
771 if old_realm in dns_root:
772 new_dns_root = re.sub('%s$' % old_realm, new_realm,
773 dns_root)
774 logger.info("Adding %s dnsRoot to %s" % (new_dns_root, dn))
776 m = ldb.Message()
777 m.dn = dn
778 m["dnsRoot"] = ldb.MessageElement(new_dns_root,
779 ldb.FLAG_MOD_ADD,
780 "dnsRoot")
781 samdb.modify(m)
783 # optionally remove the dnsRoot for the old realm
784 if delete_old_dns:
785 logger.info("Removing %s dnsRoot from %s" % (dns_root,
786 dn))
787 m["dnsRoot"] = ldb.MessageElement(dns_root,
788 ldb.FLAG_MOD_DELETE,
789 "dnsRoot")
790 samdb.modify(m)
792 # Updates the CN=<domain>,CN=Partitions,CN=Configuration,... object to
793 # reflect the domain rename
794 def rename_domain_partition(self, logger, samdb, new_netbios_name):
795 """Renames the domain partition object and updates its nETBIOSName"""
797 # lookup the crossRef object that holds the nETBIOSName (nCName has
798 # already been updated by this point, but the netBIOS hasn't)
799 base_dn = samdb.get_default_basedn()
800 nc_name = ldb.binary_encode(str(base_dn))
801 partitions_dn = samdb.get_partitions_dn()
802 res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
803 attrs=["nETBIOSName"],
804 expression='ncName=%s' % nc_name)
806 logger.info("Changing backup domain's NetBIOS name to %s" %
807 new_netbios_name)
808 m = ldb.Message()
809 m.dn = res[0].dn
810 m["nETBIOSName"] = ldb.MessageElement(new_netbios_name,
811 ldb.FLAG_MOD_REPLACE,
812 "nETBIOSName")
813 samdb.modify(m)
815 # renames the object itself to reflect the change in domain
816 new_dn = "CN=%s,%s" % (new_netbios_name, partitions_dn)
817 logger.info("Renaming %s --> %s" % (res[0].dn, new_dn))
818 samdb.rename(res[0].dn, new_dn, controls=['relax:0'])
820 def delete_old_dns_zones(self, logger, samdb, old_realm):
821 # remove the top-level DNS entries for the old realm
822 basedn = samdb.get_default_basedn()
823 dn = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (old_realm, basedn)
824 logger.info("Deleting old DNS zone %s" % dn)
825 samdb.delete(dn, ["tree_delete:1"])
827 forestdn = samdb.get_root_basedn().get_linearized()
828 dn = "DC=_msdcs.%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (old_realm,
829 forestdn)
830 logger.info("Deleting old DNS zone %s" % dn)
831 samdb.delete(dn, ["tree_delete:1"])
833 def fix_old_dn_attributes(self, samdb):
834 """Fixes attributes (i.e. objectCategory) that still use the old DN"""
836 samdb.transaction_start()
837 # Just fix any mismatches in DN detected (leave any other errors)
838 chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
839 in_transaction=True)
840 # fix up incorrect objectCategory/etc attributes
841 setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
842 cross_ncs_ctrl = 'search_options:1:2'
843 controls = ['show_deleted:1', cross_ncs_ctrl]
844 chk.check_database(controls=controls)
845 samdb.transaction_commit()
847 def run(self, new_domain_name, new_dns_realm, sambaopts=None,
848 credopts=None, server=None, targetdir=None, keep_dns_realm=False,
849 no_secrets=False, backend_store=None):
850 logger = self.get_logger()
851 logger.setLevel(logging.INFO)
853 lp = sambaopts.get_loadparm()
854 creds = credopts.get_credentials(lp)
856 # Make sure we have all the required args.
857 if server is None:
858 raise CommandError('Server required')
860 check_targetdir(logger, targetdir)
862 delete_old_dns = not keep_dns_realm
864 new_dns_realm = new_dns_realm.lower()
865 new_domain_name = new_domain_name.upper()
867 new_base_dn = samba.dn_from_dns_name(new_dns_realm)
868 logger.info("New realm for backed up domain: %s" % new_dns_realm)
869 logger.info("New base DN for backed up domain: %s" % new_base_dn)
870 logger.info("New domain NetBIOS name: %s" % new_domain_name)
872 tmpdir = tempfile.mkdtemp(dir=targetdir)
874 # setup a join-context for cloning the remote server
875 include_secrets = not no_secrets
876 ctx = DCCloneAndRenameContext(new_base_dn, new_domain_name,
877 new_dns_realm, logger=logger,
878 creds=creds, lp=lp,
879 include_secrets=include_secrets,
880 dns_backend='SAMBA_INTERNAL',
881 server=server, targetdir=tmpdir,
882 backend_store=backend_store)
884 # sanity-check we're not "renaming" the domain to the same values
885 old_domain = ctx.domain_name
886 if old_domain == new_domain_name:
887 shutil.rmtree(tmpdir)
888 raise CommandError("Cannot use the current domain NetBIOS name.")
890 old_realm = ctx.realm
891 if old_realm == new_dns_realm:
892 shutil.rmtree(tmpdir)
893 raise CommandError("Cannot use the current domain DNS realm.")
895 # do the clone/rename
896 ctx.do_join()
898 # get the paths used for the clone, then drop the old samdb connection
899 del ctx.local_samdb
900 paths = ctx.paths
902 # get a free RID to use as the new DC's SID (when it gets restored)
903 remote_sam = SamDB(url='ldap://' + server, credentials=creds,
904 session_info=system_session(), lp=lp)
905 new_sid = get_sid_for_restore(remote_sam, logger)
907 # Grab the remote DC's sysvol files and bundle them into a tar file.
908 # Note we end up with 2 sysvol dirs - the original domain's files (that
909 # use the old realm) backed here, as well as default files generated
910 # for the new realm as part of the clone/join.
911 sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
912 smb_conn = smb_sysvol_conn(server, lp, creds)
913 backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
915 # connect to the local DB (making sure we use the new/renamed config)
916 lp.load(paths.smbconf)
917 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp,
918 flags=ldb.FLG_DONT_CREATE_DB)
920 # Edit the cloned sam.ldb to mark it as a backup
921 time_str = get_timestamp()
922 add_backup_marker(samdb, "backupDate", time_str)
923 add_backup_marker(samdb, "sidForRestore", new_sid)
924 add_backup_marker(samdb, "backupRename", old_realm)
925 add_backup_marker(samdb, "backupType", "rename")
927 # fix up the DNS objects that are using the old dnsRoot value
928 self.update_dns_root(logger, samdb, old_realm, delete_old_dns)
930 # update the netBIOS name and the Partition object for the domain
931 self.rename_domain_partition(logger, samdb, new_domain_name)
933 if delete_old_dns:
934 self.delete_old_dns_zones(logger, samdb, old_realm)
936 logger.info("Fixing DN attributes after rename...")
937 self.fix_old_dn_attributes(samdb)
939 # ensure the admin user always has a password set (same as provision)
940 if no_secrets:
941 set_admin_password(logger, samdb)
943 # Add everything in the tmpdir to the backup tar file
944 backup_file = backup_filepath(targetdir, new_dns_realm, time_str)
945 create_log_file(tmpdir, lp, "rename", server, include_secrets,
946 "Original domain %s (NetBIOS), %s (DNS realm)" %
947 (old_domain, old_realm))
948 create_backup_tar(logger, tmpdir, backup_file)
950 shutil.rmtree(tmpdir)
953 class cmd_domain_backup_offline(samba.netcmd.Command):
954 """Backup the local domain directories safely into a tar file.
956 Takes a backup copy of the current domain from the local files on disk,
957 with proper locking of the DB to ensure consistency. If the domain were to
958 undergo a catastrophic failure, then the backup file can be used to recover
959 the domain.
961 An offline backup differs to an online backup in the following ways:
962 - a backup can be created even if the DC isn't currently running.
963 - includes non-replicated attributes that an online backup wouldn't store.
964 - takes a copy of the raw database files, which has the risk that any
965 hidden problems in the DB are preserved in the backup."""
967 synopsis = "%prog [options]"
968 takes_optiongroups = {
969 "sambaopts": options.SambaOptions,
972 takes_options = [
973 Option("--targetdir",
974 help="Output directory (required)",
975 type=str),
978 backup_ext = '.bak-offline'
980 def offline_tdb_copy(self, path):
981 backup_path = path + self.backup_ext
982 try:
983 tdb_copy(path, backup_path, readonly=True)
984 except CalledProcessError as copy_err:
985 # If the copy didn't work, check if it was caused by an EINVAL
986 # error on opening the DB. If so, it's a mutex locked database,
987 # which we can safely ignore.
988 try:
989 tdb.open(path)
990 except Exception as e:
991 if hasattr(e, 'errno') and e.errno == errno.EINVAL:
992 return
993 raise e
994 raise copy_err
996 except FileNotFoundError as e:
997 # tdbbackup tool was not found.
998 raise CommandError(e.strerror, e)
1000 if not os.path.exists(backup_path):
1001 s = "tdbbackup said backup succeeded but {0} not found"
1002 raise CommandError(s.format(backup_path))
1005 def offline_mdb_copy(self, path):
1006 mdb_copy(path, path + self.backup_ext)
1008 # Secrets databases are a special case: a transaction must be started
1009 # on the secrets.ldb file before backing up that file and secrets.tdb
1010 def backup_secrets(self, private_dir, lp, logger):
1011 secrets_path = os.path.join(private_dir, 'secrets')
1012 secrets_obj = Ldb(secrets_path + '.ldb', lp=lp,
1013 flags=ldb.FLG_DONT_CREATE_DB)
1014 logger.info('Starting transaction on ' + secrets_path)
1015 secrets_obj.transaction_start()
1016 self.offline_tdb_copy(secrets_path + '.ldb')
1017 self.offline_tdb_copy(secrets_path + '.tdb')
1018 secrets_obj.transaction_cancel()
1020 # sam.ldb must have a transaction started on it before backing up
1021 # everything in sam.ldb.d with the appropriate backup function.
1023 # Obtains the sidForRestore (SID for the new DC) and returns it
1024 # from under the transaction
1025 def backup_smb_dbs(self, private_dir, samdb, lp, logger):
1026 sam_ldb_path = os.path.join(private_dir, 'sam.ldb')
1028 # First, determine if DB backend is MDB. Assume not unless there is a
1029 # 'backendStore' attribute on @PARTITION containing the text 'mdb'
1030 store_label = "backendStore"
1031 res = samdb.search(base="@PARTITION", scope=ldb.SCOPE_BASE,
1032 attrs=[store_label])
1033 mdb_backend = store_label in res[0] and str(res[0][store_label][0]) == 'mdb'
1035 # This is needed to keep this variable in scope until the end
1036 # of the transaction.
1037 res_iterator = None
1039 copy_function = None
1040 if mdb_backend:
1041 logger.info('MDB backend detected. Using mdb backup function.')
1042 copy_function = self.offline_mdb_copy
1044 # We can't backup with a write transaction open, so get a
1045 # read lock with a search_iterator().
1047 # We have tests in lib/ldb/tests/python/api.py that the
1048 # search iterator takes a read lock effective against a
1049 # transaction. This in turn will ensure there are no
1050 # transactions on either the main or sub-database, even if
1051 # the read locks were not enforced globally (they are).
1052 res_iterator = samdb.search_iterator()
1053 else:
1054 logger.info('Starting transaction on ' + sam_ldb_path)
1055 copy_function = self.offline_tdb_copy
1056 samdb.transaction_start()
1058 logger.info(' backing up ' + sam_ldb_path)
1059 self.offline_tdb_copy(sam_ldb_path)
1060 sam_ldb_d = sam_ldb_path + '.d'
1061 for sam_file in os.listdir(sam_ldb_d):
1062 sam_file = os.path.join(sam_ldb_d, sam_file)
1063 if sam_file.endswith('.ldb'):
1064 logger.info(' backing up locked/related file ' + sam_file)
1065 copy_function(sam_file)
1066 elif sam_file.endswith('.tdb'):
1067 logger.info(' tdbbackup of locked/related file ' + sam_file)
1068 self.offline_tdb_copy(sam_file)
1069 else:
1070 logger.info(' copying locked/related file ' + sam_file)
1071 shutil.copyfile(sam_file, sam_file + self.backup_ext)
1073 sid = get_sid_for_restore(samdb, logger)
1075 if mdb_backend:
1076 # Delete the iterator, release the read lock
1077 del(res_iterator)
1078 else:
1079 samdb.transaction_cancel()
1081 return sid
1083 # Find where a path should go in the fixed backup archive structure.
1084 def get_arc_path(self, path, conf_paths):
1085 backup_dirs = {"private": conf_paths.private_dir,
1086 "state": conf_paths.state_dir,
1087 "etc": os.path.dirname(conf_paths.smbconf)}
1088 matching_dirs = [(_, p) for (_, p) in backup_dirs.items() if
1089 path.startswith(p)]
1090 arc_path, fs_path = matching_dirs[0]
1092 # If more than one directory is a parent of this path, then at least
1093 # one configured path is a subdir of another. Use closest match.
1094 if len(matching_dirs) > 1:
1095 arc_path, fs_path = max(matching_dirs, key=lambda p: len(p[1]))
1096 arc_path += path[len(fs_path):]
1098 return arc_path
1100 def run(self, sambaopts=None, targetdir=None):
1102 logger = logging.getLogger()
1103 logger.setLevel(logging.DEBUG)
1104 logger.addHandler(logging.StreamHandler(sys.stdout))
1106 # Get the absolute paths of all the directories we're going to backup
1107 lp = sambaopts.get_loadparm()
1109 paths = samba.provision.provision_paths_from_lp(lp, lp.get('realm'))
1110 if not (paths.samdb and os.path.exists(paths.samdb)):
1111 logger.error("No database found at {0}".format(paths.samdb))
1112 raise CommandError('Please check you are root, and ' +
1113 'are running this command on an AD DC')
1115 check_targetdir(logger, targetdir)
1117 # Iterating over the directories in this specific order ensures that
1118 # when the private directory contains hardlinks that are also contained
1119 # in other directories to be backed up (such as in paths.binddns_dir),
1120 # the hardlinks in the private directory take precedence.
1121 backup_dirs = [paths.private_dir, paths.state_dir,
1122 os.path.dirname(paths.smbconf)] # etc dir
1123 logger.info('running backup on dirs: {0}'.format(' '.join(backup_dirs)))
1125 # Recursively get all file paths in the backup directories
1126 all_files = []
1127 all_stats = set()
1128 for backup_dir in backup_dirs:
1129 for (working_dir, _, filenames) in os.walk(backup_dir):
1130 if working_dir.startswith(paths.sysvol):
1131 continue
1132 if working_dir.endswith('.sock') or '.sock/' in working_dir:
1133 continue
1134 # The BIND DNS database can be regenerated, so it doesn't need
1135 # to be backed up.
1136 if working_dir.startswith(os.path.join(paths.binddns_dir, 'dns')):
1137 continue
1139 for filename in filenames:
1140 full_path = os.path.join(working_dir, filename)
1142 # Ignore files that have already been added. This prevents
1143 # duplicates if one backup dir is a subdirectory of another,
1144 # or if backup dirs contain hardlinks.
1145 try:
1146 s = os.stat(full_path, follow_symlinks=False)
1147 except FileNotFoundError:
1148 logger.warning(f"{full_path} does not exist!")
1149 continue
1151 if (s.st_ino, s.st_dev) in all_stats:
1152 continue
1154 # Assume existing backup files are from a previous backup.
1155 # Delete and ignore.
1156 if filename.endswith(self.backup_ext):
1157 os.remove(full_path)
1158 continue
1160 # Sock files are autogenerated at runtime, ignore.
1161 if filename.endswith('.sock'):
1162 continue
1164 all_files.append(full_path)
1165 all_stats.add((s.st_ino, s.st_dev))
1167 # We would prefer to open with FLG_RDONLY but then we can't
1168 # start a transaction which is the strong isolation we want
1169 # for the backup.
1170 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp,
1171 flags=ldb.FLG_DONT_CREATE_DB)
1173 # Backup secrets, sam.ldb and their downstream files
1174 self.backup_secrets(paths.private_dir, lp, logger)
1175 sid = self.backup_smb_dbs(paths.private_dir, samdb, lp, logger)
1177 # Get the domain SID so we can later place it in the backup
1178 dom_sid_str = samdb.get_domain_sid()
1179 dom_sid = security.dom_sid(dom_sid_str)
1181 # Close the original samdb, to avoid any confusion, we will
1182 # not use this any more as the data has all been copied under
1183 # the transaction
1184 samdb.disconnect()
1185 samdb = None
1187 # Open the new backed up samdb, flag it as backed up, and write
1188 # the next SID so the restore tool can add objects. We use
1189 # options=["modules:"] here to prevent any modules from loading.
1190 # WARNING: Don't change this code unless you know what you're doing.
1191 # Writing to a .bak file only works because the DN being
1192 # written to happens to be top level.
1193 samdb = Ldb(url=paths.samdb + self.backup_ext,
1194 session_info=system_session(), lp=lp,
1195 options=["modules:"], flags=ldb.FLG_DONT_CREATE_DB)
1196 time_str = get_timestamp()
1197 add_backup_marker(samdb, "backupDate", time_str)
1198 add_backup_marker(samdb, "sidForRestore", sid)
1199 add_backup_marker(samdb, "backupType", "offline")
1201 # Close the backed up samdb
1202 samdb.disconnect()
1203 samdb = None
1205 # Now handle all the LDB and TDB files that are not linked to
1206 # anything else. Use transactions for LDBs.
1207 for path in all_files:
1208 if not os.path.exists(path + self.backup_ext):
1209 if path.endswith('.ldb'):
1210 logger.info('Starting transaction on solo db: ' + path)
1211 ldb_obj = Ldb(path, lp=lp, flags=ldb.FLG_DONT_CREATE_DB)
1212 ldb_obj.transaction_start()
1213 logger.info(' running tdbbackup on the same file')
1214 self.offline_tdb_copy(path)
1215 ldb_obj.transaction_cancel()
1216 elif path.endswith('.tdb'):
1217 logger.info('running tdbbackup on lone tdb file ' + path)
1218 self.offline_tdb_copy(path)
1220 # Now make the backup tar file and add all
1221 # backed up files and any other files to it.
1222 temp_tar_dir = tempfile.mkdtemp(dir=targetdir,
1223 prefix='INCOMPLETEsambabackupfile')
1224 temp_tar_name = os.path.join(temp_tar_dir, "samba-backup.tar.bz2")
1225 tar = tarfile.open(temp_tar_name, 'w:bz2')
1227 logger.info('running offline ntacl backup of sysvol')
1228 sysvol_tar_fn = 'sysvol.tar.gz'
1229 sysvol_tar = os.path.join(temp_tar_dir, sysvol_tar_fn)
1230 backup_offline(paths.sysvol, sysvol_tar, paths.smbconf, dom_sid)
1231 tar.add(sysvol_tar, sysvol_tar_fn)
1232 os.remove(sysvol_tar)
1234 create_log_file(temp_tar_dir, lp, "offline", "localhost", True)
1235 backup_fn = os.path.join(temp_tar_dir, "backup.txt")
1236 tar.add(backup_fn, os.path.basename(backup_fn))
1237 os.remove(backup_fn)
1239 logger.info('building backup tar')
1241 chksum_list = []
1243 for path in all_files:
1244 arc_path = self.get_arc_path(path, paths)
1246 if os.path.exists(path + self.backup_ext):
1247 logger.info(' adding backup ' + arc_path + self.backup_ext +
1248 ' to tar and deleting file')
1249 chksum_list.append(
1250 "%s %s" % (create_sha256sum(path + self.backup_ext),
1251 arc_path))
1252 tar.add(path + self.backup_ext, arcname=arc_path)
1253 os.remove(path + self.backup_ext)
1254 elif path.endswith('.ldb') or path.endswith('.tdb'):
1255 logger.info(' skipping ' + arc_path)
1256 elif os.path.isfile(path):
1257 logger.info(' adding misc file ' + arc_path)
1258 chksum_list.append("%s %s" %
1259 (create_sha256sum(path),
1260 arc_path))
1261 tar.add(path, arcname=arc_path)
1263 chksum_filepath = os.path.join(temp_tar_dir, "SHA256SUM")
1264 with open(chksum_filepath, "w") as f:
1265 for c in chksum_list:
1266 f.write(c + '\n')
1267 tar.add(chksum_filepath, os.path.basename(chksum_filepath))
1268 os.remove(chksum_filepath)
1270 tar.close()
1271 os.rename(temp_tar_name,
1272 os.path.join(targetdir,
1273 'samba-backup-{0}.tar.bz2'.format(time_str)))
1274 os.rmdir(temp_tar_dir)
1275 logger.info('Backup succeeded.')
1278 class cmd_domain_backup(samba.netcmd.SuperCommand):
1279 """Create or restore a backup of the domain."""
1280 subcommands = {'offline': cmd_domain_backup_offline(),
1281 'online': cmd_domain_backup_online(),
1282 'rename': cmd_domain_backup_rename(),
1283 'restore': cmd_domain_backup_restore()}