samba-tool drs showrepl: add a --color flag
[bbaumbach/samba-autobuild/.git] / python / samba / netcmd / drs.py
index 40981314d9ccab6b2c5c2191afee4ebe485580ab..254122a292499e853908f28fbbb27e2a30c76681 100644 (file)
@@ -1,6 +1,7 @@
 # implement samba_tool drs commands
 #
 # Copyright Andrew Tridgell 2010
+# Copyright Andrew Bartlett 2017
 #
 # based on C implementation by Kamen Mazdrashki <kamen.mazdrashki@postpath.com>
 #
@@ -21,6 +22,8 @@
 import samba.getopt as options
 import ldb
 import logging
+import common
+import json
 
 from samba.auth import system_session
 from samba.netcmd import (
@@ -32,15 +35,18 @@ from samba.netcmd import (
 from samba.samdb import SamDB
 from samba import drs_utils, nttime2string, dsdb
 from samba.dcerpc import drsuapi, misc
-import common
 from samba.join import join_clone
-from samba.messaging import IRPC_CALL_TIMEOUT_INF
+from samba.ndr import ndr_unpack
+from samba.dcerpc import drsblobs
+from samba import colour
+import logging
+
 
 def drsuapi_connect(ctx):
     '''make a DRSUAPI connection to the server'''
     try:
         (ctx.drsuapi, ctx.drsuapi_handle, ctx.bind_supported_extensions) = drs_utils.drsuapi_connect(ctx.server, ctx.lp, ctx.creds)
-    except Exception, e:
+    except Exception as e:
         raise CommandError("DRS connection to %s failed" % ctx.server, e)
 
 def samdb_connect(ctx):
@@ -49,7 +55,7 @@ def samdb_connect(ctx):
         ctx.samdb = SamDB(url="ldap://%s" % ctx.server,
                           session_info=system_session(),
                           credentials=ctx.creds, lp=ctx.lp)
-    except Exception, e:
+    except Exception as e:
         raise CommandError("LDAP connection to %s failed" % ctx.server, e)
 
 def drs_errmsg(werr):
@@ -79,8 +85,7 @@ def drs_parse_ntds_dn(ntds_dn):
     return (site, server)
 
 
-
-
+DEFAULT_SHOWREPL_FORMAT = 'classic'
 
 class cmd_drs_showrepl(Command):
     """Show replication status."""
@@ -93,71 +98,179 @@ class cmd_drs_showrepl(Command):
         "credopts": options.CredentialsOptions,
     }
 
+    takes_options = [
+        Option("--json", help="replication details in JSON format",
+               dest='format', action='store_const', const='json'),
+        Option("--classic", help="print local replication details",
+               dest='format', action='store_const', const='classic',
+               default=DEFAULT_SHOWREPL_FORMAT),
+        Option("-v", "--verbose", help="Be verbose", action="store_true"),
+        Option("--color", help="Use colour output (yes|no|auto)",
+               default='no'),
+    ]
+
     takes_args = ["DC?"]
 
-    def print_neighbour(self, n):
-        '''print one set of neighbour information'''
-        self.message("%s" % n.naming_context_dn)
+    def parse_neighbour(self, n):
+        """Convert an ldb neighbour object into a python dictionary"""
+        d = {
+            'NC dn': n.naming_context_dn,
+            "DSA objectGUID": str(n.source_dsa_obj_guid),
+            "last attempt time": nttime2string(n.last_attempt),
+            "last attempt message": drs_errmsg(n.result_last_attempt),
+            "consecutive failures": n.consecutive_sync_failures,
+            "last success": nttime2string(n.last_success),
+            "NTDS DN": str(n.source_dsa_obj_dn)
+        }
+
         try:
             (site, server) = drs_parse_ntds_dn(n.source_dsa_obj_dn)
-            self.message("\t%s\%s via RPC" % (site, server))
+            d["DSA"] = "%s\%s" % (site, server)
         except RuntimeError:
-            self.message("\tNTDS DN: %s" % n.source_dsa_obj_dn)
-        self.message("\t\tDSA object GUID: %s" % n.source_dsa_obj_guid)
-        self.message("\t\tLast attempt @ %s %s" % (nttime2string(n.last_attempt),
-                                                   drs_errmsg(n.result_last_attempt)))
-        self.message("\t\t%u consecutive failure(s)." % n.consecutive_sync_failures)
-        self.message("\t\tLast success @ %s" % nttime2string(n.last_success))
+            pass
+        return d
+
+    def print_neighbour(self, d):
+        '''print one set of neighbour information'''
+        self.message("%s" % d['NC dn'])
+        if 'DSA' in d:
+            self.message("\t%s via RPC" % d['DSA'])
+        else:
+            self.message("\tNTDS DN: %s" % d['NTDS DN'])
+        self.message("\t\tDSA object GUID: %s" % d['DSA objectGUID'])
+        self.message("\t\tLast attempt @ %s %s" % (d['last attempt time'],
+                                                   d['last attempt message']))
+        self.message("\t\t%u consecutive failure(s)." %
+                     d['consecutive failures'])
+        self.message("\t\tLast success @ %s" % d['last success'])
         self.message("")
 
-    def drsuapi_ReplicaInfo(ctx, info_type):
+    def drsuapi_ReplicaInfo(self, info_type):
         '''call a DsReplicaInfo'''
 
         req1 = drsuapi.DsReplicaGetInfoRequest1()
         req1.info_type = info_type
         try:
-            (info_type, info) = ctx.drsuapi.DsReplicaGetInfo(ctx.drsuapi_handle, 1, req1)
-        except Exception, e:
+            (info_type, info) = self.drsuapi.DsReplicaGetInfo(
+                self.drsuapi_handle, 1, req1)
+        except Exception as e:
             raise CommandError("DsReplicaGetInfo of type %u failed" % info_type, e)
         return (info_type, info)
 
     def run(self, DC=None, sambaopts=None,
-            credopts=None, versionopts=None, server=None):
-
+            credopts=None, versionopts=None,
+            format=DEFAULT_SHOWREPL_FORMAT,
+            verbose=False, color='no'):
+        self.apply_colour_choice(color)
         self.lp = sambaopts.get_loadparm()
         if DC is None:
             DC = common.netcmd_dnsname(self.lp)
         self.server = DC
         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
