netcmd: Change domain backup commands to use s3 SMB Py bindings
[samba.git] / python / samba / netcmd / domain_backup.py
index bff2bdda783c9b3e5ae969d0d30f4cdedd6ba89f..4cacf571f3d3cfebbfe82d349f2b1a6cac46d45f 100644 (file)
@@ -25,18 +25,19 @@ import tempfile
 import samba
 import tdb
 import samba.getopt as options
-from samba.samdb import SamDB
+from samba.samdb import SamDB, get_default_backend_store
 import ldb
-from samba import smb
+from samba.samba3 import libsmb_samba_internal as libsmb
+from samba.samba3 import param as s3param
 from samba.ntacls import backup_online, backup_restore, backup_offline
 from samba.auth import system_session
 from samba.join import DCJoinContext, join_clone, DCCloneAndRenameContext
 from samba.dcerpc.security import dom_sid
 from samba.netcmd import Option, CommandError
-from samba.dcerpc import misc, security
+from samba.dcerpc import misc, security, drsblobs
 from samba import Ldb
 from . fsmo import cmd_fsmo_seize
-from samba.provision import make_smbconf
+from samba.provision import make_smbconf, DEFAULTSITE
 from samba.upgradehelpers import update_krbtgt_account_password
 from samba.remove_dc import remove_dc
 from samba.provision import secretsdb_self_join
@@ -49,8 +50,10 @@ from samba.provision.sambadns import (fill_dns_data_partitions,
 from samba.tdb_util import tdb_copy
 from samba.mdb_util import mdb_copy
 import errno
-import tdb
 from subprocess import CalledProcessError
+from samba import sites
+from samba.dsdb import _dsdb_load_udv_v2
+from samba.ndr import ndr_pack
 
 
 # work out a SID (based on a free RID) to use when the domain gets restored.
@@ -60,11 +63,11 @@ def get_sid_for_restore(samdb):
     # Find the DN of the RID set of the server
     res = samdb.search(base=ldb.Dn(samdb, samdb.get_serverName()),
                        scope=ldb.SCOPE_BASE, attrs=["serverReference"])
-    server_ref_dn = ldb.Dn(samdb, res[0]['serverReference'][0])
+    server_ref_dn = ldb.Dn(samdb, str(res[0]['serverReference'][0]))
     res = samdb.search(base=server_ref_dn,
                        scope=ldb.SCOPE_BASE,
                        attrs=['rIDSetReferences'])
-    rid_set_dn = ldb.Dn(samdb, res[0]['rIDSetReferences'][0])
+    rid_set_dn = ldb.Dn(samdb, str(res[0]['rIDSetReferences'][0]))
 
     # Get the alloc pools and next RID of the RID set
     res = samdb.search(base=rid_set_dn,
@@ -99,12 +102,20 @@ def get_sid_for_restore(samdb):
     return str(sid) + '-' + str(rid)
 
 
+def smb_sysvol_conn(server, lp, creds):
+    """Returns an SMB connection to the sysvol share on the DC"""
+    # the SMB bindings rely on having a s3 loadparm
+    s3_lp = s3param.get_context()
+    s3_lp.load(lp.configfile)
+    return libsmb.Conn(server, "sysvol", lp=s3_lp, creds=creds, sign=True)
+
+
 def get_timestamp():
     return datetime.datetime.now().isoformat().replace(':', '-')
 
 
 def backup_filepath(targetdir, name, time_str):
-    filename = 'samba-backup-{}-{}.tar.bz2'.format(name, time_str)
+    filename = 'samba-backup-%s-%s.tar.bz2' % (name, time_str)
     return os.path.join(targetdir, filename)
 
 
@@ -163,9 +174,9 @@ def set_admin_password(logger, samdb):
 
     # match the admin user by RID
     domainsid = samdb.get_domain_sid()
-    match_admin = "(objectsid={}-{})".format(domainsid,
-                                             security.DOMAIN_RID_ADMINISTRATOR)
-    search_expr = "(&(objectClass=user){})".format(match_admin)
+    match_admin = "(objectsid=%s-%s)" % (domainsid,
+                                         security.DOMAIN_RID_ADMINISTRATOR)
+    search_expr = "(&(objectClass=user)%s)" % (match_admin,)
 
     # retrieve the admin username (just in case it's been renamed)
     res = samdb.search(base=samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
@@ -207,11 +218,15 @@ class cmd_domain_backup_online(samba.netcmd.Command):
         Option("--targetdir", type=str,
                help="Directory to write the backup file to"),
         Option("--no-secrets", action="store_true", default=False,
-               help="Exclude secret values from the backup created")
+               help="Exclude secret values from the backup created"),
+        Option("--backend-store", type="choice", metavar="BACKENDSTORE",
+               choices=["tdb", "mdb"],
+               help="Specify the database backend to be used "
+               "(default is %s)" % get_default_backend_store()),
     ]
 
     def run(self, sambaopts=None, credopts=None, server=None, targetdir=None,
-            no_secrets=False):
+            no_secrets=False, backend_store=None):
         logger = self.get_logger()
         logger.setLevel(logging.DEBUG)
 
