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