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