+        self.verbose = verbose
 
+        output_function = {
+            'json': self.json_output,
+            'classic': self.classic_output,
+        }.get(format)
+        if output_function is None:
+            raise CommandError("unknown showrepl format %s" % format)
+
+        return output_function()
+
+    def json_output(self):
+        data = self.get_local_repl_data()
+        del data['site']
+        del data['server']
+        json.dump(data, self.outf, indent=2)
+
+    def get_local_repl_data(self):
         drsuapi_connect(self)
         samdb_connect(self)
 
         # show domain information
         ntds_dn = self.samdb.get_dsServiceName()
-        server_dns = self.samdb.search(base="", scope=ldb.SCOPE_BASE, attrs=["dnsHostName"])[0]['dnsHostName'][0]
 
         (site, server) = drs_parse_ntds_dn(ntds_dn)
         try:
             ntds = self.samdb.search(base=ntds_dn, scope=ldb.SCOPE_BASE, attrs=['options', 'objectGUID', 'invocationId'])
-        except Exception, e:
+        except Exception as e:
             raise CommandError("Failed to search NTDS DN %s" % ntds_dn)
+
+        dsa_details = {
+            "options": int(attr_default(ntds[0], "options", 0)),
+            "objectGUID": self.samdb.schema_format_value(
+                "objectGUID", ntds[0]["objectGUID"][0]),
+            "invocationId": self.samdb.schema_format_value(
+                "objectGUID", ntds[0]["invocationId"][0])
+        }
+
         conn = self.samdb.search(base=ntds_dn, expression="(objectClass=nTDSConnection)")