@@ -230,7 +245,8 @@ class cmd_domain_backup_online(samba.netcmd.Command):
         include_secrets = not no_secrets
         ctx = join_clone(logger=logger, creds=creds, lp=lp,
                          include_secrets=include_secrets, server=server,
-                         dns_backend='SAMBA_INTERNAL', targetdir=tmpdir)
+                         dns_backend='SAMBA_INTERNAL', targetdir=tmpdir,
+                         backend_store=backend_store)
 
         # get the paths used for the clone, then drop the old samdb connection
         paths = ctx.paths
@@ -244,7 +260,7 @@ class cmd_domain_backup_online(samba.netcmd.Command):
 
         # Grab the remote DC's sysvol files and bundle them into a tar file
         sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
-        smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
+        smb_conn = smb_sysvol_conn(server, lp, creds)
         backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
 
         # remove the default sysvol files created by the clone (we want to
@@ -256,6 +272,7 @@ class cmd_domain_backup_online(samba.netcmd.Command):
         time_str = get_timestamp()
         add_backup_marker(samdb, "backupDate", time_str)
         add_backup_marker(samdb, "sidForRestore", new_sid)
+        add_backup_marker(samdb, "backupType", "online")
 
         # ensure the admin user always has a password set (same as provision)
         if no_secrets:
@@ -295,6 +312,7 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
                help="set IPv4 ipaddress"),
         Option("--host-ip6", type="string", metavar="IP6ADDRESS",
                help="set IPv6 ipaddress"),
