c156046b26ea98037be5d7f27dd19a0c89e62b34
[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 samba.getopt as options
27 from samba.samdb import SamDB
28 import ldb
29 from samba import smb
30 from samba.ntacls import backup_online, backup_restore
31 from samba.auth import system_session
32 from samba.join import DCJoinContext, join_clone, DCCloneAndRenameContext
33 from samba.dcerpc.security import dom_sid
34 from samba.netcmd import Option, CommandError
35 from samba.dcerpc import misc
36 from samba import Ldb
37 from fsmo import cmd_fsmo_seize
38 from samba.provision import make_smbconf
39 from samba.upgradehelpers import update_krbtgt_account_password
40 from samba.remove_dc import remove_dc
41 from samba.provision import secretsdb_self_join
42 from samba.dbchecker import dbcheck
43 import re
44 from samba.provision import guess_names, determine_host_ip, determine_host_ip6
45 from samba.provision.sambadns import (fill_dns_data_partitions,
46                                       get_dnsadmins_sid,
47                                       get_domainguid)
48
49
50 # work out a SID (based on a free RID) to use when the domain gets restored.
51 # This ensures that the restored DC's SID won't clash with any other RIDs
52 # already in use in the domain
53 def get_sid_for_restore(samdb):
54     # Find the DN of the RID set of the server
55     res = samdb.search(base=ldb.Dn(samdb, samdb.get_serverName()),
56                        scope=ldb.SCOPE_BASE, attrs=["serverReference"])
57     server_ref_dn = ldb.Dn(samdb, res[0]['serverReference'][0])
58     res = samdb.search(base=server_ref_dn,
59                        scope=ldb.SCOPE_BASE,
60                        attrs=['rIDSetReferences'])
61     rid_set_dn = ldb.Dn(samdb, res[0]['rIDSetReferences'][0])
62
63     # Get the alloc pools and next RID of the RID set
64     res = samdb.search(base=rid_set_dn,
65                        scope=ldb.SCOPE_SUBTREE,
66                        expression="(rIDNextRID=*)",
67                        attrs=['rIDAllocationPool',
68                               'rIDPreviousAllocationPool',
69                               'rIDNextRID'])
70
71     # Decode the bounds of the RID allocation pools
72     rid = int(res[0].get('rIDNextRID')[0])
73
74     def split_val(num):
75         high = (0xFFFFFFFF00000000 & int(num)) >> 32
76         low = 0x00000000FFFFFFFF & int(num)
77         return low, high
78     pool_l, pool_h = split_val(res[0].get('rIDPreviousAllocationPool')[0])
79     npool_l, npool_h = split_val(res[0].get('rIDAllocationPool')[0])
80
81     # Calculate next RID based on pool bounds
82     if rid == npool_h:
83         raise CommandError('Out of RIDs, finished AllocPool')
84     if rid == pool_h:
85         if pool_h == npool_h:
86             raise CommandError('Out of RIDs, finished PrevAllocPool.')
87         rid = npool_l
88     else:
89         rid += 1
90
91     # Construct full SID
92     sid = dom_sid(samdb.get_domain_sid())
93     return str(sid) + '-' + str(rid)
94
95
96 def get_timestamp():
97     return datetime.datetime.now().isoformat().replace(':', '-')
98
99
100 def backup_filepath(targetdir, name, time_str):
101     filename = 'samba-backup-{}-{}.tar.bz2'.format(name, time_str)
102     return os.path.join(targetdir, filename)
103
104
105 def create_backup_tar(logger, tmpdir, backup_filepath):
106     # Adds everything in the tmpdir into a new tar file
107     logger.info("Creating backup file %s..." % backup_filepath)
108     tf = tarfile.open(backup_filepath, 'w:bz2')
109     tf.add(tmpdir, arcname='./')
110     tf.close()
111
112
113 # Add a backup-specific marker to the DB with info that we'll use during
114 # the restore process
115 def add_backup_marker(samdb, marker, value):
116     m = ldb.Message()
117     m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
118     m[marker] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, marker)
119     samdb.modify(m)
120
121
122 def check_online_backup_args(logger, credopts, server, targetdir):
123     # Make sure we have all the required args.
124     u_p = {'user': credopts.creds.get_username(),
125            'pass': credopts.creds.get_password()}
126     if None in u_p.values():
127         raise CommandError("Creds required.")
128     if server is None:
129         raise CommandError('Server required')
130     if targetdir is None:
131         raise CommandError('Target directory required')
132
133     if not os.path.exists(targetdir):
134         logger.info('Creating targetdir %s...' % targetdir)
135         os.makedirs(targetdir)
136
137
138 class cmd_domain_backup_online(samba.netcmd.Command):
139     '''Copy a running DC's current DB into a backup tar file.
140
141     Takes a backup copy of the current domain from a running DC. If the domain
142     were to undergo a catastrophic failure, then the backup file can be used to
143     recover the domain. The backup created is similar to the DB that a new DC
144     would receive when it joins the domain.
145
146     Note that:
147     - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
148       and fix any errors it reports.
149     - all the domain's secrets are included in the backup file.
150     - although the DB contents can be untarred and examined manually, you need
151       to run 'samba-tool domain backup restore' before you can start a Samba DC
152       from the backup file.'''
153
154     synopsis = "%prog --server=<DC-to-backup> --targetdir=<output-dir>"
155     takes_optiongroups = {
156         "sambaopts": options.SambaOptions,
157         "credopts": options.CredentialsOptions,
158     }
159
160     takes_options = [
161         Option("--server", help="The DC to backup", type=str),
162         Option("--targetdir", type=str,
163                help="Directory to write the backup file to"),
164        ]
165
166     def run(self, sambaopts=None, credopts=None, server=None, targetdir=None):
167         logger = self.get_logger()
168         logger.setLevel(logging.DEBUG)
169
170         # Make sure we have all the required args.
171         check_online_backup_args(logger, credopts, server, targetdir)
172
173         lp = sambaopts.get_loadparm()
174         creds = credopts.get_credentials(lp)
175
176         if not os.path.exists(targetdir):
177             logger.info('Creating targetdir %s...' % targetdir)
178             os.makedirs(targetdir)
179
180         tmpdir = tempfile.mkdtemp(dir=targetdir)
181
182         # Run a clone join on the remote
183         ctx = join_clone(logger=logger, creds=creds, lp=lp,
184                          include_secrets=True, dns_backend='SAMBA_INTERNAL',
185                          server=server, targetdir=tmpdir)
186
187         # get the paths used for the clone, then drop the old samdb connection
188         paths = ctx.paths
189         del ctx
190
191         # Get a free RID to use as the new DC's SID (when it gets restored)
192         remote_sam = SamDB(url='ldap://' + server, credentials=creds,
193                            session_info=system_session(), lp=lp)
194         new_sid = get_sid_for_restore(remote_sam)
195         realm = remote_sam.domain_dns_name()
196
197         # Grab the remote DC's sysvol files and bundle them into a tar file
198         sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
199         smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
200         backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
201
202         # remove the default sysvol files created by the clone (we want to
203         # make sure we restore the sysvol.tar.gz files instead)
204         shutil.rmtree(paths.sysvol)
205
206         # Edit the downloaded sam.ldb to mark it as a backup
207         samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
208         time_str = get_timestamp()
209         add_backup_marker(samdb, "backupDate", time_str)
210         add_backup_marker(samdb, "sidForRestore", new_sid)
211
212         # Add everything in the tmpdir to the backup tar file
213         backup_file = backup_filepath(targetdir, realm, time_str)
214         create_backup_tar(logger, tmpdir, backup_file)
215
216         shutil.rmtree(tmpdir)
217
218
219 class cmd_domain_backup_restore(cmd_fsmo_seize):
220     '''Restore the domain's DB from a backup-file.
221
222     This restores a previously backed up copy of the domain's DB on a new DC.
223
224     Note that the restored DB will not contain the original DC that the backup
225     was taken from (or any other DCs in the original domain). Only the new DC
226     (specified by --newservername) will be present in the restored DB.
227
228     Samba can then be started against the restored DB. Any existing DCs for the
229     domain should be shutdown before the new DC is started. Other DCs can then
230     be joined to the new DC to recover the network.
231
232     Note that this command should be run as the root user - it will fail
233     otherwise.'''
234
235     synopsis = ("%prog --backup-file=<tar-file> --targetdir=<output-dir> "
236                 "--newservername=<DC-name>")
237     takes_options = [
238         Option("--backup-file", help="Path to backup file", type=str),
239         Option("--targetdir", help="Path to write to", type=str),
240         Option("--newservername", help="Name for new server", type=str),
241         Option("--host-ip", type="string", metavar="IPADDRESS",
242                help="set IPv4 ipaddress"),
243         Option("--host-ip6", type="string", metavar="IP6ADDRESS",
244                help="set IPv6 ipaddress"),
245     ]
246
247     takes_optiongroups = {
248         "sambaopts": options.SambaOptions,
249         "credopts": options.CredentialsOptions,
250     }
251
252     def register_dns_zone(self, logger, samdb, lp, ntdsguid, host_ip,
253                           host_ip6):
254         '''
255         Registers the new realm's DNS objects when a renamed domain backup
256         is restored.
257         '''
258         names = guess_names(lp)
259         domaindn = names.domaindn
260         forestdn = samdb.get_root_basedn().get_linearized()
261         dnsdomain = names.dnsdomain.lower()
262         dnsforest = dnsdomain
263         hostname = names.netbiosname.lower()
264         domainsid = dom_sid(samdb.get_domain_sid())
265         dnsadmins_sid = get_dnsadmins_sid(samdb, domaindn)
266         domainguid = get_domainguid(samdb, domaindn)
267
268         # work out the IP address to use for the new DC's DNS records
269         host_ip = determine_host_ip(logger, lp, host_ip)
270         host_ip6 = determine_host_ip6(logger, lp, host_ip6)
271
272         if host_ip is None and host_ip6 is None:
273             raise CommandError('Please specify a host-ip for the new server')
274
275         logger.info("DNS realm was renamed to %s" % dnsdomain)
276         logger.info("Populating DNS partitions for new realm...")
277
278         # Add the DNS objects for the new realm (note: the backup clone already
279         # has the root server objects, so don't add them again)
280         fill_dns_data_partitions(samdb, domainsid, names.sitename, domaindn,
281                                  forestdn, dnsdomain, dnsforest, hostname,
282                                  host_ip, host_ip6, domainguid, ntdsguid,
283                                  dnsadmins_sid, add_root=False)
284
285     def fix_old_dc_references(self, samdb):
286         '''Fixes attributes that reference the old/removed DCs'''
287
288         # we just want to fix up DB problems here that were introduced by us
289         # removing the old DCs. We restrict what we fix up so that the restored
290         # DB matches the backed-up DB as close as possible. (There may be other
291         # DB issues inherited from the backed-up DC, but it's not our place to
292         # silently try to fix them here).
293         samdb.transaction_start()
294         chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
295                       in_transaction=True)
296
297         # fix up stale references to the old DC
298         setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
299         attrs = ['lastKnownParent', 'interSiteTopologyGenerator']
300
301         # fix-up stale one-way links that point to the old DC
302         setattr(chk, 'remove_plausible_deleted_DN_links', 'ALL')
303         attrs += ['msDS-NC-Replica-Locations']
304
305         cross_ncs_ctrl = 'search_options:1:2'
306         controls = ['show_deleted:1', cross_ncs_ctrl]
307         chk.check_database(controls=controls, attrs=attrs)
308         samdb.transaction_commit()
309
310     def run(self, sambaopts=None, credopts=None, backup_file=None,
311             targetdir=None, newservername=None, host_ip=None, host_ip6=None):
312         if not (backup_file and os.path.exists(backup_file)):
313             raise CommandError('Backup file not found.')
314         if targetdir is None:
315             raise CommandError('Please specify a target directory')
316         # allow restoredc to install into a directory prepopulated by selftest
317         if (os.path.exists(targetdir) and os.listdir(targetdir) and
318             os.environ.get('SAMBA_SELFTEST') != '1'):
319             raise CommandError('Target directory is not empty')
320         if not newservername:
321             raise CommandError('Server name required')
322
323         logger = logging.getLogger()
324         logger.setLevel(logging.DEBUG)
325         logger.addHandler(logging.StreamHandler(sys.stdout))
326
327         # ldapcmp prefers the server's netBIOS name in upper-case
328         newservername = newservername.upper()
329
330         # extract the backup .tar to a temp directory
331         targetdir = os.path.abspath(targetdir)
332         tf = tarfile.open(backup_file)
333         tf.extractall(targetdir)
334         tf.close()
335
336         # use the smb.conf that got backed up, by default (save what was
337         # actually backed up, before we mess with it)
338         smbconf = os.path.join(targetdir, 'etc', 'smb.conf')
339         shutil.copyfile(smbconf, smbconf + ".orig")
340
341         # if a smb.conf was specified on the cmd line, then use that instead
342         cli_smbconf = sambaopts.get_loadparm_path()
343         if cli_smbconf:
344             logger.info("Using %s as restored domain's smb.conf" % cli_smbconf)
345             shutil.copyfile(cli_smbconf, smbconf)
346
347         lp = samba.param.LoadParm()
348         lp.load(smbconf)
349
350         # open a DB connection to the restored DB
351         private_dir = os.path.join(targetdir, 'private')
352         samdb_path = os.path.join(private_dir, 'sam.ldb')
353         samdb = SamDB(url=samdb_path, session_info=system_session(), lp=lp)
354
355         # Create account using the join_add_objects function in the join object
356         # We need namingContexts, account control flags, and the sid saved by
357         # the backup process.
358         res = samdb.search(base="", scope=ldb.SCOPE_BASE,
359                            attrs=['namingContexts'])
360         ncs = [str(r) for r in res[0].get('namingContexts')]
361
362         creds = credopts.get_credentials(lp)
363         ctx = DCJoinContext(logger, creds=creds, lp=lp,
364                             forced_local_samdb=samdb,
365                             netbios_name=newservername)
366         ctx.nc_list = ncs
367         ctx.full_nc_list = ncs
368         ctx.userAccountControl = (samba.dsdb.UF_SERVER_TRUST_ACCOUNT |
369                                   samba.dsdb.UF_TRUSTED_FOR_DELEGATION)
370
371         # rewrite the smb.conf to make sure it uses the new targetdir settings.
372         # (This doesn't update all filepaths in a customized config, but it
373         # corrects the same paths that get set by a new provision)
374         logger.info('Updating basic smb.conf settings...')
375         make_smbconf(smbconf, newservername, ctx.domain_name,
376                      ctx.realm, targetdir, lp=lp,
377                      serverrole="active directory domain controller")
378
379         # Get the SID saved by the backup process and create account
380         res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
381                            scope=ldb.SCOPE_BASE,
382                            attrs=['sidForRestore', 'backupRename'])
383         is_rename = True if 'backupRename' in res[0] else False
384         sid = res[0].get('sidForRestore')[0]
385         logger.info('Creating account with SID: ' + str(sid))
386         ctx.join_add_objects(specified_sid=dom_sid(sid))
387
388         m = ldb.Message()
389         m.dn = ldb.Dn(samdb, '@ROOTDSE')
390         ntds_guid = str(ctx.ntds_guid)
391         m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % ntds_guid,
392                                                 ldb.FLAG_MOD_REPLACE,
393                                                 "dsServiceName")
394         samdb.modify(m)
395
396         # if we renamed the backed-up domain, then we need to add the DNS
397         # objects for the new realm (we do this in the restore, now that we
398         # know the new DC's IP address)
399         if is_rename:
400             self.register_dns_zone(logger, samdb, lp, ctx.ntds_guid,
401                                    host_ip, host_ip6)
402
403         secrets_path = os.path.join(private_dir, 'secrets.ldb')
404         secrets_ldb = Ldb(secrets_path, session_info=system_session(), lp=lp)
405         secretsdb_self_join(secrets_ldb, domain=ctx.domain_name,
406                             realm=ctx.realm, dnsdomain=ctx.dnsdomain,
407                             netbiosname=ctx.myname, domainsid=ctx.domsid,
408                             machinepass=ctx.acct_pass,
409                             key_version_number=ctx.key_version_number,
410                             secure_channel_type=misc.SEC_CHAN_BDC)
411
412         # Seize DNS roles
413         domain_dn = samdb.domain_dn()
414         forest_dn = samba.dn_from_dns_name(samdb.forest_dns_name())
415         domaindns_dn = ("CN=Infrastructure,DC=DomainDnsZones,", domain_dn)
416         forestdns_dn = ("CN=Infrastructure,DC=ForestDnsZones,", forest_dn)
417         for dn_prefix, dns_dn in [forestdns_dn, domaindns_dn]:
418             if dns_dn not in ncs:
419                 continue
420             full_dn = dn_prefix + dns_dn
421             m = ldb.Message()
422             m.dn = ldb.Dn(samdb, full_dn)
423             m["fSMORoleOwner"] = ldb.MessageElement(samdb.get_dsServiceName(),
424                                                     ldb.FLAG_MOD_REPLACE,
425                                                     "fSMORoleOwner")
426             samdb.modify(m)
427
428         # Seize other roles
429         for role in ['rid', 'pdc', 'naming', 'infrastructure', 'schema']:
430             self.seize_role(role, samdb, force=True)
431
432         # Get all DCs and remove them (this ensures these DCs cannot
433         # replicate because they will not have a password)
434         search_expr = "(&(objectClass=Server)(serverReference=*))"
435         res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
436                            expression=search_expr)
437         for m in res:
438             cn = m.get('cn')[0]
439             if cn != newservername:
440                 remove_dc(samdb, logger, cn)
441
442         # Remove the repsFrom and repsTo from each NC to ensure we do
443         # not try (and fail) to talk to the old DCs
444         for nc in ncs:
445             msg = ldb.Message()
446             msg.dn = ldb.Dn(samdb, nc)
447
448             msg["repsFrom"] = ldb.MessageElement([],
449                                                  ldb.FLAG_MOD_REPLACE,
450                                                  "repsFrom")
451             msg["repsTo"] = ldb.MessageElement([],
452                                                ldb.FLAG_MOD_REPLACE,
453                                                "repsTo")
454             samdb.modify(msg)
455
456         # Update the krbtgt passwords twice, ensuring no tickets from
457         # the old domain are valid
458         update_krbtgt_account_password(samdb)
459         update_krbtgt_account_password(samdb)
460
461         # restore the sysvol directory from the backup tar file, including the
462         # original NTACLs. Note that the backup_restore() will fail if not root
463         sysvol_tar = os.path.join(targetdir, 'sysvol.tar.gz')
464         dest_sysvol_dir = lp.get('path', 'sysvol')
465         if not os.path.exists(dest_sysvol_dir):
466             os.makedirs(dest_sysvol_dir)
467         backup_restore(sysvol_tar, dest_sysvol_dir, samdb, smbconf)
468         os.remove(sysvol_tar)
469
470         # fix up any stale links to the old DCs we just removed
471         logger.info("Fixing up any remaining references to the old DCs...")
472         self.fix_old_dc_references(samdb)
473
474         # Remove DB markers added by the backup process
475         m = ldb.Message()
476         m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
477         m["backupDate"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
478                                              "backupDate")
479         m["sidForRestore"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
480                                                 "sidForRestore")
481         if is_rename:
482             m["backupRename"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
483                                                    "backupRename")
484         samdb.modify(m)
485
486         logger.info("Backup file successfully restored to %s" % targetdir)
487         logger.info("Please check the smb.conf settings are correct before "
488                     "starting samba.")
489
490
491 class cmd_domain_backup_rename(samba.netcmd.Command):
492     '''Copy a running DC's DB to backup file, renaming the domain in the process.
493
494     Where <new-domain> is the new domain's NetBIOS name, and <new-dnsrealm> is
495     the new domain's realm in DNS form.
496
497     This is similar to 'samba-tool backup online' in that it clones the DB of a
498     running DC. However, this option also renames all the domain entries in the
499     DB. Renaming the domain makes it possible to restore and start a new Samba
500     DC without it interfering with the existing Samba domain. In other words,
501     you could use this option to clone your production samba domain and restore
502     it to a separate pre-production environment that won't overlap or interfere
503     with the existing production Samba domain.
504
505     Note that:
506     - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
507       and fix any errors it reports.
508     - all the domain's secrets are included in the backup file.
509     - although the DB contents can be untarred and examined manually, you need
510       to run 'samba-tool domain backup restore' before you can start a Samba DC
511       from the backup file.
512     - GPO and sysvol information will still refer to the old realm and will
513       need to be updated manually.
514     - if you specify 'keep-dns-realm', then the DNS records will need updating
515       in order to work (they will still refer to the old DC's IP instead of the
516       new DC's address).
517     - we recommend that you only use this option if you know what you're doing.
518     '''
519
520     synopsis = ("%prog <new-domain> <new-dnsrealm> --server=<DC-to-backup> "
521                 "--targetdir=<output-dir>")
522     takes_optiongroups = {
523         "sambaopts": options.SambaOptions,
524         "credopts": options.CredentialsOptions,
525     }
526
527     takes_options = [
528         Option("--server", help="The DC to backup", type=str),
529         Option("--targetdir", help="Directory to write the backup file",
530                type=str),
531         Option("--keep-dns-realm", action="store_true", default=False,
532                help="Retain the DNS entries for the old realm in the backup"),
533        ]
534
535     takes_args = ["new_domain_name", "new_dns_realm"]
536
537     def update_dns_root(self, logger, samdb, old_realm, delete_old_dns):
538         '''Updates dnsRoot for the partition objects to reflect the rename'''
539
540         # lookup the crossRef objects that hold the old realm's dnsRoot
541         partitions_dn = samdb.get_partitions_dn()
542         res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
543                            attrs=["dnsRoot"],
544                            expression='(&(objectClass=crossRef)(dnsRoot=*))')
545         new_realm = samdb.domain_dns_name()
546
547         # go through and add the new realm
548         for res_msg in res:
549             # dnsRoot can be multi-valued, so only look for the old realm
550             for dns_root in res_msg["dnsRoot"]:
551                 dn = res_msg.dn
552                 if old_realm in dns_root:
553                     new_dns_root = re.sub('%s$' % old_realm, new_realm,
554                                           dns_root)
555                     logger.info("Adding %s dnsRoot to %s" % (new_dns_root, dn))
556
557                     m = ldb.Message()
558                     m.dn = dn
559                     m["dnsRoot"] = ldb.MessageElement(new_dns_root,
560                                                       ldb.FLAG_MOD_ADD,
561                                                       "dnsRoot")
562                     samdb.modify(m)
563
564                     # optionally remove the dnsRoot for the old realm
565                     if delete_old_dns:
566                         logger.info("Removing %s dnsRoot from %s" % (dns_root,
567                                                                      dn))
568                         m["dnsRoot"] = ldb.MessageElement(dns_root,
569                                                           ldb.FLAG_MOD_DELETE,
570                                                           "dnsRoot")
571                         samdb.modify(m)
572
573     # Updates the CN=<domain>,CN=Partitions,CN=Configuration,... object to
574     # reflect the domain rename
575     def rename_domain_partition(self, logger, samdb, new_netbios_name):
576         '''Renames the domain parition object and updates its nETBIOSName'''
577
578         # lookup the crossRef object that holds the nETBIOSName (nCName has
579         # already been updated by this point, but the netBIOS hasn't)
580         base_dn = samdb.get_default_basedn()
581         nc_name = ldb.binary_encode(str(base_dn))
582         partitions_dn = samdb.get_partitions_dn()
583         res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
584                            attrs=["nETBIOSName"],
585                            expression='ncName=%s' % nc_name)
586
587         logger.info("Changing backup domain's NetBIOS name to %s" %
588                     new_netbios_name)
589         m = ldb.Message()
590         m.dn = res[0].dn
591         m["nETBIOSName"] = ldb.MessageElement(new_netbios_name,
592                                               ldb.FLAG_MOD_REPLACE,
593                                               "nETBIOSName")
594         samdb.modify(m)
595
596         # renames the object itself to reflect the change in domain
597         new_dn = "CN=%s,%s" % (new_netbios_name, partitions_dn)
598         logger.info("Renaming %s --> %s" % (res[0].dn, new_dn))
599         samdb.rename(res[0].dn, new_dn, controls=['relax:0'])
600
601     def delete_old_dns_zones(self, logger, samdb, old_realm):
602         # remove the top-level DNS entries for the old realm
603         basedn = samdb.get_default_basedn()
604         dn = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (old_realm, basedn)
605         logger.info("Deleting old DNS zone %s" % dn)
606         samdb.delete(dn, ["tree_delete:1"])
607
608         forestdn = samdb.get_root_basedn().get_linearized()
609         dn = "DC=_msdcs.%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (old_realm,
610                                                                     forestdn)
611         logger.info("Deleting old DNS zone %s" % dn)
612         samdb.delete(dn, ["tree_delete:1"])
613
614     def fix_old_dn_attributes(self, samdb):
615         '''Fixes attributes (i.e. objectCategory) that still use the old DN'''
616
617         samdb.transaction_start()
618         # Just fix any mismatches in DN detected (leave any other errors)
619         chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
620                       in_transaction=True)
621         # fix up incorrect objectCategory/etc attributes
622         setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
623         cross_ncs_ctrl = 'search_options:1:2'
624         controls = ['show_deleted:1', cross_ncs_ctrl]
625         chk.check_database(controls=controls)
626         samdb.transaction_commit()
627
628     def run(self, new_domain_name, new_dns_realm, sambaopts=None,
629             credopts=None, server=None, targetdir=None, keep_dns_realm=False):
630         logger = self.get_logger()
631         logger.setLevel(logging.INFO)
632
633         # Make sure we have all the required args.
634         check_online_backup_args(logger, credopts, server, targetdir)
635         delete_old_dns = not keep_dns_realm
636
637         new_dns_realm = new_dns_realm.lower()
638         new_domain_name = new_domain_name.upper()
639
640         new_base_dn = samba.dn_from_dns_name(new_dns_realm)
641         logger.info("New realm for backed up domain: %s" % new_dns_realm)
642         logger.info("New base DN for backed up domain: %s" % new_base_dn)
643         logger.info("New domain NetBIOS name: %s" % new_domain_name)
644
645         tmpdir = tempfile.mkdtemp(dir=targetdir)
646
647         # Clone and rename the remote server
648         lp = sambaopts.get_loadparm()
649         creds = credopts.get_credentials(lp)
650         ctx = DCCloneAndRenameContext(new_base_dn, new_domain_name,
651                                       new_dns_realm, logger=logger,
652                                       creds=creds, lp=lp, include_secrets=True,
653                                       dns_backend='SAMBA_INTERNAL',
654                                       server=server, targetdir=tmpdir)
655         ctx.do_join()
656
657         # get the paths used for the clone, then drop the old samdb connection
658         del ctx.local_samdb
659         paths = ctx.paths
660
661         # get a free RID to use as the new DC's SID (when it gets restored)
662         remote_sam = SamDB(url='ldap://' + server, credentials=creds,
663                            session_info=system_session(), lp=lp)
664         new_sid = get_sid_for_restore(remote_sam)
665         old_realm = remote_sam.domain_dns_name()
666
667         # Grab the remote DC's sysvol files and bundle them into a tar file.
668         # Note we end up with 2 sysvol dirs - the original domain's files (that
669         # use the old realm) backed here, as well as default files generated
670         # for the new realm as part of the clone/join.
671         sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
672         smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
673         backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
674
675         # connect to the local DB (making sure we use the new/renamed config)
676         lp.load(paths.smbconf)
677         samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp)
678
679         # Edit the cloned sam.ldb to mark it as a backup
680         time_str = get_timestamp()
681         add_backup_marker(samdb, "backupDate", time_str)
682         add_backup_marker(samdb, "sidForRestore", new_sid)
683         add_backup_marker(samdb, "backupRename", old_realm)
684
685         # fix up the DNS objects that are using the old dnsRoot value
686         self.update_dns_root(logger, samdb, old_realm, delete_old_dns)
687
688         # update the netBIOS name and the Partition object for the domain
689         self.rename_domain_partition(logger, samdb, new_domain_name)
690
691         if delete_old_dns:
692             self.delete_old_dns_zones(logger, samdb, old_realm)
693
694         logger.info("Fixing DN attributes after rename...")
695         self.fix_old_dn_attributes(samdb)
696
697         # Add everything in the tmpdir to the backup tar file
698         backup_file = backup_filepath(targetdir, new_dns_realm, time_str)
699         create_backup_tar(logger, tmpdir, backup_file)
700
701         shutil.rmtree(tmpdir)
702
703
704 class cmd_domain_backup(samba.netcmd.SuperCommand):
705     '''Create or restore a backup of the domain.'''
706     subcommands = {'online': cmd_domain_backup_online(),
707                    'rename': cmd_domain_backup_rename(),
708                    'restore': cmd_domain_backup_restore()}