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