netcmd: Fix error-checking condition
[Samba.git] / python / samba / netcmd / domain_backup.py
bloba9c840d3caee35bb4b59a56ac991946cee50c9c4
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, get_default_backend_store
29 import ldb
30 from ldb import LdbError
31 from samba.samba3 import libsmb_samba_internal as libsmb
32 from samba.samba3 import param as s3param
33 from samba.ntacls import backup_online, backup_restore, backup_offline
34 from samba.auth import system_session
35 from samba.join import DCJoinContext, join_clone, DCCloneAndRenameContext
36 from samba.dcerpc.security import dom_sid
37 from samba.netcmd import Option, CommandError
38 from samba.dcerpc import misc, security, drsblobs
39 from samba import Ldb
40 from . fsmo import cmd_fsmo_seize
41 from samba.provision import make_smbconf, DEFAULTSITE
42 from samba.upgradehelpers import update_krbtgt_account_password
43 from samba.remove_dc import remove_dc
44 from samba.provision import secretsdb_self_join
45 from samba.dbchecker import dbcheck
46 import re
47 from samba.provision import guess_names, determine_host_ip, determine_host_ip6
48 from samba.provision.sambadns import (fill_dns_data_partitions,
49 get_dnsadmins_sid,
50 get_domainguid)
51 from samba.tdb_util import tdb_copy
52 from samba.mdb_util import mdb_copy
53 import errno
54 from subprocess import CalledProcessError
55 from samba import sites
56 from samba.dsdb import _dsdb_load_udv_v2
57 from samba.ndr import ndr_pack
58 from samba.credentials import SMB_SIGNING_REQUIRED
61 # work out a SID (based on a free RID) to use when the domain gets restored.
62 # This ensures that the restored DC's SID won't clash with any other RIDs
63 # already in use in the domain
64 def get_sid_for_restore(samdb, logger):
65 # Find the DN of the RID set of the server
66 res = samdb.search(base=ldb.Dn(samdb, samdb.get_serverName()),
67 scope=ldb.SCOPE_BASE, attrs=["serverReference"])
68 server_ref_dn = ldb.Dn(samdb, str(res[0]['serverReference'][0]))
69 res = samdb.search(base=server_ref_dn,
70 scope=ldb.SCOPE_BASE,
71 attrs=['rIDSetReferences'])
72 rid_set_dn = ldb.Dn(samdb, str(res[0]['rIDSetReferences'][0]))
74 # Get the alloc pools and next RID of the RID set
75 res = samdb.search(base=rid_set_dn,
76 scope=ldb.SCOPE_SUBTREE,
77 expression="(rIDNextRID=*)",
78 attrs=['rIDAllocationPool',
79 'rIDPreviousAllocationPool',
80 'rIDNextRID'])
82 # Decode the bounds of the RID allocation pools
83 try:
84 rid = int(res[0].get('rIDNextRID')[0])
85 except IndexError:
86 logger.info("The RID pool for this DC is not initalized "
87 "(e.g. it may be a fairly new DC).")
88 logger.info("To initialize it, create a temporary user on this DC "
89 "(you can delete it later).")
90 raise CommandError("Cannot create backup - "
91 "please initialize this DC's RID pool first.")
93 def split_val(num):
94 high = (0xFFFFFFFF00000000 & int(num)) >> 32
95 low = 0x00000000FFFFFFFF & int(num)
96 return low, high
97 pool_l, pool_h = split_val(res[0].get('rIDPreviousAllocationPool')[0])
98 npool_l, npool_h = split_val(res[0].get('rIDAllocationPool')[0])
100 # Calculate next RID based on pool bounds
101 if rid == npool_h:
102 raise CommandError('Out of RIDs, finished AllocPool')
103 if rid == pool_h:
104 if pool_h == npool_h:
105 raise CommandError('Out of RIDs, finished PrevAllocPool.')
106 rid = npool_l
107 else:
108 rid += 1
110 # Construct full SID
111 sid = dom_sid(samdb.get_domain_sid())
112 sid_for_restore = str(sid) + '-' + str(rid)
114 # Confirm the SID is not already in use
115 try:
116 res = samdb.search(scope=ldb.SCOPE_BASE,
117 base='<SID=%s>' % sid_for_restore,
118 attrs=[],
119 controls=['show_deleted:1',
120 'show_recycled:1'])
121 if len(res) != 1:
122 # This case makes no sense, but neither does a corrupt RID set
123 raise CommandError("Cannot create backup - "
124 "this DC's RID pool is corrupt, "
125 "the next SID (%s) appears to be in use." %
126 sid_for_restore)
127 raise CommandError("Cannot create backup - "
128 "this DC's RID pool is corrupt, "
129 "the next SID %s points to existing object %s. "
130 "Please run samba-tool dbcheck on the source DC." %
131 (sid_for_restore, res[0].dn))
132 except ldb.LdbError as e:
133 (enum, emsg) = e.args
134 if enum != ldb.ERR_NO_SUCH_OBJECT:
135 # We want NO_SUCH_OBJECT, anything else is a serious issue
136 raise
138 return str(sid) + '-' + str(rid)
141 def smb_sysvol_conn(server, lp, creds):
142 """Returns an SMB connection to the sysvol share on the DC"""
143 # the SMB bindings rely on having a s3 loadparm
144 s3_lp = s3param.get_context()
145 s3_lp.load(lp.configfile)
147 # Force signing for the connection
148 saved_signing_state = creds.get_smb_signing()
149 creds.set_smb_signing(SMB_SIGNING_REQUIRED)
150 conn = libsmb.Conn(server, "sysvol", lp=s3_lp, creds=creds)
151 # Reset signing state
152 creds.set_smb_signing(saved_signing_state)
153 return conn
156 def get_timestamp():
157 return datetime.datetime.now().isoformat().replace(':', '-')
160 def backup_filepath(targetdir, name, time_str):
161 filename = 'samba-backup-%s-%s.tar.bz2' % (name, time_str)
162 return os.path.join(targetdir, filename)
165 def create_backup_tar(logger, tmpdir, backup_filepath):
166 # Adds everything in the tmpdir into a new tar file
167 logger.info("Creating backup file %s..." % backup_filepath)
168 tf = tarfile.open(backup_filepath, 'w:bz2')
169 tf.add(tmpdir, arcname='./')
170 tf.close()
173 def create_log_file(targetdir, lp, backup_type, server, include_secrets,
174 extra_info=None):
175 # create a summary file about the backup, which will get included in the
176 # tar file. This makes it easy for users to see what the backup involved,
177 # without having to untar the DB and interrogate it
178 f = open(os.path.join(targetdir, "backup.txt"), 'w')
179 try:
180 time_str = datetime.datetime.now().strftime('%Y-%b-%d %H:%M:%S')
181 f.write("Backup created %s\n" % time_str)
182 f.write("Using samba-tool version: %s\n" % lp.get('server string'))
183 f.write("Domain %s backup, using DC '%s'\n" % (backup_type, server))
184 f.write("Backup for domain %s (NetBIOS), %s (DNS realm)\n" %
185 (lp.get('workgroup'), lp.get('realm').lower()))
186 f.write("Backup contains domain secrets: %s\n" % str(include_secrets))
187 if extra_info:
188 f.write("%s\n" % extra_info)
189 finally:
190 f.close()
193 # Add a backup-specific marker to the DB with info that we'll use during
194 # the restore process
195 def add_backup_marker(samdb, marker, value):
196 m = ldb.Message()
197 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
198 m[marker] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, marker)
199 samdb.modify(m)
202 def check_targetdir(logger, targetdir):
203 if targetdir is None:
204 raise CommandError('Target directory required')
206 if not os.path.exists(targetdir):
207 logger.info('Creating targetdir %s...' % targetdir)
208 os.makedirs(targetdir)
209 elif not os.path.isdir(targetdir):
210 raise CommandError("%s is not a directory" % targetdir)
213 # For '--no-secrets' backups, this sets the Administrator user's password to a
214 # randomly-generated value. This is similar to the provision behaviour
215 def set_admin_password(logger, samdb):
216 """Sets a randomly generated password for the backup DB's admin user"""
218 # match the admin user by RID
219 domainsid = samdb.get_domain_sid()
220 match_admin = "(objectsid=%s-%s)" % (domainsid,
221 security.DOMAIN_RID_ADMINISTRATOR)
222 search_expr = "(&(objectClass=user)%s)" % (match_admin,)
224 # retrieve the admin username (just in case it's been renamed)
225 res = samdb.search(base=samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
226 expression=search_expr)
227 username = str(res[0]['samaccountname'])
229 adminpass = samba.generate_random_password(12, 32)
230 logger.info("Setting %s password in backup to: %s" % (username, adminpass))
231 logger.info("Run 'samba-tool user setpassword %s' after restoring DB" %
232 username)
233 samdb.setpassword(search_expr, adminpass, force_change_at_next_login=False,
234 username=username)
237 class cmd_domain_backup_online(samba.netcmd.Command):
238 '''Copy a running DC's current DB into a backup tar file.
240 Takes a backup copy of the current domain from a running DC. If the domain
241 were to undergo a catastrophic failure, then the backup file can be used to
242 recover the domain. The backup created is similar to the DB that a new DC
243 would receive when it joins the domain.
245 Note that:
246 - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
247 and fix any errors it reports.
248 - all the domain's secrets are included in the backup file.
249 - although the DB contents can be untarred and examined manually, you need
250 to run 'samba-tool domain backup restore' before you can start a Samba DC
251 from the backup file.'''
253 synopsis = "%prog --server=<DC-to-backup> --targetdir=<output-dir>"
254 takes_optiongroups = {
255 "sambaopts": options.SambaOptions,
256 "credopts": options.CredentialsOptions,
259 takes_options = [
260 Option("--server", help="The DC to backup", type=str),
261 Option("--targetdir", type=str,
262 help="Directory to write the backup file to"),
263 Option("--no-secrets", action="store_true", default=False,
264 help="Exclude secret values from the backup created"),
265 Option("--backend-store", type="choice", metavar="BACKENDSTORE",
266 choices=["tdb", "mdb"],
267 help="Specify the database backend to be used "
268 "(default is %s)" % get_default_backend_store()),
271 def run(self, sambaopts=None, credopts=None, server=None, targetdir=None,
272 no_secrets=False, backend_store=None):
273 logger = self.get_logger()
274 logger.setLevel(logging.DEBUG)
276 lp = sambaopts.get_loadparm()
277 creds = credopts.get_credentials(lp)
279 # Make sure we have all the required args.
280 if server is None:
281 raise CommandError('Server required')
283 check_targetdir(logger, targetdir)
285 tmpdir = tempfile.mkdtemp(dir=targetdir)
287 # Run a clone join on the remote
288 include_secrets = not no_secrets
289 try:
290 ctx = join_clone(logger=logger, creds=creds, lp=lp,
291 include_secrets=include_secrets, server=server,
292 dns_backend='SAMBA_INTERNAL', targetdir=tmpdir,
293 backend_store=backend_store)
295 # get the paths used for the clone, then drop the old samdb connection
296 paths = ctx.paths
297 del ctx
299 # Get a free RID to use as the new DC's SID (when it gets restored)
300 remote_sam = SamDB(url='ldap://' + server, credentials=creds,
301 session_info=system_session(), lp=lp)
302 new_sid = get_sid_for_restore(remote_sam, logger)
303 realm = remote_sam.domain_dns_name()
305 # Grab the remote DC's sysvol files and bundle them into a tar file
306 logger.info("Backing up sysvol files (via SMB)...")
307 sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
308 smb_conn = smb_sysvol_conn(server, lp, creds)
309 backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
311 # remove the default sysvol files created by the clone (we want to
312 # make sure we restore the sysvol.tar.gz files instead)
313 shutil.rmtree(paths.sysvol)
315 # Edit the downloaded sam.ldb to mark it as a backup
316 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp,
317 flags=ldb.FLG_DONT_CREATE_DB)
318 time_str = get_timestamp()
319 add_backup_marker(samdb, "backupDate", time_str)
320 add_backup_marker(samdb, "sidForRestore", new_sid)
321 add_backup_marker(samdb, "backupType", "online")
323 # ensure the admin user always has a password set (same as provision)
324 if no_secrets:
325 set_admin_password(logger, samdb)
327 # Add everything in the tmpdir to the backup tar file
328 backup_file = backup_filepath(targetdir, realm, time_str)
329 create_log_file(tmpdir, lp, "online", server, include_secrets)
330 create_backup_tar(logger, tmpdir, backup_file)
331 finally:
332 shutil.rmtree(tmpdir)
335 class cmd_domain_backup_restore(cmd_fsmo_seize):
336 '''Restore the domain's DB from a backup-file.
338 This restores a previously backed up copy of the domain's DB on a new DC.
340 Note that the restored DB will not contain the original DC that the backup
341 was taken from (or any other DCs in the original domain). Only the new DC
342 (specified by --newservername) will be present in the restored DB.
344 Samba can then be started against the restored DB. Any existing DCs for the
345 domain should be shutdown before the new DC is started. Other DCs can then
346 be joined to the new DC to recover the network.
348 Note that this command should be run as the root user - it will fail
349 otherwise.'''
351 synopsis = ("%prog --backup-file=<tar-file> --targetdir=<output-dir> "
352 "--newservername=<DC-name>")
353 takes_options = [
354 Option("--backup-file", help="Path to backup file", type=str),
355 Option("--targetdir", help="Path to write to", type=str),
356 Option("--newservername", help="Name for new server", type=str),
357 Option("--host-ip", type="string", metavar="IPADDRESS",
358 help="set IPv4 ipaddress"),
359 Option("--host-ip6", type="string", metavar="IP6ADDRESS",
360 help="set IPv6 ipaddress"),
361 Option("--site", help="Site to add the new server in", type=str),
364 takes_optiongroups = {
365 "sambaopts": options.SambaOptions,
366 "credopts": options.CredentialsOptions,
369 def register_dns_zone(self, logger, samdb, lp, ntdsguid, host_ip,
370 host_ip6, site):
372 Registers the new realm's DNS objects when a renamed domain backup
373 is restored.
375 names = guess_names(lp)
376 domaindn = names.domaindn
377 forestdn = samdb.get_root_basedn().get_linearized()
378 dnsdomain = names.dnsdomain.lower()
379 dnsforest = dnsdomain
380 hostname = names.netbiosname.lower()
381 domainsid = dom_sid(samdb.get_domain_sid())
382 dnsadmins_sid = get_dnsadmins_sid(samdb, domaindn)
383 domainguid = get_domainguid(samdb, domaindn)
385 # work out the IP address to use for the new DC's DNS records
386 host_ip = determine_host_ip(logger, lp, host_ip)
387 host_ip6 = determine_host_ip6(logger, lp, host_ip6)
389 if host_ip is None and host_ip6 is None:
390 raise CommandError('Please specify a host-ip for the new server')
392 logger.info("DNS realm was renamed to %s" % dnsdomain)
393 logger.info("Populating DNS partitions for new realm...")
395 # Add the DNS objects for the new realm (note: the backup clone already
396 # has the root server objects, so don't add them again)
397 fill_dns_data_partitions(samdb, domainsid, site, domaindn,
398 forestdn, dnsdomain, dnsforest, hostname,
399 host_ip, host_ip6, domainguid, ntdsguid,
400 dnsadmins_sid, add_root=False)
402 def fix_old_dc_references(self, samdb):
403 '''Fixes attributes that reference the old/removed DCs'''
405 # we just want to fix up DB problems here that were introduced by us
406 # removing the old DCs. We restrict what we fix up so that the restored
407 # DB matches the backed-up DB as close as possible. (There may be other
408 # DB issues inherited from the backed-up DC, but it's not our place to
409 # silently try to fix them here).
410 samdb.transaction_start()
411 chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
412 in_transaction=True)
414 # fix up stale references to the old DC
415 setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
416 attrs = ['lastKnownParent', 'interSiteTopologyGenerator']
418 # fix-up stale one-way links that point to the old DC
419 setattr(chk, 'remove_plausible_deleted_DN_links', 'ALL')
420 attrs += ['msDS-NC-Replica-Locations']
422 cross_ncs_ctrl = 'search_options:1:2'
423 controls = ['show_deleted:1', cross_ncs_ctrl]
424 chk.check_database(controls=controls, attrs=attrs)
425 samdb.transaction_commit()
427 def create_default_site(self, samdb, logger):
428 '''Creates the default site, if it doesn't already exist'''
430 sitename = DEFAULTSITE
431 search_expr = "(&(cn={0})(objectclass=site))".format(sitename)
432 res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
433 expression=search_expr)
435 if len(res) == 0:
436 logger.info("Creating default site '{0}'".format(sitename))
437 sites.create_site(samdb, samdb.get_config_basedn(), sitename)
439 return sitename
441 def remove_backup_markers(self, samdb):
442 """Remove DB markers added by the backup process"""
444 # check what markers we need to remove (this may vary)
445 markers = ['sidForRestore', 'backupRename', 'backupDate', 'backupType']
446 res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
447 scope=ldb.SCOPE_BASE,
448 attrs=markers)
450 # remove any markers that exist in the DB
451 m = ldb.Message()
452 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
454 for attr in markers:
455 if attr in res[0]:
456 m[attr] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, attr)
458 samdb.modify(m)
460 def get_backup_type(self, samdb):
461 res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
462 scope=ldb.SCOPE_BASE,
463 attrs=['backupRename', 'backupType'])
465 # note that the backupType marker won't exist on backups created on
466 # v4.9. However, we can still infer the type, as only rename and
467 # online backups are supported on v4.9
468 if 'backupType' in res[0]:
469 backup_type = str(res[0]['backupType'])
470 elif 'backupRename' in res[0]:
471 backup_type = "rename"
472 else:
473 backup_type = "online"
475 return backup_type
477 def save_uptodate_vectors(self, samdb, partitions):
478 """Ensures the UTDV used by DRS is correct after an offline backup"""
479 for nc in partitions:
480 # load the replUpToDateVector we *should* have
481 utdv = _dsdb_load_udv_v2(samdb, nc)
483 # convert it to NDR format and write it into the DB
484 utdv_blob = drsblobs.replUpToDateVectorBlob()
485 utdv_blob.version = 2
486 utdv_blob.ctr.cursors = utdv
487 utdv_blob.ctr.count = len(utdv)
488 new_value = ndr_pack(utdv_blob)
490 m = ldb.Message()
491 m.dn = ldb.Dn(samdb, nc)
492 m["replUpToDateVector"] = ldb.MessageElement(new_value,
493 ldb.FLAG_MOD_REPLACE,
494 "replUpToDateVector")
495 samdb.modify(m)
497 def run(self, sambaopts=None, credopts=None, backup_file=None,
498 targetdir=None, newservername=None, host_ip=None, host_ip6=None,
499 site=None):
500 if not (backup_file and os.path.exists(backup_file)):
501 raise CommandError('Backup file not found.')
502 if targetdir is None:
503 raise CommandError('Please specify a target directory')
504 # allow restoredc to install into a directory prepopulated by selftest
505 if (os.path.exists(targetdir) and os.listdir(targetdir) and
506 os.environ.get('SAMBA_SELFTEST') != '1'):
507 raise CommandError('Target directory is not empty')
508 if not newservername:
509 raise CommandError('Server name required')
511 logger = logging.getLogger()
512 logger.setLevel(logging.DEBUG)
513 logger.addHandler(logging.StreamHandler(sys.stdout))
515 # ldapcmp prefers the server's netBIOS name in upper-case
516 newservername = newservername.upper()
518 # extract the backup .tar to a temp directory
519 targetdir = os.path.abspath(targetdir)
520 tf = tarfile.open(backup_file)
521 tf.extractall(targetdir)
522 tf.close()
524 # use the smb.conf that got backed up, by default (save what was
525 # actually backed up, before we mess with it)
526 smbconf = os.path.join(targetdir, 'etc', 'smb.conf')
527 shutil.copyfile(smbconf, smbconf + ".orig")
529 # if a smb.conf was specified on the cmd line, then use that instead
530 cli_smbconf = sambaopts.get_loadparm_path()
531 if cli_smbconf:
532 logger.info("Using %s as restored domain's smb.conf" % cli_smbconf)
533 shutil.copyfile(cli_smbconf, smbconf)
535 lp = samba.param.LoadParm()
536 lp.load(smbconf)
538 # open a DB connection to the restored DB
539 private_dir = os.path.join(targetdir, 'private')
540 samdb_path = os.path.join(private_dir, 'sam.ldb')
541 samdb = SamDB(url=samdb_path, session_info=system_session(), lp=lp,
542 flags=ldb.FLG_DONT_CREATE_DB)
543 backup_type = self.get_backup_type(samdb)
545 if site is None:
546 # There's no great way to work out the correct site to add the
547 # restored DC to. By default, add it to Default-First-Site-Name,
548 # creating the site if it doesn't already exist
549 site = self.create_default_site(samdb, logger)
550 logger.info("Adding new DC to site '{0}'".format(site))
552 # read the naming contexts out of the DB
553 res = samdb.search(base="", scope=ldb.SCOPE_BASE,
554 attrs=['namingContexts'])
555 ncs = [str(r) for r in res[0].get('namingContexts')]
557 # for offline backups we need to make sure the upToDateness info
558 # contains the invocation-ID and highest-USN of the DC we backed up.
559 # Otherwise replication propagation dampening won't correctly filter
560 # objects created by that DC
561 if backup_type == "offline":
562 self.save_uptodate_vectors(samdb, ncs)
564 # Create account using the join_add_objects function in the join object
565 # We need namingContexts, account control flags, and the sid saved by
566 # the backup process.
567 creds = credopts.get_credentials(lp)
568 ctx = DCJoinContext(logger, creds=creds, lp=lp, site=site,
569 forced_local_samdb=samdb,
570 netbios_name=newservername)
571 ctx.nc_list = ncs
572 ctx.full_nc_list = ncs
573 ctx.userAccountControl = (samba.dsdb.UF_SERVER_TRUST_ACCOUNT |
574 samba.dsdb.UF_TRUSTED_FOR_DELEGATION)
576 # rewrite the smb.conf to make sure it uses the new targetdir settings.
577 # (This doesn't update all filepaths in a customized config, but it
578 # corrects the same paths that get set by a new provision)
579 logger.info('Updating basic smb.conf settings...')
580 make_smbconf(smbconf, newservername, ctx.domain_name,
581 ctx.realm, targetdir, lp=lp,
582 serverrole="active directory domain controller")
584 # Get the SID saved by the backup process and create account
585 res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
586 scope=ldb.SCOPE_BASE,
587 attrs=['sidForRestore'])
588 sid = res[0].get('sidForRestore')[0]
589 logger.info('Creating account with SID: ' + str(sid))
590 try:
591 ctx.join_add_objects(specified_sid=dom_sid(str(sid)))
592 except LdbError as e:
593 (enum, emsg) = e.args
594 if enum != ldb.ERR_CONSTRAINT_VIOLATION:
595 raise
597 dup_res = []
598 try:
599 dup_res = samdb.search(base=ldb.Dn(samdb, "<SID=%s>" % sid),
600 scope=ldb.SCOPE_BASE,
601 attrs=['objectGUID'],
602 controls=["show_deleted:0",
603 "show_recycled:0"])
604 except LdbError as dup_e:
605 (dup_enum, _) = dup_e.args
606 if dup_enum != ldb.ERR_NO_SUCH_OBJECT:
607 raise
609 if (len(dup_res) != 1):
610 raise
612 objectguid = samdb.schema_format_value("objectGUID",
613 dup_res[0]["objectGUID"][0])
614 objectguid = objectguid.decode('utf-8')
615 logger.error("The RID Pool on the source DC for the backup in %s "
616 "may be corrupt "
617 "or in conflict with SIDs already allocated "
618 "in the domain. " % backup_file)
619 logger.error("Running 'samba-tool dbcheck' on the source "
620 "DC (and obtaining a new backup) may correct the issue.")
621 logger.error("Alternatively please obtain a new backup "
622 "against a different DC.")
623 logger.error("The SID we wish to use (%s) is recorded in "
624 "@SAMBA_DSDB as the sidForRestore attribute."
625 % sid)
627 raise CommandError("Domain restore failed because there "
628 "is already an existing object (%s) "
629 "with SID %s and objectGUID %s. "
630 "This conflicts with "
631 "the new DC account we want to add "
632 "for the restored domain. " % (
633 dup_res[0].dn, sid, objectguid))
635 m = ldb.Message()
636 m.dn = ldb.Dn(samdb, '@ROOTDSE')
637 ntds_guid = str(ctx.ntds_guid)
638 m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % ntds_guid,
639 ldb.FLAG_MOD_REPLACE,
640 "dsServiceName")
641 samdb.modify(m)
643 # if we renamed the backed-up domain, then we need to add the DNS
644 # objects for the new realm (we do this in the restore, now that we
645 # know the new DC's IP address)
646 if backup_type == "rename":
647 self.register_dns_zone(logger, samdb, lp, ctx.ntds_guid,
648 host_ip, host_ip6, site)
650 secrets_path = os.path.join(private_dir, 'secrets.ldb')
651 secrets_ldb = Ldb(secrets_path, session_info=system_session(), lp=lp,
652 flags=ldb.FLG_DONT_CREATE_DB)
653 secretsdb_self_join(secrets_ldb, domain=ctx.domain_name,
654 realm=ctx.realm, dnsdomain=ctx.dnsdomain,
655 netbiosname=ctx.myname, domainsid=ctx.domsid,
656 machinepass=ctx.acct_pass,
657 key_version_number=ctx.key_version_number,
658 secure_channel_type=misc.SEC_CHAN_BDC)
660 # Seize DNS roles
661 domain_dn = samdb.domain_dn()
662 forest_dn = samba.dn_from_dns_name(samdb.forest_dns_name())
663 domaindns_dn = ("CN=Infrastructure,DC=DomainDnsZones,", domain_dn)
664 forestdns_dn = ("CN=Infrastructure,DC=ForestDnsZones,", forest_dn)
665 for dn_prefix, dns_dn in [forestdns_dn, domaindns_dn]:
666 if dns_dn not in ncs:
667 continue
668 full_dn = dn_prefix + dns_dn
669 m = ldb.Message()
670 m.dn = ldb.Dn(samdb, full_dn)
671 m["fSMORoleOwner"] = ldb.MessageElement(samdb.get_dsServiceName(),
672 ldb.FLAG_MOD_REPLACE,
673 "fSMORoleOwner")
674 samdb.modify(m)
676 # Seize other roles
677 for role in ['rid', 'pdc', 'naming', 'infrastructure', 'schema']:
678 self.seize_role(role, samdb, force=True)
680 # Get all DCs and remove them (this ensures these DCs cannot
681 # replicate because they will not have a password)
682 search_expr = "(&(objectClass=Server)(serverReference=*))"
683 res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
684 expression=search_expr)
685 for m in res:
686 cn = str(m.get('cn')[0])
687 if cn != newservername:
688 remove_dc(samdb, logger, cn)
690 # Remove the repsFrom and repsTo from each NC to ensure we do
691 # not try (and fail) to talk to the old DCs
692 for nc in ncs:
693 msg = ldb.Message()
694 msg.dn = ldb.Dn(samdb, nc)
696 msg["repsFrom"] = ldb.MessageElement([],
697 ldb.FLAG_MOD_REPLACE,
698 "repsFrom")
699 msg["repsTo"] = ldb.MessageElement([],
700 ldb.FLAG_MOD_REPLACE,
701 "repsTo")
702 samdb.modify(msg)
704 # Update the krbtgt passwords twice, ensuring no tickets from
705 # the old domain are valid
706 update_krbtgt_account_password(samdb)
707 update_krbtgt_account_password(samdb)
709 # restore the sysvol directory from the backup tar file, including the
710 # original NTACLs. Note that the backup_restore() will fail if not root
711 sysvol_tar = os.path.join(targetdir, 'sysvol.tar.gz')
712 dest_sysvol_dir = lp.get('path', 'sysvol')
713 if not os.path.exists(dest_sysvol_dir):
714 os.makedirs(dest_sysvol_dir)
715 backup_restore(sysvol_tar, dest_sysvol_dir, samdb, smbconf)
716 os.remove(sysvol_tar)
718 # fix up any stale links to the old DCs we just removed
719 logger.info("Fixing up any remaining references to the old DCs...")
720 self.fix_old_dc_references(samdb)
722 # Remove DB markers added by the backup process
723 self.remove_backup_markers(samdb)
725 logger.info("Backup file successfully restored to %s" % targetdir)
726 logger.info("Please check the smb.conf settings are correct before "
727 "starting samba.")
730 class cmd_domain_backup_rename(samba.netcmd.Command):
731 '''Copy a running DC's DB to backup file, renaming the domain in the process.
733 Where <new-domain> is the new domain's NetBIOS name, and <new-dnsrealm> is
734 the new domain's realm in DNS form.
736 This is similar to 'samba-tool backup online' in that it clones the DB of a
737 running DC. However, this option also renames all the domain entries in the
738 DB. Renaming the domain makes it possible to restore and start a new Samba
739 DC without it interfering with the existing Samba domain. In other words,
740 you could use this option to clone your production samba domain and restore
741 it to a separate pre-production environment that won't overlap or interfere
742 with the existing production Samba domain.
744 Note that:
745 - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
746 and fix any errors it reports.
747 - all the domain's secrets are included in the backup file.
748 - although the DB contents can be untarred and examined manually, you need
749 to run 'samba-tool domain backup restore' before you can start a Samba DC
750 from the backup file.
751 - GPO and sysvol information will still refer to the old realm and will
752 need to be updated manually.
753 - if you specify 'keep-dns-realm', then the DNS records will need updating
754 in order to work (they will still refer to the old DC's IP instead of the
755 new DC's address).
756 - we recommend that you only use this option if you know what you're doing.
759 synopsis = ("%prog <new-domain> <new-dnsrealm> --server=<DC-to-backup> "
760 "--targetdir=<output-dir>")
761 takes_optiongroups = {
762 "sambaopts": options.SambaOptions,
763 "credopts": options.CredentialsOptions,
766 takes_options = [
767 Option("--server", help="The DC to backup", type=str),
768 Option("--targetdir", help="Directory to write the backup file",
769 type=str),
770 Option("--keep-dns-realm", action="store_true", default=False,
771 help="Retain the DNS entries for the old realm in the backup"),
772 Option("--no-secrets", action="store_true", default=False,
773 help="Exclude secret values from the backup created"),
774 Option("--backend-store", type="choice", metavar="BACKENDSTORE",
775 choices=["tdb", "mdb"],
776 help="Specify the database backend to be used "
777 "(default is %s)" % get_default_backend_store()),
780 takes_args = ["new_domain_name", "new_dns_realm"]
782 def update_dns_root(self, logger, samdb, old_realm, delete_old_dns):
783 '''Updates dnsRoot for the partition objects to reflect the rename'''
785 # lookup the crossRef objects that hold the old realm's dnsRoot
786 partitions_dn = samdb.get_partitions_dn()
787 res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
788 attrs=["dnsRoot"],
789 expression='(&(objectClass=crossRef)(dnsRoot=*))')
790 new_realm = samdb.domain_dns_name()
792 # go through and add the new realm
793 for res_msg in res:
794 # dnsRoot can be multi-valued, so only look for the old realm
795 for dns_root in res_msg["dnsRoot"]:
796 dns_root = str(dns_root)
797 dn = res_msg.dn
798 if old_realm in dns_root:
799 new_dns_root = re.sub('%s$' % old_realm, new_realm,
800 dns_root)
801 logger.info("Adding %s dnsRoot to %s" % (new_dns_root, dn))
803 m = ldb.Message()
804 m.dn = dn
805 m["dnsRoot"] = ldb.MessageElement(new_dns_root,
806 ldb.FLAG_MOD_ADD,
807 "dnsRoot")
808 samdb.modify(m)
810 # optionally remove the dnsRoot for the old realm
811 if delete_old_dns:
812 logger.info("Removing %s dnsRoot from %s" % (dns_root,
813 dn))
814 m["dnsRoot"] = ldb.MessageElement(dns_root,
815 ldb.FLAG_MOD_DELETE,
816 "dnsRoot")
817 samdb.modify(m)
819 # Updates the CN=<domain>,CN=Partitions,CN=Configuration,... object to
820 # reflect the domain rename
821 def rename_domain_partition(self, logger, samdb, new_netbios_name):
822 '''Renames the domain parition object and updates its nETBIOSName'''
824 # lookup the crossRef object that holds the nETBIOSName (nCName has
825 # already been updated by this point, but the netBIOS hasn't)
826 base_dn = samdb.get_default_basedn()
827 nc_name = ldb.binary_encode(str(base_dn))
828 partitions_dn = samdb.get_partitions_dn()
829 res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
830 attrs=["nETBIOSName"],
831 expression='ncName=%s' % nc_name)
833 logger.info("Changing backup domain's NetBIOS name to %s" %
834 new_netbios_name)
835 m = ldb.Message()
836 m.dn = res[0].dn
837 m["nETBIOSName"] = ldb.MessageElement(new_netbios_name,
838 ldb.FLAG_MOD_REPLACE,
839 "nETBIOSName")
840 samdb.modify(m)
842 # renames the object itself to reflect the change in domain
843 new_dn = "CN=%s,%s" % (new_netbios_name, partitions_dn)
844 logger.info("Renaming %s --> %s" % (res[0].dn, new_dn))
845 samdb.rename(res[0].dn, new_dn, controls=['relax:0'])
847 def delete_old_dns_zones(self, logger, samdb, old_realm):
848 # remove the top-level DNS entries for the old realm
849 basedn = samdb.get_default_basedn()
850 dn = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (old_realm, basedn)
851 logger.info("Deleting old DNS zone %s" % dn)
852 samdb.delete(dn, ["tree_delete:1"])
854 forestdn = samdb.get_root_basedn().get_linearized()
855 dn = "DC=_msdcs.%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (old_realm,
856 forestdn)
857 logger.info("Deleting old DNS zone %s" % dn)
858 samdb.delete(dn, ["tree_delete:1"])
860 def fix_old_dn_attributes(self, samdb):
861 '''Fixes attributes (i.e. objectCategory) that still use the old DN'''
863 samdb.transaction_start()
864 # Just fix any mismatches in DN detected (leave any other errors)
865 chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
866 in_transaction=True)
867 # fix up incorrect objectCategory/etc attributes
868 setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
869 cross_ncs_ctrl = 'search_options:1:2'
870 controls = ['show_deleted:1', cross_ncs_ctrl]
871 chk.check_database(controls=controls)
872 samdb.transaction_commit()
874 def run(self, new_domain_name, new_dns_realm, sambaopts=None,
875 credopts=None, server=None, targetdir=None, keep_dns_realm=False,
876 no_secrets=False, backend_store=None):
877 logger = self.get_logger()
878 logger.setLevel(logging.INFO)
880 lp = sambaopts.get_loadparm()
881 creds = credopts.get_credentials(lp)
883 # Make sure we have all the required args.
884 if server is None:
885 raise CommandError('Server required')
887 check_targetdir(logger, targetdir)
889 delete_old_dns = not keep_dns_realm
891 new_dns_realm = new_dns_realm.lower()
892 new_domain_name = new_domain_name.upper()
894 new_base_dn = samba.dn_from_dns_name(new_dns_realm)
895 logger.info("New realm for backed up domain: %s" % new_dns_realm)
896 logger.info("New base DN for backed up domain: %s" % new_base_dn)
897 logger.info("New domain NetBIOS name: %s" % new_domain_name)
899 tmpdir = tempfile.mkdtemp(dir=targetdir)
901 # setup a join-context for cloning the remote server
902 include_secrets = not no_secrets
903 ctx = DCCloneAndRenameContext(new_base_dn, new_domain_name,
904 new_dns_realm, logger=logger,
905 creds=creds, lp=lp,
906 include_secrets=include_secrets,
907 dns_backend='SAMBA_INTERNAL',
908 server=server, targetdir=tmpdir,
909 backend_store=backend_store)
911 # sanity-check we're not "renaming" the domain to the same values
912 old_domain = ctx.domain_name
913 if old_domain == new_domain_name:
914 shutil.rmtree(tmpdir)
915 raise CommandError("Cannot use the current domain NetBIOS name.")
917 old_realm = ctx.realm
918 if old_realm == new_dns_realm:
919 shutil.rmtree(tmpdir)
920 raise CommandError("Cannot use the current domain DNS realm.")
922 # do the clone/rename
923 ctx.do_join()
925 # get the paths used for the clone, then drop the old samdb connection
926 del ctx.local_samdb
927 paths = ctx.paths
929 # get a free RID to use as the new DC's SID (when it gets restored)
930 remote_sam = SamDB(url='ldap://' + server, credentials=creds,
931 session_info=system_session(), lp=lp)
932 new_sid = get_sid_for_restore(remote_sam, logger)
934 # Grab the remote DC's sysvol files and bundle them into a tar file.
935 # Note we end up with 2 sysvol dirs - the original domain's files (that
936 # use the old realm) backed here, as well as default files generated
937 # for the new realm as part of the clone/join.
938 sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
939 smb_conn = smb_sysvol_conn(server, lp, creds)
940 backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
942 # connect to the local DB (making sure we use the new/renamed config)
943 lp.load(paths.smbconf)
944 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp,
945 flags=ldb.FLG_DONT_CREATE_DB)
947 # Edit the cloned sam.ldb to mark it as a backup
948 time_str = get_timestamp()
949 add_backup_marker(samdb, "backupDate", time_str)
950 add_backup_marker(samdb, "sidForRestore", new_sid)
951 add_backup_marker(samdb, "backupRename", old_realm)
952 add_backup_marker(samdb, "backupType", "rename")
954 # fix up the DNS objects that are using the old dnsRoot value
955 self.update_dns_root(logger, samdb, old_realm, delete_old_dns)
957 # update the netBIOS name and the Partition object for the domain
958 self.rename_domain_partition(logger, samdb, new_domain_name)
960 if delete_old_dns:
961 self.delete_old_dns_zones(logger, samdb, old_realm)
963 logger.info("Fixing DN attributes after rename...")
964 self.fix_old_dn_attributes(samdb)
966 # ensure the admin user always has a password set (same as provision)
967 if no_secrets:
968 set_admin_password(logger, samdb)
970 # Add everything in the tmpdir to the backup tar file
971 backup_file = backup_filepath(targetdir, new_dns_realm, time_str)
972 create_log_file(tmpdir, lp, "rename", server, include_secrets,
973 "Original domain %s (NetBIOS), %s (DNS realm)" %
974 (old_domain, old_realm))
975 create_backup_tar(logger, tmpdir, backup_file)
977 shutil.rmtree(tmpdir)
980 class cmd_domain_backup_offline(samba.netcmd.Command):
981 '''Backup the local domain directories safely into a tar file.
983 Takes a backup copy of the current domain from the local files on disk,
984 with proper locking of the DB to ensure consistency. If the domain were to
985 undergo a catastrophic failure, then the backup file can be used to recover
986 the domain.
988 An offline backup differs to an online backup in the following ways:
989 - a backup can be created even if the DC isn't currently running.
990 - includes non-replicated attributes that an online backup wouldn't store.
991 - takes a copy of the raw database files, which has the risk that any
992 hidden problems in the DB are preserved in the backup.'''
994 synopsis = "%prog [options]"
995 takes_optiongroups = {
996 "sambaopts": options.SambaOptions,
999 takes_options = [
1000 Option("--targetdir",
1001 help="Output directory (required)",
1002 type=str),
1005 backup_ext = '.bak-offline'
1007 def offline_tdb_copy(self, path):
1008 backup_path = path + self.backup_ext
1009 try:
1010 tdb_copy(path, backup_path, readonly=True)
1011 except CalledProcessError as copy_err:
1012 # If the copy didn't work, check if it was caused by an EINVAL
1013 # error on opening the DB. If so, it's a mutex locked database,
1014 # which we can safely ignore.
1015 try:
1016 tdb.open(path)
1017 except Exception as e:
1018 if hasattr(e, 'errno') and e.errno == errno.EINVAL:
1019 return
1020 raise e
1021 raise copy_err
1022 if not os.path.exists(backup_path):
1023 s = "tdbbackup said backup succeeded but {0} not found"
1024 raise CommandError(s.format(backup_path))
1026 def offline_mdb_copy(self, path):
1027 mdb_copy(path, path + self.backup_ext)
1029 # Secrets databases are a special case: a transaction must be started
1030 # on the secrets.ldb file before backing up that file and secrets.tdb
1031 def backup_secrets(self, private_dir, lp, logger):
1032 secrets_path = os.path.join(private_dir, 'secrets')
1033 secrets_obj = Ldb(secrets_path + '.ldb', lp=lp,
1034 flags=ldb.FLG_DONT_CREATE_DB)
1035 logger.info('Starting transaction on ' + secrets_path)
1036 secrets_obj.transaction_start()
1037 self.offline_tdb_copy(secrets_path + '.ldb')
1038 self.offline_tdb_copy(secrets_path + '.tdb')
1039 secrets_obj.transaction_cancel()
1041 # sam.ldb must have a transaction started on it before backing up
1042 # everything in sam.ldb.d with the appropriate backup function.
1043 def backup_smb_dbs(self, private_dir, samdb, lp, logger):
1044 # First, determine if DB backend is MDB. Assume not unless there is a
1045 # 'backendStore' attribute on @PARTITION containing the text 'mdb'
1046 store_label = "backendStore"
1047 res = samdb.search(base="@PARTITION", scope=ldb.SCOPE_BASE,
1048 attrs=[store_label])
1049 mdb_backend = store_label in res[0] and str(res[0][store_label][0]) == 'mdb'
1051 sam_ldb_path = os.path.join(private_dir, 'sam.ldb')
1052 copy_function = None
1053 if mdb_backend:
1054 logger.info('MDB backend detected. Using mdb backup function.')
1055 copy_function = self.offline_mdb_copy
1056 else:
1057 logger.info('Starting transaction on ' + sam_ldb_path)
1058 copy_function = self.offline_tdb_copy
1059 sam_obj = Ldb(sam_ldb_path, lp=lp, flags=ldb.FLG_DONT_CREATE_DB)
1060 sam_obj.transaction_start()
1062 logger.info(' backing up ' + sam_ldb_path)
1063 self.offline_tdb_copy(sam_ldb_path)
1064 sam_ldb_d = sam_ldb_path + '.d'
1065 for sam_file in os.listdir(sam_ldb_d):
1066 sam_file = os.path.join(sam_ldb_d, sam_file)
1067 if sam_file.endswith('.ldb'):
1068 logger.info(' backing up locked/related file ' + sam_file)
1069 copy_function(sam_file)
1070 else:
1071 logger.info(' copying locked/related file ' + sam_file)
1072 shutil.copyfile(sam_file, sam_file + self.backup_ext)
1074 if not mdb_backend:
1075 sam_obj.transaction_cancel()
1077 # Find where a path should go in the fixed backup archive structure.
1078 def get_arc_path(self, path, conf_paths):
1079 backup_dirs = {"private": conf_paths.private_dir,
1080 "statedir": conf_paths.state_dir,
1081 "etc": os.path.dirname(conf_paths.smbconf)}
1082 matching_dirs = [(_, p) for (_, p) in backup_dirs.items() if
1083 path.startswith(p)]
1084 arc_path, fs_path = matching_dirs[0]
1086 # If more than one directory is a parent of this path, then at least
1087 # one configured path is a subdir of another. Use closest match.
1088 if len(matching_dirs) > 1:
1089 arc_path, fs_path = max(matching_dirs, key=lambda p: len(p[1]))
1090 arc_path += path[len(fs_path):]
1092 return arc_path
1094 def run(self, sambaopts=None, targetdir=None):
1096 logger = logging.getLogger()
1097 logger.setLevel(logging.DEBUG)
1098 logger.addHandler(logging.StreamHandler(sys.stdout))
1100 # Get the absolute paths of all the directories we're going to backup
1101 lp = sambaopts.get_loadparm()
1103 paths = samba.provision.provision_paths_from_lp(lp, lp.get('realm'))
1104 if not (paths.samdb and os.path.exists(paths.samdb)):
1105 logger.error("No database found at {0}".format(paths.samdb))
1106 raise CommandError('Please check you are root, and ' +
1107 'are running this command on an AD DC')
1109 check_targetdir(logger, targetdir)
1111 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp,
1112 flags=ldb.FLG_RDONLY)
1113 sid = get_sid_for_restore(samdb, logger)
1115 # Iterating over the directories in this specific order ensures that
1116 # when the private directory contains hardlinks that are also contained
1117 # in other directories to be backed up (such as in paths.binddns_dir),
1118 # the hardlinks in the private directory take precedence.
1119 backup_dirs = [paths.private_dir, paths.state_dir,
1120 os.path.dirname(paths.smbconf)] # etc dir
1121 logger.info('running backup on dirs: {0}'.format(' '.join(backup_dirs)))
1123 # Recursively get all file paths in the backup directories
1124 all_files = []
1125 for backup_dir in backup_dirs:
1126 for (working_dir, _, filenames) in os.walk(backup_dir):
1127 if working_dir.startswith(paths.sysvol):
1128 continue
1129 if working_dir.endswith('.sock') or '.sock/' in working_dir:
1130 continue
1131 # The BIND DNS database can be regenerated, so it doesn't need
1132 # to be backed up.
1133 if working_dir.startswith(os.path.join(paths.binddns_dir, 'dns')):
1134 continue
1136 for filename in filenames:
1137 full_path = os.path.join(working_dir, filename)
1139 # Ignore files that have already been added. This prevents
1140 # duplicates if one backup dir is a subdirectory of another,
1141 # or if backup dirs contain hardlinks.
1142 if any(os.path.samefile(full_path, file) for file in all_files):
1143 continue
1145 # Assume existing backup files are from a previous backup.
1146 # Delete and ignore.
1147 if filename.endswith(self.backup_ext):
1148 os.remove(full_path)
1149 continue
1151 # Sock files are autogenerated at runtime, ignore.
1152 if filename.endswith('.sock'):
1153 continue
1155 all_files.append(full_path)
1157 # Backup secrets, sam.ldb and their downstream files
1158 self.backup_secrets(paths.private_dir, lp, logger)
1159 self.backup_smb_dbs(paths.private_dir, samdb, lp, logger)
1161 # Get the domain SID so we can later place it in the backup
1162 dom_sid_str = samdb.get_domain_sid()
1163 dom_sid = security.dom_sid(dom_sid_str)
1165 # Close the original samdb
1166 samdb = None
1168 # Open the new backed up samdb, flag it as backed up, and write
1169 # the next SID so the restore tool can add objects. We use
1170 # options=["modules:"] here to prevent any modules from loading.
1171 # WARNING: Don't change this code unless you know what you're doing.
1172 # Writing to a .bak file only works because the DN being
1173 # written to happens to be top level.
1174 samdb = Ldb(url=paths.samdb + self.backup_ext,
1175 session_info=system_session(), lp=lp,
1176 options=["modules:"], flags=ldb.FLG_DONT_CREATE_DB)
1177 time_str = get_timestamp()
1178 add_backup_marker(samdb, "backupDate", time_str)
1179 add_backup_marker(samdb, "sidForRestore", sid)
1180 add_backup_marker(samdb, "backupType", "offline")
1182 # Close the backed up samdb
1183 samdb = None
1185 # Now handle all the LDB and TDB files that are not linked to
1186 # anything else. Use transactions for LDBs.
1187 for path in all_files:
1188 if not os.path.exists(path + self.backup_ext):
1189 if path.endswith('.ldb'):
1190 logger.info('Starting transaction on solo db: ' + path)
1191 ldb_obj = Ldb(path, lp=lp, flags=ldb.FLG_DONT_CREATE_DB)
1192 ldb_obj.transaction_start()
1193 logger.info(' running tdbbackup on the same file')
1194 self.offline_tdb_copy(path)
1195 ldb_obj.transaction_cancel()
1196 elif path.endswith('.tdb'):
1197 logger.info('running tdbbackup on lone tdb file ' + path)
1198 self.offline_tdb_copy(path)
1200 # Now make the backup tar file and add all
1201 # backed up files and any other files to it.
1202 temp_tar_dir = tempfile.mkdtemp(dir=targetdir,
1203 prefix='INCOMPLETEsambabackupfile')
1204 temp_tar_name = os.path.join(temp_tar_dir, "samba-backup.tar.bz2")
1205 tar = tarfile.open(temp_tar_name, 'w:bz2')
1207 logger.info('running offline ntacl backup of sysvol')
1208 sysvol_tar_fn = 'sysvol.tar.gz'
1209 sysvol_tar = os.path.join(temp_tar_dir, sysvol_tar_fn)
1210 backup_offline(paths.sysvol, sysvol_tar, paths.smbconf, dom_sid)
1211 tar.add(sysvol_tar, sysvol_tar_fn)
1212 os.remove(sysvol_tar)
1214 create_log_file(temp_tar_dir, lp, "offline", "localhost", True)
1215 backup_fn = os.path.join(temp_tar_dir, "backup.txt")
1216 tar.add(backup_fn, os.path.basename(backup_fn))
1217 os.remove(backup_fn)
1219 logger.info('building backup tar')
1220 for path in all_files:
1221 arc_path = self.get_arc_path(path, paths)
1223 if os.path.exists(path + self.backup_ext):
1224 logger.info(' adding backup ' + arc_path + self.backup_ext +
1225 ' to tar and deleting file')
1226 tar.add(path + self.backup_ext, arcname=arc_path)
1227 os.remove(path + self.backup_ext)
1228 elif path.endswith('.ldb') or path.endswith('.tdb'):
1229 logger.info(' skipping ' + arc_path)
1230 else:
1231 logger.info(' adding misc file ' + arc_path)
1232 tar.add(path, arcname=arc_path)
1234 tar.close()
1235 os.rename(temp_tar_name,
1236 os.path.join(targetdir,
1237 'samba-backup-{0}.tar.bz2'.format(time_str)))
1238 os.rmdir(temp_tar_dir)
1239 logger.info('Backup succeeded.')
1242 class cmd_domain_backup(samba.netcmd.SuperCommand):
1243 '''Create or restore a backup of the domain.'''
1244 subcommands = {'offline': cmd_domain_backup_offline(),
1245 'online': cmd_domain_backup_online(),
1246 'rename': cmd_domain_backup_rename(),
1247 'restore': cmd_domain_backup_restore()}