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/>.
26 import samba.getopt as options
27 from samba.samdb import SamDB
30 from samba.ntacls import backup_online, backup_restore
31 from samba.auth import system_session
32 from samba.join import DCJoinContext, join_clone
33 from samba.dcerpc.security import dom_sid
34 from samba.netcmd import Option, CommandError
35 from samba.dcerpc import misc
37 from fsmo import cmd_fsmo_seize
38 from samba.provision import make_smbconf
39 from samba.upgradehelpers import update_krbtgt_account_password
40 from samba.remove_dc import remove_dc
41 from samba.provision import secretsdb_self_join
45 # work out a SID (based on a free RID) to use when the domain gets restored.
46 # This ensures that the restored DC's SID won't clash with any other RIDs
47 # already in use in the domain
48 def get_sid_for_restore(samdb):
49 # Find the DN of the RID set of the server
50 res = samdb.search(base=ldb.Dn(samdb, samdb.get_serverName()),
51 scope=ldb.SCOPE_BASE, attrs=["serverReference"])
52 server_ref_dn = ldb.Dn(samdb, res[0]['serverReference'][0])
53 res = samdb.search(base=server_ref_dn,
55 attrs=['rIDSetReferences'])
56 rid_set_dn = ldb.Dn(samdb, res[0]['rIDSetReferences'][0])
58 # Get the alloc pools and next RID of the RID set
59 res = samdb.search(base=rid_set_dn,
60 scope=ldb.SCOPE_SUBTREE,
61 expression="(rIDNextRID=*)",
62 attrs=['rIDAllocationPool',
63 'rIDPreviousAllocationPool',
66 # Decode the bounds of the RID allocation pools
67 rid = int(res[0].get('rIDNextRID')[0])
70 high = (0xFFFFFFFF00000000 & int(num)) >> 32
71 low = 0x00000000FFFFFFFF & int(num)
73 pool_l, pool_h = split_val(res[0].get('rIDPreviousAllocationPool')[0])
74 npool_l, npool_h = split_val(res[0].get('rIDAllocationPool')[0])
76 # Calculate next RID based on pool bounds
78 raise CommandError('Out of RIDs, finished AllocPool')
81 raise CommandError('Out of RIDs, finished PrevAllocPool.')
87 sid = dom_sid(samdb.get_domain_sid())
88 return str(sid) + '-' + str(rid)
92 return datetime.datetime.now().isoformat().replace(':', '-')
95 def backup_filepath(targetdir, name, time_str):
96 filename = 'samba-backup-{}-{}.tar.bz2'.format(name, time_str)
97 return os.path.join(targetdir, filename)
100 def create_backup_tar(logger, tmpdir, backup_filepath):
101 # Adds everything in the tmpdir into a new tar file
102 logger.info("Creating backup file %s..." % backup_filepath)
103 tf = tarfile.open(backup_filepath, 'w:bz2')
104 tf.add(tmpdir, arcname='./')
108 # Add a backup-specific marker to the DB with info that we'll use during
109 # the restore process
110 def add_backup_marker(samdb, marker, value):
112 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
113 m[marker] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, marker)
117 def check_online_backup_args(logger, credopts, server, targetdir):
118 # Make sure we have all the required args.
119 u_p = {'user': credopts.creds.get_username(),
120 'pass': credopts.creds.get_password()}
121 if None in u_p.values():
122 raise CommandError("Creds required.")
124 raise CommandError('Server required')
125 if targetdir is None:
126 raise CommandError('Target directory required')
128 if not os.path.exists(targetdir):
129 logger.info('Creating targetdir %s...' % targetdir)
130 os.makedirs(targetdir)
133 class cmd_domain_backup_online(samba.netcmd.Command):
134 '''Copy a running DC's current DB into a backup tar file.
136 Takes a backup copy of the current domain from a running DC. If the domain
137 were to undergo a catastrophic failure, then the backup file can be used to
138 recover the domain. The backup created is similar to the DB that a new DC
139 would receive when it joins the domain.
142 - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
143 and fix any errors it reports.
144 - all the domain's secrets are included in the backup file.
145 - although the DB contents can be untarred and examined manually, you need
146 to run 'samba-tool domain backup restore' before you can start a Samba DC
147 from the backup file.'''
149 synopsis = "%prog --server=<DC-to-backup> --targetdir=<output-dir>"
150 takes_optiongroups = {
151 "sambaopts": options.SambaOptions,
152 "credopts": options.CredentialsOptions,
156 Option("--server", help="The DC to backup", type=str),
157 Option("--targetdir", type=str,
158 help="Directory to write the backup file to"),
161 def run(self, sambaopts=None, credopts=None, server=None, targetdir=None):
162 logger = self.get_logger()
163 logger.setLevel(logging.DEBUG)
165 # Make sure we have all the required args.
166 check_online_backup_args(logger, credopts, server, targetdir)
168 lp = sambaopts.get_loadparm()
169 creds = credopts.get_credentials(lp)
171 if not os.path.exists(targetdir):
172 logger.info('Creating targetdir %s...' % targetdir)
173 os.makedirs(targetdir)
175 tmpdir = tempfile.mkdtemp(dir=targetdir)
177 # Run a clone join on the remote
178 ctx = join_clone(logger=logger, creds=creds, lp=lp,
179 include_secrets=True, dns_backend='SAMBA_INTERNAL',
180 server=server, targetdir=tmpdir)
182 # get the paths used for the clone, then drop the old samdb connection
186 # Get a free RID to use as the new DC's SID (when it gets restored)
187 remote_sam = SamDB(url='ldap://' + server, credentials=creds,
188 session_info=system_session(), lp=lp)
189 new_sid = get_sid_for_restore(remote_sam)
190 realm = remote_sam.domain_dns_name()
192 # Grab the remote DC's sysvol files and bundle them into a tar file
193 sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
194 smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
195 backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
197 # remove the default sysvol files created by the clone (we want to
198 # make sure we restore the sysvol.tar.gz files instead)
199 shutil.rmtree(paths.sysvol)
201 # Edit the downloaded sam.ldb to mark it as a backup
202 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
203 time_str = get_timestamp()
204 add_backup_marker(samdb, "backupDate", time_str)
205 add_backup_marker(samdb, "sidForRestore", new_sid)
207 # Add everything in the tmpdir to the backup tar file
208 backup_file = backup_filepath(targetdir, realm, time_str)
209 create_backup_tar(logger, tmpdir, backup_file)
211 shutil.rmtree(tmpdir)
214 class cmd_domain_backup_restore(cmd_fsmo_seize):
215 '''Restore the domain's DB from a backup-file.
217 This restores a previously backed up copy of the domain's DB on a new DC.
219 Note that the restored DB will not contain the original DC that the backup
220 was taken from (or any other DCs in the original domain). Only the new DC
221 (specified by --newservername) will be present in the restored DB.
223 Samba can then be started against the restored DB. Any existing DCs for the
224 domain should be shutdown before the new DC is started. Other DCs can then
225 be joined to the new DC to recover the network.
227 Note that this command should be run as the root user - it will fail
230 synopsis = ("%prog --backup-file=<tar-file> --targetdir=<output-dir> "
231 "--newservername=<DC-name>")
233 Option("--backup-file", help="Path to backup file", type=str),
234 Option("--targetdir", help="Path to write to", type=str),
235 Option("--newservername", help="Name for new server", type=str),
238 takes_optiongroups = {
239 "sambaopts": options.SambaOptions,
240 "credopts": options.CredentialsOptions,
243 def run(self, sambaopts=None, credopts=None, backup_file=None,
244 targetdir=None, newservername=None):
245 if not (backup_file and os.path.exists(backup_file)):
246 raise CommandError('Backup file not found.')
247 if targetdir is None:
248 raise CommandError('Please specify a target directory')
249 if os.path.exists(targetdir) and os.listdir(targetdir):
250 raise CommandError('Target directory is not empty')
251 if not newservername:
252 raise CommandError('Server name required')
254 logger = logging.getLogger()
255 logger.setLevel(logging.DEBUG)
256 logger.addHandler(logging.StreamHandler(sys.stdout))
258 # ldapcmp prefers the server's netBIOS name in upper-case
259 newservername = newservername.upper()
261 # extract the backup .tar to a temp directory
262 targetdir = os.path.abspath(targetdir)
263 tf = tarfile.open(backup_file)
264 tf.extractall(targetdir)
267 # use the smb.conf that got backed up, by default (save what was
268 # actually backed up, before we mess with it)
269 smbconf = os.path.join(targetdir, 'etc', 'smb.conf')
270 shutil.copyfile(smbconf, smbconf + ".orig")
272 # if a smb.conf was specified on the cmd line, then use that instead
273 cli_smbconf = sambaopts.get_loadparm_path()
275 logger.info("Using %s as restored domain's smb.conf" % cli_smbconf)
276 shutil.copyfile(cli_smbconf, smbconf)
278 lp = samba.param.LoadParm()
281 # open a DB connection to the restored DB
282 private_dir = os.path.join(targetdir, 'private')
283 samdb_path = os.path.join(private_dir, 'sam.ldb')
284 samdb = SamDB(url=samdb_path, session_info=system_session(), lp=lp)
286 # Create account using the join_add_objects function in the join object
287 # We need namingContexts, account control flags, and the sid saved by
288 # the backup process.
289 res = samdb.search(base="", scope=ldb.SCOPE_BASE,
290 attrs=['namingContexts'])
291 ncs = [str(r) for r in res[0].get('namingContexts')]
293 creds = credopts.get_credentials(lp)
294 ctx = DCJoinContext(logger, creds=creds, lp=lp,
295 forced_local_samdb=samdb,
296 netbios_name=newservername)
298 ctx.full_nc_list = ncs
299 ctx.userAccountControl = (samba.dsdb.UF_SERVER_TRUST_ACCOUNT |
300 samba.dsdb.UF_TRUSTED_FOR_DELEGATION)
302 # rewrite the smb.conf to make sure it uses the new targetdir settings.
303 # (This doesn't update all filepaths in a customized config, but it
304 # corrects the same paths that get set by a new provision)
305 logger.info('Updating basic smb.conf settings...')
306 make_smbconf(smbconf, newservername, ctx.domain_name,
307 ctx.realm, targetdir, lp=lp,
308 serverrole="active directory domain controller")
310 # Get the SID saved by the backup process and create account
311 res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
312 scope=ldb.SCOPE_BASE,
313 attrs=['sidForRestore'])
314 sid = res[0].get('sidForRestore')[0]
315 logger.info('Creating account with SID: ' + str(sid))
316 ctx.join_add_objects(specified_sid=dom_sid(sid))
319 m.dn = ldb.Dn(samdb, '@ROOTDSE')
320 ntds_guid = str(ctx.ntds_guid)
321 m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % ntds_guid,
322 ldb.FLAG_MOD_REPLACE,
326 secrets_path = os.path.join(private_dir, 'secrets.ldb')
327 secrets_ldb = Ldb(secrets_path, session_info=system_session(), lp=lp)
328 secretsdb_self_join(secrets_ldb, domain=ctx.domain_name,
329 realm=ctx.realm, dnsdomain=ctx.dnsdomain,
330 netbiosname=ctx.myname, domainsid=ctx.domsid,
331 machinepass=ctx.acct_pass,
332 key_version_number=ctx.key_version_number,
333 secure_channel_type=misc.SEC_CHAN_BDC)
336 domain_dn = samdb.domain_dn()
337 forest_dn = samba.dn_from_dns_name(samdb.forest_dns_name())
338 domaindns_dn = ("CN=Infrastructure,DC=DomainDnsZones,", domain_dn)
339 forestdns_dn = ("CN=Infrastructure,DC=ForestDnsZones,", forest_dn)
340 for dn_prefix, dns_dn in [forestdns_dn, domaindns_dn]:
341 if dns_dn not in ncs:
343 full_dn = dn_prefix + dns_dn
345 m.dn = ldb.Dn(samdb, full_dn)
346 m["fSMORoleOwner"] = ldb.MessageElement(samdb.get_dsServiceName(),
347 ldb.FLAG_MOD_REPLACE,
352 for role in ['rid', 'pdc', 'naming', 'infrastructure', 'schema']:
353 self.seize_role(role, samdb, force=True)
355 # Get all DCs and remove them (this ensures these DCs cannot
356 # replicate because they will not have a password)
357 search_expr = "(&(objectClass=Server)(serverReference=*))"
358 res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
359 expression=search_expr)
362 if cn != newservername:
363 remove_dc(samdb, logger, cn)
365 # Remove the repsFrom and repsTo from each NC to ensure we do
366 # not try (and fail) to talk to the old DCs
369 msg.dn = ldb.Dn(samdb, nc)
371 msg["repsFrom"] = ldb.MessageElement([],
372 ldb.FLAG_MOD_REPLACE,
374 msg["repsTo"] = ldb.MessageElement([],
375 ldb.FLAG_MOD_REPLACE,
379 # Update the krbtgt passwords twice, ensuring no tickets from
380 # the old domain are valid
381 update_krbtgt_account_password(samdb)
382 update_krbtgt_account_password(samdb)
384 # restore the sysvol directory from the backup tar file, including the
385 # original NTACLs. Note that the backup_restore() will fail if not root
386 sysvol_tar = os.path.join(targetdir, 'sysvol.tar.gz')
387 dest_sysvol_dir = lp.get('path', 'sysvol')
388 if not os.path.exists(dest_sysvol_dir):
389 os.makedirs(dest_sysvol_dir)
390 backup_restore(sysvol_tar, dest_sysvol_dir, samdb, smbconf)
391 os.remove(sysvol_tar)
393 # Remove DB markers added by the backup process
395 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
396 m["backupDate"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
398 m["sidForRestore"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
402 logger.info("Backup file successfully restored to %s" % targetdir)
403 logger.info("Please check the smb.conf settings are correct before "
407 class cmd_domain_backup(samba.netcmd.SuperCommand):
408 '''Create or restore a backup of the domain.'''
409 subcommands = {'online': cmd_domain_backup_online(),
410 'restore': cmd_domain_backup_restore()}