+        Option("--site", help="Site to add the new server in", type=str),
     ]
 
     takes_optiongroups = {
@@ -303,7 +321,7 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
     }
 
     def register_dns_zone(self, logger, samdb, lp, ntdsguid, host_ip,
-                          host_ip6):
+                          host_ip6, site):
         '''
         Registers the new realm's DNS objects when a renamed domain backup
         is restored.
@@ -330,7 +348,7 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
 
         # Add the DNS objects for the new realm (note: the backup clone already
         # has the root server objects, so don't add them again)
-        fill_dns_data_partitions(samdb, domainsid, names.sitename, domaindn,
+        fill_dns_data_partitions(samdb, domainsid, site, domaindn,
                                  forestdn, dnsdomain, dnsforest, hostname,
                                  host_ip, host_ip6, domainguid, ntdsguid,
                                  dnsadmins_sid, add_root=False)
@@ -360,8 +378,79 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
         chk.check_database(controls=controls, attrs=attrs)
         samdb.transaction_commit()
 
+    def create_default_site(self, samdb, logger):
+        '''Creates the default site, if it doesn't already exist'''
+
+        sitename = DEFAULTSITE
+        search_expr = "(&(cn={0})(objectclass=site))".format(sitename)
+        res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
+                           expression=search_expr)
+
+        if len(res) == 0:
+            logger.info("Creating default site '{0}'".format(sitename))
+            sites.create_site(samdb, samdb.get_config_basedn(), sitename)
+
+        return sitename
+
+    def remove_backup_markers(self, samdb):
+        """Remove DB markers added by the backup process"""
+
+        # check what markers we need to remove (this may vary)
+        markers = ['sidForRestore', 'backupRename', 'backupDate', 'backupType']
+        res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
+                           scope=ldb.SCOPE_BASE,
+                           attrs=markers)
+
+        # remove any markers that exist in the DB
+        m = ldb.Message()
+        m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
+
+        for attr in markers:
+            if attr in res[0]:
+                m[attr] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, attr)
+
+        samdb.modify(m)
+
+    def get_backup_type(self, samdb):
+        res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
+                           scope=ldb.SCOPE_BASE,
+                           attrs=['backupRename', 'backupType'])
+
+        # note that the backupType marker won't exist on backups created on
+        # v4.9. However, we can still infer the type, as only rename and
+        # online backups are supported on v4.9
+        if 'backupType' in res[0]:
+            backup_type = str(res[0]['backupType'])
+        elif 'backupRename' in res[0]:
+            backup_type = "rename"
+        else:
+            backup_type = "online"
+
+        return backup_type
+
+    def save_uptodate_vectors(self, samdb, partitions):
+        """Ensures the UTDV used by DRS is correct after an offline backup"""
+        for nc in partitions:
+            # load the replUpToDateVector we *should* have
+            utdv = _dsdb_load_udv_v2(samdb, nc)
+
+            # convert it to NDR format and write it into the DB
+            utdv_blob = drsblobs.replUpToDateVectorBlob()
+            utdv_blob.version = 2
+            utdv_blob.ctr.cursors = utdv
+            utdv_blob.ctr.count = len(utdv)
+            new_value = ndr_pack(utdv_blob)
+
+            m = ldb.Message()
+            m.dn = ldb.Dn(samdb, nc)
+            m["replUpToDateVector"] = ldb.MessageElement(new_value,
+                                                         ldb.FLAG_MOD_REPLACE,
+                                                         "replUpToDateVector")
+            samdb.modify(m)
+
     def run(self, sambaopts=None, credopts=None, backup_file=None,
-            targetdir=None, newservername=None, host_ip=None, host_ip6=None):
+            targetdir=None, newservername=None, host_ip=None, host_ip6=None,
+            site=None):
         if not (backup_file and os.path.exists(backup_file)):
             raise CommandError('Backup file not found.')
         if targetdir is None:
@@ -404,16 +493,32 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
         private_dir = os.path.join(targetdir, 'private')
         samdb_path = os.path.join(private_dir, 'sam.ldb')
         samdb = SamDB(url=samdb_path, session_info=system_session(), lp=lp)
+        backup_type = self.get_backup_type(samdb)
 
-        # Create account using the join_add_objects function in the join object
-        # We need namingContexts, account control flags, and the sid saved by
-        # the backup process.
+        if site is None:
+            # There's no great way to work out the correct site to add the
+            # restored DC to. By default, add it to Default-First-Site-Name,
+            # creating the site if it doesn't already exist
+            site = self.create_default_site(samdb, logger)
+            logger.info("Adding new DC to site '{0}'".format(site))
+
+        # read the naming contexts out of the DB
         res = samdb.search(base="", scope=ldb.SCOPE_BASE,
                            attrs=['namingContexts'])
         ncs = [str(r) for r in res[0].get('namingContexts')]
 
+        # for offline backups we need to make sure the upToDateness info
+        # contains the invocation-ID and highest-USN of the DC we backed up.
+        # Otherwise replication propagation dampening won't correctly filter
+        # objects created by that DC
+        if backup_type == "offline":
+            self.save_uptodate_vectors(samdb, ncs)
+
+        # Create account using the join_add_objects function in the join object
+        # We need namingContexts, account control flags, and the sid saved by
+        # the backup process.
         creds = credopts.get_credentials(lp)
-        ctx = DCJoinContext(logger, creds=creds, lp=lp,
+        ctx = DCJoinContext(logger, creds=creds, lp=lp, site=site,
                             forced_local_samdb=samdb,
                             netbios_name=newservername)
         ctx.nc_list = ncs
@@ -432,11 +537,10 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
         # Get the SID saved by the backup process and create account
         res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
                            scope=ldb.SCOPE_BASE,
-                           attrs=['sidForRestore', 'backupRename'])
-        is_rename = True if 'backupRename' in res[0] else False
+                           attrs=['sidForRestore'])
         sid = res[0].get('sidForRestore')[0]
         logger.info('Creating account with SID: ' + str(sid))
