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