+        info = self.drsuapi_ReplicaInfo(
+            drsuapi.DRSUAPI_DS_REPLICA_INFO_NEIGHBORS)[1]
+        repsfrom =  [self.parse_neighbour(n) for n in info.array]
+        info = self.drsuapi_ReplicaInfo(
+            drsuapi.DRSUAPI_DS_REPLICA_INFO_REPSTO)[1]
+        repsto = [self.parse_neighbour(n) for n in info.array]
+
+        conn_details = []
+        for c in conn:
+            c_rdn, sep, c_server_dn = c['fromServer'][0].partition(',')
+            d = {
+                'name': str(c['name']),
+                'remote DN': c['fromServer'][0],
+                'options': int(attr_default(c, 'options', 0)),
+                'enabled': (attr_default(c, 'enabledConnection',
+                                         'TRUE').upper() == 'TRUE')
+            }
+
+            conn_details.append(d)
+            try:
+                c_server_res = self.samdb.search(base=c_server_dn,
+                                                 scope=ldb.SCOPE_BASE,
+                                                 attrs=["dnsHostName"])
+                d['dns name'] = c_server_res[0]["dnsHostName"][0]
+            except ldb.LdbError as e:
+                (errno, _) = e.args
+                if errno == ldb.ERR_NO_SUCH_OBJECT:
+                    d['is deleted'] = True
+            except KeyError:
+                pass
+
+            d['replicates NC'] = []
+            for r in c.get('mS-DS-ReplicatesNCReason', []):
+                a = str(r).split(':')
+                d['replicates NC'].append((a[3], int(a[2])))
+
+        return {
+            'dsa': dsa_details,
+            'repsFrom': repsfrom,
+            'repsTo': repsto,
+            'NTDSConnections': conn_details,
+            'site': site,
+            'server': server
+        }
+
+    def classic_output(self):
+        data = self.get_local_repl_data()
+        dsa_details = data['dsa']
+        repsfrom = data['repsFrom']
+        repsto = data['repsTo']
+        conn_details = data['NTDSConnections']
+        site = data['site']
+        server = data['server']
 
         self.message("%s\\%s" % (site, server))
-        self.message("DSA Options: 0x%08x" % int(attr_default(ntds[0], "options", 0)))
-        self.message("DSA object GUID: %s" % self.samdb.schema_format_value("objectGUID", ntds[0]["objectGUID"][0]))
-        self.message("DSA invocationId: %s\n" % self.samdb.schema_format_value("objectGUID", ntds[0]["invocationId"][0]))
+        self.message("DSA Options: 0x%08x" % dsa_details["options"])
+        self.message("DSA object GUID: %s" % dsa_details["objectGUID"])
+        self.message("DSA invocationId: %s\n" % dsa_details["invocationId"])
 
         self.message("==== INBOUND NEIGHBORS ====\n")
-        (info_type, info) = self.drsuapi_ReplicaInfo(drsuapi.DRSUAPI_DS_REPLICA_INFO_NEIGHBORS)
-        for n in info.array:
+        for n in repsfrom:
             self.print_neighbour(n)
 
-
         self.message("==== OUTBOUND NEIGHBORS ====\n")
-        (info_type, info) = self.drsuapi_ReplicaInfo(drsuapi.DRSUAPI_DS_REPLICA_INFO_REPSTO)
-        for n in info.array:
+        for n in repsto:
             self.print_neighbour(n)
 
         reasons = ['NTDSCONN_KCC_GC_TOPOLOGY',
@@ -172,37 +285,27 @@ class cmd_drs_showrepl(Command):
                    'NTDSCONN_KCC_REDUNDANT_SERVER_TOPOLOGY']
 
         self.message("==== KCC CONNECTION OBJECTS ====\n")
-        for c in conn:
+        for d in conn_details:
             self.message("Connection --")
+            if d.get('is deleted'):
+                self.message("\tWARNING: Connection to DELETED server!")
 