-        ctx.join_add_objects(specified_sid=dom_sid(sid))
+        ctx.join_add_objects(specified_sid=dom_sid(str(sid)))
 
         m = ldb.Message()
         m.dn = ldb.Dn(samdb, '@ROOTDSE')
@@ -449,9 +553,9 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
         # if we renamed the backed-up domain, then we need to add the DNS
         # objects for the new realm (we do this in the restore, now that we
         # know the new DC's IP address)
-        if is_rename:
+        if backup_type == "rename":
             self.register_dns_zone(logger, samdb, lp, ctx.ntds_guid,
-                                   host_ip, host_ip6)
+                                   host_ip, host_ip6, site)
 
         secrets_path = os.path.join(private_dir, 'secrets.ldb')
         secrets_ldb = Ldb(secrets_path, session_info=system_session(), lp=lp)
@@ -488,7 +592,7 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
         res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
                            expression=search_expr)
         for m in res:
-            cn = m.get('cn')[0]
+            cn = str(m.get('cn')[0])
             if cn != newservername:
                 remove_dc(samdb, logger, cn)
 
@@ -525,16 +629,7 @@ class cmd_domain_backup_restore(cmd_fsmo_seize):
         self.fix_old_dc_references(samdb)
 
         # Remove DB markers added by the backup process
-        m = ldb.Message()
-        m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
-        m["backupDate"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
-                                             "backupDate")
-        m["sidForRestore"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
-                                                "sidForRestore")
-        if is_rename:
-            m["backupRename"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE,
-                                                   "backupRename")
-        samdb.modify(m)
+        self.remove_backup_markers(samdb)
 
         logger.info("Backup file successfully restored to %s" % targetdir)
         logger.info("Please check the smb.conf settings are correct before "
@@ -584,7 +679,11 @@ class cmd_domain_backup_rename(samba.netcmd.Command):
         Option("--keep-dns-realm", action="store_true", default=False,
                help="Retain the DNS entries for the old realm in the backup"),
         Option("--no-secrets", action="store_true", default=False,
-               help="Exclude secret values from the backup created")
+               help="Exclude secret values from the backup created"),
+        Option("--backend-store", type="choice", metavar="BACKENDSTORE",
+               choices=["tdb", "mdb"],
+               help="Specify the database backend to be used "
+               "(default is %s)" % get_default_backend_store()),
     ]
 
     takes_args = ["new_domain_name", "new_dns_realm"]
@@ -603,6 +702,7 @@ class cmd_domain_backup_rename(samba.netcmd.Command):
         for res_msg in res:
             # dnsRoot can be multi-valued, so only look for the old realm
             for dns_root in res_msg["dnsRoot"]:
+                dns_root = str(dns_root)
                 dn = res_msg.dn
                 if old_realm in dns_root:
                     new_dns_root = re.sub('%s$' % old_realm, new_realm,
@@ -682,7 +782,7 @@ class cmd_domain_backup_rename(samba.netcmd.Command):
 
     def run(self, new_domain_name, new_dns_realm, sambaopts=None,
             credopts=None, server=None, targetdir=None, keep_dns_realm=False,
-            no_secrets=False):
+            no_secrets=False, backend_store=None):
         logger = self.get_logger()
         logger.setLevel(logging.INFO)
 
@@ -714,7 +814,8 @@ class cmd_domain_backup_rename(samba.netcmd.Command):
                                       creds=creds, lp=lp,
                                       include_secrets=include_secrets,
                                       dns_backend='SAMBA_INTERNAL',
-                                      server=server, targetdir=tmpdir)
+                                      server=server, targetdir=tmpdir,
+                                      backend_store=backend_store)
 
         # sanity-check we're not "renaming" the domain to the same values
         old_domain = ctx.domain_name
