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