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