@@ -744,7 +845,7 @@ class cmd_domain_backup_rename(samba.netcmd.Command):
         # use the old realm) backed here, as well as default files generated
         # for the new realm as part of the clone/join.
         sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
-        smb_conn = smb.SMB(server, "sysvol", lp=lp, creds=creds)
+        smb_conn = smb_sysvol_conn(server, lp, creds)
         backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
 
         # connect to the local DB (making sure we use the new/renamed config)
@@ -756,6 +857,7 @@ class cmd_domain_backup_rename(samba.netcmd.Command):
         add_backup_marker(samdb, "backupDate", time_str)
         add_backup_marker(samdb, "sidForRestore", new_sid)
         add_backup_marker(samdb, "backupRename", old_realm)
+        add_backup_marker(samdb, "backupType", "rename")
 
         # fix up the DNS objects that are using the old dnsRoot value
         self.update_dns_root(logger, samdb, old_realm, delete_old_dns)
@@ -826,7 +928,7 @@ class cmd_domain_backup_offline(samba.netcmd.Command):
                 raise e
             raise copy_err
         if not os.path.exists(backup_path):
-            s = "tdbbackup said backup succeeded but {} not found"
+            s = "tdbbackup said backup succeeded but {0} not found"
             raise CommandError(s.format(backup_path))
 
     def offline_mdb_copy(self, path):
@@ -851,7 +953,7 @@ class cmd_domain_backup_offline(samba.netcmd.Command):
         store_label = "backendStore"
         res = samdb.search(base="@PARTITION", scope=ldb.SCOPE_BASE,
                            attrs=[store_label])
-        mdb_backend = store_label in res[0] and res[0][store_label][0] == 'mdb'
+        mdb_backend = store_label in res[0] and str(res[0][store_label][0]) == 'mdb'
 
         sam_ldb_path = os.path.join(private_dir, 'sam.ldb')
         copy_function = None
@@ -917,7 +1019,7 @@ class cmd_domain_backup_offline(samba.netcmd.Command):
 
         backup_dirs = [paths.private_dir, paths.state_dir,
                        os.path.dirname(paths.smbconf)]  # etc dir
-        logger.info('running backup on dirs: {}'.format(backup_dirs))
+        logger.info('running backup on dirs: {0}'.format(' '.join(backup_dirs)))
 
         # Recursively get all file paths in the backup directories
         all_files = []
@@ -925,6 +1027,8 @@ class cmd_domain_backup_offline(samba.netcmd.Command):
             for (working_dir, _, filenames) in os.walk(backup_dir):
                 if working_dir.startswith(paths.sysvol):
                     continue
+                if working_dir.endswith('.sock') or '.sock/' in working_dir:
+                    continue
 
                 for filename in filenames:
                     if filename in all_files:
@@ -935,6 +1039,11 @@ class cmd_domain_backup_offline(samba.netcmd.Command):
                     if filename.endswith(self.backup_ext):
                         os.remove(os.path.join(working_dir, filename))
                         continue
+
+                    # Sock files are autogenerated at runtime, ignore.
+                    if filename.endswith('.sock'):
+                        continue
+
                     all_files.append(os.path.join(working_dir, filename))
 
         # Backup secrets, sam.ldb and their downstream files
@@ -951,6 +1060,7 @@ class cmd_domain_backup_offline(samba.netcmd.Command):
         time_str = get_timestamp()
         add_backup_marker(samdb, "backupDate", time_str)
         add_backup_marker(samdb, "sidForRestore", sid)
+        add_backup_marker(samdb, "backupType", "offline")
 
         # Now handle all the LDB and TDB files that are not linked to
         # anything else.  Use transactions for LDBs.
@@ -1002,8 +1112,9 @@ class cmd_domain_backup_offline(samba.netcmd.Command):
                 tar.add(path, arcname=arc_path)
 
         tar.close()
-        os.rename(temp_tar_name, os.path.join(targetdir,
-                  'samba-backup-{}.tar.bz2'.format(time_str)))
+        os.rename(temp_tar_name,
+                  os.path.join(targetdir,
+                               'samba-backup-{0}.tar.bz2'.format(time_str)))
         os.rmdir(temp_tar_dir)
         logger.info('Backup succeeded.')