-            c_rdn, sep, c_server_dn = c['fromServer'][0].partition(',')
-            try:
-                c_server_res = self.samdb.search(base=c_server_dn, scope=ldb.SCOPE_BASE, attrs=["dnsHostName"])
-                c_server_dns = c_server_res[0]["dnsHostName"][0]
-            except ldb.LdbError, (errno, _):
-                if errno == ldb.ERR_NO_SUCH_OBJECT:
-                    self.message("\tWARNING: Connection to DELETED server!")
-                c_server_dns = ""
-            except KeyError:
-                c_server_dns = ""
-
-            self.message("\tConnection name: %s" % c['name'][0])
-            self.message("\tEnabled        : %s" % attr_default(c, 'enabledConnection', 'TRUE'))
-            self.message("\tServer DNS name : %s" % c_server_dns)
-            self.message("\tServer DN name  : %s" % c['fromServer'][0])
+            self.message("\tConnection name: %s" % d['name'])
+            self.message("\tEnabled        : %s" % str(d['enabled']).upper())
+            self.message("\tServer DNS name : %s" % d['dns name'])
+            self.message("\tServer DN name  : %s" % d['remote DN'])
             self.message("\t\tTransportType: RPC")
-            self.message("\t\toptions: 0x%08X" % int(attr_default(c, 'options', 0)))
-            if not 'mS-DS-ReplicatesNCReason' in c:
+            self.message("\t\toptions: 0x%08X" % d['options'])
+
+            if d['replicates NC']:
+                for nc, reason in d['replicates NC']:
+                    self.message("\t\tReplicatesNC: %s" % nc)
+                    self.message("\t\tReason: 0x%08x" % reason)
+                    for s in reasons:
+                        if getattr(dsdb, s, 0) & reason:
+                            self.message("\t\t\t%s" % s)
+            else:
                 self.message("Warning: No NC replicated for Connection!")
-                continue
-            for r in c['mS-DS-ReplicatesNCReason']:
-                a = str(r).split(':')
-                self.message("\t\tReplicatesNC: %s" % a[3])
-                self.message("\t\tReason: 0x%08x" % int(a[2]))
-                for s in reasons:
-                    if getattr(dsdb, s, 0) & int(a[2]):
-                        self.message("\t\t\t%s" % s)
-
 
 
 class cmd_drs_kcc(Command):
@@ -219,7 +322,7 @@ class cmd_drs_kcc(Command):
     takes_args = ["DC?"]
 
     def run(self, DC=None, sambaopts=None,
-            credopts=None, versionopts=None, server=None):
+            credopts=None, versionopts=None):
 
         self.lp = sambaopts.get_loadparm()
         if DC is None:
@@ -233,54 +336,11 @@ class cmd_drs_kcc(Command):
         req1 = drsuapi.DsExecuteKCC1()
         try:
             self.drsuapi.DsExecuteKCC(self.drsuapi_handle, 1, req1)
-        except Exception, e:
+        except Exception as e:
             raise CommandError("DsExecuteKCC failed", e)
         self.message("Consistency check on %s successful." % DC)
 
 
