KCC: Share commit wrapper between forget_ntdsconn and intrasite
[samba.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_dsas_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
805         # NTDS Connection must satisfy all the following criteria
806         # to imply a repsFrom tuple is needed:
807         #
808         #    cn!enabledConnection = true.
809         #    cn!options does not contain NTDSCONN_OPT_RODC_TOPOLOGY.
810         #    cn!fromServer references an nTDSDSA object.
811
812         if not cn_conn.is_enabled() or cn_conn.is_rodc_topology():
813             return None
814
815         s_dnstr = cn_conn.get_from_dnstr()
816         s_dsa = self.get_dsa(s_dnstr)
817
818         # No DSA matching this source DN string?
819         if s_dsa is None:
820             return None
821
822         # To imply a repsFrom tuple is needed, each of these
823         # must be True:
824         #
825         #     An NC replica of the NC "is present" on the DC to
826         #     which the nTDSDSA object referenced by cn!fromServer
827         #     corresponds.
828         #
829         #     An NC replica of the NC "should be present" on
830         #     the local DC
831         s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
832
833         if s_rep is None or not s_rep.is_present():
834             return None
835
836         # To imply a repsFrom tuple is needed, each of these
837         # must be True:
838         #
839         #     The NC replica on the DC referenced by cn!fromServer is
840         #     a writable replica or the NC replica that "should be
841         #     present" on the local DC is a partial replica.
842         #
843         #     The NC is not a domain NC, the NC replica that
844         #     "should be present" on the local DC is a partial
845         #     replica, cn!transportType has no value, or
846         #     cn!transportType has an RDN of CN=IP.
847         #
848         implied = (not s_rep.is_ro() or n_rep.is_partial()) and \
849                   (not n_rep.is_domain() or
850                    n_rep.is_partial() or
851                    cn_conn.transport_dnstr is None or
852                    cn_conn.transport_dnstr.find("CN=IP") == 0)
853
854         if implied:
855             return s_dsa
856         return None
857
858     def translate_ntdsconn(self, current_dsa=None):
859         """Adjust repsFrom to match NTDSConnections
860
861         This function adjusts values of repsFrom abstract attributes of NC
862         replicas on the local DC to match those implied by
863         nTDSConnection objects.
864
865         Based on [MS-ADTS] 6.2.2.5
866
867         :param current_dsa: optional DSA on whose behalf we are acting.
868         :return: None
869         """
870         count = 0
871
872         if current_dsa is None:
873             current_dsa = self.my_dsa
874
875         if current_dsa.is_translate_ntdsconn_disabled():
876             DEBUG_FN("skipping translate_ntdsconn() "
877                      "because disabling flag is set")
878             return
879
880         DEBUG_FN("translate_ntdsconn(): enter")
881
882         current_rep_table, needed_rep_table = current_dsa.get_rep_tables()
883
884         # Filled in with replicas we currently have that need deleting
885         delete_reps = set()
886
887         # We're using the MS notation names here to allow
888         # correlation back to the published algorithm.
889         #
890         # n_rep      - NC replica (n)
891         # t_repsFrom - tuple (t) in n!repsFrom
892         # s_dsa      - Source DSA of the replica. Defined as nTDSDSA
893         #              object (s) such that (s!objectGUID = t.uuidDsa)
894         #              In our IDL representation of repsFrom the (uuidDsa)
895         #              attribute is called (source_dsa_obj_guid)
896         # cn_conn    - (cn) is nTDSConnection object and child of the local
897         #               DC's nTDSDSA object and (cn!fromServer = s)
898         # s_rep      - source DSA replica of n
899         #
900         # If we have the replica and its not needed
901         # then we add it to the "to be deleted" list.
902         for dnstr in current_rep_table:
903             if dnstr not in needed_rep_table:
904                 delete_reps.add(dnstr)
905
906         DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
907                  len(needed_rep_table), len(delete_reps)))
908
909         if delete_reps:
910             DEBUG('deleting these reps: %s' % delete_reps)
911             for dnstr in delete_reps:
912                 del current_rep_table[dnstr]
913
914         # Now perform the scan of replicas we'll need
915         # and compare any current repsFrom against the
916         # connections
917         for n_rep in needed_rep_table.values():
918
919             # load any repsFrom and fsmo roles as we'll
920             # need them during connection translation
921             n_rep.load_repsFrom(self.samdb)
922             n_rep.load_fsmo_roles(self.samdb)
923
924             # Loop thru the existing repsFrom tupples (if any)
925             # XXX This is a list and could contain duplicates
926             #     (multiple load_repsFrom calls)
927             for t_repsFrom in n_rep.rep_repsFrom:
928
929                 # for each tuple t in n!repsFrom, let s be the nTDSDSA
930                 # object such that s!objectGUID = t.uuidDsa
931                 guidstr = str(t_repsFrom.source_dsa_obj_guid)
932                 s_dsa = self.get_dsa_by_guidstr(guidstr)
933
934                 # Source dsa is gone from config (strange)
935                 # so cleanup stale repsFrom for unlisted DSA
936                 if s_dsa is None:
937                     logger.warning("repsFrom source DSA guid (%s) not found" %
938                                    guidstr)
939                     t_repsFrom.to_be_deleted = True
940                     continue
941
942                 # Find the connection that this repsFrom would use. If
943                 # there isn't a good one (i.e. non-RODC_TOPOLOGY,
944                 # meaning non-FRS), we delete the repsFrom.
945                 s_dnstr = s_dsa.dsa_dnstr
946                 connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
947                 for cn_conn in connections:
948                     if not cn_conn.is_rodc_topology():
949                         break
950                 else:
951                     # no break means no non-rodc_topology connection exists
952                     t_repsFrom.to_be_deleted = True
953                     continue
954
955                 # KCC removes this repsFrom tuple if any of the following
956                 # is true:
957                 #     No NC replica of the NC "is present" on DSA that
958                 #     would be source of replica
959                 #
960                 #     A writable replica of the NC "should be present" on
961                 #     the local DC, but a partial replica "is present" on
962                 #     the source DSA
963                 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
964
965                 if s_rep is None or not s_rep.is_present() or \
966                    (not n_rep.is_ro() and s_rep.is_partial()):
967
968                     t_repsFrom.to_be_deleted = True
969                     continue
970
971                 # If the KCC did not remove t from n!repsFrom, it updates t
972                 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
973
974             # Loop thru connections and add implied repsFrom tuples
975             # for each NTDSConnection under our local DSA if the
976             # repsFrom is not already present
977             for cn_conn in current_dsa.connect_table.values():
978
979                 s_dsa = self.get_dsa_for_implied_replica(n_rep, cn_conn)
980                 if s_dsa is None:
981                     continue
982
983                 # Loop thru the existing repsFrom tupples (if any) and
984                 # if we already have a tuple for this connection then
985                 # no need to proceed to add.  It will have been changed
986                 # to have the correct attributes above
987                 for t_repsFrom in n_rep.rep_repsFrom:
988                     guidstr = str(t_repsFrom.source_dsa_obj_guid)
989                     if s_dsa is self.get_dsa_by_guidstr(guidstr):
990                         s_dsa = None
991                         break
992
993                 if s_dsa is None:
994                     continue
995
996                 # Create a new RepsFromTo and proceed to modify
997                 # it according to specification
998                 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
999
1000                 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
1001
1002                 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1003
1004                 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1005
1006                 # Add to our NC repsFrom as this is newly computed
1007                 if t_repsFrom.is_modified():
1008                     n_rep.rep_repsFrom.append(t_repsFrom)
1009
1010             if self.readonly:
1011                 # Display any to be deleted or modified repsFrom
1012                 text = n_rep.dumpstr_to_be_deleted()
1013                 if text:
1014                     logger.info("TO BE DELETED:\n%s" % text)
1015                 text = n_rep.dumpstr_to_be_modified()
1016                 if text:
1017                     logger.info("TO BE MODIFIED:\n%s" % text)
1018
1019                 # Peform deletion from our tables but perform
1020                 # no database modification
1021                 n_rep.commit_repsFrom(self.samdb, ro=True)
1022             else:
1023                 # Commit any modified repsFrom to the NC replica
1024                 n_rep.commit_repsFrom(self.samdb)
1025
1026     def merge_failed_links(self, ping=None):
1027         """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1028
1029         The KCC on a writable DC attempts to merge the link and connection
1030         failure information from bridgehead DCs in its own site to help it
1031         identify failed bridgehead DCs.
1032
1033         Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1034         from Bridgeheads"
1035
1036         :param ping: An oracle of current bridgehead availability
1037         :return: None
1038         """
1039         # 1. Queries every bridgehead server in your site (other than yourself)
1040         # 2. For every ntDSConnection that references a server in a different
1041         #    site merge all the failure info
1042         #
1043         # XXX - not implemented yet
1044         if ping is not None:
1045             debug.DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1046         else:
1047             DEBUG_FN("skipping merge_failed_links() because it requires "
1048                      "real network connections\n"
1049                      "and we weren't asked to --attempt-live-connections")
1050
1051     def setup_graph(self, part):
1052         """Set up an intersite graph
1053
1054         An intersite graph has a Vertex for each site object, a
1055         MultiEdge for each SiteLink object, and a MutliEdgeSet for
1056         each siteLinkBridge object (or implied siteLinkBridge). It
1057         reflects the intersite topology in a slightly more abstract
1058         graph form.
1059
1060         Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1061
1062         :param part: a Partition object
1063         :returns: an InterSiteGraph object
1064         """
1065         # If 'Bridge all site links' is enabled and Win2k3 bridges required
1066         # is not set
1067         # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1068         # No documentation for this however, ntdsapi.h appears to have:
1069         # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1070         bridges_required = self.my_site.site_options & 0x00001002 != 0
1071         transport_guid = str(self.ip_transport.guid)
1072
1073         g = setup_graph(part, self.site_table, transport_guid,
1074                         self.sitelink_table, bridges_required)
1075
1076         if self.verify or self.dot_file_dir is not None:
1077             dot_edges = []
1078             for edge in g.edges:
1079                 for a, b in itertools.combinations(edge.vertices, 2):
1080                     dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1081             verify_properties = ()
1082             name = 'site_edges_%s' % part.partstr
1083             verify_and_dot(name, dot_edges, directed=False,
1084                            label=self.my_dsa_dnstr,
1085                            properties=verify_properties, debug=DEBUG,
1086                            verify=self.verify,
1087                            dot_file_dir=self.dot_file_dir)
1088
1089         return g
1090
1091     def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1092         """Get a bridghead DC for a site.
1093
1094         Part of MS-ADTS 6.2.2.3.4.4
1095
1096         :param site: site object representing for which a bridgehead
1097             DC is desired.
1098         :param part: crossRef for NC to replicate.
1099         :param transport: interSiteTransport object for replication
1100             traffic.
1101         :param partial_ok: True if a DC containing a partial
1102             replica or a full replica will suffice, False if only
1103             a full replica will suffice.
1104         :param detect_failed: True to detect failed DCs and route
1105             replication traffic around them, False to assume no DC
1106             has failed.
1107         :return: dsa object for the bridgehead DC or None
1108         """
1109
1110         bhs = self.get_all_bridgeheads(site, part, transport,
1111                                        partial_ok, detect_failed)
1112         if len(bhs) == 0:
1113             debug.DEBUG_MAGENTA("get_bridgehead:\n\tsitedn=%s\n\tbhdn=None" %
1114                                 site.site_dnstr)
1115             return None
1116         else:
1117             debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn=%s\n\tbhdn=%s" %
1118                               (site.site_dnstr, bhs[0].dsa_dnstr))
1119             return bhs[0]
1120
1121     def get_all_bridgeheads(self, site, part, transport,
1122                             partial_ok, detect_failed):
1123         """Get all bridghead DCs on a site satisfying the given criteria
1124
1125         Part of MS-ADTS 6.2.2.3.4.4
1126
1127         :param site: site object representing the site for which
1128             bridgehead DCs are desired.
1129         :param part: partition for NC to replicate.
1130         :param transport: interSiteTransport object for
1131             replication traffic.
1132         :param partial_ok: True if a DC containing a partial
1133             replica or a full replica will suffice, False if
1134             only a full replica will suffice.
1135         :param detect_failed: True to detect failed DCs and route
1136             replication traffic around them, FALSE to assume
1137             no DC has failed.
1138         :return: list of dsa object for available bridgehead DCs
1139         """
1140         bhs = []
1141
1142         if transport.name != "IP":
1143             raise KCCError("get_all_bridgeheads has run into a "
1144                            "non-IP transport! %r"
1145                            % (transport.name,))
1146
1147         DEBUG_FN("get_all_bridgeheads")
1148         DEBUG_FN(site.rw_dsa_table)
1149         for dsa in site.rw_dsa_table.values():
1150
1151             pdnstr = dsa.get_parent_dnstr()
1152
1153             # IF t!bridgeheadServerListBL has one or more values and
1154             # t!bridgeheadServerListBL does not contain a reference
1155             # to the parent object of dc then skip dc
1156             if ((len(transport.bridgehead_list) != 0 and
1157                  pdnstr not in transport.bridgehead_list)):
1158                 continue
1159
1160             # IF dc is in the same site as the local DC
1161             #    IF a replica of cr!nCName is not in the set of NC replicas
1162             #    that "should be present" on dc or a partial replica of the
1163             #    NC "should be present" but partialReplicasOkay = FALSE
1164             #        Skip dc
1165             if self.my_site.same_site(dsa):
1166                 needed, ro, partial = part.should_be_present(dsa)
1167                 if not needed or (partial and not partial_ok):
1168                     continue
1169                 rep = dsa.get_current_replica(part.nc_dnstr)
1170
1171             # ELSE
1172             #     IF an NC replica of cr!nCName is not in the set of NC
1173             #     replicas that "are present" on dc or a partial replica of
1174             #     the NC "is present" but partialReplicasOkay = FALSE
1175             #          Skip dc
1176             else:
1177                 rep = dsa.get_current_replica(part.nc_dnstr)
1178                 if rep is None or (rep.is_partial() and not partial_ok):
1179                     continue
1180
1181             # IF AmIRODC() and cr!nCName corresponds to default NC then
1182             #     Let dsaobj be the nTDSDSA object of the dc
1183             #     IF  dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1184             #         Skip dc
1185             if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1186                 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1187                     continue
1188
1189             # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1190             #     Skip dc
1191             if self.is_bridgehead_failed(dsa, detect_failed):
1192                 DEBUG("bridgehead is failed")
1193                 continue
1194
1195             DEBUG_FN("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
1196             bhs.append(dsa)
1197
1198         # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1199         # s!options
1200         #    SORT bhs such that all GC servers precede DCs that are not GC
1201         #    servers, and otherwise by ascending objectGUID
1202         # ELSE
1203         #    SORT bhs in a random order
1204         if site.is_random_bridgehead_disabled():
1205             bhs.sort(sort_dsa_by_gc_and_guid)
1206         else:
1207             random.shuffle(bhs)
1208         debug.DEBUG_YELLOW(bhs)
1209         return bhs
1210
1211     def is_bridgehead_failed(self, dsa, detect_failed):
1212         """Determine whether a given DC is known to be in a failed state
1213
1214         :param dsa: the bridgehead to test
1215         :param detect_failed: True to really check, False to assume no failure
1216         :return: True if and only if the DC should be considered failed
1217
1218         Here we DEPART from the pseudo code spec which appears to be
1219         wrong. It says, in full:
1220
1221     /***** BridgeheadDCFailed *****/
1222     /* Determine whether a given DC is known to be in a failed state.
1223      * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1224      * IN: detectFailedDCs - TRUE if and only failed DC detection is
1225      *     enabled.
1226      * RETURNS: TRUE if and only if the DC should be considered to be in a
1227      *          failed state.
1228      */
1229     BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1230     {
1231         IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1232         the options attribute of the site settings object for the local
1233         DC's site
1234             RETURN FALSE
1235         ELSEIF a tuple z exists in the kCCFailedLinks or
1236         kCCFailedConnections variables such that z.UUIDDsa =
1237         objectGUID, z.FailureCount > 1, and the current time -
1238         z.TimeFirstFailure > 2 hours
1239             RETURN TRUE
1240         ELSE
1241             RETURN detectFailedDCs
1242         ENDIF
1243     }
1244
1245         where you will see detectFailedDCs is not behaving as
1246         advertised -- it is acting as a default return code in the
1247         event that a failure is not detected, not a switch turning
1248         detection on or off. Elsewhere the documentation seems to
1249         concur with the comment rather than the code.
1250         """
1251         if not detect_failed:
1252             return False
1253
1254         # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1255         # When DETECT_STALE_DISABLED, we can never know of if
1256         # it's in a failed state
1257         if self.my_site.site_options & 0x00000008:
1258             return False
1259
1260         return self.is_stale_link_connection(dsa)
1261
1262     def create_connection(self, part, rbh, rsite, transport,
1263                           lbh, lsite, link_opt, link_sched,
1264                           partial_ok, detect_failed):
1265         """Create an nTDSConnection object as specified if it doesn't exist.
1266
1267         Part of MS-ADTS 6.2.2.3.4.5
1268
1269         :param part: crossRef object for the NC to replicate.
1270         :param rbh: nTDSDSA object for DC to act as the
1271             IDL_DRSGetNCChanges server (which is in a site other
1272             than the local DC's site).
1273         :param rsite: site of the rbh
1274         :param transport: interSiteTransport object for the transport
1275             to use for replication traffic.
1276         :param lbh: nTDSDSA object for DC to act as the
1277             IDL_DRSGetNCChanges client (which is in the local DC's site).
1278         :param lsite: site of the lbh
1279         :param link_opt: Replication parameters (aggregated siteLink options,
1280                                                  etc.)
1281         :param link_sched: Schedule specifying the times at which
1282             to begin replicating.
1283         :partial_ok: True if bridgehead DCs containing partial
1284             replicas of the NC are acceptable.
1285         :param detect_failed: True to detect failed DCs and route
1286             replication traffic around them, FALSE to assume no DC
1287             has failed.
1288         """
1289         rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1290                                             partial_ok, False)
1291         rbh_table = dict((x.dsa_dnstr, x) for x in rbhs_all)
1292
1293         debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1294                                               [x.dsa_dnstr for x in rbhs_all]))
1295
1296         # MS-TECH says to compute rbhs_avail but then doesn't use it
1297         # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1298         #                                        partial_ok, detect_failed)
1299
1300         lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1301                                             partial_ok, False)
1302         if lbh.is_ro():
1303             lbhs_all.append(lbh)
1304
1305         debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1306                                               [x.dsa_dnstr for x in lbhs_all]))
1307
1308         # MS-TECH says to compute lbhs_avail but then doesn't use it
1309         # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1310         #                                       partial_ok, detect_failed)
1311
1312         # FOR each nTDSConnection object cn such that the parent of cn is
1313         # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1314         for ldsa in lbhs_all:
1315             for cn in ldsa.connect_table.values():
1316
1317                 rdsa = rbh_table.get(cn.from_dnstr)
1318                 if rdsa is None:
1319                     continue
1320
1321                 debug.DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1322                 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1323                 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1324                 # cn!transportType references t
1325                 if ((cn.is_generated() and
1326                      not cn.is_rodc_topology() and
1327                      cn.transport_guid == transport.guid)):
1328
1329                     # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1330                     # cn!options and cn!schedule != sch
1331                     #     Perform an originating update to set cn!schedule to
1332                     #     sched
1333                     if ((not cn.is_user_owned_schedule() and
1334                          not cn.is_equivalent_schedule(link_sched))):
1335                         cn.schedule = link_sched
1336                         cn.set_modified(True)
1337
1338                     # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1339                     # NTDSCONN_OPT_USE_NOTIFY are set in cn
1340                     if cn.is_override_notify_default() and \
1341                        cn.is_use_notify():
1342
1343                         # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1344                         # ri.Options
1345                         #    Perform an originating update to clear bits
1346                         #    NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1347                         #    NTDSCONN_OPT_USE_NOTIFY in cn!options
1348                         if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1349                             cn.options &= \
1350                                 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1351                                   dsdb.NTDSCONN_OPT_USE_NOTIFY)
1352                             cn.set_modified(True)
1353
1354                     # ELSE
1355                     else:
1356
1357                         # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1358                         # ri.Options
1359                         #     Perform an originating update to set bits
1360                         #     NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1361                         #     NTDSCONN_OPT_USE_NOTIFY in cn!options
1362                         if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1363                             cn.options |= \
1364                                 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1365                                  dsdb.NTDSCONN_OPT_USE_NOTIFY)
1366                             cn.set_modified(True)
1367
1368                     # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1369                     if cn.is_twoway_sync():
1370
1371                         # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1372                         # ri.Options
1373                         #     Perform an originating update to clear bit
1374                         #     NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1375                         if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1376                             cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1377                             cn.set_modified(True)
1378
1379                     # ELSE
1380                     else:
1381
1382                         # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1383                         # ri.Options
1384                         #     Perform an originating update to set bit
1385                         #     NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1386                         if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1387                             cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1388                             cn.set_modified(True)
1389
1390                     # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1391                     # in cn!options
1392                     if cn.is_intersite_compression_disabled():
1393
1394                         # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1395                         # in ri.Options
1396                         #     Perform an originating update to clear bit
1397                         #     NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1398                         #     cn!options
1399                         if ((link_opt &
1400                              dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1401                             cn.options &= \
1402                                 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1403                             cn.set_modified(True)
1404
1405                     # ELSE
1406                     else:
1407                         # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1408                         # ri.Options
1409                         #     Perform an originating update to set bit
1410                         #     NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1411                         #     cn!options
1412                         if ((link_opt &
1413                              dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1414                             cn.options |= \
1415                                 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1416                             cn.set_modified(True)
1417
1418                     # Display any modified connection
1419                     if self.readonly:
1420                         if cn.to_be_modified:
1421                             logger.info("TO BE MODIFIED:\n%s" % cn)
1422
1423                         ldsa.commit_connections(self.samdb, ro=True)
1424                     else:
1425                         ldsa.commit_connections(self.samdb)
1426         # ENDFOR
1427
1428         valid_connections = 0
1429
1430         # FOR each nTDSConnection object cn such that cn!parent is
1431         # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1432         for ldsa in lbhs_all:
1433             for cn in ldsa.connect_table.values():
1434
1435                 rdsa = rbh_table.get(cn.from_dnstr)
1436                 if rdsa is None:
1437                     continue
1438
1439                 debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1440
1441                 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1442                 # cn!transportType references t) and
1443                 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1444                 if (((not cn.is_generated() or
1445                       cn.transport_guid == transport.guid) and
1446                      not cn.is_rodc_topology())):
1447
1448                     # LET rguid be the objectGUID of the nTDSDSA object
1449                     # referenced by cn!fromServer
1450                     # LET lguid be (cn!parent)!objectGUID
1451
1452                     # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1453                     # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1454                     #     Increment cValidConnections by 1
1455                     if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1456                          not self.is_bridgehead_failed(ldsa, detect_failed))):
1457                         valid_connections += 1
1458
1459                     # IF keepConnections does not contain cn!objectGUID
1460                     #     APPEND cn!objectGUID to keepConnections
1461                     self.kept_connections.add(cn)
1462
1463         # ENDFOR
1464         debug.DEBUG_RED("valid connections %d" % valid_connections)
1465         DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1466         # IF cValidConnections = 0
1467         if valid_connections == 0:
1468
1469             # LET opt be NTDSCONN_OPT_IS_GENERATED
1470             opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1471
1472             # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1473             #     SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1474             #     NTDSCONN_OPT_USE_NOTIFY in opt
1475             if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1476                 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1477                         dsdb.NTDSCONN_OPT_USE_NOTIFY)
1478
1479             # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1480             #     SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1481             if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1482                 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1483
1484             # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1485             # ri.Options
1486             #     SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1487             if ((link_opt &
1488                  dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1489                 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1490
1491             # Perform an originating update to create a new nTDSConnection
1492             # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1493             # cn!options = opt, cn!transportType is a reference to t,
1494             # cn!fromServer is a reference to rbh, and cn!schedule = sch
1495             DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1496             cn = lbh.new_connection(opt, 0, transport,
1497                                     rbh.dsa_dnstr, link_sched)
1498
1499             # Display any added connection
1500             if self.readonly:
1501                 if cn.to_be_added:
1502                     logger.info("TO BE ADDED:\n%s" % cn)
1503
1504                     lbh.commit_connections(self.samdb, ro=True)
1505             else:
1506                 lbh.commit_connections(self.samdb)
1507
1508             # APPEND cn!objectGUID to keepConnections
1509             self.kept_connections.add(cn)
1510
1511     def add_transports(self, vertex, local_vertex, graph, detect_failed):
1512         """Build a Vertex's transport lists
1513
1514         Each vertex has accept_red_red and accept_black lists that
1515         list what transports they accept under various conditions. The
1516         only transport that is ever accepted is IP, and a dummy extra
1517         transport called "EDGE_TYPE_ALL".
1518
1519         Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1520
1521         :param vertex: the remote vertex we are thinking about
1522         :param local_vertex: the vertex relating to the local site.
1523         :param graph: the intersite graph
1524         :param detect_failed: whether to detect failed links
1525         :return: True if some bridgeheads were not found
1526         """
1527         # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1528         # here, but using vertex seems to make more sense. That is,
1529         # the docs want this:
1530         #
1531         #bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1532         #                         local_vertex.is_black(), detect_failed)
1533         #
1534         # TODO WHY?????
1535
1536         vertex.accept_red_red = []
1537         vertex.accept_black = []
1538         found_failed = False
1539
1540         if vertex in graph.connected_vertices:
1541             t_guid = str(self.ip_transport.guid)
1542
1543             bh = self.get_bridgehead(vertex.site, vertex.part,
1544                                      self.ip_transport,
1545                                      vertex.is_black(), detect_failed)
1546             if bh is None:
1547                 if vertex.site.is_rodc_site():
1548                     vertex.accept_red_red.append(t_guid)
1549                 else:
1550                     found_failed = True
1551             else:
1552                 vertex.accept_red_red.append(t_guid)
1553                 vertex.accept_black.append(t_guid)
1554
1555         # Add additional transport to ensure another run of Dijkstra
1556         vertex.accept_red_red.append("EDGE_TYPE_ALL")
1557         vertex.accept_black.append("EDGE_TYPE_ALL")
1558
1559         return found_failed
1560
1561     def create_connections(self, graph, part, detect_failed):
1562         """Create intersite NTDSConnections as needed by a partition
1563
1564         Construct an NC replica graph for the NC identified by
1565         the given crossRef, then create any additional nTDSConnection
1566         objects required.
1567
1568         :param graph: site graph.
1569         :param part: crossRef object for NC.
1570         :param detect_failed:  True to detect failed DCs and route
1571             replication traffic around them, False to assume no DC
1572             has failed.
1573
1574         Modifies self.kept_connections by adding any connections
1575         deemed to be "in use".
1576
1577         :return: (all_connected, found_failed_dc)
1578         (all_connected) True if the resulting NC replica graph
1579             connects all sites that need to be connected.
1580         (found_failed_dc) True if one or more failed DCs were
1581             detected.
1582         """
1583         all_connected = True
1584         found_failed = False
1585
1586         DEBUG_FN("create_connections(): enter\n"
1587                  "\tpartdn=%s\n\tdetect_failed=%s" %
1588                  (part.nc_dnstr, detect_failed))
1589
1590         # XXX - This is a highly abbreviated function from the MS-TECH
1591         #       ref.  It creates connections between bridgeheads to all
1592         #       sites that have appropriate replicas.  Thus we are not
1593         #       creating a minimum cost spanning tree but instead
1594         #       producing a fully connected tree.  This should produce
1595         #       a full (albeit not optimal cost) replication topology.
1596
1597         my_vertex = Vertex(self.my_site, part)
1598         my_vertex.color_vertex()
1599
1600         for v in graph.vertices:
1601             v.color_vertex()
1602             if self.add_transports(v, my_vertex, graph, False):
1603                 found_failed = True
1604
1605         # No NC replicas for this NC in the site of the local DC,
1606         # so no nTDSConnection objects need be created
1607         if my_vertex.is_white():
1608             return all_connected, found_failed
1609
1610         edge_list, n_components = get_spanning_tree_edges(graph,
1611                                                           self.my_site,
1612                                                           label=part.partstr)
1613
1614         DEBUG_FN("%s Number of components: %d" %
1615                  (part.nc_dnstr, n_components))
1616         if n_components > 1:
1617             all_connected = False
1618
1619         # LET partialReplicaOkay be TRUE if and only if
1620         # localSiteVertex.Color = COLOR.BLACK
1621         partial_ok = my_vertex.is_black()
1622
1623         # Utilize the IP transport only for now
1624         transport = self.ip_transport
1625
1626         DEBUG("edge_list %s" % edge_list)
1627         for e in edge_list:
1628             # XXX more accurate comparison?
1629             if e.directed and e.vertices[0].site is self.my_site:
1630                 continue
1631
1632             if e.vertices[0].site is self.my_site:
1633                 rsite = e.vertices[1].site
1634             else:
1635                 rsite = e.vertices[0].site
1636
1637             # We don't make connections to our own site as that
1638             # is intrasite topology generator's job
1639             if rsite is self.my_site:
1640                 DEBUG("rsite is my_site")
1641                 continue
1642
1643             # Determine bridgehead server in remote site
1644             rbh = self.get_bridgehead(rsite, part, transport,
1645                                       partial_ok, detect_failed)
1646             if rbh is None:
1647                 continue
1648
1649             # RODC acts as an BH for itself
1650             # IF AmIRODC() then
1651             #     LET lbh be the nTDSDSA object of the local DC
1652             # ELSE
1653             #     LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1654             #     cr, t, partialReplicaOkay, detectFailedDCs)
1655             if self.my_dsa.is_ro():
1656                 lsite = self.my_site
1657                 lbh = self.my_dsa
1658             else:
1659                 lsite = self.my_site
1660                 lbh = self.get_bridgehead(lsite, part, transport,
1661                                           partial_ok, detect_failed)
1662             # TODO
1663             if lbh is None:
1664                 debug.DEBUG_RED("DISASTER! lbh is None")
1665                 return False, True
1666
1667             DEBUG_FN("lsite: %s\nrsite: %s" % (lsite, rsite))
1668             DEBUG_FN("vertices %s" % (e.vertices,))
1669             debug.DEBUG_BLUE("bridgeheads\n%s\n%s\n%s" % (lbh, rbh, "-" * 70))
1670
1671             sitelink = e.site_link
1672             if sitelink is None:
1673                 link_opt = 0x0
1674                 link_sched = None
1675             else:
1676                 link_opt = sitelink.options
1677                 link_sched = sitelink.schedule
1678
1679             self.create_connection(part, rbh, rsite, transport,
1680                                    lbh, lsite, link_opt, link_sched,
1681                                    partial_ok, detect_failed)
1682
1683         return all_connected, found_failed
1684
1685     def create_intersite_connections(self):
1686         """Create NTDSConnections as necessary for all partitions.
1687
1688         Computes an NC replica graph for each NC replica that "should be
1689         present" on the local DC or "is present" on any DC in the same site
1690         as the local DC. For each edge directed to an NC replica on such a
1691         DC from an NC replica on a DC in another site, the KCC creates an
1692         nTDSConnection object to imply that edge if one does not already
1693         exist.
1694
1695         Modifies self.kept_connections - A set of nTDSConnection
1696         objects for edges that are directed
1697         to the local DC's site in one or more NC replica graphs.
1698
1699         :return: True if spanning trees were created for all NC replica
1700                  graphs, otherwise False.
1701         """
1702         all_connected = True
1703         self.kept_connections = set()
1704
1705         # LET crossRefList be the set containing each object o of class
1706         # crossRef such that o is a child of the CN=Partitions child of the
1707         # config NC
1708
1709         # FOR each crossRef object cr in crossRefList
1710         #    IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1711         #        is clear in cr!systemFlags, skip cr.
1712         #    LET g be the GRAPH return of SetupGraph()
1713
1714         for part in self.part_table.values():
1715
1716             if not part.is_enabled():
1717                 continue
1718
1719             if part.is_foreign():
1720                 continue
1721
1722             graph = self.setup_graph(part)
1723
1724             # Create nTDSConnection objects, routing replication traffic
1725             # around "failed" DCs.
1726             found_failed = False
1727
1728             connected, found_failed = self.create_connections(graph,
1729                                                               part, True)
1730
1731             DEBUG("with detect_failed: connected %s Found failed %s" %
1732                   (connected, found_failed))
1733             if not connected:
1734                 all_connected = False
1735
1736                 if found_failed:
1737                     # One or more failed DCs preclude use of the ideal NC
1738                     # replica graph. Add connections for the ideal graph.
1739                     self.create_connections(graph, part, False)
1740
1741         return all_connected
1742
1743     def intersite(self, ping):
1744         """Generate the inter-site KCC replica graph and nTDSConnections
1745
1746         As per MS-ADTS 6.2.2.3.
1747
1748         If self.readonly is False, the connections are added to self.samdb.
1749
1750         Produces self.kept_connections which is a set of NTDS
1751         Connections that should be kept during subsequent pruning
1752         process.
1753
1754         After this has run, all sites should be connected in a minimum
1755         spanning tree.
1756
1757         :param ping: An oracle function of remote site availability
1758         :return (True or False):  (True) if the produced NC replica
1759             graph connects all sites that need to be connected
1760         """
1761
1762         # Retrieve my DSA
1763         mydsa = self.my_dsa
1764         mysite = self.my_site
1765         all_connected = True
1766
1767         DEBUG_FN("intersite(): enter")
1768
1769         # Determine who is the ISTG
1770         if self.readonly:
1771             mysite.select_istg(self.samdb, mydsa, ro=True)
1772         else:
1773             mysite.select_istg(self.samdb, mydsa, ro=False)
1774
1775         # Test whether local site has topology disabled
1776         if mysite.is_intersite_topology_disabled():
1777             DEBUG_FN("intersite(): exit disabled all_connected=%d" %
1778                      all_connected)
1779             return all_connected
1780
1781         if not mydsa.is_istg():
1782             DEBUG_FN("intersite(): exit not istg all_connected=%d" %
1783                      all_connected)
1784             return all_connected
1785
1786         self.merge_failed_links(ping)
1787
1788         # For each NC with an NC replica that "should be present" on the
1789         # local DC or "is present" on any DC in the same site as the
1790         # local DC, the KCC constructs a site graph--a precursor to an NC
1791         # replica graph. The site connectivity for a site graph is defined
1792         # by objects of class interSiteTransport, siteLink, and
1793         # siteLinkBridge in the config NC.
1794
1795         all_connected = self.create_intersite_connections()
1796
1797         DEBUG_FN("intersite(): exit all_connected=%d" % all_connected)
1798         return all_connected
1799
1800     def update_rodc_connection(self):
1801         """Updates the RODC NTFRS connection object.
1802
1803         If the local DSA is not an RODC, this does nothing.
1804         """
1805         if not self.my_dsa.is_ro():
1806             return
1807
1808         # Given an nTDSConnection object cn1, such that cn1.options contains
1809         # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1810         # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1811         # that the following is true:
1812         #
1813         #     cn1.fromServer = cn2.fromServer
1814         #     cn1.schedule = cn2.schedule
1815         #
1816         # If no such cn2 can be found, cn1 is not modified.
1817         # If no such cn1 can be found, nothing is modified by this task.
1818
1819         all_connections = self.my_dsa.connect_table.values()
1820         ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1821         rw_connections = [x for x in all_connections
1822                           if x not in ro_connections]
1823
1824         # XXX here we are dealing with multiple RODC_TOPO connections,
1825         # if they exist. It is not clear whether the spec means that
1826         # or if it ever arises.
1827         if rw_connections and ro_connections:
1828             for con in ro_connections:
1829                 cn2 = rw_connections[0]
1830                 con.from_dnstr = cn2.from_dnstr
1831                 con.schedule = cn2.schedule
1832                 con.to_be_modified = True
1833
1834             self.my_dsa.commit_connections(self.samdb, ro=self.readonly)
1835
1836     def intrasite_max_node_edges(self, node_count):
1837         """Find the maximum number of edges directed to an intrasite node
1838
1839         The KCC does not create more than 50 edges directed to a
1840         single DC. To optimize replication, we compute that each node
1841         should have n+2 total edges directed to it such that (n) is
1842         the smallest non-negative integer satisfying
1843         (node_count <= 2*(n*n) + 6*n + 7)
1844
1845         (If the number of edges is m (i.e. n + 2), that is the same as
1846         2 * m*m - 2 * m + 3). We think in terms of n because that is
1847         the number of extra connections over the double directed ring
1848         that exists by default.
1849
1850         edges  n   nodecount
1851           2    0    7
1852           3    1   15
1853           4    2   27
1854           5    3   43
1855                   ...
1856          50   48 4903
1857
1858         :param node_count: total number of nodes in the replica graph
1859
1860         The intention is that there should be no more than 3 hops
1861         between any two DSAs at a site. With up to 7 nodes the 2 edges
1862         of the ring are enough; any configuration of extra edges with
1863         8 nodes will be enough. It is less clear that the 3 hop
1864         guarantee holds at e.g. 15 nodes in degenerate cases, but
1865         those are quite unlikely given the extra edges are randomly
1866         arranged.
1867
1868         :param node_count: the number of nodes in the site
1869         "return: The desired maximum number of connections
1870         """
1871         n = 0
1872         while True:
1873             if node_count <= (2 * (n * n) + (6 * n) + 7):
1874                 break
1875             n = n + 1
1876         n = n + 2
1877         if n < 50:
1878             return n
1879         return 50
1880
1881     def construct_intrasite_graph(self, site_local, dc_local,
1882                                   nc_x, gc_only, detect_stale):
1883         """Create an intrasite graph using given parameters
1884
1885         This might be called a number of times per site with different
1886         parameters.
1887
1888         Based on [MS-ADTS] 6.2.2.2
1889
1890         :param site_local: site for which we are working
1891         :param dc_local: local DC that potentially needs a replica
1892         :param nc_x:  naming context (x) that we are testing if it
1893                     "should be present" on the local DC
1894         :param gc_only: Boolean - only consider global catalog servers
1895         :param detect_stale: Boolean - check whether links seems down
1896         :return: None
1897         """
1898         # We're using the MS notation names here to allow
1899         # correlation back to the published algorithm.
1900         #
1901         # nc_x     - naming context (x) that we are testing if it
1902         #            "should be present" on the local DC
1903         # f_of_x   - replica (f) found on a DC (s) for NC (x)
1904         # dc_s     - DC where f_of_x replica was found
1905         # dc_local - local DC that potentially needs a replica
1906         #            (f_of_x)
1907         # r_list   - replica list R
1908         # p_of_x   - replica (p) is partial and found on a DC (s)
1909         #            for NC (x)
1910         # l_of_x   - replica (l) is the local replica for NC (x)
1911         #            that should appear on the local DC
1912         # r_len = is length of replica list |R|
1913         #
1914         # If the DSA doesn't need a replica for this
1915         # partition (NC x) then continue
1916         needed, ro, partial = nc_x.should_be_present(dc_local)
1917
1918         debug.DEBUG_YELLOW("construct_intrasite_graph(): enter" +
1919                            "\n\tgc_only=%d" % gc_only +
1920                            "\n\tdetect_stale=%d" % detect_stale +
1921                            "\n\tneeded=%s" % needed +
1922                            "\n\tro=%s" % ro +
1923                            "\n\tpartial=%s" % partial +
1924                            "\n%s" % nc_x)
1925
1926         if not needed:
1927             debug.DEBUG_RED("%s lacks 'should be present' status, "
1928                             "aborting construct_intersite_graph!" %
1929                             nc_x.nc_dnstr)
1930             return
1931
1932         # Create a NCReplica that matches what the local replica
1933         # should say.  We'll use this below in our r_list
1934         l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
1935                            nc_x.nc_dnstr)
1936
1937         l_of_x.identify_by_basedn(self.samdb)
1938
1939         l_of_x.rep_partial = partial
1940         l_of_x.rep_ro = ro
1941
1942         # Add this replica that "should be present" to the
1943         # needed replica table for this DSA
1944         dc_local.add_needed_replica(l_of_x)
1945
1946         # Replica list
1947         #
1948         # Let R be a sequence containing each writable replica f of x
1949         # such that f "is present" on a DC s satisfying the following
1950         # criteria:
1951         #
1952         #  * s is a writable DC other than the local DC.
1953         #
1954         #  * s is in the same site as the local DC.
1955         #
1956         #  * If x is a read-only full replica and x is a domain NC,
1957         #    then the DC's functional level is at least
1958         #    DS_BEHAVIOR_WIN2008.
1959         #
1960         #  * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
1961         #    in the options attribute of the site settings object for
1962         #    the local DC's site, or no tuple z exists in the
1963         #    kCCFailedLinks or kCCFailedConnections variables such
1964         #    that z.UUIDDsa is the objectGUID of the nTDSDSA object
1965         #    for s, z.FailureCount > 0, and the current time -
1966         #    z.TimeFirstFailure > 2 hours.
1967
1968         r_list = []
1969
1970         # We'll loop thru all the DSAs looking for
1971         # writeable NC replicas that match the naming
1972         # context dn for (nc_x)
1973         #
1974         for dc_s in self.my_site.dsa_table.values():
1975             # If this partition (nc_x) doesn't appear as a
1976             # replica (f_of_x) on (dc_s) then continue
1977             if not nc_x.nc_dnstr in dc_s.current_rep_table:
1978                 continue
1979
1980             # Pull out the NCReplica (f) of (x) with the dn
1981             # that matches NC (x) we are examining.
1982             f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
1983
1984             # Replica (f) of NC (x) must be writable
1985             if f_of_x.is_ro():
1986                 continue
1987
1988             # Replica (f) of NC (x) must satisfy the
1989             # "is present" criteria for DC (s) that
1990             # it was found on
1991             if not f_of_x.is_present():
1992                 continue
1993
1994             # DC (s) must be a writable DSA other than
1995             # my local DC.  In other words we'd only replicate
1996             # from other writable DC
1997             if dc_s.is_ro() or dc_s is dc_local:
1998                 continue
1999
2000             # Certain replica graphs are produced only
2001             # for global catalogs, so test against
2002             # method input parameter
2003             if gc_only and not dc_s.is_gc():
2004                 continue
2005
2006             # DC (s) must be in the same site as the local DC
2007             # as this is the intra-site algorithm. This is
2008             # handled by virtue of placing DSAs in per
2009             # site objects (see enclosing for() loop)
2010
2011             # If NC (x) is intended to be read-only full replica
2012             # for a domain NC on the target DC then the source
2013             # DC should have functional level at minimum WIN2008
2014             #
2015             # Effectively we're saying that in order to replicate
2016             # to a targeted RODC (which was introduced in Windows 2008)
2017             # then we have to replicate from a DC that is also minimally
2018             # at that level.
2019             #
2020             # You can also see this requirement in the MS special
2021             # considerations for RODC which state that to deploy
2022             # an RODC, at least one writable domain controller in
2023             # the domain must be running Windows Server 2008
2024             if ro and not partial and nc_x.nc_type == NCType.domain:
2025                 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2026                     continue
2027
2028             # If we haven't been told to turn off stale connection
2029             # detection and this dsa has a stale connection then
2030             # continue
2031             if detect_stale and self.is_stale_link_connection(dc_s):
2032                 continue
2033
2034             # Replica meets criteria.  Add it to table indexed
2035             # by the GUID of the DC that it appears on
2036             r_list.append(f_of_x)
2037
2038         # If a partial (not full) replica of NC (x) "should be present"
2039         # on the local DC, append to R each partial replica (p of x)
2040         # such that p "is present" on a DC satisfying the same
2041         # criteria defined above for full replica DCs.
2042         #
2043         # XXX This loop and the previous one differ only in whether
2044         # the replica is partial or not. here we only accept partial
2045         # (because we're partial); before we only accepted full. Order
2046         # doen't matter (the list is sorted a few lines down) so these
2047         # loops could easily be merged. Or this could be a helper
2048         # function.
2049
2050         if partial:
2051             # Now we loop thru all the DSAs looking for
2052             # partial NC replicas that match the naming
2053             # context dn for (NC x)
2054             for dc_s in self.my_site.dsa_table.values():
2055
2056                 # If this partition NC (x) doesn't appear as a
2057                 # replica (p) of NC (x) on the dsa DC (s) then
2058                 # continue
2059                 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2060                     continue
2061
2062                 # Pull out the NCReplica with the dn that
2063                 # matches NC (x) we are examining.
2064                 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2065
2066                 # Replica (p) of NC (x) must be partial
2067                 if not p_of_x.is_partial():
2068                     continue
2069
2070                 # Replica (p) of NC (x) must satisfy the
2071                 # "is present" criteria for DC (s) that
2072                 # it was found on
2073                 if not p_of_x.is_present():
2074                     continue
2075
2076                 # DC (s) must be a writable DSA other than
2077                 # my DSA.  In other words we'd only replicate
2078                 # from other writable DSA
2079                 if dc_s.is_ro() or dc_s is dc_local:
2080                     continue
2081
2082                 # Certain replica graphs are produced only
2083                 # for global catalogs, so test against
2084                 # method input parameter
2085                 if gc_only and not dc_s.is_gc():
2086                     continue
2087
2088                 # If we haven't been told to turn off stale connection
2089                 # detection and this dsa has a stale connection then
2090                 # continue
2091                 if detect_stale and self.is_stale_link_connection(dc_s):
2092                     continue
2093
2094                 # Replica meets criteria.  Add it to table indexed
2095                 # by the GUID of the DSA that it appears on
2096                 r_list.append(p_of_x)
2097
2098         # Append to R the NC replica that "should be present"
2099         # on the local DC
2100         r_list.append(l_of_x)
2101
2102         r_list.sort(sort_replica_by_dsa_guid)
2103         r_len = len(r_list)
2104
2105         max_node_edges = self.intrasite_max_node_edges(r_len)
2106
2107         # Add a node for each r_list element to the replica graph
2108         graph_list = []
2109         for rep in r_list:
2110             node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2111             graph_list.append(node)
2112
2113         # For each r(i) from (0 <= i < |R|-1)
2114         i = 0
2115         while i < (r_len-1):
2116             # Add an edge from r(i) to r(i+1) if r(i) is a full
2117             # replica or r(i+1) is a partial replica
2118             if not r_list[i].is_partial() or r_list[i+1].is_partial():
2119                 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2120
2121             # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2122             # replica or ri is a partial replica.
2123             if not r_list[i+1].is_partial() or r_list[i].is_partial():
2124                 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2125             i = i + 1
2126
2127         # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2128         # or r0 is a partial replica.
2129         if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2130             graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2131
2132         # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2133         # r|R|-1 is a partial replica.
2134         if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2135             graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2136
2137         DEBUG("r_list is length %s" % len(r_list))
2138         DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2139                         for x in r_list))
2140
2141         do_dot_files = self.dot_file_dir is not None and self.debug
2142         if self.verify or do_dot_files:
2143             dot_edges = []
2144             dot_vertices = set()
2145             for v1 in graph_list:
2146                 dot_vertices.add(v1.dsa_dnstr)
2147                 for v2 in v1.edge_from:
2148                     dot_edges.append((v2, v1.dsa_dnstr))
2149                     dot_vertices.add(v2)
2150
2151             verify_properties = ('connected',)
2152             verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2153                            label='%s__%s__%s' % (site_local.site_dnstr,
2154                                                  nctype_lut[nc_x.nc_type],
2155                                                  nc_x.nc_dnstr),
2156                            properties=verify_properties, debug=DEBUG,
2157                            verify=self.verify,
2158                            dot_file_dir=self.dot_file_dir,
2159                            directed=True)
2160
2161             rw_dot_vertices = set(x for x in dot_vertices
2162                                   if not self.get_dsa(x).is_ro())
2163             rw_dot_edges = [(a, b) for a, b in dot_edges if
2164                             a in rw_dot_vertices and b in rw_dot_vertices]
2165             print rw_dot_edges, rw_dot_vertices
2166             rw_verify_properties = ('connected',
2167                                     'directed_double_ring_or_small')
2168             verify_and_dot('intrasite_rw_pre_ntdscon', rw_dot_edges,
2169                            rw_dot_vertices,
2170                            label='%s__%s__%s' % (site_local.site_dnstr,
2171                                                  nctype_lut[nc_x.nc_type],
2172                                                  nc_x.nc_dnstr),
2173                            properties=rw_verify_properties, debug=DEBUG,
2174                            verify=self.verify,
2175                            dot_file_dir=self.dot_file_dir,
2176                            directed=True)
2177
2178         # For each existing nTDSConnection object implying an edge
2179         # from rj of R to ri such that j != i, an edge from rj to ri
2180         # is not already in the graph, and the total edges directed
2181         # to ri is less than n+2, the KCC adds that edge to the graph.
2182         for vertex in graph_list:
2183             dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2184             for connect in dsa.connect_table.values():
2185                 remote = connect.from_dnstr
2186                 if remote in self.my_site.dsa_table:
2187                     vertex.add_edge_from(remote)
2188
2189         DEBUG('reps are:  %s' % '   '.join(x.rep_dsa_dnstr for x in r_list))
2190         DEBUG('dsas are:  %s' % '   '.join(x.dsa_dnstr for x in graph_list))
2191
2192         for tnode in graph_list:
2193             # To optimize replication latency in sites with many NC
2194             # replicas, the KCC adds new edges directed to ri to bring
2195             # the total edges to n+2, where the NC replica rk of R
2196             # from which the edge is directed is chosen at random such
2197             # that k != i and an edge from rk to ri is not already in
2198             # the graph.
2199             #
2200             # Note that the KCC tech ref does not give a number for
2201             # the definition of "sites with many NC replicas". At a
2202             # bare minimum to satisfy n+2 edges directed at a node we
2203             # have to have at least three replicas in |R| (i.e. if n
2204             # is zero then at least replicas from two other graph
2205             # nodes may direct edges to us).
2206             if r_len >= 3 and not tnode.has_sufficient_edges():
2207                 candidates = [x for x in graph_list if
2208                               (x is not tnode and
2209                                x.dsa_dnstr not in tnode.edge_from)]
2210
2211                 debug.DEBUG_BLUE("looking for random link for %s. r_len %d, "
2212                                  "graph len %d candidates %d"
2213                                  % (tnode.dsa_dnstr, r_len, len(graph_list),
2214                                     len(candidates)))
2215
2216                 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2217
2218                 while candidates and not tnode.has_sufficient_edges():
2219                     other = random.choice(candidates)
2220                     DEBUG("trying to add candidate %s" % other.dsa_dstr)
2221                     if not tnode.add_edge_from(other):
2222                         debug.DEBUG_RED("could not add %s" % other.dsa_dstr)
2223                     candidates.remove(other)
2224             else:
2225                 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2226                          (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2227                           tnode.max_edges))
2228
2229             # Print the graph node in debug mode
2230             DEBUG_FN("%s" % tnode)
2231
2232             # For each edge directed to the local DC, ensure a nTDSConnection
2233             # points to us that satisfies the KCC criteria
2234
2235             if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2236                 tnode.add_connections_from_edges(dc_local)
2237
2238         if self.verify or do_dot_files:
2239             dot_edges = []
2240             dot_vertices = set()
2241             for v1 in graph_list:
2242                 dot_vertices.add(v1.dsa_dnstr)
2243                 for v2 in v1.edge_from:
2244                     dot_edges.append((v2, v1.dsa_dnstr))
2245                     dot_vertices.add(v2)
2246
2247             verify_properties = ('connected',)
2248             verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2249                            label='%s__%s__%s' % (site_local.site_dnstr,
2250                                                  nctype_lut[nc_x.nc_type],
2251                                                  nc_x.nc_dnstr),
2252                            properties=verify_properties, debug=DEBUG,
2253                            verify=self.verify,
2254                            dot_file_dir=self.dot_file_dir,
2255                            directed=True)
2256
2257             rw_dot_vertices = set(x for x in dot_vertices
2258                                   if not self.get_dsa(x).is_ro())
2259             rw_dot_edges = [(a, b) for a, b in dot_edges if
2260                             a in rw_dot_vertices and b in rw_dot_vertices]
2261             print rw_dot_edges, rw_dot_vertices
2262             rw_verify_properties = ('connected',
2263                                     'directed_double_ring_or_small')
2264             verify_and_dot('intrasite_rw_post_ntdscon', rw_dot_edges,
2265                            rw_dot_vertices,
2266                            label='%s__%s__%s' % (site_local.site_dnstr,
2267                                                  nctype_lut[nc_x.nc_type],
2268                                                  nc_x.nc_dnstr),
2269                            properties=rw_verify_properties, debug=DEBUG,
2270                            verify=self.verify,
2271                            dot_file_dir=self.dot_file_dir,
2272                            directed=True)
2273
2274     def intrasite(self):
2275         """Generate the intrasite KCC connections
2276
2277         As per MS-ADTS 6.2.2.2.
2278
2279         If self.readonly is False, the connections are added to self.samdb.
2280
2281         After this call, all DCs in each site with more than 3 DCs
2282         should be connected in a bidirectional ring. If a site has 2
2283         DCs, they will bidirectionally connected. Sites with many DCs
2284         may have arbitrary extra connections.
2285
2286         :return: None
2287         """
2288         mydsa = self.my_dsa
2289
2290         DEBUG_FN("intrasite(): enter")
2291
2292         # Test whether local site has topology disabled
2293         mysite = self.my_site
2294         if mysite.is_intrasite_topology_disabled():
2295             return
2296
2297         detect_stale = (not mysite.is_detect_stale_disabled())
2298         for connect in mydsa.connect_table.values():
2299             if connect.to_be_added:
2300                 debug.DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2301
2302         # Loop thru all the partitions, with gc_only False
2303         for partdn, part in self.part_table.items():
2304             self.construct_intrasite_graph(mysite, mydsa, part, False,
2305                                            detect_stale)
2306             for connect in mydsa.connect_table.values():
2307                 if connect.to_be_added:
2308                     debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2309
2310         # If the DC is a GC server, the KCC constructs an additional NC
2311         # replica graph (and creates nTDSConnection objects) for the
2312         # config NC as above, except that only NC replicas that "are present"
2313         # on GC servers are added to R.
2314         for connect in mydsa.connect_table.values():
2315             if connect.to_be_added:
2316                 debug.DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2317
2318         # Do it again, with gc_only True
2319         for partdn, part in self.part_table.items():
2320             if part.is_config():
2321                 self.construct_intrasite_graph(mysite, mydsa, part, True,
2322                                                detect_stale)
2323
2324         # The DC repeats the NC replica graph computation and nTDSConnection
2325         # creation for each of the NC replica graphs, this time assuming
2326         # that no DC has failed. It does so by re-executing the steps as
2327         # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2328         # set in the options attribute of the site settings object for
2329         # the local DC's site.  (ie. we set "detec_stale" flag to False)
2330         for connect in mydsa.connect_table.values():
2331             if connect.to_be_added:
2332                 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2333
2334         # Loop thru all the partitions.
2335         for partdn, part in self.part_table.items():
2336             self.construct_intrasite_graph(mysite, mydsa, part, False,
2337                                            False)  # don't detect stale
2338
2339         # If the DC is a GC server, the KCC constructs an additional NC
2340         # replica graph (and creates nTDSConnection objects) for the
2341         # config NC as above, except that only NC replicas that "are present"
2342         # on GC servers are added to R.
2343         for connect in mydsa.connect_table.values():
2344             if connect.to_be_added:
2345                 debug.DEBUG_RED("TO BE ADDED:\n%s" % connect)
2346
2347         for partdn, part in self.part_table.items():
2348             if part.is_config():
2349                 self.construct_intrasite_graph(mysite, mydsa, part, True,
2350                                                False)  # don't detect stale
2351
2352         self._commit_changes(mydsa)
2353
2354     def list_dsas(self):
2355         """Compile a comprehensive list of DSA DNs
2356
2357         These are all the DSAs on all the sites that KCC would be
2358         dealing with.
2359
2360         This method is not idempotent and may not work correctly in
2361         sequence with KCC.run().
2362
2363         :return: a list of DSA DN strings.
2364         """
2365         self.load_my_site()
2366         self.load_my_dsa()
2367
2368         self.load_all_sites()
2369         self.load_all_partitions()
2370         self.load_ip_transport()
2371         self.load_all_sitelinks()
2372         dsas = []
2373         for site in self.site_table.values():
2374             dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2375                          for dsa in site.dsa_table.values()])
2376         return dsas
2377
2378     def load_samdb(self, dburl, lp, creds):
2379         """Load the database using an url, loadparm, and credentials
2380
2381         :param dburl: a database url.
2382         :param lp: a loadparm object.
2383         :param creds: a Credentials object.
2384         """
2385         self.samdb = SamDB(url=dburl,
2386                            session_info=system_session(),
2387                            credentials=creds, lp=lp)
2388
2389     def plot_all_connections(self, basename, verify_properties=()):
2390         """Helper function to plot and verify NTDSConnections
2391
2392         :param basename: an identifying string to use in filenames and logs.
2393         :param verify_properties: properties to verify (default empty)
2394         """
2395         verify = verify_properties and self.verify
2396         if not verify and self.dot_file_dir is None:
2397             return
2398
2399         dot_edges = []
2400         dot_vertices = []
2401         edge_colours = []
2402         vertex_colours = []
2403
2404         for dsa in self.dsa_by_dnstr.values():
2405             dot_vertices.append(dsa.dsa_dnstr)
2406             if dsa.is_ro():
2407                 vertex_colours.append('#cc0000')
2408             else:
2409                 vertex_colours.append('#0000cc')
2410             for con in dsa.connect_table.values():
2411                 if con.is_rodc_topology():
2412                     edge_colours.append('red')
2413                 else:
2414                     edge_colours.append('blue')
2415                 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2416
2417         verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2418                        label=self.my_dsa_dnstr,
2419                        properties=verify_properties, debug=DEBUG,
2420                        verify=verify, dot_file_dir=self.dot_file_dir,
2421                        directed=True, edge_colors=edge_colours,
2422                        vertex_colors=vertex_colours)
2423
2424     def run(self, dburl, lp, creds, forced_local_dsa=None,
2425             forget_local_links=False, forget_intersite_links=False,
2426             attempt_live_connections=False):
2427         """Perform a KCC run, possibly updating repsFrom topology
2428
2429         :param dburl: url of the database to work with.
2430         :param lp: a loadparm object.
2431         :param creds: a Credentials object.
2432         :param forced_local_dsa: pretend to be on the DSA with this dn_str
2433         :param forget_local_links: calculate as if no connections existed
2434                (boolean, default False)
2435         :param forget_intersite_links: calculate with only intrasite connection
2436                (boolean, default False)
2437         :param attempt_live_connections: attempt to connect to remote DSAs to
2438                determine link availability (boolean, default False)
2439         :return: 1 on error, 0 otherwise
2440         """
2441         # We may already have a samdb setup if we are
2442         # currently importing an ldif for a test run
2443         if self.samdb is None:
2444             try:
2445                 self.load_samdb(dburl, lp, creds)
2446             except ldb.LdbError, (num, msg):
2447                 logger.error("Unable to open sam database %s : %s" %
2448                              (dburl, msg))
2449                 return 1
2450
2451         if forced_local_dsa:
2452             self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2453                                             forced_local_dsa)
2454
2455         try:
2456             # Setup
2457             self.load_my_site()
2458             self.load_my_dsa()
2459
2460             self.load_all_sites()
2461             self.load_all_partitions()
2462             self.load_ip_transport()
2463             self.load_all_sitelinks()
2464
2465             if self.verify or self.dot_file_dir is not None:
2466                 guid_to_dnstr = {}
2467                 for site in self.site_table.values():
2468                     guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2469                                          for dnstr, dsa
2470                                          in site.dsa_table.items())
2471
2472                 self.plot_all_connections('dsa_initial')
2473
2474                 dot_edges = []
2475                 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2476                 for dnstr, c_rep in current_reps.items():
2477                     DEBUG("c_rep %s" % c_rep)
2478                     dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2479
2480                 verify_and_dot('dsa_repsFrom_initial', dot_edges,
2481                                directed=True, label=self.my_dsa_dnstr,
2482                                properties=(), debug=DEBUG, verify=self.verify,
2483                                dot_file_dir=self.dot_file_dir)
2484
2485                 dot_edges = []
2486                 for site in self.site_table.values():
2487                     for dsa in site.dsa_table.values():
2488                         current_reps, needed_reps = dsa.get_rep_tables()
2489                         for dn_str, rep in current_reps.items():
2490                             for reps_from in rep.rep_repsFrom:
2491                                 DEBUG("rep %s" % rep)
2492                                 dsa_guid = str(reps_from.source_dsa_obj_guid)
2493                                 dsa_dn = guid_to_dnstr[dsa_guid]
2494                                 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2495
2496                 verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2497                                directed=True, label=self.my_dsa_dnstr,
2498                                properties=(), debug=DEBUG, verify=self.verify,
2499                                dot_file_dir=self.dot_file_dir)
2500
2501                 dot_edges = []
2502                 for link in self.sitelink_table.values():
2503                     for a, b in itertools.combinations(link.site_list, 2):
2504                         dot_edges.append((str(a), str(b)))
2505                 properties = ('connected',)
2506                 verify_and_dot('dsa_sitelink_initial', dot_edges,
2507                                directed=False,
2508                                label=self.my_dsa_dnstr, properties=properties,
2509                                debug=DEBUG, verify=self.verify,
2510                                dot_file_dir=self.dot_file_dir)
2511
2512             if forget_local_links:
2513                 for dsa in self.my_site.dsa_table.values():
2514                     dsa.connect_table = dict((k, v) for k, v in
2515                                              dsa.connect_table.items()
2516                                              if v.is_rodc_topology())
2517                 self.plot_all_connections('dsa_forgotten_local')
2518
2519             if forget_intersite_links:
2520                 for site in self.site_table.values():
2521                     for dsa in site.dsa_table.values():
2522                         dsa.connect_table = dict((k, v) for k, v in
2523                                                  dsa.connect_table.items()
2524                                                  if site is self.my_site and
2525                                                  v.is_rodc_topology())
2526
2527                 self.plot_all_connections('dsa_forgotten_all')
2528
2529             if attempt_live_connections:
2530                 # Encapsulates lp and creds in a function that
2531                 # attempts connections to remote DSAs.
2532                 def ping(self, dnsname):
2533                     try:
2534                         drs_utils.drsuapi_connect(dnsname, self.lp, self.creds)
2535                     except drs_utils.drsException:
2536                         return False
2537                     return True
2538             else:
2539                 ping = None
2540             # These are the published steps (in order) for the
2541             # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2542
2543             # Step 1
2544             self.refresh_failed_links_connections(ping)
2545
2546             # Step 2
2547             self.intrasite()
2548
2549             # Step 3
2550             all_connected = self.intersite(ping)
2551
2552             # Step 4
2553             self.remove_unneeded_ntdsconn(all_connected)
2554
2555             # Step 5
2556             self.translate_ntdsconn()
2557
2558             # Step 6
2559             self.remove_unneeded_failed_links_connections()
2560
2561             # Step 7
2562             self.update_rodc_connection()
2563
2564             if self.verify or self.dot_file_dir is not None:
2565                 self.plot_all_connections('dsa_final',
2566                                           ('connected',))
2567
2568                 debug.DEBUG_MAGENTA("there are %d dsa guids" %
2569                                     len(guid_to_dnstr))
2570
2571                 dot_edges = []
2572                 edge_colors = []
2573                 my_dnstr = self.my_dsa.dsa_dnstr
2574                 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2575                 for dnstr, n_rep in needed_reps.items():
2576                     for reps_from in n_rep.rep_repsFrom:
2577                         guid_str = str(reps_from.source_dsa_obj_guid)
2578                         dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2579                         edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2580
2581                 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True,
2582                                label=self.my_dsa_dnstr,
2583                                properties=(), debug=DEBUG, verify=self.verify,
2584                                dot_file_dir=self.dot_file_dir,
2585                                edge_colors=edge_colors)
2586
2587                 dot_edges = []
2588
2589                 for site in self.site_table.values():
2590                     for dsa in site.dsa_table.values():
2591                         current_reps, needed_reps = dsa.get_rep_tables()
2592                         for n_rep in needed_reps.values():
2593                             for reps_from in n_rep.rep_repsFrom:
2594                                 dsa_guid = str(reps_from.source_dsa_obj_guid)
2595                                 dsa_dn = guid_to_dnstr[dsa_guid]
2596                                 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2597
2598                 verify_and_dot('dsa_repsFrom_final_all', dot_edges,
2599                                directed=True, label=self.my_dsa_dnstr,
2600                                properties=(), debug=DEBUG, verify=self.verify,
2601                                dot_file_dir=self.dot_file_dir)
2602
2603         except:
2604             raise
2605
2606         return 0
2607
2608     def import_ldif(self, dburl, lp, creds, ldif_file, forced_local_dsa=None):
2609         """Import relevant objects and attributes from an LDIF file.
2610
2611         The point of this function is to allow a programmer/debugger to
2612         import an LDIF file with non-security relevent information that
2613         was previously extracted from a DC database.  The LDIF file is used
2614         to create a temporary abbreviated database.  The KCC algorithm can
2615         then run against this abbreviated database for debug or test
2616         verification that the topology generated is computationally the
2617         same between different OSes and algorithms.
2618
2619         :param dburl: path to the temporary abbreviated db to create
2620         :param lp: a loadparm object.
2621         :param cred: a Credentials object.
2622         :param ldif_file: path to the ldif file to import
2623         :param forced_local_dsa: perform KCC from this DSA's point of view
2624         :return: zero on success, 1 on error
2625         """
2626         try:
2627             self.samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif_file,
2628                                                           forced_local_dsa)
2629         except ldif_import_export.LdifError, e:
2630             logger.critical(e)
2631             return 1
2632         return 0
2633
2634     def export_ldif(self, dburl, lp, creds, ldif_file):
2635         """Save KCC relevant details to an ldif file
2636
2637         The point of this function is to allow a programmer/debugger to
2638         extract an LDIF file with non-security relevent information from
2639         a DC database.  The LDIF file can then be used to "import" via
2640         the import_ldif() function this file into a temporary abbreviated
2641         database.  The KCC algorithm can then run against this abbreviated
2642         database for debug or test verification that the topology generated
2643         is computationally the same between different OSes and algorithms.
2644
2645         :param dburl: LDAP database URL to extract info from
2646         :param lp: a loadparm object.
2647         :param cred: a Credentials object.
2648         :param ldif_file: output LDIF file name to create
2649         :return: zero on success, 1 on error
2650         """
2651         try:
2652             ldif_import_export.samdb_to_ldif_file(self.samdb, dburl, lp, creds,
2653                                                   ldif_file)
2654         except ldif_import_export.LdifError, e:
2655             logger.critical(e)
2656             return 1
2657         return 0