The branch, master has been updated via 9a7e9e5 autobuild: Fix random-sleep.sh invocation in autobuild.py via 41d86e5 samba_tool_showrepl_pull_summary_all_good is flapping via 64e3502 samba-tool drs showrepl test: turn subprocess error into failure via 2860bd0 netcmd: Use dbcheck to fix DB problems introduced by restore itself via 62948a3 tests: Add new tests for backup-rename command via 20568e0 selftest: Add dedicated RENAMEDC testenv for 'backup rename' via 6681f90 netcmd: Extend 'backup restore' command to handle renamed domains via ab65647 netcmd: Add 'samba-tool domain backup rename' command via cd727c9 tests: Tweak the backup online tests so they're generic via 850bba4 drs_utils: Always set the GET_TGT flag for clone renames via 634a72d join: Add more framework for renaming a domain from 734ea27 uid_wrapper: Be strict when checking __attribute__ features
https://git.samba.org/?p=samba.git;a=shortlog;h=master - Log ----------------------------------------------------------------- commit 9a7e9e527f5fa50883137f7acc33ba72627aae16 Author: Andrew Bartlett <abart...@samba.org> Date: Thu Jul 5 11:09:50 2018 +1200 autobuild: Fix random-sleep.sh invocation in autobuild.py The scripts were not running with the correct path and this causes sn-devel to hit a very high load as many of the compile jobs start at once. Signed-off-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz> Autobuild-User(master): Douglas Bagnall <dbagn...@samba.org> Autobuild-Date(master): Thu Jul 5 06:51:26 CEST 2018 on sn-devel-144 commit 41d86e5f2bf537f72f8a543492fcafe38c936657 Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz> Date: Thu Jul 5 13:49:23 2018 +1200 samba_tool_showrepl_pull_summary_all_good is flapping Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> commit 64e350219826355bb2d4f3c7b42ea4f29439526e Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz> Date: Thu Jul 5 11:01:58 2018 +1200 samba-tool drs showrepl test: turn subprocess error into failure Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> commit 2860bd0777eff1bff85f2000adf8fe4d788aad56 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Wed Jul 4 13:23:59 2018 +1200 netcmd: Use dbcheck to fix DB problems introduced by restore itself As part of the restore process, we remove all the old DCs from the DB. However, this introduces some dbcheck errors - there are some DN attributes and one-way links that reference the deleted objects that need fixing up. To resolve this, we can run dbcheck as part of the restore process. This problem affects both renames and plain restores. The dbcheck.sh test didn't spot this problem because it fixes this type of DB error first, before it checks the DB. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Gary Lockyer <g...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> commit 62948a3099cbb6073f3e5e454eaaaadd134e3082 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Tue Jul 3 13:55:53 2018 +1200 tests: Add new tests for backup-rename command Extend the existing 'backup online' tests to also test the domain rename case. This mostly involves some extra assertions that the restored DB has been modified appropriatelt (i.e. domain NetBIOS name is updated, etc). I've also added an extra test case that creates a few objects and links and specifically asserts that they get renamed appropriately. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Gary Lockyer <g...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> commit 20568e00e68631cf7b514024ba2926fd8c73c8e1 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Mon Jun 11 11:02:11 2018 +1200 selftest: Add dedicated RENAMEDC testenv for 'backup rename' Add a new testenv that's similar to the existing restoredc, except we use 'backup rename' to rename the domain as we back it up. Restoring this backup then proves that a valid DC can be started from a renamed backup. Run the same sub-set of RESTOREDC tests to prove that the new testenv is sound. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Gary Lockyer <g...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> commit 6681f904aaf0cd9d757572a4f41376bd2e1c01fd Author: Tim Beale <timbe...@catalyst.net.nz> Date: Mon Jun 11 11:18:09 2018 +1200 netcmd: Extend 'backup restore' command to handle renamed domains When restoring a renamed domain backup, we need to register the new realm's DNS zone. We do this in the restore step because we don't know the new server's IP/hostname in the backup step. Because we may have removed the old realm's DNS entries in the rename step, the remove_dc() code may fail to find the expected DNS entries for the DC's domain (the DCs' dnsHostname still maps to the old DNS realm). We just needed to adjust remove_dns_references() as it was getting a slightly different error code. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Gary Lockyer <g...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> commit ab65647a8b146832eea6e98ee9e1b891f9c36788 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Wed Jun 13 12:22:19 2018 +1200 netcmd: Add 'samba-tool domain backup rename' command Add a new command that takes a clone of the domain's DB, and renames the domain as well. (We rename the domain during the clone because it's easier to implement - the DRS code handles most of the renaming for us, as it applies the received replication chunks). The new option is similar to an online backup, except we also do the following: - use the new DCCloneAndRenameContext code to clone the DB - run dbcheck to fix up any residual old DNs (mostly objectCategory references) - rename the domain's netBIOSName - add dnsRoot objects for the new DNS realm - by default, remove the old realm's DNS objects (optional) - add an extra backupRename marker to the backed-up DB. In the restore code, if the backup was renamed, then we need to register the new domain's DNS zone at that point (we only know the new DC's host IP at the restore stage). Note that the backup will contain the old DC entries that still use the old dnsHostname, but these DC entries will all be removed during the restore, and a new DC will be added with the correct dnsHostname. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Gary Lockyer <g...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> commit cd727c95a288995686cf4300b9a069ccf76b8e46 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Tue Jul 3 13:43:29 2018 +1200 tests: Tweak the backup online tests so they're generic Update backup-online tests to be more generic. We can then re-use the common framework for other types of backups (offline, rename), and just change what's specific to those particular cases. This change includes asserting the restored backup's domain/realm are correct, which we weren't doing previously but makes sense. The new 'return samdb' is for convenience, so that child classes can easily extend the checks we run over the restored DB. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Gary Lockyer <g...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> commit 850bba4d329d062c8a79824c0a9987fa51340e2b Author: Tim Beale <timbe...@catalyst.net.nz> Date: Wed Jun 13 14:09:06 2018 +1200 drs_utils: Always set the GET_TGT flag for clone renames The DCCloneAndRenameContext replication was a little inefficient, in that it would essentially replicate the entire DB twice. This was due to resolving the link targets - it finds a target object it doesn't know about, so retries the entire replication again with the GET_TGT flag set this time. Normally, the repl_meta_data code will use the target object's GUID, however, it can't do this for cross-partition links (if it hasn't replicated the target partition yet). The repl_md code can normally detect that the link is a cross-parition link by checking the base-DN, however, this doesn't work in the DCCloneAndRenameContext case because we have renamed the base-DN. This is not a big deal - it just means extra work. However, because the domains being backed up could potentially be quite large, it probably makes sense to just always set the GET_TGT in the rename case and skip this extra work. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Gary Lockyer <g...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> commit 634a72df24e74bce07d2481fb86b0ce4b7ebaa38 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Mon Jun 11 16:50:28 2018 +1200 join: Add more framework for renaming a domain Add a DCCloneContext subclass which will rename the DB objects as they get cloned. This uses the drs_ReplicateRenamer class added to drs_utils in an earlier patch. Where the drs_Replicate object currently gets created has been split out into a simple new function, which we can then override in the rename case. The other important difference is overriding the provision step, so that we use the new domain-DN/realm when setting up the initial SAM DB (and smb.conf, secrets.ldb, etc). Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Gary Lockyer <g...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> ----------------------------------------------------------------------- Summary of changes: docs-xml/manpages/samba-tool.8.xml | 5 + python/samba/drs_utils.py | 9 +- python/samba/join.py | 100 ++++++- python/samba/netcmd/domain_backup.py | 308 ++++++++++++++++++++- python/samba/provision/sambadns.py | 5 +- python/samba/remove_dc.py | 3 +- python/samba/tests/domain_backup.py | 179 ++++++++++-- script/autobuild.py | 24 +- selftest/flapping.d/samba_tool_drs_showrepl | 1 + selftest/target/Samba.pm | 1 + selftest/target/Samba4.pm | 45 +++ source4/selftest/tests.py | 11 +- .../torture/drs/python/samba_tool_drs_showrepl.py | 36 ++- 13 files changed, 667 insertions(+), 60 deletions(-) create mode 100644 selftest/flapping.d/samba_tool_drs_showrepl Changeset truncated at 500 lines: diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml index b8038bc..7f000e9 100644 --- a/docs-xml/manpages/samba-tool.8.xml +++ b/docs-xml/manpages/samba-tool.8.xml @@ -302,6 +302,11 @@ </refsect3> <refsect3> + <title>domain backup rename</title> + <para>Copy a running DC's DB to backup file, renaming the domain in the process.</para> +</refsect3> + +<refsect3> <title>domain backup restore</title> <para>Restore the domain's DB from a backup-file.</para> </refsect3> diff --git a/python/samba/drs_utils.py b/python/samba/drs_utils.py index db83267..66f4750 100644 --- a/python/samba/drs_utils.py +++ b/python/samba/drs_utils.py @@ -200,6 +200,7 @@ class drs_Replicate(object): if invocation_id == misc.GUID("00000000-0000-0000-0000-000000000000"): raise RuntimeError("Must not set GUID 00000000-0000-0000-0000-000000000000 as invocation_id") self.replication_state = self.net.replicate_init(self.samdb, lp, self.drs, invocation_id) + self.more_flags = 0 def _should_retry_with_get_tgt(self, error_code, req): @@ -228,7 +229,7 @@ class drs_Replicate(object): # setup for a GetNCChanges call if self.supported_extensions & drsuapi.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V10: req = drsuapi.DsGetNCChangesRequest10() - req.more_flags = more_flags + req.more_flags = (more_flags | self.more_flags) req_level = 10 else: req_level = 8 @@ -367,6 +368,12 @@ class drs_ReplicateRenamer(drs_Replicate): self.old_base_dn = old_base_dn self.new_base_dn = new_base_dn + # because we're renaming the DNs, we know we're going to have trouble + # resolving link targets. Normally we'd get to the end of replication + # only to find we need to retry the whole replication with the GET_TGT + # flag set. Always setting the GET_TGT flag avoids this extra work. + self.more_flags = drsuapi.DRSUAPI_DRS_GET_TGT + def rename_dn(self, dn_str): '''Uses string substitution to replace the base DN''' return re.sub('%s$' % self.old_base_dn, self.new_base_dn, dn_str) diff --git a/python/samba/join.py b/python/samba/join.py index 39c9a3a..3b648f5 100644 --- a/python/samba/join.py +++ b/python/samba/join.py @@ -43,6 +43,9 @@ import logging import talloc import random import time +import re +import os +import tempfile class DCJoinException(Exception): @@ -904,6 +907,12 @@ class DCJoinContext(object): dns_backend=ctx.dns_backend, adminpass=ctx.adminpass) print("Provision OK for domain %s" % ctx.names.dnsdomain) + def create_replicator(ctx, repl_creds, binding_options): + '''Creates a new DRS object for managing replications''' + return drs_utils.drs_Replicate( + "ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options), + ctx.lp, repl_creds, ctx.local_samdb, ctx.invocation_id) + def join_replicate(ctx): """Replicate the SAM.""" @@ -929,9 +938,8 @@ class DCJoinContext(object): binding_options = "seal" if ctx.lp.log_level() >= 9: binding_options += ",print" - repl = drs_utils.drs_Replicate( - "ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options), - ctx.lp, repl_creds, ctx.local_samdb, ctx.invocation_id) + + repl = ctx.create_replicator(repl_creds, binding_options) repl.replicate(ctx.schema_dn, source_dsa_invocation_id, destination_dsa_guid, schema=True, rodc=ctx.RODC, @@ -1614,3 +1622,89 @@ class DCCloneContext(DCJoinContext): ctx.join_provision() ctx.join_replicate() ctx.join_finalise() + + +# Used to create a renamed backup of a DC. Renaming the domain means that the +# cloned/backup DC can be started without interfering with the production DC. +class DCCloneAndRenameContext(DCCloneContext): + """Clones a remote DC, renaming the domain along the way.""" + + def __init__(ctx, new_base_dn, new_domain_name, new_realm, logger=None, + server=None, creds=None, lp=None, targetdir=None, domain=None, + dns_backend=None, include_secrets=True): + super(DCCloneAndRenameContext, ctx).__init__(logger, server, creds, lp, + targetdir=targetdir, + domain=domain, + dns_backend=dns_backend, + include_secrets=include_secrets) + # store the new DN (etc) that we want the cloned DB to use + ctx.new_base_dn = new_base_dn + ctx.new_domain_name = new_domain_name + ctx.new_realm = new_realm + + def create_replicator(ctx, repl_creds, binding_options): + """Creates a new DRS object for managing replications""" + + # We want to rename all the domain objects, and the simplest way to do + # this is during replication. This is because the base DN of the top- + # level replicated object will flow through to all the objects below it + binding_str = "ncacn_ip_tcp:%s[%s]" % (ctx.server, binding_options) + return drs_utils.drs_ReplicateRenamer(binding_str, ctx.lp, repl_creds, + ctx.local_samdb, + ctx.invocation_id, + ctx.base_dn, ctx.new_base_dn) + + def create_non_global_lp(ctx, global_lp): + '''Creates a non-global LoadParm based on the global LP's settings''' + + # the samba code shares a global LoadParm by default. Here we create a + # new LoadParm that retains the global settings, but any changes we + # make to it won't automatically affect the rest of the samba code. + # The easiest way to do this is to dump the global settings to a + # temporary smb.conf file, and then load the temp file into a new + # non-global LoadParm + fd, tmp_file = tempfile.mkstemp() + global_lp.dump(False, tmp_file) + local_lp = samba.param.LoadParm(filename_for_non_global_lp=tmp_file) + os.remove(tmp_file) + return local_lp + + def rename_dn(ctx, dn_str): + '''Uses string substitution to replace the base DN''' + old_base_dn = ctx.base_dn + return re.sub('%s$' % old_base_dn, ctx.new_base_dn, dn_str) + + # we want to override the normal DCCloneContext's join_provision() so that + # use the new domain DNs during the provision. We do this because: + # - it sets up smb.conf/secrets.ldb with the new realm/workgroup values + # - it sets up a default SAM DB that uses the new Schema DNs (without which + # we couldn't apply the renamed DRS objects during replication) + def join_provision(ctx): + """Provision the local (renamed) SAM.""" + + print("Provisioning the new (renamed) domain...") + + # the provision() calls make_smbconf() which uses lp.dump()/lp.load() + # to create a new smb.conf. By default, it uses the global LoadParm to + # do this, and so it would overwrite the realm/domain values globally. + # We still need the global LoadParm to retain the old domain's details, + # so we can connect to (and clone) the existing DC. + # So, copy the global settings into a non-global LoadParm, which we can + # then pass into provision(). This generates a new smb.conf correctly, + # without overwriting the global realm/domain values just yet. + non_global_lp = ctx.create_non_global_lp(ctx.lp) + + # do the provision with the new/renamed domain DN values + presult = provision(ctx.logger, system_session(), + targetdir=ctx.targetdir, samdb_fill=FILL_DRS, + realm=ctx.new_realm, lp=non_global_lp, + rootdn=ctx.rename_dn(ctx.root_dn), domaindn=ctx.new_base_dn, + schemadn=ctx.rename_dn(ctx.schema_dn), + configdn=ctx.rename_dn(ctx.config_dn), + domain=ctx.new_domain_name, domainsid=ctx.domsid, + serverrole="active directory domain controller", + dns_backend=ctx.dns_backend) + + print("Provision OK for renamed domain DN %s" % presult.domaindn) + ctx.local_samdb = presult.samdb + ctx.paths = presult.paths diff --git a/python/samba/netcmd/domain_backup.py b/python/samba/netcmd/domain_backup.py index c706c3c..c156046 100644 --- a/python/samba/netcmd/domain_backup.py +++ b/python/samba/netcmd/domain_backup.py @@ -29,7 +29,7 @@ import ldb from samba import smb from samba.ntacls import backup_online, backup_restore from samba.auth import system_session -from samba.join import DCJoinContext, join_clone +from samba.join import DCJoinContext, join_clone, DCCloneAndRenameContext from samba.dcerpc.security import dom_sid from samba.netcmd import Option, CommandError from samba.dcerpc import misc @@ -39,7 +39,12 @@ from samba.provision import make_smbconf from samba.upgradehelpers import update_krbtgt_account_password from samba.remove_dc import remove_dc from samba.provision import secretsdb_self_join - +from samba.dbchecker import dbcheck +import re +from samba.provision import guess_names, determine_host_ip, determine_host_ip6 +from samba.provision.sambadns import (fill_dns_data_partitions, + get_dnsadmins_sid, + get_domainguid) # work out a SID (based on a free RID) to use when the domain gets restored. @@ -233,6 +238,10 @@ class cmd_domain_backup_restore(cmd_fsmo_seize): Option("--backup-file", help="Path to backup file", type=str), Option("--targetdir", help="Path to write to", type=str), Option("--newservername", help="Name for new server", type=str), + Option("--host-ip", type="string", metavar="IPADDRESS", + help="set IPv4 ipaddress"), + Option("--host-ip6", type="string", metavar="IP6ADDRESS", + help="set IPv6 ipaddress"), ] takes_optiongroups = { @@ -240,8 +249,66 @@ class cmd_domain_backup_restore(cmd_fsmo_seize): "credopts": options.CredentialsOptions, } + def register_dns_zone(self, logger, samdb, lp, ntdsguid, host_ip, + host_ip6): + ''' + Registers the new realm's DNS objects when a renamed domain backup + is restored. + ''' + names = guess_names(lp) + domaindn = names.domaindn + forestdn = samdb.get_root_basedn().get_linearized() + dnsdomain = names.dnsdomain.lower() + dnsforest = dnsdomain + hostname = names.netbiosname.lower() + domainsid = dom_sid(samdb.get_domain_sid()) + dnsadmins_sid = get_dnsadmins_sid(samdb, domaindn) + domainguid = get_domainguid(samdb, domaindn) + + # work out the IP address to use for the new DC's DNS records + host_ip = determine_host_ip(logger, lp, host_ip) + host_ip6 = determine_host_ip6(logger, lp, host_ip6) + + if host_ip is None and host_ip6 is None: + raise CommandError('Please specify a host-ip for the new server') + + logger.info("DNS realm was renamed to %s" % dnsdomain) + logger.info("Populating DNS partitions for new realm...") + + # Add the DNS objects for the new realm (note: the backup clone already + # has the root server objects, so don't add them again) + fill_dns_data_partitions(samdb, domainsid, names.sitename, domaindn, + forestdn, dnsdomain, dnsforest, hostname, + host_ip, host_ip6, domainguid, ntdsguid, + dnsadmins_sid, add_root=False) + + def fix_old_dc_references(self, samdb): + '''Fixes attributes that reference the old/removed DCs''' + + # we just want to fix up DB problems here that were introduced by us + # removing the old DCs. We restrict what we fix up so that the restored + # DB matches the backed-up DB as close as possible. (There may be other + # DB issues inherited from the backed-up DC, but it's not our place to + # silently try to fix them here). + samdb.transaction_start() + chk = dbcheck(samdb, quiet=True, fix=True, yes=False, + in_transaction=True) + + # fix up stale references to the old DC + setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL') + attrs = ['lastKnownParent', 'interSiteTopologyGenerator'] + + # fix-up stale one-way links that point to the old DC + setattr(chk, 'remove_plausible_deleted_DN_links', 'ALL') + attrs += ['msDS-NC-Replica-Locations'] + + cross_ncs_ctrl = 'search_options:1:2' + controls = ['show_deleted:1', cross_ncs_ctrl] + chk.check_database(controls=controls, attrs=attrs) + samdb.transaction_commit() + def run(self, sambaopts=None, credopts=None, backup_file=None, - targetdir=None, newservername=None): + targetdir=None, newservername=None, host_ip=None, host_ip6=None): if not (backup_file and os.path.exists(backup_file)): raise CommandError('Backup file not found.') if targetdir is None: @@ -312,7 +379,8 @@ class cmd_domain_backup_restore(cmd_fsmo_seize): # Get the SID saved by the backup process and create account res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"), scope=ldb.SCOPE_BASE, - attrs=['sidForRestore']) + attrs=['sidForRestore', 'backupRename']) + is_rename = True if 'backupRename' in res[0] else False sid = res[0].get('sidForRestore')[0] logger.info('Creating account with SID: ' + str(sid)) ctx.join_add_objects(specified_sid=dom_sid(sid)) @@ -325,6 +393,13 @@ class cmd_domain_backup_restore(cmd_fsmo_seize): "dsServiceName") samdb.modify(m) + # if we renamed the backed-up domain, then we need to add the DNS + # objects for the new realm (we do this in the restore, now that we + # know the new DC's IP address) + if is_rename: + self.register_dns_zone(logger, samdb, lp, ctx.ntds_guid, + host_ip, host_ip6) + secrets_path = os.path.join(private_dir, 'secrets.ldb') secrets_ldb = Ldb(secrets_path, session_info=system_session(), lp=lp) secretsdb_self_join(secrets_ldb, domain=ctx.domain_name, @@ -374,8 +449,8 @@ class cmd_domain_backup_restore(cmd_fsmo_seize): ldb.FLAG_MOD_REPLACE, "repsFrom") msg["repsTo"] = ldb.MessageElement([], - ldb.FLAG_MOD_REPLACE, - "repsTo") + ldb.FLAG_MOD_REPLACE, + "repsTo") samdb.modify(msg) # Update the krbtgt passwords twice, ensuring no tickets from @@ -392,6 +467,10 @@ class cmd_domain_backup_restore(cmd_fsmo_seize): backup_restore(sysvol_tar, dest_sysvol_dir, samdb, smbconf) os.remove(sysvol_tar) + # fix up any stale links to the old DCs we just removed + logger.info("Fixing up any remaining references to the old DCs...") + self.fix_old_dc_references(samdb) + # Remove DB markers added by the backup process m = ldb.Message() m.dn = ldb.Dn(samdb, "@SAMBA_DSDB") @@ -399,6 +478,9 @@ class cmd_domain_backup_restore(cmd_fsmo_seize): "backupDate") m["sidForRestore"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "sidForRestore") + if is_rename: + m["backupRename"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, + "backupRename") samdb.modify(m) logger.info("Backup file successfully restored to %s" % targetdir) @@ -406,7 +488,221 @@ class cmd_domain_backup_restore(cmd_fsmo_seize): "starting samba.") +class cmd_domain_backup_rename(samba.netcmd.Command): + '''Copy a running DC's DB to backup file, renaming the domain in the process. + + Where <new-domain> is the new domain's NetBIOS name, and <new-dnsrealm> is + the new domain's realm in DNS form. + + This is similar to 'samba-tool backup online' in that it clones the DB of a + running DC. However, this option also renames all the domain entries in the + DB. Renaming the domain makes it possible to restore and start a new Samba + DC without it interfering with the existing Samba domain. In other words, + you could use this option to clone your production samba domain and restore + it to a separate pre-production environment that won't overlap or interfere + with the existing production Samba domain. + + Note that: + - it's recommended to run 'samba-tool dbcheck' before taking a backup-file + and fix any errors it reports. + - all the domain's secrets are included in the backup file. + - although the DB contents can be untarred and examined manually, you need + to run 'samba-tool domain backup restore' before you can start a Samba DC + from the backup file. + - GPO and sysvol information will still refer to the old realm and will + need to be updated manually. + - if you specify 'keep-dns-realm', then the DNS records will need updating + in order to work (they will still refer to the old DC's IP instead of the + new DC's address). + - we recommend that you only use this option if you know what you're doing. + ''' + + synopsis = ("%prog <new-domain> <new-dnsrealm> --server=<DC-to-backup> " + "--targetdir=<output-dir>") + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("--server", help="The DC to backup", type=str), + Option("--targetdir", help="Directory to write the backup file", + type=str), + Option("--keep-dns-realm", action="store_true", default=False, + help="Retain the DNS entries for the old realm in the backup"), + ] + + takes_args = ["new_domain_name", "new_dns_realm"] + + def update_dns_root(self, logger, samdb, old_realm, delete_old_dns): + '''Updates dnsRoot for the partition objects to reflect the rename''' + + # lookup the crossRef objects that hold the old realm's dnsRoot + partitions_dn = samdb.get_partitions_dn() + res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL, + attrs=["dnsRoot"], + expression='(&(objectClass=crossRef)(dnsRoot=*))') + new_realm = samdb.domain_dns_name() + + # go through and add the new realm + for res_msg in res: + # dnsRoot can be multi-valued, so only look for the old realm + for dns_root in res_msg["dnsRoot"]: + dn = res_msg.dn + if old_realm in dns_root: + new_dns_root = re.sub('%s$' % old_realm, new_realm, + dns_root) + logger.info("Adding %s dnsRoot to %s" % (new_dns_root, dn)) + + m = ldb.Message() + m.dn = dn + m["dnsRoot"] = ldb.MessageElement(new_dns_root, + ldb.FLAG_MOD_ADD, + "dnsRoot") + samdb.modify(m) + + # optionally remove the dnsRoot for the old realm + if delete_old_dns: + logger.info("Removing %s dnsRoot from %s" % (dns_root, + dn)) + m["dnsRoot"] = ldb.MessageElement(dns_root, + ldb.FLAG_MOD_DELETE, + "dnsRoot") + samdb.modify(m) + + # Updates the CN=<domain>,CN=Partitions,CN=Configuration,... object to + # reflect the domain rename + def rename_domain_partition(self, logger, samdb, new_netbios_name): + '''Renames the domain parition object and updates its nETBIOSName''' + + # lookup the crossRef object that holds the nETBIOSName (nCName has + # already been updated by this point, but the netBIOS hasn't) + base_dn = samdb.get_default_basedn() + nc_name = ldb.binary_encode(str(base_dn)) + partitions_dn = samdb.get_partitions_dn() + res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL, + attrs=["nETBIOSName"], + expression='ncName=%s' % nc_name) + + logger.info("Changing backup domain's NetBIOS name to %s" % + new_netbios_name) + m = ldb.Message() + m.dn = res[0].dn + m["nETBIOSName"] = ldb.MessageElement(new_netbios_name, + ldb.FLAG_MOD_REPLACE, + "nETBIOSName") + samdb.modify(m) + + # renames the object itself to reflect the change in domain + new_dn = "CN=%s,%s" % (new_netbios_name, partitions_dn) + logger.info("Renaming %s --> %s" % (res[0].dn, new_dn)) + samdb.rename(res[0].dn, new_dn, controls=['relax:0']) + + def delete_old_dns_zones(self, logger, samdb, old_realm): + # remove the top-level DNS entries for the old realm + basedn = samdb.get_default_basedn() + dn = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (old_realm, basedn) + logger.info("Deleting old DNS zone %s" % dn) + samdb.delete(dn, ["tree_delete:1"]) + + forestdn = samdb.get_root_basedn().get_linearized() + dn = "DC=_msdcs.%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (old_realm, + forestdn) + logger.info("Deleting old DNS zone %s" % dn) + samdb.delete(dn, ["tree_delete:1"]) + + def fix_old_dn_attributes(self, samdb): + '''Fixes attributes (i.e. objectCategory) that still use the old DN''' + + samdb.transaction_start() + # Just fix any mismatches in DN detected (leave any other errors) + chk = dbcheck(samdb, quiet=True, fix=True, yes=False, + in_transaction=True) + # fix up incorrect objectCategory/etc attributes + setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL') + cross_ncs_ctrl = 'search_options:1:2' + controls = ['show_deleted:1', cross_ncs_ctrl] + chk.check_database(controls=controls) + samdb.transaction_commit() + + def run(self, new_domain_name, new_dns_realm, sambaopts=None, + credopts=None, server=None, targetdir=None, keep_dns_realm=False): + logger = self.get_logger() + logger.setLevel(logging.INFO) + + # Make sure we have all the required args. + check_online_backup_args(logger, credopts, server, targetdir) + delete_old_dns = not keep_dns_realm + + new_dns_realm = new_dns_realm.lower() + new_domain_name = new_domain_name.upper() + + new_base_dn = samba.dn_from_dns_name(new_dns_realm) + logger.info("New realm for backed up domain: %s" % new_dns_realm) + logger.info("New base DN for backed up domain: %s" % new_base_dn) + logger.info("New domain NetBIOS name: %s" % new_domain_name) + + tmpdir = tempfile.mkdtemp(dir=targetdir) -- Samba Shared Repository