c937c70eb66e776d23e93dd15171e3c9e70223c1
[metze/samba/wip.git] / python / samba / netcmd / drs.py
1 # implement samba_tool drs commands
2 #
3 # Copyright Andrew Tridgell 2010
4 # Copyright Andrew Bartlett 2017
5 #
6 # based on C implementation by Kamen Mazdrashki <kamen.mazdrashki@postpath.com>
7 #
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 from __future__ import print_function
22
23 import samba.getopt as options
24 import ldb
25 import logging
26 from . import common
27 import json
28
29 from samba.auth import system_session
30 from samba.netcmd import (
31     Command,
32     CommandError,
33     Option,
34     SuperCommand,
35 )
36 from samba.samdb import SamDB
37 from samba import drs_utils, nttime2string, dsdb
38 from samba.dcerpc import drsuapi, misc
39 from samba.join import join_clone
40 from samba import colour
41
42 from samba.uptodateness import (
43     get_partition_maps,
44     get_utdv_edges,
45     get_utdv_distances,
46     get_utdv_summary,
47     get_kcc_and_dsas,
48 )
49
50
51 def drsuapi_connect(ctx):
52     '''make a DRSUAPI connection to the server'''
53     try:
54         (ctx.drsuapi, ctx.drsuapi_handle, ctx.bind_supported_extensions) = drs_utils.drsuapi_connect(ctx.server, ctx.lp, ctx.creds)
55     except Exception as e:
56         raise CommandError("DRS connection to %s failed" % ctx.server, e)
57
58
59 def samdb_connect(ctx):
60     '''make a ldap connection to the server'''
61     try:
62         ctx.samdb = SamDB(url="ldap://%s" % ctx.server,
63                           session_info=system_session(),
64                           credentials=ctx.creds, lp=ctx.lp)
65     except Exception as e:
66         raise CommandError("LDAP connection to %s failed" % ctx.server, e)
67
68
69 def drs_errmsg(werr):
70     '''return "was successful" or an error string'''
71     (ecode, estring) = werr
72     if ecode == 0:
73         return "was successful"
74     return "failed, result %u (%s)" % (ecode, estring)
75
76
77 def attr_default(msg, attrname, default):
78     '''get an attribute from a ldap msg with a default'''
79     if attrname in msg:
80         return msg[attrname][0]
81     return default
82
83
84 def drs_parse_ntds_dn(ntds_dn):
85     '''parse a NTDS DN returning a site and server'''
86     a = ntds_dn.split(',')
87     if a[0] != "CN=NTDS Settings" or a[2] != "CN=Servers" or a[4] != 'CN=Sites':
88         raise RuntimeError("bad NTDS DN %s" % ntds_dn)
89     server = a[1].split('=')[1]
90     site   = a[3].split('=')[1]
91     return (site, server)
92
93
94 DEFAULT_SHOWREPL_FORMAT = 'classic'
95
96
97 class cmd_drs_showrepl(Command):
98     """Show replication status."""
99
100     synopsis = "%prog [<DC>] [options]"
101
102     takes_optiongroups = {
103         "sambaopts": options.SambaOptions,
104         "versionopts": options.VersionOptions,
105         "credopts": options.CredentialsOptions,
106     }
107
108     takes_options = [
109         Option("--json", help="replication details in JSON format",
110                dest='format', action='store_const', const='json'),
111         Option("--summary", help=("summarize overall DRS health as seen "
112                                   "from this server"),
113                dest='format', action='store_const', const='summary'),
114         Option("--pull-summary", help=("Have we successfully replicated "
115                                        "from all relevent servers?"),
116                dest='format', action='store_const', const='pull_summary'),
117         Option("--notify-summary", action='store_const',
118                const='notify_summary', dest='format',
119                help=("Have we successfully notified all relevent servers of "
120                      "local changes, and did they say they successfully "
121                      "replicated?")),
122         Option("--classic", help="print local replication details",
123                dest='format', action='store_const', const='classic',
124                default=DEFAULT_SHOWREPL_FORMAT),
125         Option("-v", "--verbose", help="Be verbose", action="store_true"),
126         Option("--color", help="Use colour output (yes|no|auto)",
127                default='no'),
128     ]
129
130     takes_args = ["DC?"]
131
132     def parse_neighbour(self, n):
133         """Convert an ldb neighbour object into a python dictionary"""
134         dsa_objectguid = str(n.source_dsa_obj_guid)
135         d = {
136             'NC dn': n.naming_context_dn,
137             "DSA objectGUID": dsa_objectguid,
138             "last attempt time": nttime2string(n.last_attempt),
139             "last attempt message": drs_errmsg(n.result_last_attempt),
140             "consecutive failures": n.consecutive_sync_failures,
141             "last success": nttime2string(n.last_success),
142             "NTDS DN": str(n.source_dsa_obj_dn),
143             'is deleted': False
144         }
145
146         try:
147             self.samdb.search(base="<GUID=%s>" % dsa_objectguid,
148                               scope=ldb.SCOPE_BASE,
149                               attrs=[])
150         except ldb.LdbError as e:
151             (errno, _) = e.args
152             if errno == ldb.ERR_NO_SUCH_OBJECT:
153                 d['is deleted'] = True
154             else:
155                 raise
156         try:
157             (site, server) = drs_parse_ntds_dn(n.source_dsa_obj_dn)
158             d["DSA"] = "%s\%s" % (site, server)
159         except RuntimeError:
160             pass
161         return d
162
163     def print_neighbour(self, d):
164         '''print one set of neighbour information'''
165         self.message("%s" % d['NC dn'])
166         if 'DSA' in d:
167             self.message("\t%s via RPC" % d['DSA'])
168         else:
169             self.message("\tNTDS DN: %s" % d['NTDS DN'])
170         self.message("\t\tDSA object GUID: %s" % d['DSA objectGUID'])
171         self.message("\t\tLast attempt @ %s %s" % (d['last attempt time'],
172                                                    d['last attempt message']))
173         self.message("\t\t%u consecutive failure(s)." %
174                      d['consecutive failures'])
175         self.message("\t\tLast success @ %s" % d['last success'])
176         self.message("")
177
178     def get_neighbours(self, info_type):
179         req1 = drsuapi.DsReplicaGetInfoRequest1()
180         req1.info_type = info_type
181         try:
182             (info_type, info) = self.drsuapi.DsReplicaGetInfo(
183                 self.drsuapi_handle, 1, req1)
184         except Exception as e:
185             raise CommandError("DsReplicaGetInfo of type %u failed" % info_type, e)
186
187         reps = [self.parse_neighbour(n) for n in info.array]
188         return reps
189
190     def run(self, DC=None, sambaopts=None,
191             credopts=None, versionopts=None,
192             format=DEFAULT_SHOWREPL_FORMAT,
193             verbose=False, color='no'):
194         self.apply_colour_choice(color)
195         self.lp = sambaopts.get_loadparm()
196         if DC is None:
197             DC = common.netcmd_dnsname(self.lp)
198         self.server = DC
199         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
200         self.verbose = verbose
201
202         output_function = {
203             'summary': self.summary_output,
204             'notify_summary': self.notify_summary_output,
205             'pull_summary': self.pull_summary_output,
206             'json': self.json_output,
207             'classic': self.classic_output,
208         }.get(format)
209         if output_function is None:
210             raise CommandError("unknown showrepl format %s" % format)
211
212         return output_function()
213
214     def json_output(self):
215         data = self.get_local_repl_data()
216         del data['site']
217         del data['server']
218         json.dump(data, self.outf, indent=2)
219
220     def summary_output_handler(self, typeof_output):
221         """Print a short message if every seems fine, but print details of any
222         links that seem broken."""
223         failing_repsto = []
224         failing_repsfrom = []
225
226         local_data = self.get_local_repl_data()
227
228         if typeof_output != "pull_summary":
229             for rep in local_data['repsTo']:
230                 if rep['is deleted']:
231                     continue
232                 if rep["consecutive failures"] != 0 or rep["last success"] == 0:
233                     failing_repsto.append(rep)
234
235         if typeof_output != "notify_summary":
236             for rep in local_data['repsFrom']:
237                 if rep['is deleted']:
238                     continue
239                 if rep["consecutive failures"] != 0 or rep["last success"] == 0:
240                     failing_repsfrom.append(rep)
241
242         if failing_repsto or failing_repsfrom:
243             self.message(colour.c_RED("There are failing connections"))
244             if failing_repsto:
245                 self.message(colour.c_RED("Failing outbound connections:"))
246                 for rep in failing_repsto:
247                     self.print_neighbour(rep)
248             if failing_repsfrom:
249                 self.message(colour.c_RED("Failing inbound connection:"))
250                 for rep in failing_repsfrom:
251                     self.print_neighbour(rep)
252
253             return 1
254
255         self.message(colour.c_GREEN("[ALL GOOD]"))
256
257     def summary_output(self):
258         return self.summary_output_handler("summary")
259
260     def notify_summary_output(self):
261         return self.summary_output_handler("notify_summary")
262
263     def pull_summary_output(self):
264         return self.summary_output_handler("pull_summary")
265
266     def get_local_repl_data(self):
267         drsuapi_connect(self)
268         samdb_connect(self)
269
270         # show domain information
271         ntds_dn = self.samdb.get_dsServiceName()
272
273         (site, server) = drs_parse_ntds_dn(ntds_dn)
274         try:
275             ntds = self.samdb.search(base=ntds_dn, scope=ldb.SCOPE_BASE, attrs=['options', 'objectGUID', 'invocationId'])
276         except Exception as e:
277             raise CommandError("Failed to search NTDS DN %s" % ntds_dn)
278
279         dsa_details = {
280             "options": int(attr_default(ntds[0], "options", 0)),
281             "objectGUID": self.samdb.schema_format_value(
282                 "objectGUID", ntds[0]["objectGUID"][0]),
283             "invocationId": self.samdb.schema_format_value(
284                 "objectGUID", ntds[0]["invocationId"][0])
285         }
286
287         conn = self.samdb.search(base=ntds_dn, expression="(objectClass=nTDSConnection)")
288         repsfrom = self.get_neighbours(drsuapi.DRSUAPI_DS_REPLICA_INFO_NEIGHBORS)
289         repsto = self.get_neighbours(drsuapi.DRSUAPI_DS_REPLICA_INFO_REPSTO)
290
291         conn_details = []
292         for c in conn:
293             c_rdn, sep, c_server_dn = c['fromServer'][0].partition(',')
294             d = {
295                 'name': str(c['name']),
296                 'remote DN': c['fromServer'][0],
297                 'options': int(attr_default(c, 'options', 0)),
298                 'enabled': (attr_default(c, 'enabledConnection',
299                                          'TRUE').upper() == 'TRUE')
300             }
301
302             conn_details.append(d)
303             try:
304                 c_server_res = self.samdb.search(base=c_server_dn,
305                                                  scope=ldb.SCOPE_BASE,
306                                                  attrs=["dnsHostName"])
307                 d['dns name'] = c_server_res[0]["dnsHostName"][0]
308             except ldb.LdbError as e:
309                 (errno, _) = e.args
310                 if errno == ldb.ERR_NO_SUCH_OBJECT:
311                     d['is deleted'] = True
312             except (KeyError, IndexError):
313                 pass
314
315             d['replicates NC'] = []
316             for r in c.get('mS-DS-ReplicatesNCReason', []):
317                 a = str(r).split(':')
318                 d['replicates NC'].append((a[3], int(a[2])))
319
320         return {
321             'dsa': dsa_details,
322             'repsFrom': repsfrom,
323             'repsTo': repsto,
324             'NTDSConnections': conn_details,
325             'site': site,
326             'server': server
327         }
328
329     def classic_output(self):
330         data = self.get_local_repl_data()
331         dsa_details = data['dsa']
332         repsfrom = data['repsFrom']
333         repsto = data['repsTo']
334         conn_details = data['NTDSConnections']
335         site = data['site']
336         server = data['server']
337
338         self.message("%s\\%s" % (site, server))
339         self.message("DSA Options: 0x%08x" % dsa_details["options"])
340         self.message("DSA object GUID: %s" % dsa_details["objectGUID"])
341         self.message("DSA invocationId: %s\n" % dsa_details["invocationId"])
342
343         self.message("==== INBOUND NEIGHBORS ====\n")
344         for n in repsfrom:
345             self.print_neighbour(n)
346
347         self.message("==== OUTBOUND NEIGHBORS ====\n")
348         for n in repsto:
349             self.print_neighbour(n)
350
351         reasons = ['NTDSCONN_KCC_GC_TOPOLOGY',
352                    'NTDSCONN_KCC_RING_TOPOLOGY',
353                    'NTDSCONN_KCC_MINIMIZE_HOPS_TOPOLOGY',
354                    'NTDSCONN_KCC_STALE_SERVERS_TOPOLOGY',
355                    'NTDSCONN_KCC_OSCILLATING_CONNECTION_TOPOLOGY',
356                    'NTDSCONN_KCC_INTERSITE_GC_TOPOLOGY',
357                    'NTDSCONN_KCC_INTERSITE_TOPOLOGY',
358                    'NTDSCONN_KCC_SERVER_FAILOVER_TOPOLOGY',
359                    'NTDSCONN_KCC_SITE_FAILOVER_TOPOLOGY',
360                    'NTDSCONN_KCC_REDUNDANT_SERVER_TOPOLOGY']
361
362         self.message("==== KCC CONNECTION OBJECTS ====\n")
363         for d in conn_details:
364             self.message("Connection --")
365             if d.get('is deleted'):
366                 self.message("\tWARNING: Connection to DELETED server!")
367
368             self.message("\tConnection name: %s" % d['name'])
369             self.message("\tEnabled        : %s" % str(d['enabled']).upper())
370             self.message("\tServer DNS name : %s" % d.get('dns name'))
371             self.message("\tServer DN name  : %s" % d['remote DN'])
372             self.message("\t\tTransportType: RPC")
373             self.message("\t\toptions: 0x%08X" % d['options'])
374
375             if d['replicates NC']:
376                 for nc, reason in d['replicates NC']:
377                     self.message("\t\tReplicatesNC: %s" % nc)
378                     self.message("\t\tReason: 0x%08x" % reason)
379                     for s in reasons:
380                         if getattr(dsdb, s, 0) & reason:
381                             self.message("\t\t\t%s" % s)
382             else:
383                 self.message("Warning: No NC replicated for Connection!")
384
385
386 class cmd_drs_kcc(Command):
387     """Trigger knowledge consistency center run."""
388
389     synopsis = "%prog [<DC>] [options]"
390
391     takes_optiongroups = {
392         "sambaopts": options.SambaOptions,
393         "versionopts": options.VersionOptions,
394         "credopts": options.CredentialsOptions,
395     }
396
397     takes_args = ["DC?"]
398
399     def run(self, DC=None, sambaopts=None,
400             credopts=None, versionopts=None):
401
402         self.lp = sambaopts.get_loadparm()
403         if DC is None:
404             DC = common.netcmd_dnsname(self.lp)
405         self.server = DC
406
407         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
408
409         drsuapi_connect(self)
410
411         req1 = drsuapi.DsExecuteKCC1()
412         try:
413             self.drsuapi.DsExecuteKCC(self.drsuapi_handle, 1, req1)
414         except Exception as e:
415             raise CommandError("DsExecuteKCC failed", e)
416         self.message("Consistency check on %s successful." % DC)
417
418
419 class cmd_drs_replicate(Command):
420     """Replicate a naming context between two DCs."""
421
422     synopsis = "%prog <destinationDC> <sourceDC> <NC> [options]"
423
424     takes_optiongroups = {
425         "sambaopts": options.SambaOptions,
426         "versionopts": options.VersionOptions,
427         "credopts": options.CredentialsOptions,
428     }
429
430     takes_args = ["DEST_DC", "SOURCE_DC", "NC"]
431
432     takes_options = [
433         Option("--add-ref", help="use ADD_REF to add to repsTo on source", action="store_true"),
434         Option("--sync-forced", help="use SYNC_FORCED to force inbound replication", action="store_true"),
435         Option("--sync-all", help="use SYNC_ALL to replicate from all DCs", action="store_true"),
436         Option("--full-sync", help="resync all objects", action="store_true"),
437         Option("--local", help="pull changes directly into the local database (destination DC is ignored)", action="store_true"),
438         Option("--local-online", help="pull changes into the local database (destination DC is ignored) as a normal online replication", action="store_true"),
439         Option("--async-op", help="use ASYNC_OP for the replication", action="store_true"),
440         Option("--single-object", help="Replicate only the object specified, instead of the whole Naming Context (only with --local)", action="store_true"),
441     ]
442
443     def drs_local_replicate(self, SOURCE_DC, NC, full_sync=False,
444                             single_object=False,
445                             sync_forced=False):
446         '''replicate from a source DC to the local SAM'''
447
448         self.server = SOURCE_DC
449         drsuapi_connect(self)
450
451         self.local_samdb = SamDB(session_info=system_session(), url=None,
452                                  credentials=self.creds, lp=self.lp)
453
454         self.samdb = SamDB(url="ldap://%s" % self.server,
455                            session_info=system_session(),
456                            credentials=self.creds, lp=self.lp)
457
458         # work out the source and destination GUIDs
459         res = self.local_samdb.search(base="", scope=ldb.SCOPE_BASE,
460                                       attrs=["dsServiceName"])
461         self.ntds_dn = res[0]["dsServiceName"][0]
462
463         res = self.local_samdb.search(base=self.ntds_dn, scope=ldb.SCOPE_BASE,
464                                       attrs=["objectGUID"])
465         self.ntds_guid = misc.GUID(
466             self.samdb.schema_format_value("objectGUID",
467                                            res[0]["objectGUID"][0]))
468
469         source_dsa_invocation_id = misc.GUID(self.samdb.get_invocation_id())
470         dest_dsa_invocation_id = misc.GUID(self.local_samdb.get_invocation_id())
471         destination_dsa_guid = self.ntds_guid
472
473         exop = drsuapi.DRSUAPI_EXOP_NONE
474
475         if single_object:
476             exop = drsuapi.DRSUAPI_EXOP_REPL_OBJ
477             full_sync = True
478
479         self.samdb.transaction_start()
480         repl = drs_utils.drs_Replicate("ncacn_ip_tcp:%s[seal]" % self.server,
481                                        self.lp,
482                                        self.creds, self.local_samdb,
483                                        dest_dsa_invocation_id)
484
485         # Work out if we are an RODC, so that a forced local replicate
486         # with the admin pw does not sync passwords
487         rodc = self.local_samdb.am_rodc()
488         try:
489             (num_objects, num_links) = repl.replicate(NC,
490                                                       source_dsa_invocation_id,
491                                                       destination_dsa_guid,
492                                                       rodc=rodc,
493                                                       full_sync=full_sync,
494                                                       exop=exop,
495                                                       sync_forced=sync_forced)
496         except Exception as e:
497             raise CommandError("Error replicating DN %s" % NC, e)
498         self.samdb.transaction_commit()
499
500         if full_sync:
501             self.message("Full Replication of all %d objects and %d links "
502                          "from %s to %s was successful." %
503                          (num_objects, num_links, SOURCE_DC,
504                           self.local_samdb.url))
505         else:
506             self.message("Incremental replication of %d objects and %d links "
507                          "from %s to %s was successful." %
508                          (num_objects, num_links, SOURCE_DC,
509                           self.local_samdb.url))
510
511     def run(self, DEST_DC, SOURCE_DC, NC,
512             add_ref=False, sync_forced=False, sync_all=False, full_sync=False,
513             local=False, local_online=False, async_op=False, single_object=False,
514             sambaopts=None, credopts=None, versionopts=None):
515
516         self.server = DEST_DC
517         self.lp = sambaopts.get_loadparm()
518
519         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
520
521         if local:
522             self.drs_local_replicate(SOURCE_DC, NC, full_sync=full_sync,
523                                      single_object=single_object,
524                                      sync_forced=sync_forced)
525             return
526
527         if local_online:
528             server_bind = drsuapi.drsuapi("irpc:dreplsrv", lp_ctx=self.lp)
529             server_bind_handle = misc.policy_handle()
530         else:
531             drsuapi_connect(self)
532             server_bind = self.drsuapi
533             server_bind_handle = self.drsuapi_handle
534
535         if not async_op:
536             # Give the sync replication 5 minutes time
537             server_bind.request_timeout = 5 * 60
538
539         samdb_connect(self)
540
541         # we need to find the NTDS GUID of the source DC
542         msg = self.samdb.search(base=self.samdb.get_config_basedn(),
543                                 expression="(&(objectCategory=server)(|(name=%s)(dNSHostName=%s)))" % (
544             ldb.binary_encode(SOURCE_DC),
545             ldb.binary_encode(SOURCE_DC)),
546                                 attrs=[])
547         if len(msg) == 0:
548             raise CommandError("Failed to find source DC %s" % SOURCE_DC)
549         server_dn = msg[0]['dn']
550
551         msg = self.samdb.search(base=server_dn, scope=ldb.SCOPE_ONELEVEL,
552                                 expression="(|(objectCategory=nTDSDSA)(objectCategory=nTDSDSARO))",
553                                 attrs=['objectGUID', 'options'])
554         if len(msg) == 0:
555             raise CommandError("Failed to find source NTDS DN %s" % SOURCE_DC)
556         source_dsa_guid = msg[0]['objectGUID'][0]
557         dsa_options = int(attr_default(msg, 'options', 0))
558
559         req_options = 0
560         if not (dsa_options & dsdb.DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL):
561             req_options |= drsuapi.DRSUAPI_DRS_WRIT_REP
562         if add_ref:
563             req_options |= drsuapi.DRSUAPI_DRS_ADD_REF
564         if sync_forced:
565             req_options |= drsuapi.DRSUAPI_DRS_SYNC_FORCED
566         if sync_all:
567             req_options |= drsuapi.DRSUAPI_DRS_SYNC_ALL
568         if full_sync:
569             req_options |= drsuapi.DRSUAPI_DRS_FULL_SYNC_NOW
570         if async_op:
571             req_options |= drsuapi.DRSUAPI_DRS_ASYNC_OP
572
573         try:
574             drs_utils.sendDsReplicaSync(server_bind, server_bind_handle, source_dsa_guid, NC, req_options)
575         except drs_utils.drsException as estr:
576             raise CommandError("DsReplicaSync failed", estr)
577         if async_op:
578             self.message("Replicate from %s to %s was started." % (SOURCE_DC, DEST_DC))
579         else:
580             self.message("Replicate from %s to %s was successful." % (SOURCE_DC, DEST_DC))
581
582
583 class cmd_drs_bind(Command):
584     """Show DRS capabilities of a server."""
585
586     synopsis = "%prog [<DC>] [options]"
587
588     takes_optiongroups = {
589         "sambaopts": options.SambaOptions,
590         "versionopts": options.VersionOptions,
591         "credopts": options.CredentialsOptions,
592     }
593
594     takes_args = ["DC?"]
595
596     def run(self, DC=None, sambaopts=None,
597             credopts=None, versionopts=None):
598
599         self.lp = sambaopts.get_loadparm()
600         if DC is None:
601             DC = common.netcmd_dnsname(self.lp)
602         self.server = DC
603         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
604
605         drsuapi_connect(self)
606
607         bind_info = drsuapi.DsBindInfoCtr()
608         bind_info.length = 28
609         bind_info.info = drsuapi.DsBindInfo28()
610         (info, handle) = self.drsuapi.DsBind(misc.GUID(drsuapi.DRSUAPI_DS_BIND_GUID), bind_info)
611
612         optmap = [
613             ("DRSUAPI_SUPPORTED_EXTENSION_BASE", "DRS_EXT_BASE"),
614             ("DRSUAPI_SUPPORTED_EXTENSION_ASYNC_REPLICATION", "DRS_EXT_ASYNCREPL"),
615             ("DRSUAPI_SUPPORTED_EXTENSION_REMOVEAPI", "DRS_EXT_REMOVEAPI"),
616             ("DRSUAPI_SUPPORTED_EXTENSION_MOVEREQ_V2", "DRS_EXT_MOVEREQ_V2"),
617             ("DRSUAPI_SUPPORTED_EXTENSION_GETCHG_COMPRESS", "DRS_EXT_GETCHG_DEFLATE"),
618             ("DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V1", "DRS_EXT_DCINFO_V1"),
619             ("DRSUAPI_SUPPORTED_EXTENSION_RESTORE_USN_OPTIMIZATION", "DRS_EXT_RESTORE_USN_OPTIMIZATION"),
620             ("DRSUAPI_SUPPORTED_EXTENSION_ADDENTRY", "DRS_EXT_ADDENTRY"),
621             ("DRSUAPI_SUPPORTED_EXTENSION_KCC_EXECUTE", "DRS_EXT_KCC_EXECUTE"),
622             ("DRSUAPI_SUPPORTED_EXTENSION_ADDENTRY_V2", "DRS_EXT_ADDENTRY_V2"),
623             ("DRSUAPI_SUPPORTED_EXTENSION_LINKED_VALUE_REPLICATION", "DRS_EXT_LINKED_VALUE_REPLICATION"),
624             ("DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V2", "DRS_EXT_DCINFO_V2"),
625             ("DRSUAPI_SUPPORTED_EXTENSION_INSTANCE_TYPE_NOT_REQ_ON_MOD", "DRS_EXT_INSTANCE_TYPE_NOT_REQ_ON_MOD"),
626             ("DRSUAPI_SUPPORTED_EXTENSION_CRYPTO_BIND", "DRS_EXT_CRYPTO_BIND"),
627             ("DRSUAPI_SUPPORTED_EXTENSION_GET_REPL_INFO", "DRS_EXT_GET_REPL_INFO"),
628             ("DRSUAPI_SUPPORTED_EXTENSION_STRONG_ENCRYPTION", "DRS_EXT_STRONG_ENCRYPTION"),
629             ("DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V01", "DRS_EXT_DCINFO_VFFFFFFFF"),
630             ("DRSUAPI_SUPPORTED_EXTENSION_TRANSITIVE_MEMBERSHIP", "DRS_EXT_TRANSITIVE_MEMBERSHIP"),
631             ("DRSUAPI_SUPPORTED_EXTENSION_ADD_SID_HISTORY", "DRS_EXT_ADD_SID_HISTORY"),
632             ("DRSUAPI_SUPPORTED_EXTENSION_POST_BETA3", "DRS_EXT_POST_BETA3"),
633             ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V5", "DRS_EXT_GETCHGREQ_V5"),
634             ("DRSUAPI_SUPPORTED_EXTENSION_GET_MEMBERSHIPS2", "DRS_EXT_GETMEMBERSHIPS2"),
635             ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V6", "DRS_EXT_GETCHGREQ_V6"),
636             ("DRSUAPI_SUPPORTED_EXTENSION_NONDOMAIN_NCS", "DRS_EXT_NONDOMAIN_NCS"),
637             ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8", "DRS_EXT_GETCHGREQ_V8"),
638             ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V5", "DRS_EXT_GETCHGREPLY_V5"),
639             ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V6", "DRS_EXT_GETCHGREPLY_V6"),
640             ("DRSUAPI_SUPPORTED_EXTENSION_ADDENTRYREPLY_V3", "DRS_EXT_WHISTLER_BETA3"),
641             ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V7", "DRS_EXT_WHISTLER_BETA3"),
642             ("DRSUAPI_SUPPORTED_EXTENSION_VERIFY_OBJECT", "DRS_EXT_WHISTLER_BETA3"),
643             ("DRSUAPI_SUPPORTED_EXTENSION_XPRESS_COMPRESS", "DRS_EXT_W2K3_DEFLATE"),
644             ("DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V10", "DRS_EXT_GETCHGREQ_V10"),
645             ("DRSUAPI_SUPPORTED_EXTENSION_RESERVED_PART2", "DRS_EXT_RESERVED_FOR_WIN2K_OR_DOTNET_PART2"),
646             ("DRSUAPI_SUPPORTED_EXTENSION_RESERVED_PART3", "DRS_EXT_RESERVED_FOR_WIN2K_OR_DOTNET_PART3")
647         ]
648
649         optmap_ext = [
650             ("DRSUAPI_SUPPORTED_EXTENSION_ADAM", "DRS_EXT_ADAM"),
651             ("DRSUAPI_SUPPORTED_EXTENSION_LH_BETA2", "DRS_EXT_LH_BETA2"),
652             ("DRSUAPI_SUPPORTED_EXTENSION_RECYCLE_BIN", "DRS_EXT_RECYCLE_BIN")]
653
654         self.message("Bind to %s succeeded." % DC)
655         self.message("Extensions supported:")
656         for (opt, str) in optmap:
657             optval = getattr(drsuapi, opt, 0)
658             if info.info.supported_extensions & optval:
659                 yesno = "Yes"
660             else:
661                 yesno = "No "
662             self.message("  %-60s: %s (%s)" % (opt, yesno, str))
663
664         if isinstance(info.info, drsuapi.DsBindInfo48):
665             self.message("\nExtended Extensions supported:")
666             for (opt, str) in optmap_ext:
667                 optval = getattr(drsuapi, opt, 0)
668                 if info.info.supported_extensions_ext & optval:
669                     yesno = "Yes"
670                 else:
671                     yesno = "No "
672                 self.message("  %-60s: %s (%s)" % (opt, yesno, str))
673
674         self.message("\nSite GUID: %s" % info.info.site_guid)
675         self.message("Repl epoch: %u" % info.info.repl_epoch)
676         if isinstance(info.info, drsuapi.DsBindInfo48):
677             self.message("Forest GUID: %s" % info.info.config_dn_guid)
678
679
680 class cmd_drs_options(Command):
681     """Query or change 'options' for NTDS Settings object of a Domain Controller."""
682
683     synopsis = "%prog [<DC>] [options]"
684
685     takes_optiongroups = {
686         "sambaopts": options.SambaOptions,
687         "versionopts": options.VersionOptions,
688         "credopts": options.CredentialsOptions,
689     }
690
691     takes_args = ["DC?"]
692
693     takes_options = [
694         Option("--dsa-option", help="DSA option to enable/disable", type="str",
695                metavar="{+|-}IS_GC | {+|-}DISABLE_INBOUND_REPL | {+|-}DISABLE_OUTBOUND_REPL | {+|-}DISABLE_NTDSCONN_XLATE"),
696     ]
697
698     option_map = {"IS_GC": 0x00000001,
699                   "DISABLE_INBOUND_REPL": 0x00000002,
700                   "DISABLE_OUTBOUND_REPL": 0x00000004,
701                   "DISABLE_NTDSCONN_XLATE": 0x00000008}
702
703     def run(self, DC=None, dsa_option=None,
704             sambaopts=None, credopts=None, versionopts=None):
705
706         self.lp = sambaopts.get_loadparm()
707         if DC is None:
708             DC = common.netcmd_dnsname(self.lp)
709         self.server = DC
710         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
711
712         samdb_connect(self)
713
714         ntds_dn = self.samdb.get_dsServiceName()
715         res = self.samdb.search(base=ntds_dn, scope=ldb.SCOPE_BASE, attrs=["options"])
716         dsa_opts = int(res[0]["options"][0])
717
718         # print out current DSA options
719         cur_opts = [x for x in self.option_map if self.option_map[x] & dsa_opts]
720         self.message("Current DSA options: " + ", ".join(cur_opts))
721
722         # modify options
723         if dsa_option:
724             if dsa_option[:1] not in ("+", "-"):
725                 raise CommandError("Unknown option %s" % dsa_option)
726             flag = dsa_option[1:]
727             if flag not in self.option_map.keys():
728                 raise CommandError("Unknown option %s" % dsa_option)
729             if dsa_option[:1] == "+":
730                 dsa_opts |= self.option_map[flag]
731             else:
732                 dsa_opts &= ~self.option_map[flag]
733             # save new options
734             m = ldb.Message()
735             m.dn = ldb.Dn(self.samdb, ntds_dn)
736             m["options"] = ldb.MessageElement(str(dsa_opts), ldb.FLAG_MOD_REPLACE, "options")
737             self.samdb.modify(m)
738             # print out new DSA options
739             cur_opts = [x for x in self.option_map if self.option_map[x] & dsa_opts]
740             self.message("New DSA options: " + ", ".join(cur_opts))
741
742
743 class cmd_drs_clone_dc_database(Command):
744     """Replicate an initial clone of domain, but DO NOT JOIN it."""
745
746     synopsis = "%prog <dnsdomain> [options]"
747
748     takes_optiongroups = {
749         "sambaopts": options.SambaOptions,
750         "versionopts": options.VersionOptions,
751         "credopts": options.CredentialsOptions,
752     }
753
754     takes_options = [
755         Option("--server", help="DC to join", type=str),
756         Option("--targetdir", help="where to store provision (required)", type=str),
757         Option("-q", "--quiet", help="Be quiet", action="store_true"),
758         Option("--include-secrets", help="Also replicate secret values", action="store_true"),
759         Option("-v", "--verbose", help="Be verbose", action="store_true")
760     ]
761
762     takes_args = ["domain"]
763
764     def run(self, domain, sambaopts=None, credopts=None,
765             versionopts=None, server=None, targetdir=None,
766             quiet=False, verbose=False, include_secrets=False):
767         lp = sambaopts.get_loadparm()
768         creds = credopts.get_credentials(lp)
769
770         logger = self.get_logger(verbose=verbose, quiet=quiet)
771
772         if targetdir is None:
773             raise CommandError("--targetdir option must be specified")
774
775         join_clone(logger=logger, server=server, creds=creds, lp=lp,
776                    domain=domain, dns_backend='SAMBA_INTERNAL',
777                    targetdir=targetdir, include_secrets=include_secrets)
778
779
780 class cmd_drs_uptodateness(Command):
781     """Show uptodateness status"""
782
783     synopsis = "%prog [options]"
784
785     takes_optiongroups = {
786         "sambaopts": options.SambaOptions,
787         "versionopts": options.VersionOptions,
788         "credopts": options.CredentialsOptions,
789     }
790
791     takes_options = [
792         Option("-H", "--URL", metavar="URL", dest="H",
793                help="LDB URL for database or target server"),
794         Option("-p", "--partition",
795                help="restrict to this partition"),
796         Option("--json", action='store_true',
797                help="Print data in json format"),
798         Option("--maximum", action='store_true',
799                help="Print maximum out-of-date-ness only"),
800         Option("--median", action='store_true',
801                help="Print median out-of-date-ness only"),
802         Option("--full", action='store_true',
803                help="Print full out-of-date-ness data"),
804     ]
805
806     def format_as_json(self, partitions_summaries):
807         return json.dumps(partitions_summaries, indent=2)
808
809     def format_as_text(self, partitions_summaries):
810         lines = []
811         for part_name, summary in partitions_summaries.items():
812             items = ['%s: %s' % (k, v) for k, v in summary.items()]
813             line = '%-15s %s' % (part_name, '  '.join(items))
814             lines.append(line)
815         return '\n'.join(lines)
816
817     def run(self, H=None, partition=None,
818             json=False, maximum=False, median=False, full=False,
819             sambaopts=None, credopts=None, versionopts=None,
820             quiet=False, verbose=False):
821
822         lp = sambaopts.get_loadparm()
823         creds = credopts.get_credentials(lp, fallback_machine=True)
824         local_kcc, dsas = get_kcc_and_dsas(H, lp, creds)
825         samdb = local_kcc.samdb
826         short_partitions, _ = get_partition_maps(samdb)
827         if partition:
828             if partition in short_partitions:
829                 part_dn = short_partitions[partition]
830                 # narrow down to specified partition only
831                 short_partitions = {partition: part_dn}
832             else:
833                 raise CommandError("unknown partition %s" % partition)
834
835         filters = []
836         if maximum:
837             filters.append('maximum')
838         if median:
839             filters.append('median')
840
841         partitions_distances = {}
842         partitions_summaries = {}
843         for part_name, part_dn in short_partitions.items():
844             utdv_edges = get_utdv_edges(local_kcc, dsas, part_dn, lp, creds)
845             distances = get_utdv_distances(utdv_edges, dsas)
846             summary = get_utdv_summary(distances, filters=filters)
847             partitions_distances[part_name] = distances
848             partitions_summaries[part_name] = summary
849
850         if full:
851             # always print json format
852             output = self.format_as_json(partitions_distances)
853         else:
854             if json:
855                 output = self.format_as_json(partitions_summaries)
856             else:
857                 output = self.format_as_text(partitions_summaries)
858
859         print(output, file=self.outf)
860
861
862 class cmd_drs(SuperCommand):
863     """Directory Replication Services (DRS) management."""
864
865     subcommands = {}
866     subcommands["bind"] = cmd_drs_bind()
867     subcommands["kcc"] = cmd_drs_kcc()
868     subcommands["replicate"] = cmd_drs_replicate()
869     subcommands["showrepl"] = cmd_drs_showrepl()
870     subcommands["options"] = cmd_drs_options()
871     subcommands["clone-dc-database"] = cmd_drs_clone_dc_database()
872     subcommands["uptodateness"] = cmd_drs_uptodateness()