-
-def drs_local_replicate(self, SOURCE_DC, NC):
-    '''replicate from a source DC to the local SAM'''
-
-    self.server = SOURCE_DC
-    drsuapi_connect(self)
-
-    self.local_samdb = SamDB(session_info=system_session(), url=None,
-                             credentials=self.creds, lp=self.lp)
-
-    self.samdb = SamDB(url="ldap://%s" % self.server,
-                       session_info=system_session(),
-                       credentials=self.creds, lp=self.lp)
-
-    # work out the source and destination GUIDs
-    res = self.local_samdb.search(base="", scope=ldb.SCOPE_BASE, attrs=["dsServiceName"])
-    self.ntds_dn = res[0]["dsServiceName"][0]
-
-    res = self.local_samdb.search(base=self.ntds_dn, scope=ldb.SCOPE_BASE, attrs=["objectGUID"])
-    self.ntds_guid = misc.GUID(self.samdb.schema_format_value("objectGUID", res[0]["objectGUID"][0]))
-
-
-    source_dsa_invocation_id = misc.GUID(self.samdb.get_invocation_id())
-    dest_dsa_invocation_id = misc.GUID(self.local_samdb.get_invocation_id())
-    destination_dsa_guid = self.ntds_guid
-
-    self.samdb.transaction_start()
-    repl = drs_utils.drs_Replicate("ncacn_ip_tcp:%s[seal]" % self.server, self.lp,
-                                   self.creds, self.local_samdb, dest_dsa_invocation_id)
-
-    # Work out if we are an RODC, so that a forced local replicate
-    # with the admin pw does not sync passwords
-    rodc = self.local_samdb.am_rodc()
-    try:
-        repl.replicate(NC, source_dsa_invocation_id, destination_dsa_guid, rodc=rodc)
-    except Exception, e:
-        raise CommandError("Error replicating DN %s" % NC, e)
-    self.samdb.transaction_commit()
-
-    self.message("Replicate from %s to %s was successful." % (SOURCE_DC, self.local_samdb.url))
-
-
-
 class cmd_drs_replicate(Command):
     """Replicate a naming context between two DCs."""
 
@@ -301,12 +361,82 @@ class cmd_drs_replicate(Command):
         Option("--full-sync", help="resync all objects", action="store_true"),
         Option("--local", help="pull changes directly into the local database (destination DC is ignored)", action="store_true"),
         Option("--local-online", help="pull changes into the local database (destination DC is ignored) as a normal online replication", action="store_true"),
+        Option("--async-op", help="use ASYNC_OP for the replication", action="store_true"),
+        Option("--single-object", help="Replicate only the object specified, instead of the whole Naming Context (only with --local)", action="store_true"),
         ]
 
+    def drs_local_replicate(self, SOURCE_DC, NC, full_sync=False,
+                            single_object=False,
+                            sync_forced=False):
+        '''replicate from a source DC to the local SAM'''
+
+        self.server = SOURCE_DC
+        drsuapi_connect(self)
+
+        self.local_samdb = SamDB(session_info=system_session(), url=None,
+                                 credentials=self.creds, lp=self.lp)
+
+        self.samdb = SamDB(url="ldap://%s" % self.server,
+                           session_info=system_session(),
+                           credentials=self.creds, lp=self.lp)
+
+        # work out the source and destination GUIDs
+        res = self.local_samdb.search(base="", scope=ldb.SCOPE_BASE,
+                                      attrs=["dsServiceName"])
+        self.ntds_dn = res[0]["dsServiceName"][0]
+
+        res = self.local_samdb.search(base=self.ntds_dn, scope=ldb.SCOPE_BASE,
+                                      attrs=["objectGUID"])
+        self.ntds_guid = misc.GUID(
+            self.samdb.schema_format_value("objectGUID",
+                                           res[0]["objectGUID"][0]))
+
+        source_dsa_invocation_id = misc.GUID(self.samdb.get_invocation_id())
+        dest_dsa_invocation_id = misc.GUID(self.local_samdb.get_invocation_id())
+        destination_dsa_guid = self.ntds_guid
+
+        exop = drsuapi.DRSUAPI_EXOP_NONE
+
+        if single_object:
+            exop = drsuapi.DRSUAPI_EXOP_REPL_OBJ
+            full_sync = True
+
+        self.samdb.transaction_start()
+        repl = drs_utils.drs_Replicate("ncacn_ip_tcp:%s[seal]" % self.server,
+                                       self.lp,
+                                       self.creds, self.local_samdb,
+                                       dest_dsa_invocation_id)
+
+        # Work out if we are an RODC, so that a forced local replicate
+        # with the admin pw does not sync passwords
+        rodc = self.local_samdb.am_rodc()
+        try:
+            (num_objects, num_links) = repl.replicate(NC,
+                                                      source_dsa_invocation_id,
+                                                      destination_dsa_guid,
+                                                      rodc=rodc,
+                                                      full_sync=full_sync,
+                                                      exop=exop,
+                                                      sync_forced=sync_forced)
+        except Exception as e:
+            raise CommandError("Error replicating DN %s" % NC, e)
+        self.samdb.transaction_commit()
+
+        if full_sync:
+            self.message("Full Replication of all %d objects and %d links "
+                         "from %s to %s was successful." %
+                         (num_objects, num_links, SOURCE_DC,
+                          self.local_samdb.url))
+        else:
+            self.message("Incremental replication of %d objects and %d links "
+                         "from %s to %s was successful." %
+                         (num_objects, num_links, SOURCE_DC,
+                          self.local_samdb.url))
+
     def run(self, DEST_DC, SOURCE_DC, NC,
             add_ref=False, sync_forced=False, sync_all=False, full_sync=False,
-            local=False, local_online=False,
-            sambaopts=None, credopts=None, versionopts=None, server=None):
+            local=False, local_online=False, async_op=False, single_object=False,
+            sambaopts=None, credopts=None, versionopts=None):
 
         self.server = DEST_DC
         self.lp = sambaopts.get_loadparm()
