class cmd_drs_clone_dc_database(Command): """Replicate an initial clone of domain, but DO NOT JOIN it.""" synopsis = "%prog <dnsdomain> [options]" takes_optiongroups = { "sambaopts": options.SambaOptions, "versionopts": options.VersionOptions, "credopts": options.CredentialsOptions, } takes_options = [ Option("--server", help="DC to join", type=str), Option("--targetdir", help="where to store provision (required)", type=str), Option("-q", "--quiet", help="Be quiet", action="store_true"), Option("--include-secrets", help="Also replicate secret values", action="store_true"), Option("--backend-store", type="choice", metavar="BACKENDSTORE", choices=["tdb", "mdb"], help="Specify the database backend to be used " "(default is %s)" % get_default_backend_store()), Option("--backend-store-size", type="bytes", metavar="SIZE", help="Specify the size of the backend database, currently" + "only supported by lmdb backends (default is 8 Gb).") ] takes_args = ["domain"] def run(self, domain, sambaopts=None, credopts=None, versionopts=None, server=None, targetdir=None, quiet=False, verbose=False, include_secrets=False, backend_store=None, backend_store_size=None): lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp) logger = self.get_logger(verbose=verbose, quiet=quiet) if targetdir is None: raise CommandError("--targetdir option must be specified") join_clone(logger=logger, server=server, creds=creds, lp=lp, domain=domain, dns_backend='SAMBA_INTERNAL', targetdir=targetdir, include_secrets=include_secrets, backend_store=backend_store, backend_store_size=backend_store_size)
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"), Option("--no-secrets", action="store_true", default=False, help="Exclude secret values from the backup created"), Option("--backend-store", type="choice", metavar="BACKENDSTORE", choices=["tdb", "mdb"], help="Specify the database backend to be used " "(default is %s)" % get_default_backend_store()), ] 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"]: dns_root = str(dns_root) 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, no_secrets=False, backend_store=None): logger = self.get_logger() logger.setLevel(logging.INFO) lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp) # Make sure we have all the required args. if server is None: raise CommandError('Server required') check_targetdir(logger, 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) # setup a join-context for cloning the remote server include_secrets = not no_secrets ctx = DCCloneAndRenameContext(new_base_dn, new_domain_name, new_dns_realm, logger=logger, creds=creds, lp=lp, include_secrets=include_secrets, dns_backend='SAMBA_INTERNAL', server=server, targetdir=tmpdir, backend_store=backend_store) # sanity-check we're not "renaming" the domain to the same values old_domain = ctx.domain_name if old_domain == new_domain_name: shutil.rmtree(tmpdir) raise CommandError("Cannot use the current domain NetBIOS name.") old_realm = ctx.realm if old_realm == new_dns_realm: shutil.rmtree(tmpdir) raise CommandError("Cannot use the current domain DNS realm.") # do the clone/rename ctx.do_join() # get the paths used for the clone, then drop the old samdb connection del ctx.local_samdb paths = ctx.paths # get a free RID to use as the new DC's SID (when it gets restored) remote_sam = SamDB(url='ldap://' + server, credentials=creds, session_info=system_session(), lp=lp) new_sid = get_sid_for_restore(remote_sam, logger) # Grab the remote DC's sysvol files and bundle them into a tar file. # Note we end up with 2 sysvol dirs - the original domain's files (that # use the old realm) backed here, as well as default files generated # for the new realm as part of the clone/join. sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz') smb_conn = smb_sysvol_conn(server, lp, creds) backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid()) # connect to the local DB (making sure we use the new/renamed config) lp.load(paths.smbconf) samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp) # Edit the cloned sam.ldb to mark it as a backup time_str = get_timestamp() add_backup_marker(samdb, "backupDate", time_str) add_backup_marker(samdb, "sidForRestore", new_sid) add_backup_marker(samdb, "backupRename", old_realm) add_backup_marker(samdb, "backupType", "rename") # fix up the DNS objects that are using the old dnsRoot value self.update_dns_root(logger, samdb, old_realm, delete_old_dns) # update the netBIOS name and the Partition object for the domain self.rename_domain_partition(logger, samdb, new_domain_name) if delete_old_dns: self.delete_old_dns_zones(logger, samdb, old_realm) logger.info("Fixing DN attributes after rename...") self.fix_old_dn_attributes(samdb) # ensure the admin user always has a password set (same as provision) if no_secrets: set_admin_password(logger, samdb) # Add everything in the tmpdir to the backup tar file backup_file = backup_filepath(targetdir, new_dns_realm, time_str) create_log_file(tmpdir, lp, "rename", server, include_secrets, "Original domain %s (NetBIOS), %s (DNS realm)" % (old_domain, old_realm)) create_backup_tar(logger, tmpdir, backup_file) shutil.rmtree(tmpdir)
class cmd_domain_backup_online(samba.netcmd.Command): '''Copy a running DC's current DB into a backup tar file. Takes a backup copy of the current domain from a running DC. If the domain were to undergo a catastrophic failure, then the backup file can be used to recover the domain. The backup created is similar to the DB that a new DC would receive when it joins the 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.''' synopsis = "%prog --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", type=str, help="Directory to write the backup file to"), Option("--no-secrets", action="store_true", default=False, help="Exclude secret values from the backup created"), Option("--backend-store", type="choice", metavar="BACKENDSTORE", choices=["tdb", "mdb"], help="Specify the database backend to be used " "(default is %s)" % get_default_backend_store()), ] def run(self, sambaopts=None, credopts=None, server=None, targetdir=None, no_secrets=False, backend_store=None): logger = self.get_logger() logger.setLevel(logging.DEBUG) lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp) # Make sure we have all the required args. if server is None: raise CommandError('Server required') check_targetdir(logger, targetdir) tmpdir = tempfile.mkdtemp(dir=targetdir) # Run a clone join on the remote include_secrets = not no_secrets ctx = join_clone(logger=logger, creds=creds, lp=lp, include_secrets=include_secrets, server=server, dns_backend='SAMBA_INTERNAL', targetdir=tmpdir, backend_store=backend_store) # get the paths used for the clone, then drop the old samdb connection paths = ctx.paths del ctx # Get a free RID to use as the new DC's SID (when it gets restored) remote_sam = SamDB(url='ldap://' + server, credentials=creds, session_info=system_session(), lp=lp) new_sid = get_sid_for_restore(remote_sam, logger) realm = remote_sam.domain_dns_name() # Grab the remote DC's sysvol files and bundle them into a tar file sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz') smb_conn = smb_sysvol_conn(server, lp, creds) backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid()) # remove the default sysvol files created by the clone (we want to # make sure we restore the sysvol.tar.gz files instead) shutil.rmtree(paths.sysvol) # Edit the downloaded sam.ldb to mark it as a backup samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp) time_str = get_timestamp() add_backup_marker(samdb, "backupDate", time_str) add_backup_marker(samdb, "sidForRestore", new_sid) add_backup_marker(samdb, "backupType", "online") # ensure the admin user always has a password set (same as provision) if no_secrets: set_admin_password(logger, samdb) # Add everything in the tmpdir to the backup tar file backup_file = backup_filepath(targetdir, realm, time_str) create_log_file(tmpdir, lp, "online", server, include_secrets) create_backup_tar(logger, tmpdir, backup_file) shutil.rmtree(tmpdir)
def create_samdb_copy(samdb, logger, paths, names, domainsid, domainguid): """Create a copy of samdb and give write permissions to named for dns partitions """ private_dir = paths.private_dir samldb_dir = os.path.join(private_dir, "sam.ldb.d") dns_dir = os.path.dirname(paths.dns) dns_samldb_dir = os.path.join(dns_dir, "sam.ldb.d") # Find the partitions and corresponding filenames partfile = {} res = samdb.search(base="@PARTITION", scope=ldb.SCOPE_BASE, attrs=["partition", "backendStore"]) for tmp in res[0]["partition"]: (nc, fname) = tmp.split(':') partfile[nc.upper()] = fname backend_store = get_default_backend_store() if "backendStore" in res[0]: backend_store = res[0]["backendStore"][0] # Create empty domain partition domaindn = names.domaindn.upper() domainpart_file = os.path.join(dns_dir, partfile[domaindn]) try: os.mkdir(dns_samldb_dir) open(domainpart_file, 'w').close() # Fill the basedn and @OPTION records in domain partition dom_url = "%s://%s" % (backend_store, domainpart_file) dom_ldb = samba.Ldb(dom_url) # We need the dummy main-domain DB to have the correct @INDEXLIST index_res = samdb.search(base="@INDEXLIST", scope=ldb.SCOPE_BASE) dom_ldb.add(index_res[0]) domainguid_line = "objectGUID: %s\n-" % domainguid descr = b64encode(get_domain_descriptor(domainsid)).decode('utf8') setup_add_ldif(dom_ldb, setup_path("provision_basedn.ldif"), { "DOMAINDN" : names.domaindn, "DOMAINGUID" : domainguid_line, "DOMAINSID" : str(domainsid), "DESCRIPTOR" : descr}) setup_add_ldif(dom_ldb, setup_path("provision_basedn_options.ldif"), None) except: logger.error( "Failed to setup database for BIND, AD based DNS cannot be used") raise # This line is critical to the security of the whole scheme. # We assume there is no secret data in the (to be left out of # date and essentially read-only) config, schema and metadata partitions. # # Only the stub of the domain partition is created above. # # That way, things like the krbtgt key do not leak. del partfile[domaindn] # Link dns partitions and metadata domainzonedn = "DC=DOMAINDNSZONES,%s" % names.domaindn.upper() forestzonedn = "DC=FORESTDNSZONES,%s" % names.rootdn.upper() domainzone_file = partfile[domainzonedn] forestzone_file = partfile.get(forestzonedn) metadata_file = "metadata.tdb" try: os.link(os.path.join(samldb_dir, metadata_file), os.path.join(dns_samldb_dir, metadata_file)) os.link(os.path.join(private_dir, domainzone_file), os.path.join(dns_dir, domainzone_file)) if forestzone_file: os.link(os.path.join(private_dir, forestzone_file), os.path.join(dns_dir, forestzone_file)) except OSError: logger.error( "Failed to setup database for BIND, AD based DNS cannot be used") raise del partfile[domainzonedn] if forestzone_file: del partfile[forestzonedn] # Copy root, config, schema partitions (and any other if any) # Since samdb is open in the current process, copy them in a child process try: tdb_copy(os.path.join(private_dir, "sam.ldb"), os.path.join(dns_dir, "sam.ldb")) for nc in partfile: pfile = partfile[nc] if backend_store == "mdb": mdb_copy(os.path.join(private_dir, pfile), os.path.join(dns_dir, pfile)) else: tdb_copy(os.path.join(private_dir, pfile), os.path.join(dns_dir, pfile)) except: logger.error( "Failed to setup database for BIND, AD based DNS cannot be used") raise # Give bind read/write permissions dns partitions if paths.bind_gid is not None: try: for dirname, dirs, files in os.walk(dns_dir): for d in dirs: dpath = os.path.join(dirname, d) os.chown(dpath, -1, paths.bind_gid) os.chmod(dpath, 0o770) for f in files: if f.endswith(('.ldb', '.tdb', 'ldb-lock')): fpath = os.path.join(dirname, f) os.chown(fpath, -1, paths.bind_gid) os.chmod(fpath, 0o660) except OSError: if not os.environ.has_key('SAMBA_SELFTEST'): logger.error( "Failed to set permissions to sam.ldb* files, fix manually") else: if not os.environ.has_key('SAMBA_SELFTEST'): logger.warning("""Unable to find group id for BIND, set permissions to sam.ldb* files manually""")
def create_samdb_copy(samdb, logger, paths, names, domainsid, domainguid): """Create a copy of samdb and give write permissions to named for dns partitions """ private_dir = paths.private_dir samldb_dir = os.path.join(private_dir, "sam.ldb.d") dns_dir = os.path.dirname(paths.dns) dns_samldb_dir = os.path.join(dns_dir, "sam.ldb.d") # Find the partitions and corresponding filenames partfile = {} res = samdb.search(base="@PARTITION", scope=ldb.SCOPE_BASE, attrs=["partition", "backendStore"]) for tmp in res[0]["partition"]: (nc, fname) = str(tmp).split(':') partfile[nc.upper()] = fname backend_store = get_default_backend_store() if "backendStore" in res[0]: backend_store = str(res[0]["backendStore"][0]) # Create empty domain partition domaindn = names.domaindn.upper() domainpart_file = os.path.join(dns_dir, partfile[domaindn]) try: os.mkdir(dns_samldb_dir) open(domainpart_file, 'w').close() # Fill the basedn and @OPTION records in domain partition dom_url = "%s://%s" % (backend_store, domainpart_file) dom_ldb = samba.Ldb(dom_url) # We need the dummy main-domain DB to have the correct @INDEXLIST index_res = samdb.search(base="@INDEXLIST", scope=ldb.SCOPE_BASE) dom_ldb.add(index_res[0]) domainguid_line = "objectGUID: %s\n-" % domainguid descr = b64encode(get_domain_descriptor(domainsid)).decode('utf8') setup_add_ldif(dom_ldb, setup_path("provision_basedn.ldif"), { "DOMAINDN": names.domaindn, "DOMAINGUID": domainguid_line, "DOMAINSID": str(domainsid), "DESCRIPTOR": descr}) setup_add_ldif(dom_ldb, setup_path("provision_basedn_options.ldif"), None) except: logger.error( "Failed to setup database for BIND, AD based DNS cannot be used") raise # This line is critical to the security of the whole scheme. # We assume there is no secret data in the (to be left out of # date and essentially read-only) config, schema and metadata partitions. # # Only the stub of the domain partition is created above. # # That way, things like the krbtgt key do not leak. del partfile[domaindn] # Link dns partitions and metadata domainzonedn = "DC=DOMAINDNSZONES,%s" % names.domaindn.upper() forestzonedn = "DC=FORESTDNSZONES,%s" % names.rootdn.upper() domainzone_file = partfile[domainzonedn] forestzone_file = partfile.get(forestzonedn) metadata_file = "metadata.tdb" try: os.link(os.path.join(samldb_dir, metadata_file), os.path.join(dns_samldb_dir, metadata_file)) os.link(os.path.join(private_dir, domainzone_file), os.path.join(dns_dir, domainzone_file)) if backend_store == "mdb": # If the file is an lmdb data file need to link the # lock file as well os.link(os.path.join(private_dir, domainzone_file + "-lock"), os.path.join(dns_dir, domainzone_file + "-lock")) if forestzone_file: os.link(os.path.join(private_dir, forestzone_file), os.path.join(dns_dir, forestzone_file)) if backend_store == "mdb": # If the database file is an lmdb data file need to link the # lock file as well os.link(os.path.join(private_dir, forestzone_file + "-lock"), os.path.join(dns_dir, forestzone_file + "-lock")) except OSError: logger.error( "Failed to setup database for BIND, AD based DNS cannot be used") raise del partfile[domainzonedn] if forestzone_file: del partfile[forestzonedn] # Copy root, config, schema partitions (and any other if any) # Since samdb is open in the current process, copy them in a child process try: tdb_copy(os.path.join(private_dir, "sam.ldb"), os.path.join(dns_dir, "sam.ldb")) for nc in partfile: pfile = partfile[nc] if backend_store == "mdb": mdb_copy(os.path.join(private_dir, pfile), os.path.join(dns_dir, pfile)) else: tdb_copy(os.path.join(private_dir, pfile), os.path.join(dns_dir, pfile)) except: logger.error( "Failed to setup database for BIND, AD based DNS cannot be used") raise # Give bind read/write permissions dns partitions if paths.bind_gid is not None: try: for dirname, dirs, files in os.walk(dns_dir): for d in dirs: dpath = os.path.join(dirname, d) os.chown(dpath, -1, paths.bind_gid) os.chmod(dpath, 0o770) for f in files: if f.endswith(('.ldb', '.tdb', 'ldb-lock')): fpath = os.path.join(dirname, f) os.chown(fpath, -1, paths.bind_gid) os.chmod(fpath, 0o660) except OSError: if 'SAMBA_SELFTEST' not in os.environ: logger.error( "Failed to set permissions to sam.ldb* files, fix manually") else: if 'SAMBA_SELFTEST' not in os.environ: logger.warning("""Unable to find group id for BIND, set permissions to sam.ldb* files manually""")