KCC: fix typo in error path
[nivanova/samba-autobuild/.git] / python / samba / kcc / __init__.py
1 # define the KCC object
2 #
3 # Copyright (C) Dave Craft 2011
4 # Copyright (C) Andrew Bartlett 2015
5 #
6 # Andrew Bartlett's alleged work performed by his underlings Douglas
7 # Bagnall and Garming Sam.
8 #
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
22 import random
23 import uuid
24
25 import itertools
26 from samba import unix2nttime, nttime2unix
27 from samba import ldb, dsdb, drs_utils
28 from samba.auth import system_session
29 from samba.samdb import SamDB
30 from samba.dcerpc import drsuapi, misc
31
32 from samba.kcc.kcc_utils import Site, Partition, Transport, SiteLink
33 from samba.kcc.kcc_utils import NCReplica, NCType, nctype_lut, GraphNode
34 from samba.kcc.kcc_utils import RepsFromTo, KCCError, KCCFailedObject
35 from samba.kcc.graph import convert_schedule_to_repltimes
36
37 from samba.ndr import ndr_pack
38
39 from samba.kcc.graph_utils import verify_and_dot
40
41 from samba.kcc import ldif_import_export
42 from samba.kcc.graph import setup_graph, get_spanning_tree_edges
43 from samba.kcc.graph import Vertex
44
45 from samba.kcc.debug import DEBUG, DEBUG_FN, logger
46 from samba.kcc import debug
47
48
49 def sort_replica_by_dsa_guid(rep1, rep2):
50     """Helper to sort NCReplicas by their DSA guids
51
52     The guids need to be sorted in their NDR form.
53
54     :param rep1: An NC replica
55     :param rep2: Another replica
56     :return: -1, 0, or 1, indicating sort order.
57     """
58     return cmp(ndr_pack(rep1.rep_dsa_guid), ndr_pack(rep2.rep_dsa_guid))
59
60
61 def sort_dsa_by_gc_and_guid(dsa1, dsa2):
62     """Helper to sort DSAs by guid global catalog status
63
64     GC DSAs come before non-GC DSAs, other than that, the guids are
65     sorted in NDR form.
66
67     :param dsa1: A DSA object
68     :param dsa2: Another DSA
69     :return: -1, 0, or 1, indicating sort order.
70     """
71     if dsa1.is_gc() and not dsa2.is_gc():
72         return -1
73     if not dsa1.is_gc() and dsa2.is_gc():
74         return +1
75     return cmp(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
76
77
78 def is_smtp_replication_available():
79     """Can the KCC use SMTP replication?
80
81     Currently always returns false because Samba doesn't implement
82     SMTP transfer for NC changes between DCs.
83
84     :return: Boolean (always False)
85     """
86     return False
87
88
89 class KCC(object):
90     """The Knowledge Consistency Checker class.
91
92     A container for objects and methods allowing a run of the KCC.  Produces a
93     set of connections in the samdb for which the Distributed Replication
94     Service can then utilize to replicate naming contexts
95
96     :param unix_now: The putative current time in seconds since 1970.
97     :param read_only: Don't write to the database.
98     :param verify: Check topological invariants for the generated graphs
99     :param debug: Write verbosely to stderr.
100     "param dot_file_dir: write diagnostic Graphviz files in this directory
101     """
102     def __init__(self, unix_now, readonly=False, verify=False, debug=False,
103                  dot_file_dir=None):
104         """Initializes the partitions class which can hold
105         our local DCs partitions or all the partitions in
106         the forest
107         """
108         self.part_table = {}    # partition objects
109         self.site_table = {}
110         self.ip_transport = None
111         self.sitelink_table = {}
112         self.dsa_by_dnstr = {}
113         self.dsa_by_guid = {}
114
115         self.get_dsa_by_guidstr = self.dsa_by_guid.get
116         self.get_dsa = self.dsa_by_dnstr.get
117
118         # TODO: These should be backed by a 'permanent' store so that when
119         # calling DRSGetReplInfo with DS_REPL_INFO_KCC_DSA_CONNECT_FAILURES,
120         # the failure information can be returned
121         self.kcc_failed_links = {}
122         self.kcc_failed_connections = set()
123
124         # Used in inter-site topology computation.  A list
125         # of connections (by NTDSConnection object) that are
126         # to be kept when pruning un-needed NTDS Connections
127         self.kept_connections = set()
128
129         self.my_dsa_dnstr = None  # My dsa DN
130         self.my_dsa = None  # My dsa object
131
132         self.my_site_dnstr = None
133         self.my_site = None
134
135         self.samdb = None
136
137         self.unix_now = unix_now
138         self.nt_now = unix2nttime(unix_now)
139         self.readonly = readonly
140         self.verify = verify
141         self.debug = debug
142         self.dot_file_dir = dot_file_dir
143
144     def load_ip_transport(self):
145         """Loads the inter-site transport objects for Sites
146
147         :return: None
148         :raise KCCError: if no IP transport is found
149         """
150         try:
151             res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
152                                     self.samdb.get_config_basedn(),
153                                     scope=ldb.SCOPE_SUBTREE,
154                                     expression="(objectClass=interSiteTransport)")
155         except ldb.LdbError, (enum, estr):
156             raise KCCError("Unable to find inter-site transports - (%s)" %
157                            estr)
158
159         for msg in res:
160             dnstr = str(msg.dn)
161
162             transport = Transport(dnstr)
163
164             transport.load_transport(self.samdb)
165             if transport.name == 'IP':
166                 self.ip_transport = transport
167             elif transport.name == 'SMTP':
168                 logger.info("Samba KCC is ignoring the obsolete SMTP transport.")
169
170             else:
171                 logger.warning("Samba KCC does not support the transport called %r."
172                                % (transport.name,))
173
174         if self.ip_transport is None:
175             raise KCCError("there doesn't seem to be an IP transport")
176
177     def load_all_sitelinks(self):
178         """Loads the inter-site siteLink objects
179
180         :return: None
181         :raise KCCError: if site-links aren't found
182         """
183         try:
184             res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
185                                     self.samdb.get_config_basedn(),
186                                     scope=ldb.SCOPE_SUBTREE,
187                                     expression="(objectClass=siteLink)")
188         except ldb.LdbError, (enum, estr):
189             raise KCCError("Unable to find inter-site siteLinks - (%s)" % estr)
190
191         for msg in res:
192             dnstr = str(msg.dn)
193
194             # already loaded
195             if dnstr in self.sitelink_table:
196                 continue
197
198             sitelink = SiteLink(dnstr)
199
200             sitelink.load_sitelink(self.samdb)
201
202             # Assign this siteLink to table
203             # and index by dn
204             self.sitelink_table[dnstr] = sitelink
205
206     def load_site(self, dn_str):
207         """Helper for load_my_site and load_all_sites.
208
209         Put all the site's DSAs into the KCC indices.
210
211         :param dn_str: a site dn_str
212         :return: the Site object pertaining to the dn_str
213         """
214         site = Site(dn_str, self.unix_now)
215         site.load_site(self.samdb)
216
217         # We avoid replacing the site with an identical copy in case
218         # somewhere else has a reference to the old one, which would
219         # lead to all manner of confusion and chaos.
220         guid = str(site.site_guid)
221         if guid not in self.site_table:
222             self.site_table[guid] = site
223             self.dsa_by_dnstr.update(site.dsa_table)
224             self.dsa_by_guid.update((str(x.dsa_guid), x)
225                                     for x in site.dsa_table.values())
226
227         return self.site_table[guid]
228
229     def load_my_site(self):
230         """Load the Site object for the local DSA.
231
232         :return: None
233         """
234         self.my_site_dnstr = ("CN=%s,CN=Sites,%s" % (
235             self.samdb.server_site_name(),
236             self.samdb.get_config_basedn()))
237
238         self.my_site = self.load_site(self.my_site_dnstr)
239
240     def load_all_sites(self):
241         """Discover all sites and create Site objects.
242
243         :return: None
244         :raise: KCCError if sites can't be found
245         """
246         try:
247             res = self.samdb.search("CN=Sites,%s" %
248                                     self.samdb.get_config_basedn(),
249                                     scope=ldb.SCOPE_SUBTREE,
250                                     expression="(objectClass=site)")
251         except ldb.LdbError, (enum, estr):
252             raise KCCError("Unable to find sites - (%s)" % estr)
253
254         for msg in res:
255             sitestr = str(msg.dn)
256             self.load_site(sitestr)
257
258     def load_my_dsa(self):
259         """Discover my nTDSDSA dn thru the rootDSE entry
260
261         :return: None
262         :raise: KCCError if DSA can't be found
263         """
264         dn = ldb.Dn(self.samdb, "<GUID=%s>" % self.samdb.get_ntds_GUID())
265         try:
266             res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
267                                     attrs=["objectGUID"])
268         except ldb.LdbError, (enum, estr):
269             logger.warning("Search for %s failed: %s.  This typically happens"
270                            " in --importldif mode due to lack of module"
271                            " support.", dn, estr)
272             try:
273                 # We work around the failure above by looking at the
274                 # dsServiceName that was put in the fake rootdse by
275                 # the --exportldif, rather than the
276                 # samdb.get_ntds_GUID(). The disadvantage is that this
277                 # mode requires we modify the @ROOTDSE dnq to support
278                 # --forced-local-dsa
279                 service_name_res = self.samdb.search(base="",
280                                                      scope=ldb.SCOPE_BASE,
281                                                      attrs=["dsServiceName"])
282                 dn = ldb.Dn(self.samdb,
283                             service_name_res[0]["dsServiceName"][0])
284
285                 res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
286                                         attrs=["objectGUID"])
287             except ldb.LdbError, (enum, estr):
288                 raise KCCError("Unable to find my nTDSDSA - (%s)" % estr)
289
290         if len(res) != 1:
291             raise KCCError("Unable to find my nTDSDSA at %s" %
292                            dn.extended_str())
293
294         ntds_guid = misc.GUID(self.samdb.get_ntds_GUID())
295         if misc.GUID(res[0]["objectGUID"][0]) != ntds_guid:
296             raise KCCError("Did not find the GUID we expected,"
297                            " perhaps due to --importldif")
298
299         self.my_dsa_dnstr = str(res[0].dn)
300
301         self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
302
303         if self.my_dsa_dnstr not in self.dsa_by_dnstr:
304             debug.DEBUG_DARK_YELLOW("my_dsa %s isn't in self.dsas_by_dnstr:"
305                                     " it must be RODC.\n"
306                                     "Let's add it, because my_dsa is special!"
307                                     "\n(likewise for self.dsa_by_guid)" %
308                                     self.my_dsa_dnstr)
309
310             self.dsa_by_dnstr[self.my_dsa_dnstr] = self.my_dsa
311             self.dsa_by_guid[str(self.my_dsa.dsa_guid)] = self.my_dsa
312
313     def load_all_partitions(self):
314         """Discover and load all partitions.
315
316         Each NC is inserted into the part_table by partition
317         dn string (not the nCName dn string)
318
319         :return: None
320         :raise: KCCError if partitions can't be found
321         """
322         try:
323             res = self.samdb.search("CN=Partitions,%s" %
324                                     self.samdb.get_config_basedn(),
325                                     scope=ldb.SCOPE_SUBTREE,
326                                     expression="(objectClass=crossRef)")
327         except ldb.LdbError, (enum, estr):
328             raise KCCError("Unable to find partitions - (%s)" % estr)
329
330         for msg in res:
331             partstr = str(msg.dn)
332
333             # already loaded
334             if partstr in self.part_table:
335                 continue
336
337             part = Partition(partstr)
338
339             part.load_partition(self.samdb)
340             self.part_table[partstr] = part
341
342     def refresh_failed_links_connections(self, ping=None):
343         """Ensure the failed links list is up to date
344
345         Based on MS-ADTS 6.2.2.1
346
347         :param ping: An oracle function of remote site availability
348         :return: None
349         """
350         # LINKS: Refresh failed links
351         self.kcc_failed_links = {}
352         current, needed = self.my_dsa.get_rep_tables()
353         for replica in current.values():
354             # For every possible connection to replicate
355             for reps_from in replica.rep_repsFrom:
356                 failure_count = reps_from.consecutive_sync_failures
357                 if failure_count <= 0:
358                     continue
359
360                 dsa_guid = str(reps_from.source_dsa_obj_guid)
361                 time_first_failure = reps_from.last_success
362                 last_result = reps_from.last_attempt
363                 dns_name = reps_from.dns_name1
364
365                 f = self.kcc_failed_links.get(dsa_guid)
366                 if f is None:
367                     f = KCCFailedObject(dsa_guid, failure_count,
368                                         time_first_failure, last_result,
369                                         dns_name)
370                     self.kcc_failed_links[dsa_guid] = f
371                 else:
372                     f.failure_count = max(f.failure_count, failure_count)
373                     f.time_first_failure = min(f.time_first_failure,
374                                                time_first_failure)
375                     f.last_result = last_result
376
377         # CONNECTIONS: Refresh failed connections
378         restore_connections = set()
379         if ping is not None:
380             DEBUG("refresh_failed_links: checking if links are still down")
381             for connection in self.kcc_failed_connections:
382                 if ping(connection.dns_name):
383                     # Failed connection is no longer failing
384                     restore_connections.add(connection)
385                 else:
386                     connection.failure_count += 1
387         else:
388             DEBUG("refresh_failed_links: not checking live links because we\n"
389                   "weren't asked to --attempt-live-connections")
390
391         # Remove the restored connections from the failed connections
392         self.kcc_failed_connections.difference_update(restore_connections)
393
394     def is_stale_link_connection(self, target_dsa):
395         """Check whether a link to a remote DSA is stale
396
397         Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation
398
399         Returns True if the remote seems to have been down for at
400         least two hours, otherwise False.
401
402         :param target_dsa: the remote DSA object
403         :return: True if link is stale, otherwise False
404         """
405         failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
406         if failed_link:
407             # failure_count should be > 0, but check anyways
408             if failed_link.failure_count > 0:
409                 unix_first_failure = \
410                     nttime2unix(failed_link.time_first_failure)
411                 # TODO guard against future
412                 if unix_first_failure > self.unix_now:
413                     logger.error("The last success time attribute for \
414                                  repsFrom is in the future!")
415
416                 # Perform calculation in seconds
417                 if (self.unix_now - unix_first_failure) > 60 * 60 * 2:
418                     return True
419
420         # TODO connections.
421         # We have checked failed *links*, but we also need to check
422         # *connections*
423
424         return False
425
426     # TODO: This should be backed by some form of local database
427     def remove_unneeded_failed_links_connections(self):
428         # Remove all tuples in kcc_failed_links where failure count = 0
429         # In this implementation, this should never happen.
430
431         # Remove all connections which were not used this run or connections
432         # that became active during this run.
433         pass
434
435     def _ensure_connections_are_loaded(self, connections):
436         """Load or fake-load NTDSConnections lacking GUIDs
437
438         New connections don't have GUIDs and created times which are
439         needed for sorting. If we're in read-only mode, we make fake
440         GUIDs, otherwise we ask SamDB to do it for us.
441
442         :param connections: an iterable of NTDSConnection objects.
443         :return: None
444         """
445         for cn_conn in connections:
446             if cn_conn.guid is None:
447                 if self.readonly:
448                     cn_conn.guid = misc.GUID(str(uuid.uuid4()))
449                     cn_conn.whenCreated = self.nt_now
450                 else:
451                     cn_conn.load_connection(self.samdb)
452
453     def _mark_broken_ntdsconn(self):
454         """Find NTDS Connections that lack a remote
455
456         I'm not sure how they appear. Let's be rid of them by marking
457         them with the to_be_deleted attribute.
458
459         :return: None
460         """
461         for cn_conn in self.my_dsa.connect_table.values():
462             s_dnstr = cn_conn.get_from_dnstr()
463             if s_dnstr is None:
464                 DEBUG_FN("%s has phantom connection %s" % (self.my_dsa,
465                                                            cn_conn))
466                 cn_conn.to_be_deleted = True
467
468     def _mark_unneeded_local_ntdsconn(self):
469         """Find unneeded intrasite NTDS Connections for removal
470
471         Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections.
472         Every DC removes its own unnecessary intrasite connections.
473         This function tags them with the to_be_deleted attribute.
474
475         :return: None
476         """
477         # XXX should an RODC be regarded as same site? It isn't part
478         # of the intrasite ring.
479
480         if self.my_site.is_cleanup_ntdsconn_disabled():
481             DEBUG_FN("not doing ntdsconn cleanup for site %s, "
482                      "because it is disabled" % self.my_site)
483             return
484
485         mydsa = self.my_dsa
486
487         self._ensure_connections_are_loaded(mydsa.connect_table.values())
488
489         local_connections = []
490
491         for cn_conn in mydsa.connect_table.values():
492             s_dnstr = cn_conn.get_from_dnstr()
493             if s_dnstr in self.my_site.dsa_table:
494                 removable = not (cn_conn.is_generated() or
495                                  cn_conn.is_rodc_topology())
496                 packed_guid = ndr_pack(cn_conn.guid)
497                 local_connections.append((cn_conn, s_dnstr,
498                                           packed_guid, removable))
499
500         for a, b in itertools.permutations(local_connections, 2):
501             cn_conn, s_dnstr, packed_guid, removable = a
502             cn_conn2, s_dnstr2, packed_guid2, removable2 = b
503             if (removable and
504                 s_dnstr == s_dnstr2 and
505                 cn_conn.whenCreated < cn_conn2.whenCreated or
506                 (cn_conn.whenCreated == cn_conn2.whenCreated and
507                  packed_guid < packed_guid2)):
508                 cn_conn.to_be_deleted = True
509
510     def _mark_unneeded_intersite_ntdsconn(self):
511         """find unneeded intersite NTDS Connections for removal
512
513         Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections. The
514         intersite topology generator removes links for all DCs in its
515         site. Here we just tag them with the to_be_deleted attribute.
516
517         :return: None
518         """
519         # Find the intersite connections
520         local_dsas = self.my_site.dsa_table
521         connections_and_dsas = []
522         for dsa in local_dsas.values():
523             for cn in dsa.connect_table.values():
524                 s_dnstr = cn.get_from_dnstr()
525                 if s_dnstr not in local_dsas:
526                     from_dsa = self.get_dsa(s_dnstr)
527                     connections_and_dsas.append((cn, dsa, from_dsa))
528
529         self._ensure_connections_are_loaded(x[0] for x in connections_and_dsas)
530         for cn, to_dsa, from_dsa in connections_and_dsas:
531             if not cn.is_generated() or cn.is_rodc_topology():
532                 continue
533
534             # If the connection is in the kept_connections list, we
535             # only remove it if an endpoint seems down.
536             if (cn in self.kept_connections and
537                 not (self.is_bridgehead_failed(to_dsa, True) or
538                      self.is_bridgehead_failed(from_dsa, True))):
539                 continue
540
541             # this one is broken and might be superseded by another.
542             # But which other? Let's just say another link to the same
543             # site can supersede.
544             from_dnstr = from_dsa.dsa_dnstr
545             for site in self.site_table.values():
546                 if from_dnstr in site.rw_dsa_table:
547                     for cn2, to_dsa2, from_dsa2 in connections_and_dsas:
548                         if (cn is not cn2 and
549                             from_dsa2 in site.rw_dsa_table):
550                             cn.to_be_deleted = True
551
552     def _commit_changes(self, dsa):
553         if dsa.is_ro() or self.readonly:
554             for connect in dsa.connect_table.values():
555                 if connect.to_be_deleted:
556                     logger.info("TO BE DELETED:\n%s" % connect)
557                 if connect.to_be_added:
558                     logger.info("TO BE ADDED:\n%s" % connect)
559                 if connect.to_be_modified:
560                     logger.info("TO BE MODIFIED:\n%s" % connect)
561
562             # Peform deletion from our tables but perform
563             # no database modification
564             dsa.commit_connections(self.samdb, ro=True)
565         else:
566             # Commit any modified connections
567             dsa.commit_connections(self.samdb)
568
569     def remove_unneeded_ntdsconn(self, all_connected):
570         """Remove unneeded NTDS Connections once topology is calculated
571
572         Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections
573
574         :param all_connected: indicates whether all sites are connected
575         :return: None
576         """
577         self._mark_broken_ntdsconn()
578         self._mark_unneeded_local_ntdsconn()
579         # if we are not the istg, we're done!
580         # if we are the istg, but all_connected is False, we also do nothing.
581         if self.my_dsa.is_istg() and all_connected:
582             self._mark_unneeded_intersite_ntdsconn()
583
584         for dsa in self.my_site.dsa_table.values():
585             self._commit_changes(dsa)
586
587
588     def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
589         """Update an repsFrom object if required.
590
591         Part of MS-ADTS 6.2.2.5.
592
593         Update t_repsFrom if necessary to satisfy requirements. Such
594         updates are typically required when the IDL_DRSGetNCChanges
595         server has moved from one site to another--for example, to
596         enable compression when the server is moved from the
597         client's site to another site.
598
599         The repsFrom.update_flags bit field may be modified
600         auto-magically if any changes are made here. See
601         kcc_utils.RepsFromTo for gory details.
602
603
604         :param n_rep: NC replica we need
605         :param t_repsFrom: repsFrom tuple to modify
606         :param s_rep: NC replica at source DSA
607         :param s_dsa: source DSA
608         :param cn_conn: Local DSA NTDSConnection child
609
610         :return: None
611         """
612         s_dnstr = s_dsa.dsa_dnstr
613         same_site = s_dnstr in self.my_site.dsa_table
614
615         # if schedule doesn't match then update and modify
616         times = convert_schedule_to_repltimes(cn_conn.schedule)
617         if times != t_repsFrom.schedule:
618             t_repsFrom.schedule = times
619
620         # Bit DRS_PER_SYNC is set in replicaFlags if and only
621         # if nTDSConnection schedule has a value v that specifies
622         # scheduled replication is to be performed at least once
623         # per week.
624         if cn_conn.is_schedule_minimum_once_per_week():
625
626             if ((t_repsFrom.replica_flags &
627                  drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0):
628                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
629
630         # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
631         # if the source DSA and the local DC's nTDSDSA object are
632         # in the same site or source dsa is the FSMO role owner
633         # of one or more FSMO roles in the NC replica.
634         if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
635
636             if ((t_repsFrom.replica_flags &
637                  drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0):
638                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
639
640         # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
641         # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
642         # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
643         # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
644         # t.replicaFlags if and only if s and the local DC's
645         # nTDSDSA object are in different sites.
646         if ((cn_conn.options &
647              dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0):
648
649             if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
650                 # WARNING
651                 #
652                 # it LOOKS as if this next test is a bit silly: it
653                 # checks the flag then sets it if it not set; the same
654                 # effect could be achieved by unconditionally setting
655                 # it. But in fact the repsFrom object has special
656                 # magic attached to it, and altering replica_flags has
657                 # side-effects. That is bad in my opinion, but there
658                 # you go.
659                 if ((t_repsFrom.replica_flags &
660                      drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
661                     t_repsFrom.replica_flags |= \
662                         drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
663
664         elif not same_site:
665
666             if ((t_repsFrom.replica_flags &
667                  drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
668                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
669
670         # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
671         # and only if s and the local DC's nTDSDSA object are
672         # not in the same site and the
673         # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
674         # clear in cn!options
675         if (not same_site and
676             (cn_conn.options &
677              dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
678
679             if ((t_repsFrom.replica_flags &
680                  drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0):
681                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
682
683         # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
684         # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
685         if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
686
687             if ((t_repsFrom.replica_flags &
688                  drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0):
689                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
690
691         # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
692         # set in t.replicaFlags if and only if cn!enabledConnection = false.
693         if not cn_conn.is_enabled():
694
695             if ((t_repsFrom.replica_flags &
696                  drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0):
697                 t_repsFrom.replica_flags |= \
698                     drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
699
700             if ((t_repsFrom.replica_flags &
701                  drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0):
702                 t_repsFrom.replica_flags |= \
703                     drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
704
705         # If s and the local DC's nTDSDSA object are in the same site,
706         # cn!transportType has no value, or the RDN of cn!transportType
707         # is CN=IP:
708         #
709         #     Bit DRS_MAIL_REP in t.replicaFlags is clear.
710         #
711         #     t.uuidTransport = NULL GUID.
712         #
713         #     t.uuidDsa = The GUID-based DNS name of s.
714         #
715         # Otherwise:
716         #
717         #     Bit DRS_MAIL_REP in t.replicaFlags is set.
718         #
719         #     If x is the object with dsname cn!transportType,
720         #     t.uuidTransport = x!objectGUID.
721         #
722         #     Let a be the attribute identified by
723         #     x!transportAddressAttribute. If a is
724         #     the dNSHostName attribute, t.uuidDsa = the GUID-based
725         #      DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
726         #
727         # It appears that the first statement i.e.
728         #
729         #     "If s and the local DC's nTDSDSA object are in the same
730         #      site, cn!transportType has no value, or the RDN of
731         #      cn!transportType is CN=IP:"
732         #
733         # could be a slightly tighter statement if it had an "or"
734         # between each condition.  I believe this should
735         # be interpreted as:
736         #
737         #     IF (same-site) OR (no-value) OR (type-ip)
738         #
739         # because IP should be the primary transport mechanism
740         # (even in inter-site) and the absense of the transportType
741         # attribute should always imply IP no matter if its multi-site
742         #
743         # NOTE MS-TECH INCORRECT:
744         #
745         #     All indications point to these statements above being
746         #     incorrectly stated:
747         #
748         #         t.uuidDsa = The GUID-based DNS name of s.
749         #
750         #         Let a be the attribute identified by
751         #         x!transportAddressAttribute. If a is
752         #         the dNSHostName attribute, t.uuidDsa = the GUID-based
753         #         DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
754         #
755         #     because the uuidDSA is a GUID and not a GUID-base DNS
756         #     name.  Nor can uuidDsa hold (s!parent)!a if not
757         #     dNSHostName.  What should have been said is:
758         #
759         #         t.naDsa = The GUID-based DNS name of s
760         #
761         #     That would also be correct if transportAddressAttribute
762         #     were "mailAddress" because (naDsa) can also correctly
763         #     hold the SMTP ISM service address.
764         #
765         nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
766
767         if ((t_repsFrom.replica_flags &
768              drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0):
769             t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
770
771         t_repsFrom.transport_guid = misc.GUID()
772
773         # See (NOTE MS-TECH INCORRECT) above
774
775         # NOTE: it looks like these conditionals are pointless,
776         # because the state will end up as `t_repsFrom.dns_name1 ==
777         # nastr` in either case, BUT the repsFrom thing is magic and
778         # assigning to it alters some flags. So we try not to update
779         # it unless necessary.
780         if t_repsFrom.dns_name1 != nastr:
781             t_repsFrom.dns_name1 = nastr
782
783         if t_repsFrom.version > 0x1 and t_repsFrom.dns_name2 != nastr:
784             t_repsFrom.dns_name2 = nastr
785
786         if t_repsFrom.is_modified():
787             DEBUG_FN("modify_repsFrom(): %s" % t_repsFrom)
788
789     def get_dsa_for_implied_replica(self, n_rep, cn_conn):
790         """If a connection imply a replica, find the relevant DSA
791
792         Given a NC replica and NTDS Connection, determine if the
793         connection implies a repsFrom tuple should be present from the
794         source DSA listed in the connection to the naming context. If
795         it should be, return the DSA; otherwise return None.
796
797         Based on part of MS-ADTS 6.2.2.5
798
799         :param n_rep: NC replica
800         :param cn_conn: NTDS Connection
801         :return: source DSA or None
802         """
803         # XXX different conditions for "implies" than MS-ADTS 6.2.2
804         # preamble.
805
806         # It boils down to: we want an enabled, non-FRS connections to
807         # a valid remote DSA with a non-RO replica corresponding to
808         # n_rep.
809
810         if not cn_conn.is_enabled() or cn_conn.is_rodc_topology():
811             return None
812
813         s_dnstr = cn_conn.get_from_dnstr()
814         s_dsa = self.get_dsa(s_dnstr)
815
816         # No DSA matching this source DN string?
817         if s_dsa is None:
818             return None
819
820         s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
821
822         if (s_rep is not None and
823             s_rep.is_present() and
824             (not s_rep.is_ro() or n_rep.is_partial())):
825             return s_dsa
826         return None
827
828
829     def translate_ntdsconn(self, current_dsa=None):
830         """Adjust repsFrom to match NTDSConnections
831
832         This function adjusts values of repsFrom abstract attributes of NC
833         replicas on the local DC to match those implied by
834         nTDSConnection objects.
835
836         Based on [MS-ADTS] 6.2.2.5
837
838         :param current_dsa: optional DSA on whose behalf we are acting.
839         :return: None
840         """
841         count = 0
842
843         if current_dsa is None:
844             current_dsa = self.my_dsa
845
846         if current_dsa.is_translate_ntdsconn_disabled():
847             DEBUG_FN("skipping translate_ntdsconn() "
848                      "because disabling flag is set")
849             return
850
851         DEBUG_FN("translate_ntdsconn(): enter")
852
853         current_rep_table, needed_rep_table = current_dsa.get_rep_tables()
854
855         # Filled in with replicas we currently have that need deleting
856         delete_reps = set()
857
858         # We're using the MS notation names here to allow
859         # correlation back to the published algorithm.
860         #
861         # n_rep      - NC replica (n)
862         # t_repsFrom - tuple (t) in n!repsFrom
863         # s_dsa      - Source DSA of the replica. Defined as nTDSDSA
864         #              object (s) such that (s!objectGUID = t.uuidDsa)
865         #              In our IDL representation of repsFrom the (uuidDsa)
866         #              attribute is called (source_dsa_obj_guid)
867         # cn_conn    - (cn) is nTDSConnection object and child of the local
868         #               DC's nTDSDSA object and (cn!fromServer = s)
869         # s_rep      - source DSA replica of n
870         #
871         # If we have the replica and its not needed
872         # then we add it to the "to be deleted" list.
873         for dnstr in current_rep_table:
874             if dnstr not in needed_rep_table:
875                 delete_reps.add(dnstr)
876
877         DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
878                  len(needed_rep_table), len(delete_reps)))
879
880         if delete_reps:
881             DEBUG('deleting these reps: %s' % delete_reps)
882             for dnstr in delete_reps:
883                 del current_rep_table[dnstr]
884
885         # Now perform the scan of replicas we'll need
886         # and compare any current repsFrom against the
887         # connections
888         for n_rep in needed_rep_table.values():
889
890             # load any repsFrom and fsmo roles as we'll
891             # need them during connection translation
892             n_rep.load_repsFrom(self.samdb)
893             n_rep.load_fsmo_roles(self.samdb)
894
895             # Loop thru the existing repsFrom tupples (if any)
896             # XXX This is a list and could contain duplicates
897             #     (multiple load_repsFrom calls)
898             for t_repsFrom in n_rep.rep_repsFrom:
899
900                 # for each tuple t in n!repsFrom, let s be the nTDSDSA
901                 # object such that s!objectGUID = t.uuidDsa
902                 guidstr = str(t_repsFrom.source_dsa_obj_guid)
903                 s_dsa = self.get_dsa_by_guidstr(guidstr)
904
905                 # Source dsa is gone from config (strange)
906                 # so cleanup stale repsFrom for unlisted DSA
907                 if s_dsa is None:
908                     logger.warning("repsFrom source DSA guid (%s) not found" %
909                                    guidstr)
910                     t_repsFrom.to_be_deleted = True
911                     continue
912
913                 # Find the connection that this repsFrom would use. If
914                 # there isn't a good one (i.e. non-RODC_TOPOLOGY,
915                 # meaning non-FRS), we delete the repsFrom.
916                 s_dnstr = s_dsa.dsa_dnstr
917                 connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
918                 for cn_conn in connections:
919                     if not cn_conn.is_rodc_topology():
920                         break
921                 else:
922                     # no break means no non-rodc_topology connection exists
923                     t_repsFrom.to_be_deleted = True
924                     continue
925
926                 # KCC removes this repsFrom tuple if any of the following
927                 # is true:
928                 #     No NC replica of the NC "is present" on DSA that
929                 #     would be source of replica
930                 #
931                 #     A writable replica of the NC "should be present" on
932                 #     the local DC, but a partial replica "is present" on
933                 #     the source DSA
934                 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
935
936                 if s_rep is None or not s_rep.is_present() or \
937                    (not n_rep.is_ro() and s_rep.is_partial()):
938
939                     t_repsFrom.to_be_deleted = True
940                     continue
941
942                 # If the KCC did not remove t from n!repsFrom, it updates t
943                 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
944
945             # Loop thru connections and add implied repsFrom tuples
946             # for each NTDSConnection under our local DSA if the
947             # repsFrom is not already present
948             for cn_conn in current_dsa.connect_table.values():
949
950                 s_dsa = self.get_dsa_for_implied_replica(n_rep, cn_conn)
951                 if s_dsa is None:
952                     continue
953
954                 # Loop thru the existing repsFrom tupples (if any) and
955                 # if we already have a tuple for this connection then
956                 # no need to proceed to add.  It will have been changed
957                 # to have the correct attributes above
958                 for t_repsFrom in n_rep.rep_repsFrom:
959                     guidstr = str(t_repsFrom.source_dsa_obj_guid)
960                     if s_dsa is self.get_dsa_by_guidstr(guidstr):
961                         s_dsa = None
962                         break
963
964                 if s_dsa is None:
965                     continue
966
967                 # Create a new RepsFromTo and proceed to modify
968                 # it according to specification
969                 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
970
971                 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
972
973                 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
974
975                 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
976
977                 # Add to our NC repsFrom as this is newly computed
978                 if t_repsFrom.is_modified():
979                     n_rep.rep_repsFrom.append(t_repsFrom)
980
981             if self.readonly:
982                 # Display any to be deleted or modified repsFrom
983                 text = n_rep.dumpstr_to_be_deleted()
984                 if text:
985                     logger.info("TO BE DELETED:\n%s" % text)
986                 text = n_rep.dumpstr_to_be_modified()
987                 if text:
988                     logger.info("TO BE MODIFIED:\n%s" % text)
989
990                 # Peform deletion from our tables but perform
991                 # no database modification
992                 n_rep.commit_repsFrom(self.samdb, ro=True)
993             else:
994                 # Commit any modified repsFrom to the NC replica
995                 n_rep.commit_repsFrom(self.samdb)
996
997     def merge_failed_links(self, ping=None):
998         """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
999
1000         The KCC on a writable DC attempts to merge the link and connection
1001         failure information from bridgehead DCs in its own site to help it
1002         identify failed bridgehead DCs.
1003
1004         Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1005         from Bridgeheads"
1006
1007         :param ping: An oracle of current bridgehead availability
1008         :return: None
1009         """
1010         # 1. Queries every bridgehead server in your site (other than yourself)
1011         # 2. For every ntDSConnection that references a server in a different
1012         #    site merge all the failure info
1013         #
1014         # XXX - not implemented yet
1015         if ping is not None:
1016             debug.DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1017         else:
1018             DEBUG_FN("skipping merge_failed_links() because it requires "
1019                      "real network connections\n"
1020                      "and we weren't asked to --attempt-live-connections")
1021
1022     def setup_graph(self, part):
1023         """Set up an intersite graph
1024
1025         An intersite graph has a Vertex for each site object, a
1026         MultiEdge for each SiteLink object, and a MutliEdgeSet for
1027         each siteLinkBridge object (or implied siteLinkBridge). It
1028         reflects the intersite topology in a slightly more abstract
1029         graph form.
1030
1031         Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1032
1033         :param part: a Partition object
1034         :returns: an InterSiteGraph object
1035         """
1036         # If 'Bridge all site links' is enabled and Win2k3 bridges required
1037         # is not set
1038         # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1039         # No documentation for this however, ntdsapi.h appears to have:
1040         # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1041         bridges_required = self.my_site.site_options & 0x00001002 != 0
1042         transport_guid = str(self.ip_transport.guid)
1043
1044         g = setup_graph(part, self.site_table, transport_guid,
1045                         self.sitelink_table, bridges_required)
1046
1047         if self.verify or self.dot_file_dir is not None:
1048             dot_edges = []
1049             for edge in g.edges:
1050                 for a, b in itertools.combinations(edge.vertices, 2):
1051                     dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1052             verify_properties = ()
1053             name = 'site_edges_%s' % part.partstr
1054             verify_and_dot(name, dot_edges, directed=False,
1055                            label=self.my_dsa_dnstr,
1056                            properties=verify_properties, debug=DEBUG,
1057                            verify=self.verify,
1058                            dot_file_dir=self.dot_file_dir)
1059
1060         return g
1061
1062     def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1063         """Get a bridghead DC for a site.
1064
1065         Part of MS-ADTS 6.2.2.3.4.4
1066
1067         :param site: site object representing for which a bridgehead
1068             DC is desired.
1069         :param part: crossRef for NC to replicate.
1070         :param transport: interSiteTransport object for replication
1071             traffic.
1072         :param partial_ok: True if a DC containing a partial
1073             replica or a full replica will suffice, False if only
1074             a full replica will suffice.
1075         :param detect_failed: True to detect failed DCs and route
1076             replication traffic around them, False to assume no DC
1077             has failed.
1078         :return: dsa object for the bridgehead DC or None
1079         """
1080
1081         bhs = self.get_all_bridgeheads(site, part, transport,
1082                                        partial_ok, detect_failed)
1083         if not bhs:
1084             debug.DEBUG_MAGENTA("get_bridgehead FAILED:\nsitedn = %s" %
1085                                 site.site_dnstr)
1086             return None
1087
1088         debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn = %s\n\tbhdn = %s" %
1089                           (site.site_dnstr, bhs[0].dsa_dnstr))
1090         return bhs[0]
1091
1092     def get_all_bridgeheads(self, site, part, transport,
1093                             partial_ok, detect_failed):
1094         """Get all bridghead DCs on a site satisfying the given criteria
1095
1096         Part of MS-ADTS 6.2.2.3.4.4
1097
1098         :param site: site object representing the site for which
1099             bridgehead DCs are desired.
1100         :param part: partition for NC to replicate.
1101         :param transport: interSiteTransport object for
1102             replication traffic.
1103         :param partial_ok: True if a DC containing a partial
1104             replica or a full replica will suffice, False if
1105             only a full replica will suffice.
1106         :param detect_failed: True to detect failed DCs and route
1107             replication traffic around them, FALSE to assume
1108             no DC has failed.
1109         :return: list of dsa object for available bridgehead DCs
1110         """
1111         bhs = []
1112
1113         if transport.name != "IP":
1114             raise KCCError("get_all_bridgeheads has run into a "
1115                            "non-IP transport! %r"
1116                            % (transport.name,))
1117
1118         DEBUG_FN(site.rw_dsa_table)
1119         for dsa in site.rw_dsa_table.values():
1120
1121             pdnstr = dsa.get_parent_dnstr()
1122
1123             # IF t!bridgeheadServerListBL has one or more values and
1124             # t!bridgeheadServerListBL does not contain a reference
1125             # to the parent object of dc then skip dc
1126             if ((len(transport.bridgehead_list) != 0 and
1127                  pdnstr not in transport.bridgehead_list)):
1128                 continue
1129
1130             # IF dc is in the same site as the local DC
1131             #    IF a replica of cr!nCName is not in the set of NC replicas
1132             #    that "should be present" on dc or a partial replica of the
1133             #    NC "should be present" but partialReplicasOkay = FALSE
1134             #        Skip dc
1135             if self.my_site.same_site(dsa):
1136                 needed, ro, partial = part.should_be_present(dsa)
1137                 if not needed or (partial and not partial_ok):
1138                     continue
1139                 rep = dsa.get_current_replica(part.nc_dnstr)
1140
1141             # ELSE
1142             #     IF an NC replica of cr!nCName is not in the set of NC
1143             #     replicas that "are present" on dc or a partial replica of
1144             #     the NC "is present" but partialReplicasOkay = FALSE
1145             #          Skip dc
1146             else:
1147                 rep = dsa.get_current_replica(part.nc_dnstr)
1148                 if rep is None or (rep.is_partial() and not partial_ok):
1149                     continue
1150
1151             # IF AmIRODC() and cr!nCName corresponds to default NC then
1152             #     Let dsaobj be the nTDSDSA object of the dc
1153             #     IF  dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1154             #         Skip dc
1155             if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1156                 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1157                     continue
1158
1159             # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1160             #     Skip dc
1161             if self.is_bridgehead_failed(dsa, detect_failed):
1162                 DEBUG("bridgehead is failed")
1163                 continue
1164
1165             DEBUG_FN("found a bridgehead: %s" % dsa.dsa_dnstr)
1166             bhs.append(dsa)
1167
1168         # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1169         # s!options
1170         #    SORT bhs such that all GC servers precede DCs that are not GC
1171         #    servers, and otherwise by ascending objectGUID
1172         # ELSE
1173         #    SORT bhs in a random order
1174         if site.is_random_bridgehead_disabled():
1175             bhs.sort(sort_dsa_by_gc_and_guid)
1176         else:
1177             random.shuffle(bhs)
1178         debug.DEBUG_YELLOW(bhs)
1179         return bhs
1180
1181     def is_bridgehead_failed(self, dsa, detect_failed):
1182         """Determine whether a given DC is known to be in a failed state
1183
1184         :param dsa: the bridgehead to test
1185         :param detect_failed: True to really check, False to assume no failure
1186         :return: True if and only if the DC should be considered failed
1187
1188         Here we DEPART from the pseudo code spec which appears to be
1189         wrong. It says, in full:
1190
1191     /***** BridgeheadDCFailed *****/
1192     /* Determine whether a given DC is known to be in a failed state.
1193      * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1194      * IN: detectFailedDCs - TRUE if and only failed DC detection is
1195      *     enabled.
1196      * RETURNS: TRUE if and only if the DC should be considered to be in a
1197      *          failed state.
1198      */
1199     BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1200     {
1201         IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1202         the options attribute of the site settings object for the local
1203         DC's site
1204             RETURN FALSE
1205         ELSEIF a tuple z exists in the kCCFailedLinks or
1206         kCCFailedConnections variables such that z.UUIDDsa =
1207         objectGUID, z.FailureCount > 1, and the current time -
1208         z.TimeFirstFailure > 2 hours
1209             RETURN TRUE
1210         ELSE
1211             RETURN detectFailedDCs
1212         ENDIF
1213     }
1214
1215         where you will see detectFailedDCs is not behaving as
1216         advertised -- it is acting as a default return code in the
1217         event that a failure is not detected, not a switch turning
1218         detection on or off. Elsewhere the documentation seems to
1219         concur with the comment rather than the code.
1220         """
1221         if not detect_failed:
1222             return False
1223
1224         # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1225         # When DETECT_STALE_DISABLED, we can never know of if
1226         # it's in a failed state
1227         if self.my_site.site_options & 0x00000008:
1228             return False
1229
1230         return self.is_stale_link_connection(dsa)
1231
1232     def create_connection(self, part, rbh, rsite, transport,
1233                           lbh, lsite, link_opt, link_sched,
1234                           partial_ok, detect_failed):
1235         """Create an nTDSConnection object as specified if it doesn't exist.
1236
1237         Part of MS-ADTS 6.2.2.3.4.5
1238
1239         :param part: crossRef object for the NC to replicate.
1240         :param rbh: nTDSDSA object for DC to act as the
1241             IDL_DRSGetNCChanges server (which is in a site other
1242             than the local DC's site).
1243         :param rsite: site of the rbh
1244         :param transport: interSiteTransport object for the transport
1245             to use for replication traffic.
1246         :param lbh: nTDSDSA object for DC to act as the
1247             IDL_DRSGetNCChanges client (which is in the local DC's site).
1248         :param lsite: site of the lbh
1249         :param link_opt: Replication parameters (aggregated siteLink options,
1250                                                  etc.)
1251         :param link_sched: Schedule specifying the times at which
1252             to begin replicating.
1253         :partial_ok: True if bridgehead DCs containing partial
1254             replicas of the NC are acceptable.
1255         :param detect_failed: True to detect failed DCs and route
1256             replication traffic around them, FALSE to assume no DC
1257             has failed.
1258         """
1259         rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1260                                             partial_ok, False)
1261         rbh_table = dict((x.dsa_dnstr, x) for x in rbhs_all)
1262
1263         debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1264                                               [x.dsa_dnstr for x in rbhs_all]))
1265
1266         # MS-TECH says to compute rbhs_avail but then doesn't use it
1267         # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1268         #                                        partial_ok, detect_failed)
1269
1270         lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1271                                             partial_ok, False)
1272         if lbh.is_ro():
1273             lbhs_all.append(lbh)
1274
1275         debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1276                                               [x.dsa_dnstr for x in lbhs_all]))
1277
1278         # MS-TECH says to compute lbhs_avail but then doesn't use it
1279         # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1280         #                                       partial_ok, detect_failed)
1281
1282         # FOR each nTDSConnection object cn such that the parent of cn is
1283         # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1284         for ldsa in lbhs_all:
1285             for cn in ldsa.connect_table.values():
1286
1287                 rdsa = rbh_table.get(cn.from_dnstr)
1288                 if rdsa is None:
1289                     continue
1290
1291                 debug.DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1292                 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1293                 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1294                 # cn!transportType references t
1295                 if ((cn.is_generated() and
1296                      not cn.is_rodc_topology() and
1297                      cn.transport_guid == transport.guid)):
1298
1299                     # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1300                     # cn!options and cn!schedule != sch
1301                     #     Perform an originating update to set cn!schedule to
1302                     #     sched
1303                     if ((not cn.is_user_owned_schedule() and
1304                          not cn.is_equivalent_schedule(link_sched))):
1305                         cn.schedule = link_sched
1306                         cn.set_modified(True)
1307
1308                     # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1309                     # NTDSCONN_OPT_USE_NOTIFY are set in cn
1310                     if cn.is_override_notify_default() and \
1311                        cn.is_use_notify():
1312
1313                         # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1314                         # ri.Options
1315                         #    Perform an originating update to clear bits
1316                         #    NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1317                         #    NTDSCONN_OPT_USE_NOTIFY in cn!options
1318                         if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1319                             cn.options &= \
1320                                 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1321                                   dsdb.NTDSCONN_OPT_USE_NOTIFY)
1322                             cn.set_modified(True)
1323
1324                     # ELSE
1325                     else:
1326
1327                         # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1328                         # ri.Options
1329                         #     Perform an originating update to set bits
1330                         #     NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1331                         #     NTDSCONN_OPT_USE_NOTIFY in cn!options
1332                         if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1333                             cn.options |= \
1334                                 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1335                                  dsdb.NTDSCONN_OPT_USE_NOTIFY)
1336                             cn.set_modified(True)
1337
1338                     # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1339                     if cn.is_twoway_sync():
1340
1341                         # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1342                         # ri.Options
1343                         #     Perform an originating update to clear bit
1344                         #     NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1345                         if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1346                             cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1347                             cn.set_modified(True)
1348
1349                     # ELSE
1350                     else:
1351
1352                         # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1353                         # ri.Options
1354                         #     Perform an originating update to set bit
1355                         #     NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1356                         if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1357                             cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1358                             cn.set_modified(True)
1359
1360                     # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1361                     # in cn!options
1362                     if cn.is_intersite_compression_disabled():
1363
1364                         # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1365                         # in ri.Options
1366                         #     Perform an originating update to clear bit
1367                         #     NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1368                         #     cn!options
1369                         if ((link_opt &
1370                              dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1371                             cn.options &= \
1372                                 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1373                             cn.set_modified(True)
1374
1375                     # ELSE
1376                     else:
1377                         # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1378                         # ri.Options
1379                         #     Perform an originating update to set bit
1380                         #     NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1381                         #     cn!options
1382                         if ((link_opt &
1383                              dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1384                             cn.options |= \
1385                                 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1386                             cn.set_modified(True)
1387
1388                     # Display any modified connection
1389                     if self.readonly:
1390                         if cn.to_be_modified:
1391                             logger.info("TO BE MODIFIED:\n%s" % cn)
1392
1393                         ldsa.commit_connections(self.samdb, ro=True)
1394                     else:
1395                         ldsa.commit_connections(self.samdb)
1396         # ENDFOR
1397
1398         valid_connections = 0
1399
1400         # FOR each nTDSConnection object cn such that cn!parent is
1401         # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1402         for ldsa in lbhs_all:
1403             for cn in ldsa.connect_table.values():
1404
1405                 rdsa = rbh_table.get(cn.from_dnstr)
1406                 if rdsa is None:
1407                     continue
1408
1409                 debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1410
1411                 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1412                 # cn!transportType references t) and
1413                 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1414                 if (((not cn.is_generated() or
1415                       cn.transport_guid == transport.guid) and
1416                      not cn.is_rodc_topology())):
1417
1418                     # LET rguid be the objectGUID of the nTDSDSA object
1419                     # referenced by cn!fromServer
1420                     # LET lguid be (cn!parent)!objectGUID
1421
1422                     # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1423                     # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1424                     #     Increment cValidConnections by 1
1425                     if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1426                          not self.is_bridgehead_failed(ldsa, detect_failed))):
1427                         valid_connections += 1
1428
1429                     # IF keepConnections does not contain cn!objectGUID
1430                     #     APPEND cn!objectGUID to keepConnections
1431                     self.kept_connections.add(cn)
1432
1433         # ENDFOR
1434         debug.DEBUG_RED("valid connections %d" % valid_connections)
1435         DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1436         # IF cValidConnections = 0
1437         if valid_connections == 0:
1438
1439             # LET opt be NTDSCONN_OPT_IS_GENERATED
1440             opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1441
1442             # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1443             #     SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1444             #     NTDSCONN_OPT_USE_NOTIFY in opt
1445             if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1446                 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1447                         dsdb.NTDSCONN_OPT_USE_NOTIFY)
1448
1449             # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1450             #     SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1451             if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1452                 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1453
1454             # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1455             # ri.Options
1456             #     SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1457             if ((link_opt &
1458                  dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1459                 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1460
1461             # Perform an originating update to create a new nTDSConnection
1462             # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1463             # cn!options = opt, cn!transportType is a reference to t,
1464             # cn!fromServer is a reference to rbh, and cn!schedule = sch
1465             DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1466             system_flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1467                             dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1468
1469             cn = lbh.new_connection(opt, system_flags, transport,
1470                                     rbh.dsa_dnstr, link_sched)
1471
1472             # Display any added connection
1473             if self.readonly:
1474                 if cn.to_be_added:
1475                     logger.info("TO BE ADDED:\n%s" % cn)
1476
1477                     lbh.commit_connections(self.samdb, ro=True)
1478             else:
1479                 lbh.commit_connections(self.samdb)
1480
1481             # APPEND cn!objectGUID to keepConnections
1482             self.kept_connections.add(cn)
1483
1484     def add_transports(self, vertex, local_vertex, graph, detect_failed):
1485         """Build a Vertex's transport lists
1486
1487         Each vertex has accept_red_red and accept_black lists that
1488         list what transports they accept under various conditions. The
1489         only transport that is ever accepted is IP, and a dummy extra
1490         transport called "EDGE_TYPE_ALL".
1491
1492         Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1493
1494         :param vertex: the remote vertex we are thinking about
1495         :param local_vertex: the vertex relating to the local site.
1496         :param graph: the intersite graph
1497         :param detect_failed: whether to detect failed links
1498         :return: True if some bridgeheads were not found
1499         """
1500         # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1501         # here, but using vertex seems to make more sense. That is,
1502         # the docs want this:
1503         #
1504         #bh = self.get_bridgehead(local_vertex.site, vertex.part, transport,
1505         #                         local_vertex.is_black(), detect_failed)
1506         #
1507         # TODO WHY?????
1508
1509         vertex.accept_red_red = []
1510         vertex.accept_black = []
1511         found_failed = False
1512
1513         if vertex in graph.connected_vertices:
1514             t_guid = str(self.ip_transport.guid)
1515
1516             bh = self.get_bridgehead(vertex.site, vertex.part,
1517                                      self.ip_transport,
1518                                      vertex.is_black(), detect_failed)
1519             if bh is None:
1520                 if vertex.site.is_rodc_site():
1521                     vertex.accept_red_red.append(t_guid)
1522                 else:
1523                     found_failed = True
1524             else:
1525                 vertex.accept_red_red.append(t_guid)
1526                 vertex.accept_black.append(t_guid)
1527
1528         # Add additional transport to ensure another run of Dijkstra
1529         vertex.accept_red_red.append("EDGE_TYPE_ALL")
1530         vertex.accept_black.append("EDGE_TYPE_ALL")
1531
1532         return found_failed
1533
1534     def create_connections(self, graph, part, detect_failed):
1535         """Create intersite NTDSConnections as needed by a partition
1536
1537         Construct an NC replica graph for the NC identified by
1538         the given crossRef, then create any additional nTDSConnection
1539         objects required.
1540
1541         :param graph: site graph.
1542         :param part: crossRef object for NC.
1543         :param detect_failed:  True to detect failed DCs and route
1544             replication traffic around them, False to assume no DC
1545             has failed.
1546
1547         Modifies self.kept_connections by adding any connections
1548         deemed to be "in use".
1549
1550         :return: (all_connected, found_failed_dc)
1551         (all_connected) True if the resulting NC replica graph
1552             connects all sites that need to be connected.
1553         (found_failed_dc) True if one or more failed DCs were
1554             detected.
1555         """
1556         all_connected = True
1557         found_failed = False
1558
1559         DEBUG_FN("create_connections(): enter\n"
1560                  "\tpartdn=%s\n\tdetect_failed=%s" %
1561                  (part.nc_dnstr, detect_failed))
1562
1563         # XXX - This is a highly abbreviated function from the MS-TECH
1564         #       ref.  It creates connections between bridgeheads to all
1565         #       sites that have appropriate replicas.  Thus we are not
1566         #       creating a minimum cost spanning tree but instead
1567         #       producing a fully connected tree.  This should produce
1568         #       a full (albeit not optimal cost) replication topology.
1569
1570         my_vertex = Vertex(self.my_site, part)
1571         my_vertex.color_vertex()
1572
1573         for v in graph.vertices:
1574             v.color_vertex()
1575             if self.add_transports(v, my_vertex, graph, detect_failed):
1576                 found_failed = True
1577
1578         # No NC replicas for this NC in the site of the local DC,
1579         # so no nTDSConnection objects need be created
1580         if my_vertex.is_white():
1581             return all_connected, found_failed
1582
1583         edge_list, n_components = get_spanning_tree_edges(graph,
1584                                                           self.my_site,
1585                                                           label=part.partstr)
1586
1587         DEBUG_FN("%s Number of components: %d" %
1588                  (part.nc_dnstr, n_components))
1589         if n_components > 1:
1590             all_connected = False
1591
1592         # LET partialReplicaOkay be TRUE if and only if
1593         # localSiteVertex.Color = COLOR.BLACK
1594         partial_ok = my_vertex.is_black()
1595
1596         # Utilize the IP transport only for now
1597         transport = self.ip_transport
1598
1599         DEBUG("edge_list %s" % edge_list)
1600         for e in edge_list:
1601             # XXX more accurate comparison?
1602             if e.directed and e.vertices[0].site is self.my_site:
1603                 continue
1604
1605             if e.vertices[0].site is self.my_site:
1606                 rsite = e.vertices[1].site
1607             else:
1608                 rsite = e.vertices[0].site
1609
1610             # We don't make connections to our own site as that
1611             # is intrasite topology generator's job
1612             if rsite is self.my_site:
1613                 DEBUG("rsite is my_site")
1614                 continue
1615
1616             # Determine bridgehead server in remote site
1617             rbh = self.get_bridgehead(rsite, part, transport,
1618                                       partial_ok, detect_failed)
1619             if rbh is None:
1620                 continue
1621
1622             # RODC acts as an BH for itself
1623             # IF AmIRODC() then
1624             #     LET lbh be the nTDSDSA object of the local DC
1625             # ELSE
1626             #     LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1627             #     cr, t, partialReplicaOkay, detectFailedDCs)
1628             if self.my_dsa.is_ro():
1629                 lsite = self.my_site
1630                 lbh = self.my_dsa
1631             else:
1632                 lsite = self.my_site
1633                 lbh = self.get_bridgehead(lsite, part, transport,
1634                                           partial_ok, detect_failed)
1635             # TODO
1636             if lbh is None:
1637                 debug.DEBUG_RED("DISASTER! lbh is None")
1638                 return False, True
1639
1640             DEBUG_FN("lsite: %s\nrsite: %s" % (lsite, rsite))
1641             DEBUG_FN("vertices %s" % (e.vertices,))
1642             debug.DEBUG_BLUE("bridgeheads\n%s\n%s\n%s" % (lbh, rbh, "-" * 70))
1643
1644             sitelink = e.site_link
1645             if sitelink is None:
1646                 link_opt = 0x0
1647                 link_sched = None
1648             else:
1649                 link_opt = sitelink.options
1650                 link_sched = sitelink.schedule
1651
1652             self.create_connection(part, rbh, rsite, transport,
1653                                    lbh, lsite, link_opt, link_sched,
1654                                    partial_ok, detect_failed)
1655
1656         return all_connected, found_failed
1657
1658     def create_intersite_connections(self):
1659         """Create NTDSConnections as necessary for all partitions.
1660
1661         Computes an NC replica graph for each NC replica that "should be
1662         present" on the local DC or "is present" on any DC in the same site
1663         as the local DC. For each edge directed to an NC replica on such a
1664         DC from an NC replica on a DC in another site, the KCC creates an
1665         nTDSConnection object to imply that edge if one does not already
1666         exist.
1667
1668         Modifies self.kept_connections - A set of nTDSConnection
1669         objects for edges that are directed
1670         to the local DC's site in one or more NC replica graphs.
1671
1672         :return: True if spanning trees were created for all NC replica
1673                  graphs, otherwise False.
1674         """
1675         all_connected = True
1676         self.kept_connections = set()
1677
1678         # LET crossRefList be the set containing each object o of class
1679         # crossRef such that o is a child of the CN=Partitions child of the
1680         # config NC
1681
1682         # FOR each crossRef object cr in crossRefList
1683         #    IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1684         #        is clear in cr!systemFlags, skip cr.
1685         #    LET g be the GRAPH return of SetupGraph()
1686
1687         for part in self.part_table.values():
1688
1689             if not part.is_enabled():
1690                 continue
1691
1692             if part.is_foreign():
1693                 continue
1694
1695             graph = self.setup_graph(part)
1696
1697             # Create nTDSConnection objects, routing replication traffic
1698             # around "failed" DCs.
1699             found_failed = False
1700
1701             connected, found_failed = self.create_connections(graph,
1702                                                               part, True)
1703
1704             DEBUG("with detect_failed: connected %s Found failed %s" %
1705                   (connected, found_failed))
1706             if not connected:
1707                 all_connected = False
1708
1709                 if found_failed:
1710                     # One or more failed DCs preclude use of the ideal NC
1711                     # replica graph. Add connections for the ideal graph.
1712                     self.create_connections(graph, part, False)
1713
1714         return all_connected
1715
1716     def intersite(self, ping):
1717         """Generate the inter-site KCC replica graph and nTDSConnections
1718
1719         As per MS-ADTS 6.2.2.3.
1720
1721         If self.readonly is False, the connections are added to self.samdb.
1722
1723         Produces self.kept_connections which is a set of NTDS
1724         Connections that should be kept during subsequent pruning
1725         process.
1726
1727         After this has run, all sites should be connected in a minimum
1728         spanning tree.
1729
1730         :param ping: An oracle function of remote site availability
1731         :return (True or False):  (True) if the produced NC replica
1732             graph connects all sites that need to be connected
1733         """
1734
1735         # Retrieve my DSA
1736         mydsa = self.my_dsa
1737         mysite = self.my_site
1738         all_connected = True
1739
1740         DEBUG_FN("intersite(): enter")
1741
1742         # Determine who is the ISTG
1743         if self.readonly:
1744             mysite.select_istg(self.samdb, mydsa, ro=True)
1745         else:
1746             mysite.select_istg(self.samdb, mydsa, ro=False)
1747
1748         # Test whether local site has topology disabled
1749         if mysite.is_intersite_topology_disabled():
1750             DEBUG_FN("intersite(): exit disabled all_connected=%d" %
1751                      all_connected)
1752             return all_connected
1753
1754         if not mydsa.is_istg():
1755             DEBUG_FN("intersite(): exit not istg all_connected=%d" %
1756                      all_connected)
1757             return all_connected
1758
1759         self.merge_failed_links(ping)
1760
1761         # For each NC with an NC replica that "should be present" on the
1762         # local DC or "is present" on any DC in the same site as the
1763         # local DC, the KCC constructs a site graph--a precursor to an NC
1764         # replica graph. The site connectivity for a site graph is defined
1765         # by objects of class interSiteTransport, siteLink, and
1766         # siteLinkBridge in the config NC.
1767
1768         all_connected = self.create_intersite_connections()
1769
1770         DEBUG_FN("intersite(): exit all_connected=%d" % all_connected)
1771         return all_connected
1772
1773     def update_rodc_connection(self):
1774         """Updates the RODC NTFRS connection object.
1775
1776         If the local DSA is not an RODC, this does nothing.
1777         """
1778         if not self.my_dsa.is_ro():
1779             return
1780
1781         # Given an nTDSConnection object cn1, such that cn1.options contains
1782         # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1783         # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1784         # that the following is true:
1785         #
1786         #     cn1.fromServer = cn2.fromServer
1787         #     cn1.schedule = cn2.schedule
1788         #
1789         # If no such cn2 can be found, cn1 is not modified.
1790         # If no such cn1 can be found, nothing is modified by this task.
1791
1792         all_connections = self.my_dsa.connect_table.values()
1793         ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1794         rw_connections = [x for x in all_connections
1795                           if x not in ro_connections]
1796
1797         # XXX here we are dealing with multiple RODC_TOPO connections,
1798         # if they exist. It is not clear whether the spec means that
1799         # or if it ever arises.
1800         if rw_connections and ro_connections:
1801             for con in ro_connections:
1802                 cn2 = rw_connections[0]
1803                 con.from_dnstr = cn2.from_dnstr
1804                 con.schedule = cn2.schedule
1805                 con.to_be_modified = True
1806
1807             self.my_dsa.commit_connections(self.samdb, ro=self.readonly)
1808
1809     def intrasite_max_node_edges(self, node_count):
1810         """Find the maximum number of edges directed to an intrasite node
1811
1812         The KCC does not create more than 50 edges directed to a
1813         single DC. To optimize replication, we compute that each node
1814         should have n+2 total edges directed to it such that (n) is
1815         the smallest non-negative integer satisfying
1816         (node_count <= 2*(n*n) + 6*n + 7)
1817
1818         (If the number of edges is m (i.e. n + 2), that is the same as
1819         2 * m*m - 2 * m + 3). We think in terms of n because that is
1820         the number of extra connections over the double directed ring
1821         that exists by default.
1822
1823         edges  n   nodecount
1824           2    0    7
1825           3    1   15
1826           4    2   27
1827           5    3   43
1828                   ...
1829          50   48 4903
1830
1831         :param node_count: total number of nodes in the replica graph
1832
1833         The intention is that there should be no more than 3 hops
1834         between any two DSAs at a site. With up to 7 nodes the 2 edges
1835         of the ring are enough; any configuration of extra edges with
1836         8 nodes will be enough. It is less clear that the 3 hop
1837         guarantee holds at e.g. 15 nodes in degenerate cases, but
1838         those are quite unlikely given the extra edges are randomly
1839         arranged.
1840
1841         :param node_count: the number of nodes in the site
1842         "return: The desired maximum number of connections
1843         """
1844         n = 0
1845         while True:
1846             if node_count <= (2 * (n * n) + (6 * n) + 7):
1847                 break
1848             n = n + 1
1849         n = n + 2
1850         if n < 50:
1851             return n
1852         return 50
1853
1854     def construct_intrasite_graph(self, site_local, dc_local,
1855                                   nc_x, gc_only, detect_stale):
1856         """Create an intrasite graph using given parameters
1857
1858         This might be called a number of times per site with different
1859         parameters.
1860
1861         Based on [MS-ADTS] 6.2.2.2
1862
1863         :param site_local: site for which we are working
1864         :param dc_local: local DC that potentially needs a replica
1865         :param nc_x:  naming context (x) that we are testing if it
1866                     "should be present" on the local DC
1867         :param gc_only: Boolean - only consider global catalog servers
1868         :param detect_stale: Boolean - check whether links seems down
1869         :return: None
1870         """
1871         # We're using the MS notation names here to allow
1872         # correlation back to the published algorithm.
1873         #
1874         # nc_x     - naming context (x) that we are testing if it
1875         #            "should be present" on the local DC
1876         # f_of_x   - replica (f) found on a DC (s) for NC (x)
1877         # dc_s     - DC where f_of_x replica was found
1878         # dc_local - local DC that potentially needs a replica
1879         #            (f_of_x)
1880         # r_list   - replica list R
1881         # p_of_x   - replica (p) is partial and found on a DC (s)
1882         #            for NC (x)
1883         # l_of_x   - replica (l) is the local replica for NC (x)
1884         #            that should appear on the local DC
1885         # r_len = is length of replica list |R|
1886         #
1887         # If the DSA doesn't need a replica for this
1888         # partition (NC x) then continue
1889         needed, ro, partial = nc_x.should_be_present(dc_local)
1890
1891         debug.DEBUG_YELLOW("construct_intrasite_graph(): enter" +
1892                            "\n\tgc_only=%d" % gc_only +
1893                            "\n\tdetect_stale=%d" % detect_stale +
1894                            "\n\tneeded=%s" % needed +
1895                            "\n\tro=%s" % ro +
1896                            "\n\tpartial=%s" % partial +
1897                            "\n%s" % nc_x)
1898
1899         if not needed:
1900             debug.DEBUG_RED("%s lacks 'should be present' status, "
1901                             "aborting construct_intersite_graph!" %
1902                             nc_x.nc_dnstr)
1903             return
1904
1905         # Create a NCReplica that matches what the local replica
1906         # should say.  We'll use this below in our r_list
1907         l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
1908                            nc_x.nc_dnstr)
1909
1910         l_of_x.identify_by_basedn(self.samdb)
1911
1912         l_of_x.rep_partial = partial
1913         l_of_x.rep_ro = ro
1914
1915         # Add this replica that "should be present" to the
1916         # needed replica table for this DSA
1917         dc_local.add_needed_replica(l_of_x)
1918
1919         # Replica list
1920         #
1921         # Let R be a sequence containing each writable replica f of x
1922         # such that f "is present" on a DC s satisfying the following
1923         # criteria:
1924         #
1925         #  * s is a writable DC other than the local DC.
1926         #
1927         #  * s is in the same site as the local DC.
1928         #
1929         #  * If x is a read-only full replica and x is a domain NC,
1930         #    then the DC's functional level is at least
1931         #    DS_BEHAVIOR_WIN2008.
1932         #
1933         #  * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
1934         #    in the options attribute of the site settings object for
1935         #    the local DC's site, or no tuple z exists in the
1936         #    kCCFailedLinks or kCCFailedConnections variables such
1937         #    that z.UUIDDsa is the objectGUID of the nTDSDSA object
1938         #    for s, z.FailureCount > 0, and the current time -
1939         #    z.TimeFirstFailure > 2 hours.
1940
1941         r_list = []
1942
1943         # We'll loop thru all the DSAs looking for
1944         # writeable NC replicas that match the naming
1945         # context dn for (nc_x)
1946         #
1947         for dc_s in self.my_site.dsa_table.values():
1948             # If this partition (nc_x) doesn't appear as a
1949             # replica (f_of_x) on (dc_s) then continue
1950             if not nc_x.nc_dnstr in dc_s.current_rep_table:
1951                 continue
1952
1953             # Pull out the NCReplica (f) of (x) with the dn
1954             # that matches NC (x) we are examining.
1955             f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
1956
1957             # Replica (f) of NC (x) must be writable
1958             if f_of_x.is_ro():
1959                 continue
1960
1961             # Replica (f) of NC (x) must satisfy the
1962             # "is present" criteria for DC (s) that
1963             # it was found on
1964             if not f_of_x.is_present():
1965                 continue
1966
1967             # DC (s) must be a writable DSA other than
1968             # my local DC.  In other words we'd only replicate
1969             # from other writable DC
1970             if dc_s.is_ro() or dc_s is dc_local:
1971                 continue
1972
1973             # Certain replica graphs are produced only
1974             # for global catalogs, so test against
1975             # method input parameter
1976             if gc_only and not dc_s.is_gc():
1977                 continue
1978
1979             # DC (s) must be in the same site as the local DC
1980             # as this is the intra-site algorithm. This is
1981             # handled by virtue of placing DSAs in per
1982             # site objects (see enclosing for() loop)
1983
1984             # If NC (x) is intended to be read-only full replica
1985             # for a domain NC on the target DC then the source
1986             # DC should have functional level at minimum WIN2008
1987             #
1988             # Effectively we're saying that in order to replicate
1989             # to a targeted RODC (which was introduced in Windows 2008)
1990             # then we have to replicate from a DC that is also minimally
1991             # at that level.
1992             #
1993             # You can also see this requirement in the MS special
1994             # considerations for RODC which state that to deploy
1995             # an RODC, at least one writable domain controller in
1996             # the domain must be running Windows Server 2008
1997             if ro and not partial and nc_x.nc_type == NCType.domain:
1998                 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1999                     continue
2000
2001             # If we haven't been told to turn off stale connection
2002             # detection and this dsa has a stale connection then
2003             # continue
2004             if detect_stale and self.is_stale_link_connection(dc_s):
2005                 continue
2006
2007             # Replica meets criteria.  Add it to table indexed
2008             # by the GUID of the DC that it appears on
2009             r_list.append(f_of_x)
2010
2011         # If a partial (not full) replica of NC (x) "should be present"
2012         # on the local DC, append to R each partial replica (p of x)
2013         # such that p "is present" on a DC satisfying the same
2014         # criteria defined above for full replica DCs.
2015         #
2016         # XXX This loop and the previous one differ only in whether
2017         # the replica is partial or not. here we only accept partial
2018         # (because we're partial); before we only accepted full. Order
2019         # doen't matter (the list is sorted a few lines down) so these
2020         # loops could easily be merged. Or this could be a helper
2021         # function.
2022
2023         if partial:
2024             # Now we loop thru all the DSAs looking for
2025             # partial NC replicas that match the naming
2026             # context dn for (NC x)
2027             for dc_s in self.my_site.dsa_table.values():
2028
2029                 # If this partition NC (x) doesn't appear as a
2030                 # replica (p) of NC (x) on the dsa DC (s) then
2031                 # continue
2032                 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2033                     continue
2034
2035                 # Pull out the NCReplica with the dn that
2036                 # matches NC (x) we are examining.
2037                 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2038
2039                 # Replica (p) of NC (x) must be partial
2040                 if not p_of_x.is_partial():
2041                     continue
2042
2043                 # Replica (p) of NC (x) must satisfy the
2044                 # "is present" criteria for DC (s) that
2045                 # it was found on
2046                 if not p_of_x.is_present():
2047                     continue
2048
2049                 # DC (s) must be a writable DSA other than
2050                 # my DSA.  In other words we'd only replicate
2051                 # from other writable DSA
2052                 if dc_s.is_ro() or dc_s is dc_local:
2053                     continue
2054
2055                 # Certain replica graphs are produced only
2056                 # for global catalogs, so test against
2057                 # method input parameter
2058                 if gc_only and not dc_s.is_gc():
2059                     continue
2060
2061                 # If we haven't been told to turn off stale connection
2062                 # detection and this dsa has a stale connection then
2063                 # continue
2064                 if detect_stale and self.is_stale_link_connection(dc_s):
2065                     continue
2066
2067                 # Replica meets criteria.  Add it to table indexed
2068                 # by the GUID of the DSA that it appears on
2069                 r_list.append(p_of_x)
2070
2071         # Append to R the NC replica that "should be present"
2072         # on the local DC
2073         r_list.append(l_of_x)
2074
2075         r_list.sort(sort_replica_by_dsa_guid)
2076         r_len = len(r_list)
2077
2078         max_node_edges = self.intrasite_max_node_edges(r_len)
2079
2080         # Add a node for each r_list element to the replica graph
2081         graph_list = []
2082         for rep in r_list:
2083             node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2084             graph_list.append(node)
2085
2086         # For each r(i) from (0 <= i < |R|-1)
2087         i = 0
2088         while i < (r_len-1):
2089             # Add an edge from r(i) to r(i+1) if r(i) is a full
2090             # replica or r(i+1) is a partial replica
2091             if not r_list[i].is_partial() or r_list[i+1].is_partial():
2092                 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2093
2094             # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2095             # replica or ri is a partial replica.
2096             if not r_list[i+1].is_partial() or r_list[i].is_partial():
2097                 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2098             i = i + 1
2099
2100         # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2101         # or r0 is a partial replica.
2102         if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2103             graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2104
2105         # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2106         # r|R|-1 is a partial replica.
2107         if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2108             graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2109
2110         DEBUG("r_list is length %s" % len(r_list))
2111         DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2112                         for x in r_list))
2113
2114         do_dot_files = self.dot_file_dir is not None and self.debug
2115         if self.verify or do_dot_files:
2116             dot_edges = []
2117             dot_vertices = set()
2118             for v1 in graph_list:
2119                 dot_vertices.add(v1.dsa_dnstr)
2120                 for v2 in v1.edge_from:
2121                     dot_edges.append((v2, v1.dsa_dnstr))
2122                     dot_vertices.add(v2)
2123
2124             verify_properties = ('connected',)
2125             verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2126                            label='%s__%s__%s' % (site_local.site_dnstr,
2127                                                  nctype_lut[nc_x.nc_type],
2128                                                  nc_x.nc_dnstr),
2129                            properties=verify_properties, debug=DEBUG,
2130                            verify=self.verify,
2131                            dot_file_dir=self.dot_file_dir,
2132                            directed=True)
2133
2134             rw_dot_vertices = set(x for x in dot_vertices
2135                                   if not self.get_dsa(x).is_ro())
2136             rw_dot_edges = [(a, b) for a, b in dot_edges if
2137                             a in rw_dot_vertices and b in rw_dot_vertices]
2138             print rw_dot_edges, rw_dot_vertices
2139             rw_verify_properties = ('connected',
2140                                     'directed_double_ring_or_small')
2141             verify_and_dot('intrasite_rw_pre_ntdscon', rw_dot_edges,
2142                            rw_dot_vertices,
2143                            label='%s__%s__%s' % (site_local.site_dnstr,
2144                                                  nctype_lut[nc_x.nc_type],
2145                                                  nc_x.nc_dnstr),
2146                            properties=rw_verify_properties, debug=DEBUG,
2147                            verify=self.verify,
2148                            dot_file_dir=self.dot_file_dir,
2149                            directed=True)
2150
2151         # For each existing nTDSConnection object implying an edge
2152         # from rj of R to ri such that j != i, an edge from rj to ri
2153         # is not already in the graph, and the total edges directed
2154         # to ri is less than n+2, the KCC adds that edge to the graph.
2155         for vertex in graph_list:
2156             dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2157             for connect in dsa.connect_table.values():
2158                 remote = connect.from_dnstr
2159                 if remote in self.my_site.dsa_table:
2160                     vertex.add_edge_from(remote)
2161
2162         DEBUG('reps are:  %s' % '   '.join(x.rep_dsa_dnstr for x in r_list))
2163         DEBUG('dsas are:  %s' % '   '.join(x.dsa_dnstr for x in graph_list))
2164
2165         for tnode in graph_list:
2166             # To optimize replication latency in sites with many NC
2167             # replicas, the KCC adds new edges directed to ri to bring
2168             # the total edges to n+2, where the NC replica rk of R
2169             # from which the edge is directed is chosen at random such
2170             # that k != i and an edge from rk to ri is not already in
2171             # the graph.
2172             #
2173             # Note that the KCC tech ref does not give a number for
2174             # the definition of "sites with many NC replicas". At a
2175             # bare minimum to satisfy n+2 edges directed at a node we
2176             # have to have at least three replicas in |R| (i.e. if n
2177             # is zero then at least replicas from two other graph
2178             # nodes may direct edges to us).
2179             if r_len >= 3 and not tnode.has_sufficient_edges():
2180                 candidates = [x for x in graph_list if
2181                               (x is not tnode and
2182                                x.dsa_dnstr not in tnode.edge_from)]
2183
2184                 debug.DEBUG_BLUE("looking for random link for %s. r_len %d, "
2185                                  "graph len %d candidates %d"
2186                                  % (tnode.dsa_dnstr, r_len, len(graph_list),
2187                                     len(candidates)))
2188
2189                 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2190
2191                 while candidates and not tnode.has_sufficient_edges():
2192                     other = random.choice(candidates)
2193                     DEBUG("trying to add candidate %s" % other.dsa_dstr)
2194                     if not tnode.add_edge_from(other):
2195                         debug.DEBUG_RED("could not add %s" % other.dsa_dstr)
2196                     candidates.remove(other)
2197             else:
2198                 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2199                          (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2200                           tnode.max_edges))
2201
2202             # Print the graph node in debug mode
2203             DEBUG_FN("%s" % tnode)
2204
2205             # For each edge directed to the local DC, ensure a nTDSConnection
2206             # points to us that satisfies the KCC criteria
2207
2208             if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2209                 tnode.add_connections_from_edges(dc_local, self.ip_transport)
2210
2211         if self.verify or do_dot_files:
2212             dot_edges = []
2213             dot_vertices = set()
2214             for v1 in graph_list:
2215                 dot_vertices.add(v1.dsa_dnstr)
2216                 for v2 in v1.edge_from:
2217                     dot_edges.append((v2, v1.dsa_dnstr))
2218                     dot_vertices.add(v2)
2219
2220             verify_properties = ('connected',)
2221             verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2222                            label='%s__%s__%s' % (site_local.site_dnstr,
2223                                                  nctype_lut[nc_x.nc_type],
2224                                                  nc_x.nc_dnstr),
2225                            properties=verify_properties, debug=DEBUG,
2226                            verify=self.verify,
2227                            dot_file_dir=self.dot_file_dir,
2228                            directed=True)
2229
2230             rw_dot_vertices = set(x for x in dot_vertices
2231                                   if not self.get_dsa(x).is_ro())
2232             rw_dot_edges = [(a, b) for a, b in dot_edges if
2233                             a in rw_dot_vertices and b in rw_dot_vertices]
2234             print rw_dot_edges, rw_dot_vertices
2235             rw_verify_properties = ('connected',
2236                                     'directed_double_ring_or_small')
2237             verify_and_dot('intrasite_rw_post_ntdscon', rw_dot_edges,
2238                            rw_dot_vertices,
2239                            label='%s__%s__%s' % (site_local.site_dnstr,
2240                                                  nctype_lut[nc_x.nc_type],
2241                                                  nc_x.nc_dnstr),
2242                            properties=rw_verify_properties, debug=DEBUG,
2243                            verify=self.verify,
2244                            dot_file_dir=self.dot_file_dir,
2245                            directed=True)
2246
2247     def intrasite(self):
2248         """Generate the intrasite KCC connections
2249
2250         As per MS-ADTS 6.2.2.2.
2251
2252         If self.readonly is False, the connections are added to self.samdb.
2253
2254         After this call, all DCs in each site with more than 3 DCs
2255         should be connected in a bidirectional ring. If a site has 2
2256         DCs, they will bidirectionally connected. Sites with many DCs
2257         may have arbitrary extra connections.
2258
2259         :return: None
2260         """
2261         mydsa = self.my_dsa
2262
2263         DEBUG_FN("intrasite(): enter")
2264
2265         # Test whether local site has topology disabled
2266         mysite = self.my_site
2267         if mysite.is_intrasite_topology_disabled():
2268             return
2269
2270         detect_stale = (not mysite.is_detect_stale_disabled())
2271         for connect in mydsa.connect_table.values():
2272             if connect.to_be_added:
2273                 debug.DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2274
2275         # Loop thru all the partitions, with gc_only False
2276         for partdn, part in self.part_table.items():
2277             self.construct_intrasite_graph(mysite, mydsa, part, False,
2278                                            detect_stale)
2279             for connect in mydsa.connect_table.values():
2280                 if connect.to_be_added:
2281                     debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2282
2283         # If the DC is a GC server, the KCC constructs an additional NC
2284         # replica graph (and creates nTDSConnection objects) for the
2285         # config NC as above, except that only NC replicas that "are present"
2286         # on GC servers are added to R.
2287         for connect in mydsa.connect_table.values():
2288             if connect.to_be_added:
2289                 debug.DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2290
2291         # Do it again, with gc_only True
2292         for partdn, part in self.part_table.items():
2293             if part.is_config():
2294                 self.construct_intrasite_graph(mysite, mydsa, part, True,
2295                                                detect_stale)
2296
2297         # The DC repeats the NC replica graph computation and nTDSConnection
2298         # creation for each of the NC replica graphs, this time assuming
2299         # that no DC has failed. It does so by re-executing the steps as
2300         # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2301         # set in the options attribute of the site settings object for
2302         # the local DC's site.  (ie. we set "detec_stale" flag to False)
2303         for connect in mydsa.connect_table.values():
2304             if connect.to_be_added:
2305                 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2306
2307         # Loop thru all the partitions.
2308         for partdn, part in self.part_table.items():
2309             self.construct_intrasite_graph(mysite, mydsa, part, False,
2310                                            False)  # don't detect stale
2311
2312         # If the DC is a GC server, the KCC constructs an additional NC
2313         # replica graph (and creates nTDSConnection objects) for the
2314         # config NC as above, except that only NC replicas that "are present"
2315         # on GC servers are added to R.
2316         for connect in mydsa.connect_table.values():
2317             if connect.to_be_added:
2318                 debug.DEBUG_RED("TO BE ADDED:\n%s" % connect)
2319
2320         for partdn, part in self.part_table.items():
2321             if part.is_config():
2322                 self.construct_intrasite_graph(mysite, mydsa, part, True,
2323                                                False)  # don't detect stale
2324
2325         self._commit_changes(mydsa)
2326
2327     def list_dsas(self):
2328         """Compile a comprehensive list of DSA DNs
2329
2330         These are all the DSAs on all the sites that KCC would be
2331         dealing with.
2332
2333         This method is not idempotent and may not work correctly in
2334         sequence with KCC.run().
2335
2336         :return: a list of DSA DN strings.
2337         """
2338         self.load_my_site()
2339         self.load_my_dsa()
2340
2341         self.load_all_sites()
2342         self.load_all_partitions()
2343         self.load_ip_transport()
2344         self.load_all_sitelinks()
2345         dsas = []
2346         for site in self.site_table.values():
2347             dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2348                          for dsa in site.dsa_table.values()])
2349         return dsas
2350
2351     def load_samdb(self, dburl, lp, creds):
2352         """Load the database using an url, loadparm, and credentials
2353
2354         :param dburl: a database url.
2355         :param lp: a loadparm object.
2356         :param creds: a Credentials object.
2357         """
2358         self.samdb = SamDB(url=dburl,
2359                            session_info=system_session(),
2360                            credentials=creds, lp=lp)
2361
2362     def plot_all_connections(self, basename, verify_properties=()):
2363         """Helper function to plot and verify NTDSConnections
2364
2365         :param basename: an identifying string to use in filenames and logs.
2366         :param verify_properties: properties to verify (default empty)
2367         """
2368         verify = verify_properties and self.verify
2369         if not verify and self.dot_file_dir is None:
2370             return
2371
2372         dot_edges = []
2373         dot_vertices = []
2374         edge_colours = []
2375         vertex_colours = []
2376
2377         for dsa in self.dsa_by_dnstr.values():
2378             dot_vertices.append(dsa.dsa_dnstr)
2379             if dsa.is_ro():
2380                 vertex_colours.append('#cc0000')
2381             else:
2382                 vertex_colours.append('#0000cc')
2383             for con in dsa.connect_table.values():
2384                 if con.is_rodc_topology():
2385                     edge_colours.append('red')
2386                 else:
2387                     edge_colours.append('blue')
2388                 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2389
2390         verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2391                        label=self.my_dsa_dnstr,
2392                        properties=verify_properties, debug=DEBUG,
2393                        verify=verify, dot_file_dir=self.dot_file_dir,
2394                        directed=True, edge_colors=edge_colours,
2395                        vertex_colors=vertex_colours)
2396
2397     def run(self, dburl, lp, creds, forced_local_dsa=None,
2398             forget_local_links=False, forget_intersite_links=False,
2399             attempt_live_connections=False):
2400         """Perform a KCC run, possibly updating repsFrom topology
2401
2402         :param dburl: url of the database to work with.
2403         :param lp: a loadparm object.
2404         :param creds: a Credentials object.
2405         :param forced_local_dsa: pretend to be on the DSA with this dn_str
2406         :param forget_local_links: calculate as if no connections existed
2407                (boolean, default False)
2408         :param forget_intersite_links: calculate with only intrasite connection
2409                (boolean, default False)
2410         :param attempt_live_connections: attempt to connect to remote DSAs to
2411                determine link availability (boolean, default False)
2412         :return: 1 on error, 0 otherwise
2413         """
2414         # We may already have a samdb setup if we are
2415         # currently importing an ldif for a test run
2416         if self.samdb is None:
2417             try:
2418                 self.load_samdb(dburl, lp, creds)
2419             except ldb.LdbError, (num, msg):
2420                 logger.error("Unable to open sam database %s : %s" %
2421                              (dburl, msg))
2422                 return 1
2423
2424         if forced_local_dsa:
2425             self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2426                                             forced_local_dsa)
2427
2428         try:
2429             # Setup
2430             self.load_my_site()
2431             self.load_my_dsa()
2432
2433             self.load_all_sites()
2434             self.load_all_partitions()
2435             self.load_ip_transport()
2436             self.load_all_sitelinks()
2437
2438             if self.verify or self.dot_file_dir is not None:
2439                 guid_to_dnstr = {}
2440                 for site in self.site_table.values():
2441                     guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2442                                          for dnstr, dsa
2443                                          in site.dsa_table.items())
2444
2445                 self.plot_all_connections('dsa_initial')
2446
2447                 dot_edges = []
2448                 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2449                 for dnstr, c_rep in current_reps.items():
2450                     DEBUG("c_rep %s" % c_rep)
2451                     dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2452
2453                 verify_and_dot('dsa_repsFrom_initial', dot_edges,
2454                                directed=True, label=self.my_dsa_dnstr,
2455                                properties=(), debug=DEBUG, verify=self.verify,
2456                                dot_file_dir=self.dot_file_dir)
2457
2458                 dot_edges = []
2459                 for site in self.site_table.values():
2460                     for dsa in site.dsa_table.values():
2461                         current_reps, needed_reps = dsa.get_rep_tables()
2462                         for dn_str, rep in current_reps.items():
2463                             for reps_from in rep.rep_repsFrom:
2464                                 DEBUG("rep %s" % rep)
2465                                 dsa_guid = str(reps_from.source_dsa_obj_guid)
2466                                 dsa_dn = guid_to_dnstr[dsa_guid]
2467                                 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2468
2469                 verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2470                                directed=True, label=self.my_dsa_dnstr,
2471                                properties=(), debug=DEBUG, verify=self.verify,
2472                                dot_file_dir=self.dot_file_dir)
2473
2474                 dot_edges = []
2475                 for link in self.sitelink_table.values():
2476                     for a, b in itertools.combinations(link.site_list, 2):
2477                         dot_edges.append((str(a), str(b)))
2478                 properties = ('connected',)
2479                 verify_and_dot('dsa_sitelink_initial', dot_edges,
2480                                directed=False,
2481                                label=self.my_dsa_dnstr, properties=properties,
2482                                debug=DEBUG, verify=self.verify,
2483                                dot_file_dir=self.dot_file_dir)
2484
2485             if forget_local_links:
2486                 for dsa in self.my_site.dsa_table.values():
2487                     dsa.connect_table = dict((k, v) for k, v in
2488                                              dsa.connect_table.items()
2489                                              if v.is_rodc_topology() or
2490                                              (v.from_dnstr not in
2491                                               self.my_site.dsa_table))
2492                 self.plot_all_connections('dsa_forgotten_local')
2493
2494             if forget_intersite_links:
2495                 for site in self.site_table.values():
2496                     for dsa in site.dsa_table.values():
2497                         dsa.connect_table = dict((k, v) for k, v in
2498                                                  dsa.connect_table.items()
2499                                                  if site is self.my_site and
2500                                                  v.is_rodc_topology())
2501
2502                 self.plot_all_connections('dsa_forgotten_all')
2503
2504             if attempt_live_connections:
2505                 # Encapsulates lp and creds in a function that
2506                 # attempts connections to remote DSAs.
2507                 def ping(self, dnsname):
2508                     try:
2509                         drs_utils.drsuapi_connect(dnsname, self.lp, self.creds)
2510                     except drs_utils.drsException:
2511                         return False
2512                     return True
2513             else:
2514                 ping = None
2515             # These are the published steps (in order) for the
2516             # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2517
2518             # Step 1
2519             self.refresh_failed_links_connections(ping)
2520
2521             # Step 2
2522             self.intrasite()
2523
2524             # Step 3
2525             all_connected = self.intersite(ping)
2526
2527             # Step 4
2528             self.remove_unneeded_ntdsconn(all_connected)
2529
2530             # Step 5
2531             self.translate_ntdsconn()
2532
2533             # Step 6
2534             self.remove_unneeded_failed_links_connections()
2535
2536             # Step 7
2537             self.update_rodc_connection()
2538
2539             if self.verify or self.dot_file_dir is not None:
2540                 self.plot_all_connections('dsa_final',
2541                                           ('connected',))
2542
2543                 debug.DEBUG_MAGENTA("there are %d dsa guids" %
2544                                     len(guid_to_dnstr))
2545
2546                 dot_edges = []
2547                 edge_colors = []
2548                 my_dnstr = self.my_dsa.dsa_dnstr
2549                 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2550                 for dnstr, n_rep in needed_reps.items():
2551                     for reps_from in n_rep.rep_repsFrom:
2552                         guid_str = str(reps_from.source_dsa_obj_guid)
2553                         dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2554                         edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2555
2556                 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True,
2557                                label=self.my_dsa_dnstr,
2558                                properties=(), debug=DEBUG, verify=self.verify,
2559                                dot_file_dir=self.dot_file_dir,
2560                                edge_colors=edge_colors)
2561
2562                 dot_edges = []
2563
2564                 for site in self.site_table.values():
2565                     for dsa in site.dsa_table.values():
2566                         current_reps, needed_reps = dsa.get_rep_tables()
2567                         for n_rep in needed_reps.values():
2568                             for reps_from in n_rep.rep_repsFrom:
2569                                 dsa_guid = str(reps_from.source_dsa_obj_guid)
2570                                 dsa_dn = guid_to_dnstr[dsa_guid]
2571                                 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2572
2573                 verify_and_dot('dsa_repsFrom_final_all', dot_edges,
2574                                directed=True, label=self.my_dsa_dnstr,
2575                                properties=(), debug=DEBUG, verify=self.verify,
2576                                dot_file_dir=self.dot_file_dir)
2577
2578         except:
2579             raise
2580
2581         return 0
2582
2583     def import_ldif(self, dburl, lp, creds, ldif_file, forced_local_dsa=None):
2584         """Import relevant objects and attributes from an LDIF file.
2585
2586         The point of this function is to allow a programmer/debugger to
2587         import an LDIF file with non-security relevent information that
2588         was previously extracted from a DC database.  The LDIF file is used
2589         to create a temporary abbreviated database.  The KCC algorithm can
2590         then run against this abbreviated database for debug or test
2591         verification that the topology generated is computationally the
2592         same between different OSes and algorithms.
2593
2594         :param dburl: path to the temporary abbreviated db to create
2595         :param lp: a loadparm object.
2596         :param cred: a Credentials object.
2597         :param ldif_file: path to the ldif file to import
2598         :param forced_local_dsa: perform KCC from this DSA's point of view
2599         :return: zero on success, 1 on error
2600         """
2601         try:
2602             self.samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif_file,
2603                                                           forced_local_dsa)
2604         except ldif_import_export.LdifError, e:
2605             logger.critical(e)
2606             return 1
2607         return 0
2608
2609     def export_ldif(self, dburl, lp, creds, ldif_file):
2610         """Save KCC relevant details to an ldif file
2611
2612         The point of this function is to allow a programmer/debugger to
2613         extract an LDIF file with non-security relevent information from
2614         a DC database.  The LDIF file can then be used to "import" via
2615         the import_ldif() function this file into a temporary abbreviated
2616         database.  The KCC algorithm can then run against this abbreviated
2617         database for debug or test verification that the topology generated
2618         is computationally the same between different OSes and algorithms.
2619
2620         :param dburl: LDAP database URL to extract info from
2621         :param lp: a loadparm object.
2622         :param cred: a Credentials object.
2623         :param ldif_file: output LDIF file name to create
2624         :return: zero on success, 1 on error
2625         """
2626         try:
2627             ldif_import_export.samdb_to_ldif_file(self.samdb, dburl, lp, creds,
2628                                                   ldif_file)
2629         except ldif_import_export.LdifError, e:
2630             logger.critical(e)
2631             return 1
2632         return 0