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