@@ -314,18 +444,23 @@ class cmd_drs_replicate(Command):
         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
 
         if local:
-            drs_local_replicate(self, SOURCE_DC, NC)
+            self.drs_local_replicate(SOURCE_DC, NC, full_sync=full_sync,
+                                     single_object=single_object,
+                                     sync_forced=sync_forced)
             return
 
         if local_online:
-            server_bind = drsuapi.drsuapi("irpc:dreplsrv", lp_ctx=self.lp,
-                                          timeout=IRPC_CALL_TIMEOUT_INF)
+            server_bind = drsuapi.drsuapi("irpc:dreplsrv", lp_ctx=self.lp)
             server_bind_handle = misc.policy_handle()
         else:
             drsuapi_connect(self)
             server_bind = self.drsuapi
             server_bind_handle = self.drsuapi_handle
 
+        if not async_op:
+            # Give the sync replication 5 minutes time
+            server_bind.request_timeout = 5 * 60
+
         samdb_connect(self)
 
         # we need to find the NTDS GUID of the source DC
@@ -358,12 +493,17 @@ class cmd_drs_replicate(Command):
             req_options |= drsuapi.DRSUAPI_DRS_SYNC_ALL
         if full_sync:
             req_options |= drsuapi.DRSUAPI_DRS_FULL_SYNC_NOW
+        if async_op:
+            req_options |= drsuapi.DRSUAPI_DRS_ASYNC_OP
 
         try:
             drs_utils.sendDsReplicaSync(server_bind, server_bind_handle, source_dsa_guid, NC, req_options)
-        except drs_utils.drsException, estr:
+        except drs_utils.drsException as estr:
             raise CommandError("DsReplicaSync failed", estr)
-        self.message("Replicate from %s to %s was successful." % (SOURCE_DC, DEST_DC))
+        if async_op:
+            self.message("Replicate from %s to %s was started." % (SOURCE_DC, DEST_DC))
+        else:
+            self.message("Replicate from %s to %s was successful." % (SOURCE_DC, DEST_DC))
 
 
 
@@ -381,7 +521,7 @@ class cmd_drs_bind(Command):
     takes_args = ["DC?"]
 
     def run(self, DC=None, sambaopts=None,
-            credopts=None, versionopts=None, server=None):
+            credopts=None, versionopts=None):
 
         self.lp = sambaopts.get_loadparm()
         if DC is None:
@@ -542,9 +682,9 @@ class cmd_drs_clone_dc_database(Command):
     takes_options = [
         Option("--server", help="DC to join", type=str),
         Option("--targetdir", help="where to store provision (required)", type=str),
-        Option("--quiet", help="Be quiet", action="store_true"),
+        Option("-q", "--quiet", help="Be quiet", action="store_true"),
         Option("--include-secrets", help="Also replicate secret values", action="store_true"),
-        Option("--verbose", help="Be verbose", action="store_true")
+        Option("-v", "--verbose", help="Be verbose", action="store_true")
        ]
 
     takes_args = ["domain"]