KCC: remove unnecessary debug message in KCC.get_all_bridgeheads()
[Samba.git] / python / samba / kcc / __init__.py
blobf6a54979577775460311de96d405e7630bc4f9a6
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.graph 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,verify=False, debug=False,
103 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.verify = verify
142 self.debug = debug
143 self.dot_files = dot_files
145 def load_all_transports(self):
146 """Loads the inter-site transport objects for Sites
148 :return: None
149 :raise KCCError: if no IP transport is found
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)
160 for msg in res:
161 dnstr = str(msg.dn)
163 transport = Transport(dnstr)
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
171 if self.ip_transport is None:
172 raise KCCError("there doesn't seem to be an IP transport")
174 def load_all_sitelinks(self):
175 """Loads the inter-site siteLink objects
177 :return: None
178 :raise KCCError: if site-links aren't found
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)
188 for msg in res:
189 dnstr = str(msg.dn)
191 # already loaded
192 if dnstr in self.sitelink_table:
193 continue
195 sitelink = SiteLink(dnstr)
197 sitelink.load_sitelink(self.samdb)
199 # Assign this siteLink to table
200 # and index by dn
201 self.sitelink_table[dnstr] = sitelink
203 def load_site(self, dn_str):
204 """Helper for load_my_site and load_all_sites.
206 Put all the site's DSAs into the KCC indices.
208 :param dn_str: a site dn_str
209 :return: the Site object pertaining to the dn_str
211 site = Site(dn_str, self.unix_now)
212 site.load_site(self.samdb)
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())
224 return self.site_table[guid]
226 def load_my_site(self):
227 """Load the Site object for the local DSA.
229 :return: None
231 self.my_site_dnstr = ("CN=%s,CN=Sites,%s" % (
232 self.samdb.server_site_name(),
233 self.samdb.get_config_basedn()))
235 self.my_site = self.load_site(self.my_site_dnstr)
237 def load_all_sites(self):
238 """Discover all sites and create Site objects.
240 :return: None
241 :raise: KCCError if sites can't be found
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)
251 for msg in res:
252 sitestr = str(msg.dn)
253 self.load_site(sitestr)
255 def load_my_dsa(self):
256 """Discover my nTDSDSA dn thru the rootDSE entry
258 :return: None
259 :raise: KCCError if DSA can't be found
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])
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)
287 if len(res) != 1:
288 raise KCCError("Unable to find my nTDSDSA at %s" %
289 dn.extended_str())
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")
296 self.my_dsa_dnstr = str(res[0].dn)
298 self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
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)
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
310 def load_all_partitions(self):
311 """Discover and load all partitions.
313 Each NC is inserted into the part_table by partition
314 dn string (not the nCName dn string)
316 :return: None
317 :raise: KCCError if partitions can't be found
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)
327 for msg in res:
328 partstr = str(msg.dn)
330 # already loaded
331 if partstr in self.part_table:
332 continue
334 part = Partition(partstr)
336 part.load_partition(self.samdb)
337 self.part_table[partstr] = part
339 def refresh_failed_links_connections(self, ping=None):
340 """Ensure the failed links list is up to date
342 Based on MS-ADTS 6.2.2.1
344 :param ping: An oracle function of remote site availability
345 :return: None
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
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
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
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")
388 # Remove the restored connections from the failed connections
389 self.kcc_failed_connections.difference_update(restore_connections)
391 def is_stale_link_connection(self, target_dsa):
392 """Check whether a link to a remote DSA is stale
394 Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation
396 Returns True if the remote seems to have been down for at
397 least two hours, otherwise False.
399 :param target_dsa: the remote DSA object
400 :return: True if link is stale, otherwise False
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!")
413 # Perform calculation in seconds
414 if (self.unix_now - unix_first_failure) > 60 * 60 * 2:
415 return True
417 # TODO connections.
418 # We have checked failed *links*, but we also need to check
419 # *connections*
421 return False
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.
428 # Remove all connections which were not used this run or connections
429 # that became active during this run.
430 pass
432 def remove_unneeded_ntdsconn(self, all_connected):
433 """Remove unneeded NTDS Connections once topology is calculated
435 Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections
437 :param all_connected: indicates whether all sites are connected
438 :return: None
440 mydsa = self.my_dsa
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)
452 for cn_conn in mydsa.connect_table.values():
454 s_dnstr = cn_conn.get_from_dnstr()
455 if s_dnstr is None:
456 cn_conn.to_be_deleted = True
457 continue
459 #XXX should an RODC be regarded as same site
460 same_site = s_dnstr in self.my_site.dsa_table
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:
468 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
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.
474 # Another nTDSConnection object cn2 exists such that cn and
475 # cn2 have the same parent object, cn!fromServer = cn2!fromServer,
476 # and either
478 # cn!whenCreated < cn2!whenCreated
480 # cn!whenCreated = cn2!whenCreated and
481 # cn!objectGUID < cn2!objectGUID
483 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
484 if same_site:
485 if not cn_conn.is_generated():
486 continue
488 if self.my_site.is_cleanup_ntdsconn_disabled():
489 continue
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
499 s2_dnstr = cn2_conn.get_from_dnstr()
501 # If the NTDS Connections has a different
502 # fromServer field then no match
503 if s2_dnstr != s_dnstr:
504 continue
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)))
511 if lesser:
512 break
514 if lesser and not cn_conn.is_rodc_topology():
515 cn_conn.to_be_deleted = True
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:
523 # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
525 # cn!fromServer references an nTDSDSA object for a DC
526 # in a site other than the local DC's site.
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.
534 # The return value of CreateIntersiteConnections()
535 # was true.
537 # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in
538 # cn!options
540 else: # different site
542 if not mydsa.is_istg():
543 continue
545 if not cn_conn.is_generated():
546 continue
548 # TODO
549 # We are directly using this connection in intersite or
550 # we are using a connection which can supersede this one.
552 # MS-ADTS 6.2.2.4 - Removing Unnecessary Connections does not
553 # appear to be correct.
555 # 1. cn!fromServer and cn!parent appear inconsistent with
556 # no cn2
557 # 2. The repsFrom do not imply each other
559 if cn_conn in self.kept_connections: # and not_superceded:
560 continue
562 # This is the result of create_intersite_connections
563 if not all_connected:
564 continue
566 if not cn_conn.is_rodc_topology():
567 cn_conn.to_be_deleted = True
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)
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)
583 def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
584 """Update an repsFrom object if required.
586 Part of MS-ADTS 6.2.2.5.
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.
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.
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
605 :return: None
607 s_dnstr = s_dsa.dsa_dnstr
608 same_site = s_dnstr in self.my_site.dsa_table
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
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():
621 if ((t_repsFrom.replica_flags &
622 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0):
623 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
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):
631 if ((t_repsFrom.replica_flags &
632 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0):
633 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
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):
644 if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
645 # XXX WARNING
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
659 elif not same_site:
661 if ((t_repsFrom.replica_flags &
662 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
663 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
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):
674 if ((t_repsFrom.replica_flags &
675 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0):
676 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
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:
682 if ((t_repsFrom.replica_flags &
683 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0):
684 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
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():
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
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
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:
704 # Bit DRS_MAIL_REP in t.replicaFlags is clear.
706 # t.uuidTransport = NULL GUID.
708 # t.uuidDsa = The GUID-based DNS name of s.
710 # Otherwise:
712 # Bit DRS_MAIL_REP in t.replicaFlags is set.
714 # If x is the object with dsname cn!transportType,
715 # t.uuidTransport = x!objectGUID.
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.
722 # It appears that the first statement i.e.
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:"
728 # could be a slightly tighter statement if it had an "or"
729 # between each condition. I believe this should
730 # be interpreted as:
732 # IF (same-site) OR (no-value) OR (type-ip)
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
738 # NOTE MS-TECH INCORRECT:
740 # All indications point to these statements above being
741 # incorrectly stated:
743 # t.uuidDsa = The GUID-based DNS name of s.
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.
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:
754 # t.naDsa = The GUID-based DNS name of s
756 # That would also be correct if transportAddressAttribute
757 # were "mailAddress" because (naDsa) can also correctly
758 # hold the SMTP ISM service address.
760 nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
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())):
770 if ((t_repsFrom.replica_flags &
771 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0):
772 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
774 t_repsFrom.transport_guid = misc.GUID()
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
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
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)
802 x_transport = self.transport_table[str(cn_conn.transport_guid)]
804 if t_repsFrom.transport_guid != x_transport.guid:
805 t_repsFrom.transport_guid = x_transport.guid
807 # See (NOTE MS-TECH INCORRECT) above
808 if x_transport.address_attr == "dNSHostName":
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
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]
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))
837 msg = res[0]
838 nastr = str(msg[x_transport.address_attr][0])
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:
851 t_repsFrom.dns_name1 = nastr
852 t_repsFrom.dns_name2 = nastr
854 if t_repsFrom.is_modified():
855 DEBUG_FN("modify_repsFrom(): %s" % t_repsFrom)
857 def get_dsa_for_implied_replica(self, n_rep, cn_conn):
858 """If a connection imply a replica, find the relevant DSA
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.
865 Based on part of MS-ADTS 6.2.2.5
867 :param n_rep: NC replica
868 :param cn_conn: NTDS Connection
869 :return: source DSA or None
871 #XXX different conditions for "implies" than MS-ADTS 6.2.2
873 # NTDS Connection must satisfy all the following criteria
874 # to imply a repsFrom tuple is needed:
876 # cn!enabledConnection = true.
877 # cn!options does not contain NTDSCONN_OPT_RODC_TOPOLOGY.
878 # cn!fromServer references an nTDSDSA object.
880 if not cn_conn.is_enabled() or cn_conn.is_rodc_topology():
881 return None
883 s_dnstr = cn_conn.get_from_dnstr()
884 s_dsa = self.get_dsa(s_dnstr)
886 # No DSA matching this source DN string?
887 if s_dsa is None:
888 return None
890 # To imply a repsFrom tuple is needed, each of these
891 # must be True:
893 # An NC replica of the NC "is present" on the DC to
894 # which the nTDSDSA object referenced by cn!fromServer
895 # corresponds.
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)
901 if s_rep is None or not s_rep.is_present():
902 return None
904 # To imply a repsFrom tuple is needed, each of these
905 # must be True:
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.
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.
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)
922 if implied:
923 return s_dsa
924 return None
926 def translate_ntdsconn(self, current_dsa=None):
927 """Adjust repsFrom to match NTDSConnections
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.
933 Based on [MS-ADTS] 6.2.2.5
935 :param current_dsa: optional DSA on whose behalf we are acting.
936 :return: None
938 count = 0
940 if current_dsa is None:
941 current_dsa = self.my_dsa
943 if current_dsa.is_translate_ntdsconn_disabled():
944 DEBUG_FN("skipping translate_ntdsconn() "
945 "because disabling flag is set")
946 return
948 DEBUG_FN("translate_ntdsconn(): enter")
950 current_rep_table, needed_rep_table = current_dsa.get_rep_tables()
952 # Filled in with replicas we currently have that need deleting
953 delete_reps = set()
955 # We're using the MS notation names here to allow
956 # correlation back to the published algorithm.
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
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)
974 DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
975 len(needed_rep_table), len(delete_reps)))
977 if delete_reps:
978 DEBUG('deleting these reps: %s' % delete_reps)
979 for dnstr in delete_reps:
980 del current_rep_table[dnstr]
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():
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)
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:
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)
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
1010 s_dnstr = s_dsa.dsa_dnstr
1012 # Retrieve my DSAs connection object (if it exists)
1013 # that specifies the fromServer equivalent to
1014 # the DSA that is specified in the repsFrom source
1015 connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
1017 count = 0
1018 cn_conn = None
1020 for con in connections:
1021 if con.is_rodc_topology():
1022 continue
1023 cn_conn = con
1025 # Let (cn) be the nTDSConnection object such that (cn)
1026 # is a child of the local DC's nTDSDSA object and
1027 # (cn!fromServer = s) and (cn!options) does not contain
1028 # NTDSCONN_OPT_RODC_TOPOLOGY or NULL if no such (cn) exists.
1030 # KCC removes this repsFrom tuple if any of the following
1031 # is true:
1032 # cn = NULL.
1033 # [...]
1035 #XXX varying possible interpretations of rodc_topology
1036 if cn_conn is None:
1037 t_repsFrom.to_be_deleted = True
1038 continue
1040 # [...] KCC removes this repsFrom tuple if:
1042 # No NC replica of the NC "is present" on DSA that
1043 # would be source of replica
1045 # A writable replica of the NC "should be present" on
1046 # the local DC, but a partial replica "is present" on
1047 # the source DSA
1048 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1050 if s_rep is None or not s_rep.is_present() or \
1051 (not n_rep.is_ro() and s_rep.is_partial()):
1053 t_repsFrom.to_be_deleted = True
1054 continue
1056 # If the KCC did not remove t from n!repsFrom, it updates t
1057 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1059 # Loop thru connections and add implied repsFrom tuples
1060 # for each NTDSConnection under our local DSA if the
1061 # repsFrom is not already present
1062 for cn_conn in current_dsa.connect_table.values():
1064 s_dsa = self.get_dsa_for_implied_replica(n_rep, cn_conn)
1065 if s_dsa is None:
1066 continue
1068 # Loop thru the existing repsFrom tupples (if any) and
1069 # if we already have a tuple for this connection then
1070 # no need to proceed to add. It will have been changed
1071 # to have the correct attributes above
1072 for t_repsFrom in n_rep.rep_repsFrom:
1073 guidstr = str(t_repsFrom.source_dsa_obj_guid)
1074 #XXX what?
1075 if s_dsa is self.get_dsa_by_guidstr(guidstr):
1076 s_dsa = None
1077 break
1079 if s_dsa is None:
1080 continue
1082 # Create a new RepsFromTo and proceed to modify
1083 # it according to specification
1084 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
1086 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
1088 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1090 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1092 # Add to our NC repsFrom as this is newly computed
1093 if t_repsFrom.is_modified():
1094 n_rep.rep_repsFrom.append(t_repsFrom)
1096 if self.readonly:
1097 # Display any to be deleted or modified repsFrom
1098 text = n_rep.dumpstr_to_be_deleted()
1099 if text:
1100 logger.info("TO BE DELETED:\n%s" % text)
1101 text = n_rep.dumpstr_to_be_modified()
1102 if text:
1103 logger.info("TO BE MODIFIED:\n%s" % text)
1105 # Peform deletion from our tables but perform
1106 # no database modification
1107 n_rep.commit_repsFrom(self.samdb, ro=True)
1108 else:
1109 # Commit any modified repsFrom to the NC replica
1110 n_rep.commit_repsFrom(self.samdb)
1112 def merge_failed_links(self, ping=None):
1113 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1115 The KCC on a writable DC attempts to merge the link and connection
1116 failure information from bridgehead DCs in its own site to help it
1117 identify failed bridgehead DCs.
1119 Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1120 from Bridgeheads"
1122 :param ping: An oracle of current bridgehead availability
1123 :return: None
1125 # 1. Queries every bridgehead server in your site (other than yourself)
1126 # 2. For every ntDSConnection that references a server in a different
1127 # site merge all the failure info
1129 # XXX - not implemented yet
1130 if ping is not None:
1131 debug.DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1132 else:
1133 DEBUG_FN("skipping merge_failed_links() because it requires "
1134 "real network connections\n"
1135 "and we weren't asked to --attempt-live-connections")
1137 def setup_graph(self, part):
1138 """Set up an intersite graph
1140 An intersite graph has a Vertex for each site object, a
1141 MultiEdge for each SiteLink object, and a MutliEdgeSet for
1142 each siteLinkBridge object (or implied siteLinkBridge). It
1143 reflects the intersite topology in a slightly more abstract
1144 graph form.
1146 Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1148 :param part: a Partition object
1149 :returns: an InterSiteGraph object
1151 # If 'Bridge all site links' is enabled and Win2k3 bridges required
1152 # is not set
1153 # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1154 # No documentation for this however, ntdsapi.h appears to have:
1155 # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1156 bridges_required = self.my_site.site_options & 0x00001002 == 0
1158 g = setup_graph(part, self.site_table, self.transport_table,
1159 self.sitelink_table, bridges_required)
1161 if self.verify or self.dot_files:
1162 dot_edges = []
1163 for edge in g.edges:
1164 for a, b in itertools.combinations(edge.vertices, 2):
1165 dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1166 verify_properties = ()
1167 verify_and_dot('site_edges', dot_edges, directed=False,
1168 label=self.my_dsa_dnstr,
1169 properties=verify_properties, debug=DEBUG,
1170 verify=self.verify,
1171 dot_files=self.dot_files)
1173 return g
1175 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1176 """Get a bridghead DC for a site.
1178 Part of MS-ADTS 6.2.2.3.4.4
1180 :param site: site object representing for which a bridgehead
1181 DC is desired.
1182 :param part: crossRef for NC to replicate.
1183 :param transport: interSiteTransport object for replication
1184 traffic.
1185 :param partial_ok: True if a DC containing a partial
1186 replica or a full replica will suffice, False if only
1187 a full replica will suffice.
1188 :param detect_failed: True to detect failed DCs and route
1189 replication traffic around them, False to assume no DC
1190 has failed.
1191 :return: dsa object for the bridgehead DC or None
1194 bhs = self.get_all_bridgeheads(site, part, transport,
1195 partial_ok, detect_failed)
1196 if len(bhs) == 0:
1197 debug.DEBUG_MAGENTA("get_bridgehead:\n\tsitedn=%s\n\tbhdn=None" %
1198 site.site_dnstr)
1199 return None
1200 else:
1201 debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn=%s\n\tbhdn=%s" %
1202 (site.site_dnstr, bhs[0].dsa_dnstr))
1203 return bhs[0]
1205 def get_all_bridgeheads(self, site, part, transport,
1206 partial_ok, detect_failed):
1207 """Get all bridghead DCs on a site satisfying the given criteria
1209 Part of MS-ADTS 6.2.2.3.4.4
1211 :param site: site object representing the site for which
1212 bridgehead DCs are desired.
1213 :param part: partition for NC to replicate.
1214 :param transport: interSiteTransport object for
1215 replication traffic.
1216 :param partial_ok: True if a DC containing a partial
1217 replica or a full replica will suffice, False if
1218 only a full replica will suffice.
1219 :param detect_failed: True to detect failed DCs and route
1220 replication traffic around them, FALSE to assume
1221 no DC has failed.
1222 :return: list of dsa object for available bridgehead DCs
1224 bhs = []
1226 DEBUG_FN("get_all_bridgeheads: %s" % transport.name)
1227 DEBUG_FN(site.rw_dsa_table)
1228 for dsa in site.rw_dsa_table.values():
1230 pdnstr = dsa.get_parent_dnstr()
1232 # IF t!bridgeheadServerListBL has one or more values and
1233 # t!bridgeheadServerListBL does not contain a reference
1234 # to the parent object of dc then skip dc
1235 if ((len(transport.bridgehead_list) != 0 and
1236 pdnstr not in transport.bridgehead_list)):
1237 continue
1239 # IF dc is in the same site as the local DC
1240 # IF a replica of cr!nCName is not in the set of NC replicas
1241 # that "should be present" on dc or a partial replica of the
1242 # NC "should be present" but partialReplicasOkay = FALSE
1243 # Skip dc
1244 if self.my_site.same_site(dsa):
1245 needed, ro, partial = part.should_be_present(dsa)
1246 if not needed or (partial and not partial_ok):
1247 continue
1248 rep = dsa.get_current_replica(part.nc_dnstr)
1250 # ELSE
1251 # IF an NC replica of cr!nCName is not in the set of NC
1252 # replicas that "are present" on dc or a partial replica of
1253 # the NC "is present" but partialReplicasOkay = FALSE
1254 # Skip dc
1255 else:
1256 rep = dsa.get_current_replica(part.nc_dnstr)
1257 if rep is None or (rep.is_partial() and not partial_ok):
1258 continue
1260 # IF AmIRODC() and cr!nCName corresponds to default NC then
1261 # Let dsaobj be the nTDSDSA object of the dc
1262 # IF dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1263 # Skip dc
1264 if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1265 if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1266 continue
1268 # IF t!name != "IP" and the parent object of dc has no value for
1269 # the attribute specified by t!transportAddressAttribute
1270 # Skip dc
1271 if transport.name != "IP":
1272 # MS tech specification says we retrieve the named
1273 # attribute in "transportAddressAttribute" from the parent
1274 # of the DSA object
1275 try:
1276 attrs = [transport.address_attr]
1278 res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
1279 attrs=attrs)
1280 except ldb.LdbError, (enum, estr):
1281 continue
1283 msg = res[0]
1284 if transport.address_attr not in msg:
1285 continue
1286 #XXX nastr is NEVER USED. It will be removed.
1287 nastr = str(msg[transport.address_attr][0])
1289 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1290 # Skip dc
1291 if self.is_bridgehead_failed(dsa, detect_failed):
1292 DEBUG("bridgehead is failed")
1293 continue
1295 DEBUG_FN("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
1296 bhs.append(dsa)
1298 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1299 # s!options
1300 # SORT bhs such that all GC servers precede DCs that are not GC
1301 # servers, and otherwise by ascending objectGUID
1302 # ELSE
1303 # SORT bhs in a random order
1304 if site.is_random_bridgehead_disabled():
1305 bhs.sort(sort_dsa_by_gc_and_guid)
1306 else:
1307 random.shuffle(bhs)
1308 debug.DEBUG_YELLOW(bhs)
1309 return bhs
1311 def is_bridgehead_failed(self, dsa, detect_failed):
1312 """Determine whether a given DC is known to be in a failed state
1314 :param dsa: the bridgehead to test
1315 :param detect_failed: True to really check, False to assume no failure
1316 :return: True if and only if the DC should be considered failed
1318 Here we DEPART from the pseudo code spec which appears to be
1319 wrong. It says, in full:
1321 /***** BridgeheadDCFailed *****/
1322 /* Determine whether a given DC is known to be in a failed state.
1323 * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1324 * IN: detectFailedDCs - TRUE if and only failed DC detection is
1325 * enabled.
1326 * RETURNS: TRUE if and only if the DC should be considered to be in a
1327 * failed state.
1329 BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1331 IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1332 the options attribute of the site settings object for the local
1333 DC's site
1334 RETURN FALSE
1335 ELSEIF a tuple z exists in the kCCFailedLinks or
1336 kCCFailedConnections variables such that z.UUIDDsa =
1337 objectGUID, z.FailureCount > 1, and the current time -
1338 z.TimeFirstFailure > 2 hours
1339 RETURN TRUE
1340 ELSE
1341 RETURN detectFailedDCs
1342 ENDIF
1345 where you will see detectFailedDCs is not behaving as
1346 advertised -- it is acting as a default return code in the
1347 event that a failure is not detected, not a switch turning
1348 detection on or off. Elsewhere the documentation seems to
1349 concur with the comment rather than the code.
1351 if not detect_failed:
1352 return False
1354 # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1355 # When DETECT_STALE_DISABLED, we can never know of if
1356 # it's in a failed state
1357 if self.my_site.site_options & 0x00000008:
1358 return False
1360 return self.is_stale_link_connection(dsa)
1362 def create_connection(self, part, rbh, rsite, transport,
1363 lbh, lsite, link_opt, link_sched,
1364 partial_ok, detect_failed):
1365 """Create an nTDSConnection object as specified if it doesn't exist.
1367 Part of MS-ADTS 6.2.2.3.4.5
1369 :param part: crossRef object for the NC to replicate.
1370 :param rbh: nTDSDSA object for DC to act as the
1371 IDL_DRSGetNCChanges server (which is in a site other
1372 than the local DC's site).
1373 :param rsite: site of the rbh
1374 :param transport: interSiteTransport object for the transport
1375 to use for replication traffic.
1376 :param lbh: nTDSDSA object for DC to act as the
1377 IDL_DRSGetNCChanges client (which is in the local DC's site).
1378 :param lsite: site of the lbh
1379 :param link_opt: Replication parameters (aggregated siteLink options,
1380 etc.)
1381 :param link_sched: Schedule specifying the times at which
1382 to begin replicating.
1383 :partial_ok: True if bridgehead DCs containing partial
1384 replicas of the NC are acceptable.
1385 :param detect_failed: True to detect failed DCs and route
1386 replication traffic around them, FALSE to assume no DC
1387 has failed.
1389 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1390 partial_ok, False)
1391 rbh_table = {x.dsa_dnstr: x for x in rbhs_all}
1393 debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1394 [x.dsa_dnstr for x in rbhs_all]))
1396 # MS-TECH says to compute rbhs_avail but then doesn't use it
1397 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1398 # partial_ok, detect_failed)
1400 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1401 partial_ok, False)
1402 if lbh.is_ro():
1403 lbhs_all.append(lbh)
1405 debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1406 [x.dsa_dnstr for x in lbhs_all]))
1408 # MS-TECH says to compute lbhs_avail but then doesn't use it
1409 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1410 # partial_ok, detect_failed)
1412 # FOR each nTDSConnection object cn such that the parent of cn is
1413 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1414 for ldsa in lbhs_all:
1415 for cn in ldsa.connect_table.values():
1417 rdsa = rbh_table.get(cn.from_dnstr)
1418 if rdsa is None:
1419 continue
1421 debug.DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1422 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1423 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1424 # cn!transportType references t
1425 if ((cn.is_generated() and
1426 not cn.is_rodc_topology() and
1427 cn.transport_guid == transport.guid)):
1429 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1430 # cn!options and cn!schedule != sch
1431 # Perform an originating update to set cn!schedule to
1432 # sched
1433 if ((not cn.is_user_owned_schedule() and
1434 not cn.is_equivalent_schedule(link_sched))):
1435 cn.schedule = link_sched
1436 cn.set_modified(True)
1438 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1439 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1440 if cn.is_override_notify_default() and \
1441 cn.is_use_notify():
1443 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1444 # ri.Options
1445 # Perform an originating update to clear bits
1446 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1447 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1448 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1449 cn.options &= \
1450 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1451 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1452 cn.set_modified(True)
1454 # ELSE
1455 else:
1457 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1458 # ri.Options
1459 # Perform an originating update to set bits
1460 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1461 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1462 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1463 cn.options |= \
1464 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1465 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1466 cn.set_modified(True)
1468 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1469 if cn.is_twoway_sync():
1471 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1472 # ri.Options
1473 # Perform an originating update to clear bit
1474 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1475 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1476 cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1477 cn.set_modified(True)
1479 # ELSE
1480 else:
1482 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1483 # ri.Options
1484 # Perform an originating update to set bit
1485 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1486 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1487 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1488 cn.set_modified(True)
1490 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1491 # in cn!options
1492 if cn.is_intersite_compression_disabled():
1494 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1495 # in ri.Options
1496 # Perform an originating update to clear bit
1497 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1498 # cn!options
1499 if ((link_opt &
1500 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1501 cn.options &= \
1502 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1503 cn.set_modified(True)
1505 # ELSE
1506 else:
1507 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1508 # ri.Options
1509 # Perform an originating update to set bit
1510 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1511 # cn!options
1512 if ((link_opt &
1513 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1514 cn.options |= \
1515 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1516 cn.set_modified(True)
1518 # Display any modified connection
1519 if self.readonly:
1520 if cn.to_be_modified:
1521 logger.info("TO BE MODIFIED:\n%s" % cn)
1523 ldsa.commit_connections(self.samdb, ro=True)
1524 else:
1525 ldsa.commit_connections(self.samdb)
1526 # ENDFOR
1528 valid_connections = 0
1530 # FOR each nTDSConnection object cn such that cn!parent is
1531 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1532 for ldsa in lbhs_all:
1533 for cn in ldsa.connect_table.values():
1535 rdsa = rbh_table.get(cn.from_dnstr)
1536 if rdsa is None:
1537 continue
1539 debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1541 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1542 # cn!transportType references t) and
1543 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1544 if (((not cn.is_generated() or
1545 cn.transport_guid == transport.guid) and
1546 not cn.is_rodc_topology())):
1548 # LET rguid be the objectGUID of the nTDSDSA object
1549 # referenced by cn!fromServer
1550 # LET lguid be (cn!parent)!objectGUID
1552 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1553 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1554 # Increment cValidConnections by 1
1555 if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1556 not self.is_bridgehead_failed(ldsa, detect_failed))):
1557 valid_connections += 1
1559 # IF keepConnections does not contain cn!objectGUID
1560 # APPEND cn!objectGUID to keepConnections
1561 self.kept_connections.add(cn)
1563 # ENDFOR
1564 debug.DEBUG_RED("valid connections %d" % valid_connections)
1565 DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1566 # IF cValidConnections = 0
1567 if valid_connections == 0:
1569 # LET opt be NTDSCONN_OPT_IS_GENERATED
1570 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1572 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1573 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1574 # NTDSCONN_OPT_USE_NOTIFY in opt
1575 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1576 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1577 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1579 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1580 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1581 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1582 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1584 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1585 # ri.Options
1586 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1587 if ((link_opt &
1588 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1589 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1591 # Perform an originating update to create a new nTDSConnection
1592 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1593 # cn!options = opt, cn!transportType is a reference to t,
1594 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1595 DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1596 cn = lbh.new_connection(opt, 0, transport,
1597 rbh.dsa_dnstr, link_sched)
1599 # Display any added connection
1600 if self.readonly:
1601 if cn.to_be_added:
1602 logger.info("TO BE ADDED:\n%s" % cn)
1604 lbh.commit_connections(self.samdb, ro=True)
1605 else:
1606 lbh.commit_connections(self.samdb)
1608 # APPEND cn!objectGUID to keepConnections
1609 self.kept_connections.add(cn)
1611 def add_transports(self, vertex, local_vertex, graph, detect_failed):
1612 """Build a Vertex's transport lists
1614 Each vertex has accept_red_red and accept_black lists that
1615 list what transports they accept under various conditions. The
1616 only transport that is ever accepted is IP, and a dummy extra
1617 transport called "EDGE_TYPE_ALL".
1619 Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1621 :param vertex: the remote vertex we are thinking about
1622 :param local_vertex: the vertex relating to the local site.
1623 :param graph: the intersite graph
1624 :param detect_failed: whether to detect failed links
1625 :return: True if some bridgeheads were not found
1627 # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1628 # here, but using vertex seems to make more sense. That is,
1629 # the docs want this:
1631 #bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1632 # local_vertex.is_black(), detect_failed)
1634 # TODO WHY?????
1636 vertex.accept_red_red = []
1637 vertex.accept_black = []
1638 found_failed = False
1639 for t_guid, transport in self.transport_table.items():
1640 if transport.name != 'IP':
1641 #XXX well this is cheating a bit
1642 logger.warning("WARNING: we are ignoring a transport named %r"
1643 % transport.name)
1644 continue
1646 # FLAG_CR_NTDS_DOMAIN 0x00000002
1647 if ((vertex.is_red() and transport.name != "IP" and
1648 vertex.part.system_flags & 0x00000002)):
1649 continue
1651 if vertex not in graph.connected_vertices:
1652 continue
1654 partial_replica_okay = vertex.is_black()
1655 bh = self.get_bridgehead(vertex.site, vertex.part, transport,
1656 partial_replica_okay, detect_failed)
1657 if bh is None:
1658 found_failed = True
1659 continue
1661 vertex.accept_red_red.append(t_guid)
1662 vertex.accept_black.append(t_guid)
1664 # Add additional transport to allow another run of Dijkstra
1665 vertex.accept_red_red.append("EDGE_TYPE_ALL")
1666 vertex.accept_black.append("EDGE_TYPE_ALL")
1668 return found_failed
1670 def create_connections(self, graph, part, detect_failed):
1671 """Construct an NC replica graph for the NC identified by
1672 the given crossRef, then create any additional nTDSConnection
1673 objects required.
1675 :param graph: site graph.
1676 :param part: crossRef object for NC.
1677 :param detect_failed: True to detect failed DCs and route
1678 replication traffic around them, False to assume no DC
1679 has failed.
1681 Modifies self.kept_connections by adding any connections
1682 deemed to be "in use".
1684 ::returns: (all_connected, found_failed_dc)
1685 (all_connected) True if the resulting NC replica graph
1686 connects all sites that need to be connected.
1687 (found_failed_dc) True if one or more failed DCs were
1688 detected.
1690 all_connected = True
1691 found_failed = False
1693 DEBUG_FN("create_connections(): enter\n"
1694 "\tpartdn=%s\n\tdetect_failed=%s" %
1695 (part.nc_dnstr, detect_failed))
1697 # XXX - This is a highly abbreviated function from the MS-TECH
1698 # ref. It creates connections between bridgeheads to all
1699 # sites that have appropriate replicas. Thus we are not
1700 # creating a minimum cost spanning tree but instead
1701 # producing a fully connected tree. This should produce
1702 # a full (albeit not optimal cost) replication topology.
1704 my_vertex = Vertex(self.my_site, part)
1705 my_vertex.color_vertex()
1707 for v in graph.vertices:
1708 v.color_vertex()
1709 if self.add_transports(v, my_vertex, graph, False):
1710 found_failed = True
1712 # No NC replicas for this NC in the site of the local DC,
1713 # so no nTDSConnection objects need be created
1714 if my_vertex.is_white():
1715 return all_connected, found_failed
1717 edge_list, n_components = get_spanning_tree_edges(graph,
1718 self.my_site,
1719 label=part.partstr)
1721 DEBUG_FN("%s Number of components: %d" %
1722 (part.nc_dnstr, n_components))
1723 if n_components > 1:
1724 all_connected = False
1726 # LET partialReplicaOkay be TRUE if and only if
1727 # localSiteVertex.Color = COLOR.BLACK
1728 partial_ok = my_vertex.is_black()
1730 # Utilize the IP transport only for now
1731 transport = self.ip_transport
1733 DEBUG("edge_list %s" % edge_list)
1734 for e in edge_list:
1735 # XXX more accurate comparison?
1736 if e.directed and e.vertices[0].site is self.my_site:
1737 continue
1739 if e.vertices[0].site is self.my_site:
1740 rsite = e.vertices[1].site
1741 else:
1742 rsite = e.vertices[0].site
1744 # We don't make connections to our own site as that
1745 # is intrasite topology generator's job
1746 if rsite is self.my_site:
1747 DEBUG("rsite is my_site")
1748 continue
1750 # Determine bridgehead server in remote site
1751 rbh = self.get_bridgehead(rsite, part, transport,
1752 partial_ok, detect_failed)
1753 if rbh is None:
1754 continue
1756 # RODC acts as an BH for itself
1757 # IF AmIRODC() then
1758 # LET lbh be the nTDSDSA object of the local DC
1759 # ELSE
1760 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1761 # cr, t, partialReplicaOkay, detectFailedDCs)
1762 if self.my_dsa.is_ro():
1763 lsite = self.my_site
1764 lbh = self.my_dsa
1765 else:
1766 lsite = self.my_site
1767 lbh = self.get_bridgehead(lsite, part, transport,
1768 partial_ok, detect_failed)
1769 # TODO
1770 if lbh is None:
1771 debug.DEBUG_RED("DISASTER! lbh is None")
1772 return False, True
1774 debug.DEBUG_CYAN("SITES")
1775 print lsite, rsite
1776 debug.DEBUG_BLUE("vertices")
1777 print e.vertices
1778 debug.DEBUG_BLUE("bridgeheads")
1779 print lbh, rbh
1780 debug.DEBUG_BLUE("-" * 70)
1782 sitelink = e.site_link
1783 if sitelink is None:
1784 link_opt = 0x0
1785 link_sched = None
1786 else:
1787 link_opt = sitelink.options
1788 link_sched = sitelink.schedule
1790 self.create_connection(part, rbh, rsite, transport,
1791 lbh, lsite, link_opt, link_sched,
1792 partial_ok, detect_failed)
1794 return all_connected, found_failed
1796 def create_intersite_connections(self):
1797 """Create NTDSConnections as necessary for all partitions.
1799 Computes an NC replica graph for each NC replica that "should be
1800 present" on the local DC or "is present" on any DC in the same site
1801 as the local DC. For each edge directed to an NC replica on such a
1802 DC from an NC replica on a DC in another site, the KCC creates an
1803 nTDSConnection object to imply that edge if one does not already
1804 exist.
1806 Modifies self.kept_connections - A set of nTDSConnection
1807 objects for edges that are directed
1808 to the local DC's site in one or more NC replica graphs.
1810 :return: True if spanning trees were created for all NC replica
1811 graphs, otherwise False.
1813 all_connected = True
1814 self.kept_connections = set()
1816 # LET crossRefList be the set containing each object o of class
1817 # crossRef such that o is a child of the CN=Partitions child of the
1818 # config NC
1820 # FOR each crossRef object cr in crossRefList
1821 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1822 # is clear in cr!systemFlags, skip cr.
1823 # LET g be the GRAPH return of SetupGraph()
1825 for part in self.part_table.values():
1827 if not part.is_enabled():
1828 continue
1830 if part.is_foreign():
1831 continue
1833 graph = self.setup_graph(part)
1835 # Create nTDSConnection objects, routing replication traffic
1836 # around "failed" DCs.
1837 found_failed = False
1839 connected, found_failed = self.create_connections(graph,
1840 part, True)
1842 DEBUG("with detect_failed: connected %s Found failed %s" %
1843 (connected, found_failed))
1844 if not connected:
1845 all_connected = False
1847 if found_failed:
1848 # One or more failed DCs preclude use of the ideal NC
1849 # replica graph. Add connections for the ideal graph.
1850 self.create_connections(graph, part, False)
1852 return all_connected
1854 def intersite(self, ping):
1855 """The head method for generating the inter-site KCC replica
1856 connection graph and attendant nTDSConnection objects
1857 in the samdb.
1859 Produces self.kept_connections set of NTDS Connections
1860 that should be kept during subsequent pruning process.
1862 ::return (True or False): (True) if the produced NC replica
1863 graph connects all sites that need to be connected
1866 # Retrieve my DSA
1867 mydsa = self.my_dsa
1868 mysite = self.my_site
1869 all_connected = True
1871 DEBUG_FN("intersite(): enter")
1873 # Determine who is the ISTG
1874 if self.readonly:
1875 mysite.select_istg(self.samdb, mydsa, ro=True)
1876 else:
1877 mysite.select_istg(self.samdb, mydsa, ro=False)
1879 # Test whether local site has topology disabled
1880 if mysite.is_intersite_topology_disabled():
1881 DEBUG_FN("intersite(): exit disabled all_connected=%d" %
1882 all_connected)
1883 return all_connected
1885 if not mydsa.is_istg():
1886 DEBUG_FN("intersite(): exit not istg all_connected=%d" %
1887 all_connected)
1888 return all_connected
1890 self.merge_failed_links(ping)
1892 # For each NC with an NC replica that "should be present" on the
1893 # local DC or "is present" on any DC in the same site as the
1894 # local DC, the KCC constructs a site graph--a precursor to an NC
1895 # replica graph. The site connectivity for a site graph is defined
1896 # by objects of class interSiteTransport, siteLink, and
1897 # siteLinkBridge in the config NC.
1899 all_connected = self.create_intersite_connections()
1901 DEBUG_FN("intersite(): exit all_connected=%d" % all_connected)
1902 return all_connected
1904 def update_rodc_connection(self):
1905 """Updates the RODC NTFRS connection object.
1907 If the local DSA is not an RODC, this does nothing.
1909 if not self.my_dsa.is_ro():
1910 return
1912 # Given an nTDSConnection object cn1, such that cn1.options contains
1913 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1914 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1915 # that the following is true:
1917 # cn1.fromServer = cn2.fromServer
1918 # cn1.schedule = cn2.schedule
1920 # If no such cn2 can be found, cn1 is not modified.
1921 # If no such cn1 can be found, nothing is modified by this task.
1923 all_connections = self.my_dsa.connect_table.values()
1924 ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1925 rw_connections = [x for x in all_connections
1926 if x not in ro_connections]
1928 # XXX here we are dealing with multiple RODC_TOPO connections,
1929 # if they exist. It is not clear whether the spec means that
1930 # or if it ever arises.
1931 if rw_connections and ro_connections:
1932 for con in ro_connections:
1933 cn2 = rw_connections[0]
1934 con.from_dnstr = cn2.from_dnstr
1935 con.schedule = cn2.schedule
1936 con.to_be_modified = True
1938 self.my_dsa.commit_connections(self.samdb, ro=self.readonly)
1940 def intrasite_max_node_edges(self, node_count):
1941 """Returns the maximum number of edges directed to a node in
1942 the intrasite replica graph.
1944 The KCC does not create more
1945 than 50 edges directed to a single DC. To optimize replication,
1946 we compute that each node should have n+2 total edges directed
1947 to it such that (n) is the smallest non-negative integer
1948 satisfying (node_count <= 2*(n*n) + 6*n + 7)
1950 (If the number of edges is m (i.e. n + 2), that is the same as
1951 2 * m*m - 2 * m + 3).
1953 edges n nodecount
1954 2 0 7
1955 3 1 15
1956 4 2 27
1957 5 3 43
1959 50 48 4903
1961 :param node_count: total number of nodes in the replica graph
1963 The intention is that there should be no more than 3 hops
1964 between any two DSAs at a site. With up to 7 nodes the 2 edges
1965 of the ring are enough; any configuration of extra edges with
1966 8 nodes will be enough. It is less clear that the 3 hop
1967 guarantee holds at e.g. 15 nodes in degenerate cases, but
1968 those are quite unlikely given the extra edges are randomly
1969 arranged.
1971 n = 0
1972 while True:
1973 if node_count <= (2 * (n * n) + (6 * n) + 7):
1974 break
1975 n = n + 1
1976 n = n + 2
1977 if n < 50:
1978 return n
1979 return 50
1981 def construct_intrasite_graph(self, site_local, dc_local,
1982 nc_x, gc_only, detect_stale):
1983 """Create an intrasite graph using given parameters
1985 This might be called a number of times per site with different
1986 parameters.
1988 Based on [MS-ADTS] 6.2.2.2
1990 :param site_local: site for which we are working
1991 :param dc_local: local DC that potentially needs a replica
1992 :param nc_x: naming context (x) that we are testing if it
1993 "should be present" on the local DC
1994 :param gc_only: Boolean - only consider global catalog servers
1995 :param detect_stale: Boolean - check whether links seems down
1996 :return: None
1998 # We're using the MS notation names here to allow
1999 # correlation back to the published algorithm.
2001 # nc_x - naming context (x) that we are testing if it
2002 # "should be present" on the local DC
2003 # f_of_x - replica (f) found on a DC (s) for NC (x)
2004 # dc_s - DC where f_of_x replica was found
2005 # dc_local - local DC that potentially needs a replica
2006 # (f_of_x)
2007 # r_list - replica list R
2008 # p_of_x - replica (p) is partial and found on a DC (s)
2009 # for NC (x)
2010 # l_of_x - replica (l) is the local replica for NC (x)
2011 # that should appear on the local DC
2012 # r_len = is length of replica list |R|
2014 # If the DSA doesn't need a replica for this
2015 # partition (NC x) then continue
2016 needed, ro, partial = nc_x.should_be_present(dc_local)
2018 debug.DEBUG_YELLOW("construct_intrasite_graph(): enter" +
2019 "\n\tgc_only=%d" % gc_only +
2020 "\n\tdetect_stale=%d" % detect_stale +
2021 "\n\tneeded=%s" % needed +
2022 "\n\tro=%s" % ro +
2023 "\n\tpartial=%s" % partial +
2024 "\n%s" % nc_x)
2026 if not needed:
2027 debug.DEBUG_RED("%s lacks 'should be present' status, "
2028 "aborting construct_intersite_graph!" %
2029 nc_x.nc_dnstr)
2030 return
2032 # Create a NCReplica that matches what the local replica
2033 # should say. We'll use this below in our r_list
2034 l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
2035 nc_x.nc_dnstr)
2037 l_of_x.identify_by_basedn(self.samdb)
2039 l_of_x.rep_partial = partial
2040 l_of_x.rep_ro = ro
2042 # Add this replica that "should be present" to the
2043 # needed replica table for this DSA
2044 dc_local.add_needed_replica(l_of_x)
2046 # Replica list
2048 # Let R be a sequence containing each writable replica f of x
2049 # such that f "is present" on a DC s satisfying the following
2050 # criteria:
2052 # * s is a writable DC other than the local DC.
2054 # * s is in the same site as the local DC.
2056 # * If x is a read-only full replica and x is a domain NC,
2057 # then the DC's functional level is at least
2058 # DS_BEHAVIOR_WIN2008.
2060 # * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
2061 # in the options attribute of the site settings object for
2062 # the local DC's site, or no tuple z exists in the
2063 # kCCFailedLinks or kCCFailedConnections variables such
2064 # that z.UUIDDsa is the objectGUID of the nTDSDSA object
2065 # for s, z.FailureCount > 0, and the current time -
2066 # z.TimeFirstFailure > 2 hours.
2068 r_list = []
2070 # We'll loop thru all the DSAs looking for
2071 # writeable NC replicas that match the naming
2072 # context dn for (nc_x)
2074 for dc_s in self.my_site.dsa_table.values():
2075 # If this partition (nc_x) doesn't appear as a
2076 # replica (f_of_x) on (dc_s) then continue
2077 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2078 continue
2080 # Pull out the NCReplica (f) of (x) with the dn
2081 # that matches NC (x) we are examining.
2082 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2084 # Replica (f) of NC (x) must be writable
2085 if f_of_x.is_ro():
2086 continue
2088 # Replica (f) of NC (x) must satisfy the
2089 # "is present" criteria for DC (s) that
2090 # it was found on
2091 if not f_of_x.is_present():
2092 continue
2094 # DC (s) must be a writable DSA other than
2095 # my local DC. In other words we'd only replicate
2096 # from other writable DC
2097 if dc_s.is_ro() or dc_s is dc_local:
2098 continue
2100 # Certain replica graphs are produced only
2101 # for global catalogs, so test against
2102 # method input parameter
2103 if gc_only and not dc_s.is_gc():
2104 continue
2106 # DC (s) must be in the same site as the local DC
2107 # as this is the intra-site algorithm. This is
2108 # handled by virtue of placing DSAs in per
2109 # site objects (see enclosing for() loop)
2111 # If NC (x) is intended to be read-only full replica
2112 # for a domain NC on the target DC then the source
2113 # DC should have functional level at minimum WIN2008
2115 # Effectively we're saying that in order to replicate
2116 # to a targeted RODC (which was introduced in Windows 2008)
2117 # then we have to replicate from a DC that is also minimally
2118 # at that level.
2120 # You can also see this requirement in the MS special
2121 # considerations for RODC which state that to deploy
2122 # an RODC, at least one writable domain controller in
2123 # the domain must be running Windows Server 2008
2124 if ro and not partial and nc_x.nc_type == NCType.domain:
2125 if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2126 continue
2128 # If we haven't been told to turn off stale connection
2129 # detection and this dsa has a stale connection then
2130 # continue
2131 if detect_stale and self.is_stale_link_connection(dc_s):
2132 continue
2134 # Replica meets criteria. Add it to table indexed
2135 # by the GUID of the DC that it appears on
2136 r_list.append(f_of_x)
2138 # If a partial (not full) replica of NC (x) "should be present"
2139 # on the local DC, append to R each partial replica (p of x)
2140 # such that p "is present" on a DC satisfying the same
2141 # criteria defined above for full replica DCs.
2143 # XXX This loop and the previous one differ only in whether
2144 # the replica is partial or not. here we only accept partial
2145 # (because we're partial); before we only accepted full. Order
2146 # doen't matter (the list is sorted a few lines down) so these
2147 # loops could easily be merged. Or this could be a helper
2148 # function.
2150 if partial:
2151 # Now we loop thru all the DSAs looking for
2152 # partial NC replicas that match the naming
2153 # context dn for (NC x)
2154 for dc_s in self.my_site.dsa_table.values():
2156 # If this partition NC (x) doesn't appear as a
2157 # replica (p) of NC (x) on the dsa DC (s) then
2158 # continue
2159 if not nc_x.nc_dnstr in dc_s.current_rep_table:
2160 continue
2162 # Pull out the NCReplica with the dn that
2163 # matches NC (x) we are examining.
2164 p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2166 # Replica (p) of NC (x) must be partial
2167 if not p_of_x.is_partial():
2168 continue
2170 # Replica (p) of NC (x) must satisfy the
2171 # "is present" criteria for DC (s) that
2172 # it was found on
2173 if not p_of_x.is_present():
2174 continue
2176 # DC (s) must be a writable DSA other than
2177 # my DSA. In other words we'd only replicate
2178 # from other writable DSA
2179 if dc_s.is_ro() or dc_s is dc_local:
2180 continue
2182 # Certain replica graphs are produced only
2183 # for global catalogs, so test against
2184 # method input parameter
2185 if gc_only and not dc_s.is_gc():
2186 continue
2188 # If we haven't been told to turn off stale connection
2189 # detection and this dsa has a stale connection then
2190 # continue
2191 if detect_stale and self.is_stale_link_connection(dc_s):
2192 continue
2194 # Replica meets criteria. Add it to table indexed
2195 # by the GUID of the DSA that it appears on
2196 r_list.append(p_of_x)
2198 # Append to R the NC replica that "should be present"
2199 # on the local DC
2200 r_list.append(l_of_x)
2202 r_list.sort(sort_replica_by_dsa_guid)
2203 r_len = len(r_list)
2205 max_node_edges = self.intrasite_max_node_edges(r_len)
2207 # Add a node for each r_list element to the replica graph
2208 graph_list = []
2209 for rep in r_list:
2210 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2211 graph_list.append(node)
2213 # For each r(i) from (0 <= i < |R|-1)
2214 i = 0
2215 while i < (r_len-1):
2216 # Add an edge from r(i) to r(i+1) if r(i) is a full
2217 # replica or r(i+1) is a partial replica
2218 if not r_list[i].is_partial() or r_list[i+1].is_partial():
2219 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
2221 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2222 # replica or ri is a partial replica.
2223 if not r_list[i+1].is_partial() or r_list[i].is_partial():
2224 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
2225 i = i + 1
2227 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2228 # or r0 is a partial replica.
2229 if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
2230 graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
2232 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2233 # r|R|-1 is a partial replica.
2234 if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
2235 graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
2237 DEBUG("r_list is length %s" % len(r_list))
2238 DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2239 for x in r_list))
2241 do_dot_files = self.dot_files and self.debug
2242 if self.verify or do_dot_files:
2243 dot_edges = []
2244 dot_vertices = set()
2245 for v1 in graph_list:
2246 dot_vertices.add(v1.dsa_dnstr)
2247 for v2 in v1.edge_from:
2248 dot_edges.append((v2, v1.dsa_dnstr))
2249 dot_vertices.add(v2)
2251 verify_properties = ('connected', 'directed_double_ring_or_small')
2252 verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2253 label='%s__%s__%s' % (site_local.site_dnstr,
2254 nctype_lut[nc_x.nc_type],
2255 nc_x.nc_dnstr),
2256 properties=verify_properties, debug=DEBUG,
2257 verify=self.verify,
2258 dot_files=do_dot_files, directed=True)
2260 # For each existing nTDSConnection object implying an edge
2261 # from rj of R to ri such that j != i, an edge from rj to ri
2262 # is not already in the graph, and the total edges directed
2263 # to ri is less than n+2, the KCC adds that edge to the graph.
2264 for vertex in graph_list:
2265 dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2266 for connect in dsa.connect_table.values():
2267 remote = connect.from_dnstr
2268 if remote in self.my_site.dsa_table:
2269 vertex.add_edge_from(remote)
2271 DEBUG('reps are: %s' % ' '.join(x.rep_dsa_dnstr for x in r_list))
2272 DEBUG('dsas are: %s' % ' '.join(x.dsa_dnstr for x in graph_list))
2274 for tnode in graph_list:
2275 # To optimize replication latency in sites with many NC
2276 # replicas, the KCC adds new edges directed to ri to bring
2277 # the total edges to n+2, where the NC replica rk of R
2278 # from which the edge is directed is chosen at random such
2279 # that k != i and an edge from rk to ri is not already in
2280 # the graph.
2282 # Note that the KCC tech ref does not give a number for
2283 # the definition of "sites with many NC replicas". At a
2284 # bare minimum to satisfy n+2 edges directed at a node we
2285 # have to have at least three replicas in |R| (i.e. if n
2286 # is zero then at least replicas from two other graph
2287 # nodes may direct edges to us).
2288 if r_len >= 3 and not tnode.has_sufficient_edges():
2289 candidates = [x for x in graph_list if
2290 (x is not tnode and
2291 x.dsa_dnstr not in tnode.edge_from)]
2293 debug.DEBUG_BLUE("looking for random link for %s. r_len %d, "
2294 "graph len %d candidates %d"
2295 % (tnode.dsa_dnstr, r_len, len(graph_list),
2296 len(candidates)))
2298 DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2300 while candidates and not tnode.has_sufficient_edges():
2301 other = random.choice(candidates)
2302 DEBUG("trying to add candidate %s" % other.dsa_dstr)
2303 if not tnode.add_edge_from(other):
2304 debug.DEBUG_RED("could not add %s" % other.dsa_dstr)
2305 candidates.remove(other)
2306 else:
2307 DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2308 (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2309 tnode.max_edges))
2311 # Print the graph node in debug mode
2312 DEBUG_FN("%s" % tnode)
2314 # For each edge directed to the local DC, ensure a nTDSConnection
2315 # points to us that satisfies the KCC criteria
2317 if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2318 tnode.add_connections_from_edges(dc_local)
2320 if self.verify or do_dot_files:
2321 dot_edges = []
2322 dot_vertices = set()
2323 for v1 in graph_list:
2324 dot_vertices.add(v1.dsa_dnstr)
2325 for v2 in v1.edge_from:
2326 dot_edges.append((v2, v1.dsa_dnstr))
2327 dot_vertices.add(v2)
2329 verify_properties = ('connected', 'directed_double_ring_or_small')
2330 verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2331 label='%s__%s__%s' % (site_local.site_dnstr,
2332 nctype_lut[nc_x.nc_type],
2333 nc_x.nc_dnstr),
2334 properties=verify_properties, debug=DEBUG,
2335 verify=self.verify,
2336 dot_files=do_dot_files, directed=True)
2338 def intrasite(self):
2339 """The head method for generating the intra-site KCC replica
2340 connection graph and attendant nTDSConnection objects
2341 in the samdb
2343 # Retrieve my DSA
2344 mydsa = self.my_dsa
2346 DEBUG_FN("intrasite(): enter")
2348 # Test whether local site has topology disabled
2349 mysite = self.my_site
2350 if mysite.is_intrasite_topology_disabled():
2351 return
2353 detect_stale = (not mysite.is_detect_stale_disabled())
2354 for connect in mydsa.connect_table.values():
2355 if connect.to_be_added:
2356 debug.DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2358 # Loop thru all the partitions, with gc_only False
2359 for partdn, part in self.part_table.items():
2360 self.construct_intrasite_graph(mysite, mydsa, part, False,
2361 detect_stale)
2362 for connect in mydsa.connect_table.values():
2363 if connect.to_be_added:
2364 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2366 # If the DC is a GC server, the KCC constructs an additional NC
2367 # replica graph (and creates nTDSConnection objects) for the
2368 # config NC as above, except that only NC replicas that "are present"
2369 # on GC servers are added to R.
2370 for connect in mydsa.connect_table.values():
2371 if connect.to_be_added:
2372 debug.DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2374 # Do it again, with gc_only True
2375 for partdn, part in self.part_table.items():
2376 if part.is_config():
2377 self.construct_intrasite_graph(mysite, mydsa, part, True,
2378 detect_stale)
2380 # The DC repeats the NC replica graph computation and nTDSConnection
2381 # creation for each of the NC replica graphs, this time assuming
2382 # that no DC has failed. It does so by re-executing the steps as
2383 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2384 # set in the options attribute of the site settings object for
2385 # the local DC's site. (ie. we set "detec_stale" flag to False)
2386 for connect in mydsa.connect_table.values():
2387 if connect.to_be_added:
2388 debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2390 # Loop thru all the partitions.
2391 for partdn, part in self.part_table.items():
2392 self.construct_intrasite_graph(mysite, mydsa, part, False,
2393 False) # don't detect stale
2395 # If the DC is a GC server, the KCC constructs an additional NC
2396 # replica graph (and creates nTDSConnection objects) for the
2397 # config NC as above, except that only NC replicas that "are present"
2398 # on GC servers are added to R.
2399 for connect in mydsa.connect_table.values():
2400 if connect.to_be_added:
2401 debug.DEBUG_RED("TO BE ADDED:\n%s" % connect)
2403 for partdn, part in self.part_table.items():
2404 if part.is_config():
2405 self.construct_intrasite_graph(mysite, mydsa, part, True,
2406 False) # don't detect stale
2408 if self.readonly:
2409 # Display any to be added or modified repsFrom
2410 for connect in mydsa.connect_table.values():
2411 if connect.to_be_deleted:
2412 logger.info("TO BE DELETED:\n%s" % connect)
2413 if connect.to_be_modified:
2414 logger.info("TO BE MODIFIED:\n%s" % connect)
2415 if connect.to_be_added:
2416 debug.DEBUG_GREEN("TO BE ADDED:\n%s" % connect)
2418 mydsa.commit_connections(self.samdb, ro=True)
2419 else:
2420 # Commit any newly created connections to the samdb
2421 mydsa.commit_connections(self.samdb)
2423 def list_dsas(self):
2424 """Compile a comprehensive list of DSA DNs
2426 These are all the DSAs on all the sites that KCC would be
2427 dealing with.
2429 This method is not idempotent and may not work correctly in
2430 sequence with KCC.run().
2432 :return: a list of DSA DN strings.
2434 self.load_my_site()
2435 self.load_my_dsa()
2437 self.load_all_sites()
2438 self.load_all_partitions()
2439 self.load_all_transports()
2440 self.load_all_sitelinks()
2441 dsas = []
2442 for site in self.site_table.values():
2443 dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2444 for dsa in site.dsa_table.values()])
2445 return dsas
2447 def load_samdb(self, dburl, lp, creds):
2448 """Load the database using an url, loadparm, and credentials
2450 :param dburl: a database url.
2451 :param lp: a loadparm object.
2452 :param creds: a Credentials object.
2454 self.samdb = SamDB(url=dburl,
2455 session_info=system_session(),
2456 credentials=creds, lp=lp)
2458 def plot_all_connections(self, basename, verify_properties=()):
2459 verify = verify_properties and self.verify
2460 plot = self.dot_files
2461 if not (verify or plot):
2462 return
2464 dot_edges = []
2465 dot_vertices = []
2466 edge_colours = []
2467 vertex_colours = []
2469 for dsa in self.dsa_by_dnstr.values():
2470 dot_vertices.append(dsa.dsa_dnstr)
2471 if dsa.is_ro():
2472 vertex_colours.append('#cc0000')
2473 else:
2474 vertex_colours.append('#0000cc')
2475 for con in dsa.connect_table.values():
2476 if con.is_rodc_topology():
2477 edge_colours.append('red')
2478 else:
2479 edge_colours.append('blue')
2480 dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2482 verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2483 label=self.my_dsa_dnstr, properties=verify_properties,
2484 debug=DEBUG, verify=verify, dot_files=plot,
2485 directed=True, edge_colors=edge_colours,
2486 vertex_colors=vertex_colours)
2488 def run(self, dburl, lp, creds, forced_local_dsa=None,
2489 forget_local_links=False, forget_intersite_links=False,
2490 attempt_live_connections=False):
2491 """Method to perform a complete run of the KCC and
2492 produce an updated topology for subsequent NC replica
2493 syncronization between domain controllers
2495 # We may already have a samdb setup if we are
2496 # currently importing an ldif for a test run
2497 if self.samdb is None:
2498 try:
2499 self.load_samdb(dburl, lp, creds)
2500 except ldb.LdbError, (num, msg):
2501 logger.error("Unable to open sam database %s : %s" %
2502 (dburl, msg))
2503 return 1
2505 if forced_local_dsa:
2506 self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2507 forced_local_dsa)
2509 try:
2510 # Setup
2511 self.load_my_site()
2512 self.load_my_dsa()
2514 self.load_all_sites()
2515 self.load_all_partitions()
2516 self.load_all_transports()
2517 self.load_all_sitelinks()
2519 if self.verify or self.dot_files:
2520 guid_to_dnstr = {}
2521 for site in self.site_table.values():
2522 guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2523 for dnstr, dsa
2524 in site.dsa_table.items())
2526 self.plot_all_connections('dsa_initial')
2528 dot_edges = []
2529 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2530 for dnstr, c_rep in current_reps.items():
2531 DEBUG("c_rep %s" % c_rep)
2532 dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2534 verify_and_dot('dsa_repsFrom_initial', dot_edges,
2535 directed=True, label=self.my_dsa_dnstr,
2536 properties=(), debug=DEBUG, verify=self.verify,
2537 dot_files=self.dot_files)
2539 dot_edges = []
2540 for site in self.site_table.values():
2541 for dsa in site.dsa_table.values():
2542 current_reps, needed_reps = dsa.get_rep_tables()
2543 for dn_str, rep in current_reps.items():
2544 for reps_from in rep.rep_repsFrom:
2545 DEBUG("rep %s" % rep)
2546 dsa_guid = str(reps_from.source_dsa_obj_guid)
2547 dsa_dn = guid_to_dnstr[dsa_guid]
2548 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2550 verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2551 directed=True, label=self.my_dsa_dnstr,
2552 properties=(), debug=DEBUG, verify=self.verify,
2553 dot_files=self.dot_files)
2555 dot_edges = []
2556 for link in self.sitelink_table.values():
2557 for a, b in itertools.combinations(link.site_list, 2):
2558 dot_edges.append((str(a), str(b)))
2559 properties = ('connected',)
2560 verify_and_dot('dsa_sitelink_initial', dot_edges,
2561 directed=False,
2562 label=self.my_dsa_dnstr, properties=properties,
2563 debug=DEBUG, verify=self.verify,
2564 dot_files=self.dot_files)
2566 if forget_local_links:
2567 for dsa in self.my_site.dsa_table.values():
2568 dsa.connect_table = {k: v for k, v in
2569 dsa.connect_table.items()
2570 if v.is_rodc_topology()}
2571 self.plot_all_connections('dsa_forgotten_local')
2573 if forget_intersite_links:
2574 for site in self.site_table.values():
2575 for dsa in site.dsa_table.values():
2576 dsa.connect_table = {k: v for k, v in
2577 dsa.connect_table.items()
2578 if site is self.my_site and
2579 v.is_rodc_topology()}
2581 self.plot_all_connections('dsa_forgotten_all')
2583 if attempt_live_connections:
2584 # Encapsulates lp and creds in a function that
2585 # attempts connections to remote DSAs.
2586 def ping(self, dnsname):
2587 try:
2588 drs_utils.drsuapi_connect(dnsname, self.lp, self.creds)
2589 except drs_utils.drsException:
2590 return False
2591 return True
2592 else:
2593 ping = None
2594 # These are the published steps (in order) for the
2595 # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2597 # Step 1
2598 self.refresh_failed_links_connections(ping)
2600 # Step 2
2601 self.intrasite()
2603 # Step 3
2604 all_connected = self.intersite(ping)
2606 # Step 4
2607 self.remove_unneeded_ntdsconn(all_connected)
2609 # Step 5
2610 self.translate_ntdsconn()
2612 # Step 6
2613 self.remove_unneeded_failed_links_connections()
2615 # Step 7
2616 self.update_rodc_connection()
2618 if self.verify or self.dot_files:
2619 self.plot_all_connections('dsa_final',
2620 ('connected', 'forest_of_rings'))
2622 debug.DEBUG_MAGENTA("there are %d dsa guids" %
2623 len(guid_to_dnstr))
2625 dot_edges = []
2626 edge_colors = []
2627 my_dnstr = self.my_dsa.dsa_dnstr
2628 current_reps, needed_reps = self.my_dsa.get_rep_tables()
2629 for dnstr, n_rep in needed_reps.items():
2630 for reps_from in n_rep.rep_repsFrom:
2631 guid_str = str(reps_from.source_dsa_obj_guid)
2632 dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2633 edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2635 verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True,
2636 label=self.my_dsa_dnstr,
2637 properties=(), debug=DEBUG, verify=self.verify,
2638 dot_files=self.dot_files,
2639 edge_colors=edge_colors)
2641 dot_edges = []
2643 for site in self.site_table.values():
2644 for dsa in site.dsa_table.values():
2645 current_reps, needed_reps = dsa.get_rep_tables()
2646 for n_rep in needed_reps.values():
2647 for reps_from in n_rep.rep_repsFrom:
2648 dsa_guid = str(reps_from.source_dsa_obj_guid)
2649 dsa_dn = guid_to_dnstr[dsa_guid]
2650 dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2652 verify_and_dot('dsa_repsFrom_final_all', dot_edges,
2653 directed=True, label=self.my_dsa_dnstr,
2654 properties=(), debug=DEBUG, verify=self.verify,
2655 dot_files=self.dot_files)
2657 except:
2658 raise
2660 return 0
2662 def import_ldif(self, dburl, lp, creds, ldif_file):
2663 """Import all objects and attributes that are relevent
2664 to the KCC algorithms from a previously exported LDIF file.
2666 The point of this function is to allow a programmer/debugger to
2667 import an LDIF file with non-security relevent information that
2668 was previously extracted from a DC database. The LDIF file is used
2669 to create a temporary abbreviated database. The KCC algorithm can
2670 then run against this abbreviated database for debug or test
2671 verification that the topology generated is computationally the
2672 same between different OSes and algorithms.
2674 :param dburl: path to the temporary abbreviated db to create
2675 :param ldif_file: path to the ldif file to import
2677 try:
2678 self.samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif_file,
2679 self.forced_local_dsa)
2680 except ldif_import_export.LdifError, e:
2681 print e
2682 return 1
2683 return 0
2685 def export_ldif(self, dburl, lp, creds, ldif_file):
2686 """Routine to extract all objects and attributes that are relevent
2687 to the KCC algorithms from a DC database.
2689 The point of this function is to allow a programmer/debugger to
2690 extract an LDIF file with non-security relevent information from
2691 a DC database. The LDIF file can then be used to "import" via
2692 the import_ldif() function this file into a temporary abbreviated
2693 database. The KCC algorithm can then run against this abbreviated
2694 database for debug or test verification that the topology generated
2695 is computationally the same between different OSes and algorithms.
2697 :param dburl: LDAP database URL to extract info from
2698 :param ldif_file: output LDIF file name to create
2700 try:
2701 ldif_import_export.samdb_to_ldif_file(self.samdb, dburl, lp, creds,
2702 ldif_file)
2703 except ldif_import_export.LdifError, e:
2704 print e
2705 return 1
2706 return 0