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, DCCloneAndRenameContext
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
42 from samba.dbchecker import dbcheck
44 from samba.provision import guess_names, determine_host_ip, determine_host_ip6
45 from samba.provision.sambadns import (fill_dns_data_partitions,
50 # work out a SID (based on a free RID) to use when the domain gets restored.
51 # This ensures that the restored DC's SID won't clash with any other RIDs
52 # already in use in the domain
53 def get_sid_for_restore(samdb):
54 # Find the DN of the RID set of the server
55 res = samdb.search(base=ldb.Dn(samdb, samdb.get_serverName()),
56 scope=ldb.SCOPE_BASE, attrs=["serverReference"])
57 server_ref_dn = ldb.Dn(samdb, res[0]['serverReference'][0])
58 res = samdb.search(base=server_ref_dn,
60 attrs=['rIDSetReferences'])
61 rid_set_dn = ldb.Dn(samdb, res[0]['rIDSetReferences'][0])
63 # Get the alloc pools and next RID of the RID set
64 res = samdb.search(base=rid_set_dn,
65 scope=ldb.SCOPE_SUBTREE,
66 expression="(rIDNextRID=*)",
67 attrs=['rIDAllocationPool',
68 'rIDPreviousAllocationPool',
71 # Decode the bounds of the RID allocation pools
72 rid = int(res[0].get('rIDNextRID')[0])
75 high = (0xFFFFFFFF00000000 & int(num)) >> 32
76 low = 0x00000000FFFFFFFF & int(num)
78 pool_l, pool_h = split_val(res[0].get('rIDPreviousAllocationPool')[0])
79 npool_l, npool_h = split_val(res[0].get('rIDAllocationPool')[0])
81 # Calculate next RID based on pool bounds
83 raise CommandError('Out of RIDs, finished AllocPool')
86 raise CommandError('Out of RIDs, finished PrevAllocPool.')
92 sid = dom_sid(samdb.get_domain_sid())
93 return str(sid) + '-' + str(rid)
97 return datetime.datetime.now().isoformat().replace(':', '-')
100 def backup_filepath(targetdir, name, time_str):
101 filename = 'samba-backup-{}-{}.tar.bz2'.format(name, time_str)
102 return os.path.join(targetdir, filename)
105 def create_backup_tar(logger, tmpdir, backup_filepath):
106 # Adds everything in the tmpdir into a new tar file
107 logger.info("Creating backup file %s..." % backup_filepath)
108 tf = tarfile.open(backup_filepath, 'w:bz2')
109 tf.add(tmpdir, arcname='./')
113 def create_log_file(targetdir, lp, backup_type, server, include_secrets,
115 # create a summary file about the backup, which will get included in the
116 # tar file. This makes it easy for users to see what the backup involved,
117 # without having to untar the DB and interrogate it
118 f = open(os.path.join(targetdir, "backup.txt"), 'w')
120 time_str = datetime.datetime.now().strftime('%Y-%b-%d %H:%M:%S')
121 f.write("Backup created %s\n" % time_str)
122 f.write("Using samba-tool version: %s\n" % lp.get('server string'))
123 f.write("Domain %s backup, using DC '%s'\n" % (backup_type, server))
124 f.write("Backup for domain %s (NetBIOS), %s (DNS realm)\n" %
125 (lp.get('workgroup'), lp.get('realm').lower()))
126 f.write("Backup contains domain secrets: %s\n" % str(include_secrets))
128 f.write("%s\n" % extra_info)
133 # Add a backup-specific marker to the DB with info that we'll use during
134 # the restore process
135 def add_backup_marker(samdb, marker, value):
137 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
138 m[marker] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, marker)
142 def check_targetdir(logger, targetdir):
143 if targetdir is None:
144 raise CommandError('Target directory required')
146 if not os.path.exists(targetdir):
147 logger.info('Creating targetdir %s...' % targetdir)
148 os.makedirs(targetdir)
149 elif not os.path.isdir(targetdir):
150 raise CommandError("%s is not a directory" % targetdir)
153 def check_online_backup_args(logger, credopts, server, targetdir):
154 # Make sure we have all the required args.
155 u_p = {'user': credopts.creds.get_username(),
156 'pass': credopts.creds.get_password()}
157 if None in u_p.values():
158 raise CommandError("Creds required.")
160 raise CommandError('Server required')
162 check_targetdir(logger, targetdir)
165 # For '--no-secrets' backups, this sets the Administrator user's password to a
166 # randomly-generated value. This is similar to the provision behaviour
167 def set_admin_password(logger, samdb, username):
168 """Sets a randomly generated password for the backup DB's admin user"""
170 adminpass = samba.generate_random_password(12, 32)
171 logger.info("Setting %s password in backup to: %s" % (username, adminpass))
172 logger.info("Run 'samba-tool user setpassword %s' after restoring DB" %
174 samdb.setpassword("(&(objectClass=user)(sAMAccountName=%s))"
175 % ldb.binary_encode(username), adminpass,
176 force_change_at_next_login=False,
180 class cmd_domain_backup_online(samba.netcmd.Command):
181 '''Copy a running DC's current DB into a backup tar file.
183 Takes a backup copy of the current domain from a running DC. If the domain
184 were to undergo a catastrophic failure, then the backup file can be used to
185 recover the domain. The backup created is similar to the DB that a new DC
186 would receive when it joins the domain.
189 - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
190 and fix any errors it reports.
191 - all the domain's secrets are included in the backup file.
192 - although the DB contents can be untarred and examined manually, you need
193 to run 'samba-tool domain backup restore' before you can start a Samba DC
194 from the backup file.'''
196 synopsis = "%prog --server=<DC-to-backup> --targetdir=<output-dir>"
197 takes_optiongroups = {
198 "sambaopts": options.SambaOptions,
199 "credopts": options.CredentialsOptions,
203 Option("--server", help="The DC to backup", type=str),
204 Option("--targetdir", type=str,
205 help="Directory to write the backup file to"),
206 Option("--no-secrets", action="store_true", default=False,
207 help="Exclude secret values from the backup created")
210 def run(self, sambaopts=None, credopts=None, server=None, targetdir=None,
212 logger = self.get_logger()
213 logger.setLevel(logging.DEBUG)
215 # Make sure we have all the required args.
216 check_online_backup_args(logger, credopts, server, targetdir)
218 lp = sambaopts.get_loadparm()
219 creds = credopts.get_credentials(lp)
221 tmpdir = tempfile.mkdtemp(dir=targetdir)
223 # Run a clone join on the remote
224 include_secrets = not no_secrets
225 ctx = join_clone(logger=logger, creds=creds, lp=lp,
226 include_secrets=include_secrets, server=server,
227 dns_backend='SAMBA_INTERNAL', targetdir=tmpdir)
229 # get the paths used for the clone, then drop the old samdb connection
233 # Get a free RID to use as the new DC's SID (when it gets restored)
234 remote_sam = SamDB(url='ldap://' + server, credentials=creds,
235 session_info=system_session(), lp=lp)
236 new_sid = get_sid_for_restore(remote_sam)
237 realm = remote_sam.domain_dns_name()
239 # Grab the remote DC's sysvol files and bundle them into a tar file
240 sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
241 smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
242 backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
244 # remove the default sysvol files created by the clone (we want to
245 # make sure we restore the sysvol.tar.gz files instead)
246 shutil.rmtree(paths.sysvol)
248 # Edit the downloaded sam.ldb to mark it as a backup
249 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
250 time_str = get_timestamp()
251 add_backup_marker(samdb, "backupDate", time_str)
252 add_backup_marker(samdb, "sidForRestore", new_sid)
254 # ensure the admin user always has a password set (same as provision)
256 set_admin_password(logger, samdb, creds.get_username())
258 # Add everything in the tmpdir to the backup tar file
259 backup_file = backup_filepath(targetdir, realm, time_str)
260 create_log_file(tmpdir, lp, "online", server, include_secrets)
261 create_backup_tar(logger, tmpdir, backup_file)
263 shutil.rmtree(tmpdir)
266 class cmd_domain_backup_restore(cmd_fsmo_seize):
267 '''Restore the domain's DB from a backup-file.
269 This restores a previously backed up copy of the domain's DB on a new DC.
271 Note that the restored DB will not contain the original DC that the backup
272 was taken from (or any other DCs in the original domain). Only the new DC
273 (specified by --newservername) will be present in the restored DB.
275 Samba can then be started against the restored DB. Any existing DCs for the
276 domain should be shutdown before the new DC is started. Other DCs can then
277 be joined to the new DC to recover the network.
279 Note that this command should be run as the root user - it will fail
282 synopsis = ("%prog --backup-file=<tar-file> --targetdir=<output-dir> "
283 "--newservername=<DC-name>")
285 Option("--backup-file", help="Path to backup file", type=str),
286 Option("--targetdir", help="Path to write to", type=str),
287 Option("--newservername", help="Name for new server", type=str),
288 Option("--host-ip", type="string", metavar="IPADDRESS",
289 help="set IPv4 ipaddress"),
290 Option("--host-ip6", type="string", metavar="IP6ADDRESS",
291 help="set IPv6 ipaddress"),
294 takes_optiongroups = {
295 "sambaopts": options.SambaOptions,
296 "credopts": options.CredentialsOptions,
299 def register_dns_zone(self, logger, samdb, lp, ntdsguid, host_ip,
302 Registers the new realm's DNS objects when a renamed domain backup
305 names = guess_names(lp)
306 domaindn = names.domaindn
307 forestdn = samdb.get_root_basedn().get_linearized()
308 dnsdomain = names.dnsdomain.lower()
309 dnsforest = dnsdomain
310 hostname = names.netbiosname.lower()
311 domainsid = dom_sid(samdb.get_domain_sid())
312 dnsadmins_sid = get_dnsadmins_sid(samdb, domaindn)
313 domainguid = get_domainguid(samdb, domaindn)
315 # work out the IP address to use for the new DC's DNS records
316 host_ip = determine_host_ip(logger, lp, host_ip)
317 host_ip6 = determine_host_ip6(logger, lp, host_ip6)
319 if host_ip is None and host_ip6 is None:
320 raise CommandError('Please specify a host-ip for the new server')
322 logger.info("DNS realm was renamed to %s" % dnsdomain)
323 logger.info("Populating DNS partitions for new realm...")
325 # Add the DNS objects for the new realm (note: the backup clone already
326 # has the root server objects, so don't add them again)
327 fill_dns_data_partitions(samdb, domainsid, names.sitename, domaindn,
328 forestdn, dnsdomain, dnsforest, hostname,
329 host_ip, host_ip6, domainguid, ntdsguid,
330 dnsadmins_sid, add_root=False)
332 def fix_old_dc_references(self, samdb):
333 '''Fixes attributes that reference the old/removed DCs'''
335 # we just want to fix up DB problems here that were introduced by us
336 # removing the old DCs. We restrict what we fix up so that the restored
337 # DB matches the backed-up DB as close as possible. (There may be other
338 # DB issues inherited from the backed-up DC, but it's not our place to
339 # silently try to fix them here).
340 samdb.transaction_start()
341 chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
344 # fix up stale references to the old DC
345 setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
346 attrs = ['lastKnownParent', 'interSiteTopologyGenerator']
348 # fix-up stale one-way links that point to the old DC
349 setattr(chk, 'remove_plausible_deleted_DN_links', 'ALL')
350 attrs += ['msDS-NC-Replica-Locations']
352 cross_ncs_ctrl = 'search_options:1:2'
353 controls = ['show_deleted:1', cross_ncs_ctrl]
354 chk.check_database(controls=controls, attrs=attrs)
355 samdb.transaction_commit()
357 def run(self, sambaopts=None, credopts=None, backup_file=None,
358 targetdir=None, newservername=None, host_ip=None, host_ip6=None):
359 if not (backup_file and os.path.exists(backup_file)):
360 raise CommandError('Backup file not found.')
361 if targetdir is None:
362 raise CommandError('Please specify a target directory')
363 # allow restoredc to install into a directory prepopulated by selftest
364 if (os.path.exists(targetdir) and os.listdir(targetdir) and
365 os.environ.get('SAMBA_SELFTEST') != '1'):
366 raise CommandError('Target directory is not empty')
367 if not newservername:
368 raise CommandError('Server name required')
370 logger = logging.getLogger()
371 logger.setLevel(logging.DEBUG)
372 logger.addHandler(logging.StreamHandler(sys.stdout))
374 # ldapcmp prefers the server's netBIOS name in upper-case
375 newservername = newservername.upper()
377 # extract the backup .tar to a temp directory
378 targetdir = os.path.abspath(targetdir)
379 tf = tarfile.open(backup_file)
380 tf.extractall(targetdir)
383 # use the smb.conf that got backed up, by default (save what was
384 # actually backed up, before we mess with it)
385 smbconf = os.path.join(targetdir, 'etc', 'smb.conf')
386 shutil.copyfile(smbconf, smbconf + ".orig")
388 # if a smb.conf was specified on the cmd line, then use that instead
389 cli_smbconf = sambaopts.get_loadparm_path()
391 logger.info("Using %s as restored domain's smb.conf" % cli_smbconf)
392 shutil.copyfile(cli_smbconf, smbconf)
394 lp = samba.param.LoadParm()
397 # open a DB connection to the restored DB
398 private_dir = os.path.join(targetdir, 'private')
399 samdb_path = os.path.join(private_dir, 'sam.ldb')
400 samdb = SamDB(url=samdb_path, session_info=system_session(), lp=lp)
402 # Create account using the join_add_objects function in the join object
403 # We need namingContexts, account control flags, and the sid saved by
404 # the backup process.
405 res = samdb.search(base="", scope=ldb.SCOPE_BASE,
406 attrs=['namingContexts'])
407 ncs = [str(r) for r in res[0].get('namingContexts')]
409 creds = credopts.get_credentials(lp)
410 ctx = DCJoinContext(logger, creds=creds, lp=lp,
411 forced_local_samdb=samdb,
412 netbios_name=newservername)
414 ctx.full_nc_list = ncs
415 ctx.userAccountControl = (samba.dsdb.UF_SERVER_TRUST_ACCOUNT |
416 samba.dsdb.UF_TRUSTED_FOR_DELEGATION)
418 # rewrite the smb.conf to make sure it uses the new targetdir settings.
419 # (This doesn't update all filepaths in a customized config, but it
420 # corrects the same paths that get set by a new provision)
421 logger.info('Updating basic smb.conf settings...')
422 make_smbconf(smbconf, newservername, ctx.domain_name,
423 ctx.realm, targetdir, lp=lp,
424 serverrole="active directory domain controller")
426 # Get the SID saved by the backup process and create account
427 res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
428 scope=ldb.SCOPE_BASE,
429 attrs=['sidForRestore', 'backupRename'])
430 is_rename = True if 'backupRename' in res[0] else False
431 sid = res[0].get('sidForRestore')[0]
432 logger.info('Creating account with SID: ' + str(sid))
433 ctx.join_add_objects(specified_sid=dom_sid(sid))
436 m.dn = ldb.Dn(samdb, '@ROOTDSE')
437 ntds_guid = str(ctx.ntds_guid)
438 m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % ntds_guid,
439 ldb.FLAG_MOD_REPLACE,
443 # if we renamed the backed-up domain, then we need to add the DNS
444 # objects for the new realm (we do this in the restore, now that we
445 # know the new DC's IP address)
447 self.register_dns_zone(logger, samdb, lp, ctx.ntds_guid,
450 secrets_path = os.path.join(private_dir, 'secrets.ldb')
451 secrets_ldb = Ldb(secrets_path, session_info=system_session(), lp=lp)
452 secretsdb_self_join(secrets_ldb, domain=ctx.domain_name,
453 realm=ctx.realm, dnsdomain=ctx.dnsdomain,
454 netbiosname=ctx.myname, domainsid=ctx.domsid,
455 machinepass=ctx.acct_pass,
456 key_version_number=ctx.key_version_number,
457 secure_channel_type=misc.SEC_CHAN_BDC)
460 domain_dn = samdb.domain_dn()
461 forest_dn = samba.dn_from_dns_name(samdb.forest_dns_name())
462 domaindns_dn = ("CN=Infrastructure,DC=DomainDnsZones,", domain_dn)
463 forestdns_dn = ("CN=Infrastructure,DC=ForestDnsZones,", forest_dn)
464 for dn_prefix, dns_dn in [forestdns_dn, domaindns_dn]:
465 if dns_dn not in ncs:
467 full_dn = dn_prefix + dns_dn
469 m.dn = ldb.Dn(samdb, full_dn)
470 m["fSMORoleOwner"] = ldb.MessageElement(samdb.get_dsServiceName(),
471 ldb.FLAG_MOD_REPLACE,
476 for role in ['rid', 'pdc', 'naming', 'infrastructure', 'schema']:
477 self.seize_role(role, samdb, force=True)
479 # Get all DCs and remove them (this ensures these DCs cannot
480 # replicate because they will not have a password)
481 search_expr = "(&(objectClass=Server)(serverReference=*))"
482 res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
483 expression=search_expr)
486 if cn != newservername:
487 remove_dc(samdb, logger, cn)
489 # Remove the repsFrom and repsTo from each NC to ensure we do
490 # not try (and fail) to talk to the old DCs
493 msg.dn = ldb.Dn(samdb, nc)
495 msg["repsFrom"] = ldb.MessageElement([],
496 ldb.FLAG_MOD_REPLACE,
498 msg["repsTo"] = ldb.MessageElement([],
499 ldb.FLAG_MOD_REPLACE,
503 # Update the krbtgt passwords twice, ensuring no tickets from
504 # the old domain are valid
505 update_krbtgt_account_password(samdb)
506 update_krbtgt_account_password(samdb)
508 # restore the sysvol directory from the backup tar file, including the
509 # original NTACLs. Note that the backup_restore() will fail if not root
510 sysvol_tar = os.path.join(targetdir, 'sysvol.tar.gz')
511 dest_sysvol_dir = lp.get('path', 'sysvol')
512 if not os.path.exists(dest_sysvol_dir):
513 os.makedirs(dest_sysvol_dir)
514 backup_restore(sysvol_tar, dest_sysvol_dir, samdb, smbconf)
515 os.remove(sysvol_tar)
517 # fix up any stale links to the old DCs we just removed
518 logger.info("Fixing up any remaining references to the old DCs...")
519 self.fix_old_dc_references(samdb)
521 # Remove DB markers added by the backup process
523 m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
524 m["backupDate"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
526 m["sidForRestore"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
529 m["backupRename"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
533 logger.info("Backup file successfully restored to %s" % targetdir)
534 logger.info("Please check the smb.conf settings are correct before "
538 class cmd_domain_backup_rename(samba.netcmd.Command):
539 '''Copy a running DC's DB to backup file, renaming the domain in the process.
541 Where <new-domain> is the new domain's NetBIOS name, and <new-dnsrealm> is
542 the new domain's realm in DNS form.
544 This is similar to 'samba-tool backup online' in that it clones the DB of a
545 running DC. However, this option also renames all the domain entries in the
546 DB. Renaming the domain makes it possible to restore and start a new Samba
547 DC without it interfering with the existing Samba domain. In other words,
548 you could use this option to clone your production samba domain and restore
549 it to a separate pre-production environment that won't overlap or interfere
550 with the existing production Samba domain.
553 - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
554 and fix any errors it reports.
555 - all the domain's secrets are included in the backup file.
556 - although the DB contents can be untarred and examined manually, you need
557 to run 'samba-tool domain backup restore' before you can start a Samba DC
558 from the backup file.
559 - GPO and sysvol information will still refer to the old realm and will
560 need to be updated manually.
561 - if you specify 'keep-dns-realm', then the DNS records will need updating
562 in order to work (they will still refer to the old DC's IP instead of the
564 - we recommend that you only use this option if you know what you're doing.
567 synopsis = ("%prog <new-domain> <new-dnsrealm> --server=<DC-to-backup> "
568 "--targetdir=<output-dir>")
569 takes_optiongroups = {
570 "sambaopts": options.SambaOptions,
571 "credopts": options.CredentialsOptions,
575 Option("--server", help="The DC to backup", type=str),
576 Option("--targetdir", help="Directory to write the backup file",
578 Option("--keep-dns-realm", action="store_true", default=False,
579 help="Retain the DNS entries for the old realm in the backup"),
580 Option("--no-secrets", action="store_true", default=False,
581 help="Exclude secret values from the backup created")
584 takes_args = ["new_domain_name", "new_dns_realm"]
586 def update_dns_root(self, logger, samdb, old_realm, delete_old_dns):
587 '''Updates dnsRoot for the partition objects to reflect the rename'''
589 # lookup the crossRef objects that hold the old realm's dnsRoot
590 partitions_dn = samdb.get_partitions_dn()
591 res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
593 expression='(&(objectClass=crossRef)(dnsRoot=*))')
594 new_realm = samdb.domain_dns_name()
596 # go through and add the new realm
598 # dnsRoot can be multi-valued, so only look for the old realm
599 for dns_root in res_msg["dnsRoot"]:
601 if old_realm in dns_root:
602 new_dns_root = re.sub('%s$' % old_realm, new_realm,
604 logger.info("Adding %s dnsRoot to %s" % (new_dns_root, dn))
608 m["dnsRoot"] = ldb.MessageElement(new_dns_root,
613 # optionally remove the dnsRoot for the old realm
615 logger.info("Removing %s dnsRoot from %s" % (dns_root,
617 m["dnsRoot"] = ldb.MessageElement(dns_root,
622 # Updates the CN=<domain>,CN=Partitions,CN=Configuration,... object to
623 # reflect the domain rename
624 def rename_domain_partition(self, logger, samdb, new_netbios_name):
625 '''Renames the domain parition object and updates its nETBIOSName'''
627 # lookup the crossRef object that holds the nETBIOSName (nCName has
628 # already been updated by this point, but the netBIOS hasn't)
629 base_dn = samdb.get_default_basedn()
630 nc_name = ldb.binary_encode(str(base_dn))
631 partitions_dn = samdb.get_partitions_dn()
632 res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
633 attrs=["nETBIOSName"],
634 expression='ncName=%s' % nc_name)
636 logger.info("Changing backup domain's NetBIOS name to %s" %
640 m["nETBIOSName"] = ldb.MessageElement(new_netbios_name,
641 ldb.FLAG_MOD_REPLACE,
645 # renames the object itself to reflect the change in domain
646 new_dn = "CN=%s,%s" % (new_netbios_name, partitions_dn)
647 logger.info("Renaming %s --> %s" % (res[0].dn, new_dn))
648 samdb.rename(res[0].dn, new_dn, controls=['relax:0'])
650 def delete_old_dns_zones(self, logger, samdb, old_realm):
651 # remove the top-level DNS entries for the old realm
652 basedn = samdb.get_default_basedn()
653 dn = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (old_realm, basedn)
654 logger.info("Deleting old DNS zone %s" % dn)
655 samdb.delete(dn, ["tree_delete:1"])
657 forestdn = samdb.get_root_basedn().get_linearized()
658 dn = "DC=_msdcs.%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (old_realm,
660 logger.info("Deleting old DNS zone %s" % dn)
661 samdb.delete(dn, ["tree_delete:1"])
663 def fix_old_dn_attributes(self, samdb):
664 '''Fixes attributes (i.e. objectCategory) that still use the old DN'''
666 samdb.transaction_start()
667 # Just fix any mismatches in DN detected (leave any other errors)
668 chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
670 # fix up incorrect objectCategory/etc attributes
671 setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
672 cross_ncs_ctrl = 'search_options:1:2'
673 controls = ['show_deleted:1', cross_ncs_ctrl]
674 chk.check_database(controls=controls)
675 samdb.transaction_commit()
677 def run(self, new_domain_name, new_dns_realm, sambaopts=None,
678 credopts=None, server=None, targetdir=None, keep_dns_realm=False,
680 logger = self.get_logger()
681 logger.setLevel(logging.INFO)
683 # Make sure we have all the required args.
684 check_online_backup_args(logger, credopts, server, targetdir)
685 delete_old_dns = not keep_dns_realm
687 new_dns_realm = new_dns_realm.lower()
688 new_domain_name = new_domain_name.upper()
690 new_base_dn = samba.dn_from_dns_name(new_dns_realm)
691 logger.info("New realm for backed up domain: %s" % new_dns_realm)
692 logger.info("New base DN for backed up domain: %s" % new_base_dn)
693 logger.info("New domain NetBIOS name: %s" % new_domain_name)
695 tmpdir = tempfile.mkdtemp(dir=targetdir)
697 # setup a join-context for cloning the remote server
698 lp = sambaopts.get_loadparm()
699 creds = credopts.get_credentials(lp)
700 include_secrets = not no_secrets
701 ctx = DCCloneAndRenameContext(new_base_dn, new_domain_name,
702 new_dns_realm, logger=logger,
704 include_secrets=include_secrets,
705 dns_backend='SAMBA_INTERNAL',
706 server=server, targetdir=tmpdir)
708 # sanity-check we're not "renaming" the domain to the same values
709 old_domain = ctx.domain_name
710 if old_domain == new_domain_name:
711 shutil.rmtree(tmpdir)
712 raise CommandError("Cannot use the current domain NetBIOS name.")
714 old_realm = ctx.realm
715 if old_realm == new_dns_realm:
716 shutil.rmtree(tmpdir)
717 raise CommandError("Cannot use the current domain DNS realm.")
719 # do the clone/rename
722 # get the paths used for the clone, then drop the old samdb connection
726 # get a free RID to use as the new DC's SID (when it gets restored)
727 remote_sam = SamDB(url='ldap://' + server, credentials=creds,
728 session_info=system_session(), lp=lp)
729 new_sid = get_sid_for_restore(remote_sam)
731 # Grab the remote DC's sysvol files and bundle them into a tar file.
732 # Note we end up with 2 sysvol dirs - the original domain's files (that
733 # use the old realm) backed here, as well as default files generated
734 # for the new realm as part of the clone/join.
735 sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
736 smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
737 backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
739 # connect to the local DB (making sure we use the new/renamed config)
740 lp.load(paths.smbconf)
741 samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
743 # Edit the cloned sam.ldb to mark it as a backup
744 time_str = get_timestamp()
745 add_backup_marker(samdb, "backupDate", time_str)
746 add_backup_marker(samdb, "sidForRestore", new_sid)
747 add_backup_marker(samdb, "backupRename", old_realm)
749 # fix up the DNS objects that are using the old dnsRoot value
750 self.update_dns_root(logger, samdb, old_realm, delete_old_dns)
752 # update the netBIOS name and the Partition object for the domain
753 self.rename_domain_partition(logger, samdb, new_domain_name)
756 self.delete_old_dns_zones(logger, samdb, old_realm)
758 logger.info("Fixing DN attributes after rename...")
759 self.fix_old_dn_attributes(samdb)
761 # ensure the admin user always has a password set (same as provision)
763 set_admin_password(logger, samdb, creds.get_username())
765 # Add everything in the tmpdir to the backup tar file
766 backup_file = backup_filepath(targetdir, new_dns_realm, time_str)
767 create_log_file(tmpdir, lp, "rename", server, include_secrets,
768 "Original domain %s (NetBIOS), %s (DNS realm)" %
769 (old_domain, old_realm))
770 create_backup_tar(logger, tmpdir, backup_file)
772 shutil.rmtree(tmpdir)
775 class cmd_domain_backup(samba.netcmd.SuperCommand):
776 '''Create or restore a backup of the domain.'''
777 subcommands = {'online': cmd_domain_backup_online(),
778 'rename': cmd_domain_backup_rename(),
779 'restore': cmd_domain_backup_restore()}