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