KCC: use less verbose constructions in a few places
[samba.git] / source4 / scripting / bin / samba_kcc
1 #!/usr/bin/env python
2 #
3 # Compute our KCC topology
4 #
5 # Copyright (C) Dave Craft 2011
6 # Copyright (C) Andrew Bartlett 2015
7 #
8 # Andrew Bartlett's alleged work performed by his underlings Douglas
9 # Bagnall and Garming Sam.
10 #
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
23
24 import os
25 import sys
26 import random
27 import uuid
28
29 # ensure we get messages out immediately, so they get in the samba logs,
30 # and don't get swallowed by a timeout
31 os.environ['PYTHONUNBUFFERED'] = '1'
32
33 # forcing GMT avoids a problem in some timezones with kerberos. Both MIT
34 # heimdal can get mutual authentication errors due to the 24 second difference
35 # between UTC and GMT when using some zone files (eg. the PDT zone from
36 # the US)
37 os.environ["TZ"] = "GMT"
38
39 # Find right directory when running from source tree
40 sys.path.insert(0, "bin/python")
41
42 import optparse
43 import logging
44 import itertools
45 import heapq
46 import time
47 from functools import partial
48
49 from samba import (
50     getopt as options,
51     Ldb,
52     ldb,
53     dsdb,
54     read_and_sub_file,
55     drs_utils,
56     nttime2unix)
57 from samba.auth import system_session
58 from samba.samdb import SamDB
59 from samba.dcerpc import drsuapi
60 from samba.kcc_utils import *
61 from samba.graph_utils import *
62 from samba import ldif_utils
63
64 class KCC(object):
65     """The Knowledge Consistency Checker class.
66
67     A container for objects and methods allowing a run of the KCC.  Produces a
68     set of connections in the samdb for which the Distributed Replication
69     Service can then utilize to replicate naming contexts
70     """
71     def __init__(self):
72         """Initializes the partitions class which can hold
73         our local DCs partitions or all the partitions in
74         the forest
75         """
76         self.part_table = {}    # partition objects
77         self.site_table = {}
78         self.transport_table = {}
79         self.sitelink_table = {}
80         self.dsa_by_dnstr = {}
81         self.dsa_by_guid = {}
82
83         self.get_dsa_by_guidstr = self.dsa_by_guid.get
84         self.get_dsa = self.dsa_by_dnstr.get
85
86         # TODO: These should be backed by a 'permanent' store so that when
87         # calling DRSGetReplInfo with DS_REPL_INFO_KCC_DSA_CONNECT_FAILURES,
88         # the failure information can be returned
89         self.kcc_failed_links = {}
90         self.kcc_failed_connections = set()
91
92         # Used in inter-site topology computation.  A list
93         # of connections (by NTDSConnection object) that are
94         # to be kept when pruning un-needed NTDS Connections
95         self.kept_connections = set()
96
97         self.my_dsa_dnstr = None  # My dsa DN
98         self.my_dsa = None  # My dsa object
99
100         self.my_site_dnstr = None
101         self.my_site = None
102
103         self.samdb = None
104
105     def load_all_transports(self):
106         """Loads the inter-site transport objects for Sites
107
108         ::returns: Raises an Exception on error
109         """
110         try:
111             res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
112                                     self.samdb.get_config_basedn(),
113                                     scope=ldb.SCOPE_SUBTREE,
114                                     expression="(objectClass=interSiteTransport)")
115         except ldb.LdbError, (enum, estr):
116             raise Exception("Unable to find inter-site transports - (%s)" %
117                     estr)
118
119         for msg in res:
120             dnstr = str(msg.dn)
121
122             transport = Transport(dnstr)
123
124             transport.load_transport(self.samdb)
125
126             # already loaded
127             if str(transport.guid) in self.transport_table:
128                 continue
129
130             # Assign this transport to table
131             # and index by guid
132             self.transport_table[str(transport.guid)] = transport
133
134     def load_all_sitelinks(self):
135         """Loads the inter-site siteLink objects
136
137         ::returns: Raises an Exception on error
138         """
139         try:
140             res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
141                                     self.samdb.get_config_basedn(),
142                                     scope=ldb.SCOPE_SUBTREE,
143                                     expression="(objectClass=siteLink)")
144         except ldb.LdbError, (enum, estr):
145             raise Exception("Unable to find inter-site siteLinks - (%s)" % estr)
146
147         for msg in res:
148             dnstr = str(msg.dn)
149
150             # already loaded
151             if dnstr in self.sitelink_table:
152                 continue
153
154             sitelink = SiteLink(dnstr)
155
156             sitelink.load_sitelink(self.samdb)
157
158             # Assign this siteLink to table
159             # and index by dn
160             self.sitelink_table[dnstr] = sitelink
161
162     def load_site(self, dn_str):
163         """Helper for load_my_site and load_all_sites. It puts all the site's
164         DSAs into the KCC indices.
165         """
166         site = Site(dn_str, unix_now)
167         site.load_site(self.samdb)
168
169         # I am not sure why, but we avoid replacing the site with an
170         # identical copy.
171         guid = str(site.site_guid)
172         if guid not in self.site_table:
173             self.site_table[guid] = site
174
175         self.dsa_by_dnstr.update(site.dsa_table)
176         self.dsa_by_guid.update((str(x.dsa_guid), x) for x in site.dsa_table.values())
177
178         return site
179
180     def load_my_site(self):
181         """Loads the Site class for the local DSA
182
183         ::returns: Raises an Exception on error
184         """
185         self.my_site_dnstr = "CN=%s,CN=Sites,%s" % (
186                               self.samdb.server_site_name(),
187                               self.samdb.get_config_basedn())
188
189         self.my_site = self.load_site(self.my_site_dnstr)
190
191     def load_all_sites(self):
192         """Discover all sites and instantiate and load each
193         NTDS Site settings.
194
195         ::returns: Raises an Exception on error
196         """
197         try:
198             res = self.samdb.search("CN=Sites,%s" %
199                                     self.samdb.get_config_basedn(),
200                                     scope=ldb.SCOPE_SUBTREE,
201                                     expression="(objectClass=site)")
202         except ldb.LdbError, (enum, estr):
203             raise Exception("Unable to find sites - (%s)" % estr)
204
205         for msg in res:
206             sitestr = str(msg.dn)
207             self.load_site(sitestr)
208
209     def load_my_dsa(self):
210         """Discover my nTDSDSA dn thru the rootDSE entry
211
212         ::returns: Raises an Exception on error.
213         """
214         dn = ldb.Dn(self.samdb, "<GUID=%s>" % self.samdb.get_ntds_GUID())
215         try:
216             res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
217                                     attrs=["objectGUID"])
218         except ldb.LdbError, (enum, estr):
219             DEBUG("Search for %s failed: %s.  This typically happens in"
220                   " --importldif mode due to lack of module support",
221                   dn, estr)
222             try:
223                 # We work around the failure above by looking at the
224                 # dsServiceName that was put in the fake rootdse by
225                 # the --exportldif, rather than the
226                 # samdb.get_ntds_GUID(). The disadvantage is that this
227                 # mode requires we modify the @ROOTDSE dnq to support
228                 # --forced-local-dsa
229                 service_name_res = self.samdb.search(base="", scope=ldb.SCOPE_BASE,
230                                                      attrs=["dsServiceName"])
231                 dn = ldb.Dn(self.samdb, service_name_res[0]["dsServiceName"][0])
232
233                 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
234                                         attrs=["objectGUID"])
235             except ldb.LdbError, (enum, estr):
236                 raise Exception("Unable to find my nTDSDSA - (%s)" % estr)
237
238         if len(res) != 1:
239             raise Exception("Unable to find my nTDSDSA at %s" % dn.extended_str())
240
241         if misc.GUID(res[0]["objectGUID"][0]) != misc.GUID(self.samdb.get_ntds_GUID()):
242             raise Exception("Did not find the GUID we expected, perhaps due to --importldif")
243
244         self.my_dsa_dnstr = str(res[0].dn)
245
246         self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
247
248     def load_all_partitions(self):
249         """Discover all NCs thru the Partitions dn and
250         instantiate and load the NCs.
251
252         Each NC is inserted into the part_table by partition
253         dn string (not the nCName dn string)
254
255         ::returns: Raises an Exception on error
256         """
257         try:
258             res = self.samdb.search("CN=Partitions,%s" %
259                                     self.samdb.get_config_basedn(),
260                                     scope=ldb.SCOPE_SUBTREE,
261                                     expression="(objectClass=crossRef)")
262         except ldb.LdbError, (enum, estr):
263             raise Exception("Unable to find partitions - (%s)" % estr)
264
265         for msg in res:
266             partstr = str(msg.dn)
267
268             # already loaded
269             if partstr in self.part_table:
270                 continue
271
272             part = Partition(partstr)
273
274             part.load_partition(self.samdb)
275             self.part_table[partstr] = part
276
277     def should_be_present_test(self):
278         """Enumerate all loaded partitions and DSAs in local
279         site and test if NC should be present as replica
280         """
281         for partdn, part in self.part_table.items():
282             for dsadn, dsa in self.my_site.dsa_table.items():
283                needed, ro, partial = part.should_be_present(dsa)
284                logger.info("dsadn:%s\nncdn:%s\nneeded=%s:ro=%s:partial=%s\n" %
285                            (dsadn, part.nc_dnstr, needed, ro, partial))
286
287     def refresh_failed_links_connections(self):
288         """Based on MS-ADTS 6.2.2.1"""
289
290         # Instead of NULL link with failure_count = 0, the tuple is simply removed
291
292         # LINKS: Refresh failed links
293         self.kcc_failed_links = {}
294         current, needed = self.my_dsa.get_rep_tables()
295         for replica in current.values():
296             # For every possible connection to replicate
297             for reps_from in replica.rep_repsFrom:
298                 failure_count = reps_from.consecutive_sync_failures
299                 if failure_count <= 0:
300                     continue
301
302                 dsa_guid = str(reps_from.source_dsa_obj_guid)
303                 time_first_failure = reps_from.last_success
304                 last_result = reps_from.last_attempt
305                 dns_name = reps_from.dns_name1
306
307                 f = self.kcc_failed_links.get(dsa_guid)
308                 if not f:
309                     f = KCCFailedObject(dsa_guid, failure_count,
310                                         time_first_failure, last_result,
311                                         dns_name)
312                     self.kcc_failed_links[dsa_guid] = f
313                 #elif f.failure_count == 0:
314                 #    f.failure_count = failure_count
315                 #    f.time_first_failure = time_first_failure
316                 #    f.last_result = last_result
317                 else:
318                     f.failure_count = max(f.failure_count, failure_count)
319                     f.time_first_failure = min(f.time_first_failure, time_first_failure)
320                     f.last_result = last_result
321
322         # CONNECTIONS: Refresh failed connections
323         restore_connections = set()
324         if opts.attempt_live_connections:
325             DEBUG("refresh_failed_links: checking if links are still down")
326             for connection in self.kcc_failed_connections:
327                 try:
328                     drs_utils.drsuapi_connect(connection.dns_name, lp, creds)
329                     # Failed connection is no longer failing
330                     restore_connections.add(connection)
331                 except drs_utils.drsException:
332                     # Failed connection still failing
333                     connection.failure_count += 1
334         else:
335             DEBUG("refresh_failed_links: not checking live links because we weren't\n"
336                   "asked to --attempt-live-connections")
337
338         # Remove the restored connections from the failed connections
339         self.kcc_failed_connections.difference_update(restore_connections)
340
341     def is_stale_link_connection(self, target_dsa):
342         """Returns False if no tuple z exists in the kCCFailedLinks or
343         kCCFailedConnections variables such that z.UUIDDsa is the
344         objectGUID of the target dsa, z.FailureCount > 0, and
345         the current time - z.TimeFirstFailure > 2 hours.
346         """
347         # Returns True if tuple z exists...
348         failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
349         if failed_link:
350             # failure_count should be > 0, but check anyways
351             if failed_link.failure_count > 0:
352                 unix_first_time_failure = nttime2unix(failed_link.time_first_failure)
353                 # TODO guard against future
354                 if unix_first_time_failure > unix_now:
355                     logger.error("The last success time attribute for \
356                                  repsFrom is in the future!")
357
358                 # Perform calculation in seconds
359                 if (unix_now - unix_first_time_failure) > 60 * 60 * 2:
360                     return True
361
362         # TODO connections
363
364         return False
365
366     # TODO: This should be backed by some form of local database
367     def remove_unneeded_failed_links_connections(self):
368         # Remove all tuples in kcc_failed_links where failure count = 0
369         # In this implementation, this should never happen.
370
371         # Remove all connections which were not used this run or connections
372         # that became active during this run.
373         pass
374
375     def remove_unneeded_ntdsconn(self, all_connected):
376         """Removes unneeded NTDS Connections after computation
377         of KCC intra and inter-site topology has finished.
378         """
379         mydsa = self.my_dsa
380
381         # Loop thru connections
382         for cn_conn in mydsa.connect_table.values():
383             if cn_conn.guid is None:
384                 if opts.readonly:
385                     cn_conn.guid = misc.GUID(str(uuid.uuid4()))
386                     cn_conn.whenCreated = nt_now
387                 else:
388                     cn_conn.load_connection(self.samdb)
389
390         for cn_conn in mydsa.connect_table.values():
391
392             s_dnstr = cn_conn.get_from_dnstr()
393             if s_dnstr is None:
394                 cn_conn.to_be_deleted = True
395                 continue
396
397             # Get the source DSA no matter what site
398             s_dsa = self.get_dsa(s_dnstr)
399
400             #XXX should an RODC be regarded as same site
401             same_site = s_dnstr in self.my_site.dsa_table
402
403             # Given an nTDSConnection object cn, if the DC with the
404             # nTDSDSA object dc that is the parent object of cn and
405             # the DC with the nTDSDA object referenced by cn!fromServer
406             # are in the same site, the KCC on dc deletes cn if all of
407             # the following are true:
408             #
409             # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
410             #
411             # No site settings object s exists for the local DC's site, or
412             # bit NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED is clear in
413             # s!options.
414             #
415             # Another nTDSConnection object cn2 exists such that cn and
416             # cn2 have the same parent object, cn!fromServer = cn2!fromServer,
417             # and either
418             #
419             #     cn!whenCreated < cn2!whenCreated
420             #
421             #     cn!whenCreated = cn2!whenCreated and
422             #     cn!objectGUID < cn2!objectGUID
423             #
424             # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
425             if same_site:
426                 if not cn_conn.is_generated():
427                     continue
428
429                 if self.my_site.is_cleanup_ntdsconn_disabled():
430                     continue
431
432                 # Loop thru connections looking for a duplicate that
433                 # fulfills the previous criteria
434                 lesser = False
435
436                 for cn2_conn in mydsa.connect_table.values():
437                     if cn2_conn is cn_conn:
438                         continue
439
440                     s2_dnstr = cn2_conn.get_from_dnstr()
441
442                     # If the NTDS Connections has a different
443                     # fromServer field then no match
444                     if s2_dnstr != s_dnstr:
445                         continue
446
447                     #XXX GUID comparison
448                     lesser = (cn_conn.whenCreated < cn2_conn.whenCreated or
449                               (cn_conn.whenCreated == cn2_conn.whenCreated and
450                                ndr_pack(cn_conn.guid) < ndr_pack(cn2_conn.guid)))
451
452                     if lesser:
453                         break
454
455                 if lesser and not cn_conn.is_rodc_topology():
456                     cn_conn.to_be_deleted = True
457
458             # Given an nTDSConnection object cn, if the DC with the nTDSDSA
459             # object dc that is the parent object of cn and the DC with
460             # the nTDSDSA object referenced by cn!fromServer are in
461             # different sites, a KCC acting as an ISTG in dc's site
462             # deletes cn if all of the following are true:
463             #
464             #     Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
465             #
466             #     cn!fromServer references an nTDSDSA object for a DC
467             #     in a site other than the local DC's site.
468             #
469             #     The keepConnections sequence returned by
470             #     CreateIntersiteConnections() does not contain
471             #     cn!objectGUID, or cn is "superseded by" (see below)
472             #     another nTDSConnection cn2 and keepConnections
473             #     contains cn2!objectGUID.
474             #
475             #     The return value of CreateIntersiteConnections()
476             #     was true.
477             #
478             #     Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in
479             #     cn!options
480             #
481             else: # different site
482
483                 if not mydsa.is_istg():
484                     continue
485
486                 if not cn_conn.is_generated():
487                     continue
488
489                 # TODO
490                 # We are directly using this connection in intersite or
491                 # we are using a connection which can supersede this one.
492                 #
493                 # MS-ADTS 6.2.2.4 - Removing Unnecessary Connections does not
494                 # appear to be correct.
495                 #
496                 # 1. cn!fromServer and cn!parent appear inconsistent with no cn2
497                 # 2. The repsFrom do not imply each other
498                 #
499                 if cn_conn in self.kept_connections: # and not_superceded:
500                     continue
501
502                 # This is the result of create_intersite_connections
503                 if not all_connected:
504                     continue
505
506                 if not cn_conn.is_rodc_topology():
507                     cn_conn.to_be_deleted = True
508
509
510         if mydsa.is_ro() or opts.readonly:
511             for connect in mydsa.connect_table.values():
512                 if connect.to_be_deleted:
513                     DEBUG_GREEN("TO BE DELETED:\n%s" % connect)
514                 if connect.to_be_added:
515                     DEBUG_GREEN("TO BE ADDED:\n%s" % connect)
516
517             # Peform deletion from our tables but perform
518             # no database modification
519             mydsa.commit_connections(self.samdb, ro=True)
520         else:
521             # Commit any modified connections
522             mydsa.commit_connections(self.samdb)
523
524     def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
525         """Part of MS-ADTS 6.2.2.5.
526
527         Update t_repsFrom if necessary to satisfy requirements. Such
528         updates are typically required when the IDL_DRSGetNCChanges
529         server has moved from one site to another--for example, to
530         enable compression when the server is moved from the
531         client's site to another site.
532
533         :param n_rep: NC replica we need
534         :param t_repsFrom: repsFrom tuple to modify
535         :param s_rep: NC replica at source DSA
536         :param s_dsa: source DSA
537         :param cn_conn: Local DSA NTDSConnection child
538
539         ::returns: (update) bit field containing which portion of the
540            repsFrom was modified.  This bit field is suitable as input
541            to IDL_DRSReplicaModify ulModifyFields element, as it consists
542            of these bits:
543                drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
544                drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
545                drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
546         """
547         s_dnstr = s_dsa.dsa_dnstr
548         update = 0x0
549
550         same_site = s_dnstr in self.my_site.dsa_table
551
552         # if schedule doesn't match then update and modify
553         times = convert_schedule_to_repltimes(cn_conn.schedule)
554         if times != t_repsFrom.schedule:
555             t_repsFrom.schedule = times
556             update |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
557
558         # Bit DRS_PER_SYNC is set in replicaFlags if and only
559         # if nTDSConnection schedule has a value v that specifies
560         # scheduled replication is to be performed at least once
561         # per week.
562         if cn_conn.is_schedule_minimum_once_per_week():
563
564             if (t_repsFrom.replica_flags &
565                 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0:
566                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
567
568         # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
569         # if the source DSA and the local DC's nTDSDSA object are
570         # in the same site or source dsa is the FSMO role owner
571         # of one or more FSMO roles in the NC replica.
572         if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
573
574             if (t_repsFrom.replica_flags &
575                 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0:
576                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
577
578         # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
579         # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
580         # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
581         # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
582         # t.replicaFlags if and only if s and the local DC's
583         # nTDSDSA object are in different sites.
584         if (cn_conn.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0:
585
586             if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
587
588                 if (t_repsFrom.replica_flags &
589                     drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0:
590                     t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
591
592         elif not same_site:
593
594             if (t_repsFrom.replica_flags &
595                 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0:
596                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
597
598         # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
599         # and only if s and the local DC's nTDSDSA object are
600         # not in the same site and the
601         # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
602         # clear in cn!options
603         if (not same_site and
604            (cn_conn.options &
605             dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
606
607             if (t_repsFrom.replica_flags &
608                 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0:
609                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
610
611         # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
612         # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
613         if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
614
615             if (t_repsFrom.replica_flags &
616                 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0:
617                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
618
619         # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
620         # set in t.replicaFlags if and only if cn!enabledConnection = false.
621         if not cn_conn.is_enabled():
622
623             if (t_repsFrom.replica_flags &
624                 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0:
625                 t_repsFrom.replica_flags |= \
626                     drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
627
628             if (t_repsFrom.replica_flags &
629                 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0:
630                 t_repsFrom.replica_flags |= \
631                     drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
632
633         # If s and the local DC's nTDSDSA object are in the same site,
634         # cn!transportType has no value, or the RDN of cn!transportType
635         # is CN=IP:
636         #
637         #     Bit DRS_MAIL_REP in t.replicaFlags is clear.
638         #
639         #     t.uuidTransport = NULL GUID.
640         #
641         #     t.uuidDsa = The GUID-based DNS name of s.
642         #
643         # Otherwise:
644         #
645         #     Bit DRS_MAIL_REP in t.replicaFlags is set.
646         #
647         #     If x is the object with dsname cn!transportType,
648         #     t.uuidTransport = x!objectGUID.
649         #
650         #     Let a be the attribute identified by
651         #     x!transportAddressAttribute. If a is
652         #     the dNSHostName attribute, t.uuidDsa = the GUID-based
653         #      DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
654         #
655         # It appears that the first statement i.e.
656         #
657         #     "If s and the local DC's nTDSDSA object are in the same
658         #      site, cn!transportType has no value, or the RDN of
659         #      cn!transportType is CN=IP:"
660         #
661         # could be a slightly tighter statement if it had an "or"
662         # between each condition.  I believe this should
663         # be interpreted as:
664         #
665         #     IF (same-site) OR (no-value) OR (type-ip)
666         #
667         # because IP should be the primary transport mechanism
668         # (even in inter-site) and the absense of the transportType
669         # attribute should always imply IP no matter if its multi-site
670         #
671         # NOTE MS-TECH INCORRECT:
672         #
673         #     All indications point to these statements above being
674         #     incorrectly stated:
675         #
676         #         t.uuidDsa = The GUID-based DNS name of s.
677         #
678         #         Let a be the attribute identified by
679         #         x!transportAddressAttribute. If a is
680         #         the dNSHostName attribute, t.uuidDsa = the GUID-based
681         #         DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
682         #
683         #     because the uuidDSA is a GUID and not a GUID-base DNS
684         #     name.  Nor can uuidDsa hold (s!parent)!a if not
685         #     dNSHostName.  What should have been said is:
686         #
687         #         t.naDsa = The GUID-based DNS name of s
688         #
689         #     That would also be correct if transportAddressAttribute
690         #     were "mailAddress" because (naDsa) can also correctly
691         #     hold the SMTP ISM service address.
692         #
693         nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
694
695         # We're not currently supporting SMTP replication
696         # so is_smtp_replication_available() is currently
697         # always returning False
698         if (same_site or
699             cn_conn.transport_dnstr is None or
700             cn_conn.transport_dnstr.find("CN=IP") == 0 or
701             not is_smtp_replication_available()):
702
703             if (t_repsFrom.replica_flags &
704                 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0:
705                 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
706
707             t_repsFrom.transport_guid = misc.GUID()
708
709             # See (NOTE MS-TECH INCORRECT) above
710             if t_repsFrom.version == 0x1:
711                 if t_repsFrom.dns_name1 is None or \
712                    t_repsFrom.dns_name1 != nastr:
713                     t_repsFrom.dns_name1 = nastr
714             else:
715                 if t_repsFrom.dns_name1 is None or \
716                    t_repsFrom.dns_name2 is None or \
717                    t_repsFrom.dns_name1 != nastr or \
718                    t_repsFrom.dns_name2 != nastr:
719                     t_repsFrom.dns_name1 = nastr
720                     t_repsFrom.dns_name2 = nastr
721
722         else:
723             if (t_repsFrom.replica_flags &
724                 drsuapi.DRSUAPI_DRS_MAIL_REP) == 0x0:
725                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_MAIL_REP
726
727             # We have a transport type but its not an
728             # object in the database
729             if cn_conn.transport_guid not in self.transport_table:
730                 raise Exception("Missing inter-site transport - (%s)" %
731                                 cn_conn.transport_dnstr)
732
733             x_transport = self.transport_table[str(cn_conn.transport_guid)]
734
735             if t_repsFrom.transport_guid != x_transport.guid:
736                 t_repsFrom.transport_guid = x_transport.guid
737
738             # See (NOTE MS-TECH INCORRECT) above
739             if x_transport.address_attr == "dNSHostName":
740
741                 if t_repsFrom.version == 0x1:
742                     if t_repsFrom.dns_name1 is None or \
743                        t_repsFrom.dns_name1 != nastr:
744                         t_repsFrom.dns_name1 = nastr
745                 else:
746                     if t_repsFrom.dns_name1 is None or \
747                        t_repsFrom.dns_name2 is None or \
748                        t_repsFrom.dns_name1 != nastr or \
749                        t_repsFrom.dns_name2 != nastr:
750                         t_repsFrom.dns_name1 = nastr
751                         t_repsFrom.dns_name2 = nastr
752
753             else:
754                 # MS tech specification says we retrieve the named
755                 # attribute in "transportAddressAttribute" from the parent of
756                 # the DSA object
757                 try:
758                     pdnstr = s_dsa.get_parent_dnstr()
759                     attrs = [ x_transport.address_attr ]
760
761                     res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
762                                             attrs=attrs)
763                 except ldb.LdbError, (enum, estr):
764                     raise Exception(
765                         "Unable to find attr (%s) for (%s) - (%s)" %
766                          (x_transport.address_attr, pdnstr, estr))
767
768                 msg = res[0]
769                 nastr = str(msg[x_transport.address_attr][0])
770
771                 # See (NOTE MS-TECH INCORRECT) above
772                 if t_repsFrom.version == 0x1:
773                     if t_repsFrom.dns_name1 is None or \
774                        t_repsFrom.dns_name1 != nastr:
775                         t_repsFrom.dns_name1 = nastr
776                 else:
777                     if t_repsFrom.dns_name1 is None or \
778                        t_repsFrom.dns_name2 is None or \
779                        t_repsFrom.dns_name1 != nastr or \
780                        t_repsFrom.dns_name2 != nastr:
781
782                         t_repsFrom.dns_name1 = nastr
783                         t_repsFrom.dns_name2 = nastr
784
785         if t_repsFrom.is_modified():
786             logger.debug("modify_repsFrom(): %s" % t_repsFrom)
787
788     def is_repsFrom_implied(self, n_rep, cn_conn):
789         """Given a NC replica and NTDS Connection, determine if the connection
790         implies a repsFrom tuple should be present from the source DSA listed
791         in the connection to the naming context
792
793         :param n_rep: NC replica
794         :param conn: NTDS Connection
795         ::returns (True || False), source DSA:
796         """
797         # NTDS Connection must satisfy all the following criteria
798         # to imply a repsFrom tuple is needed:
799         #
800         #    cn!enabledConnection = true.
801         #    cn!options does not contain NTDSCONN_OPT_RODC_TOPOLOGY.
802         #    cn!fromServer references an nTDSDSA object.
803         s_dsa = None
804
805         if cn_conn.is_enabled() and not cn_conn.is_rodc_topology():
806
807             s_dnstr = cn_conn.get_from_dnstr()
808             if s_dnstr is not None:
809                 s_dsa = self.get_dsa(s_dnstr)
810
811         # No DSA matching this source DN string?
812         if s_dsa is None:
813             return False, None
814
815         # To imply a repsFrom tuple is needed, each of these
816         # must be True:
817         #
818         #     An NC replica of the NC "is present" on the DC to
819         #     which the nTDSDSA object referenced by cn!fromServer
820         #     corresponds.
821         #
822         #     An NC replica of the NC "should be present" on
823         #     the local DC
824         s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
825
826         if s_rep is None or not s_rep.is_present():
827             return False, None
828
829         # To imply a repsFrom tuple is needed, each of these
830         # must be True:
831         #
832         #     The NC replica on the DC referenced by cn!fromServer is
833         #     a writable replica or the NC replica that "should be
834         #     present" on the local DC is a partial replica.
835         #
836         #     The NC is not a domain NC, the NC replica that
837         #     "should be present" on the local DC is a partial
838         #     replica, cn!transportType has no value, or
839         #     cn!transportType has an RDN of CN=IP.
840         #
841         implied = (not s_rep.is_ro() or n_rep.is_partial()) and \
842                   (not n_rep.is_domain() or
843                    n_rep.is_partial() or
844                    cn_conn.transport_dnstr is None or
845                    cn_conn.transport_dnstr.find("CN=IP") == 0)
846
847         if implied:
848             return True, s_dsa
849         else:
850             return False, None
851
852     def translate_ntdsconn(self):
853         """This function adjusts values of repsFrom abstract attributes of NC
854         replicas on the local DC to match those implied by
855         nTDSConnection objects.
856         [MS-ADTS] 6.2.2.5
857         """
858         if self.my_dsa.is_translate_ntdsconn_disabled():
859             logger.debug("skipping translate_ntdsconn() because disabling flag is set")
860             return
861
862         logger.debug("translate_ntdsconn(): enter")
863
864         current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
865
866         # Filled in with replicas we currently have that need deleting
867         delete_reps = set()
868
869         # We're using the MS notation names here to allow
870         # correlation back to the published algorithm.
871         #
872         # n_rep      - NC replica (n)
873         # t_repsFrom - tuple (t) in n!repsFrom
874         # s_dsa      - Source DSA of the replica. Defined as nTDSDSA
875         #              object (s) such that (s!objectGUID = t.uuidDsa)
876         #              In our IDL representation of repsFrom the (uuidDsa)
877         #              attribute is called (source_dsa_obj_guid)
878         # cn_conn    - (cn) is nTDSConnection object and child of the local DC's
879         #              nTDSDSA object and (cn!fromServer = s)
880         # s_rep      - source DSA replica of n
881         #
882         # If we have the replica and its not needed
883         # then we add it to the "to be deleted" list.
884         for dnstr in current_rep_table:
885             if dnstr not in needed_rep_table:
886                 delete_reps.add(dnstr)
887
888         if delete_reps:
889             DEBUG('current %d needed %d delete %d', len(current_rep_table),
890                   len(needed_rep_table), len(delete_reps))
891             DEBUG('deleting these reps: %s', delete_reps)
892             for dnstr in delete_reps:
893                 del current_rep_table[dnstr]
894
895         # Now perform the scan of replicas we'll need
896         # and compare any current repsFrom against the
897         # connections
898         for n_rep in needed_rep_table.values():
899
900             # load any repsFrom and fsmo roles as we'll
901             # need them during connection translation
902             n_rep.load_repsFrom(self.samdb)
903             n_rep.load_fsmo_roles(self.samdb)
904
905             # Loop thru the existing repsFrom tupples (if any)
906             for i, t_repsFrom in enumerate(n_rep.rep_repsFrom):
907
908                 # for each tuple t in n!repsFrom, let s be the nTDSDSA
909                 # object such that s!objectGUID = t.uuidDsa
910                 guidstr = str(t_repsFrom.source_dsa_obj_guid)
911                 s_dsa = self.get_dsa_by_guidstr(guidstr)
912
913                 # Source dsa is gone from config (strange)
914                 # so cleanup stale repsFrom for unlisted DSA
915                 if s_dsa is None:
916                     logger.warning("repsFrom source DSA guid (%s) not found" %
917                                    guidstr)
918                     t_repsFrom.to_be_deleted = True
919                     continue
920
921                 s_dnstr = s_dsa.dsa_dnstr
922
923                 # Retrieve my DSAs connection object (if it exists)
924                 # that specifies the fromServer equivalent to
925                 # the DSA that is specified in the repsFrom source
926                 cn_conn = self.my_dsa.get_connection_by_from_dnstr(s_dnstr)
927
928                 # Let (cn) be the nTDSConnection object such that (cn)
929                 # is a child of the local DC's nTDSDSA object and
930                 # (cn!fromServer = s) and (cn!options) does not contain
931                 # NTDSCONN_OPT_RODC_TOPOLOGY or NULL if no such (cn) exists.
932                 if cn_conn and not cn_conn.is_rodc_topology():
933                     cn_conn = None
934
935                 # KCC removes this repsFrom tuple if any of the following
936                 # is true:
937                 #     cn = NULL.
938                 #
939                 #     No NC replica of the NC "is present" on DSA that
940                 #     would be source of replica
941                 #
942                 #     A writable replica of the NC "should be present" on
943                 #     the local DC, but a partial replica "is present" on
944                 #     the source DSA
945                 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
946
947                 if cn_conn is None or \
948                    s_rep is None or not s_rep.is_present() or \
949                    (not n_rep.is_ro() and s_rep.is_partial()):
950
951                     t_repsFrom.to_be_deleted = True
952                     continue
953
954                 # If the KCC did not remove t from n!repsFrom, it updates t
955                 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
956
957             # Loop thru connections and add implied repsFrom tuples
958             # for each NTDSConnection under our local DSA if the
959             # repsFrom is not already present
960             for cn_conn in self.my_dsa.connect_table.values():
961
962                 implied, s_dsa = self.is_repsFrom_implied(n_rep, cn_conn)
963                 if not implied:
964                     continue
965
966                 # Loop thru the existing repsFrom tupples (if any) and
967                 # if we already have a tuple for this connection then
968                 # no need to proceed to add.  It will have been changed
969                 # to have the correct attributes above
970                 for t_repsFrom in n_rep.rep_repsFrom:
971                      guidstr = str(t_repsFrom.source_dsa_obj_guid)
972                      if s_dsa is self.get_dsa_by_guidstr(guidstr):
973                          s_dsa = None
974                          break
975
976                 if s_dsa is None:
977                     continue
978
979                 # Create a new RepsFromTo and proceed to modify
980                 # it according to specification
981                 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
982
983                 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
984
985                 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
986
987                 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
988
989                 # Add to our NC repsFrom as this is newly computed
990                 if t_repsFrom.is_modified():
991                     n_rep.rep_repsFrom.append(t_repsFrom)
992
993             if opts.readonly:
994                 # Display any to be deleted or modified repsFrom
995                 text = n_rep.dumpstr_to_be_deleted()
996                 if text:
997                     logger.info("TO BE DELETED:\n%s" % text)
998                 text = n_rep.dumpstr_to_be_modified()
999                 if text:
1000                     logger.info("TO BE MODIFIED:\n%s" % text)
1001
1002                 # Peform deletion from our tables but perform
1003                 # no database modification
1004                 n_rep.commit_repsFrom(self.samdb, ro=True)
1005             else:
1006                 # Commit any modified repsFrom to the NC replica
1007                 n_rep.commit_repsFrom(self.samdb)
1008
1009
1010
1011     def merge_failed_links(self):
1012         """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1013         The KCC on a writable DC attempts to merge the link and connection
1014         failure information from bridgehead DCs in its own site to help it
1015         identify failed bridgehead DCs.
1016         """
1017         # MS-TECH Ref 6.2.2.3.2 Merge of kCCFailedLinks and kCCFailedLinks
1018         #     from Bridgeheads
1019
1020         # 1. Queries every bridgehead server in your site (other than yourself)
1021         # 2. For every ntDSConnection that references a server in a different
1022         #    site merge all the failure info
1023         #
1024         # XXX - not implemented yet
1025         if opts.attempt_live_connections:
1026             DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1027         else:
1028             DEBUG("skipping merge_failed_links() because it requires real network connections\n"
1029                   "and we weren't asked to --attempt-live-connctions")
1030
1031
1032     def setup_graph(self, part):
1033         """Set up a GRAPH, populated with a VERTEX for each site
1034         object, a MULTIEDGE for each siteLink object, and a
1035         MUTLIEDGESET for each siteLinkBridge object (or implied
1036         siteLinkBridge).
1037
1038         ::returns: a new graph
1039         """
1040         guid_to_vertex = {}
1041         # Create graph
1042         g = IntersiteGraph()
1043         # Add vertices
1044         for site_guid, site in self.site_table.items():
1045             vertex = Vertex(site, part)
1046             vertex.guid = site_guid
1047             vertex.ndrpacked_guid = ndr_pack(site.site_guid)
1048             g.vertices.add(vertex)
1049
1050             if not guid_to_vertex.get(site_guid):
1051                 guid_to_vertex[site_guid] = []
1052
1053             guid_to_vertex[site_guid].append(vertex)
1054
1055         connected_vertices = set()
1056         for transport_guid, transport in self.transport_table.items():
1057             # Currently only ever "IP"
1058             for site_link_dn, site_link in self.sitelink_table.items():
1059                 new_edge = create_edge(transport_guid, site_link, guid_to_vertex)
1060                 connected_vertices.update(new_edge.vertices)
1061                 g.edges.add(new_edge)
1062
1063             # If 'Bridge all site links' is enabled and Win2k3 bridges required is not set
1064             # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1065             # No documentation for this however, ntdsapi.h appears to have listed:
1066             # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1067             if ((self.my_site.site_options & 0x00000002) == 0
1068                 and (self.my_site.site_options & 0x00001000) == 0):
1069                 g.edge_set.add(create_auto_edge_set(g, transport_guid))
1070             else:
1071                 # TODO get all site link bridges
1072                 for site_link_bridge in []:
1073                     g.edge_set.add(create_edge_set(g, transport_guid,
1074                                                    site_link_bridge))
1075
1076         g.connected_vertices = connected_vertices
1077
1078         #be less verbose in dot file output unless --debug
1079         do_dot_files = opts.dot_files and opts.debug
1080         dot_edges = []
1081         for edge in g.edges:
1082             for a, b in itertools.combinations(edge.vertices, 2):
1083                 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1084         verify_properties = ()
1085         verify_and_dot('site_edges', dot_edges, directed=False, label=self.my_dsa_dnstr,
1086                        properties=verify_properties, debug=DEBUG, verify=opts.verify,
1087                        dot_files=do_dot_files)
1088
1089         return g
1090
1091     def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1092         """Get a bridghead DC.
1093
1094         :param site: site object representing for which a bridgehead
1095             DC is desired.
1096         :param part: crossRef for NC to replicate.
1097         :param transport: interSiteTransport object for replication
1098             traffic.
1099         :param partial_ok: True if a DC containing a partial
1100             replica or a full replica will suffice, False if only
1101             a full replica will suffice.
1102         :param detect_failed: True to detect failed DCs and route
1103             replication traffic around them, False to assume no DC
1104             has failed.
1105         ::returns: dsa object for the bridgehead DC or None
1106         """
1107
1108         bhs = self.get_all_bridgeheads(site, part, transport,
1109                                        partial_ok, detect_failed)
1110         if len(bhs) == 0:
1111             DEBUG_MAGENTA("get_bridgehead:\n\tsitedn=%s\n\tbhdn=None" %
1112                           site.site_dnstr)
1113             return None
1114         else:
1115             DEBUG_GREEN("get_bridgehead:\n\tsitedn=%s\n\tbhdn=%s" %
1116                         (site.site_dnstr, bhs[0].dsa_dnstr))
1117             return bhs[0]
1118
1119     def get_all_bridgeheads(self, site, part, transport,
1120                             partial_ok, detect_failed):
1121         """Get all bridghead DCs satisfying the given criteria
1122
1123         :param site: site object representing the site for which
1124             bridgehead DCs are desired.
1125         :param part: partition for NC to replicate.
1126         :param transport: interSiteTransport object for
1127             replication traffic.
1128         :param partial_ok: True if a DC containing a partial
1129             replica or a full replica will suffice, False if
1130             only a full replica will suffice.
1131         :param detect_failed: True to detect failed DCs and route
1132             replication traffic around them, FALSE to assume
1133             no DC has failed.
1134         ::returns: list of dsa object for available bridgehead
1135             DCs or None
1136         """
1137
1138         bhs = []
1139
1140         logger.debug("get_all_bridgeheads: %s" % transport)
1141         if 'Site-5' in site.site_dnstr:
1142             DEBUG_RED("get_all_bridgeheads with %s, part%s, partial_ok %s"
1143                       " detect_failed %s" % (site.site_dnstr, part.partstr,
1144                                              partial_ok, detect_failed))
1145         logger.debug(site.dsa_table)
1146         for key, dsa in site.dsa_table.items():
1147
1148             pdnstr = dsa.get_parent_dnstr()
1149
1150             # IF t!bridgeheadServerListBL has one or more values and
1151             # t!bridgeheadServerListBL does not contain a reference
1152             # to the parent object of dc then skip dc
1153             if (len(transport.bridgehead_list) != 0 and
1154                 pdnstr not in transport.bridgehead_list):
1155                 continue
1156
1157             # IF dc is in the same site as the local DC
1158             #    IF a replica of cr!nCName is not in the set of NC replicas
1159             #    that "should be present" on dc or a partial replica of the
1160             #    NC "should be present" but partialReplicasOkay = FALSE
1161             #        Skip dc
1162             if self.my_site.same_site(dsa):
1163                 needed, ro, partial = part.should_be_present(dsa)
1164                 if not needed or (partial and not partial_ok):
1165                     continue
1166                 rep = dsa.get_current_replica(part.nc_dnstr)
1167
1168             # ELSE
1169             #     IF an NC replica of cr!nCName is not in the set of NC
1170             #     replicas that "are present" on dc or a partial replica of
1171             #     the NC "is present" but partialReplicasOkay = FALSE
1172             #          Skip dc
1173             else:
1174                 rep = dsa.get_current_replica(part.nc_dnstr)
1175                 if rep is None or (rep.is_partial() and not partial_ok):
1176                     continue
1177
1178             # IF AmIRODC() and cr!nCName corresponds to default NC then
1179             #     Let dsaobj be the nTDSDSA object of the dc
1180             #     IF  dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1181             #         Skip dc
1182             if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1183                 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1184                     continue
1185
1186             # IF t!name != "IP" and the parent object of dc has no value for
1187             # the attribute specified by t!transportAddressAttribute
1188             #     Skip dc
1189             if transport.name != "IP":
1190                 # MS tech specification says we retrieve the named
1191                 # attribute in "transportAddressAttribute" from the parent
1192                 # of the DSA object
1193                 try:
1194                     attrs = [ transport.address_attr ]
1195
1196                     res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
1197                                             attrs=attrs)
1198                 except ldb.LdbError, (enum, estr):
1199                     continue
1200
1201                 msg = res[0]
1202                 if transport.address_attr not in msg:
1203                     continue
1204
1205                 nastr = str(msg[transport.address_attr][0])
1206
1207             # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1208             #     Skip dc
1209             if self.is_bridgehead_failed(dsa, detect_failed):
1210                 DEBUG("bridgehead is failed")
1211                 continue
1212
1213             logger.debug("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
1214             bhs.append(dsa)
1215
1216         # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1217         # s!options
1218         #    SORT bhs such that all GC servers precede DCs that are not GC
1219         #    servers, and otherwise by ascending objectGUID
1220         # ELSE
1221         #    SORT bhs in a random order
1222         if site.is_random_bridgehead_disabled():
1223             bhs.sort(sort_dsa_by_gc_and_guid)
1224         else:
1225             random.shuffle(bhs)
1226         DEBUG_YELLOW(bhs)
1227         return bhs
1228
1229
1230     def is_bridgehead_failed(self, dsa, detect_failed):
1231         """Determine whether a given DC is known to be in a failed state
1232         ::returns: True if and only if the DC should be considered failed
1233
1234         Here we DEPART from the pseudo code spec which appears to be
1235         wrong. It says, in full:
1236
1237     /***** BridgeheadDCFailed *****/
1238     /* Determine whether a given DC is known to be in a failed state.
1239      * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1240      * IN: detectFailedDCs - TRUE if and only failed DC detection is
1241      *     enabled.
1242      * RETURNS: TRUE if and only if the DC should be considered to be in a
1243      *          failed state.
1244      */
1245     BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1246     {
1247         IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1248         the options attribute of the site settings object for the local
1249         DC's site
1250             RETURN FALSE
1251         ELSEIF a tuple z exists in the kCCFailedLinks or
1252         kCCFailedConnections variables such that z.UUIDDsa =
1253         objectGUID, z.FailureCount > 1, and the current time -
1254         z.TimeFirstFailure > 2 hours
1255             RETURN TRUE
1256         ELSE
1257             RETURN detectFailedDCs
1258         ENDIF
1259     }
1260
1261         where you will see detectFailedDCs is not behaving as
1262         advertised -- it is acting as a default return code in the
1263         event that a failure is not detected, not a switch turning
1264         detection on or off. Elsewhere the documentation seems to
1265         concur with the comment rather than the code.
1266         """
1267         if not detect_failed:
1268             return False
1269
1270         # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1271         # When DETECT_STALE_DISABLED, we can never know of if it's in a failed state
1272         if self.my_site.site_options & 0x00000008:
1273             return False
1274
1275         return self.is_stale_link_connection(dsa)
1276
1277
1278     def create_connection(self, part, rbh, rsite, transport,
1279                           lbh, lsite, link_opt, link_sched,
1280                           partial_ok, detect_failed):
1281         """Create an nTDSConnection object with the given parameters
1282         if one does not already exist.
1283
1284         :param part: crossRef object for the NC to replicate.
1285         :param rbh: nTDSDSA object for DC to act as the
1286             IDL_DRSGetNCChanges server (which is in a site other
1287             than the local DC's site).
1288         :param rsite: site of the rbh
1289         :param transport: interSiteTransport object for the transport
1290             to use for replication traffic.
1291         :param lbh: nTDSDSA object for DC to act as the
1292             IDL_DRSGetNCChanges client (which is in the local DC's site).
1293         :param lsite: site of the lbh
1294         :param link_opt: Replication parameters (aggregated siteLink options, etc.)
1295         :param link_sched: Schedule specifying the times at which
1296             to begin replicating.
1297         :partial_ok: True if bridgehead DCs containing partial
1298             replicas of the NC are acceptable.
1299         :param detect_failed: True to detect failed DCs and route
1300             replication traffic around them, FALSE to assume no DC
1301             has failed.
1302         """
1303         rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1304                                             partial_ok, False)
1305         rbh_table = {x.dsa_dnstr:x for x in rbhs_all}
1306
1307         DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all), [x.dsa_dnstr for x in rbhs_all]))
1308
1309         # MS-TECH says to compute rbhs_avail but then doesn't use it
1310         # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1311         #                                        partial_ok, detect_failed)
1312
1313         lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1314                                             partial_ok, False)
1315
1316         DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all), [x.dsa_dnstr for x in lbhs_all]))
1317
1318         # MS-TECH says to compute lbhs_avail but then doesn't use it
1319         # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1320         #                                       partial_ok, detect_failed)
1321
1322         # FOR each nTDSConnection object cn such that the parent of cn is
1323         # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1324         for ldsa in lbhs_all:
1325             for cn in ldsa.connect_table.values():
1326
1327                 rdsa = rbh_table.get(cn.from_dnstr)
1328                 if rdsa is None:
1329                     continue
1330
1331                 DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1332                 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1333                 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1334                 # cn!transportType references t
1335                 if (cn.is_generated() and not cn.is_rodc_topology() and
1336                     cn.transport_guid == transport.guid):
1337
1338                     # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1339                     # cn!options and cn!schedule != sch
1340                     #     Perform an originating update to set cn!schedule to
1341                     #     sched
1342                     if (not cn.is_user_owned_schedule() and
1343                         not cn.is_equivalent_schedule(link_sched)):
1344                         cn.schedule = link_sched
1345                         cn.set_modified(True)
1346
1347                     # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1348                     # NTDSCONN_OPT_USE_NOTIFY are set in cn
1349                     if cn.is_override_notify_default() and \
1350                        cn.is_use_notify():
1351
1352                         # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1353                         # ri.Options
1354                         #    Perform an originating update to clear bits
1355                         #    NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1356                         #    NTDSCONN_OPT_USE_NOTIFY in cn!options
1357                         if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1358                             cn.options &= \
1359                                 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1360                                   dsdb.NTDSCONN_OPT_USE_NOTIFY)
1361                             cn.set_modified(True)
1362
1363                     # ELSE
1364                     else:
1365
1366                         # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1367                         # ri.Options
1368                         #     Perform an originating update to set bits
1369                         #     NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1370                         #     NTDSCONN_OPT_USE_NOTIFY in cn!options
1371                         if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1372                             cn.options |= \
1373                                 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1374                                  dsdb.NTDSCONN_OPT_USE_NOTIFY)
1375                             cn.set_modified(True)
1376
1377
1378                     # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1379                     if cn.is_twoway_sync():
1380
1381                         # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1382                         # ri.Options
1383                         #     Perform an originating update to clear bit
1384                         #     NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1385                         if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1386                             cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1387                             cn.set_modified(True)
1388
1389                     # ELSE
1390                     else:
1391
1392                         # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1393                         # ri.Options
1394                         #     Perform an originating update to set bit
1395                         #     NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1396                         if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1397                             cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1398                             cn.set_modified(True)
1399
1400
1401                     # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1402                     # in cn!options
1403                     if cn.is_intersite_compression_disabled():
1404
1405                         # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1406                         # in ri.Options
1407                         #     Perform an originating update to clear bit
1408                         #     NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1409                         #     cn!options
1410                         if (link_opt &
1411                             dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0:
1412                             cn.options &= \
1413                                 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1414                             cn.set_modified(True)
1415
1416                     # ELSE
1417                     else:
1418                         # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1419                         # ri.Options
1420                         #     Perform an originating update to set bit
1421                         #     NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1422                         #     cn!options
1423                         if (link_opt &
1424                             dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
1425                             cn.options |= \
1426                                 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1427                             cn.set_modified(True)
1428
1429                     # Display any modified connection
1430                     if opts.readonly:
1431                         if cn.to_be_modified:
1432                             logger.info("TO BE MODIFIED:\n%s" % cn)
1433
1434                         ldsa.commit_connections(self.samdb, ro=True)
1435                     else:
1436                         ldsa.commit_connections(self.samdb)
1437         # ENDFOR
1438
1439         valid_connections = 0
1440
1441         # FOR each nTDSConnection object cn such that cn!parent is
1442         # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1443         for ldsa in lbhs_all:
1444             for cn in ldsa.connect_table.values():
1445
1446                 rdsa = rbh_table.get(cn.from_dnstr)
1447                 if rdsa is None:
1448                     continue
1449
1450                 DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1451
1452                 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1453                 # cn!transportType references t) and
1454                 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1455                 if ((not cn.is_generated() or
1456                      cn.transport_guid == transport.guid) and
1457                      not cn.is_rodc_topology()):
1458
1459                     # LET rguid be the objectGUID of the nTDSDSA object
1460                     # referenced by cn!fromServer
1461                     # LET lguid be (cn!parent)!objectGUID
1462
1463                     # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1464                     # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1465                     #     Increment cValidConnections by 1
1466                     if (not self.is_bridgehead_failed(rdsa, detect_failed) and
1467                         not self.is_bridgehead_failed(ldsa, detect_failed)):
1468                         valid_connections += 1
1469
1470                     # IF keepConnections does not contain cn!objectGUID
1471                     #     APPEND cn!objectGUID to keepConnections
1472                     self.kept_connections.add(cn)
1473
1474         # ENDFOR
1475         DEBUG_RED("valid connections %d" % valid_connections)
1476         DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1477         # IF cValidConnections = 0
1478         if valid_connections == 0:
1479
1480             # LET opt be NTDSCONN_OPT_IS_GENERATED
1481             opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1482
1483             # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1484             #     SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1485             #     NTDSCONN_OPT_USE_NOTIFY in opt
1486             if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1487                 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1488                         dsdb.NTDSCONN_OPT_USE_NOTIFY)
1489
1490             # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1491             #     SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1492             if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1493                 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1494
1495             # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1496             # ri.Options
1497             #     SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1498             if (link_opt &
1499                 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
1500                 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1501
1502             # Perform an originating update to create a new nTDSConnection
1503             # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1504             # cn!options = opt, cn!transportType is a reference to t,
1505             # cn!fromServer is a reference to rbh, and cn!schedule = sch
1506             cn = lbh.new_connection(opt, 0, transport, rbh.dsa_dnstr, link_sched)
1507
1508             # Display any added connection
1509             if opts.readonly:
1510                 if cn.to_be_added:
1511                     logger.info("TO BE ADDED:\n%s" % cn)
1512
1513                     lbh.commit_connections(self.samdb, ro=True)
1514             else:
1515                 lbh.commit_connections(self.samdb)
1516
1517             # APPEND cn!objectGUID to keepConnections
1518             self.kept_connections.add(cn)
1519
1520     def add_transports(self, vertex, local_vertex, graph, detect_failed):
1521
1522         # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1523         # here and in the, but using vertex seems to make more
1524         # sense. That is, it wants this:
1525         #
1526         #bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1527         #                         local_vertex.is_black(), detect_failed)
1528         #
1529         # TODO WHY?????
1530
1531         vertex.accept_red_red = []
1532         vertex.accept_black = []
1533         found_failed = False
1534         for t_guid, transport in self.transport_table.items():
1535             if transport.name != 'IP':
1536                 #XXX well this is cheating a bit
1537                 logging.warning("WARNING: we are ignoring a transport named %r" % transport.name)
1538                 continue
1539
1540             # FLAG_CR_NTDS_DOMAIN 0x00000002
1541             if (vertex.is_red() and transport.name != "IP" and
1542                 vertex.part.system_flags & 0x00000002):
1543                 continue
1544
1545             if vertex not in graph.connected_vertices:
1546                 continue
1547
1548             partial_replica_okay = vertex.is_black()
1549             bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1550                                      partial_replica_okay, detect_failed)
1551             if bh is None:
1552                 found_failed = True
1553                 continue
1554
1555             vertex.accept_red_red.append(t_guid)
1556             vertex.accept_black.append(t_guid)
1557
1558             # Add additional transport to allow another run of Dijkstra
1559             vertex.accept_red_red.append("EDGE_TYPE_ALL")
1560             vertex.accept_black.append("EDGE_TYPE_ALL")
1561
1562         return found_failed
1563
1564     def create_connections(self, graph, part, detect_failed):
1565         """Construct an NC replica graph for the NC identified by
1566         the given crossRef, then create any additional nTDSConnection
1567         objects required.
1568
1569         :param graph: site graph.
1570         :param part: crossRef object for NC.
1571         :param detect_failed:  True to detect failed DCs and route
1572             replication traffic around them, False to assume no DC
1573             has failed.
1574
1575         Modifies self.kept_connections by adding any connections
1576         deemed to be "in use".
1577
1578         ::returns: (all_connected, found_failed_dc)
1579         (all_connected) True if the resulting NC replica graph
1580             connects all sites that need to be connected.
1581         (found_failed_dc) True if one or more failed DCs were
1582             detected.
1583         """
1584         all_connected = True
1585         found_failed = False
1586
1587         logger.debug("create_connections(): enter\n\tpartdn=%s\n\tdetect_failed=%s" %
1588                      (part.nc_dnstr, detect_failed))
1589
1590         # XXX - This is a highly abbreviated function from the MS-TECH
1591         #       ref.  It creates connections between bridgeheads to all
1592         #       sites that have appropriate replicas.  Thus we are not
1593         #       creating a minimum cost spanning tree but instead
1594         #       producing a fully connected tree.  This should produce
1595         #       a full (albeit not optimal cost) replication topology.
1596
1597         my_vertex = Vertex(self.my_site, part)
1598         my_vertex.color_vertex()
1599
1600         for v in graph.vertices:
1601             v.color_vertex()
1602             if self.add_transports(v, my_vertex, graph, False):
1603                 found_failed = True
1604
1605         # No NC replicas for this NC in the site of the local DC,
1606         # so no nTDSConnection objects need be created
1607         if my_vertex.is_white():
1608             return all_connected, found_failed
1609
1610         edge_list, component_count = self.get_spanning_tree_edges(graph, label=part.partstr)
1611
1612         logger.debug("%s Number of components: %d" % (part.nc_dnstr, component_count))
1613         if component_count > 1:
1614             all_connected = False
1615
1616         # LET partialReplicaOkay be TRUE if and only if
1617         # localSiteVertex.Color = COLOR.BLACK
1618         if my_vertex.is_black():
1619             partial_ok = True
1620         else:
1621             partial_ok = False
1622
1623         # Utilize the IP transport only for now
1624         transport = None
1625         for transport in self.transport_table.values():
1626             if transport.name == "IP":
1627                break
1628
1629         if transport is None:
1630             raise Exception("Unable to find inter-site transport for IP")
1631
1632         DEBUG("edge_list %s" % edge_list)
1633         for e in edge_list:
1634             if e.directed and e.vertices[0].site is self.my_site: # more accurate comparison?
1635                 continue
1636
1637             if e.vertices[0].site is self.my_site:
1638                 rsite = e.vertices[1].site
1639             else:
1640                 rsite = e.vertices[0].site
1641
1642             # We don't make connections to our own site as that
1643             # is intrasite topology generator's job
1644             if rsite is self.my_site:
1645                 DEBUG("rsite is my_site")
1646                 continue
1647
1648             # Determine bridgehead server in remote site
1649             rbh = self.get_bridgehead(rsite, part, transport,
1650                                       partial_ok, detect_failed)
1651             if rbh is None:
1652                 continue
1653
1654             # RODC acts as an BH for itself
1655             # IF AmIRODC() then
1656             #     LET lbh be the nTDSDSA object of the local DC
1657             # ELSE
1658             #     LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1659             #     cr, t, partialReplicaOkay, detectFailedDCs)
1660             if self.my_dsa.is_ro():
1661                lsite = self.my_site
1662                lbh = self.my_dsa
1663             else:
1664                lsite = self.my_site
1665                lbh = self.get_bridgehead(lsite, part, transport,
1666                                          partial_ok, detect_failed)
1667             # TODO
1668             if lbh is None:
1669                 DEBUG_RED("DISASTER! lbh is None")
1670                 return False, True
1671
1672             DEBUG_CYAN("SITES")
1673             print lsite, rsite
1674             DEBUG_BLUE("vertices")
1675             print e.vertices
1676             DEBUG_BLUE("bridgeheads")
1677             print lbh, rbh
1678             DEBUG_BLUE("-" * 70)
1679
1680             sitelink = e.site_link
1681             if sitelink is None:
1682                 link_opt = 0x0
1683                 link_sched = None
1684             else:
1685                 link_opt = sitelink.options
1686                 link_sched = sitelink.schedule
1687
1688             self.create_connection(part, rbh, rsite, transport,
1689                                    lbh, lsite, link_opt, link_sched,
1690                                    partial_ok, detect_failed)
1691
1692         return all_connected, found_failed
1693
1694     def create_intersite_connections(self):
1695         """Computes an NC replica graph for each NC replica that "should be
1696         present" on the local DC or "is present" on any DC in the same site
1697         as the local DC. For each edge directed to an NC replica on such a
1698         DC from an NC replica on a DC in another site, the KCC creates an
1699         nTDSConnection object to imply that edge if one does not already
1700         exist.
1701
1702         Modifies self.kept_connections - A set of nTDSConnection
1703         objects for edges that are directed
1704         to the local DC's site in one or more NC replica graphs.
1705
1706         returns: True if spanning trees were created for all NC replica
1707             graphs, otherwise False.
1708         """
1709         all_connected = True
1710         self.kept_connections = set()
1711
1712         # LET crossRefList be the set containing each object o of class
1713         # crossRef such that o is a child of the CN=Partitions child of the
1714         # config NC
1715
1716         # FOR each crossRef object cr in crossRefList
1717         #    IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1718         #        is clear in cr!systemFlags, skip cr.
1719         #    LET g be the GRAPH return of SetupGraph()
1720
1721         for part in self.part_table.values():
1722
1723             if not part.is_enabled():
1724                 continue
1725
1726             if part.is_foreign():
1727                 continue
1728
1729             graph = self.setup_graph(part)
1730
1731             # Create nTDSConnection objects, routing replication traffic
1732             # around "failed" DCs.
1733             found_failed = False
1734
1735             connected, found_failed = self.create_connections(graph, part, True)
1736
1737             DEBUG("with detect_failed: connected %s Found failed %s" % (connected, found_failed))
1738             if not connected:
1739                 all_connected = False
1740
1741                 if found_failed:
1742                     # One or more failed DCs preclude use of the ideal NC
1743                     # replica graph. Add connections for the ideal graph.
1744                     self.create_connections(graph, part, False)
1745
1746         return all_connected
1747
1748     def get_spanning_tree_edges(self, graph, label=None):
1749         # Phase 1: Run Dijkstra's to get a list of internal edges, which are
1750         # just the shortest-paths connecting colored vertices
1751
1752         internal_edges = set()
1753
1754         for e_set in graph.edge_set:
1755             edgeType = None
1756             for v in graph.vertices:
1757                 v.edges = []
1758
1759             # All con_type in an edge set is the same
1760             for e in e_set.edges:
1761                 edgeType = e.con_type
1762                 for v in e.vertices:
1763                     v.edges.append(e)
1764
1765             if opts.verify or opts.dot_files:
1766                 graph_edges = [(a.site.site_dnstr, b.site.site_dnstr)
1767                                for a, b in itertools.chain(*(itertools.combinations(edge.vertices, 2)
1768                                                              for edge in e_set.edges))]
1769                 graph_nodes = [v.site.site_dnstr for v in graph.vertices]
1770
1771                 if opts.dot_files and opts.debug:
1772                     write_dot_file('edgeset_%s' % (edgeType,), graph_edges, vertices=graph_nodes,
1773                                    label=label)
1774
1775                 if opts.verify:
1776                     verify_graph('spanning tree edge set %s' % edgeType, graph_edges, vertices=graph_nodes,
1777                                  properties=('complete', 'connected'), debug=DEBUG)
1778
1779             # Run dijkstra's algorithm with just the red vertices as seeds
1780             # Seed from the full replicas
1781             dijkstra(graph, edgeType, False)
1782
1783             # Process edge set
1784             process_edge_set(graph, e_set, internal_edges)
1785
1786             # Run dijkstra's algorithm with red and black vertices as the seeds
1787             # Seed from both full and partial replicas
1788             dijkstra(graph, edgeType, True)
1789
1790             # Process edge set
1791             process_edge_set(graph, e_set, internal_edges)
1792
1793         # All vertices have root/component as itself
1794         setup_vertices(graph)
1795         process_edge_set(graph, None, internal_edges)
1796
1797         if opts.verify or opts.dot_files:
1798             graph_edges = [(e.v1.site.site_dnstr, e.v2.site.site_dnstr) for e in internal_edges]
1799             graph_nodes = [v.site.site_dnstr for v in graph.vertices]
1800             verify_properties = ('multi_edge_forest',)
1801             verify_and_dot('prekruskal', graph_edges, graph_nodes, label=label,
1802                            properties=verify_properties, debug=DEBUG, verify=opts.verify,
1803                            dot_files=opts.dot_files)
1804
1805
1806         # Phase 2: Run Kruskal's on the internal edges
1807         output_edges, components = kruskal(graph, internal_edges)
1808
1809         # This recalculates the cost for the path connecting the closest red vertex
1810         # Ignoring types is fine because NO suboptimal edge should exist in the graph
1811         dijkstra(graph, "EDGE_TYPE_ALL", False) # TODO rename
1812         # Phase 3: Process the output
1813         for v in graph.vertices:
1814             if v.is_red():
1815                 v.dist_to_red = 0
1816             else:
1817                 v.dist_to_red = v.repl_info.cost
1818
1819         if opts.verify or opts.dot_files:
1820             graph_edges = [(e.v1.site.site_dnstr, e.v2.site.site_dnstr) for e in internal_edges]
1821             graph_nodes = [v.site.site_dnstr for v in graph.vertices]
1822             verify_properties = ('multi_edge_forest',)
1823             verify_and_dot('postkruskal', graph_edges, graph_nodes, label=label,
1824                            properties=verify_properties, debug=DEBUG, verify=opts.verify,
1825                            dot_files=opts.dot_files)
1826
1827         # count the components
1828         return self.copy_output_edges(graph, output_edges), components
1829
1830     # This ensures only one-way connections for partial-replicas
1831     def copy_output_edges(self, graph, output_edges):
1832         edge_list = []
1833         vid = self.my_site # object guid for the local dc's site
1834
1835         for edge in output_edges:
1836             # Three-way edges are no problem here since these were created by
1837             # add_out_edge which only has two endpoints
1838             v = edge.vertices[0]
1839             w = edge.vertices[1]
1840             if v.site is vid or w.site is vid:
1841                 if (v.is_black() or w.is_black()) and not v.dist_to_red == MAX_DWORD:
1842                     edge.directed = True
1843
1844                     if w.dist_to_red < v.dist_to_red:
1845                         edge.vertices[0] = w
1846                         edge.vertices[1] = v
1847
1848                 edge_list.append(edge)
1849
1850         return edge_list
1851
1852     def intersite(self):
1853         """The head method for generating the inter-site KCC replica
1854         connection graph and attendant nTDSConnection objects
1855         in the samdb.
1856
1857         Produces self.kept_connections set of NTDS Connections
1858         that should be kept during subsequent pruning process.
1859
1860         ::return (True or False):  (True) if the produced NC replica
1861             graph connects all sites that need to be connected
1862         """
1863
1864         # Retrieve my DSA
1865         mydsa = self.my_dsa
1866         mysite = self.my_site
1867         all_connected = True
1868
1869         logger.debug("intersite(): enter")
1870
1871         # Determine who is the ISTG
1872         if opts.readonly:
1873             mysite.select_istg(self.samdb, mydsa, ro=True)
1874         else:
1875             mysite.select_istg(self.samdb, mydsa, ro=False)
1876
1877         # Test whether local site has topology disabled
1878         if mysite.is_intersite_topology_disabled():
1879             logger.debug("intersite(): exit disabled all_connected=%d" %
1880                          all_connected)
1881             return all_connected
1882
1883         if not mydsa.is_istg():
1884             logger.debug("intersite(): exit not istg all_connected=%d" %
1885                          all_connected)
1886             return all_connected
1887
1888         self.merge_failed_links()
1889
1890         # For each NC with an NC replica that "should be present" on the
1891         # local DC or "is present" on any DC in the same site as the
1892         # local DC, the KCC constructs a site graph--a precursor to an NC
1893         # replica graph. The site connectivity for a site graph is defined
1894         # by objects of class interSiteTransport, siteLink, and
1895         # siteLinkBridge in the config NC.
1896
1897         all_connected = self.create_intersite_connections()
1898
1899         logger.debug("intersite(): exit all_connected=%d" % all_connected)
1900         return all_connected
1901
1902     def update_rodc_connection(self):
1903         """Runs when the local DC is an RODC and updates the RODC NTFRS
1904         connection object.
1905         """
1906         # Given an nTDSConnection object cn1, such that cn1.options contains
1907         # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1908         # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1909         # that the following is true:
1910         #
1911         #     cn1.fromServer = cn2.fromServer
1912         #     cn1.schedule = cn2.schedule
1913         #
1914         # If no such cn2 can be found, cn1 is not modified.
1915         # If no such cn1 can be found, nothing is modified by this task.
1916
1917         if not self.my_dsa.is_ro():
1918             return
1919
1920         all_connections = self.my_dsa.connect_table.values()
1921         ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1922         rw_connections = [x for x in all_connections if x not in ro_connections]
1923
1924         # XXX here we are dealing with multiple RODC_TOPO connections,
1925         # if they exist. It is not clear whether the spec means that
1926         # or if it ever arises.
1927         if rw_connections and ro_connections:
1928             for con in ro_connections:
1929                 cn2 = rw_connections[0]
1930                 con.from_dnstr = cn2.from_dnstr
1931                 con.schedule = cn2.schedule
1932                 con.to_be_modified = True
1933
1934             self.my_dsa.commit_connections(self.samdb, ro=opts.readonly)
1935
1936     def intrasite_max_node_edges(self, node_count):
1937         """Returns the maximum number of edges directed to a node in
1938         the intrasite replica graph.
1939
1940         The KCC does not create more
1941         than 50 edges directed to a single DC. To optimize replication,
1942         we compute that each node should have n+2 total edges directed
1943         to it such that (n) is the smallest non-negative integer
1944         satisfying (node_count <= 2*(n*n) + 6*n + 7)
1945
1946         (If the number of edges is m (i.e. n + 2), that is the same as
1947         2 * m*m - 2 * m + 3).
1948
1949         edges  n   nodecount
1950           2    0    7
1951           3    1   15
1952           4    2   27
1953           5    3   43
1954                   ...
1955          50   48 4903
1956
1957         :param node_count: total number of nodes in the replica graph
1958         """
1959         n = 0
1960         while True:
1961             if node_count <= (2 * (n * n) + (6 * n) + 7):
1962                 break
1963             n = n + 1
1964         n = n + 2
1965         if n < 50:
1966             return n
1967         return 50
1968
1969     def construct_intrasite_graph(self, site_local, dc_local,
1970                                   nc_x, gc_only, detect_stale):
1971         # [MS-ADTS] 6.2.2.2
1972         # We're using the MS notation names here to allow
1973         # correlation back to the published algorithm.
1974         #
1975         # nc_x     - naming context (x) that we are testing if it
1976         #            "should be present" on the local DC
1977         # f_of_x   - replica (f) found on a DC (s) for NC (x)
1978         # dc_s     - DC where f_of_x replica was found
1979         # dc_local - local DC that potentially needs a replica
1980         #            (f_of_x)
1981         # r_list   - replica list R
1982         # p_of_x   - replica (p) is partial and found on a DC (s)
1983         #            for NC (x)
1984         # l_of_x   - replica (l) is the local replica for NC (x)
1985         #            that should appear on the local DC
1986         # r_len = is length of replica list |R|
1987         #
1988         # If the DSA doesn't need a replica for this
1989         # partition (NC x) then continue
1990         needed, ro, partial = nc_x.should_be_present(dc_local)
1991
1992         DEBUG_YELLOW("construct_intrasite_graph(): enter" +
1993                      "\n\tgc_only=%d" % gc_only +
1994                      "\n\tdetect_stale=%d" % detect_stale +
1995                      "\n\tneeded=%s" % needed +
1996                      "\n\tro=%s" % ro +
1997                      "\n\tpartial=%s" % partial +
1998                      "\n%s" % nc_x)
1999
2000         if not needed:
2001             DEBUG_RED("%s lacks 'should be present' status, aborting construct_intersite_graph!" %
2002                       nc_x.nc_dnstr)
2003             return
2004
2005         # Create a NCReplica that matches what the local replica
2006         # should say.  We'll use this below in our r_list
2007         l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
2008                            nc_x.nc_dnstr)
2009
2010         l_of_x.identify_by_basedn(self.samdb)
2011
2012         l_of_x.rep_partial = partial
2013         l_of_x.rep_ro = ro
2014
2015         # Add this replica that "should be present" to the
2016         # needed replica table for this DSA
2017         dc_local.add_needed_replica(l_of_x)
2018
2019         # Replica list
2020         #
2021         # Let R be a sequence containing each writable replica f of x
2022         # such that f "is present" on a DC s satisfying the following
2023         # criteria:
2024         #
2025         #  * s is a writable DC other than the local DC.
2026         #
2027         #  * s is in the same site as the local DC.
2028         #
2029         #  * If x is a read-only full replica and x is a domain NC,
2030         #    then the DC's functional level is at least
2031         #    DS_BEHAVIOR_WIN2008.
2032         #
2033         #  * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
2034         #    in the options attribute of the site settings object for
2035         #    the local DC's site, or no tuple z exists in the
2036         #    kCCFailedLinks or kCCFailedConnections variables such
2037         #    that z.UUIDDsa is the objectGUID of the nTDSDSA object
2038         #    for s, z.FailureCount > 0, and the current time -
2039         #    z.TimeFirstFailure > 2 hours.
2040
2041         r_list = []
2042
2043         # We'll loop thru all the DSAs looking for
2044         # writeable NC replicas that match the naming
2045         # context dn for (nc_x)
2046         #
2047         for dc_s in self.my_site.dsa_table.values():
2048             # If this partition (nc_x) doesn't appear as a
2049             # replica (f_of_x) on (dc_s) then continue
2050             if not nc_x.nc_dnstr in dc_s.current_rep_table:
2051                 continue
2052
2053             # Pull out the NCReplica (f) of (x) with the dn
2054             # that matches NC (x) we are examining.
2055             f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2056
2057             # Replica (f) of NC (x) must be writable
2058             if f_of_x.is_ro():
2059                 continue
2060
2061             # Replica (f) of NC (x) must satisfy the
2062             # "is present" criteria for DC (s) that
2063             # it was found on
2064             if not f_of_x.is_present():
2065                 continue
2066
2067             # DC (s) must be a writable DSA other than
2068             # my local DC.  In other words we'd only replicate
2069             # from other writable DC
2070             if dc_s.is_ro() or dc_s is dc_local:
2071                 continue
2072
2073             # Certain replica graphs are produced only
2074             # for global catalogs, so test against
2075             # method input parameter
2076             if gc_only and not dc_s.is_gc():
2077                 continue
2078
2079             # DC (s) must be in the same site as the local DC
2080             # as this is the intra-site algorithm. This is
2081             # handled by virtue of placing DSAs in per
2082             # site objects (see enclosing for() loop)
2083
2084             # If NC (x) is intended to be read-only full replica
2085             # for a domain NC on the target DC then the source
2086             # DC should have functional level at minimum WIN2008
2087             #
2088             # Effectively we're saying that in order to replicate
2089             # to a targeted RODC (which was introduced in Windows 2008)
2090             # then we have to replicate from a DC that is also minimally
2091             # at that level.
2092             #
2093             # You can also see this requirement in the MS special
2094             # considerations for RODC which state that to deploy
2095             # an RODC, at least one writable domain controller in
2096             # the domain must be running Windows Server 2008
2097             if ro and not partial and nc_x.nc_type == NCType.domain:
2098                 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2099                     continue
2100
2101             # If we haven't been told to turn off stale connection
2102             # detection and this dsa has a stale connection then
2103             # continue
2104             if detect_stale and self.is_stale_link_connection(dc_s):
2105                continue
2106
2107             # Replica meets criteria.  Add it to table indexed
2108             # by the GUID of the DC that it appears on
2109             r_list.append(f_of_x)
2110
2111         # If a partial (not full) replica of NC (x) "should be present"
2112         # on the local DC, append to R each partial replica (p of x)
2113         # such that p "is present" on a DC satisfying the same
2114         # criteria defined above for full replica DCs.
2115         #
2116         # XXX This loop and the previous one differ only in whether
2117         # the replica is partial or not. here we only accept partial
2118         # (because we're partial); before we only accepted full. Order
2119         # doen't matter (the list is sorted a few lines down) so these
2120         # loops could easily be merged. Or this could be a helper
2121         # function.
2122
2123         if partial:
2124             # Now we loop thru all the DSAs looking for
2125             # partial NC replicas that match the naming
2126             # context dn for (NC x)
2127             for dc_s in self.my_site.dsa_table.values():
2128
2129                 # If this partition NC (x) doesn't appear as a
2130                 # replica (p) of NC (x) on the dsa DC (s) then
2131                 # continue
2132                 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2133                     continue
2134
2135                 # Pull out the NCReplica with the dn that
2136                 # matches NC (x) we are examining.
2137                 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2138
2139                 # Replica (p) of NC (x) must be partial
2140                 if not p_of_x.is_partial():
2141                     continue
2142
2143                 # Replica (p) of NC (x) must satisfy the
2144                 # "is present" criteria for DC (s) that
2145                 # it was found on
2146                 if not p_of_x.is_present():
2147                     continue
2148
2149                 # DC (s) must be a writable DSA other than
2150                 # my DSA.  In other words we'd only replicate
2151                 # from other writable DSA
2152                 if dc_s.is_ro() or dc_s is dc_local:
2153                     continue
2154
2155                 # Certain replica graphs are produced only
2156                 # for global catalogs, so test against
2157                 # method input parameter
2158                 if gc_only and not dc_s.is_gc():
2159                     continue
2160
2161                 # DC (s) must be in the same site as the local DC
2162                 # as this is the intra-site algorithm. This is
2163                 # handled by virtue of placing DSAs in per
2164                 # site objects (see enclosing for() loop)
2165
2166                 # This criteria is moot (a no-op) for this case
2167                 # because we are scanning for (partial = True).  The
2168                 # MS algorithm statement says partial replica scans
2169                 # should adhere to the "same" criteria as full replica
2170                 # scans so the criteria doesn't change here...its just
2171                 # rendered pointless.
2172                 #
2173                 # The case that is occurring would be a partial domain
2174                 # replica is needed on a local DC global catalog.  There
2175                 # is no minimum windows behavior for those since GCs
2176                 # have always been present.
2177                 if ro and not partial and nc_x.nc_type == NCType.domain:
2178                     if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2179                         continue
2180
2181                 # If we haven't been told to turn off stale connection
2182                 # detection and this dsa has a stale connection then
2183                 # continue
2184                 if detect_stale and self.is_stale_link_connection(dc_s):
2185                     continue
2186
2187                 # Replica meets criteria.  Add it to table indexed
2188                 # by the GUID of the DSA that it appears on
2189                 r_list.append(p_of_x)
2190
2191         # Append to R the NC replica that "should be present"
2192         # on the local DC
2193         r_list.append(l_of_x)
2194
2195         r_list.sort(sort_replica_by_dsa_guid)
2196         r_len = len(r_list)
2197
2198         max_node_edges = self.intrasite_max_node_edges(r_len)
2199
2200         # Add a node for each r_list element to the replica graph
2201         graph_list = []
2202         for rep in r_list:
2203             node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2204             graph_list.append(node)
2205
2206         # For each r(i) from (0 <= i < |R|-1)
2207         i = 0
2208         while i < (r_len-1):
2209             # Add an edge from r(i) to r(i+1) if r(i) is a full
2210             # replica or r(i+1) is a partial replica
2211             if not r_list[i].is_partial() or r_list[i+1].is_partial():
2212                 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2213
2214             # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2215             # replica or ri is a partial replica.
2216             if not r_list[i+1].is_partial() or r_list[i].is_partial():
2217                 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2218             i = i + 1
2219
2220         # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2221         # or r0 is a partial replica.
2222         if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2223             graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2224
2225         # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2226         # r|R|-1 is a partial replica.
2227         if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2228             graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2229
2230         DEBUG("r_list is length %s" % len(r_list))
2231         DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr)) for x in r_list))
2232
2233         do_dot_files = opts.dot_files and opts.debug
2234         if opts.verify or do_dot_files:
2235             dot_edges = []
2236             dot_vertices = set()
2237             for v1 in graph_list:
2238                 dot_vertices.add(v1.dsa_dnstr)
2239                 for v2 in v1.edge_from:
2240                     dot_edges.append((v2, v1.dsa_dnstr))
2241                     dot_vertices.add(v2)
2242
2243             verify_properties = ('connected', 'directed_double_ring')
2244             verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2245                            label='%s__%s__%s' % (site_local.site_dnstr, nctype_lut[nc_x.nc_type], nc_x.nc_dnstr),
2246                            properties=verify_properties, debug=DEBUG, verify=opts.verify,
2247                            dot_files=do_dot_files, directed=True)
2248
2249
2250
2251         # For each existing nTDSConnection object implying an edge
2252         # from rj of R to ri such that j != i, an edge from rj to ri
2253         # is not already in the graph, and the total edges directed
2254         # to ri is less than n+2, the KCC adds that edge to the graph.
2255         for vertex in graph_list:
2256             dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2257             for connect in dsa.connect_table.values():
2258                 remote = connect.from_dnstr
2259                 if remote in self.my_site.dsa_table:
2260                     vertex.add_edge_from(remote)
2261
2262         DEBUG('reps are:  %s' % '   '.join(x.rep_dsa_dnstr for x in r_list))
2263         DEBUG('dsas are:  %s' % '   '.join(x.dsa_dnstr for x in graph_list))
2264
2265         for tnode in graph_list:
2266             # To optimize replication latency in sites with many NC replicas, the
2267             # KCC adds new edges directed to ri to bring the total edges to n+2,
2268             # where the NC replica rk of R from which the edge is directed
2269             # is chosen at random such that k != i and an edge from rk to ri
2270             # is not already in the graph.
2271             #
2272             # Note that the KCC tech ref does not give a number for the definition
2273             # of "sites with many NC replicas".   At a bare minimum to satisfy
2274             # n+2 edges directed at a node we have to have at least three replicas
2275             # in |R| (i.e. if n is zero then at least replicas from two other graph
2276             # nodes may direct edges to us).
2277             if r_len >= 3 and not tnode.has_sufficient_edges():
2278                 candidates = [x for x in graph_list if (x is not tnode and
2279                                                         x.dsa_dnstr not in tnode.edge_from)]
2280
2281                 DEBUG_BLUE("looking for random link for %s. r_len %d, graph len %d candidates %d"
2282                            % (tnode.dsa_dnstr, r_len, len(graph_list), len(candidates)))
2283
2284                 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2285
2286                 while candidates and not tnode.has_sufficient_edges():
2287                     other = random.choice(candidates)
2288                     DEBUG("trying to add candidate %s" % other.dsa_dstr)
2289                     if not tnode.add_edge_from(other):
2290                         DEBUG_RED("could not add %s" % other.dsa_dstr)
2291                     candidates.remove(other)
2292             else:
2293                 DEBUG_CYAN("not adding links to %s: nodes %s, links is %s/%s" %
2294                            (tnode.dsa_dnstr, r_len, len(tnode.edge_from), tnode.max_edges))
2295
2296
2297             # Print the graph node in debug mode
2298             logger.debug("%s" % tnode)
2299
2300             # For each edge directed to the local DC, ensure a nTDSConnection
2301             # points to us that satisfies the KCC criteria
2302
2303             if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2304                 tnode.add_connections_from_edges(dc_local)
2305
2306
2307         if opts.verify or do_dot_files:
2308             dot_edges = []
2309             dot_vertices = set()
2310             for v1 in graph_list:
2311                 dot_vertices.add(v1.dsa_dnstr)
2312                 for v2 in v1.edge_from:
2313                     dot_edges.append((v2, v1.dsa_dnstr))
2314                     dot_vertices.add(v2)
2315
2316             verify_properties = ('connected', 'directed_double_ring_or_small')
2317             verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2318                            label='%s__%s__%s' % (site_local.site_dnstr, nctype_lut[nc_x.nc_type], nc_x.nc_dnstr),
2319                            properties=verify_properties, debug=DEBUG, verify=opts.verify,
2320                            dot_files=do_dot_files, directed=True)
2321
2322
2323     def intrasite(self):
2324         """The head method for generating the intra-site KCC replica
2325         connection graph and attendant nTDSConnection objects
2326         in the samdb
2327         """
2328         # Retrieve my DSA
2329         mydsa = self.my_dsa
2330
2331         logger.debug("intrasite(): enter")
2332
2333         # Test whether local site has topology disabled
2334         mysite = self.my_site
2335         if mysite.is_intrasite_topology_disabled():
2336             return
2337
2338         detect_stale = (not mysite.is_detect_stale_disabled())
2339         for connect in mydsa.connect_table.values():
2340             if connect.to_be_added:
2341                 DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2342
2343         # Loop thru all the partitions, with gc_only False
2344         for partdn, part in self.part_table.items():
2345             self.construct_intrasite_graph(mysite, mydsa, part, False,
2346                                            detect_stale)
2347             for connect in mydsa.connect_table.values():
2348                 if connect.to_be_added:
2349                     DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2350
2351
2352         # If the DC is a GC server, the KCC constructs an additional NC
2353         # replica graph (and creates nTDSConnection objects) for the
2354         # config NC as above, except that only NC replicas that "are present"
2355         # on GC servers are added to R.
2356         for connect in mydsa.connect_table.values():
2357             if connect.to_be_added:
2358                 DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2359
2360         # Do it again, with gc_only True
2361         for partdn, part in self.part_table.items():
2362             if part.is_config():
2363                 self.construct_intrasite_graph(mysite, mydsa, part, True,
2364                                                detect_stale)
2365
2366         # The DC repeats the NC replica graph computation and nTDSConnection
2367         # creation for each of the NC replica graphs, this time assuming
2368         # that no DC has failed. It does so by re-executing the steps as
2369         # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2370         # set in the options attribute of the site settings object for
2371         # the local DC's site.  (ie. we set "detec_stale" flag to False)
2372         for connect in mydsa.connect_table.values():
2373             if connect.to_be_added:
2374                 DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2375
2376         # Loop thru all the partitions.
2377         for partdn, part in self.part_table.items():
2378             self.construct_intrasite_graph(mysite, mydsa, part, False,
2379                                            False) # don't detect stale
2380
2381         # If the DC is a GC server, the KCC constructs an additional NC
2382         # replica graph (and creates nTDSConnection objects) for the
2383         # config NC as above, except that only NC replicas that "are present"
2384         # on GC servers are added to R.
2385         for connect in mydsa.connect_table.values():
2386             if connect.to_be_added:
2387                 DEBUG_RED("TO BE ADDED:\n%s" % connect)
2388
2389         for partdn, part in self.part_table.items():
2390             if part.is_config():
2391                 self.construct_intrasite_graph(mysite, mydsa, part, True,
2392                                                False)  # don't detect stale
2393
2394         if opts.readonly:
2395             # Display any to be added or modified repsFrom
2396             for connect in mydsa.connect_table.values():
2397                 if connect.to_be_deleted:
2398                     logger.info("TO BE DELETED:\n%s" % connect)
2399                 if connect.to_be_modified:
2400                     logger.info("TO BE MODIFIED:\n%s" % connect)
2401                 if connect.to_be_added:
2402                     DEBUG_GREEN("TO BE ADDED:\n%s" % connect)
2403
2404             mydsa.commit_connections(self.samdb, ro=True)
2405         else:
2406             # Commit any newly created connections to the samdb
2407             mydsa.commit_connections(self.samdb)
2408
2409
2410     def list_dsas(self):
2411         self.load_my_site()
2412         self.load_my_dsa()
2413
2414         self.load_all_sites()
2415         self.load_all_partitions()
2416         self.load_all_transports()
2417         self.load_all_sitelinks()
2418         dsas = []
2419         for site in self.site_table.values():
2420             dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2421                          for dsa in site.dsa_table.values()])
2422         return dsas
2423
2424     def load_samdb(self, dburl, lp, creds):
2425         self.samdb = SamDB(url=dburl,
2426                            session_info=system_session(),
2427                            credentials=creds, lp=lp)
2428
2429
2430
2431     def plot_all_connections(self, basename, verify_properties=()):
2432         verify = verify_properties and opts.verify
2433         plot = opts.dot_files
2434         if not (verify or plot):
2435             return
2436
2437         dot_edges = []
2438         dot_vertices = []
2439         edge_colours = []
2440         vertex_colours = []
2441
2442         for dsa in self.dsa_by_dnstr.values():
2443             dot_vertices.append(dsa.dsa_dnstr)
2444             if dsa.is_ro():
2445                 vertex_colours.append('#cc0000')
2446             else:
2447                 vertex_colours.append('#0000cc')
2448             for con in dsa.connect_table.values():
2449                 if con.is_rodc_topology():
2450                     edge_colours.append('red')
2451                 else:
2452                     edge_colours.append('blue')
2453                 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2454
2455         verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2456                        label=self.my_dsa_dnstr, properties=verify_properties,
2457                        debug=DEBUG, verify=verify, dot_files=plot,
2458                        directed=True, edge_colors=edge_colours,
2459                        vertex_colors=vertex_colours)
2460
2461
2462     def run(self, dburl, lp, creds, forced_local_dsa=None,
2463             forget_local_links=False, forget_intersite_links=False):
2464         """Method to perform a complete run of the KCC and
2465         produce an updated topology for subsequent NC replica
2466         syncronization between domain controllers
2467         """
2468         # We may already have a samdb setup if we are
2469         # currently importing an ldif for a test run
2470         if self.samdb is None:
2471             try:
2472                 self.load_samdb(dburl, lp, creds)
2473             except ldb.LdbError, (num, msg):
2474                 logger.error("Unable to open sam database %s : %s" %
2475                              (dburl, msg))
2476                 return 1
2477
2478
2479         if forced_local_dsa:
2480             self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" % forced_local_dsa)
2481
2482         try:
2483             # Setup
2484             self.load_my_site()
2485             self.load_my_dsa()
2486
2487             self.load_all_sites()
2488             self.load_all_partitions()
2489             self.load_all_transports()
2490             self.load_all_sitelinks()
2491
2492
2493             if opts.verify or opts.dot_files:
2494                 guid_to_dnstr = {}
2495                 for site in self.site_table.values():
2496                     guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2497                                          for dnstr, dsa in site.dsa_table.items())
2498
2499                 self.plot_all_connections('dsa_initial')
2500
2501                 dot_edges = []
2502                 current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
2503                 for dnstr, c_rep in current_rep_table.items():
2504                     DEBUG("c_rep %s" % c_rep)
2505                     dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2506
2507                 verify_and_dot('dsa_repsFrom_initial', dot_edges, directed=True, label=self.my_dsa_dnstr,
2508                                properties=(), debug=DEBUG, verify=opts.verify,
2509                                dot_files=opts.dot_files)
2510
2511
2512                 dot_edges = []
2513                 for site in self.site_table.values():
2514                     for dsa in site.dsa_table.values():
2515                         current_rep_table, needed_rep_table = dsa.get_rep_tables()
2516                         for dn_str, rep in current_rep_table.items():
2517                             for reps_from in rep.rep_repsFrom:
2518                                 DEBUG("rep %s" % rep)
2519                                 dsa_dn = guid_to_dnstr[str(reps_from.source_dsa_obj_guid)]
2520                                 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2521
2522                 verify_and_dot('dsa_repsFrom_initial_all', dot_edges, directed=True, label=self.my_dsa_dnstr,
2523                                properties=(), debug=DEBUG, verify=opts.verify,
2524                                dot_files=opts.dot_files)
2525
2526
2527                 dot_edges = []
2528                 for link in self.sitelink_table.values():
2529                     for a, b in itertools.combinations(link.site_list, 2):
2530                         dot_edges.append((str(a), str(b)))
2531                 verify_properties = ('connected',)
2532                 verify_and_dot('dsa_sitelink_initial', dot_edges, directed=False, label=self.my_dsa_dnstr,
2533                                properties=verify_properties, debug=DEBUG, verify=opts.verify,
2534                                dot_files=opts.dot_files)
2535
2536
2537             if forget_local_links:
2538                 for dsa in self.my_site.dsa_table.values():
2539                     dsa.connect_table = {k:v for k, v in dsa.connect_table.items()
2540                                          if v.is_rodc_topology()}
2541                 self.plot_all_connections('dsa_forgotten_local')
2542
2543
2544             if forget_intersite_links:
2545                 for site in self.site_table.values():
2546                     for dsa in site.dsa_table.values():
2547                         dsa.connect_table = {k:v for k, v in dsa.connect_table.items()
2548                                              if site is self.my_site and v.is_rodc_topology()}
2549
2550                 self.plot_all_connections('dsa_forgotten_all')
2551             # These are the published steps (in order) for the
2552             # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2553
2554             # Step 1
2555             self.refresh_failed_links_connections()
2556
2557             # Step 2
2558             self.intrasite()
2559
2560             # Step 3
2561             all_connected = self.intersite()
2562
2563             # Step 4
2564             self.remove_unneeded_ntdsconn(all_connected)
2565
2566             # Step 5
2567             self.translate_ntdsconn()
2568
2569             # Step 6
2570             self.remove_unneeded_failed_links_connections()
2571
2572             # Step 7
2573             self.update_rodc_connection()
2574
2575
2576             if opts.verify or opts.dot_files:
2577                 self.plot_all_connections('dsa_final', ('connected', 'forest_of_rings'))
2578
2579                 DEBUG_MAGENTA("there are %d dsa guids" % len(guid_to_dnstr))
2580
2581                 dot_edges = []
2582                 edge_colors = []
2583                 my_dnstr = self.my_dsa.dsa_dnstr
2584                 current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
2585                 for dnstr, n_rep in needed_rep_table.items():
2586                     for reps_from in n_rep.rep_repsFrom:
2587                         guid_str = str(reps_from.source_dsa_obj_guid)
2588                         dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2589                         edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2590
2591                 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True, label=self.my_dsa_dnstr,
2592                                properties=(), debug=DEBUG, verify=opts.verify,
2593                                dot_files=opts.dot_files, edge_colors=edge_colors)
2594
2595
2596                 dot_edges = []
2597
2598                 for site in self.site_table.values():
2599                     for dsa in site.dsa_table.values():
2600                         current_rep_table, needed_rep_table = dsa.get_rep_tables()
2601                         for n_rep in needed_rep_table.values():
2602                             for reps_from in n_rep.rep_repsFrom:
2603                                 dsa_dn = guid_to_dnstr[str(reps_from.source_dsa_obj_guid)]
2604                                 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2605
2606                 verify_and_dot('dsa_repsFrom_final_all', dot_edges, directed=True, label=self.my_dsa_dnstr,
2607                                properties=(), debug=DEBUG, verify=opts.verify,
2608                                dot_files=opts.dot_files)
2609
2610
2611         except:
2612             raise
2613
2614         return 0
2615
2616     def import_ldif(self, dburl, lp, creds, ldif_file):
2617         """Import all objects and attributes that are relevent
2618         to the KCC algorithms from a previously exported LDIF file.
2619
2620         The point of this function is to allow a programmer/debugger to
2621         import an LDIF file with non-security relevent information that
2622         was previously extracted from a DC database.  The LDIF file is used
2623         to create a temporary abbreviated database.  The KCC algorithm can
2624         then run against this abbreviated database for debug or test
2625         verification that the topology generated is computationally the
2626         same between different OSes and algorithms.
2627
2628         :param dburl: path to the temporary abbreviated db to create
2629         :param ldif_file: path to the ldif file to import
2630         """
2631         try:
2632             self.samdb = ldif_utils.ldif_to_samdb(dburl, lp, creds, ldif_file,
2633                                                   opts.forced_local_dsa)
2634         except ldif_utils.LdifError, e:
2635             print e
2636             return 1
2637         return 0
2638
2639     def export_ldif(self, dburl, lp, creds, ldif_file):
2640         """Routine to extract all objects and attributes that are relevent
2641         to the KCC algorithms from a DC database.
2642
2643         The point of this function is to allow a programmer/debugger to
2644         extract an LDIF file with non-security relevent information from
2645         a DC database.  The LDIF file can then be used to "import" via
2646         the import_ldif() function this file into a temporary abbreviated
2647         database.  The KCC algorithm can then run against this abbreviated
2648         database for debug or test verification that the topology generated
2649         is computationally the same between different OSes and algorithms.
2650
2651         :param dburl: LDAP database URL to extract info from
2652         :param ldif_file: output LDIF file name to create
2653         """
2654         try:
2655             ldif_utils.samdb_to_ldif_file(self.samdb, dburl, lp, creds, ldif_file)
2656         except ldif_utils.LdifError, e:
2657             print e
2658             return 1
2659         return 0
2660
2661 ##################################################
2662 # Global Functions
2663 ##################################################
2664 def sort_replica_by_dsa_guid(rep1, rep2):
2665     return cmp(ndr_pack(rep1.rep_dsa_guid), ndr_pack(rep2.rep_dsa_guid))
2666
2667 def sort_dsa_by_gc_and_guid(dsa1, dsa2):
2668     if dsa1.is_gc() and not dsa2.is_gc():
2669         return -1
2670     if not dsa1.is_gc() and dsa2.is_gc():
2671         return +1
2672     return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
2673
2674 def is_smtp_replication_available():
2675     """Currently always returns false because Samba
2676     doesn't implement SMTP transfer for NC changes
2677     between DCs
2678     """
2679     return False
2680
2681 def create_edge(con_type, site_link, guid_to_vertex):
2682     e = MultiEdge()
2683     e.site_link = site_link
2684     e.vertices = []
2685     for site_guid in site_link.site_list:
2686         if str(site_guid) in guid_to_vertex:
2687             e.vertices.extend(guid_to_vertex.get(str(site_guid)))
2688     e.repl_info.cost = site_link.cost
2689     e.repl_info.options = site_link.options
2690     e.repl_info.interval = site_link.interval
2691     e.repl_info.schedule = convert_schedule_to_repltimes(site_link.schedule)
2692     e.con_type = con_type
2693     e.directed = False
2694     return e
2695
2696 def create_auto_edge_set(graph, transport):
2697     e_set = MultiEdgeSet()
2698     e_set.guid = misc.GUID() # NULL guid, not associated with a SiteLinkBridge object
2699     for site_link in graph.edges:
2700         if site_link.con_type == transport:
2701             e_set.edges.append(site_link)
2702
2703     return e_set
2704
2705 def create_edge_set(graph, transport, site_link_bridge):
2706     # TODO not implemented - need to store all site link bridges
2707     e_set = MultiEdgeSet()
2708     # e_set.guid = site_link_bridge
2709     return e_set
2710
2711 def setup_vertices(graph):
2712     for v in graph.vertices:
2713         if v.is_white():
2714             v.repl_info.cost = MAX_DWORD
2715             v.root = None
2716             v.component_id = None
2717         else:
2718             v.repl_info.cost = 0
2719             v.root = v
2720             v.component_id = v
2721
2722         v.repl_info.interval = 0
2723         v.repl_info.options = 0xFFFFFFFF
2724         v.repl_info.schedule = None # TODO highly suspicious
2725         v.demoted = False
2726
2727 def dijkstra(graph, edge_type, include_black):
2728     queue = []
2729     setup_dijkstra(graph, edge_type, include_black, queue)
2730     while len(queue) > 0:
2731         cost, guid, vertex = heapq.heappop(queue)
2732         for edge in vertex.edges:
2733             for v in edge.vertices:
2734                 if v is not vertex:
2735                     # add new path from vertex to v
2736                     try_new_path(graph, queue, vertex, edge, v)
2737
2738 def setup_dijkstra(graph, edge_type, include_black, queue):
2739     setup_vertices(graph)
2740     for vertex in graph.vertices:
2741         if vertex.is_white():
2742             continue
2743
2744         if ((vertex.is_black() and not include_black)
2745             or edge_type not in vertex.accept_black
2746             or edge_type not in vertex.accept_red_red):
2747             vertex.repl_info.cost = MAX_DWORD
2748             vertex.root = None # NULL GUID
2749             vertex.demoted = True # Demoted appears not to be used
2750         else:
2751             heapq.heappush(queue, (vertex.repl_info.cost, vertex.guid, vertex))
2752
2753 def try_new_path(graph, queue, vfrom, edge, vto):
2754     newRI = ReplInfo()
2755     # What this function checks is that there is a valid time frame for
2756     # which replication can actually occur, despite being adequately
2757     # connected
2758     intersect = combine_repl_info(vfrom.repl_info, edge.repl_info, newRI)
2759
2760     # If the new path costs more than the current, then ignore the edge
2761     if newRI.cost > vto.repl_info.cost:
2762         return
2763
2764     if newRI.cost < vto.repl_info.cost and not intersect:
2765         return
2766
2767     new_duration = total_schedule(newRI.schedule)
2768     old_duration = total_schedule(vto.repl_info.schedule)
2769
2770     # Cheaper or longer schedule
2771     if newRI.cost < vto.repl_info.cost or new_duration > old_duration:
2772         vto.root = vfrom.root
2773         vto.component_id = vfrom.component_id
2774         vto.repl_info = newRI
2775         heapq.heappush(queue, (vto.repl_info.cost, vto.guid, vto))
2776
2777 def check_demote_vertex(vertex, edge_type):
2778     if vertex.is_white():
2779         return
2780
2781     # Accepts neither red-red nor black edges, demote
2782     if edge_type not in vertex.accept_black and edge_type not in vertex.accept_red_red:
2783         vertex.repl_info.cost = MAX_DWORD
2784         vertex.root = None
2785         vertex.demoted = True # Demoted appears not to be used
2786
2787 def undemote_vertex(vertex):
2788     if vertex.is_white():
2789         return
2790
2791     vertex.repl_info.cost = 0
2792     vertex.root = vertex
2793     vertex.demoted = False
2794
2795 def process_edge_set(graph, e_set, internal_edges):
2796     if e_set is None:
2797         for edge in graph.edges:
2798             for vertex in edge.vertices:
2799                 check_demote_vertex(vertex, edge.con_type)
2800             process_edge(graph, edge, internal_edges)
2801             for vertex in edge.vertices:
2802                 undemote_vertex(vertex)
2803     else:
2804         for edge in e_set.edges:
2805             process_edge(graph, edge, internal_edges)
2806
2807 def process_edge(graph, examine, internal_edges):
2808     # Find the set of all vertices touches the edge to examine
2809     vertices = []
2810     for v in examine.vertices:
2811         # Append a 4-tuple of color, repl cost, guid and vertex
2812         vertices.append((v.color, v.repl_info.cost, v.ndrpacked_guid, v))
2813     # Sort by color, lower
2814     DEBUG("vertices is %s" % vertices)
2815     vertices.sort()
2816
2817     color, cost, guid, bestv = vertices[0]
2818     # Add to internal edges an edge from every colored vertex to bestV
2819     for v in examine.vertices:
2820         if v.component_id is None or v.root is None:
2821             continue
2822
2823         # Only add edge if valid inter-tree edge - needs a root and
2824         # different components
2825         if (bestv.component_id is not None and bestv.root is not None
2826             and v.component_id is not None and v.root is not None and
2827             bestv.component_id != v.component_id):
2828             add_int_edge(graph, internal_edges, examine, bestv, v)
2829
2830 # Add internal edge, endpoints are roots of the vertices to pass in and are always colored
2831 def add_int_edge(graph, internal_edges, examine, v1, v2):
2832     root1 = v1.root
2833     root2 = v2.root
2834
2835     red_red = False
2836     if root1.is_red() and root2.is_red():
2837         red_red = True
2838
2839     if red_red:
2840         if (examine.con_type not in root1.accept_red_red
2841             or examine.con_type not in root2.accept_red_red):
2842             return
2843     else:
2844         if (examine.con_type not in root1.accept_black
2845             or examine.con_type not in root2.accept_black):
2846             return
2847
2848     ri = ReplInfo()
2849     ri2 = ReplInfo()
2850
2851     # Create the transitive replInfo for the two trees and this edge
2852     if not combine_repl_info(v1.repl_info, v2.repl_info, ri):
2853         return
2854     # ri is now initialized
2855     if not combine_repl_info(ri, examine.repl_info, ri2):
2856         return
2857
2858     newIntEdge = InternalEdge(root1, root2, red_red, ri2, examine.con_type, examine.site_link)
2859     # Order by vertex guid
2860     #XXX guid comparison using ndr_pack
2861     if newIntEdge.v1.ndrpacked_guid > newIntEdge.v2.ndrpacked_guid:
2862         newIntEdge.v1 = root2
2863         newIntEdge.v2 = root1
2864
2865     internal_edges.add(newIntEdge)
2866
2867 def kruskal(graph, edges):
2868     for v in graph.vertices:
2869         v.edges = []
2870
2871     components = set([x for x in graph.vertices if not x.is_white()])
2872     edges = list(edges)
2873
2874     # Sorted based on internal comparison function of internal edge
2875     edges.sort()
2876
2877     expected_num_tree_edges = 0 # TODO this value makes little sense
2878
2879     count_edges = 0
2880     output_edges = []
2881     index = 0
2882     while index < len(edges): # TODO and num_components > 1
2883         e = edges[index]
2884         parent1 = find_component(e.v1)
2885         parent2 = find_component(e.v2)
2886         if parent1 is not parent2:
2887             count_edges += 1
2888             add_out_edge(graph, output_edges, e)
2889             parent1.component_id = parent2
2890             components.discard(parent1)
2891
2892         index += 1
2893
2894     return output_edges, len(components)
2895
2896 def find_component(vertex):
2897     if vertex.component_id is vertex:
2898         return vertex
2899
2900     current = vertex
2901     while current.component_id is not current:
2902         current = current.component_id
2903
2904     root = current
2905     current = vertex
2906     while current.component_id is not root:
2907         n = current.component_id
2908         current.component_id = root
2909         current = n
2910
2911     return root
2912
2913 def add_out_edge(graph, output_edges, e):
2914     v1 = e.v1
2915     v2 = e.v2
2916
2917     # This multi-edge is a 'real' edge with no GUID
2918     ee = MultiEdge()
2919     ee.directed = False
2920     ee.site_link = e.site_link
2921     ee.vertices.append(v1)
2922     ee.vertices.append(v2)
2923     ee.con_type = e.e_type
2924     ee.repl_info = e.repl_info
2925     output_edges.append(ee)
2926
2927     v1.edges.append(ee)
2928     v2.edges.append(ee)
2929
2930
2931 def test_all_reps_from(lp, creds):
2932     kcc = KCC()
2933     kcc.load_samdb(opts.dburl, lp, creds)
2934     dsas = kcc.list_dsas()
2935     needed_parts = {}
2936     current_parts = {}
2937
2938     guid_to_dnstr = {}
2939     for site in kcc.site_table.values():
2940         guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2941                              for dnstr, dsa in site.dsa_table.items())
2942
2943     dot_edges = []
2944     dot_vertices = []
2945     colours = []
2946     vertex_colours = []
2947
2948     for dsa_dn in dsas:
2949         kcc = KCC()
2950         kcc.run(opts.dburl, lp, creds, forced_local_dsa=dsa_dn,
2951                 forget_local_links=opts.forget_local_links,
2952                 forget_intersite_links=opts.forget_intersite_links)
2953         current, needed = kcc.my_dsa.get_rep_tables()
2954
2955
2956         for name, rep_table, rep_parts in (('needed', needed, needed_parts),
2957                                            ('current', current, current_parts)):
2958             for part, nc_rep in rep_table.items():
2959                 edges = rep_parts.setdefault(part, [])
2960                 for reps_from in nc_rep.rep_repsFrom:
2961                     source = guid_to_dnstr[str(reps_from.source_dsa_obj_guid)]
2962                     dest = guid_to_dnstr[str(nc_rep.rep_dsa_guid)]
2963                     edges.append((source, dest))
2964
2965         for site in kcc.site_table.values():
2966             for dsa in site.dsa_table.values():
2967                 if dsa.is_ro():
2968                     vertex_colours.append('#cc0000')
2969                 else:
2970                     vertex_colours.append('#0000cc')
2971                 dot_vertices.append(dsa.dsa_dnstr)
2972                 for con in dsa.connect_table.values():
2973                     if con.is_rodc_topology():
2974                         colours.append('red')
2975                     else:
2976                         colours.append('blue')
2977                     dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2978
2979     verify_and_dot('all-dsa-connections', dot_edges, vertices=dot_vertices,
2980                    label="all dsa NTDSConnections", properties=(),
2981                    debug=DEBUG, verify=opts.verify, dot_files=opts.dot_files,
2982                    directed=True, edge_colors=colours, vertex_colors=vertex_colours)
2983
2984     for name, rep_parts in (('needed', needed_parts), ('current', current_parts)):
2985         for part, edges in rep_parts.items():
2986             verify_and_dot('repsFrom_%s_all_%s' % (name, part), edges, directed=True, label=part,
2987                            properties=(), debug=DEBUG, verify=opts.verify,
2988                            dot_files=opts.dot_files)
2989
2990
2991
2992 logger = logging.getLogger("samba_kcc")
2993 logger.addHandler(logging.StreamHandler(sys.stdout))
2994 DEBUG = logger.debug
2995
2996 def _colour_debug(*args, **kwargs):
2997     DEBUG('%s%s%s' % (kwargs['colour'], args[0], C_NORMAL), *args[1:])
2998
2999 _globals = globals()
3000 for _colour in ('DARK_RED', 'RED', 'DARK_GREEN', 'GREEN', 'YELLOW',
3001                 'DARK_YELLOW', 'DARK_BLUE', 'BLUE', 'PURPLE', 'MAGENTA',
3002                 'DARK_CYAN', 'CYAN', 'GREY', 'WHITE', 'REV_RED'):
3003     _globals['DEBUG_' + _colour] = partial(_colour_debug, colour=_globals[_colour])
3004
3005
3006 ##################################################
3007 # samba_kcc entry point
3008 ##################################################
3009
3010 parser = optparse.OptionParser("samba_kcc [options]")
3011 sambaopts = options.SambaOptions(parser)
3012 credopts = options.CredentialsOptions(parser)
3013
3014 parser.add_option_group(sambaopts)
3015 parser.add_option_group(credopts)
3016 parser.add_option_group(options.VersionOptions(parser))
3017
3018 parser.add_option("--readonly", default=False,
3019                   help="compute topology but do not update database",
3020                   action="store_true")
3021
3022 parser.add_option("--debug",
3023                   help="debug output",
3024                   action="store_true")
3025
3026 parser.add_option("--verify",
3027                   help="verify that assorted invariants are kept",
3028                   action="store_true")
3029
3030 parser.add_option("--list-verify-tests",
3031                   help="list what verification actions are available and do nothing else",
3032                   action="store_true")
3033
3034 parser.add_option("--no-dot-files", dest='dot_files',
3035                   help="Don't write dot graph files in /tmp",
3036                   default=True, action="store_false")
3037
3038 parser.add_option("--seed",
3039                   help="random number seed",
3040                   type=int)
3041
3042 parser.add_option("--importldif",
3043                   help="import topology ldif file",
3044                   type=str, metavar="<file>")
3045
3046 parser.add_option("--exportldif",
3047                   help="export topology ldif file",
3048                   type=str, metavar="<file>")
3049
3050 parser.add_option("-H", "--URL" ,
3051                   help="LDB URL for database or target server",
3052                   type=str, metavar="<URL>", dest="dburl")
3053
3054 parser.add_option("--tmpdb",
3055                   help="schemaless database file to create for ldif import",
3056                   type=str, metavar="<file>")
3057
3058 parser.add_option("--now",
3059                   help="assume current time is this ('YYYYmmddHHMMSS[tz]', default: system time)",
3060                   type=str, metavar="<date>")
3061
3062 parser.add_option("--forced-local-dsa",
3063                   help="run calculations assuming the DSA is this DN",
3064                   type=str, metavar="<DSA>")
3065
3066 parser.add_option("--attempt-live-connections", default=False,
3067                   help="Attempt to connect to other DSAs to test links",
3068                   action="store_true")
3069
3070 parser.add_option("--list-valid-dsas", default=False,
3071                   help="Print a list of DSA dnstrs that could be used in --forced-local-dsa",
3072                   action="store_true")
3073
3074 parser.add_option("--test-all-reps-from", default=False,
3075                   help="Create and verify a graph of reps-from for every DSA",
3076                   action="store_true")
3077
3078 parser.add_option("--forget-local-links", default=False,
3079                   help="pretend not to know the existing local topology",
3080                   action="store_true")
3081
3082 parser.add_option("--forget-intersite-links", default=False,
3083                   help="pretend not to know the existing intersite topology",
3084                   action="store_true")
3085
3086
3087 opts, args = parser.parse_args()
3088
3089
3090 if opts.list_verify_tests:
3091     list_verify_tests()
3092     sys.exit(0)
3093
3094 if opts.debug:
3095     logger.setLevel(logging.DEBUG)
3096 elif opts.readonly:
3097     logger.setLevel(logging.INFO)
3098 else:
3099     logger.setLevel(logging.WARNING)
3100
3101 # initialize seed from optional input parameter
3102 if opts.seed:
3103     random.seed(opts.seed)
3104 else:
3105     random.seed(0xACE5CA11)
3106
3107 if opts.now:
3108     for timeformat in ("%Y%m%d%H%M%S%Z", "%Y%m%d%H%M%S"):
3109         try:
3110             now_tuple = time.strptime(opts.now, timeformat)
3111             break
3112         except ValueError:
3113             pass
3114     else:
3115         # else happens if break doesn't --> no match
3116         print >> sys.stderr, "could not parse time '%s'" % opts.now
3117         sys.exit(1)
3118
3119     unix_now = int(time.mktime(now_tuple))
3120 else:
3121     unix_now = int(time.time())
3122
3123 nt_now = unix2nttime(unix_now)
3124
3125 lp = sambaopts.get_loadparm()
3126 creds = credopts.get_credentials(lp, fallback_machine=True)
3127
3128 if opts.dburl is None:
3129     opts.dburl = lp.samdb_url()
3130
3131 if opts.test_all_reps_from:
3132     opts.readonly = True
3133     test_all_reps_from(lp, creds)
3134     sys.exit()
3135
3136 # Instantiate Knowledge Consistency Checker and perform run
3137 kcc = KCC()
3138
3139 if opts.exportldif:
3140     rc = kcc.export_ldif(opts.dburl, lp, creds, opts.exportldif)
3141     sys.exit(rc)
3142
3143 if opts.importldif:
3144     if opts.tmpdb is None or opts.tmpdb.startswith('ldap'):
3145         logger.error("Specify a target temp database file with --tmpdb option.")
3146         sys.exit(1)
3147
3148     rc = kcc.import_ldif(opts.tmpdb, lp, creds, opts.importldif)
3149     if rc != 0:
3150         sys.exit(rc)
3151
3152 if opts.list_valid_dsas:
3153     kcc.load_samdb(opts.dburl, lp, creds)
3154     print '\n'.join(kcc.list_dsas())
3155     sys.exit()
3156
3157 try:
3158     rc = kcc.run(opts.dburl, lp, creds, opts.forced_local_dsa,
3159                  opts.forget_local_links, opts.forget_intersite_links)
3160     sys.exit(rc)
3161
3162 except GraphError, e:
3163     print e
3164     sys.exit(1)