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